fix: scope csrf fields and block past dates

This commit is contained in:
Marek Miklewicz
2026-06-02 21:54:56 +02:00
parent 56709817b5
commit 767016c659
8 changed files with 101 additions and 11 deletions
+21 -5
View File
@@ -9,8 +9,8 @@ function render_rule_form(AppContext $ctx, string $mode, ?array $rule): string
$replyFrequency = selected_reply_frequency($rule);
$body = $rule['body'] ?? '';
$enabled = $rule === null || !empty($rule['enabled']);
$startValue = (string)($rule['start_value'] ?? '2026-06-10T09:00');
$endValue = (string)($rule['end_value'] ?? '2026-06-24T17:00');
$startValue = (string)($rule['start_value'] ?? default_schedule_value($ctx, true));
$endValue = (string)($rule['end_value'] ?? default_schedule_value($ctx, false));
$action = $ctx->url($isUpdate ? 'update.html' : 'create.html');
$intent = $isUpdate ? 'update:' . $id : 'create';
$csrf = $ctx->csrfField($intent);
@@ -27,6 +27,16 @@ function render_rule_form(AppContext $ctx, string $mode, ?array $rule): string
'</form>';
}
function default_schedule_value(AppContext $ctx, bool $start): string
{
$today = new DateTimeImmutable('today', new DateTimeZone($ctx->settings->timezone()));
$date = $start ? $today : $today->modify('+14 days');
if ($ctx->settings->scheduleMode() === 'directadmin_period') {
return $date->format('Y-m-d') . '|' . ($start ? 'morning' : 'evening');
}
return $date->format('Y-m-d') . 'T' . ($start ? '09:00' : '17:00');
}
/** @param array<string,mixed>|null $rule */
function selected_reply_frequency(?array $rule): string
{
@@ -119,7 +129,8 @@ function render_calendar_picker(AppContext $ctx, string $name, string $title, st
}
$selectedDt = DateTimeImmutable::createFromFormat('!Y-m-d', $selectedDate) ?: new DateTimeImmutable('today');
$monthStart = $selectedDt->modify('first day of this month');
$days = render_calendar_days($name, $selectedDate, $monthStart);
$minDate = (new DateTimeImmutable('today', new DateTimeZone($ctx->settings->timezone())))->format('Y-m-d');
$days = render_calendar_days($name, $selectedDate, $monthStart, $minDate);
$visibleMonth = $monthStart->format('Y-m');
$monthLabel = calendar_month_label($ctx, $monthStart);
if ($mode === 'exact_hour') {
@@ -131,7 +142,7 @@ function render_calendar_picker(AppContext $ctx, string $name, string $title, st
'<option value="evening"' . ($selectedPeriod === 'evening' ? ' selected' : '') . '>' . AppContext::e($ctx->t('Evening')) . '</option>' .
'</select>';
}
return '<div class="panel"><h3>' . AppContext::e($title) . '</h3><div class="br-restore-layout"><div class="br-restore-calendar" data-calendar-picker data-schedule-mode="' . AppContext::e($mode) . '" data-calendar-name="' . AppContext::e($name) . '" data-visible-month="' . AppContext::e($visibleMonth) . '" data-selected-date="' . AppContext::e($selectedDate) . '"><h4>' . AppContext::e($title) . '</h4><div class="br-month-nav"><button type="button" class="br-month-btn" data-calendar-prev>&lsaquo;</button><span class="br-month-label" data-calendar-month-label>' . AppContext::e($monthLabel) . '</span><button type="button" class="br-month-btn" data-calendar-next>&rsaquo;</button></div>' .
return '<div class="panel"><h3>' . AppContext::e($title) . '</h3><div class="br-restore-layout"><div class="br-restore-calendar" data-calendar-picker data-schedule-mode="' . AppContext::e($mode) . '" data-calendar-name="' . AppContext::e($name) . '" data-visible-month="' . AppContext::e($visibleMonth) . '" data-selected-date="' . AppContext::e($selectedDate) . '" data-min-date="' . AppContext::e($minDate) . '"><h4>' . AppContext::e($title) . '</h4><div class="br-month-nav"><button type="button" class="br-month-btn" data-calendar-prev>&lsaquo;</button><span class="br-month-label" data-calendar-month-label>' . AppContext::e($monthLabel) . '</span><button type="button" class="br-month-btn" data-calendar-next>&rsaquo;</button></div>' .
'<table class="br-calendar-grid"><thead><tr>' . render_weekday_headers($ctx) . '</tr></thead><tbody data-calendar-days>' . $days . '</tbody></table>' .
'<div class="br-date-label" id="' . AppContext::e($name . '_label') . '">' . AppContext::e($ctx->t('Selected')) . ': ' . AppContext::e($selectedDate) . '</div><input type="hidden" id="' . AppContext::e($name . '_date') . '" name="' . AppContext::e($name . '_date') . '" value="' . AppContext::e($selectedDate) . '"></div>' .
'<div class="br-restore-calendar"><h4>' . AppContext::e($ctx->t('Time')) . '</h4>' . $timeControl . '</div></div>' .
@@ -153,7 +164,7 @@ function render_weekday_headers(AppContext $ctx): string
return $html;
}
function render_calendar_days(string $name, string $selectedDate, DateTimeImmutable $monthStart): string
function render_calendar_days(string $name, string $selectedDate, DateTimeImmutable $monthStart, string $minDate): string
{
$offset = ((int)$monthStart->format('N')) - 1;
$daysInMonth = (int)$monthStart->format('t');
@@ -167,6 +178,11 @@ function render_calendar_days(string $name, string $selectedDate, DateTimeImmuta
continue;
}
$date = $monthStart->setDate((int)$monthStart->format('Y'), (int)$monthStart->format('m'), $day)->format('Y-m-d');
if ($date < $minDate) {
$html .= '<td><span class="br-cal-day-disabled">' . $day . '</span></td>';
$day++;
continue;
}
$class = $date === $selectedDate ? ' is-selected' : '';
$html .= '<td><button type="button" class="br-cal-day' . $class . '" data-date-target="' . AppContext::e($name . '_date') . '" data-date-label="' . AppContext::e($name . '_label') . '" data-date-value="' . AppContext::e($date) . '">' . $day . '</button></td>';
$day++;
+10 -4
View File
@@ -58,14 +58,14 @@ final class AppContext
public function csrfField(string $intent): string
{
$issued = $this->csrf->issue($intent);
return '<input type="hidden" name="csrf_ts" value="' . self::e((string)$issued['ts']) . '">' .
'<input type="hidden" name="csrf_sid" value="' . self::e($issued['sid']) . '">' .
'<input type="hidden" name="csrf_token" value="' . self::e($issued['token']) . '">';
return '<input type="hidden" name="gar_csrf_ts" value="' . self::e((string)$issued['ts']) . '">' .
'<input type="hidden" name="gar_csrf_sid" value="' . self::e($issued['sid']) . '">' .
'<input type="hidden" name="gar_csrf_token" value="' . self::e($issued['token']) . '">';
}
public function requireCsrf(string $intent): void
{
if (!$this->csrf->validate($intent, $this->post('csrf_ts'), $this->post('csrf_token'), 3600, $this->post('csrf_sid'))) {
if (!$this->csrf->validate($intent, $this->post('gar_csrf_ts'), $this->post('gar_csrf_token'), 3600, $this->post('gar_csrf_sid'))) {
throw new RuntimeException('Nieprawidłowy lub wygasły token CSRF.');
}
}
@@ -158,6 +158,7 @@ final class AppContext
}
function renderMonth(calendar, year, month) {
var selected = calendar.getAttribute('data-selected-date') || '';
var minDate = calendar.getAttribute('data-min-date') || '';
var tbody = calendar.querySelector('[data-calendar-days]');
var label = calendar.querySelector('[data-calendar-month-label]');
if (!tbody || !label) { return; }
@@ -174,6 +175,11 @@ final class AppContext
continue;
}
var value = year + '-' + pad(month) + '-' + pad(day);
if (minDate && value < minDate) {
html += '<td><span class="br-cal-day-disabled">' + day + '</span></td>';
day++;
continue;
}
var cls = value === selected ? ' is-selected' : '';
html += '<td><button type="button" class="br-cal-day' + cls + '" data-date-target="' + calendar.getAttribute('data-calendar-name') + '_date" data-date-label="' + calendar.getAttribute('data-calendar-name') + '_label" data-date-value="' + value + '">' + day + '</button></td>';
day++;
+3
View File
@@ -39,6 +39,9 @@ final class RuleValidator
$tz = new DateTimeZone($settings->timezone());
[$startTs, $startValue] = self::parseScheduleValue((string)($input['start_value'] ?? ''), $mode, $tz, true);
[$endTs, $endValue] = self::parseScheduleValue((string)($input['end_value'] ?? ''), $mode, $tz, false);
if (substr($startValue, 0, 10) < (new DateTimeImmutable('today', $tz))->format('Y-m-d')) {
throw new InvalidArgumentException('Data rozpoczęcia nie może być wcześniejsza niż aktualna data.');
}
if ($endTs <= $startTs) {
throw new InvalidArgumentException('Data zakończenia musi być późniejsza niż data rozpoczęcia.');
}