bug #34904 [Validator][ConstraintValidator] Safe fail on invalid timezones (fancyweb)

This PR was merged into the 3.4 branch.

Discussion
----------

[Validator][ConstraintValidator] Safe fail on invalid timezones

Co-authored-by: Scott Dawson <scott@loyaltycorp.com.au>

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | https://github.com/symfony/symfony/issues/33901
| License       | MIT
| Doc PR        |

Alternative to https://github.com/symfony/symfony/pull/33902.

I will explain why I think it is better this way:

1. We set the timezone with the setter because it's 100% safe, it never fails. It fall backs to the default timezone if the provided timezone is not supported (as if we passed null, so the same behavior that always existed). We are therefore compatible with all edge cases.
2. We don't validate the timezone with `\DateTimeZone::listIdentifiers()`. It only returns full identifiers like "Europe/Paris" but it doesn't take into account "numeric" identifiers such as "+08:00" which are perfectly valid. I added a test case to ensure we stay valid with this case. + some invalid identifiers for the native `\IntlDateFormatter` are valid with the polyfill that uses `\DateTimeZone` (eg : `X`). I don't think we can validate anything safely that will work reliably on both implementations.

Commits
-------

3b1b994cb3 [Validator][ConstraintValidator] Safe fail on invalid timezones
This commit is contained in:
Nicolas Grekas 2019-12-13 13:34:40 +01:00
commit e04331657f
2 changed files with 15 additions and 11 deletions

View File

@ -87,16 +87,12 @@ abstract class ConstraintValidator implements ConstraintValidatorInterface
{
if (($format & self::PRETTY_DATE) && $value instanceof \DateTimeInterface) {
if (class_exists('IntlDateFormatter')) {
$locale = \Locale::getDefault();
$formatter = new \IntlDateFormatter($locale, \IntlDateFormatter::MEDIUM, \IntlDateFormatter::SHORT, $value->getTimezone());
$formatter = new \IntlDateFormatter(\Locale::getDefault(), \IntlDateFormatter::MEDIUM, \IntlDateFormatter::SHORT, 'UTC');
// neither the native nor the stub IntlDateFormatter support
// DateTimeImmutable as of yet
if (!$value instanceof \DateTime) {
$value = new \DateTime($value->format('Y-m-d H:i:s.u e'));
}
return $formatter->format($value);
return $formatter->format(new \DateTime(
$value->format('Y-m-d H:i:s.u'),
new \DateTimeZone('UTC')
));
}
return $value->format('Y-m-d H:i:s');

View File

@ -27,6 +27,9 @@ final class ConstraintValidatorTest extends TestCase
public function formatValueProvider()
{
$defaultTimezone = date_default_timezone_get();
date_default_timezone_set('Europe/Moscow'); // GMT+3
$data = [
['true', true],
['false', false],
@ -36,10 +39,15 @@ final class ConstraintValidatorTest extends TestCase
['array', []],
['object', $toString = new TestToStringObject()],
['ccc', $toString, ConstraintValidator::OBJECT_TO_STRING],
['object', $dateTime = (new \DateTimeImmutable('@0'))->setTimezone(new \DateTimeZone('UTC'))],
[class_exists(\IntlDateFormatter::class) ? 'Jan 1, 1970, 12:00 AM' : '1970-01-01 00:00:00', $dateTime, ConstraintValidator::PRETTY_DATE],
['object', $dateTime = new \DateTimeImmutable('1971-02-02T08:00:00UTC')],
[class_exists(\IntlDateFormatter::class) ? 'Oct 4, 2019, 11:02 AM' : '2019-10-04 11:02:03', new \DateTimeImmutable('2019-10-04T11:02:03+09:00'), ConstraintValidator::PRETTY_DATE],
[class_exists(\IntlDateFormatter::class) ? 'Feb 2, 1971, 8:00 AM' : '1971-02-02 08:00:00', $dateTime, ConstraintValidator::PRETTY_DATE],
[class_exists(\IntlDateFormatter::class) ? 'Jan 1, 1970, 6:00 AM' : '1970-01-01 06:00:00', new \DateTimeImmutable('1970-01-01T06:00:00Z'), ConstraintValidator::PRETTY_DATE],
[class_exists(\IntlDateFormatter::class) ? 'Jan 1, 1970, 3:00 PM' : '1970-01-01 15:00:00', (new \DateTimeImmutable('1970-01-01T23:00:00'))->setTimezone(new \DateTimeZone('America/New_York')), ConstraintValidator::PRETTY_DATE],
];
date_default_timezone_set($defaultTimezone);
return $data;
}
}