From acf95c3ed34a68b0cd0016333106d2fe4ee44c7e Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 3 Oct 2018 19:38:14 +0200 Subject: [PATCH 1/2] reverse transform RFC 3339 formatted dates Technically, dates formatted according to the HTML specifications do not contain any timezone information. But since our DateTimeType used to contain this information in the passed, users had configure their JS libraries to accept (and create) dates in that format. To not break BC we should accept these dates and silently ignore the additional timezone information. --- .../DateTimeToHtml5LocalDateTimeTransformer.php | 4 +++- .../DateTimeToHtml5LocalDateTimeTransformerTest.php | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php index ed90331266443..280e4d4293df2 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php @@ -81,7 +81,9 @@ public function reverseTransform($dateTimeLocal) return; } - if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})[T ]\d{2}:\d{2}(?::\d{2})?$/', $dateTimeLocal, $matches)) { + // to maintain backwards compatibility we do not strictly validate the submitted date + // see https://github.com/symfony/symfony/issues/28699 + if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})[T ]\d{2}:\d{2}(?::\d{2})?/', $dateTimeLocal, $matches)) { throw new TransformationFailedException(sprintf('The date "%s" is not a valid date.', $dateTimeLocal)); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformerTest.php index 8573ddf818d2d..3a530f66472e7 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformerTest.php @@ -51,6 +51,10 @@ public function reverseTransformProvider() array('UTC', 'UTC', '2010-02-03 04:05:00 UTC', '2010-02-03T04:05'), array('America/New_York', 'Asia/Hong_Kong', '2010-02-03 04:05:00 America/New_York', '2010-02-03T17:05'), array('Europe/Amsterdam', 'Europe/Amsterdam', '2013-08-21 10:30:00 Europe/Amsterdam', '2013-08-21T10:30:00'), + array('UTC', 'UTC', '2018-09-15T10:00:00Z', '2018-09-15T10:00:00Z'), + array('Europe/Berlin', 'Europe/Berlin', '2018-09-15T10:00:00+02:00', '2018-09-15T10:00:00+02:00'), + array('Europe/Berlin', 'Europe/Berlin', '2018-09-15T10:00:00+0200', '2018-09-15T10:00:00+0200'), + array('UTC', 'UTC', '2018-10-03T10:00:00.000Z', '2018-10-03T10:00:00.000Z'), ); } From 31328192961cce02c5d6c5ab5e53a6804fd1965d Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 4 Oct 2018 13:39:38 +0200 Subject: [PATCH 2/2] deprecate submitting non HTML5 formatted dates --- .../AbstractBootstrap3LayoutTest.php | 2 + src/Symfony/Bridge/Twig/composer.json | 4 +- .../Bundle/FrameworkBundle/composer.json | 4 +- ...ateTimeToHtml5LocalDateTimeTransformer.php | 21 +++++++++- .../Form/Extension/Core/Type/DateTimeType.php | 12 +++++- .../Form/Tests/AbstractLayoutTest.php | 2 + ...imeToHtml5LocalDateTimeTransformerTest.php | 41 ++++++++++++++----- .../Extension/Core/Type/DateTimeTypeTest.php | 8 ++++ 8 files changed, 78 insertions(+), 16 deletions(-) diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php index 87d0b7677fbec..e45f774797511 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php @@ -1568,6 +1568,7 @@ public function testDateTimeWithWidgetSingleText() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateTimeType', '2011-02-03 04:05:06', array( 'input' => 'string', 'widget' => 'single_text', + 'strict_format' => true, 'model_timezone' => 'UTC', 'view_timezone' => 'UTC', )); @@ -1589,6 +1590,7 @@ public function testDateTimeWithWidgetSingleTextIgnoreDateAndTimeWidgets() 'date_widget' => 'choice', 'time_widget' => 'choice', 'widget' => 'single_text', + 'strict_format' => true, 'model_timezone' => 'UTC', 'view_timezone' => 'UTC', )); diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index 99fe30663a70e..e703329df0fa5 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -23,7 +23,7 @@ "symfony/asset": "~3.4|~4.0", "symfony/dependency-injection": "~3.4|~4.0", "symfony/finder": "~3.4|~4.0", - "symfony/form": "^4.1.5", + "symfony/form": "^4.2", "symfony/http-foundation": "~3.4|~4.0", "symfony/http-kernel": "~3.4|~4.0", "symfony/polyfill-intl-icu": "~1.0", @@ -42,7 +42,7 @@ }, "conflict": { "symfony/console": "<3.4", - "symfony/form": "<4.1.2", + "symfony/form": "<4.2", "symfony/translation": "<4.2" }, "suggest": { diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index a3d2f6ab3656d..40a8967e3bd2a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -39,7 +39,7 @@ "symfony/dom-crawler": "~3.4|~4.0", "symfony/polyfill-intl-icu": "~1.0", "symfony/security": "~3.4|~4.0", - "symfony/form": "^4.1", + "symfony/form": "^4.2", "symfony/expression-language": "~3.4|~4.0", "symfony/messenger": "^4.2", "symfony/process": "~3.4|~4.0", @@ -66,7 +66,7 @@ "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", "symfony/asset": "<3.4", "symfony/console": "<3.4", - "symfony/form": "<4.1", + "symfony/form": "<4.2", "symfony/messenger": "<4.2", "symfony/property-info": "<3.4", "symfony/serializer": "<4.1", diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php index 280e4d4293df2..a8f33b93961de 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformer.php @@ -22,6 +22,19 @@ class DateTimeToHtml5LocalDateTimeTransformer extends BaseDateTimeTransformer { const HTML5_FORMAT = 'Y-m-d\\TH:i:s'; + private $strict; + + public function __construct(string $inputTimezone = null, string $outputTimezone = null, bool $strict = false) + { + parent::__construct($inputTimezone, $outputTimezone); + + $this->strict = $strict; + + if (!$this->strict) { + @trigger_error(sprintf('Parsing dates in %s that are not formatted according to the HTML5 specifications is deprecated since Symfony 4.2.', self::class), E_USER_DEPRECATED); + } + } + /** * Transforms a \DateTime into a local date and time string. * @@ -83,7 +96,13 @@ public function reverseTransform($dateTimeLocal) // to maintain backwards compatibility we do not strictly validate the submitted date // see https://github.com/symfony/symfony/issues/28699 - if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})[T ]\d{2}:\d{2}(?::\d{2})?/', $dateTimeLocal, $matches)) { + if (!$this->strict) { + $regex = '/^(\d{4})-(\d{2})-(\d{2})[T ]\d{2}:\d{2}(?::\d{2})?/'; + } else { + $regex = '/^(\d{4})-(\d{2})-(\d{2})[T ]\d{2}:\d{2}(?::\d{2})?$/'; + } + + if (!preg_match($regex, $dateTimeLocal, $matches)) { throw new TransformationFailedException(sprintf('The date "%s" is not a valid date.', $dateTimeLocal)); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php index c0a5e6ab1a50c..844ed6f27392e 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php @@ -78,7 +78,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) if (self::HTML5_FORMAT === $pattern) { $builder->addViewTransformer(new DateTimeToHtml5LocalDateTimeTransformer( $options['model_timezone'], - $options['view_timezone'] + $options['view_timezone'], + $options['strict_format'] )); } else { $builder->addViewTransformer(new DateTimeToLocalizedStringTransformer( @@ -218,6 +219,7 @@ public function configureOptions(OptionsResolver $resolver) 'model_timezone' => null, 'view_timezone' => null, 'format' => self::HTML5_FORMAT, + 'strict_format' => false, 'date_format' => null, 'widget' => null, 'date_widget' => $dateWidget, @@ -278,6 +280,14 @@ public function configureOptions(OptionsResolver $resolver) 'text', 'choice', )); + + $resolver->setNormalizer('strict_format', function (Options $options, $strictFormat) { + if (!$strictFormat && 'single_text' === 'widget' && self::HTML5_FORMAT === $options['format']) { + @trigger_error(sprintf('Setting the "strict_format" option of %s to "false" is deprecated since Symfony 4.2.', self::class), E_USER_DEPRECATED); + } + + return $strictFormat; + }); } /** diff --git a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php index 40d4e5688c94a..26637fda5c3d5 100644 --- a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php @@ -1493,6 +1493,7 @@ public function testDateTimeWithWidgetSingleText() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateTimeType', '2011-02-03 04:05:06', array( 'input' => 'string', 'widget' => 'single_text', + 'strict_format' => true, 'model_timezone' => 'UTC', 'view_timezone' => 'UTC', )); @@ -1513,6 +1514,7 @@ public function testDateTimeWithWidgetSingleTextIgnoreDateAndTimeWidgets() 'date_widget' => 'choice', 'time_widget' => 'choice', 'widget' => 'single_text', + 'strict_format' => true, 'model_timezone' => 'UTC', 'view_timezone' => 'UTC', )); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformerTest.php index 3a530f66472e7..dfa84ff93b790 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToHtml5LocalDateTimeTransformerTest.php @@ -51,10 +51,6 @@ public function reverseTransformProvider() array('UTC', 'UTC', '2010-02-03 04:05:00 UTC', '2010-02-03T04:05'), array('America/New_York', 'Asia/Hong_Kong', '2010-02-03 04:05:00 America/New_York', '2010-02-03T17:05'), array('Europe/Amsterdam', 'Europe/Amsterdam', '2013-08-21 10:30:00 Europe/Amsterdam', '2013-08-21T10:30:00'), - array('UTC', 'UTC', '2018-09-15T10:00:00Z', '2018-09-15T10:00:00Z'), - array('Europe/Berlin', 'Europe/Berlin', '2018-09-15T10:00:00+02:00', '2018-09-15T10:00:00+02:00'), - array('Europe/Berlin', 'Europe/Berlin', '2018-09-15T10:00:00+0200', '2018-09-15T10:00:00+0200'), - array('UTC', 'UTC', '2018-10-03T10:00:00.000Z', '2018-10-03T10:00:00.000Z'), ); } @@ -63,7 +59,7 @@ public function reverseTransformProvider() */ public function testTransform($fromTz, $toTz, $from, $to) { - $transformer = new DateTimeToHtml5LocalDateTimeTransformer($fromTz, $toTz); + $transformer = new DateTimeToHtml5LocalDateTimeTransformer($fromTz, $toTz, true); $this->assertSame($to, $transformer->transform(null !== $from ? new \DateTime($from) : null)); } @@ -73,7 +69,7 @@ public function testTransform($fromTz, $toTz, $from, $to) */ public function testTransformDateTimeImmutable($fromTz, $toTz, $from, $to) { - $transformer = new DateTimeToHtml5LocalDateTimeTransformer($fromTz, $toTz); + $transformer = new DateTimeToHtml5LocalDateTimeTransformer($fromTz, $toTz, true); $this->assertSame($to, $transformer->transform(null !== $from ? new \DateTimeImmutable($from) : null)); } @@ -83,7 +79,7 @@ public function testTransformDateTimeImmutable($fromTz, $toTz, $from, $to) */ public function testTransformRequiresValidDateTime() { - $transformer = new DateTimeToHtml5LocalDateTimeTransformer(); + $transformer = new DateTimeToHtml5LocalDateTimeTransformer(null, null, true); $transformer->transform('2010-01-01'); } @@ -91,6 +87,21 @@ public function testTransformRequiresValidDateTime() * @dataProvider reverseTransformProvider */ public function testReverseTransform($toTz, $fromTz, $to, $from) + { + $transformer = new DateTimeToHtml5LocalDateTimeTransformer($toTz, $fromTz, true); + + if (null !== $to) { + $this->assertEquals(new \DateTime($to), $transformer->reverseTransform($from)); + } else { + $this->assertNull($transformer->reverseTransform($from)); + } + } + + /** + * @group legacy + * @dataProvider nonHtml5DatesProvider + */ + public function testReverseTransformNonHtml5Dates($toTz, $fromTz, $to, $from) { $transformer = new DateTimeToHtml5LocalDateTimeTransformer($toTz, $fromTz); @@ -101,12 +112,22 @@ public function testReverseTransform($toTz, $fromTz, $to, $from) } } + public function nonHtml5DatesProvider() + { + return array( + array('UTC', 'UTC', '2018-09-15T10:00:00Z', '2018-09-15T10:00:00Z'), + array('Europe/Berlin', 'Europe/Berlin', '2018-09-15T10:00:00+02:00', '2018-09-15T10:00:00+02:00'), + array('Europe/Berlin', 'Europe/Berlin', '2018-09-15T10:00:00+0200', '2018-09-15T10:00:00+0200'), + array('UTC', 'UTC', '2018-10-03T10:00:00.000Z', '2018-10-03T10:00:00.000Z'), + ); + } + /** * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException */ public function testReverseTransformRequiresString() { - $transformer = new DateTimeToHtml5LocalDateTimeTransformer(); + $transformer = new DateTimeToHtml5LocalDateTimeTransformer(null, null, true); $transformer->reverseTransform(12345); } @@ -115,7 +136,7 @@ public function testReverseTransformRequiresString() */ public function testReverseTransformWithNonExistingDate() { - $transformer = new DateTimeToHtml5LocalDateTimeTransformer('UTC', 'UTC'); + $transformer = new DateTimeToHtml5LocalDateTimeTransformer('UTC', 'UTC', true); $transformer->reverseTransform('2010-04-31T04:05'); } @@ -125,7 +146,7 @@ public function testReverseTransformWithNonExistingDate() */ public function testReverseTransformExpectsValidDateString() { - $transformer = new DateTimeToHtml5LocalDateTimeTransformer('UTC', 'UTC'); + $transformer = new DateTimeToHtml5LocalDateTimeTransformer('UTC', 'UTC', true); $transformer->reverseTransform('2010-2010-2010'); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTimeTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTimeTypeTest.php index a45b183142c3d..f9e2073b926fa 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTimeTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTimeTypeTest.php @@ -235,6 +235,7 @@ public function testSubmitDifferentTimezonesDateTime() 'view_timezone' => 'Pacific/Tahiti', 'widget' => 'single_text', 'input' => 'datetime', + 'strict_format' => true, )); $outputTime = new \DateTime('2010-06-02 03:04:00 Pacific/Tahiti'); @@ -254,6 +255,7 @@ public function testSubmitDifferentTimezonesDateTimeImmutable() 'view_timezone' => 'Pacific/Tahiti', 'widget' => 'single_text', 'input' => 'datetime_immutable', + 'strict_format' => true, )); $outputTime = new \DateTimeImmutable('2010-06-02 03:04:00 Pacific/Tahiti'); @@ -274,6 +276,7 @@ public function testSubmitStringSingleText() 'view_timezone' => 'UTC', 'input' => 'string', 'widget' => 'single_text', + 'strict_format' => true, )); $form->submit('2010-06-02T03:04:00'); @@ -290,6 +293,7 @@ public function testSubmitStringSingleTextWithSeconds() 'input' => 'string', 'widget' => 'single_text', 'with_seconds' => true, + 'strict_format' => true, )); $form->submit('2010-06-02T03:04:05'); @@ -328,6 +332,7 @@ public function testSingleTextWidgetShouldUseTheRightInputType() { $view = $this->factory->create(static::TESTED_TYPE, null, array( 'widget' => 'single_text', + 'strict_format' => true, )) ->createView(); @@ -453,6 +458,7 @@ public function testPassHtml5TypeIfSingleTextAndHtml5Format() { $view = $this->factory->create(static::TESTED_TYPE, null, array( 'widget' => 'single_text', + 'strict_format' => true, )) ->createView(); @@ -464,6 +470,7 @@ public function testDontPassHtml5TypeIfHtml5NotAllowed() $view = $this->factory->create(static::TESTED_TYPE, null, array( 'widget' => 'single_text', 'html5' => false, + 'strict_format' => true, )) ->createView(); @@ -621,6 +628,7 @@ public function testSubmitNullWithSingleText() { $form = $this->factory->create(static::TESTED_TYPE, null, array( 'widget' => 'single_text', + 'strict_format' => true, )); $form->submit(null);