diff --git a/.travis.yml b/.travis.yml index fd9aa095c9c30..3d5253f077b03 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,6 +32,9 @@ matrix: - php: nightly services: [memcached] fast_finish: true + allow_failures: + - php: nightly + services: [memcached] cache: directories: diff --git a/CHANGELOG-3.4.md b/CHANGELOG-3.4.md index dfd39c33e34d0..a2da20414f835 100644 --- a/CHANGELOG-3.4.md +++ b/CHANGELOG-3.4.md @@ -7,6 +7,16 @@ in 3.4 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v3.4.0...v3.4.1 +* 3.4.42 (2020-06-12) + + * bug #37103 [Form] switch the context when validating nested forms (xabbuh) + * bug #37182 [HttpKernel] Fix regression where Store does not return response body correctly (mpdude) + * bug #36913 [FrameworkBundle] fix type annotation on ControllerTrait::addFlash() (ThomasLandauer) + * bug #37169 [Cache] fix forward compatibility with Doctrine DBAL 3 (xabbuh) + * bug #37085 [Form] properly cascade validation to child forms (xabbuh) + * bug #37095 [PhpUnitBridge] Fix undefined index when output of "composer show" cannot be parsed (nicolas-grekas) + * bug #37092 [PhpUnitBridge] fix undefined var on version 3.4 (nicolas-grekas) + * 3.4.41 (2020-05-31) * bug #36894 [Validator] never directly validate Existence (Required/Optional) constraints (xabbuh) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index baf025cfe0487..cd95879aaaa07 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -65,18 +65,18 @@ Symfony is the result of the work of many people who made the code better - Kevin Bond (kbond) - Saša Stamenković (umpirsky) - Peter Rehm (rpet) + - Gábor Egyed (1ed) - Gabriel Ostrolucký (gadelat) - Henrik Bjørnskov (henrikbjorn) - - Gábor Egyed (1ed) - Miha Vrhovnik - David Maicher (dmaicher) + - Titouan Galopin (tgalopin) - Diego Saint Esteben (dii3g0) - Jan Schädlich (jschaedl) - - Titouan Galopin (tgalopin) - Konstantin Kudryashov (everzet) + - Vladimir Reznichenko (kalessil) - Bilal Amarni (bamarni) - Mathieu Piot (mpiot) - - Vladimir Reznichenko (kalessil) - Florin Patan (florinpatan) - Jáchym Toušek (enumag) - Andrej Hudec (pulzarraider) @@ -91,12 +91,12 @@ Symfony is the result of the work of many people who made the code better - Henrik Westphal (snc) - Dariusz Górecki (canni) - David Buchmann (dbu) + - Jérôme Tamarelle (gromnan) - Graham Campbell (graham) - Dariusz Ruminski - Lee McDermott - Brandon Turner - Luis Cordova (cordoval) - - Jérôme Tamarelle (gromnan) - Daniel Holmes (dholmes) - Toni Uebernickel (havvg) - Fran Moreno (franmomu) @@ -106,11 +106,11 @@ Symfony is the result of the work of many people who made the code better - Antoine Hérault (herzult) - Paráda József (paradajozsef) - Arnaud Le Blanc (arnaud-lb) + - Sebastiaan Stok (sstok) - Maxime STEINHAUSSER - Baptiste Clavié (talus) - Michal Piotrowski (eventhorizon) - Tim Nagel (merk) - - Sebastiaan Stok (sstok) - Chris Wilkinson (thewilkybarkid) - Brice BERNARD (brikou) - marc.weistroff @@ -170,6 +170,7 @@ Symfony is the result of the work of many people who made the code better - Philipp Wahala (hifi) - Rafael Dohms (rdohms) - jwdeitch + - Ahmed TAILOULOUTE (ahmedtai) - Mikael Pajunen - Arman Hosseini (arman) - Niels Keurentjes (curry684) @@ -177,7 +178,6 @@ Symfony is the result of the work of many people who made the code better - Richard van Laak (rvanlaak) - Richard Shank (iampersistent) - Thomas Rabaix (rande) - - Ahmed TAILOULOUTE (ahmedtai) - Vincent Touzet (vincenttouzet) - jeremyFreeAgent (jeremyfreeagent) - Rouven Weßling (realityking) @@ -198,6 +198,7 @@ Symfony is the result of the work of many people who made the code better - GDIBass - Samuel NELA (snela) - Saif (╯°□°)╯ (azjezz) + - Gary PEGEOT (gary-p) - James Halsall (jaitsu) - Matthieu Napoli (mnapoli) - Florent Mata (fmata) @@ -220,7 +221,6 @@ Symfony is the result of the work of many people who made the code better - DQNEO - Andre Rømcke (andrerom) - mcfedr (mcfedr) - - Gary PEGEOT (gary-p) - Ruben Gonzalez (rubenrua) - Benjamin Dulau (dbenjamin) - Jan Rosier (rosier) @@ -244,6 +244,7 @@ Symfony is the result of the work of many people who made the code better - Matthieu Bontemps (mbontemps) - apetitpa - Pierre Minnieur (pminnieur) + - David Prévot - fivestar - Dominique Bongiraud - Jeremy Livingston (jeremylivingston) @@ -277,7 +278,6 @@ Symfony is the result of the work of many people who made the code better - Ruud Kamphuis (ruudk) - Pavel Batanov (scaytrase) - Mantis Development - - David Prévot - Loïc Faugeron - Hidde Wieringa (hiddewie) - dFayet @@ -371,6 +371,7 @@ Symfony is the result of the work of many people who made the code better - Chris Smith (cs278) - Thomas Bisignani (toma) - Florian Klein (docteurklein) + - Benjamin Leveque (benji07) - Manuel Kiessling (manuelkiessling) - Atsuhiro KUBO (iteman) - rudy onfroy (ronfroy) @@ -407,6 +408,7 @@ Symfony is the result of the work of many people who made the code better - Dariusz Rumiński - Berny Cantos (xphere81) - Thierry Thuon (lepiaf) + - Guilhem N (guilhemn) - Ricard Clau (ricardclau) - Mark Challoner (markchalloner) - Philippe Segatori @@ -432,6 +434,7 @@ Symfony is the result of the work of many people who made the code better - Tomasz Kowalczyk (thunderer) - Artur Eshenbrener - Timo Bakx (timobakx) + - Harm van Tilborg (hvt) - Thomas Perez (scullwm) - Felix Labrecque - Yaroslav Kiliba @@ -471,7 +474,6 @@ Symfony is the result of the work of many people who made the code better - Michele Locati - Pavel Volokitin (pvolok) - Valentine Boineau (valentineboineau) - - Benjamin Leveque (benji07) - Arthur de Moulins (4rthem) - Matthias Althaus (althaus) - Nicolas Dewez (nicolas_dewez) @@ -516,10 +518,11 @@ Symfony is the result of the work of many people who made the code better - Steffen Roßkamp - Alexandru Furculita (afurculita) - Valentin Jonovs (valentins-jonovs) - - Guilhem N (guilhemn) + - Sebastien Morel (plopix) - Jeanmonod David (jeanmonod) - Christopher Davis (chrisguitarguy) - Webnet team (webnet) + - Joe Bennett (kralos) - Farhad Safarov - Jan Schumann - Niklas Fiekas @@ -556,6 +559,7 @@ Symfony is the result of the work of many people who made the code better - Ariel Ferrandini (aferrandini) - Dirk Pahl (dirkaholic) - cedric lombardot (cedriclombardot) + - Arkadius Stefanski (arkadius) - Tim Goudriaan (codedmonkey) - Jonas Flodén (flojon) - Tobias Weichart @@ -621,6 +625,7 @@ Symfony is the result of the work of many people who made the code better - Maks Slesarenko - Filip Procházka (fprochazka) - mmoreram + - Jeroen Thora (bolle) - Markus Lanthaler (lanthaler) - Remi Collet - Vicent Soria Durá (vicentgodella) @@ -688,6 +693,7 @@ Symfony is the result of the work of many people who made the code better - Pavel Campr (pcampr) - Andrii Dembitskyi - Johnny Robeson (johnny) + - Thomas Landauer (thomas-landauer) - Guilliam Xavier - Disquedur - Michiel Boeckaert (milio) @@ -701,13 +707,11 @@ Symfony is the result of the work of many people who made the code better - Piotr Stankowski - Baptiste Leduc (bleduc) - Julien Maulny - - Sebastien Morel (plopix) - Jean-Christophe Cuvelier [Artack] - Julien Montel (julienmgel) - Simon DELICATA - Artem Henvald (artemgenvald) - Dmitry Simushev - - Joe Bennett (kralos) - alcaeus - Thomas Talbot (ioni) - Fred Cox @@ -722,10 +726,12 @@ Symfony is the result of the work of many people who made the code better - Marvin Butkereit - Renan - Ricky Su (ricky) + - Marcin Szepczynski (czepol) - Kyle Evans (kevans91) - Charles-Henri Bruyand - Max Rath (drak3) - Stéphane Escandell (sescandell) + - Baptiste Leduc (korbeil) - Konstantin S. M. Möllers (ksmmoellers) - James Johnston - Noémi Salaün (noemi-salaun) @@ -776,7 +782,6 @@ Symfony is the result of the work of many people who made the code better - maxime.steinhausser - adev - Stefan Warman - - Arkadius Stefanski (arkadius) - Tristan Maindron (tmaindron) - Behnoush Norouzali (behnoush) - Wesley Lancel @@ -949,12 +954,10 @@ Symfony is the result of the work of many people who made the code better - Jayson Xu (superjavason) - Hubert Lenoir (hubert_lenoir) - fago - - Harm van Tilborg - Jan Prieser - GDIBass - Antoine Lamirault - Adrien Lucas (adrienlucas) - - Jeroen Thora (bolle) - Zhuravlev Alexander (scif) - Stefano Degenkamp (steef) - James Michael DuPont @@ -1038,7 +1041,6 @@ Symfony is the result of the work of many people who made the code better - Don Pinkster - Maksim Muruev - Emil Einarsson - - Thomas Landauer - 243083df - Thibault Duplessis - Rimas Kudelis @@ -1060,6 +1062,7 @@ Symfony is the result of the work of many people who made the code better - Hugo Alliaume (kocal) - Marcos Gómez Vilches (markitosgv) - Matthew Davis (mdavis1982) + - Paulo Ribeiro (paulo) - Markus S. (staabm) - Benjamin Morel - Maks @@ -1118,7 +1121,6 @@ Symfony is the result of the work of many people who made the code better - xaav - Mahmoud Mostafa (mahmoud) - Antonio Jose Cerezo (ajcerezo) - - Baptiste Leduc (korbeil) - Ahmed Abdou - Daniel Iwaniec - Thomas Ferney @@ -1157,6 +1159,8 @@ Symfony is the result of the work of many people who made the code better - zairig imad (zairigimad) - Anton Babenko (antonbabenko) - Irmantas Šiupšinskas (irmantas) + - Benoit Mallo + - Lescot Edouard (idetox) - Danilo Silva - Giuseppe Campanelli - Arnaud PETITPAS (apetitpa) @@ -1226,11 +1230,13 @@ Symfony is the result of the work of many people who made the code better - gr1ev0us - mlazovla - Alejandro Diaz Torres + - quentin neyrat (qneyrat) - Max Beutel - Jan Vernieuwe (vernija) - Antanas Arvasevicius - Pierre Dudoret - Thomas + - j.schmitt - Maximilian Berghoff (electricmaxxx) - nacho - Piotr Antosik (antek88) @@ -1326,7 +1332,9 @@ Symfony is the result of the work of many people who made the code better - Carlos Ortega Huetos - rpg600 - Péter Buri (burci) + - John VanDeWeghe - kaiwa + - Claude Khedhiri (ck-developer) - Charles Sanquer (csanquer) - Albert Ganiev (helios-ag) - Neil Katin @@ -1363,6 +1371,7 @@ Symfony is the result of the work of many people who made the code better - arnaud (arnooo999) - Gilles Doge (gido) - Oscar Esteve (oesteve) + - Sobhan Sharifi (50bhan) - abulford - Philipp Kretzschmar - antograssiot @@ -1412,6 +1421,7 @@ Symfony is the result of the work of many people who made the code better - Marc Torres - Mark Spink - Alberto Aldegheri + - Sagrario Meneses - Dmitri Petmanson - heccjj - Alexandre Melard @@ -1731,7 +1741,9 @@ Symfony is the result of the work of many people who made the code better - Przemysław Piechota (kibao) - Leonid Terentyev (li0n) - Martynas Sudintas (martiis) + - Douglas Hammond (wizhippo) - ryunosuke + - Bruno BOUTAREL - victoria - Francisco Facioni (fran6co) - Stanislav Gamayunov (happyproff) @@ -1864,6 +1876,7 @@ Symfony is the result of the work of many people who made the code better - Pavel.Batanov - avi123 - Pavel Prischepa + - Sami Mussbach - alsar - downace - Aarón Nieves Fernández @@ -1891,6 +1904,7 @@ Symfony is the result of the work of many people who made the code better - Brian Graham (incognito) - Kevin Vergauwen (innocenzo) - Alessio Baglio (ioalessio) + - Jeroen Noten (jeroennoten) - Johannes Müller (johmue) - Jordi Llonch (jordillonch) - Nicholas Ruunu (nicholasruunu) @@ -1911,6 +1925,7 @@ Symfony is the result of the work of many people who made the code better - Alexey Popkov - Gijs Kunze - Artyom Protaskin + - Steven Dubois - Nathanael d. Noblet - helmer - ged15 @@ -1983,6 +1998,7 @@ Symfony is the result of the work of many people who made the code better - Andrew Marcinkevičius (ifdattic) - Ioana Hazsda (ioana-hazsda) - Jan Marek (janmarek) + - Dmitriy Mamontov (mamontovdmitriy) - Mark de Haan (markdehaan) - Dan Patrick (mdpatrick) - naitsirch (naitsirch) @@ -2343,6 +2359,7 @@ Symfony is the result of the work of many people who made the code better - Ben Miller - Peter Gribanov - Matteo Galli + - Loenix - kwiateusz - jspee - Ilya Bulakh @@ -2466,7 +2483,6 @@ Symfony is the result of the work of many people who made the code better - Marco Petersen (ocrampete16) - ollie harridge (ollietb) - Paul Andrieux (paulandrieux) - - Paulo Ribeiro (paulo) - Paweł Szczepanek (pauluz) - Philippe Degeeter (pdegeeter) - Christian López Espínola (penyaskito) @@ -2521,6 +2537,7 @@ Symfony is the result of the work of many people who made the code better - MaPePeR - Andreas Streichardt - Alexandre Segura + - Marco Pfeiffer - Vivien - Pascal Hofmann - david-binda diff --git a/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php b/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php index 4bbc9f3fcdfb4..c6e4ab3a308ba 100644 --- a/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php +++ b/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php @@ -12,6 +12,7 @@ namespace Symfony\Bridge\Doctrine\Security\RememberMe; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Driver\Result; use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types; use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken; @@ -63,7 +64,7 @@ public function loadTokenBySeries($series) $paramValues = ['series' => $series]; $paramTypes = ['series' => \PDO::PARAM_STR]; $stmt = $this->conn->executeQuery($sql, $paramValues, $paramTypes); - $row = method_exists($stmt, 'fetchAssociative') ? $stmt->fetchAssociative() : $stmt->fetch(\PDO::FETCH_ASSOC); + $row = $stmt instanceof Result ? $stmt->fetchAssociative() : $stmt->fetch(\PDO::FETCH_ASSOC); if ($row) { return new PersistentToken($row['class'], $row['username'], $series, $row['value'], new \DateTime($row['last_used'])); diff --git a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit index f0c7c4085aba4..eae4cc3125147 100755 --- a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit +++ b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit @@ -106,10 +106,15 @@ if (!file_exists("$PHPUNIT_DIR/phpunit-$PHPUNIT_VERSION/phpunit") || md5_file(__ } } + $info += array( + 'versions' => array(), + 'requires' => array('php' => '*'), + ); + if (1 === \count($info['versions'])) { - $passthruOrFail("$COMPOSER create-project --ignore-platform-reqs --no-install --prefer-dist --no-scripts --no-plugins --no-progress --ansi -s dev phpunit/phpunit $PHPUNIT_VERSION_DIR \"$PHPUNIT_VERSION.*\""); + $passthruOrFail("$COMPOSER create-project --ignore-platform-reqs --no-install --prefer-dist --no-scripts --no-plugins --no-progress --ansi -s dev phpunit/phpunit phpunit-$PHPUNIT_VERSION \"$PHPUNIT_VERSION.*\""); } else { - $passthruOrFail("$COMPOSER create-project --ignore-platform-reqs --no-install --prefer-dist --no-scripts --no-plugins --no-progress --ansi phpunit/phpunit $PHPUNIT_VERSION_DIR \"$PHPUNIT_VERSION.*\""); + $passthruOrFail("$COMPOSER create-project --ignore-platform-reqs --no-install --prefer-dist --no-scripts --no-plugins --no-progress --ansi phpunit/phpunit phpunit-$PHPUNIT_VERSION \"$PHPUNIT_VERSION.*\""); } @copy("phpunit-$PHPUNIT_VERSION/phpunit.xsd", 'phpunit.xsd'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php index 9d5640750af84..8858ca66c437b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php @@ -187,7 +187,7 @@ protected function file($file, $fileName = null, $disposition = ResponseHeaderBa * Adds a flash message to the current session for type. * * @param string $type The type - * @param string $message The message + * @param mixed $message The message * * @throws \LogicException * diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.php index 5470ca7dc6858..a79504a8fe090 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.php @@ -2,6 +2,7 @@ namespace Symfony\Bundle\WebProfilerBundle\Tests\Functional; +use Psr\Log\NullLogger; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Bundle\TwigBundle\TwigBundle; @@ -73,6 +74,11 @@ public function getLogDir() return sys_get_temp_dir().'/log-'.spl_object_hash($this); } + protected function build(ContainerBuilder $container) + { + $container->register('logger', NullLogger::class); + } + public function homepageController() { return new Response('Homepage Controller.'); diff --git a/src/Symfony/Component/Cache/Tests/Traits/PdoPruneableTrait.php b/src/Symfony/Component/Cache/Tests/Traits/PdoPruneableTrait.php index c5ba9c66d6942..113a2c28eb8ee 100644 --- a/src/Symfony/Component/Cache/Tests/Traits/PdoPruneableTrait.php +++ b/src/Symfony/Component/Cache/Tests/Traits/PdoPruneableTrait.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Cache\Tests\Traits; +use Doctrine\DBAL\Driver\Result; + trait PdoPruneableTrait { protected function isPruned($cache, $name) @@ -27,8 +29,8 @@ protected function isPruned($cache, $name) /** @var \Doctrine\DBAL\Statement|\PDOStatement $select */ $select = $getPdoConn->invoke($cache)->prepare('SELECT 1 FROM cache_items WHERE item_id LIKE :id'); $select->bindValue(':id', sprintf('%%%s', $name)); - $select->execute(); + $result = $select->execute(); - return 1 !== (int) (method_exists($select, 'fetchOne') ? $select->fetchOne() : $select->fetch(\PDO::FETCH_COLUMN)); + return 1 !== (int) ($result instanceof Result ? $result->fetchOne() : $select->fetch(\PDO::FETCH_COLUMN)); } } diff --git a/src/Symfony/Component/Cache/Traits/PdoTrait.php b/src/Symfony/Component/Cache/Traits/PdoTrait.php index 6a9f3c46aea45..1aa87cd3f1a11 100644 --- a/src/Symfony/Component/Cache/Traits/PdoTrait.php +++ b/src/Symfony/Component/Cache/Traits/PdoTrait.php @@ -11,8 +11,10 @@ namespace Symfony\Component\Cache\Traits; +use Doctrine\DBAL\Abstraction\Result; use Doctrine\DBAL\Connection; use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Driver\Result as DriverResult; use Doctrine\DBAL\Driver\ServerInfoAwareConnection; use Doctrine\DBAL\Schema\Schema; use Symfony\Component\Cache\Exception\InvalidArgumentException; @@ -175,15 +177,16 @@ protected function doFetch(array $ids) foreach ($ids as $id) { $stmt->bindValue(++$i, $id); } - $stmt->execute(); + $result = $stmt->execute(); - if (method_exists($stmt, 'iterateNumeric')) { - $stmt = $stmt->iterateNumeric(); + if ($result instanceof Result) { + $result = $result->iterateNumeric(); } else { $stmt->setFetchMode(\PDO::FETCH_NUM); + $result = $stmt; } - foreach ($stmt as $row) { + foreach ($result as $row) { if (null === $row[1]) { $expired[] = $row[0]; } else { @@ -213,9 +216,9 @@ protected function doHave($id) $stmt->bindValue(':id', $id); $stmt->bindValue(':time', time(), \PDO::PARAM_INT); - $stmt->execute(); + $result = $stmt->execute(); - return (bool) $stmt->fetchColumn(); + return (bool) ($result instanceof DriverResult ? $result->fetchOne() : $stmt->fetchColumn()); } /** @@ -335,9 +338,9 @@ protected function doSave(array $values, $lifetime) } foreach ($serialized as $id => $data) { - $stmt->execute(); + $result = $stmt->execute(); - if (null === $driver && !$stmt->rowCount()) { + if (null === $driver && !($result instanceof DriverResult ? $result->rowCount() : $stmt->rowCount())) { try { $insertStmt->execute(); } catch (DBALException $e) { diff --git a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php index 6a8923ecbf0a5..4447b2e54aa88 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php +++ b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php @@ -72,7 +72,6 @@ public function validate($form, Constraint $formConstraint) if ($groups instanceof GroupSequence) { // Validate the data, the form AND nested fields in sequence $violationsCount = $this->context->getViolations()->count(); - $fieldPropertyPath = \is_object($data) ? 'children[%s]' : 'children%s'; foreach ($groups->groups as $group) { if ($validateDataGraph) { @@ -91,7 +90,8 @@ public function validate($form, Constraint $formConstraint) // in different steps without breaking early enough $this->resolvedGroups[$field] = (array) $group; $fieldFormConstraint = new Form(); - $validator->atPath(sprintf($fieldPropertyPath, $field->getPropertyPath()))->validate($field, $fieldFormConstraint); + $this->context->setNode($this->context->getValue(), $field, $this->context->getMetadata(), $this->context->getPropertyPath()); + $validator->atPath(sprintf('children[%s]', $field->getName()))->validate($field, $fieldFormConstraint); } } @@ -100,8 +100,6 @@ public function validate($form, Constraint $formConstraint) } } } else { - $fieldPropertyPath = \is_object($data) ? 'children[%s]' : 'children%s'; - if ($validateDataGraph) { $validator->atPath('data')->validate($data, null, $groups); } @@ -132,7 +130,8 @@ public function validate($form, Constraint $formConstraint) if ($field->isSubmitted()) { $this->resolvedGroups[$field] = $groups; $fieldFormConstraint = new Form(); - $validator->atPath(sprintf($fieldPropertyPath, $field->getPropertyPath()))->validate($field, $fieldFormConstraint); + $this->context->setNode($this->context->getValue(), $field, $this->context->getMetadata(), $this->context->getPropertyPath()); + $validator->atPath(sprintf('children[%s]', $field->getName()))->validate($field, $fieldFormConstraint); } } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php new file mode 100644 index 0000000000000..b51b34bb32ab0 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php @@ -0,0 +1,361 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\Validator\Constraints; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Validator\ValidatorExtension; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormFactoryBuilder; +use Symfony\Component\Form\Test\ForwardCompatTestTrait; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\Collection; +use Symfony\Component\Validator\Constraints\Expression; +use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory; +use Symfony\Component\Validator\Mapping\Loader\StaticMethodLoader; +use Symfony\Component\Validator\Validation; + +class FormValidatorFunctionalTest extends TestCase +{ + use ForwardCompatTestTrait; + + private $validator; + private $formFactory; + + private function doSetUp() + { + $this->validator = Validation::createValidatorBuilder() + ->setMetadataFactory(new LazyLoadingMetadataFactory(new StaticMethodLoader())) + ->getValidator(); + $this->formFactory = (new FormFactoryBuilder()) + ->addExtension(new ValidatorExtension($this->validator)) + ->getFormFactory(); + } + + public function testDataConstraintsInvalidateFormEvenIfFieldIsNotSubmitted() + { + $form = $this->formFactory->create(FooType::class); + $form->submit(['baz' => 'foobar'], false); + + $this->assertTrue($form->isSubmitted()); + $this->assertFalse($form->isValid()); + $this->assertFalse($form->get('bar')->isSubmitted()); + $this->assertCount(1, $form->get('bar')->getErrors()); + } + + public function testFieldConstraintsDoNotInvalidateFormIfFieldIsNotSubmitted() + { + $form = $this->formFactory->create(FooType::class); + $form->submit(['bar' => 'foobar'], false); + + $this->assertTrue($form->isSubmitted()); + $this->assertTrue($form->isValid()); + } + + public function testFieldConstraintsInvalidateFormIfFieldIsSubmitted() + { + $form = $this->formFactory->create(FooType::class); + $form->submit(['bar' => 'foobar', 'baz' => ''], false); + + $this->assertTrue($form->isSubmitted()); + $this->assertFalse($form->isValid()); + $this->assertTrue($form->get('bar')->isSubmitted()); + $this->assertTrue($form->get('bar')->isValid()); + $this->assertTrue($form->get('baz')->isSubmitted()); + $this->assertFalse($form->get('baz')->isValid()); + } + + public function testNonCompositeConstraintValidatedOnce() + { + $form = $this->formFactory->create(TextType::class, null, [ + 'constraints' => [new NotBlank(['groups' => ['foo', 'bar']])], + 'validation_groups' => ['foo', 'bar'], + ]); + $form->submit(''); + + $violations = $this->validator->validate($form); + + $this->assertCount(1, $violations); + $this->assertSame('This value should not be blank.', $violations[0]->getMessage()); + $this->assertSame('data', $violations[0]->getPropertyPath()); + } + + public function testCompositeConstraintValidatedInEachGroup() + { + $form = $this->formFactory->create(FormType::class, null, [ + 'constraints' => [ + new Collection([ + 'field1' => new NotBlank([ + 'groups' => ['field1'], + ]), + 'field2' => new NotBlank([ + 'groups' => ['field2'], + ]), + ]), + ], + 'validation_groups' => ['field1', 'field2'], + ]); + $form->add('field1'); + $form->add('field2'); + $form->submit([ + 'field1' => '', + 'field2' => '', + ]); + + $violations = $this->validator->validate($form); + + $this->assertCount(2, $violations); + $this->assertSame('This value should not be blank.', $violations[0]->getMessage()); + $this->assertSame('data[field1]', $violations[0]->getPropertyPath()); + $this->assertSame('This value should not be blank.', $violations[1]->getMessage()); + $this->assertSame('data[field2]', $violations[1]->getPropertyPath()); + } + + public function testCompositeConstraintValidatedInSequence() + { + $form = $this->formFactory->create(FormType::class, null, [ + 'constraints' => [ + new Collection([ + 'field1' => new NotBlank([ + 'groups' => ['field1'], + ]), + 'field2' => new NotBlank([ + 'groups' => ['field2'], + ]), + ]), + ], + 'validation_groups' => new GroupSequence(['field1', 'field2']), + ]); + $form->add('field1'); + $form->add('field2'); + + $form->submit([ + 'field1' => '', + 'field2' => '', + ]); + + $violations = $this->validator->validate($form); + + $this->assertCount(1, $violations); + $this->assertSame('This value should not be blank.', $violations[0]->getMessage()); + $this->assertSame('data[field1]', $violations[0]->getPropertyPath()); + } + + public function testFieldsValidateInSequence() + { + $form = $this->formFactory->create(FormType::class, null, [ + 'validation_groups' => new GroupSequence(['group1', 'group2']), + ]) + ->add('foo', TextType::class, [ + 'constraints' => [new Length(['min' => 10, 'groups' => ['group1']])], + ]) + ->add('bar', TextType::class, [ + 'constraints' => [new NotBlank(['groups' => ['group2']])], + ]) + ; + + $form->submit(['foo' => 'invalid', 'bar' => null]); + + $errors = $form->getErrors(true); + + $this->assertCount(1, $errors); + $this->assertInstanceOf(Length::class, $errors[0]->getCause()->getConstraint()); + } + + public function testFieldsValidateInSequenceWithNestedGroupsArray() + { + $form = $this->formFactory->create(FormType::class, null, [ + 'validation_groups' => new GroupSequence([['group1', 'group2'], 'group3']), + ]) + ->add('foo', TextType::class, [ + 'constraints' => [new Length(['min' => 10, 'groups' => ['group1']])], + ]) + ->add('bar', TextType::class, [ + 'constraints' => [new Length(['min' => 10, 'groups' => ['group2']])], + ]) + ->add('baz', TextType::class, [ + 'constraints' => [new NotBlank(['groups' => ['group3']])], + ]) + ; + + $form->submit(['foo' => 'invalid', 'bar' => 'invalid', 'baz' => null]); + + $errors = $form->getErrors(true); + + $this->assertCount(2, $errors); + $this->assertInstanceOf(Length::class, $errors[0]->getCause()->getConstraint()); + $this->assertInstanceOf(Length::class, $errors[1]->getCause()->getConstraint()); + } + + public function testConstraintsInDifferentGroupsOnSingleField() + { + $form = $this->formFactory->create(FormType::class, null, [ + 'validation_groups' => new GroupSequence(['group1', 'group2']), + ]) + ->add('foo', TextType::class, [ + 'constraints' => [ + new NotBlank([ + 'groups' => ['group1'], + ]), + new Length([ + 'groups' => ['group2'], + 'max' => 3, + ]), + ], + ]); + $form->submit([ + 'foo' => 'test@example.com', + ]); + + $errors = $form->getErrors(true); + + $this->assertFalse($form->isValid()); + $this->assertCount(1, $errors); + $this->assertInstanceOf(Length::class, $errors[0]->getCause()->getConstraint()); + } + + public function testCascadeValidationToChildFormsUsingPropertyPaths() + { + $form = $this->formFactory->create(FormType::class, null, [ + 'validation_groups' => ['group1', 'group2'], + ]) + ->add('field1', null, [ + 'constraints' => [new NotBlank(['groups' => 'group1'])], + 'property_path' => '[foo]', + ]) + ->add('field2', null, [ + 'constraints' => [new NotBlank(['groups' => 'group2'])], + 'property_path' => '[bar]', + ]) + ; + + $form->submit([ + 'field1' => '', + 'field2' => '', + ]); + + $violations = $this->validator->validate($form); + + $this->assertCount(2, $violations); + $this->assertSame('This value should not be blank.', $violations[0]->getMessage()); + $this->assertSame('children[field1].data', $violations[0]->getPropertyPath()); + $this->assertSame('This value should not be blank.', $violations[1]->getMessage()); + $this->assertSame('children[field2].data', $violations[1]->getPropertyPath()); + } + + public function testCascadeValidationToChildFormsUsingPropertyPathsValidatedInSequence() + { + $form = $this->formFactory->create(FormType::class, null, [ + 'validation_groups' => new GroupSequence(['group1', 'group2']), + ]) + ->add('field1', null, [ + 'constraints' => [new NotBlank(['groups' => 'group1'])], + 'property_path' => '[foo]', + ]) + ->add('field2', null, [ + 'constraints' => [new NotBlank(['groups' => 'group2'])], + 'property_path' => '[bar]', + ]) + ; + + $form->submit([ + 'field1' => '', + 'field2' => '', + ]); + + $violations = $this->validator->validate($form); + + $this->assertCount(1, $violations); + $this->assertSame('This value should not be blank.', $violations[0]->getMessage()); + $this->assertSame('children[field1].data', $violations[0]->getPropertyPath()); + } + + public function testContextIsPopulatedWithFormBeingValidated() + { + $form = $this->formFactory->create(FormType::class) + ->add('field1', null, [ + 'constraints' => [new Expression([ + 'expression' => '!this.getParent().get("field2").getData()', + ])], + ]) + ->add('field2') + ; + + $form->submit([ + 'field1' => '', + 'field2' => '', + ]); + + $violations = $this->validator->validate($form); + + $this->assertCount(0, $violations); + } + + public function testContextIsPopulatedWithFormBeingValidatedUsingGroupSequence() + { + $form = $this->formFactory->create(FormType::class, null, [ + 'validation_groups' => new GroupSequence(['group1']), + ]) + ->add('field1', null, [ + 'constraints' => [new Expression([ + 'expression' => '!this.getParent().get("field2").getData()', + 'groups' => ['group1'], + ])], + ]) + ->add('field2') + ; + + $form->submit([ + 'field1' => '', + 'field2' => '', + ]); + + $violations = $this->validator->validate($form); + + $this->assertCount(0, $violations); + } +} + +class Foo +{ + public $bar; + public $baz; + + public static function loadValidatorMetadata(ClassMetadata $metadata) + { + $metadata->addPropertyConstraint('bar', new NotBlank()); + } +} + +class FooType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('bar') + ->add('baz', null, [ + 'constraints' => [new NotBlank()], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefault('data_class', Foo::class); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php index 3d7111f85f3c1..f789fbe1e408d 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php @@ -18,13 +18,13 @@ use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper; use Symfony\Component\Form\Extension\Validator\Constraints\Form; use Symfony\Component\Form\Extension\Validator\Constraints\FormValidator; +use Symfony\Component\Form\Extension\Validator\ValidatorExtension; use Symfony\Component\Form\FormBuilder; use Symfony\Component\Form\FormFactoryBuilder; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\SubmitButtonBuilder; use Symfony\Component\Translation\IdentityTranslator; -use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; @@ -51,7 +51,9 @@ class FormValidatorTest extends ConstraintValidatorTestCase protected function setUp() { $this->dispatcher = new EventDispatcher(); - $this->factory = (new FormFactoryBuilder())->getFormFactory(); + $this->factory = (new FormFactoryBuilder()) + ->addExtension(new ValidatorExtension(Validation::createValidator())) + ->getFormFactory(); parent::setUp(); @@ -701,96 +703,6 @@ public function testCauseForNotAllowedExtraFieldsIsTheFormConstraint() $this->assertSame($constraint, $context->getViolations()->get(0)->getConstraint()); } - public function testNonCompositeConstraintValidatedOnce() - { - $form = $this - ->getBuilder('form', null, [ - 'constraints' => [new NotBlank(['groups' => ['foo', 'bar']])], - 'validation_groups' => ['foo', 'bar'], - ]) - ->setCompound(false) - ->getForm(); - $form->submit(''); - - $context = new ExecutionContext(Validation::createValidator(), $form, new IdentityTranslator()); - $this->validator->initialize($context); - $this->validator->validate($form, new Form()); - - $this->assertCount(1, $context->getViolations()); - $this->assertSame('This value should not be blank.', $context->getViolations()[0]->getMessage()); - $this->assertSame('data', $context->getViolations()[0]->getPropertyPath()); - } - - public function testCompositeConstraintValidatedInEachGroup() - { - $form = $this->getBuilder('form', null, [ - 'constraints' => [ - new Collection([ - 'field1' => new NotBlank([ - 'groups' => ['field1'], - ]), - 'field2' => new NotBlank([ - 'groups' => ['field2'], - ]), - ]), - ], - 'validation_groups' => ['field1', 'field2'], - ]) - ->setData([]) - ->setCompound(true) - ->setDataMapper(new PropertyPathMapper()) - ->getForm(); - $form->add($this->getForm('field1')); - $form->add($this->getForm('field2')); - $form->submit([ - 'field1' => '', - 'field2' => '', - ]); - - $context = new ExecutionContext(Validation::createValidator(), $form, new IdentityTranslator()); - $this->validator->initialize($context); - $this->validator->validate($form, new Form()); - - $this->assertCount(2, $context->getViolations()); - $this->assertSame('This value should not be blank.', $context->getViolations()[0]->getMessage()); - $this->assertSame('data[field1]', $context->getViolations()[0]->getPropertyPath()); - $this->assertSame('This value should not be blank.', $context->getViolations()[1]->getMessage()); - $this->assertSame('data[field2]', $context->getViolations()[1]->getPropertyPath()); - } - - public function testCompositeConstraintValidatedInSequence() - { - $form = $this->getCompoundForm([], [ - 'constraints' => [ - new Collection([ - 'field1' => new NotBlank([ - 'groups' => ['field1'], - ]), - 'field2' => new NotBlank([ - 'groups' => ['field2'], - ]), - ]), - ], - 'validation_groups' => new GroupSequence(['field1', 'field2']), - ]) - ->add($this->getForm('field1')) - ->add($this->getForm('field2')) - ; - - $form->submit([ - 'field1' => '', - 'field2' => '', - ]); - - $context = new ExecutionContext(Validation::createValidator(), $form, new IdentityTranslator()); - $this->validator->initialize($context); - $this->validator->validate($form, new Form()); - - $this->assertCount(1, $context->getViolations()); - $this->assertSame('This value should not be blank.', $context->getViolations()[0]->getMessage()); - $this->assertSame('data[field1]', $context->getViolations()[0]->getPropertyPath()); - } - protected function createValidator() { return new FormValidator(); diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorExtensionTest.php index 9793bd78e69e7..4a12acf4126b4 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorExtensionTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/ValidatorExtensionTest.php @@ -12,23 +12,12 @@ namespace Symfony\Component\Form\Tests\Extension\Validator; use PHPUnit\Framework\TestCase; -use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\FormType; -use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Validator\Constraints\Form as FormConstraint; use Symfony\Component\Form\Extension\Validator\ValidatorExtension; use Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser; use Symfony\Component\Form\Form; -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Form\FormFactoryBuilder; -use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\GroupSequence; -use Symfony\Component\Validator\Constraints\Length; -use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Mapping\CascadingStrategy; use Symfony\Component\Validator\Mapping\ClassMetadata; -use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory; -use Symfony\Component\Validator\Mapping\Loader\StaticMethodLoader; use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\Tests\Fixtures\FakeMetadataFactory; use Symfony\Component\Validator\Validation; @@ -57,151 +46,4 @@ public function test2Dot5ValidationApi() $this->assertSame(TraversalStrategy::NONE, $metadata->traversalStrategy); $this->assertCount(0, $metadata->getPropertyMetadata('children')); } - - public function testDataConstraintsInvalidateFormEvenIfFieldIsNotSubmitted() - { - $form = $this->createForm(FooType::class); - $form->submit(['baz' => 'foobar'], false); - - $this->assertTrue($form->isSubmitted()); - $this->assertFalse($form->isValid()); - $this->assertFalse($form->get('bar')->isSubmitted()); - $this->assertCount(1, $form->get('bar')->getErrors()); - } - - public function testFieldConstraintsDoNotInvalidateFormIfFieldIsNotSubmitted() - { - $form = $this->createForm(FooType::class); - $form->submit(['bar' => 'foobar'], false); - - $this->assertTrue($form->isSubmitted()); - $this->assertTrue($form->isValid()); - } - - public function testFieldConstraintsInvalidateFormIfFieldIsSubmitted() - { - $form = $this->createForm(FooType::class); - $form->submit(['bar' => 'foobar', 'baz' => ''], false); - - $this->assertTrue($form->isSubmitted()); - $this->assertFalse($form->isValid()); - $this->assertTrue($form->get('bar')->isSubmitted()); - $this->assertTrue($form->get('bar')->isValid()); - $this->assertTrue($form->get('baz')->isSubmitted()); - $this->assertFalse($form->get('baz')->isValid()); - } - - public function testFieldsValidateInSequence() - { - $form = $this->createForm(FormType::class, null, [ - 'validation_groups' => new GroupSequence(['group1', 'group2']), - ]) - ->add('foo', TextType::class, [ - 'constraints' => [new Length(['min' => 10, 'groups' => ['group1']])], - ]) - ->add('bar', TextType::class, [ - 'constraints' => [new NotBlank(['groups' => ['group2']])], - ]) - ; - - $form->submit(['foo' => 'invalid', 'bar' => null]); - - $errors = $form->getErrors(true); - - $this->assertCount(1, $errors); - $this->assertInstanceOf(Length::class, $errors[0]->getCause()->getConstraint()); - } - - public function testFieldsValidateInSequenceWithNestedGroupsArray() - { - $form = $this->createForm(FormType::class, null, [ - 'validation_groups' => new GroupSequence([['group1', 'group2'], 'group3']), - ]) - ->add('foo', TextType::class, [ - 'constraints' => [new Length(['min' => 10, 'groups' => ['group1']])], - ]) - ->add('bar', TextType::class, [ - 'constraints' => [new Length(['min' => 10, 'groups' => ['group2']])], - ]) - ->add('baz', TextType::class, [ - 'constraints' => [new NotBlank(['groups' => ['group3']])], - ]) - ; - - $form->submit(['foo' => 'invalid', 'bar' => 'invalid', 'baz' => null]); - - $errors = $form->getErrors(true); - - $this->assertCount(2, $errors); - $this->assertInstanceOf(Length::class, $errors[0]->getCause()->getConstraint()); - $this->assertInstanceOf(Length::class, $errors[1]->getCause()->getConstraint()); - } - - public function testConstraintsInDifferentGroupsOnSingleField() - { - $form = $this->createForm(FormType::class, null, [ - 'validation_groups' => new GroupSequence(['group1', 'group2']), - ]) - ->add('foo', TextType::class, [ - 'constraints' => [ - new NotBlank([ - 'groups' => ['group1'], - ]), - new Length([ - 'groups' => ['group2'], - 'max' => 3, - ]), - ], - ]); - $form->submit([ - 'foo' => 'test@example.com', - ]); - - $errors = $form->getErrors(true); - - $this->assertFalse($form->isValid()); - $this->assertCount(1, $errors); - $this->assertInstanceOf(Length::class, $errors[0]->getCause()->getConstraint()); - } - - private function createForm($type, $data = null, array $options = []) - { - $validator = Validation::createValidatorBuilder() - ->setMetadataFactory(new LazyLoadingMetadataFactory(new StaticMethodLoader())) - ->getValidator(); - $formFactoryBuilder = new FormFactoryBuilder(); - $formFactoryBuilder->addExtension(new ValidatorExtension($validator)); - $formFactory = $formFactoryBuilder->getFormFactory(); - - return $formFactory->create($type, $data, $options); - } -} - -class Foo -{ - public $bar; - public $baz; - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addPropertyConstraint('bar', new NotBlank()); - } -} - -class FooType extends AbstractType -{ - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - ->add('bar') - ->add('baz', null, [ - 'constraints' => [new NotBlank()], - ]) - ; - } - - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefault('data_class', Foo::class); - } } diff --git a/src/Symfony/Component/Form/composer.json b/src/Symfony/Component/Form/composer.json index 161994a672609..c69ac107bd8c7 100644 --- a/src/Symfony/Component/Form/composer.json +++ b/src/Symfony/Component/Form/composer.json @@ -26,9 +26,10 @@ }, "require-dev": { "doctrine/collections": "~1.0", - "symfony/validator": "^3.2.5|~4.0", + "symfony/validator": "^3.4.3|^4.0.3", "symfony/dependency-injection": "~3.3|~4.0", "symfony/config": "~2.7|~3.0|~4.0", + "symfony/expression-language": "~3.4|~4.0", "symfony/http-foundation": "~2.8|~3.0|~4.0", "symfony/http-kernel": "^3.3.5|~4.0", "symfony/security-csrf": "^2.8.31|^3.3.13|~4.0", diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Store.php b/src/Symfony/Component/HttpKernel/HttpCache/Store.php index fd04a7b23d1d3..72793f582df85 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Store.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Store.php @@ -152,8 +152,8 @@ public function lookup(Request $request) } $headers = $match[1]; - if (file_exists($body = $this->getPath($headers['x-content-digest'][0]))) { - return $this->restoreResponse($headers, $body); + if (file_exists($path = $this->getPath($headers['x-content-digest'][0]))) { + return $this->restoreResponse($headers, $path); } // TODO the metaStore referenced an entity that doesn't exist in @@ -177,15 +177,28 @@ public function write(Request $request, Response $response) $key = $this->getCacheKey($request); $storedEnv = $this->persistRequest($request); - $digest = $this->generateContentDigest($response); - $response->headers->set('X-Content-Digest', $digest); + if ($response->headers->has('X-Body-File')) { + // Assume the response came from disk, but at least perform some safeguard checks + if (!$response->headers->has('X-Content-Digest')) { + throw new \RuntimeException('A restored response must have the X-Content-Digest header.'); + } - if (!$this->save($digest, $response->getContent(), false)) { - throw new \RuntimeException('Unable to store the entity.'); - } + $digest = $response->headers->get('X-Content-Digest'); + if ($this->getPath($digest) !== $response->headers->get('X-Body-File')) { + throw new \RuntimeException('X-Body-File and X-Content-Digest do not match.'); + } + // Everything seems ok, omit writing content to disk + } else { + $digest = $this->generateContentDigest($response); + $response->headers->set('X-Content-Digest', $digest); - if (!$response->headers->has('Transfer-Encoding')) { - $response->headers->set('Content-Length', \strlen($response->getContent())); + if (!$this->save($digest, $response->getContent(), false)) { + throw new \RuntimeException('Unable to store the entity.'); + } + + if (!$response->headers->has('Transfer-Encoding')) { + $response->headers->set('Content-Length', \strlen($response->getContent())); + } } // read existing cache entries, remove non-varying, and add this one to the list @@ -477,19 +490,19 @@ private function persistResponse(Response $response) * Restores a Response from the HTTP headers and body. * * @param array $headers An array of HTTP headers for the Response - * @param string $body The Response body + * @param string $path Path to the Response body * * @return Response */ - private function restoreResponse($headers, $body = null) + private function restoreResponse($headers, $path = null) { $status = $headers['X-Status'][0]; unset($headers['X-Status']); - if (null !== $body) { - $headers['X-Body-File'] = [$body]; + if (null !== $path) { + $headers['X-Body-File'] = [$path]; } - return new Response($body, $status, $headers); + return new Response($path, $status, $headers); } } diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 00382a95c1e9b..1f0e29b97cef5 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -67,11 +67,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private $requestStackSize = 0; private $resetServices = false; - const VERSION = '3.4.41'; - const VERSION_ID = 30441; + const VERSION = '3.4.42'; + const VERSION_ID = 30442; const MAJOR_VERSION = 3; const MINOR_VERSION = 4; - const RELEASE_VERSION = 41; + const RELEASE_VERSION = 42; const EXTRA_VERSION = ''; const END_OF_MAINTENANCE = '11/2020'; diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php index 07c41542697e3..eee8970b968c6 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php @@ -118,6 +118,39 @@ public function testWritesResponseEvenIfXContentDigestIsPresent() $this->assertNotNull($response); } + public function testWritingARestoredResponseDoesNotCorruptCache() + { + /* + * This covers the regression reported in https://github.com/symfony/symfony/issues/37174. + * + * A restored response does *not* load the body, but only keep the file path in a special X-Body-File + * header. For reasons (?), the file path was also used as the restored response body. + * It would be up to others (HttpCache...?) to honor this header and actually load the response content + * from there. + * + * When a restored response was stored again, the Store itself would ignore the header. In the first + * step, this would compute a new Content Digest based on the file path in the restored response body; + * this is covered by "Checkpoint 1" below. But, since the X-Body-File header was left untouched (Checkpoint 2), downstream + * code (HttpCache...) would not immediately notice. + * + * Only upon performing the lookup for a second time, we'd get a Response where the (wrong) Content Digest + * is also reflected in the X-Body-File header, this time also producing wrong content when the downstream + * evaluates it. + */ + $this->store->write($this->request, $this->response); + $digest = $this->response->headers->get('X-Content-Digest'); + $path = $this->getStorePath($digest); + + $response = $this->store->lookup($this->request); + $this->store->write($this->request, $response); + $this->assertEquals($digest, $response->headers->get('X-Content-Digest')); // Checkpoint 1 + $this->assertEquals($path, $response->headers->get('X-Body-File')); // Checkpoint 2 + + $response = $this->store->lookup($this->request); + $this->assertEquals($digest, $response->headers->get('X-Content-Digest')); + $this->assertEquals($path, $response->headers->get('X-Body-File')); + } + public function testFindsAStoredEntryWithLookup() { $this->storeSimpleEntry();