Reject normalized datetime-local inputs

createFromFormat() only returns false on hard parse failures.
Impossible dates such as 2026-02-31 were silently normalized
into March, so a confirmed event could land on a different day
than the booking link supplied. Non-padded values like
2026-2-3T10:00 were also accepted and reformatted.

Parse with a leading "!" so no current-date fields leak in,
require zero warnings and errors from getLastErrors(), and
round-trip the value through the format. Impossible dates,
out-of-range components and non-padded values now throw in
both the confirmation and the creation stage.
This commit is contained in:
stefan.tollkuehn committed 2026-10-26 13:39:07 +01:00
1 parent a9b7fd11af
commit 2b3e96e50a
2 files changed
+49 -11

No files matched your search

+20 -11
View File
@@ -844,29 +844,38 @@ class CalendarController extends Controller
{ {
$timeZone = $this->dateTimeZone->getTimeZone(); $timeZone = $this->dateTimeZone->getTimeZone();
/*
* "!" resets all fields, so no parts of the current date
* leak in. PHP normalizes impossible dates instead of
* failing (2026-02-31 rolls into March), so require zero
* warnings and an exact format round-trip on top of the
* parse result.
*/
foreach (['!' . self::DATETIME_LOCAL, '!Y-m-d\TH:i:s'] as $format) {
$date = \DateTimeImmutable::createFromFormat( $date = \DateTimeImmutable::createFromFormat(
self::DATETIME_LOCAL, $format,
$value, $value,
$timeZone $timeZone
); );
if ($date === false) { $errors = \DateTimeImmutable::getLastErrors();
$date = \DateTimeImmutable::createFromFormat(
'Y-m-d\TH:i:s', if (
$value, $date !== false
$timeZone && ($errors === false
); || ($errors['warning_count'] === 0
&& $errors['error_count'] === 0))
&& $date->format(substr($format, 1)) === $value
) {
return $date;
}
} }
if ($date === false) {
throw new \InvalidArgumentException( throw new \InvalidArgumentException(
$this->l10n->t('Invalid date/time.') $this->l10n->t('Invalid date/time.')
); );
} }
return $date;
}
/** /**
* Reject invalid UTF-8 and control characters. Newlines * Reject invalid UTF-8 and control characters. Newlines
* (\n, \r) are only allowed where multi-line text is * (\n, \r) are only allowed where multi-line text is
+29
View File
@@ -218,6 +218,35 @@ class CalendarControllerTest extends TestCase
$this->invoke($c, 'parseLocalDateTime', ['tomorrow']); $this->invoke($c, 'parseLocalDateTime', ['tomorrow']);
} }
public function testParseLocalDateTimeAcceptsLeapDay(): void
{
$c = $this->newController();
$date = $this->invoke($c, 'parseLocalDateTime', ['2028-02-29T10:00']);
$this->assertSame('2028-02-29 10:00:00 UTC', $date->format('Y-m-d H:i:s T'));
}
public static function provideInvalidLocalDateTimes(): iterable
{
return [
'impossible day rolls into next month' => ['2026-02-31T10:00'],
'leap day in a non-leap year' => ['2027-02-29T10:00'],
'month out of range' => ['2026-13-01T10:00'],
'day out of range' => ['2026-00-15T10:00'],
'hour out of range' => ['2026-02-03T25:00'],
'non-padded components' => ['2026-2-3T10:00'],
];
}
#[DataProvider('provideInvalidLocalDateTimes')]
public function testParseLocalDateTimeRejectsInvalidValues(string $value): void
{
$c = $this->newController();
$this->expectException(\InvalidArgumentException::class);
$this->invoke($c, 'parseLocalDateTime', [$value]);
}
/* /*
* validateEventData * validateEventData
*/ */