diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b6f39741d9dbc..ac82d7063d015 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ | Q | A | ------------- | --- -| Branch? | master for features / 2.8 up to 4.1 for bug fixes +| Branch? | master for features / 3.4 up to 4.2 for bug fixes | Bug fix? | yes/no | New feature? | yes/no | BC breaks? | no diff --git a/.travis.yml b/.travis.yml index 535d93b4a7f9c..b97864bb8418f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -159,7 +159,7 @@ before_install: fi tfold ext.apcu tpecl apcu-5.1.6 apcu.so $INI - tfold ext.mongodb tpecl mongodb-1.5.2 mongodb.so $INI + tfold ext.mongodb tpecl mongodb-1.6.0alpha1 mongodb.so $INI tfold ext.amqp tpecl amqp-1.9.3 amqp.so $INI done @@ -222,7 +222,7 @@ install: break fi phpenv global $PHP - ([[ $deps ]] && cd src/Symfony/Component/HttpFoundation; composer config platform.ext-mongodb 1.5.2; composer require --dev --no-update mongodb/mongodb) + ([[ $deps ]] && cd src/Symfony/Component/HttpFoundation; composer config platform.ext-mongodb 1.6.0; composer require --dev --no-update mongodb/mongodb) tfold 'composer update' $COMPOSER_UP tfold 'phpunit install' ./phpunit install if [[ $deps = high ]]; then diff --git a/CHANGELOG-4.1.md b/CHANGELOG-4.1.md index ad6a91cdd0b1b..a547ce44c6b78 100644 --- a/CHANGELOG-4.1.md +++ b/CHANGELOG-4.1.md @@ -7,6 +7,28 @@ in 4.1 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/v4.1.0...v4.1.1 +* 4.1.9 (2018-12-06) + + * security #cve-2018-19790 [Security\Http] detect bad redirect targets using backslashes (xabbuh) + * security #cve-2018-19789 [Form] Filter file uploads out of regular form types (nicolas-grekas) + * bug #29436 [Cache] Fixed Memcached adapter doClear()to call flush() (raitocz) + * bug #29441 [Routing] ignore trailing slash for non-GET requests (nicolas-grekas) + * bug #29444 [Workflow] Fixed BC break for Workflow metadata (lyrixx) + * bug #29432 [DI] dont inline when lazy edges are found (nicolas-grekas) + * bug #29413 [Serializer] fixed DateTimeNormalizer to maintain microseconds when a different timezone required (rvitaliy) + * bug #29424 [Routing] fix taking verb into account when redirecting (nicolas-grekas) + * bug #29414 [DI] Fix dumping expressions accessing single-use private services (chalasr) + * bug #29375 [Validator] Allow `ConstraintViolation::__toString()` to expose codes that are not null or emtpy strings (phansys) + * bug #29376 [EventDispatcher] Fix eventListener wrapper loop in TraceableEventDispatcher (jderusse) + * bug #29386 undeprecate the single-colon notation for controllers (fbourigault) + * bug #29393 [DI] fix edge case in InlineServiceDefinitionsPass (nicolas-grekas) + * bug #29380 [Routing] fix greediness of trailing slash (nicolas-grekas) + * bug #29343 [Form] Handle all case variants of "nan" when parsing a number (mwhudson, xabbuh) + * bug #29373 [Routing] fix trailing slash redirection (nicolas-grekas) + * bug #29355 [PropertyAccess] calculate cache keys for property setters depending on the value (xabbuh) + * bug #29369 [DI] fix combinatorial explosion when analyzing the service graph (nicolas-grekas) + * bug #29349 [Debug] workaround opcache bug mutating "$this" !?! (nicolas-grekas) + * 4.1.8 (2018-11-26) * bug #29318 [Console] Move back root exception to stack trace in verbose mode (chalasr) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 969c6a424cefc..1c329f2146c3e 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -14,9 +14,9 @@ Symfony is the result of the work of many people who made the code better - Victor Berchet (victor) - Robin Chalas (chalas_r) - Kévin Dunglas (dunglas) + - Maxime Steinhausser (ogizanagi) - Jakub Zalas (jakubzalas) - Johannes S (johannes) - - Maxime Steinhausser (ogizanagi) - Kris Wallsmith (kriswallsmith) - Ryan Weaver (weaverryan) - Javier Eguiluz (javier.eguiluz) @@ -37,8 +37,8 @@ Symfony is the result of the work of many people who made the code better - Benjamin Eberlei (beberlei) - Igor Wiedler (igorw) - Jules Pietri (heah) - - Eriksen Costa (eriksencosta) - Yonel Ceruto (yonelceruto) + - Eriksen Costa (eriksencosta) - Guilhem Niot (energetick) - Sarah Khalil (saro0h) - Jonathan Wage (jwage) @@ -70,6 +70,7 @@ Symfony is the result of the work of many people who made the code better - Gábor Egyed (1ed) - Mathieu Piot (mpiot) - Titouan Galopin (tgalopin) + - Vladimir Reznichenko (kalessil) - Michel Weimerskirch (mweimerskirch) - Andrej Hudec (pulzarraider) - Konstantin Myakshin (koc) @@ -77,7 +78,6 @@ Symfony is the result of the work of many people who made the code better - Jáchym Toušek (enumag) - Charles Sarrazin (csarrazi) - David Maicher (dmaicher) - - Vladimir Reznichenko (kalessil) - Christian Raue - Issei Murasawa (issei_m) - Arnout Boks (aboks) @@ -86,13 +86,13 @@ Symfony is the result of the work of many people who made the code better - Dariusz Górecki (canni) - Douglas Greenshields (shieldo) - Dariusz Ruminski + - Grégoire Paris (greg0ire) - Lee McDermott - Brandon Turner - Luis Cordova (cordoval) - Graham Campbell (graham) - Daniel Holmes (dholmes) - Toni Uebernickel (havvg) - - Grégoire Paris (greg0ire) - Bart van den Burg (burgov) - Jordan Alliot (jalliot) - Jérôme Tamarelle (gromnan) @@ -113,12 +113,12 @@ Symfony is the result of the work of many people who made the code better - lenar - Alexander Schwenn (xelaris) - Włodzimierz Gajda (gajdaw) + - Tomáš Votruba (tomas_votruba) - Peter Kokot (maastermedia) - Jacob Dreesen (jdreesen) - Florian Voutzinos (florianv) - Colin Frei - Adrien Brault (adrienbrault) - - Tomáš Votruba (tomas_votruba) - Joshua Thijssen - excelwebzone - Gordon Franke (gimler) @@ -179,6 +179,7 @@ Symfony is the result of the work of many people who made the code better - jeremyFreeAgent (Jérémy Romey) (jeremyfreeagent) - James Halsall (jaitsu) - Matthieu Napoli (mnapoli) + - Florent Mata (fmata) - Warnar Boekkooi (boekkooi) - Alessandro Chitolina (alekitto) - Dmitrii Chekaliuk (lazyhammer) @@ -193,7 +194,6 @@ Symfony is the result of the work of many people who made the code better - Dennis Benkert (denderello) - DQNEO - Benjamin Dulau (dbenjamin) - - Florent Mata (fmata) - Mathieu Lemoine (lemoinem) - Thomas Calvet (fancyweb) - Christian Schmidt @@ -258,6 +258,7 @@ Symfony is the result of the work of many people who made the code better - Benoît Burnichon (bburnichon) - Roman Marintšenko (inori) - Xavier Montaña Carreras (xmontana) + - François-Xavier de Guillebon (de-gui_f) - Mickaël Andrieu (mickaelandrieu) - Maxime Veber (nek-) - Xavier Perez @@ -294,7 +295,9 @@ Symfony is the result of the work of many people who made the code better - Thomas Lallement (raziel057) - mcfedr (mcfedr) - Colin O'Dell (colinodell) + - Fabien Bourigault (fbourigault) - Giorgio Premi + - Jan Schädlich (jschaedl) - Beau Simensen (simensen) - Michael Hirschler (mvhirsch) - Robert Kiss (kepten) @@ -305,8 +308,8 @@ Symfony is the result of the work of many people who made the code better - Jérôme Parmentier (lctrs) - Michael Babker (mbabker) - Peter Kruithof (pkruithof) - - François-Xavier de Guillebon (de-gui_f) - Michael Holm (hollo) + - Remon van de Kamp (rpkamp) - Marc Weistroff (futurecat) - Christian Schmidt - MatTheCat @@ -350,8 +353,6 @@ Symfony is the result of the work of many people who made the code better - Ricard Clau (ricardclau) - Mark Challoner (markchalloner) - Gennady Telegin (gtelegin) - - Jan Schädlich (jschaedl) - - Fabien Bourigault (fbourigault) - Ben Davies (bendavies) - Erin Millard - Artur Melo (restless) @@ -445,6 +446,7 @@ Symfony is the result of the work of many people who made the code better - lancergr - Zan Baldwin - Mihai Stancu + - Ivan Nikolaev (destillat) - Olivier Dolbeau (odolbeau) - Jan Rosier (rosier) - Alessandro Lai (jean85) @@ -461,7 +463,6 @@ Symfony is the result of the work of many people who made the code better - Boris Vujicic (boris.vujicic) - Chris Sedlmayr (catchamonkey) - Mateusz Sip (mateusz_sip) - - Remon van de Kamp - Kamil Kokot (pamil) - Seb Koelen - Christoph Mewes (xrstf) @@ -470,6 +471,7 @@ Symfony is the result of the work of many people who made the code better - Dirk Pahl (dirkaholic) - cedric lombardot (cedriclombardot) - Jonas Flodén (flojon) + - Gonzalo Vilaseca (gonzalovilaseca) - Marcin Sikoń (marphi) - Dominik Zogg (dominik.zogg) - Marek Pietrzak @@ -643,7 +645,6 @@ Symfony is the result of the work of many people who made the code better - adev - Stefan Warman - Arkadius Stefanski (arkadius) - - Gonzalo Vilaseca (gonzalovilaseca) - Tristan Maindron (tmaindron) - Wesley Lancel - Ke WANG (yktd26) @@ -665,6 +666,7 @@ Symfony is the result of the work of many people who made the code better - Sergey (upyx) - Michael Devery (mickadoo) - Antoine Corcy + - Dmitrii Poddubnyi (karser) - Sascha Grossenbacher - Szijarto Tamas - Robin Lehrmann (robinlehrmann) @@ -804,7 +806,6 @@ Symfony is the result of the work of many people who made the code better - Sofiane HADDAG (sofhad) - frost-nzcr4 - Bozhidar Hristov - - Ivan Nikolaev (destillat) - Laurent Bassin (lbassin) - andrey1s - Abhoryo @@ -989,6 +990,7 @@ Symfony is the result of the work of many people who made the code better - Mathieu Santostefano - Arjan Keeman - Máximo Cuadros (mcuadros) + - Lukas Mencl - tamirvs - julien.galenski - Christian Neff @@ -1290,6 +1292,7 @@ Symfony is the result of the work of many people who made the code better - Adrien Samson (adriensamson) - Samuel Gordalina (gordalina) - Max Romanovsky (maxromanovsky) + - Nicolas Eeckeloo (neeckeloo) - Mathieu Morlon - Daniel Tschinder - Arnaud CHASSEUX @@ -1351,6 +1354,7 @@ Symfony is the result of the work of many people who made the code better - Andrew (drew) - kor3k kor3k (kor3k) - Stelian Mocanita (stelian) + - Thomas Bisignani (toma) - Justin (wackymole) - Flavian (2much) - Gautier Deuette @@ -1448,6 +1452,7 @@ Symfony is the result of the work of many people who made the code better - Phobetor - Andreas - Markus + - Daniel Gorgan - Thomas Chmielowiec - shdev - Andrey Ryaguzov @@ -1499,6 +1504,7 @@ Symfony is the result of the work of many people who made the code better - David Barratt - Pavel.Batanov - avi123 + - Pavel Prischepa - alsar - downace - Aarón Nieves Fernández @@ -1613,6 +1619,7 @@ Symfony is the result of the work of many people who made the code better - David Zuelke - Adrian - Oleg Andreyev + - neFAST - Pierre Rineau - Maxim Lovchikov - adenkejawen @@ -1710,7 +1717,6 @@ Symfony is the result of the work of many people who made the code better - Giovanni Albero (johntree) - Jorge Martin (jorgemartind) - Joeri Verdeyen (jverdeyen) - - Dmitrii Poddubnyi (karser) - Kevin Verschaeve (keversc) - Kevin Herrera (kherge) - Luis Ramón López López (lrlopez) @@ -1872,6 +1878,7 @@ Symfony is the result of the work of many people who made the code better - zorn - Yuriy Potemkin - Emilie Lorenzo + - Edvin Hultberg - Benjamin Long - Matt Janssen - Ben Miller @@ -1992,6 +1999,7 @@ Symfony is the result of the work of many people who made the code better - Alex Carol (picard89) - Daniel Perez Pinazo (pitiflautico) - Phil Taylor (prazgod) + - Maxim Pustynnikov (pustynnikov) - Brayden Williams (redstar504) - Rich Sage (richsage) - Rokas Mikalkėnas (rokasm) diff --git a/phpunit b/phpunit index f4b80ed064121..9975195309a81 100755 --- a/phpunit +++ b/phpunit @@ -1,7 +1,7 @@ #!/usr/bin/env php addMethodCall('attach', array( - $transitionDefinition, + new Reference($transitionId), $transition['metadata'], )); } @@ -547,7 +547,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ } if ($transition['metadata']) { $transitionsMetadataDefinition->addMethodCall('attach', array( - $transitionDefinition, + new Reference($transitionId), $transition['metadata'], )); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php b/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php index 0b77b553ffa80..bf62c7cbd4b8c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php @@ -95,7 +95,7 @@ public function load($resource, $type = null) if (1 === substr_count($controller, ':')) { $nonDeprecatedNotation = str_replace(':', '::', $controller); - @trigger_error(sprintf('Referencing controllers with a single colon is deprecated since Symfony 4.1, use "%s" instead.', $nonDeprecatedNotation), E_USER_DEPRECATED); + // TODO deprecate this in 5.1 } $route->setDefault('_controller', $controller); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 2fbf496223d31..35da98ef7b0b4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -256,10 +256,8 @@ public function testWorkflows() $this->assertSame('attach', $transitionsMetadataCall[0]); $params = $transitionsMetadataCall[1]; $this->assertCount(2, $params); - $this->assertInstanceOf(Definition::class, $params[0]); - $this->assertSame(Workflow\Transition::class, $params[0]->getClass()); - $this->assertSame(array('submit', 'start', 'travis'), $params[0]->getArguments()); - $this->assertSame(array('title' => 'transition submit title'), $params[1]); + $this->assertInstanceOf(Reference::class, $params[0]); + $this->assertSame('state_machine.pull_request.transition.0', (string) $params[0]); $serviceMarkingStoreWorkflowDefinition = $container->getDefinition('workflow.service_marking_store_workflow'); /** @var Reference $markingStoreRef */ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/DelegatingLoaderTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/DelegatingLoaderTest.php index a0ad94b33e02e..70944fcac87cb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/DelegatingLoaderTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/DelegatingLoaderTest.php @@ -25,7 +25,6 @@ public function testConstructorApi() /** * @group legacy * @expectedDeprecation Referencing controllers with foo:bar:baz is deprecated since Symfony 4.1, use "some_parsed::controller" instead. - * @expectedDeprecation Referencing controllers with a single colon is deprecated since Symfony 4.1, use "foo::baz" instead. */ public function testLoad() { diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/events.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/events.html.twig index e0de7b570af4d..c0be48a377032 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/events.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/events.html.twig @@ -47,7 +47,7 @@
-

Orphaned events {{ collector.orphanedEvents|length }}

+

Orphaned Events {{ collector.orphanedEvents|length }}

{% if collector.orphanedEvents is empty %}
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig index 02b77319ac249..720da85750526 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig @@ -177,6 +177,12 @@ color: inherit; text-decoration: inherit; } + h2 + h3.form-data-type { + margin-top: 0; + } + h3.form-data-type + h3 { + margin-top: 1em; + } {% endblock %} @@ -455,9 +461,10 @@ {% macro form_tree_details(name, data, forms_by_hash, show) %} {% import _self as tree %}
-

- {{ name|default('(no name)') }} {% if data.type_class is defined %}({{ profiler_dump(data.type_class) }}){% endif %} -

+

{{ name|default('(no name)') }}

+ {% if data.type_class is defined %} +

{{ profiler_dump(data.type_class) }}

+ {% endif %} {% if data.errors is defined and data.errors|length > 0 %}
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php index 59d28a33c1bea..1f2f4d40acfa0 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php @@ -192,4 +192,9 @@ public function provideDsnWithOptions() array(\Memcached::OPT_SOCKET_RECV_SIZE => 1, \Memcached::OPT_SOCKET_SEND_SIZE => 2, \Memcached::OPT_RETRY_TIMEOUT => 8), ); } + + public function testClear() + { + $this->assertTrue($this->createCachePool()->clear()); + } } diff --git a/src/Symfony/Component/Cache/Traits/MemcachedTrait.php b/src/Symfony/Component/Cache/Traits/MemcachedTrait.php index cf04f1cf85664..8160f14116162 100644 --- a/src/Symfony/Component/Cache/Traits/MemcachedTrait.php +++ b/src/Symfony/Component/Cache/Traits/MemcachedTrait.php @@ -260,7 +260,7 @@ protected function doDelete(array $ids) */ protected function doClear($namespace) { - return false; + return '' === $namespace && $this->getClient()->flush(); } private function checkResultCode($result) diff --git a/src/Symfony/Component/Debug/DebugClassLoader.php b/src/Symfony/Component/Debug/DebugClassLoader.php index b7f3ca37fe6fd..3ee37ec4e7929 100644 --- a/src/Symfony/Component/Debug/DebugClassLoader.php +++ b/src/Symfony/Component/Debug/DebugClassLoader.php @@ -137,14 +137,14 @@ public function loadClass($class) try { if ($this->isFinder && !isset($this->loaded[$class])) { $this->loaded[$class] = true; - if ($file = $this->classLoader[0]->findFile($class) ?: false) { - $wasCached = \function_exists('opcache_is_script_cached') && @opcache_is_script_cached($file); - + if (!$file = $this->classLoader[0]->findFile($class) ?: false) { + // no-op + } elseif (\function_exists('opcache_is_script_cached') && @opcache_is_script_cached($file)) { require $file; - if ($wasCached) { - return; - } + return; + } else { + require $file; } } else { \call_user_func($this->classLoader, $class); diff --git a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php index 78073d93169d7..610a56bb0e607 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php @@ -93,8 +93,12 @@ private function isInlineableDefinition($id, Definition $definition, ServiceRefe } if (!$definition->isShared()) { + if (!$graph->hasNode($id)) { + return true; + } + foreach ($graph->getNode($id)->getInEdges() as $edge) { - if ($edge->isWeak()) { + if ($edge->isWeak() || $edge->isLazy()) { return false; } } @@ -115,13 +119,19 @@ private function isInlineableDefinition($id, Definition $definition, ServiceRefe } $ids = array(); + $isReferencedByConstructor = false; foreach ($graph->getNode($id)->getInEdges() as $edge) { - if ($edge->isWeak()) { + $isReferencedByConstructor = $isReferencedByConstructor || $edge->isReferencedByConstructor(); + if ($edge->isWeak() || $edge->isLazy()) { return false; } $ids[] = $edge->getSourceNode()->getId(); } + if (!$ids) { + return true; + } + if (\count(array_unique($ids)) > 1) { return false; } @@ -130,6 +140,10 @@ private function isInlineableDefinition($id, Definition $definition, ServiceRefe return false; } - return !$ids || $this->container->getDefinition($ids[0])->isShared(); + if ($isReferencedByConstructor && $this->container->getDefinition($ids[0])->isLazy() && ($definition->getProperties() || $definition->getMethodCalls() || $definition->getConfigurator())) { + return false; + } + + return $this->container->getDefinition($ids[0])->isShared(); } } diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index d9a9c9c8237bb..102033ce6d1fe 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -156,17 +156,18 @@ public function dump(array $options = array()) } (new AnalyzeServiceReferencesPass(false, !$this->getProxyDumper() instanceof NullDumper))->process($this->container); + $checkedNodes = array(); $this->circularReferences = array(); - foreach (array(true, false) as $byConstructor) { - foreach ($this->container->getCompiler()->getServiceReferenceGraph()->getNodes() as $id => $node) { - if (!$node->getValue() instanceof Definition) { - continue; - } - $currentPath = array($id => true); - $this->analyzeCircularReferences($node->getOutEdges(), $currentPath, $id, $byConstructor); + foreach ($this->container->getCompiler()->getServiceReferenceGraph()->getNodes() as $id => $node) { + if (!$node->getValue() instanceof Definition) { + continue; + } + if (!isset($checkedNodes[$id])) { + $this->analyzeCircularReferences($id, $node->getOutEdges(), $checkedNodes); } } $this->container->getCompiler()->getServiceReferenceGraph()->clear(); + $checkedNodes = array(); $this->docStar = $options['debug'] ? '*' : ''; @@ -307,12 +308,12 @@ private function getProxyDumper(): ProxyDumper return $this->proxyDumper; } - private function analyzeCircularReferences(array $edges, &$currentPath, $sourceId, $byConstructor) + private function analyzeCircularReferences($sourceId, array $edges, &$checkedNodes, &$currentPath = array()) { + $checkedNodes[$sourceId] = true; + $currentPath[$sourceId] = $sourceId; + foreach ($edges as $edge) { - if ($byConstructor && !$edge->isReferencedByConstructor()) { - continue; - } $node = $edge->getDestNode(); $id = $node->getId(); @@ -321,20 +322,42 @@ private function analyzeCircularReferences(array $edges, &$currentPath, $sourceI } elseif (isset($currentPath[$id])) { $currentId = $id; foreach (array_reverse($currentPath) as $parentId) { - if (!isset($this->circularReferences[$parentId][$currentId])) { - $this->circularReferences[$parentId][$currentId] = $byConstructor; + $this->circularReferences[$parentId][$currentId] = $currentId; + if ($parentId === $id) { + break; } + $currentId = $parentId; + } + } elseif (!isset($checkedNodes[$id])) { + $this->analyzeCircularReferences($id, $node->getOutEdges(), $checkedNodes, $currentPath); + } elseif (isset($this->circularReferences[$id])) { + $this->connectCircularReferences($id, $currentPath); + } + } + unset($currentPath[$sourceId]); + } + + private function connectCircularReferences($sourceId, &$currentPath, &$subPath = array()) + { + $subPath[$sourceId] = $sourceId; + $currentPath[$sourceId] = $sourceId; + + foreach ($this->circularReferences[$sourceId] as $id) { + if (isset($currentPath[$id])) { + $currentId = $id; + foreach (array_reverse($currentPath) as $parentId) { + $this->circularReferences[$parentId][$currentId] = $currentId; if ($parentId === $id) { break; } $currentId = $parentId; } - } else { - $currentPath[$id] = $id; - $this->analyzeCircularReferences($node->getOutEdges(), $currentPath, $id, $byConstructor); - unset($currentPath[$id]); + } elseif (!isset($subPath[$id]) && isset($this->circularReferences[$id])) { + $this->connectCircularReferences($id, $currentPath, $subPath); } } + unset($currentPath[$sourceId]); + unset($subPath[$sourceId]); } private function collectLineage($class, array &$lineage) @@ -539,7 +562,8 @@ private function addServiceConfigurator(Definition $definition, string $variable if (\is_array($callable)) { if ($callable[0] instanceof Reference - || ($callable[0] instanceof Definition && $this->definitionVariables->contains($callable[0]))) { + || ($callable[0] instanceof Definition && $this->definitionVariables->contains($callable[0])) + ) { return sprintf(" %s->%s(\$%s);\n", $this->dumpValue($callable[0]), $callable[1], $variableName); } @@ -691,7 +715,7 @@ private function addInlineReference(string $id, Definition $definition, string $ $hasSelfRef = isset($this->circularReferences[$id][$targetId]); $forConstructor = $forConstructor && !isset($this->definitionVariables[$definition]); - $code = $hasSelfRef && $this->circularReferences[$id][$targetId] && !$forConstructor ? $this->addInlineService($id, $definition, $definition) : ''; + $code = $hasSelfRef && !$forConstructor ? $this->addInlineService($id, $definition, $definition) : ''; if (isset($this->referenceVariables[$targetId]) || (2 > $callCount && (!$hasSelfRef || !$forConstructor))) { return $code; @@ -1584,6 +1608,7 @@ private function getServiceCall(string $id, Reference $reference = null): string if ($definition->isShared()) { $code = sprintf('$this->%s[\'%s\'] = %s', $definition->isPublic() ? 'services' : 'privates', $id, $code); } + $code = "($code)"; } elseif ($this->asFiles && !$this->isHotPath($definition)) { $code = sprintf("\$this->load('%s.php')", $this->generateMethodName($id)); if (!$definition->isShared()) { diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 962137e73da38..fa4e42017e03e 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -792,7 +792,7 @@ public function testExpressionReferencingPrivateService() ->setPublic(false); $container->register('public_foo', 'stdClass') ->setPublic(true) - ->addArgument(new Expression('service("private_foo")')); + ->addArgument(new Expression('service("private_foo").bar')); $container->compile(); $dumper = new PhpDumper($container); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt index 7f9c5cec280b1..fba001613d2b4 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt @@ -347,7 +347,7 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException; return $this->services['tagged_iterator'] = new \Bar(new RewindableGenerator(function () { yield 0 => ($this->services['foo'] ?? $this->load('getFooService.php')); - yield 1 => ($this->privates['tagged_iterator_foo'] ?? $this->privates['tagged_iterator_foo'] = new \Bar()); + yield 1 => ($this->privates['tagged_iterator_foo'] ?? ($this->privates['tagged_iterator_foo'] = new \Bar())); }, 2)); [Container%s/getTaggedIteratorFooService.php] => services['tagged_iterator'] = new \Bar(new RewindableGenerator(function () { yield 0 => ($this->services['foo'] ?? $this->getFooService()); - yield 1 => ($this->privates['tagged_iterator_foo'] ?? $this->privates['tagged_iterator_foo'] = new \Bar()); + yield 1 => ($this->privates['tagged_iterator_foo'] ?? ($this->privates['tagged_iterator_foo'] = new \Bar())); }, 2)); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php index 7923ae879f5f2..1a14c5d4d6875 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php @@ -114,7 +114,7 @@ protected function getBar3Service() { $this->services['bar3'] = $instance = new \BarCircular(); - $a = ($this->services['foobar3'] ?? $this->services['foobar3'] = new \FoobarCircular()); + $a = ($this->services['foobar3'] ?? ($this->services['foobar3'] = new \FoobarCircular())); $instance->addFoobar($a, $a); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_env_in_id.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_env_in_id.php index 93063503bd0d7..4b914f206f10f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_env_in_id.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_env_in_id.php @@ -70,7 +70,7 @@ public function getRemovedIds() */ protected function getBarService() { - return $this->services['bar'] = new \stdClass(($this->privates['bar_%env(BAR)%'] ?? $this->privates['bar_%env(BAR)%'] = new \stdClass())); + return $this->services['bar'] = new \stdClass(($this->privates['bar_%env(BAR)%'] ?? ($this->privates['bar_%env(BAR)%'] = new \stdClass()))); } /** @@ -80,7 +80,7 @@ protected function getBarService() */ protected function getFooService() { - return $this->services['foo'] = new \stdClass(($this->privates['bar_%env(BAR)%'] ?? $this->privates['bar_%env(BAR)%'] = new \stdClass()), array('baz_'.$this->getEnv('string:BAR') => new \stdClass())); + return $this->services['foo'] = new \stdClass(($this->privates['bar_%env(BAR)%'] ?? ($this->privates['bar_%env(BAR)%'] = new \stdClass())), array('baz_'.$this->getEnv('string:BAR') => new \stdClass())); } public function getParameter($name) diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_errored_definition.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_errored_definition.php index c3351a208c3eb..c5b90d19b2caa 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_errored_definition.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_errored_definition.php @@ -402,7 +402,7 @@ protected function getTaggedIteratorService() { return $this->services['tagged_iterator'] = new \Bar(new RewindableGenerator(function () { yield 0 => ($this->services['foo'] ?? $this->getFooService()); - yield 1 => ($this->privates['tagged_iterator_foo'] ?? $this->privates['tagged_iterator_foo'] = new \Bar()); + yield 1 => ($this->privates['tagged_iterator_foo'] ?? ($this->privates['tagged_iterator_foo'] = new \Bar())); }, 2)); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_locator.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_locator.php index 59bbce3a995c4..c705797921383 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_locator.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_locator.php @@ -76,7 +76,7 @@ public function getRemovedIds() */ protected function getBarServiceService() { - return $this->services['bar_service'] = new \stdClass(($this->privates['baz_service'] ?? $this->privates['baz_service'] = new \stdClass())); + return $this->services['bar_service'] = new \stdClass(($this->privates['baz_service'] ?? ($this->privates['baz_service'] = new \stdClass()))); } /** @@ -89,7 +89,7 @@ protected function getFooServiceService() return $this->services['foo_service'] = new \Symfony\Component\DependencyInjection\ServiceLocator(array('bar' => function () { return ($this->services['bar_service'] ?? $this->getBarServiceService()); }, 'baz' => function (): \stdClass { - return ($this->privates['baz_service'] ?? $this->privates['baz_service'] = new \stdClass()); + return ($this->privates['baz_service'] ?? ($this->privates['baz_service'] = new \stdClass())); }, 'nil' => function () { return NULL; })); @@ -133,7 +133,7 @@ protected function getTranslator_Loader3Service() protected function getTranslator1Service() { return $this->services['translator_1'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator(new \Symfony\Component\DependencyInjection\ServiceLocator(array('translator.loader_1' => function () { - return ($this->services['translator.loader_1'] ?? $this->services['translator.loader_1'] = new \stdClass()); + return ($this->services['translator.loader_1'] ?? ($this->services['translator.loader_1'] = new \stdClass())); }))); } @@ -145,10 +145,10 @@ protected function getTranslator1Service() protected function getTranslator2Service() { $this->services['translator_2'] = $instance = new \Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator(new \Symfony\Component\DependencyInjection\ServiceLocator(array('translator.loader_2' => function () { - return ($this->services['translator.loader_2'] ?? $this->services['translator.loader_2'] = new \stdClass()); + return ($this->services['translator.loader_2'] ?? ($this->services['translator.loader_2'] = new \stdClass())); }))); - $instance->addResource('db', ($this->services['translator.loader_2'] ?? $this->services['translator.loader_2'] = new \stdClass()), 'nl'); + $instance->addResource('db', ($this->services['translator.loader_2'] ?? ($this->services['translator.loader_2'] = new \stdClass())), 'nl'); return $instance; } @@ -161,10 +161,10 @@ protected function getTranslator2Service() protected function getTranslator3Service() { $this->services['translator_3'] = $instance = new \Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator(new \Symfony\Component\DependencyInjection\ServiceLocator(array('translator.loader_3' => function () { - return ($this->services['translator.loader_3'] ?? $this->services['translator.loader_3'] = new \stdClass()); + return ($this->services['translator.loader_3'] ?? ($this->services['translator.loader_3'] = new \stdClass())); }))); - $a = ($this->services['translator.loader_3'] ?? $this->services['translator.loader_3'] = new \stdClass()); + $a = ($this->services['translator.loader_3'] ?? ($this->services['translator.loader_3'] = new \stdClass())); $instance->addResource('db', $a, 'nl'); $instance->addResource('db', $a, 'en'); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_private_frozen.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_private_frozen.php index 86315e2ebaffc..7a55fab633998 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_private_frozen.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_private_frozen.php @@ -67,7 +67,7 @@ public function getRemovedIds() */ protected function getBarServiceService() { - return $this->services['bar_service'] = new \stdClass(($this->privates['baz_service'] ?? $this->privates['baz_service'] = new \stdClass())); + return $this->services['bar_service'] = new \stdClass(($this->privates['baz_service'] ?? ($this->privates['baz_service'] = new \stdClass()))); } /** @@ -77,6 +77,6 @@ protected function getBarServiceService() */ protected function getFooServiceService() { - return $this->services['foo_service'] = new \stdClass(($this->privates['baz_service'] ?? $this->privates['baz_service'] = new \stdClass())); + return $this->services['foo_service'] = new \stdClass(($this->privates['baz_service'] ?? ($this->privates['baz_service'] = new \stdClass()))); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_private_in_expression.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_private_in_expression.php index 5caf9104dd34d..0e9686f0f5e33 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_private_in_expression.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_private_in_expression.php @@ -67,6 +67,6 @@ public function getRemovedIds() */ protected function getPublicFooService() { - return $this->services['public_foo'] = new \stdClass(($this->privates['private_foo'] ?? $this->privates['private_foo'] = new \stdClass())); + return $this->services['public_foo'] = new \stdClass(($this->privates['private_foo'] ?? ($this->privates['private_foo'] = new \stdClass()))->bar); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_rot13_env.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_rot13_env.php index 76ce9e55328e3..12fc940a461b8 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_rot13_env.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_rot13_env.php @@ -79,7 +79,7 @@ protected function getRot13EnvVarProcessorService() protected function getContainer_EnvVarProcessorsLocatorService() { return $this->services['container.env_var_processors_locator'] = new \Symfony\Component\DependencyInjection\ServiceLocator(array('rot13' => function () { - return ($this->services['Symfony\Component\DependencyInjection\Tests\Dumper\Rot13EnvVarProcessor'] ?? $this->services['Symfony\Component\DependencyInjection\Tests\Dumper\Rot13EnvVarProcessor'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\Rot13EnvVarProcessor()); + return ($this->services['Symfony\Component\DependencyInjection\Tests\Dumper\Rot13EnvVarProcessor'] ?? ($this->services['Symfony\Component\DependencyInjection\Tests\Dumper\Rot13EnvVarProcessor'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\Rot13EnvVarProcessor())); })); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php index 2c887e0e21e0c..bc19940bb3b1e 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php @@ -80,13 +80,13 @@ protected function getTestServiceSubscriberService() protected function getFooServiceService() { return $this->services['foo_service'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber((new \Symfony\Component\DependencyInjection\ServiceLocator(array('Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => function (): ?\Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition { - return ($this->privates['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] ?? $this->privates['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition()); + return ($this->privates['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] ?? ($this->privates['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition())); }, 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber' => function (): \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber { - return ($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] ?? $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber()); + return ($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] ?? ($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber())); }, 'bar' => function (): \Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition { - return ($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] ?? $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber()); + return ($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] ?? ($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber())); }, 'baz' => function (): ?\Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition { - return ($this->privates['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] ?? $this->privates['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition()); + return ($this->privates['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] ?? ($this->privates['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition())); })))->withContext('foo_service', $this)); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_uninitialized_ref.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_uninitialized_ref.php index 0f5090c80bebe..a2fe59acdfeff 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_uninitialized_ref.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_uninitialized_ref.php @@ -107,7 +107,7 @@ protected function getBazService() { $this->services['baz'] = $instance = new \stdClass(); - $instance->foo3 = ($this->privates['foo3'] ?? $this->privates['foo3'] = new \stdClass()); + $instance->foo3 = ($this->privates['foo3'] ?? ($this->privates['foo3'] = new \stdClass())); return $instance; } diff --git a/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php b/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php index f9e581c9a6795..2a36fbea5aea3 100644 --- a/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php @@ -134,19 +134,24 @@ public function dispatch($eventName, Event $event = null) } $this->preProcess($eventName); - $this->preDispatch($eventName, $event); - - $e = $this->stopwatch->start($eventName, 'section'); - - $this->dispatcher->dispatch($eventName, $event); - - if ($e->isStarted()) { - $e->stop(); + try { + $this->preDispatch($eventName, $event); + try { + $e = $this->stopwatch->start($eventName, 'section'); + try { + $this->dispatcher->dispatch($eventName, $event); + } finally { + if ($e->isStarted()) { + $e->stop(); + } + } + } finally { + $this->postDispatch($eventName, $event); + } + } finally { + $this->postProcess($eventName); } - $this->postDispatch($eventName, $event); - $this->postProcess($eventName); - return $event; } diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php index 75502db0e9f56..95f44c86c7567 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php @@ -146,7 +146,7 @@ public function reverseTransform($value) return; } - if ('NaN' === $value) { + if (\in_array($value, array('NaN', 'NAN', 'nan'), true)) { throw new TransformationFailedException('"NaN" is not a valid number'); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FileType.php b/src/Symfony/Component/Form/Extension/Core/Type/FileType.php index 1b699d51e84ab..56fcfe17e9bf7 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FileType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FileType.php @@ -105,6 +105,7 @@ public function configureOptions(OptionsResolver $resolver) 'data_class' => $dataClass, 'empty_data' => $emptyData, 'multiple' => false, + 'allow_file_upload' => true, )); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php index b96d2d7a97f24..cbb9f7b54781f 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php @@ -179,6 +179,7 @@ public function configureOptions(OptionsResolver $resolver) 'attr' => array(), 'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.', 'upload_max_size_message' => $uploadMaxSizeMessage, // internal + 'allow_file_upload' => false, 'help' => null, )); diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index efbd259b4ef7b..85c448492e9a8 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -532,6 +532,11 @@ public function submit($submittedData, $clearMissing = true) $submittedData = null; } elseif (is_scalar($submittedData)) { $submittedData = (string) $submittedData; + } elseif ($this->config->getOption('allow_file_upload')) { + // no-op + } elseif ($this->config->getRequestHandler()->isFileUpload($submittedData)) { + $submittedData = null; + $this->transformationFailure = new TransformationFailedException('Submitted data was expected to be text or number, file upload given.'); } $dispatcher = $this->config->getEventDispatcher(); @@ -541,6 +546,10 @@ public function submit($submittedData, $clearMissing = true) $viewData = null; try { + if (null !== $this->transformationFailure) { + throw $this->transformationFailure; + } + // Hook to change content of the data submitted by the browser if ($dispatcher->hasListeners(FormEvents::PRE_SUBMIT)) { $event = new FormEvent($this, $submittedData); diff --git a/src/Symfony/Component/Form/Tests/CompoundFormTest.php b/src/Symfony/Component/Form/Tests/CompoundFormTest.php index 3128a544fc831..8b6982e454f2b 100644 --- a/src/Symfony/Component/Form/Tests/CompoundFormTest.php +++ b/src/Symfony/Component/Form/Tests/CompoundFormTest.php @@ -709,7 +709,7 @@ public function testSubmitPostOrPutRequestWithSingleChildForm($method) 'REQUEST_METHOD' => $method, )); - $form = $this->getBuilder('image') + $form = $this->getBuilder('image', null, null, array('allow_file_upload' => true)) ->setMethod($method) ->setRequestHandler(new HttpFoundationRequestHandler()) ->getForm(); @@ -1039,6 +1039,21 @@ public function testDisabledButtonIsNotSubmitted() $this->assertFalse($submit->isSubmitted()); } + public function testFileUpload() + { + $reqHandler = new HttpFoundationRequestHandler(); + $this->form->add($this->getBuilder('foo')->setRequestHandler($reqHandler)->getForm()); + $this->form->add($this->getBuilder('bar')->setRequestHandler($reqHandler)->getForm()); + + $this->form->submit(array( + 'foo' => 'Foo', + 'bar' => new UploadedFile(__FILE__, 'upload.png', 'image/png', UPLOAD_ERR_OK), + )); + + $this->assertSame('Submitted data was expected to be text or number, file upload given.', $this->form->get('bar')->getTransformationFailure()->getMessage()); + $this->assertNull($this->form->get('bar')->getData()); + } + protected function createForm() { return $this->getBuilder() diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php index d6a4662102232..176d3a9a58f9e 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php @@ -514,24 +514,24 @@ public function testReverseTransformExpectsValidNumber() /** * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException + * @dataProvider nanRepresentationProvider * * @see https://github.com/symfony/symfony/issues/3161 */ - public function testReverseTransformDisallowsNaN() + public function testReverseTransformDisallowsNaN($nan) { $transformer = new NumberToLocalizedStringTransformer(); - $transformer->reverseTransform('NaN'); + $transformer->reverseTransform($nan); } - /** - * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException - */ - public function testReverseTransformDisallowsNaN2() + public function nanRepresentationProvider() { - $transformer = new NumberToLocalizedStringTransformer(); - - $transformer->reverseTransform('nan'); + return array( + array('nan'), + array('NaN'), // see https://github.com/symfony/symfony/issues/3161 + array('NAN'), + ); } /** diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json index 0f43316313f63..f3c1bf4fbb037 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.json @@ -28,6 +28,7 @@ "parent": { "Symfony\\Component\\Form\\Extension\\Core\\Type\\FormType": [ "action", + "allow_file_upload", "attr", "auto_initialize", "block_name", diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt index 34120481edb44..c6c58f68fedd8 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_1.txt @@ -8,15 +8,16 @@ Symfony\Component\Form\Extension\Core\Type\ChoiceType (Block prefix: "choice") choice_attr FormType FormType FormTypeCsrfExtension choice_label -------------------- ------------------------- ----------------------- choice_loader compound action csrf_field_name - choice_name data_class attr csrf_message - choice_translation_domain empty_data auto_initialize csrf_protection - choice_value error_bubbling block_name csrf_token_id - choices trim by_reference csrf_token_manager - expanded data - group_by disabled - multiple help - placeholder inherit_data - preferred_choices label + choice_name data_class allow_file_upload csrf_message + choice_translation_domain empty_data attr csrf_protection + choice_value error_bubbling auto_initialize csrf_token_id + choices trim block_name csrf_token_manager + expanded by_reference + group_by data + multiple disabled + placeholder help + preferred_choices inherit_data + label label_attr label_format mapped diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_2.json b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_2.json index b0735e7248687..98828a4e6f034 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_2.json +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_2.json @@ -4,6 +4,7 @@ "options": { "own": [ "action", + "allow_file_upload", "attr", "auto_initialize", "block_name", diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_2.txt b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_2.txt index 78a4db0e684f7..ec1d372c7d7c9 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_2.txt +++ b/src/Symfony/Component/Form/Tests/Fixtures/Descriptor/resolved_form_type_2.txt @@ -6,6 +6,7 @@ Symfony\Component\Form\Extension\Core\Type\FormType (Block prefix: "form") Options ------------------------- action + allow_file_upload attr auto_initialize block_name diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index 06d210056c99d..2b713374f4216 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -248,6 +248,9 @@ private function handleException(\Exception $e, Request $request, int $type): Re } } + /** + * Returns a human-readable string for the specified variable. + */ private function varToString($var): string { if (\is_object($var)) { diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 244f664a6454c..1070b64cb96d2 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -63,11 +63,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private $requestStackSize = 0; private $resetServices = false; - const VERSION = '4.1.8'; - const VERSION_ID = 40108; + const VERSION = '4.1.9'; + const VERSION_ID = 40109; const MAJOR_VERSION = 4; const MINOR_VERSION = 1; - const RELEASE_VERSION = 8; + const RELEASE_VERSION = 9; const EXTRA_VERSION = ''; const END_OF_MAINTENANCE = '01/2019'; diff --git a/src/Symfony/Component/Intl/NumberFormatter/NumberFormatter.php b/src/Symfony/Component/Intl/NumberFormatter/NumberFormatter.php index d8a274d4a812e..574425eb457f3 100644 --- a/src/Symfony/Component/Intl/NumberFormatter/NumberFormatter.php +++ b/src/Symfony/Component/Intl/NumberFormatter/NumberFormatter.php @@ -638,8 +638,8 @@ public function setSymbol($attr, $value) /** * Not supported. Set a text attribute. * - * @param int $attr An attribute specifier, one of the text attribute constants - * @param int $value The attribute value + * @param int $attr An attribute specifier, one of the text attribute constants + * @param string $value The attribute value * * @return bool true on success or false on failure * diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 5efbe2a906ea4..2db7357d6488a 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -587,7 +587,8 @@ private function writeCollection($zval, $property, $collection, $addMethod, $rem */ private function getWriteAccessInfo(string $class, string $property, $value): array { - $key = str_replace('\\', '.', $class).'..'.$property; + $useAdderAndRemover = \is_array($value) || $value instanceof \Traversable; + $key = str_replace('\\', '.', $class).'..'.$property.'..'.(int) $useAdderAndRemover; if (isset($this->writePropertyCache[$key])) { return $this->writePropertyCache[$key]; @@ -607,6 +608,16 @@ private function getWriteAccessInfo(string $class, string $property, $value): ar $camelized = $this->camelize($property); $singulars = (array) Inflector::singularize($camelized); + if ($useAdderAndRemover) { + $methods = $this->findAdderAndRemover($reflClass, $singulars); + + if (null !== $methods) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER; + $access[self::ACCESS_ADDER] = $methods[0]; + $access[self::ACCESS_REMOVER] = $methods[1]; + } + } + if (!isset($access[self::ACCESS_TYPE])) { $setter = 'set'.$camelized; $getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item) @@ -628,22 +639,16 @@ private function getWriteAccessInfo(string $class, string $property, $value): ar $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC; $access[self::ACCESS_NAME] = $setter; } elseif (null !== $methods = $this->findAdderAndRemover($reflClass, $singulars)) { - if (\is_array($value) || $value instanceof \Traversable) { - $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER; - $access[self::ACCESS_ADDER] = $methods[0]; - $access[self::ACCESS_REMOVER] = $methods[1]; - } else { - $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND; - $access[self::ACCESS_NAME] = sprintf( - 'The property "%s" in class "%s" can be defined with the methods "%s()" but '. - 'the new value must be an array or an instance of \Traversable, '. - '"%s" given.', - $property, - $reflClass->name, - implode('()", "', $methods), - \is_object($value) ? \get_class($value) : \gettype($value) - ); - } + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND; + $access[self::ACCESS_NAME] = sprintf( + 'The property "%s" in class "%s" can be defined with the methods "%s()" but '. + 'the new value must be an array or an instance of \Traversable, '. + '"%s" given.', + $property, + $reflClass->name, + implode('()", "', $methods), + \is_object($value) ? \get_class($value) : \gettype($value) + ); } else { $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND; $access[self::ACCESS_NAME] = sprintf( @@ -680,6 +685,18 @@ private function isPropertyWritable($object, string $property): bool $access = $this->getWriteAccessInfo(\get_class($object), $property, array()); + $isWritable = self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE] + || self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE] + || self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE] + || (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) + || self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]; + + if ($isWritable) { + return true; + } + + $access = $this->getWriteAccessInfo(\get_class($object), $property, ''); + return self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE] || self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE] || self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE] diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index e2918a9b4efcb..d6bf88372cc7a 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -708,7 +708,27 @@ public function testWriteToPluralPropertyWhileSingularOneExists() $this->propertyAccessor->isWritable($object, 'emails'); //cache access info $this->propertyAccessor->setValue($object, 'emails', array('test@email.com')); - self::assertEquals(array('test@email.com'), $object->getEmails()); - self::assertNull($object->getEmail()); + $this->assertEquals(array('test@email.com'), $object->getEmails()); + $this->assertNull($object->getEmail()); + } + + public function testAdderAndRemoverArePreferredOverSetter() + { + $object = new TestPluralAdderRemoverAndSetter(); + + $this->propertyAccessor->isWritable($object, 'emails'); //cache access info + $this->propertyAccessor->setValue($object, 'emails', array('test@email.com')); + + $this->assertEquals(array('test@email.com'), $object->getEmails()); + } + + public function testAdderAndRemoverArePreferredOverSetterForSameSingularAndPlural() + { + $object = new TestPluralAdderRemoverAndSetterSameSingularAndPlural(); + + $this->propertyAccessor->isWritable($object, 'aircraft'); //cache access info + $this->propertyAccessor->setValue($object, 'aircraft', array('aeroplane')); + + $this->assertEquals(array('aeroplane'), $object->getAircraft()); } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/TestPluralAdderRemoverAndSetter.php b/src/Symfony/Component/PropertyAccess/Tests/TestPluralAdderRemoverAndSetter.php new file mode 100644 index 0000000000000..ecb3f9b4a9d32 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/TestPluralAdderRemoverAndSetter.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests; + +class TestPluralAdderRemoverAndSetter +{ + private $emails = array(); + + public function getEmails() + { + return $this->emails; + } + + public function setEmails(array $emails) + { + $this->emails = array('foo@email.com'); + } + + public function addEmail($email) + { + $this->emails[] = $email; + } + + public function removeEmail($email) + { + $this->emails = array_diff($this->emails, array($email)); + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/TestPluralAdderRemoverAndSetterSameSingularAndPlural.php b/src/Symfony/Component/PropertyAccess/Tests/TestPluralAdderRemoverAndSetterSameSingularAndPlural.php new file mode 100644 index 0000000000000..bb3b4f4688dc5 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/TestPluralAdderRemoverAndSetterSameSingularAndPlural.php @@ -0,0 +1,28 @@ +aircraft; + } + + public function setAircraft(array $aircraft) + { + $this->aircraft = array('plane'); + } + + public function addAircraft($aircraft) + { + $this->aircraft[] = $aircraft; + } + + public function removeAircraft($aircraft) + { + $this->aircraft = array_diff($this->aircraft, array($aircraft)); + } +} diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php index 4cb1c7429bb10..0dc0a178a8542 100644 --- a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php +++ b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php @@ -161,10 +161,10 @@ public function match($pathinfo) throw new ResourceNotFoundException(); } - private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): ?array + private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): array EOF - .$code."\n return null;\n }"; + .$code."\n return array();\n }"; } return " public function match(\$rawPathinfo)\n".$code."\n throw \$allow ? new MethodNotAllowedException(array_keys(\$allow)) : new ResourceNotFoundException();\n }"; @@ -550,10 +550,23 @@ private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \st private function compileSwitchDefault(bool $hasVars, bool $matchHost): string { $code = sprintf(" - if ('/' !== \$pathinfo && \$hasTrailingSlash !== ('/' === \$pathinfo[-1])) { - %s; + if ('/' !== \$pathinfo) {%s + if (\$hasTrailingSlash !== ('/' === \$pathinfo[-1])) {%s + break; + } }\n", - $this->supportsRedirections ? 'return null' : 'break' + $hasVars ? " + if ('/' === \$pathinfo[-1]) { + if (preg_match(\$regex, substr(\$pathinfo, 0, -1), \$n) && \$m === (int) \$n['MARK']) { + \$matches = \$n; + } else { + \$hasTrailingSlash = true; + } + }\n" : '', + $this->supportsRedirections ? " + if ((!\$requiredMethods || isset(\$requiredMethods['GET'])) && 'GET' === \$canonicalMethod) { + return \$allow = \$allowSchemes = array(); + }" : '' ); if ($hasVars) { @@ -610,26 +623,52 @@ private function compileSwitchDefault(bool $hasVars, bool $matchHost): string */ private function compileRoute(Route $route, string $name, bool $checkHost, bool $hasTrailingSlash): string { + $compiledRoute = $route->compile(); + $conditions = array(); + $matches = (bool) $compiledRoute->getPathVariables(); + $hostMatches = (bool) $compiledRoute->getHostVariables(); + $methods = array_flip($route->getMethods()); + $gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name); $code = " // $name"; - if ('/' !== $route->getPath()) { + if ('/' === $route->getPath()) { + $code .= "\n"; + } elseif (!$matches) { $code .= sprintf(" - if ('/' !== \$pathinfo && '/' %s \$pathinfo[-1]) { - %s; - }\n", + if ('/' !== \$pathinfo && '/' %s \$pathinfo[-1]) {%s + goto $gotoname; + }\n\n", $hasTrailingSlash ? '!==' : '===', - $this->supportsRedirections ? 'return null' : 'break' + $this->supportsRedirections && (!$methods || isset($methods['GET'])) ? " + if ('GET' === \$canonicalMethod) { + return \$allow = \$allowSchemes = array(); + }" : '' + ); + } elseif ($hasTrailingSlash) { + $code .= sprintf(" + if ('/' !== \$pathinfo[-1]) {%s + goto $gotoname; + } + if ('/' !== \$pathinfo && preg_match(\$regex, substr(\$pathinfo, 0, -1), \$n) && \$m === (int) \$n['MARK']) { + \$matches = \$n; + }\n\n", + $this->supportsRedirections && (!$methods || isset($methods['GET'])) ? " + if ('GET' === \$canonicalMethod) { + return \$allow = \$allowSchemes = array(); + }" : '' ); } else { - $code .= "\n"; + $code .= sprintf(" + if ('/' !== \$pathinfo && '/' === \$pathinfo[-1] && preg_match(\$regex, substr(\$pathinfo, 0, -1), \$n) && \$m === (int) \$n['MARK']) {%s + goto $gotoname; + }\n\n", + $this->supportsRedirections && (!$methods || isset($methods['GET'])) ? " + if ('GET' === \$canonicalMethod) { + return \$allow = \$allowSchemes = array(); + }" : '' + ); } - $compiledRoute = $route->compile(); - $conditions = array(); - $matches = (bool) $compiledRoute->getPathVariables(); - $hostMatches = (bool) $compiledRoute->getHostVariables(); - $methods = array_flip($route->getMethods()); - if ($route->getCondition()) { $expression = $this->getExpressionLanguage()->compile($route->getCondition(), array('context', 'request')); @@ -658,8 +697,6 @@ private function compileRoute(Route $route, string $name, bool $checkHost, bool $code = $this->indent($code); } - $gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name); - // the offset where the return value is appended below, with indendation $retOffset = 12 + \strlen($code); $defaults = $route->getDefaults(); @@ -741,16 +778,10 @@ private function compileRoute(Route $route, string $name, bool $checkHost, bool $code = substr_replace($code, 'return', $retOffset, 6); } if ($conditions) { - $code .= " }\n"; - } elseif ($schemes || $methods) { - $code .= ' '; - } - - if ($schemes || $methods) { - $code .= " $gotoname:\n"; + $code = $this->indent($code)." }\n"; } - return $conditions ? $this->indent($code) : $code; + return $code." $gotoname:\n"; } private function getExpressionLanguage() diff --git a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php index f2b353dfeb140..f8379b41c5153 100644 --- a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php @@ -130,21 +130,26 @@ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterfac */ protected function matchCollection($pathinfo, RouteCollection $routes) { + // HEAD and GET are equivalent as per RFC + if ('HEAD' === $method = $this->context->getMethod()) { + $method = 'GET'; + } $supportsTrailingSlash = '/' !== $pathinfo && '' !== $pathinfo && $this instanceof RedirectableUrlMatcherInterface; foreach ($routes as $name => $route) { $compiledRoute = $route->compile(); $staticPrefix = $compiledRoute->getStaticPrefix(); + $requiredMethods = $route->getMethods(); // check the static prefix of the URL first. Only use the more expensive preg_match when it matches if ('' === $staticPrefix || 0 === strpos($pathinfo, $staticPrefix)) { // no-op - } elseif (!$supportsTrailingSlash) { + } elseif (!$supportsTrailingSlash || ($requiredMethods && !\in_array('GET', $requiredMethods)) || 'GET' !== $method) { continue; } elseif ('/' === $staticPrefix[-1] && substr($staticPrefix, 0, -1) === $pathinfo) { - return; + return $this->allow = $this->allowSchemes = array(); } elseif ('/' === $pathinfo[-1] && substr($pathinfo, 0, -1) === $staticPrefix) { - return; + return $this->allow = $this->allowSchemes = array(); } else { continue; } @@ -160,8 +165,20 @@ protected function matchCollection($pathinfo, RouteCollection $routes) continue; } - if ($supportsTrailingSlash && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - return; + if ($supportsTrailingSlash) { + if ('/' === $pathinfo[-1]) { + if (preg_match($regex, substr($pathinfo, 0, -1), $m)) { + $matches = $m; + } else { + $hasTrailingSlash = true; + } + } + if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { + if ((!$requiredMethods || \in_array('GET', $requiredMethods)) && 'GET' === $method) { + return $this->allow = $this->allowSchemes = array(); + } + continue; + } } $hostMatches = array(); @@ -176,12 +193,7 @@ protected function matchCollection($pathinfo, RouteCollection $routes) } $hasRequiredScheme = !$route->getSchemes() || $route->hasScheme($this->context->getScheme()); - if ($requiredMethods = $route->getMethods()) { - // HEAD and GET are equivalent as per RFC - if ('HEAD' === $method = $this->context->getMethod()) { - $method = 'GET'; - } - + if ($requiredMethods) { if (!\in_array($method, $requiredMethods)) { if ($hasRequiredScheme) { $this->allow = array_merge($this->allow, $requiredMethods); @@ -199,6 +211,8 @@ protected function matchCollection($pathinfo, RouteCollection $routes) return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, isset($status[1]) ? $status[1] : array())); } + + return array(); } /** diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php index 531d1ecabae75..c5074514e86e4 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php @@ -54,8 +54,10 @@ public function match($rawPathinfo) } list($ret, $requiredHost, $requiredMethods, $requiredSchemes, $hasTrailingSlash) = $routes[$trimmedPathinfo]; - if ('/' !== $pathinfo && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - break; + if ('/' !== $pathinfo) { + if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { + break; + } } if ($requiredHost) { @@ -140,15 +142,24 @@ public function match($rawPathinfo) $matches = array('foo' => $matches[1] ?? null); // baz4 - if ('/' !== $pathinfo && '/' !== $pathinfo[-1]) { - break; + if ('/' !== $pathinfo[-1]) { + goto not_baz4; } + if ('/' !== $pathinfo && preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + $matches = $n; + } + return $this->mergeDefaults(array('_route' => 'baz4') + $matches, array()); + not_baz4: // baz5 - if ('/' !== $pathinfo && '/' !== $pathinfo[-1]) { - break; + if ('/' !== $pathinfo[-1]) { + goto not_baz5; + } + if ('/' !== $pathinfo && preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + $matches = $n; } + $ret = $this->mergeDefaults(array('_route' => 'baz5') + $matches, array()); if (!isset(($a = array('POST' => 0))[$requestMethod])) { $allow += $a; @@ -159,9 +170,13 @@ public function match($rawPathinfo) not_baz5: // baz.baz6 - if ('/' !== $pathinfo && '/' !== $pathinfo[-1]) { - break; + if ('/' !== $pathinfo[-1]) { + goto not_bazbaz6; + } + if ('/' !== $pathinfo && preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + $matches = $n; } + $ret = $this->mergeDefaults(array('_route' => 'baz.baz6') + $matches, array()); if (!isset(($a = array('PUT' => 0))[$requestMethod])) { $allow += $a; @@ -176,9 +191,10 @@ public function match($rawPathinfo) $matches = array('foo' => $matches[1] ?? null); // foo1 - if ('/' !== $pathinfo && '/' === $pathinfo[-1]) { - break; + if ('/' !== $pathinfo && '/' === $pathinfo[-1] && preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + goto not_foo1; } + $ret = $this->mergeDefaults(array('_route' => 'foo1') + $matches, array()); if (!isset(($a = array('PUT' => 0))[$requestMethod])) { $allow += $a; @@ -193,20 +209,24 @@ public function match($rawPathinfo) $matches = array('foo1' => $matches[1] ?? null); // foo2 - if ('/' !== $pathinfo && '/' === $pathinfo[-1]) { - break; + if ('/' !== $pathinfo && '/' === $pathinfo[-1] && preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + goto not_foo2; } + return $this->mergeDefaults(array('_route' => 'foo2') + $matches, array()); + not_foo2: break; case 279: $matches = array('_locale' => $matches[1] ?? null, 'foo' => $matches[2] ?? null); // foo3 - if ('/' !== $pathinfo && '/' === $pathinfo[-1]) { - break; + if ('/' !== $pathinfo && '/' === $pathinfo[-1] && preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + goto not_foo3; } + return $this->mergeDefaults(array('_route' => 'foo3') + $matches, array()); + not_foo3: break; default: @@ -232,8 +252,18 @@ public function match($rawPathinfo) list($ret, $vars, $requiredMethods, $requiredSchemes, $hasTrailingSlash) = $routes[$m]; - if ('/' !== $pathinfo && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - break; + if ('/' !== $pathinfo) { + if ('/' === $pathinfo[-1]) { + if (preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + $matches = $n; + } else { + $hasTrailingSlash = true; + } + } + + if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { + break; + } } foreach ($vars as $i => $v) { diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher10.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher10.php index eee7e080d89fd..ab907b2393c91 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher10.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher10.php @@ -2793,8 +2793,18 @@ public function match($rawPathinfo) list($ret, $vars, $requiredMethods, $requiredSchemes, $hasTrailingSlash) = $routes[$m]; - if ('/' !== $pathinfo && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - break; + if ('/' !== $pathinfo) { + if ('/' === $pathinfo[-1]) { + if (preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + $matches = $n; + } else { + $hasTrailingSlash = true; + } + } + + if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { + break; + } } foreach ($vars as $i => $v) { diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher11.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher11.php index 8f3f0599f2eb4..5621ec968c13e 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher11.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher11.php @@ -50,7 +50,7 @@ public function match($pathinfo) throw new ResourceNotFoundException(); } - private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): ?array + private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): array { $allow = $allowSchemes = array(); $pathinfo = rawurldecode($rawPathinfo) ?: '/'; @@ -118,8 +118,21 @@ private function doMatch(string $rawPathinfo, array &$allow = array(), array &$a list($ret, $vars, $requiredMethods, $requiredSchemes, $hasTrailingSlash) = $routes[$m]; - if ('/' !== $pathinfo && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - return null; + if ('/' !== $pathinfo) { + if ('/' === $pathinfo[-1]) { + if (preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + $matches = $n; + } else { + $hasTrailingSlash = true; + } + } + + if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { + if ((!$requiredMethods || isset($requiredMethods['GET'])) && 'GET' === $canonicalMethod) { + return $allow = $allowSchemes = array(); + } + break; + } } foreach ($vars as $i => $v) { @@ -154,6 +167,6 @@ private function doMatch(string $rawPathinfo, array &$allow = array(), array &$a throw new Symfony\Component\Routing\Exception\NoConfigurationException(); } - return null; + return array(); } } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher12.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher12.php index 406e13336a6cb..d92394f5a6f52 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher12.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher12.php @@ -63,8 +63,18 @@ public function match($rawPathinfo) list($ret, $vars, $requiredMethods, $requiredSchemes, $hasTrailingSlash) = $routes[$m]; - if ('/' !== $pathinfo && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - break; + if ('/' !== $pathinfo) { + if ('/' === $pathinfo[-1]) { + if (preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + $matches = $n; + } else { + $hasTrailingSlash = true; + } + } + + if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { + break; + } } foreach ($vars as $i => $v) { diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher13.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher13.php index 4965615b43df2..23c9327857175 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher13.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher13.php @@ -45,16 +45,20 @@ public function match($rawPathinfo) $matches = array('foo' => $matches[1] ?? null, 'foo' => $matches[2] ?? null); // r1 - if ('/' !== $pathinfo && '/' === $pathinfo[-1]) { - break; + if ('/' !== $pathinfo && '/' === $pathinfo[-1] && preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + goto not_r1; } + return $this->mergeDefaults(array('_route' => 'r1') + $matches, array()); + not_r1: // r2 - if ('/' !== $pathinfo && '/' === $pathinfo[-1]) { - break; + if ('/' !== $pathinfo && '/' === $pathinfo[-1] && preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + goto not_r2; } + return $this->mergeDefaults(array('_route' => 'r2') + $matches, array()); + not_r2: break; } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php index fc08f2b73e946..d8d0e3bc372ab 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php @@ -50,7 +50,7 @@ public function match($pathinfo) throw new ResourceNotFoundException(); } - private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): ?array + private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): array { $allow = $allowSchemes = array(); $pathinfo = rawurldecode($rawPathinfo) ?: '/'; @@ -91,8 +91,13 @@ private function doMatch(string $rawPathinfo, array &$allow = array(), array &$a } list($ret, $requiredHost, $requiredMethods, $requiredSchemes, $hasTrailingSlash) = $routes[$trimmedPathinfo]; - if ('/' !== $pathinfo && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - return null; + if ('/' !== $pathinfo) { + if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { + if ((!$requiredMethods || isset($requiredMethods['GET'])) && 'GET' === $canonicalMethod) { + return $allow = $allowSchemes = array(); + } + break; + } } if ($requiredHost) { @@ -177,15 +182,27 @@ private function doMatch(string $rawPathinfo, array &$allow = array(), array &$a $matches = array('foo' => $matches[1] ?? null); // baz4 - if ('/' !== $pathinfo && '/' !== $pathinfo[-1]) { - return null; + if ('/' !== $pathinfo[-1]) { + if ('GET' === $canonicalMethod) { + return $allow = $allowSchemes = array(); + } + goto not_baz4; + } + if ('/' !== $pathinfo && preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + $matches = $n; } + return $this->mergeDefaults(array('_route' => 'baz4') + $matches, array()); + not_baz4: // baz5 - if ('/' !== $pathinfo && '/' !== $pathinfo[-1]) { - return null; + if ('/' !== $pathinfo[-1]) { + goto not_baz5; + } + if ('/' !== $pathinfo && preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + $matches = $n; } + $ret = $this->mergeDefaults(array('_route' => 'baz5') + $matches, array()); if (!isset(($a = array('POST' => 0))[$requestMethod])) { $allow += $a; @@ -196,9 +213,13 @@ private function doMatch(string $rawPathinfo, array &$allow = array(), array &$a not_baz5: // baz.baz6 - if ('/' !== $pathinfo && '/' !== $pathinfo[-1]) { - return null; + if ('/' !== $pathinfo[-1]) { + goto not_bazbaz6; + } + if ('/' !== $pathinfo && preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + $matches = $n; } + $ret = $this->mergeDefaults(array('_route' => 'baz.baz6') + $matches, array()); if (!isset(($a = array('PUT' => 0))[$requestMethod])) { $allow += $a; @@ -213,9 +234,10 @@ private function doMatch(string $rawPathinfo, array &$allow = array(), array &$a $matches = array('foo' => $matches[1] ?? null); // foo1 - if ('/' !== $pathinfo && '/' === $pathinfo[-1]) { - return null; + if ('/' !== $pathinfo && '/' === $pathinfo[-1] && preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + goto not_foo1; } + $ret = $this->mergeDefaults(array('_route' => 'foo1') + $matches, array()); if (!isset(($a = array('PUT' => 0))[$requestMethod])) { $allow += $a; @@ -230,20 +252,30 @@ private function doMatch(string $rawPathinfo, array &$allow = array(), array &$a $matches = array('foo1' => $matches[1] ?? null); // foo2 - if ('/' !== $pathinfo && '/' === $pathinfo[-1]) { - return null; + if ('/' !== $pathinfo && '/' === $pathinfo[-1] && preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + if ('GET' === $canonicalMethod) { + return $allow = $allowSchemes = array(); + } + goto not_foo2; } + return $this->mergeDefaults(array('_route' => 'foo2') + $matches, array()); + not_foo2: break; case 279: $matches = array('_locale' => $matches[1] ?? null, 'foo' => $matches[2] ?? null); // foo3 - if ('/' !== $pathinfo && '/' === $pathinfo[-1]) { - return null; + if ('/' !== $pathinfo && '/' === $pathinfo[-1] && preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + if ('GET' === $canonicalMethod) { + return $allow = $allowSchemes = array(); + } + goto not_foo3; } + return $this->mergeDefaults(array('_route' => 'foo3') + $matches, array()); + not_foo3: break; default: @@ -269,8 +301,21 @@ private function doMatch(string $rawPathinfo, array &$allow = array(), array &$a list($ret, $vars, $requiredMethods, $requiredSchemes, $hasTrailingSlash) = $routes[$m]; - if ('/' !== $pathinfo && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - return null; + if ('/' !== $pathinfo) { + if ('/' === $pathinfo[-1]) { + if (preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + $matches = $n; + } else { + $hasTrailingSlash = true; + } + } + + if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { + if ((!$requiredMethods || isset($requiredMethods['GET'])) && 'GET' === $canonicalMethod) { + return $allow = $allowSchemes = array(); + } + break; + } } foreach ($vars as $i => $v) { @@ -305,6 +350,6 @@ private function doMatch(string $rawPathinfo, array &$allow = array(), array &$a throw new Symfony\Component\Routing\Exception\NoConfigurationException(); } - return null; + return array(); } } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher3.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher3.php index 291be1475760a..a6d11f6b7e501 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher3.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher3.php @@ -30,11 +30,13 @@ public function match($rawPathinfo) case '/with-condition': // with-condition if ('/' !== $pathinfo && '/' === $pathinfo[-1]) { - break; + goto not_withcondition; } + if (($context->getMethod() == "GET")) { return array('_route' => 'with-condition'); } + not_withcondition: break; default: $routes = array( @@ -46,8 +48,10 @@ public function match($rawPathinfo) } list($ret, $requiredHost, $requiredMethods, $requiredSchemes, $hasTrailingSlash) = $routes[$trimmedPathinfo]; - if ('/' !== $pathinfo && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - break; + if ('/' !== $pathinfo) { + if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { + break; + } } $hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]); @@ -82,8 +86,18 @@ public function match($rawPathinfo) list($ret, $vars, $requiredMethods, $requiredSchemes, $hasTrailingSlash) = $routes[$m]; - if ('/' !== $pathinfo && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - break; + if ('/' !== $pathinfo) { + if ('/' === $pathinfo[-1]) { + if (preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + $matches = $n; + } else { + $hasTrailingSlash = true; + } + } + + if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { + break; + } } foreach ($vars as $i => $v) { diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher4.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher4.php index 6bec91e309735..97ace0d51063e 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher4.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher4.php @@ -30,8 +30,9 @@ public function match($rawPathinfo) case '/put_and_post': // put_and_post if ('/' !== $pathinfo && '/' === $pathinfo[-1]) { - break; + goto not_put_and_post; } + $ret = array('_route' => 'put_and_post'); if (!isset(($a = array('PUT' => 0, 'POST' => 1))[$requestMethod])) { $allow += $a; @@ -42,8 +43,9 @@ public function match($rawPathinfo) not_put_and_post: // put_and_get_and_head if ('/' !== $pathinfo && '/' === $pathinfo[-1]) { - break; + goto not_put_and_get_and_head; } + $ret = array('_route' => 'put_and_get_and_head'); if (!isset(($a = array('PUT' => 0, 'GET' => 1, 'HEAD' => 2))[$canonicalMethod])) { $allow += $a; @@ -66,8 +68,10 @@ public function match($rawPathinfo) } list($ret, $requiredHost, $requiredMethods, $requiredSchemes, $hasTrailingSlash) = $routes[$trimmedPathinfo]; - if ('/' !== $pathinfo && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - break; + if ('/' !== $pathinfo) { + if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { + break; + } } $hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]); diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher5.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher5.php index dee606f0a9bfa..c8d7b40e951c9 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher5.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher5.php @@ -50,7 +50,7 @@ public function match($pathinfo) throw new ResourceNotFoundException(); } - private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): ?array + private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): array { $allow = $allowSchemes = array(); $pathinfo = rawurldecode($rawPathinfo) ?: '/'; @@ -83,8 +83,13 @@ private function doMatch(string $rawPathinfo, array &$allow = array(), array &$a } list($ret, $requiredHost, $requiredMethods, $requiredSchemes, $hasTrailingSlash) = $routes[$trimmedPathinfo]; - if ('/' !== $pathinfo && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - return null; + if ('/' !== $pathinfo) { + if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { + if ((!$requiredMethods || isset($requiredMethods['GET'])) && 'GET' === $canonicalMethod) { + return $allow = $allowSchemes = array(); + } + break; + } } $hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]); @@ -121,8 +126,21 @@ private function doMatch(string $rawPathinfo, array &$allow = array(), array &$a list($ret, $vars, $requiredMethods, $requiredSchemes, $hasTrailingSlash) = $routes[$m]; - if ('/' !== $pathinfo && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - return null; + if ('/' !== $pathinfo) { + if ('/' === $pathinfo[-1]) { + if (preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + $matches = $n; + } else { + $hasTrailingSlash = true; + } + } + + if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { + if ((!$requiredMethods || isset($requiredMethods['GET'])) && 'GET' === $canonicalMethod) { + return $allow = $allowSchemes = array(); + } + break; + } } foreach ($vars as $i => $v) { @@ -157,6 +175,6 @@ private function doMatch(string $rawPathinfo, array &$allow = array(), array &$a throw new Symfony\Component\Routing\Exception\NoConfigurationException(); } - return null; + return array(); } } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher6.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher6.php index 839481ba2db2d..749ebd1183353 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher6.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher6.php @@ -44,8 +44,10 @@ public function match($rawPathinfo) } list($ret, $requiredHost, $requiredMethods, $requiredSchemes, $hasTrailingSlash) = $routes[$trimmedPathinfo]; - if ('/' !== $pathinfo && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - break; + if ('/' !== $pathinfo) { + if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { + break; + } } $hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]); @@ -98,8 +100,18 @@ public function match($rawPathinfo) list($ret, $vars, $requiredMethods, $requiredSchemes, $hasTrailingSlash) = $routes[$m]; - if ('/' !== $pathinfo && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - break; + if ('/' !== $pathinfo) { + if ('/' === $pathinfo[-1]) { + if (preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + $matches = $n; + } else { + $hasTrailingSlash = true; + } + } + + if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { + break; + } } foreach ($vars as $i => $v) { diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher7.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher7.php index 07c8af66697ef..b3513bc3f7323 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher7.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher7.php @@ -50,7 +50,7 @@ public function match($pathinfo) throw new ResourceNotFoundException(); } - private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): ?array + private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): array { $allow = $allowSchemes = array(); $pathinfo = rawurldecode($rawPathinfo) ?: '/'; @@ -79,8 +79,13 @@ private function doMatch(string $rawPathinfo, array &$allow = array(), array &$a } list($ret, $requiredHost, $requiredMethods, $requiredSchemes, $hasTrailingSlash) = $routes[$trimmedPathinfo]; - if ('/' !== $pathinfo && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - return null; + if ('/' !== $pathinfo) { + if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { + if ((!$requiredMethods || isset($requiredMethods['GET'])) && 'GET' === $canonicalMethod) { + return $allow = $allowSchemes = array(); + } + break; + } } $hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]); @@ -133,8 +138,21 @@ private function doMatch(string $rawPathinfo, array &$allow = array(), array &$a list($ret, $vars, $requiredMethods, $requiredSchemes, $hasTrailingSlash) = $routes[$m]; - if ('/' !== $pathinfo && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - return null; + if ('/' !== $pathinfo) { + if ('/' === $pathinfo[-1]) { + if (preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + $matches = $n; + } else { + $hasTrailingSlash = true; + } + } + + if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { + if ((!$requiredMethods || isset($requiredMethods['GET'])) && 'GET' === $canonicalMethod) { + return $allow = $allowSchemes = array(); + } + break; + } } foreach ($vars as $i => $v) { @@ -169,6 +187,6 @@ private function doMatch(string $rawPathinfo, array &$allow = array(), array &$a throw new Symfony\Component\Routing\Exception\NoConfigurationException(); } - return null; + return array(); } } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher8.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher8.php index ec5127e7b3a89..f80eb4c9d87a1 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher8.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher8.php @@ -51,8 +51,18 @@ public function match($rawPathinfo) list($ret, $vars, $requiredMethods, $requiredSchemes, $hasTrailingSlash) = $routes[$m]; - if ('/' !== $pathinfo && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - break; + if ('/' !== $pathinfo) { + if ('/' === $pathinfo[-1]) { + if (preg_match($regex, substr($pathinfo, 0, -1), $n) && $m === (int) $n['MARK']) { + $matches = $n; + } else { + $hasTrailingSlash = true; + } + } + + if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { + break; + } } foreach ($vars as $i => $v) { diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher9.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher9.php index dbea6ab94fef7..8b567addeb8b0 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher9.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher9.php @@ -33,14 +33,17 @@ public function match($rawPathinfo) if (preg_match('#^(?P[^\\.]++)\\.e\\.c\\.b\\.a$#sDi', $host, $hostMatches)) { return $this->mergeDefaults(array('_route' => 'a') + $hostMatches, array()); } + not_a: // c if (preg_match('#^(?P[^\\.]++)\\.e\\.c\\.b\\.a$#sDi', $host, $hostMatches)) { return $this->mergeDefaults(array('_route' => 'c') + $hostMatches, array()); } + not_c: // b if ('d.c.b.a' === $host) { return array('_route' => 'b'); } + not_b: break; } diff --git a/src/Symfony/Component/Routing/Tests/Matcher/RedirectableUrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/RedirectableUrlMatcherTest.php index eb1703353b21e..f1d6b5325d0a5 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/RedirectableUrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/RedirectableUrlMatcherTest.php @@ -170,6 +170,23 @@ public function testMissingTrailingSlashAndScheme() $matcher->match('/foo'); } + public function testSlashAndVerbPrecedenceWithRedirection() + { + $coll = new RouteCollection(); + $coll->add('a', new Route('/api/customers/{customerId}/contactpersons', array(), array(), array(), '', array(), array('post'))); + $coll->add('b', new Route('/api/customers/{customerId}/contactpersons/', array(), array(), array(), '', array(), array('get'))); + + $matcher = $this->getUrlMatcher($coll); + $expected = array( + '_route' => 'b', + 'customerId' => '123', + ); + $this->assertEquals($expected, $matcher->match('/api/customers/123/contactpersons/')); + + $matcher->expects($this->once())->method('redirect')->with('/api/customers/123/contactpersons/')->willReturn(array()); + $this->assertEquals($expected, $matcher->match('/api/customers/123/contactpersons')); + } + protected function getUrlMatcher(RouteCollection $routes, RequestContext $context = null) { return $this->getMockForAbstractClass('Symfony\Component\Routing\Matcher\RedirectableUrlMatcher', array($routes, $context ?: new RequestContext())); diff --git a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php index c1add96271dc2..159f2725867d7 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php @@ -680,6 +680,59 @@ public function testHostWithDot() $this->assertEquals('b', $matcher->match('/bar/abc.123')['_route']); } + public function testSlashVariant() + { + $coll = new RouteCollection(); + $coll->add('a', new Route('/foo/{bar}', array(), array('bar' => '.*'))); + + $matcher = $this->getUrlMatcher($coll); + $this->assertEquals('a', $matcher->match('/foo/')['_route']); + } + + public function testSlashVariant2() + { + $coll = new RouteCollection(); + $coll->add('a', new Route('/foo/{bar}/', array(), array('bar' => '.*'))); + + $matcher = $this->getUrlMatcher($coll); + $this->assertEquals(array('_route' => 'a', 'bar' => 'bar'), $matcher->match('/foo/bar/')); + } + + public function testSlashWithVerb() + { + $coll = new RouteCollection(); + $coll->add('a', new Route('/{foo}', array(), array(), array(), '', array(), array('put', 'delete'))); + $coll->add('b', new Route('/bar/')); + + $matcher = $this->getUrlMatcher($coll); + $this->assertSame(array('_route' => 'b'), $matcher->match('/bar/')); + } + + public function testSlashAndVerbPrecedence() + { + $coll = new RouteCollection(); + $coll->add('a', new Route('/api/customers/{customerId}/contactpersons/', array(), array(), array(), '', array(), array('post'))); + $coll->add('b', new Route('/api/customers/{customerId}/contactpersons', array(), array(), array(), '', array(), array('get'))); + + $matcher = $this->getUrlMatcher($coll); + $expected = array( + '_route' => 'b', + 'customerId' => '123', + ); + $this->assertEquals($expected, $matcher->match('/api/customers/123/contactpersons')); + + $coll = new RouteCollection(); + $coll->add('a', new Route('/api/customers/{customerId}/contactpersons/', array(), array(), array(), '', array(), array('get'))); + $coll->add('b', new Route('/api/customers/{customerId}/contactpersons', array(), array(), array(), '', array(), array('post'))); + + $matcher = $this->getUrlMatcher($coll, new RequestContext('', 'POST')); + $expected = array( + '_route' => 'b', + 'customerId' => '123', + ); + $this->assertEquals($expected, $matcher->match('/api/customers/123/contactpersons')); + } + protected function getUrlMatcher(RouteCollection $routes, RequestContext $context = null) { return new UrlMatcher($routes, $context ?: new RequestContext()); diff --git a/src/Symfony/Component/Security/Http/HttpUtils.php b/src/Symfony/Component/Security/Http/HttpUtils.php index f334a6d5c4a92..82f6123c59a49 100644 --- a/src/Symfony/Component/Security/Http/HttpUtils.php +++ b/src/Symfony/Component/Security/Http/HttpUtils.php @@ -59,7 +59,7 @@ public function __construct(UrlGeneratorInterface $urlGenerator = null, $urlMatc */ public function createRedirectResponse(Request $request, $path, $status = 302) { - if (null !== $this->domainRegexp && preg_match('#^https?://[^/]++#i', $path, $host) && !preg_match(sprintf($this->domainRegexp, preg_quote($request->getHttpHost())), $host[0])) { + if (null !== $this->domainRegexp && preg_match('#^https?:[/\\\\]{2,}+[^/]++#i', $path, $host) && !preg_match(sprintf($this->domainRegexp, preg_quote($request->getHttpHost())), $host[0])) { $path = '/'; } diff --git a/src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php b/src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php index 28b242995d4bf..a0d6f79714414 100644 --- a/src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php +++ b/src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php @@ -54,14 +54,28 @@ public function testCreateRedirectResponseWithRequestsDomain() $this->assertTrue($response->isRedirect('http://localhost/blog')); } - public function testCreateRedirectResponseWithBadRequestsDomain() + /** + * @dataProvider badRequestDomainUrls + */ + public function testCreateRedirectResponseWithBadRequestsDomain($url) { $utils = new HttpUtils($this->getUrlGenerator(), null, '#^https?://%s$#i'); - $response = $utils->createRedirectResponse($this->getRequest(), 'http://pirate.net/foo'); + $response = $utils->createRedirectResponse($this->getRequest(), $url); $this->assertTrue($response->isRedirect('http://localhost/')); } + public function badRequestDomainUrls() + { + return array( + array('http://pirate.net/foo'), + array('http:\\\\pirate.net/foo'), + array('http:/\\pirate.net/foo'), + array('http:\\/pirate.net/foo'), + array('http://////pirate.net/foo'), + ); + } + public function testCreateRedirectResponseWithProtocolRelativeTarget() { $utils = new HttpUtils($this->getUrlGenerator(), null, '#^https?://%s$#i'); diff --git a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php index b2ebe97bd57e9..08380ddfe9e08 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php @@ -55,7 +55,8 @@ public function normalize($object, $format = null, array $context = array()) $timezone = $this->getTimezone($context); if (null !== $timezone) { - $object = (new \DateTimeImmutable('@'.$object->getTimestamp()))->setTimezone($timezone); + $object = clone $object; + $object = $object->setTimezone($timezone); } return $object->format($format); diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php index 178519b30e687..99b224996cb1b 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php @@ -78,6 +78,82 @@ public function normalizeUsingTimeZonePassedInContextProvider() yield array('2016-12-01T09:00:00+09:00', new \DateTimeImmutable('2016/12/01', new \DateTimeZone('UTC')), new \DateTimeZone('Japan')); } + /** + * @dataProvider normalizeUsingTimeZonePassedInContextAndExpectedFormatWithMicrosecondsProvider + */ + public function testNormalizeUsingTimeZonePassedInContextAndFormattedWithMicroseconds($expected, $expectedFormat, $input, $timezone) + { + $this->assertSame( + $expected, + $this->normalizer->normalize( + $input, + null, + array( + DateTimeNormalizer::TIMEZONE_KEY => $timezone, + DateTimeNormalizer::FORMAT_KEY => $expectedFormat, + ) + ) + ); + } + + public function normalizeUsingTimeZonePassedInContextAndExpectedFormatWithMicrosecondsProvider() + { + yield array( + '2018-12-01T18:03:06.067634', + 'Y-m-d\TH:i:s.u', + \DateTime::createFromFormat( + 'Y-m-d\TH:i:s.u', + '2018-12-01T18:03:06.067634', + new \DateTimeZone('UTC') + ), + null, + ); + + yield array( + '2018-12-01T18:03:06.067634', + 'Y-m-d\TH:i:s.u', + \DateTime::createFromFormat( + 'Y-m-d\TH:i:s.u', + '2018-12-01T18:03:06.067634', + new \DateTimeZone('UTC') + ), + new \DateTimeZone('UTC'), + ); + + yield array( + '2018-12-01T19:03:06.067634+01:00', + 'Y-m-d\TH:i:s.uP', + \DateTimeImmutable::createFromFormat( + 'Y-m-d\TH:i:s.u', + '2018-12-01T18:03:06.067634', + new \DateTimeZone('UTC') + ), + new \DateTimeZone('Europe/Rome'), + ); + + yield array( + '2018-12-01T20:03:06.067634+02:00', + 'Y-m-d\TH:i:s.uP', + \DateTime::createFromFormat( + 'Y-m-d\TH:i:s.u', + '2018-12-01T18:03:06.067634', + new \DateTimeZone('UTC') + ), + new \DateTimeZone('Europe/Kiev'), + ); + + yield array( + '2018-12-01T21:03:06.067634', + 'Y-m-d\TH:i:s.u', + \DateTime::createFromFormat( + 'Y-m-d\TH:i:s.u', + '2018-12-01T18:03:06.067634', + new \DateTimeZone('UTC') + ), + new \DateTimeZone('Europe/Moscow'), + ); + } + /** * @expectedException \Symfony\Component\Serializer\Exception\InvalidArgumentException * @expectedExceptionMessage The object must implement the "\DateTimeInterface". diff --git a/src/Symfony/Component/Validator/ConstraintViolation.php b/src/Symfony/Component/Validator/ConstraintViolation.php index 57ece5ba37259..8651913c3e7f3 100644 --- a/src/Symfony/Component/Validator/ConstraintViolation.php +++ b/src/Symfony/Component/Validator/ConstraintViolation.php @@ -79,13 +79,13 @@ public function __toString() } $propertyPath = (string) $this->propertyPath; - $code = $this->code; + $code = (string) $this->code; if ('' !== $propertyPath && '[' !== $propertyPath[0] && '' !== $class) { $class .= '.'; } - if (!empty($code)) { + if ('' !== $code) { $code = ' (code '.$code.')'; } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index 544c82f6c074b..e4f7df1757da6 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -112,7 +112,7 @@ public function getValidator(); * Returns the currently validated object. * * If the validator is currently validating a class constraint, the - * object of that class is returned. If it is a validating a property or + * object of that class is returned. If it is validating a property or * getter constraint, the object that the property/getter belongs to is * returned. * diff --git a/src/Symfony/Component/Validator/Tests/ConstraintViolationTest.php b/src/Symfony/Component/Validator/Tests/ConstraintViolationTest.php index cef4782e0f82d..edaa7fa50d6b0 100644 --- a/src/Symfony/Component/Validator/Tests/ConstraintViolationTest.php +++ b/src/Symfony/Component/Validator/Tests/ConstraintViolationTest.php @@ -53,4 +53,59 @@ public function testToStringHandlesArrayRoots() $this->assertSame($expected, (string) $violation); } + + public function testToStringHandlesCodes() + { + $violation = new ConstraintViolation( + '42 cannot be used here', + 'this is the message template', + array(), + array('some_value' => 42), + 'some_value', + null, + null, + 0 + ); + + $expected = <<<'EOF' +Array.some_value: + 42 cannot be used here (code 0) +EOF; + + $this->assertSame($expected, (string) $violation); + } + + public function testToStringOmitsEmptyCodes() + { + $expected = <<<'EOF' +Array.some_value: + 42 cannot be used here +EOF; + + $violation = new ConstraintViolation( + '42 cannot be used here', + 'this is the message template', + array(), + array('some_value' => 42), + 'some_value', + null, + null, + null + ); + + $this->assertSame($expected, (string) $violation); + + $violation = new ConstraintViolation( + '42 cannot be used here', + 'this is the message template', + array(), + array('some_value' => 42), + 'some_value', + null, + null, + '' + ); + + $this->assertSame($expected, (string) $violation); + } }