diff --git a/CHANGELOG-3.3.md b/CHANGELOG-3.3.md index 6c3cb3540ff0c..1817eff60299e 100644 --- a/CHANGELOG-3.3.md +++ b/CHANGELOG-3.3.md @@ -7,6 +7,72 @@ in 3.3 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v3.3.0...v3.3.1 +* 3.3.3 (2017-07-04) + + * bug #23366 [FrameworkBundle] Don't get() private services from debug:router (chalasr) + * bug #23239 [FrameworkBundle] call setContainer() for autowired controllers (xabbuh) + * bug #23351 [Dotenv] parse concatenated variable values (xabbuh) + * bug #23341 [DoctrineBridge][Security][Validator] do not validate empty values (xabbuh) + * bug #23274 Display a better error design when the toolbar cannot be displayed (yceruto) + * bug #23342 [Dotenv] parse escaped quotes in unquoted env var values (xabbuh) + * bug #23291 [Security] Fix Firewall ExceptionListener priority (chalasr) + * bug #23296 [WebProfilerBundle] Fix css trick used for offsetting html anchor from fixed header (ogizanagi) + * bug #23333 [PropertyAccess] Fix TypeError discard (dunglas) + * bug #23326 [Cache] fix cleanup of expired items for PdoAdapter (dmaicher) + * bug #23345 [Console] fix description of INF default values (xabbuh) + * bug #23328 [FrameworkBundle] Display a proper warning on cache:clear without the --no-warmup option (ogizanagi) + * bug #23299 [Workflow] Added more events to the announce function (Nyholm) + * bug #23279 Don't call count on non countable object (pierredup) + * bug #23283 [TwigBundle] add back exception check (xabbuh) + * bug #23268 Show exception is checked twice in ExceptionController of twig (gmponos) + * bug #23266 Display a better error message when the toolbar cannot be displayed (javiereguiluz) + * bug #23271 [FrameworkBundle] allow SSI fragments configuration in XML files (xabbuh) + * bug #23254 [Form][TwigBridge] render hidden _method field in form_rest() (xabbuh) + * bug #23250 [Translation] return fallback locales whenever possible (xabbuh) + * bug #23237 [Cache] Fix Predis client cluster with pipeline (flolivaud) + * bug #23240 [Console] Fix catching exception type in QuestionHelper (voronkovich) + * bug #23218 [DI] Dedup tags when using instanceof/autoconfigure (ogizanagi) + * bug #23231 Improved the exception page when there is no message (javiereguiluz) + * bug #23229 [WebProfilerBundle] Eliminate line wrap on count column (routing) (e-moe) + * bug #22732 [Security] fix switch user _exit without having current token (dmaicher) + * bug #23226 [Validator] replace hardcoded service id (xabbuh) + * bug #22730 [FrameworkBundle] Sessions: configurable "use_strict_mode" option for NativeSessionStorage (MacDada) + * bug #23195 [FrameworkBundle] [Command] Clean bundle directory, fixes #23177 (NicolasPion) + * bug #23213 Fixed composer resources between web/cli (iltar) + * bug #23160 [WebProfilerBundle] Fix the icon for the Cache panel (javiereguiluz) + * bug #23052 [TwigBundle] Add Content-Type header for exception response (rchoquet) + * bug #23173 [WebServerBundle] Fix router script option BC (1ed) + * bug #23199 Reset redirectCount when throwing exception (hvanoch) + * bug #23180 [FrameworkBundle] Expose the AbstractController's container to its subclasses (BPScott) + * bug #23186 [TwigBundle] Move template.xml loading to a compiler pass (ogizanagi) + * bug #23130 Keep s-maxage when expiry and validation are used in combination (mpdude) + * bug #23129 Fix two edge cases in ResponseCacheStrategy (mpdude) + * feature #22636 [Routing] Expose request in route conditions, if needed and possible (ro0NL) + * bug #22636 [Routing] Expose request in route conditions, if needed and possible (ro0NL) + * bug #22943 [SecurityBundle] Move cache of the firewall context into the request parameters (GromNaN) + * bug #23088 [FrameworkBundle] Dont set pre-defined esi/ssi services (ro0NL) + * bug #23057 [Translation][FrameworkBundle] Fix resource loading order inconsistency reported in #23034 (mpdude) + * bug #23092 [Filesystem] added workaround in Filesystem::rename for PHP bug (VolCh) + * bug #23074 [HttpFoundation] add back support for legacy constant values (xabbuh) + * bug #23128 [HttpFoundation] fix for Support for new 7.1 session options (vincentaubert) + * bug #23176 [VarDumper] fixes (nicolas-grekas) + * bug #23100 [PropertyAccess] Do not silence TypeErrors from client code. (tsufeki) + * bug #23156 [PropertyAccess] Fix Usage with anonymous classes (mablae) + * bug #23168 [Config] Fix ** GlobResource on Windows (nicolas-grekas) + * bug #23171 [Yaml] Fix linting yaml with constants as keys (chalasr) + * bug #23121 [Routing] Revert the change in [#b42018] with respect to Routing/Route.php (Dan Wilga) + * bug #23141 [DI] Fix keys resolution in ResolveParameterPlaceHoldersPass (nicolas-grekas) + * bug #23145 Fix the conditional definition of the SymfonyTestsListener (stof) + * bug #23091 [Cache] ApcuAdapter::isSupported() should return true when apc.enable_cli=Off (nicolas-grekas) + * bug #22953 #22839 - changed debug toolbar dump section to relative and use full window width (mkurzeja) + * bug #23086 [FrameworkBundle] Fix perf issue in CacheClearCommand::warmup() (nicolas-grekas) + * bug #23090 [SecurityBundle] Made 2 service aliases private (nicolas-grekas) + * bug #23108 [Yaml] Remove line number in deprecation notices (nicolas-grekas) + * bug #23098 Cache ipCheck (2.7) (gonzalovilaseca) + * bug #23082 [MonologBridge] Do not silence errors in ServerLogHandler::formatRecord (lyrixx) + * bug #23007 [HttpKernel][Debug] Fix missing trace on deprecations collected during bootstrapping & silenced errors (ogizanagi) + * bug #23069 [SecurityBundle] Show unique Inherited roles in profile panel (yceruto) + * 3.3.2 (2017-06-06) * bug #23073 [TwigBridge] Fix namespaced classes (ogizanagi) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 6816e9794a0a2..2ea3ea63b87fd 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -20,13 +20,13 @@ Symfony is the result of the work of many people who made the code better - Javier Eguiluz (javier.eguiluz) - Hugo Hamon (hhamon) - Abdellatif Ait boudad (aitboudad) + - Romain Neutron (romain) - Pascal Borreli (pborreli) - Wouter De Jong (wouterj) - - Romain Neutron (romain) - - Grégoire Pineau (lyrixx) - Robin Chalas (chalas_r) - - Joseph Bielawski (stloyd) - Maxime Steinhausser (ogizanagi) + - Grégoire Pineau (lyrixx) + - Joseph Bielawski (stloyd) - Karma Dordrak (drak) - Lukas Kahwe Smith (lsmith) - Martin Hasoň (hason) @@ -72,8 +72,8 @@ Symfony is the result of the work of many people who made the code better - Dariusz Górecki (canni) - Titouan Galopin (tgalopin) - Douglas Greenshields (shieldo) - - Konstantin Myakshin (koc) - Jáchym Toušek (enumag) + - Konstantin Myakshin (koc) - Lee McDermott - Brandon Turner - Luis Cordova (cordoval) @@ -112,10 +112,10 @@ Symfony is the result of the work of many people who made the code better - Jacob Dreesen (jdreesen) - Tobias Nyholm (tobias) - Tomáš Votruba (tomas_votruba) + - Yonel Ceruto González (yonelceruto) - Fabien Pennequin (fabienpennequin) - Gordon Franke (gimler) - Eric GELOEN (gelo) - - Yonel Ceruto González (yonelceruto) - Daniel Wehner (dawehner) - Tugdual Saunier (tucksaun) - Théo FIDRY (theofidry) @@ -130,6 +130,7 @@ Symfony is the result of the work of many people who made the code better - Daniel Gomes (danielcsgomes) - Hidenori Goto (hidenorigoto) - Guilherme Blanco (guilhermeblanco) + - Vincent AUBERT (vincent) - Pablo Godel (pgodel) - Jérémie Augustin (jaugustin) - Andréia Bohner (andreia) @@ -140,10 +141,10 @@ Symfony is the result of the work of many people who made the code better - Joel Wurtz (brouznouf) - Philipp Wahala (hifi) - Vyacheslav Pavlov + - Richard van Laak (rvanlaak) - Javier Spagnoletti (phansys) - Richard Shank (iampersistent) - Thomas Rabaix (rande) - - Vincent AUBERT (vincent) - Rouven Weßling (realityking) - Teoh Han Hui (teohhanhui) - Jérôme Vasseur (jvasseur) @@ -151,7 +152,6 @@ Symfony is the result of the work of many people who made the code better - Helmer Aaviksoo - Grégoire Paris (greg0ire) - Hiromi Hishida (77web) - - Richard van Laak (rvanlaak) - Matthieu Ouellette-Vachon (maoueh) - Michał Pipa (michal.pipa) - Amal Raghav (kertz) @@ -244,6 +244,7 @@ Symfony is the result of the work of many people who made the code better - Uwe Jäger (uwej711) - Eugene Leonovich (rybakit) - Filippo Tessarotto + - Oleg Voronkovich - Joseph Rouff (rouffj) - Félix Labrecque (woodspire) - GordonsLondon @@ -279,7 +280,6 @@ Symfony is the result of the work of many people who made the code better - Jordan Samouh (jordansamouh) - Chris Smith (cs278) - Florian Klein (docteurklein) - - Oleg Voronkovich - Manuel Kiessling (manuelkiessling) - Atsuhiro KUBO (iteman) - Andrew Moore (finewolf) @@ -389,6 +389,7 @@ Symfony is the result of the work of many people who made the code better - Emanuele Gaspari (inmarelibero) - Sébastien Santoro (dereckson) - Brian King + - Frank de Jonge (frenkynet) - Michel Salib (michelsalib) - geoffrey - Steffen Roßkamp @@ -525,7 +526,6 @@ Symfony is the result of the work of many people who made the code better - Romain Pierre (romain-pierre) - Jan Behrens - Mantas Var (mvar) - - Frank de Jonge (frenkynet) - Sebastian Krebs - Jean-Christophe Cuvelier [Artack] - Christopher Davis (chrisguitarguy) @@ -1280,6 +1280,7 @@ Symfony is the result of the work of many people who made the code better - ged15 - Daan van Renterghem - Nicole Cordes + - Martin Kirilov - Bram Van der Sype (brammm) - Christopher Hertel (chertel) - Guile (guile) diff --git a/composer.json b/composer.json index f91e8fc1a86b2..3b096baff3c55 100644 --- a/composer.json +++ b/composer.json @@ -115,7 +115,6 @@ "Symfony\\Bridge\\Doctrine\\": "src/Symfony/Bridge/Doctrine/", "Symfony\\Bridge\\Monolog\\": "src/Symfony/Bridge/Monolog/", "Symfony\\Bridge\\ProxyManager\\": "src/Symfony/Bridge/ProxyManager/", - "Symfony\\Bridge\\Swiftmailer\\": "src/Symfony/Bridge/Swiftmailer/", "Symfony\\Bridge\\Twig\\": "src/Symfony/Bridge/Twig/", "Symfony\\Bundle\\": "src/Symfony/Bundle/", "Symfony\\Component\\": "src/Symfony/Component/" diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php index c2fcb520e10fe..78d82b322568b 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php @@ -63,6 +63,10 @@ public function validate($entity, Constraint $constraint) throw new ConstraintDefinitionException('At least one field has to be specified.'); } + if (null === $entity) { + return; + } + if ($constraint->em) { $em = $this->registry->getManager($constraint->em); diff --git a/src/Symfony/Bridge/Monolog/Handler/ServerLogHandler.php b/src/Symfony/Bridge/Monolog/Handler/ServerLogHandler.php index fb258808f3836..d1d4968df4cda 100644 --- a/src/Symfony/Bridge/Monolog/Handler/ServerLogHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/ServerLogHandler.php @@ -51,9 +51,15 @@ public function handle(array $record) if (!$this->socket = $this->socket ?: $this->createSocket()) { return false === $this->bubble; } + } finally { + restore_error_handler(); + } - $recordFormatted = $this->formatRecord($record); + $recordFormatted = $this->formatRecord($record); + set_error_handler(self::class.'::nullErrorHandler'); + + try { if (-1 === stream_socket_sendto($this->socket, $recordFormatted)) { stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR); diff --git a/src/Symfony/Bridge/PhpUnit/SymfonyTestsListener.php b/src/Symfony/Bridge/PhpUnit/SymfonyTestsListener.php index 2dad6ef0095bc..c11fde9526eab 100644 --- a/src/Symfony/Bridge/PhpUnit/SymfonyTestsListener.php +++ b/src/Symfony/Bridge/PhpUnit/SymfonyTestsListener.php @@ -18,53 +18,53 @@ if (class_exists('PHPUnit_Runner_Version') && version_compare(\PHPUnit_Runner_Version::id(), '6.0.0', '<')) { class_alias('Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListener', 'Symfony\Bridge\PhpUnit\SymfonyTestsListener'); - - return; -} - -/** - * Collects and replays skipped tests. - * - * @author Nicolas Grekas - * - * @final - */ -class SymfonyTestsListener extends BaseTestListener -{ - private $trait; - - public function __construct(array $mockedNamespaces = array()) +// Using an early return instead of a else does not work when using the PHPUnit phar due to some weird PHP behavior (the class +// gets defined without executing the code before it and so the definition is not properly conditional) +} else { + /** + * Collects and replays skipped tests. + * + * @author Nicolas Grekas + * + * @final + */ + class SymfonyTestsListener extends BaseTestListener { - $this->trait = new Legacy\SymfonyTestsListenerTrait($mockedNamespaces); - } + private $trait; - public function globalListenerDisabled() - { - $this->trait->globalListenerDisabled(); - } + public function __construct(array $mockedNamespaces = array()) + { + $this->trait = new Legacy\SymfonyTestsListenerTrait($mockedNamespaces); + } - public function startTestSuite(TestSuite $suite) - { - return $this->trait->startTestSuite($suite); - } + public function globalListenerDisabled() + { + $this->trait->globalListenerDisabled(); + } - public function addSkippedTest(Test $test, \Exception $e, $time) - { - return $this->trait->addSkippedTest($test, $e, $time); - } + public function startTestSuite(TestSuite $suite) + { + return $this->trait->startTestSuite($suite); + } - public function startTest(Test $test) - { - return $this->trait->startTest($test); - } + public function addSkippedTest(Test $test, \Exception $e, $time) + { + return $this->trait->addSkippedTest($test, $e, $time); + } - public function addWarning(Test $test, Warning $e, $time) - { - return $this->trait->addWarning($test, $e, $time); - } + public function startTest(Test $test) + { + return $this->trait->startTest($test); + } - public function endTest(Test $test, $time) - { - return $this->trait->endTest($test, $time); + public function addWarning(Test $test, Warning $e, $time) + { + return $this->trait->addWarning($test, $e, $time); + } + + public function endTest(Test $test, $time) + { + return $this->trait->endTest($test, $time); + } } } diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig index 51e13a843033f..f70ca33809a78 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig @@ -302,6 +302,7 @@ {%- endblock form -%} {%- block form_start -%} + {%- do form.setMethodRendered() -%} {% set method = method|upper %} {%- if method in ["GET", "POST"] -%} {% set form_method = method %} @@ -337,6 +338,20 @@ {{- form_row(child) -}} {% endif %} {%- endfor %} + + {% if not form.methodRendered %} + {%- do form.setMethodRendered() -%} + {% set method = method|upper %} + {%- if method in ["GET", "POST"] -%} + {% set form_method = method %} + {%- else -%} + {% set form_method = "POST" %} + {%- endif -%} + + {%- if form_method != method -%} + + {%- endif -%} + {% endif %} {% endblock form_rest %} {# Support #} diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index a7f1e23747536..8fe41945b72aa 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -23,7 +23,7 @@ "fig/link-util": "^1.0", "symfony/asset": "~2.8|~3.0", "symfony/finder": "~2.8|~3.0", - "symfony/form": "^3.2.7", + "symfony/form": "^3.2.10|^3.3.3", "symfony/http-kernel": "~3.2", "symfony/polyfill-intl-icu": "~1.0", "symfony/routing": "~2.8|~3.0", @@ -38,6 +38,9 @@ "symfony/expression-language": "~2.8|~3.0", "symfony/web-link": "~3.3" }, + "conflict": { + "symfony/form": "<3.2.10|~3.3,<3.3.3" + }, "suggest": { "symfony/finder": "", "symfony/asset": "For using the AssetExtension", diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php index 136b1c21fae98..dff0e245f741b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php @@ -114,6 +114,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $rows = array(); $copyUsed = false; $exitCode = 0; + $validAssetDirs = array(); /** @var BundleInterface $bundle */ foreach ($this->getContainer()->get('kernel')->getBundles() as $bundle) { if (!is_dir($originDir = $bundle->getPath().'/Resources/public')) { @@ -152,6 +153,13 @@ protected function execute(InputInterface $input, OutputInterface $output) $exitCode = 1; $rows[] = array(sprintf('%s', '\\' === DIRECTORY_SEPARATOR ? 'ERROR' : "\xE2\x9C\x98" /* HEAVY BALLOT X (U+2718) */), $message, $e->getMessage()); } + $validAssetDirs[] = $targetDir; + } + // remove the assets of the bundles that no longer exist + foreach (new \FilesystemIterator($bundlesDir) as $dir) { + if (!in_array($dir, $validAssetDirs)) { + $this->filesystem->remove($dir); + } } $io->table(array('', 'Bundle', 'Method / Error'), $rows); diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php index e13d5b33c7d8e..2331afecb4e84 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -77,7 +77,11 @@ protected function execute(InputInterface $input, OutputInterface $output) if ($input->getOption('no-warmup')) { $filesystem->rename($realCacheDir, $oldCacheDir); } else { - @trigger_error('Calling cache:clear without the --no-warmup option is deprecated since version 3.3. Cache warmup should be done with the cache:warmup command instead.', E_USER_DEPRECATED); + $warning = 'Calling cache:clear without the --no-warmup option is deprecated since version 3.3. Cache warmup should be done with the cache:warmup command instead.'; + + @trigger_error($warning, E_USER_DEPRECATED); + + $io->warning($warning); $this->warmupCache($input, $output, $realCacheDir, $oldCacheDir); } @@ -158,7 +162,7 @@ protected function warmup($warmupDir, $realCacheDir, $enableOptionalWarmers = tr $safeTempKernel = str_replace('\\', '\\\\', get_class($tempKernel)); $realKernelFQN = get_class($realKernel); - foreach (Finder::create()->files()->name('*.meta')->in($warmupDir) as $file) { + foreach (Finder::create()->files()->depth('<3')->name('*.meta')->in($warmupDir) as $file) { file_put_contents($file, preg_replace( '/(C\:\d+\:)"'.$safeTempKernel.'"/', sprintf('$1"%s"', $realKernelFQN), @@ -170,14 +174,16 @@ protected function warmup($warmupDir, $realCacheDir, $enableOptionalWarmers = tr $search = array($warmupDir, str_replace('\\', '\\\\', $warmupDir)); $replace = str_replace('\\', '/', $realCacheDir); foreach (Finder::create()->files()->in($warmupDir) as $file) { - $content = str_replace($search, $replace, file_get_contents($file)); - file_put_contents($file, $content); + $content = str_replace($search, $replace, file_get_contents($file), $count); + if ($count) { + file_put_contents($file, $content); + } } // fix references to container's class $tempContainerClass = get_class($tempKernel->getContainer()); $realContainerClass = get_class($realKernel->getContainer()); - foreach (Finder::create()->files()->name($tempContainerClass.'*')->in($warmupDir) as $file) { + foreach (Finder::create()->files()->depth('<2')->name($tempContainerClass.'*')->in($warmupDir) as $file) { $content = str_replace($tempContainerClass, $realContainerClass, file_get_contents($file)); file_put_contents($file, $content); rename($file, str_replace(DIRECTORY_SEPARATOR.$tempContainerClass, DIRECTORY_SEPARATOR.$realContainerClass, $file)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php index 22f0bc6795d5b..e7d6d80a57ac7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper; +use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -112,7 +113,7 @@ protected function execute(InputInterface $input, OutputInterface $output) private function convertController(Route $route) { if ($route->hasDefault('_controller')) { - $nameParser = $this->getContainer()->get('controller_name_converter'); + $nameParser = new ControllerNameParser($this->getApplication()->getKernel()); try { $route->setDefault('_controller', $nameParser->build($route->getDefault('_controller'))); } catch (\InvalidArgumentException $e) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php b/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php index 6d0cee247da77..3ba3b8471edc4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php @@ -16,7 +16,6 @@ use Symfony\Component\DependencyInjection\ServiceSubscriberInterface; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Routing\RouterInterface; @@ -36,7 +35,10 @@ abstract class AbstractController implements ServiceSubscriberInterface { use ControllerTrait; - private $container; + /** + * @var ContainerInterface + */ + protected $container; /** * @internal diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php index 355f526fdc950..3de157bd4ff3e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php @@ -49,7 +49,19 @@ protected function createController($controller) $controller = $this->parser->parse($controller); } - return parent::createController($controller); + $resolvedController = parent::createController($controller); + + if (1 === substr_count($controller, ':') && is_array($resolvedController)) { + if ($resolvedController[0] instanceof ContainerAwareInterface) { + $resolvedController[0]->setContainer($this->container); + } + + if ($resolvedController[0] instanceof AbstractController && null !== $previousContainer = $resolvedController[0]->setContainer($this->container)) { + $resolvedController[0]->setContainer($previousContainer); + } + } + + return $resolvedController; } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 611bfad97db82..c386ebeb33709 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -435,6 +435,7 @@ private function addSessionSection(ArrayNodeDefinition $rootNode) ->scalarNode('gc_divisor')->end() ->scalarNode('gc_probability')->defaultValue(1)->end() ->scalarNode('gc_maxlifetime')->end() + ->booleanNode('use_strict_mode')->end() ->scalarNode('save_path')->defaultValue('%kernel.cache_dir%/sessions')->end() ->integerNode('metadata_update_threshold') ->defaultValue('0') diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index b607cdd2bf563..131db0ae86343 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -702,7 +702,7 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c // session storage $container->setAlias('session.storage', $config['storage_id']); $options = array(); - foreach (array('name', 'cookie_lifetime', 'cookie_path', 'cookie_domain', 'cookie_secure', 'cookie_httponly', 'use_cookies', 'gc_maxlifetime', 'gc_probability', 'gc_divisor') as $key) { + foreach (array('name', 'cookie_lifetime', 'cookie_path', 'cookie_domain', 'cookie_secure', 'cookie_httponly', 'use_cookies', 'gc_maxlifetime', 'gc_probability', 'gc_divisor', 'use_strict_mode') as $key) { if (isset($config[$key])) { $options[$key] = $config[$key]; } diff --git a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php index fad4adc04434d..06aa922210569 100644 --- a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php +++ b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php @@ -54,8 +54,7 @@ public function __construct(HttpKernelInterface $kernel, $cacheDir = null) protected function forward(Request $request, $raw = false, Response $entry = null) { $this->getKernel()->boot(); - $this->getKernel()->getContainer()->set('cache', $this); - $this->getKernel()->getContainer()->set($this->getSurrogate()->getName(), $this->getSurrogate()); + $this->getKernel()->getContainer()->set('cache', $this); // to be removed in 4.0? return parent::forward($request, $raw, $entry); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index 29d07c5758eed..d6138133cf2ea 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -13,6 +13,7 @@ + @@ -58,6 +59,10 @@ + + + + @@ -110,6 +115,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterDebugCommandTest.php index 6d32a37fd0ec9..597082485a0b6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterDebugCommandTest.php @@ -12,9 +12,10 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; use PHPUnit\Framework\TestCase; -use Symfony\Component\Console\Application; +use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Bundle\FrameworkBundle\Command\RouterDebugCommand; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; @@ -51,16 +52,15 @@ public function testDebugInvalidRoute() */ private function createCommandTester() { - $application = new Application(); + $application = new Application($this->getKernel()); $command = new RouterDebugCommand(); - $command->setContainer($this->getContainer()); $application->add($command); return new CommandTester($application->find('debug:router')); } - private function getContainer() + private function getKernel() { $routeCollection = new RouteCollection(); $routeCollection->add('foo', new Route('foo')); @@ -82,14 +82,25 @@ private function getContainer() ->with('router') ->will($this->returnValue(true)) ; - $container + ->expects($this->any()) ->method('get') - ->will($this->returnValueMap(array( - array('router', 1, $router), - array('controller_name_converter', 1, $loader), - ))); + ->with('router') + ->willReturn($router) + ; + + $kernel = $this->getMockBuilder(KernelInterface::class)->getMock(); + $kernel + ->expects($this->any()) + ->method('getContainer') + ->willReturn($container) + ; + $kernel + ->expects($this->once()) + ->method('getBundles') + ->willReturn(array()) + ; - return $container; + return $kernel; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php index cd15b8c5e82fc..384bd7ca53079 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php @@ -12,10 +12,11 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; use PHPUnit\Framework\TestCase; -use Symfony\Component\Console\Application; +use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Bundle\FrameworkBundle\Command\RouterMatchCommand; use Symfony\Bundle\FrameworkBundle\Command\RouterDebugCommand; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\RequestContext; @@ -45,20 +46,14 @@ public function testWithNotMatchPath() */ private function createCommandTester() { - $application = new Application(); - - $command = new RouterMatchCommand(); - $command->setContainer($this->getContainer()); - $application->add($command); - - $command = new RouterDebugCommand(); - $command->setContainer($this->getContainer()); - $application->add($command); + $application = new Application($this->getKernel()); + $application->add(new RouterMatchCommand()); + $application->add(new RouterDebugCommand()); return new CommandTester($application->find('router:match')); } - private function getContainer() + private function getKernel() { $routeCollection = new RouteCollection(); $routeCollection->add('foo', new Route('foo')); @@ -81,16 +76,27 @@ private function getContainer() $container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock(); $container - ->expects($this->once()) + ->expects($this->atLeastOnce()) ->method('has') ->with('router') ->will($this->returnValue(true)); - $container->method('get') - ->will($this->returnValueMap(array( - array('router', 1, $router), - array('controller_name_converter', 1, $loader), - ))); + $container + ->expects($this->any()) + ->method('get') + ->willReturn($router); + + $kernel = $this->getMockBuilder(KernelInterface::class)->getMock(); + $kernel + ->expects($this->any()) + ->method('getContainer') + ->willReturn($container) + ; + $kernel + ->expects($this->once()) + ->method('getBundles') + ->willReturn(array()) + ; - return $container; + return $kernel; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/AbstractControllerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/AbstractControllerTest.php index 937cbfc7286d0..6783ec25c5ab5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/AbstractControllerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/AbstractControllerTest.php @@ -13,7 +13,6 @@ use Psr\Container\ContainerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\HttpFoundation\File\File; class AbstractControllerTest extends ControllerTraitTest { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php index 7946a96e8d2c0..5880ee0186a18 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php @@ -13,6 +13,7 @@ use Psr\Container\ContainerInterface as Psr11ContainerInterface; use Psr\Log\LoggerInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser; use Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver; use Symfony\Component\DependencyInjection\Container; @@ -68,6 +69,24 @@ public function testGetControllerWithBundleNotation() $this->assertSame('testAction', $controller[1]); } + public function testContainerAwareControllerGetsContainerWhenNotSet() + { + class_exists(AbstractControllerTest::class); + + $controller = new ContainerAwareController(); + + $container = new Container(); + $container->set(TestAbstractController::class, $controller); + + $resolver = $this->createControllerResolver(null, $container); + + $request = Request::create('/'); + $request->attributes->set('_controller', TestAbstractController::class.':testAction'); + + $this->assertSame(array($controller, 'testAction'), $resolver->getController($request)); + $this->assertSame($container, $controller->getContainer()); + } + public function testAbstractControllerGetsContainerWhenNotSet() { class_exists(AbstractControllerTest::class); @@ -86,6 +105,24 @@ class_exists(AbstractControllerTest::class); $this->assertSame($container, $controller->setContainer($container)); } + public function testAbstractControllerServiceWithFcqnIdGetsContainerWhenNotSet() + { + class_exists(AbstractControllerTest::class); + + $controller = new DummyController(); + + $container = new Container(); + $container->set(DummyController::class, $controller); + + $resolver = $this->createControllerResolver(null, $container); + + $request = Request::create('/'); + $request->attributes->set('_controller', DummyController::class.':fooAction'); + + $this->assertSame(array($controller, 'fooAction'), $resolver->getController($request)); + $this->assertSame($container, $controller->getContainer()); + } + public function testAbstractControllerGetsNoContainerWhenSet() { class_exists(AbstractControllerTest::class); @@ -106,6 +143,26 @@ class_exists(AbstractControllerTest::class); $this->assertSame($controllerContainer, $controller->setContainer($container)); } + public function testAbstractControllerServiceWithFcqnIdGetsNoContainerWhenSet() + { + class_exists(AbstractControllerTest::class); + + $controller = new DummyController(); + $controllerContainer = new Container(); + $controller->setContainer($controllerContainer); + + $container = new Container(); + $container->set(DummyController::class, $controller); + + $resolver = $this->createControllerResolver(null, $container); + + $request = Request::create('/'); + $request->attributes->set('_controller', DummyController::class.':fooAction'); + + $this->assertSame(array($controller, 'fooAction'), $resolver->getController($request)); + $this->assertSame($controllerContainer, $controller->getContainer()); + } + protected function createControllerResolver(LoggerInterface $logger = null, Psr11ContainerInterface $container = null, ControllerNameParser $parser = null) { if (!$parser) { @@ -152,3 +209,15 @@ public function __invoke() { } } + +class DummyController extends AbstractController +{ + public function getContainer() + { + return $this->container; + } + + public function fooAction() + { + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php index 58a4340e6979f..2b2b3f45f0a8d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php @@ -13,6 +13,9 @@ 'esi' => array( 'enabled' => true, ), + 'ssi' => array( + 'enabled' => true, + ), 'profiler' => array( 'only_exceptions' => true, 'enabled' => false, diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml index 5c6a221c18489..81f7823eecb87 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml @@ -12,6 +12,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml index f471372097c15..fed4fa7d3e65f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml @@ -8,6 +8,8 @@ framework: http_method_override: false esi: enabled: true + ssi: + enabled: true profiler: only_exceptions: true enabled: false diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 4f86c85d4f8fb..eee6169ae15da 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -134,6 +134,13 @@ public function testEsi() $this->assertTrue($container->hasDefinition('esi'), '->registerEsiConfiguration() loads esi.xml'); } + public function testSsi() + { + $container = $this->createContainerFromFile('full'); + + $this->assertTrue($container->hasDefinition('ssi'), '->registerSsiConfiguration() loads ssi.xml'); + } + public function testEnabledProfiler() { $container = $this->createContainerFromFile('profiler'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/WebTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/WebTestCase.php index 2861297fe0256..a1c50e59fdb90 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/WebTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/WebTestCase.php @@ -68,6 +68,6 @@ protected static function createKernel(array $options = array()) protected static function getVarDir() { - return substr(strrchr(get_called_class(), '\\'), 1); + return 'FB'.substr(strrchr(get_called_class(), '\\'), 1); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php index ef6fa9330b0d8..6d710a76e8819 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php @@ -293,6 +293,51 @@ public function testInvalidOptions() (new Translator($container, new MessageSelector(), 'en', array(), array('foo' => 'bar'))); } + /** @dataProvider getDebugModeAndCacheDirCombinations */ + public function testResourceFilesOptionLoadsBeforeOtherAddedResources($debug, $enableCache) + { + $someCatalogue = $this->getCatalogue('some_locale', array()); + + $loader = $this->getMockBuilder('Symfony\Component\Translation\Loader\LoaderInterface')->getMock(); + + $loader->expects($this->at(0)) + ->method('load') + /* The "messages.some_locale.loader" is passed via the resource_file option and shall be loaded first */ + ->with('messages.some_locale.loader', 'some_locale', 'messages') + ->willReturn($someCatalogue); + + $loader->expects($this->at(1)) + ->method('load') + /* This resource is added by an addResource() call and shall be loaded after the resource_files */ + ->with('second_resource.some_locale.loader', 'some_locale', 'messages') + ->willReturn($someCatalogue); + + $options = array( + 'resource_files' => array('some_locale' => array('messages.some_locale.loader')), + 'debug' => $debug, + ); + + if ($enableCache) { + $options['cache_dir'] = $this->tmpDir; + } + + /** @var Translator $translator */ + $translator = $this->createTranslator($loader, $options); + $translator->addResource('loader', 'second_resource.some_locale.loader', 'some_locale', 'messages'); + + $translator->trans('some_message', array(), null, 'some_locale'); + } + + public function getDebugModeAndCacheDirCombinations() + { + return array( + array(false, false), + array(true, false), + array(false, true), + array(true, true), + ); + } + protected function getCatalogue($locale, $messages, $resources = array()) { $catalogue = new MessageCatalogue($locale); diff --git a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php index 2b83e14b10e66..d18a3b81f04ef 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php +++ b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php @@ -39,6 +39,14 @@ class Translator extends BaseTranslator implements WarmableInterface */ private $resourceLocales; + /** + * Holds parameters from addResource() calls so we can defer the actual + * parent::addResource() calls until initialize() is executed. + * + * @var array + */ + private $resources = array(); + /** * Constructor. * @@ -80,9 +88,7 @@ public function __construct(ContainerInterface $container, MessageSelector $sele $this->options = array_merge($this->options, $options); $this->resourceLocales = array_keys($this->options['resource_files']); - if (null !== $this->options['cache_dir'] && $this->options['debug']) { - $this->loadResources(); - } + $this->addResourceFiles($this->options['resource_files']); parent::__construct($defaultLocale, $selector, $this->options['cache_dir'], $this->options['debug']); } @@ -108,6 +114,11 @@ public function warmUp($cacheDir) } } + public function addResource($format, $resource, $locale, $domain = null) + { + $this->resources[] = array($format, $resource, $locale, $domain); + } + /** * {@inheritdoc} */ @@ -119,7 +130,12 @@ protected function initializeCatalogue($locale) protected function initialize() { - $this->loadResources(); + foreach ($this->resources as $key => $params) { + list($format, $resource, $locale, $domain) = $params; + parent::addResource($format, $resource, $locale, $domain); + } + $this->resources = array(); + foreach ($this->loaderIds as $id => $aliases) { foreach ($aliases as $alias) { $this->addLoader($alias, $this->container->get($id)); @@ -127,14 +143,13 @@ protected function initialize() } } - private function loadResources() + private function addResourceFiles($filesByLocale) { - foreach ($this->options['resource_files'] as $locale => $files) { + foreach ($filesByLocale as $locale => $files) { foreach ($files as $key => $file) { // filename is domain.locale.format list($domain, $locale, $format) = explode('.', basename($file), 3); $this->addResource($format, $file, $locale, $domain); - unset($this->options['resource_files'][$locale][$key]); } } } diff --git a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php index 6831e94d047af..9c0de7d2cda1a 100644 --- a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php +++ b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php @@ -118,7 +118,7 @@ public function collect(Request $request, Response $response, \Exception $except 'logout_url' => $logoutUrl, 'user' => $token->getUsername(), 'roles' => array_map(function (RoleInterface $role) { return $role->getRole(); }, $assignedRoles), - 'inherited_roles' => array_map(function (RoleInterface $role) { return $role->getRole(); }, $inheritedRoles), + 'inherited_roles' => array_unique(array_map(function (RoleInterface $role) { return $role->getRole(); }, $inheritedRoles)), 'supports_role_hierarchy' => null !== $this->roleHierarchy, ); } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 4b7dba5e45be8..79435ff5d4619 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -19,7 +19,7 @@ %security.access.always_authenticate_before_granting% - + @@ -59,7 +59,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig index 073d0d869d8f0..9cd6ec4d78db2 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig @@ -222,7 +222,7 @@ {% for voter in collector.voters %} {{ loop.index }} - {{ voter }} + {{ profiler_dump(voter) }} {% endfor %} diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php index 55272ec3043bd..af31a0fb60026 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\SecurityBundle\Security; use Psr\Container\ContainerInterface; +use Symfony\Bundle\SecurityBundle\Security\FirewallContext; use Symfony\Component\Security\Http\FirewallMapInterface; use Symfony\Component\HttpFoundation\Request; @@ -141,15 +142,27 @@ public function getFirewallConfig(Request $request) return $context->getConfig(); } + /** + * @return FirewallContext + */ private function getFirewallContext(Request $request) { - if ($this->contexts->contains($request)) { - return $this->contexts[$request]; + if ($request->attributes->has('_firewall_context')) { + $storedContextId = $request->attributes->get('_firewall_context'); + foreach ($this->map as $contextId => $requestMatcher) { + if ($contextId === $storedContextId) { + return $this->container->get($contextId); + } + } + + $request->attributes->remove('_firewall_context'); } foreach ($this->map as $contextId => $requestMatcher) { if (null === $requestMatcher || $requestMatcher->matches($request)) { - return $this->contexts[$request] = $this->container->get($contextId); + $request->attributes->set('_firewall_context', $contextId); + + return $this->container->get($contextId); } } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php index f155f901c9c3c..de30be687426b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php @@ -164,6 +164,11 @@ public function provideRoles() array('ROLE_ADMIN'), array('ROLE_USER', 'ROLE_ALLOWED_TO_SWITCH'), ), + array( + array('ROLE_ADMIN', 'ROLE_OPERATOR'), + array('ROLE_ADMIN', 'ROLE_OPERATOR'), + array('ROLE_USER', 'ROLE_ALLOWED_TO_SWITCH'), + ), ); } @@ -171,6 +176,7 @@ private function getRoleHierarchy() { return new RoleHierarchy(array( 'ROLE_ADMIN' => array('ROLE_USER', 'ROLE_ALLOWED_TO_SWITCH'), + 'ROLE_OPERATOR' => array('ROLE_USER'), )); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/WebTestCase.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/WebTestCase.php index 8bace799a37a8..bb98a2a065ca0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/WebTestCase.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/WebTestCase.php @@ -68,6 +68,6 @@ protected static function createKernel(array $options = array()) protected static function getVarDir() { - return substr(strrchr(get_called_class(), '\\'), 1); + return 'SB'.substr(strrchr(get_called_class(), '\\'), 1); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallMapTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallMapTest.php new file mode 100644 index 0000000000000..f9047cfdd233e --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallMapTest.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Security; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\SecurityBundle\Security\FirewallConfig; +use Symfony\Bundle\SecurityBundle\Security\FirewallContext; +use Symfony\Bundle\SecurityBundle\Security\FirewallMap; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; +use Symfony\Component\Security\Core\User\UserCheckerInterface; +use Symfony\Component\Security\Http\Firewall\ExceptionListener; +use Symfony\Component\Security\Http\Firewall\ListenerInterface; + +class FirewallMapTest extends TestCase +{ + const ATTRIBUTE_FIREWALL_CONTEXT = '_firewall_context'; + + public function testGetListenersWithEmptyMap() + { + $request = new Request(); + + $map = array(); + $container = $this->getMockBuilder(Container::class)->getMock(); + $container->expects($this->never())->method('get'); + + $firewallMap = new FirewallMap($container, $map); + + $this->assertEquals(array(array(), null), $firewallMap->getListeners($request)); + $this->assertNull($firewallMap->getFirewallConfig($request)); + $this->assertFalse($request->attributes->has(self::ATTRIBUTE_FIREWALL_CONTEXT)); + } + + public function testGetListenersWithInvalidParameter() + { + $request = new Request(); + $request->attributes->set(self::ATTRIBUTE_FIREWALL_CONTEXT, 'foo'); + + $map = array(); + $container = $this->getMockBuilder(Container::class)->getMock(); + $container->expects($this->never())->method('get'); + + $firewallMap = new FirewallMap($container, $map); + + $this->assertEquals(array(array(), null), $firewallMap->getListeners($request)); + $this->assertNull($firewallMap->getFirewallConfig($request)); + $this->assertFalse($request->attributes->has(self::ATTRIBUTE_FIREWALL_CONTEXT)); + } + + public function testGetListeners() + { + $request = new Request(); + + $firewallContext = $this->getMockBuilder(FirewallContext::class)->disableOriginalConstructor()->getMock(); + + $firewallConfig = new FirewallConfig('main', $this->getMockBuilder(UserCheckerInterface::class)->getMock()); + $firewallContext->expects($this->once())->method('getConfig')->willReturn($firewallConfig); + + $listener = $this->getMockBuilder(ListenerInterface::class)->getMock(); + $firewallContext->expects($this->once())->method('getListeners')->willReturn(array($listener)); + + $exceptionListener = $this->getMockBuilder(ExceptionListener::class)->disableOriginalConstructor()->getMock(); + $firewallContext->expects($this->once())->method('getExceptionListener')->willReturn($exceptionListener); + + $matcher = $this->getMockBuilder(RequestMatcherInterface::class)->getMock(); + $matcher->expects($this->once()) + ->method('matches') + ->with($request) + ->willReturn(true); + + $container = $this->getMockBuilder(Container::class)->getMock(); + $container->expects($this->exactly(2))->method('get')->willReturn($firewallContext); + + $firewallMap = new FirewallMap($container, array('security.firewall.map.context.foo' => $matcher)); + + $this->assertEquals(array(array($listener), $exceptionListener), $firewallMap->getListeners($request)); + $this->assertEquals($firewallConfig, $firewallMap->getFirewallConfig($request)); + $this->assertEquals('security.firewall.map.context.foo', $request->attributes->get(self::ATTRIBUTE_FIREWALL_CONTEXT)); + } +} diff --git a/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php b/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php index d648d63dd5688..f2db82b0995b0 100644 --- a/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php +++ b/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php @@ -72,7 +72,7 @@ public function showAction(Request $request, FlattenException $exception, DebugL 'logger' => $logger, 'currentContent' => $currentContent, ) - )); + ), 200, array('Content-Type' => $request->getMimeType($request->getRequestFormat()) ?: 'text/html')); } /** diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php index 0bd1f9049f67f..31d1b758eff56 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php @@ -61,10 +61,7 @@ public function process(ContainerBuilder $container) $container->getDefinition('twig.extension.httpkernel')->addTag('twig.extension'); // inject Twig in the hinclude service if Twig is the only registered templating engine - if ( - !$container->hasParameter('templating.engines') - || array('twig') == $container->getParameter('templating.engines') - ) { + if ((!$container->hasParameter('templating.engines') || array('twig') == $container->getParameter('templating.engines')) && $container->hasDefinition('fragment.renderer.hinclude')) { $container->getDefinition('fragment.renderer.hinclude') ->addTag('kernel.fragment_renderer', array('alias' => 'hinclude')) ->replaceArgument(0, new Reference('twig')) @@ -89,6 +86,7 @@ public function process(ContainerBuilder $container) $twigLoader->clearTag('twig.loader'); } else { $container->setAlias('twig.loader.filesystem', new Alias('twig.loader.native_filesystem', false)); + $container->removeDefinition('templating.engine.twig'); } if ($container->has('assets.packages')) { diff --git a/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php b/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php index 2b9b2502ae83d..a93a9afc58ca3 100644 --- a/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php +++ b/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php @@ -76,7 +76,6 @@ protected function findTemplate($template, $throw = true) } $file = null; - $previous = null; try { $file = parent::findTemplate($logicalName); } catch (LoaderError $e) { diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.html.twig index 4b35e4fad77db..8ed5226ad5ffa 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.html.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.html.twig @@ -13,6 +13,7 @@ + {% if exception.message is not empty %}

@@ -24,6 +25,7 @@

+ {% endif %}
diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.php index cf5722727beb7..5d779c68c2a5e 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.php @@ -22,14 +22,9 @@ class ExceptionControllerTest extends TestCase { public function testShowActionCanBeForcedToShowErrorPage() { - $twig = new Environment( - new ArrayLoader(array( - '@Twig/Exception/error404.html.twig' => 'ok', - )) - ); + $twig = $this->createTwigEnv(array('@Twig/Exception/error404.html.twig' => 'not found')); - $request = Request::create('whatever', 'GET'); - $request->headers->set('X-Php-Ob-Level', 1); + $request = $this->createRequest('html'); $request->attributes->set('showException', false); $exception = FlattenException::create(new \Exception(), 404); $controller = new ExceptionController($twig, /* "showException" defaults to --> */ true); @@ -37,25 +32,61 @@ public function testShowActionCanBeForcedToShowErrorPage() $response = $controller->showAction($request, $exception, null); $this->assertEquals(200, $response->getStatusCode()); // successful request - $this->assertEquals('ok', $response->getContent()); // content of the error404.html template + $this->assertEquals('not found', $response->getContent()); } public function testFallbackToHtmlIfNoTemplateForRequestedFormat() { - $twig = new Environment( - new ArrayLoader(array( - '@Twig/Exception/error.html.twig' => 'html', - )) - ); + $twig = $this->createTwigEnv(array('@Twig/Exception/error.html.twig' => '')); - $request = Request::create('whatever'); - $request->headers->set('X-Php-Ob-Level', 1); - $request->setRequestFormat('txt'); + $request = $this->createRequest('txt'); $exception = FlattenException::create(new \Exception()); $controller = new ExceptionController($twig, false); - $response = $controller->showAction($request, $exception); + $controller->showAction($request, $exception); + + $this->assertEquals('html', $request->getRequestFormat()); + } + + public function testFallbackToHtmlWithFullExceptionIfNoTemplateForRequestedFormatAndExceptionsShouldBeShown() + { + $twig = $this->createTwigEnv(array('@Twig/Exception/exception_full.html.twig' => '')); + + $request = $this->createRequest('txt'); + $request->attributes->set('showException', true); + $exception = FlattenException::create(new \Exception()); + $controller = new ExceptionController($twig, false); + + $controller->showAction($request, $exception); $this->assertEquals('html', $request->getRequestFormat()); } + + public function testResponseHasRequestedMimeType() + { + $twig = $this->createTwigEnv(array('@Twig/Exception/error.json.twig' => '{}')); + + $request = $this->createRequest('json'); + $exception = FlattenException::create(new \Exception()); + $controller = new ExceptionController($twig, false); + + $response = $controller->showAction($request, $exception); + + $this->assertEquals('json', $request->getRequestFormat()); + $this->assertEquals($request->getMimeType('json'), $response->headers->get('Content-Type')); + } + + private function createRequest($requestFormat) + { + $request = Request::create('whatever'); + $request->headers->set('X-Php-Ob-Level', 1); + $request->setRequestFormat($requestFormat); + + return $request; + } + + private function createTwigEnv(array $templates) + { + return new Environment(new ArrayLoader($templates)); + } } diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php index 25c3886379cf7..2de7fd10a461c 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php @@ -136,9 +136,10 @@ public function testGlobalsWithDifferentTypesAndValues() $calls = $container->getDefinition('twig')->getMethodCalls(); foreach (array_slice($calls, 2) as $call) { - list($name, $value) = each($globals); - $this->assertEquals($name, $call[1][0]); - $this->assertSame($value, $call[1][1]); + $this->assertEquals(key($globals), $call[1][0]); + $this->assertSame(current($globals), $call[1][1]); + + next($globals); } } diff --git a/src/Symfony/Bundle/TwigBundle/composer.json b/src/Symfony/Bundle/TwigBundle/composer.json index 11f071b19c32b..1b3cc91c1969c 100644 --- a/src/Symfony/Bundle/TwigBundle/composer.json +++ b/src/Symfony/Bundle/TwigBundle/composer.json @@ -35,7 +35,8 @@ "symfony/yaml": "~2.8|~3.0", "symfony/framework-bundle": "^3.2.8", "symfony/web-link": "~3.3", - "doctrine/annotations": "~1.0" + "doctrine/annotations": "~1.0", + "doctrine/cache": "~1.0" }, "conflict": { "symfony/dependency-injection": "<3.3" diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/cache.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/cache.svg index 984a4efd4a475..5b36ae37e0158 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/cache.svg +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/cache.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/forward.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/forward.svg index acb128509a9f5..96a5bd7d22e02 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/forward.svg +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/forward.svg @@ -1,4 +1,4 @@ - + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/menu.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/menu.svg index 51870801cf284..3c863393bc1ab 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/menu.svg +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/menu.svg @@ -1,3 +1,3 @@ - + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/redirect.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/redirect.svg index 9cb3b89bcfbdd..54ff4187a4c81 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/redirect.svg +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/redirect.svg @@ -1,4 +1,4 @@ - + {% for trace in traces %}
- {{ loop.index }} + {{ loop.index }} {{ trace.name }} {{ trace.path }} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Resources/IconTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Resources/IconTest.php new file mode 100644 index 0000000000000..040d4003f5c54 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Resources/IconTest.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebProfilerBundle\Tests\Resources; + +use PHPUnit\Framework\TestCase; + +class IconTest extends TestCase +{ + /** + * @dataProvider provideIconFilePaths + */ + public function testIconFileContents($iconFilePath) + { + $this->assertRegExp('~.*~s', file_get_contents($iconFilePath), sprintf('The SVG metadata of the %s icon is different than expected (use the same as the other icons).', $iconFilePath)); + } + + public function provideIconFilePaths() + { + return array_map(function ($filePath) { return (array) $filePath; }, glob(__DIR__.'/../../Resources/views/Icon/*.svg')); + } +} diff --git a/src/Symfony/Bundle/WebServerBundle/WebServerConfig.php b/src/Symfony/Bundle/WebServerBundle/WebServerConfig.php index 3c639f2034c75..88ed375dd15e8 100644 --- a/src/Symfony/Bundle/WebServerBundle/WebServerConfig.php +++ b/src/Symfony/Bundle/WebServerBundle/WebServerConfig.php @@ -36,7 +36,18 @@ public function __construct($documentRoot, $env, $address = null, $router = null $this->documentRoot = $documentRoot; $this->env = $env; - $this->router = $router ?: __DIR__.'/Resources/router.php'; + + if (null !== $router) { + $absoluteRouterPath = realpath($router); + + if (false === $absoluteRouterPath) { + throw new \InvalidArgumentException(sprintf('Router script "%s" does not exist.', $router)); + } + + $this->router = $absoluteRouterPath; + } else { + $this->router = __DIR__.'/Resources/router.php'; + } if (null === $address) { $this->hostname = '127.0.0.1'; diff --git a/src/Symfony/Component/BrowserKit/Client.php b/src/Symfony/Component/BrowserKit/Client.php index 286f2f98c45d1..50bcca301a3e2 100644 --- a/src/Symfony/Component/BrowserKit/Client.php +++ b/src/Symfony/Component/BrowserKit/Client.php @@ -468,6 +468,7 @@ public function followRedirect() if (-1 !== $this->maxRedirects) { if ($this->redirectCount > $this->maxRedirects) { + $this->redirectCount = 0; throw new \LogicException(sprintf('The maximum number (%d) of redirections was reached.', $this->maxRedirects)); } } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php index b0d2b9cb8a105..90c9ece8bcc9d 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php @@ -41,4 +41,30 @@ public function createCachePool($defaultLifetime = 0) { return new PdoAdapter('sqlite:'.self::$dbFile, 'ns', $defaultLifetime); } + + public function testCleanupExpiredItems() + { + $pdo = new \PDO('sqlite:'.self::$dbFile); + + $getCacheItemCount = function () use ($pdo) { + return (int) $pdo->query('SELECT COUNT(*) FROM cache_items')->fetch(\PDO::FETCH_COLUMN); + }; + + $this->assertSame(0, $getCacheItemCount()); + + $cache = $this->createCachePool(); + + $item = $cache->getItem('some_nice_key'); + $item->expiresAfter(1); + $item->set(1); + + $cache->save($item); + $this->assertSame(1, $getCacheItemCount()); + + sleep(2); + + $newItem = $cache->getItem($item->getKey()); + $this->assertFalse($newItem->isHit()); + $this->assertSame(0, $getCacheItemCount(), 'PDOAdapter must clean up expired items'); + } } diff --git a/src/Symfony/Component/Cache/Traits/ApcuTrait.php b/src/Symfony/Component/Cache/Traits/ApcuTrait.php index 3acb219aa60a5..7f1e87e2ada59 100644 --- a/src/Symfony/Component/Cache/Traits/ApcuTrait.php +++ b/src/Symfony/Component/Cache/Traits/ApcuTrait.php @@ -23,7 +23,7 @@ trait ApcuTrait { public static function isSupported() { - return function_exists('apcu_fetch') && ini_get('apc.enabled') && !('cli' === PHP_SAPI && !ini_get('apc.enable_cli')); + return function_exists('apcu_fetch') && ini_get('apc.enabled'); } private function init($namespace, $defaultLifetime, $version) @@ -71,7 +71,7 @@ protected function doHave($id) */ protected function doClear($namespace) { - return isset($namespace[0]) && class_exists('APCuIterator', false) + return isset($namespace[0]) && class_exists('APCuIterator', false) && ('cli' !== PHP_SAPI || ini_get('apc.enable_cli')) ? apcu_delete(new \APCuIterator(sprintf('/^%s/', preg_quote($namespace, '/')), APC_ITER_KEY)) : apcu_clear_cache(); } diff --git a/src/Symfony/Component/Cache/Traits/PdoTrait.php b/src/Symfony/Component/Cache/Traits/PdoTrait.php index 08b55ab72c003..20d1eee1bd1b6 100644 --- a/src/Symfony/Component/Cache/Traits/PdoTrait.php +++ b/src/Symfony/Component/Cache/Traits/PdoTrait.php @@ -170,7 +170,7 @@ protected function doFetch(array $ids) foreach ($expired as $id) { $stmt->bindValue(++$i, $id); } - $stmt->execute($expired); + $stmt->execute(); } } diff --git a/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php b/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php index f915cd46c8735..259caf0c74168 100644 --- a/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php +++ b/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php @@ -28,7 +28,7 @@ trait PhpFilesTrait public static function isSupported() { - return function_exists('opcache_compile_file') && ini_get('opcache.enable'); + return function_exists('opcache_invalidate') && ini_get('opcache.enable'); } /** @@ -108,7 +108,7 @@ protected function doSave(array $values, $lifetime) $ok = $this->write($file, 'redis instanceof \Predis\Client) { + if ($this->redis instanceof \Predis\Client && !$this->redis->getConnection() instanceof ClusterInterface) { $results = $this->redis->pipeline(function ($redis) use ($generator, &$ids) { foreach ($generator() as $command => $args) { call_user_func_array(array($redis, $command), $args); @@ -308,9 +309,10 @@ private function pipeline(\Closure $generator) foreach ($results as $k => list($h, $c)) { $results[$k] = $connections[$h][$c]; } - } elseif ($this->redis instanceof \RedisCluster) { - // phpredis doesn't support pipelining with RedisCluster + } elseif ($this->redis instanceof \RedisCluster || ($this->redis instanceof \Predis\Client && $this->redis->getConnection() instanceof ClusterInterface)) { + // phpredis & predis don't support pipelining with RedisCluster // see https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#pipelining + // see https://github.com/nrk/predis/issues/267#issuecomment-123781423 $results = array(); foreach ($generator() as $command => $args) { $results[] = call_user_func_array(array($this->redis, $command), $args); diff --git a/src/Symfony/Component/Config/Resource/ComposerResource.php b/src/Symfony/Component/Config/Resource/ComposerResource.php index 56224d16c01d8..64288ea1db2d6 100644 --- a/src/Symfony/Component/Config/Resource/ComposerResource.php +++ b/src/Symfony/Component/Config/Resource/ComposerResource.php @@ -18,16 +18,13 @@ */ class ComposerResource implements SelfCheckingResourceInterface, \Serializable { - private $versions; private $vendors; - private static $runtimeVersion; private static $runtimeVendors; public function __construct() { self::refresh(); - $this->versions = self::$runtimeVersion; $this->vendors = self::$runtimeVendors; } @@ -51,36 +48,23 @@ public function isFresh($timestamp) { self::refresh(); - if (self::$runtimeVersion !== $this->versions) { - return false; - } - return self::$runtimeVendors === $this->vendors; } public function serialize() { - return serialize(array($this->versions, $this->vendors)); + return serialize($this->vendors); } public function unserialize($serialized) { - list($this->versions, $this->vendors) = unserialize($serialized); + $this->vendors = unserialize($serialized); } private static function refresh() { - if (null !== self::$runtimeVersion) { - return; - } - - self::$runtimeVersion = array(); self::$runtimeVendors = array(); - foreach (get_loaded_extensions() as $ext) { - self::$runtimeVersion[$ext] = phpversion($ext); - } - foreach (get_declared_classes() as $class) { if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) { $r = new \ReflectionClass($class); diff --git a/src/Symfony/Component/Config/Resource/GlobResource.php b/src/Symfony/Component/Config/Resource/GlobResource.php index 67625201530f5..20cab1e81d55d 100644 --- a/src/Symfony/Component/Config/Resource/GlobResource.php +++ b/src/Symfony/Component/Config/Resource/GlobResource.php @@ -134,7 +134,7 @@ function (\SplFileInfo $file) { return '.' !== $file->getBasename()[0]; } $prefixLen = strlen($this->prefix); foreach ($finder->followLinks()->sortByName()->in($this->prefix) as $path => $info) { - if (preg_match($regex, substr($path, $prefixLen)) && $info->isFile()) { + if (preg_match($regex, substr('\\' === \DIRECTORY_SEPARATOR ? str_replace('\\', '/', $path) : $path, $prefixLen)) && $info->isFile()) { yield $path => $info; } } diff --git a/src/Symfony/Component/Config/Tests/Resource/GlobResourceTest.php b/src/Symfony/Component/Config/Tests/Resource/GlobResourceTest.php index b84cc9d3ae3b9..bf7291fdd66b8 100644 --- a/src/Symfony/Component/Config/Tests/Resource/GlobResourceTest.php +++ b/src/Symfony/Component/Config/Tests/Resource/GlobResourceTest.php @@ -36,6 +36,15 @@ public function testIterator() $this->assertEquals(array($file => new \SplFileInfo($file)), $paths); $this->assertInstanceOf('SplFileInfo', current($paths)); $this->assertSame($dir, $resource->getPrefix()); + + $resource = new GlobResource($dir, '/**/Resource', true); + + $paths = iterator_to_array($resource); + + $file = $dir.DIRECTORY_SEPARATOR.'Resource'.DIRECTORY_SEPARATOR.'ConditionalClass.php'; + $this->assertEquals(array($file => $file), $paths); + $this->assertInstanceOf('SplFileInfo', current($paths)); + $this->assertSame($dir, $resource->getPrefix()); } public function testIsFreshNonRecursiveDetectsNewFile() diff --git a/src/Symfony/Component/Config/composer.json b/src/Symfony/Component/Config/composer.json index 88090d463da63..7a51c91ed591a 100644 --- a/src/Symfony/Component/Config/composer.json +++ b/src/Symfony/Component/Config/composer.json @@ -20,10 +20,12 @@ "symfony/filesystem": "~2.8|~3.0" }, "require-dev": { + "symfony/finder": "~3.3", "symfony/yaml": "~3.0", "symfony/dependency-injection": "~3.3" }, "conflict": { + "symfony/finder": "<3.3", "symfony/dependency-injection": "<3.3" }, "suggest": { diff --git a/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php index a5114c6db6c1e..453577cd67907 100644 --- a/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php @@ -115,7 +115,7 @@ private function getInputArgumentData(InputArgument $argument) 'is_required' => $argument->isRequired(), 'is_array' => $argument->isArray(), 'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $argument->getDescription()), - 'default' => $argument->getDefault(), + 'default' => INF === $argument->getDefault() ? 'INF' : $argument->getDefault(), ); } @@ -133,7 +133,7 @@ private function getInputOptionData(InputOption $option) 'is_value_required' => $option->isValueRequired(), 'is_multiple' => $option->isArray(), 'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $option->getDescription()), - 'default' => $option->getDefault(), + 'default' => INF === $option->getDefault() ? 'INF' : $option->getDefault(), ); } diff --git a/src/Symfony/Component/Console/Descriptor/TextDescriptor.php b/src/Symfony/Component/Console/Descriptor/TextDescriptor.php index 1829405c98e09..ddb97b60e2a06 100644 --- a/src/Symfony/Component/Console/Descriptor/TextDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/TextDescriptor.php @@ -278,6 +278,10 @@ private function getCommandAliasesText($command) */ private function formatDefaultValue($default) { + if (INF === $default) { + return 'INF'; + } + if (is_string($default)) { $default = OutputFormatter::escape($default); } elseif (is_array($default)) { diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index dd89105f6c935..0801e1d92a133 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -132,8 +132,7 @@ public static function disableStty() * * @return bool|mixed|null|string * - * @throws \Exception - * @throws \RuntimeException + * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden */ private function doAsk(OutputInterface $output, Question $question) { @@ -147,7 +146,7 @@ private function doAsk(OutputInterface $output, Question $question) if ($question->isHidden()) { try { $ret = trim($this->getHiddenResponse($output, $inputStream)); - } catch (\RuntimeException $e) { + } catch (RuntimeException $e) { if (!$question->isHiddenFallback()) { throw $e; } diff --git a/src/Symfony/Component/Console/Tests/Descriptor/ObjectsProvider.php b/src/Symfony/Component/Console/Tests/Descriptor/ObjectsProvider.php index 8f825ecb68395..b4f34ada19ec6 100644 --- a/src/Symfony/Component/Console/Tests/Descriptor/ObjectsProvider.php +++ b/src/Symfony/Component/Console/Tests/Descriptor/ObjectsProvider.php @@ -32,6 +32,7 @@ public static function getInputArguments() 'input_argument_3' => new InputArgument('argument_name', InputArgument::OPTIONAL, 'argument description', 'default_value'), 'input_argument_4' => new InputArgument('argument_name', InputArgument::REQUIRED, "multiline\nargument description"), 'input_argument_with_style' => new InputArgument('argument_name', InputArgument::OPTIONAL, 'argument description', 'style'), + 'input_argument_with_default_inf_value' => new InputArgument('argument_name', InputArgument::OPTIONAL, 'argument description', INF), ); } @@ -46,6 +47,7 @@ public static function getInputOptions() 'input_option_6' => new InputOption('option_name', array('o', 'O'), InputOption::VALUE_REQUIRED, 'option with multiple shortcuts'), 'input_option_with_style' => new InputOption('option_name', 'o', InputOption::VALUE_REQUIRED, 'option description', 'style'), 'input_option_with_style_array' => new InputOption('option_name', 'o', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'option description', array('Hello', 'world')), + 'input_option_with_default_inf_value' => new InputOption('option_name', 'o', InputOption::VALUE_OPTIONAL, 'option description', INF), ); } diff --git a/src/Symfony/Component/Console/Tests/Fixtures/input_argument_with_default_inf_value.json b/src/Symfony/Component/Console/Tests/Fixtures/input_argument_with_default_inf_value.json new file mode 100644 index 0000000000000..b61ecf7b8c23c --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/input_argument_with_default_inf_value.json @@ -0,0 +1,7 @@ +{ + "name": "argument_name", + "is_required": false, + "is_array": false, + "description": "argument description", + "default": "INF" +} diff --git a/src/Symfony/Component/Console/Tests/Fixtures/input_argument_with_default_inf_value.md b/src/Symfony/Component/Console/Tests/Fixtures/input_argument_with_default_inf_value.md new file mode 100644 index 0000000000000..4f4d9b164a775 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/input_argument_with_default_inf_value.md @@ -0,0 +1,7 @@ +#### `argument_name` + +argument description + +* Is required: no +* Is array: no +* Default: `INF` diff --git a/src/Symfony/Component/Console/Tests/Fixtures/input_argument_with_default_inf_value.txt b/src/Symfony/Component/Console/Tests/Fixtures/input_argument_with_default_inf_value.txt new file mode 100644 index 0000000000000..c32d768c6f328 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/input_argument_with_default_inf_value.txt @@ -0,0 +1 @@ + argument_name argument description [default: INF] diff --git a/src/Symfony/Component/Console/Tests/Fixtures/input_argument_with_default_inf_value.xml b/src/Symfony/Component/Console/Tests/Fixtures/input_argument_with_default_inf_value.xml new file mode 100644 index 0000000000000..d457260070be0 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/input_argument_with_default_inf_value.xml @@ -0,0 +1,7 @@ + + + argument description + + INF + + diff --git a/src/Symfony/Component/Console/Tests/Fixtures/input_option_with_default_inf_value.json b/src/Symfony/Component/Console/Tests/Fixtures/input_option_with_default_inf_value.json new file mode 100644 index 0000000000000..7c96ad30405c6 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/input_option_with_default_inf_value.json @@ -0,0 +1,9 @@ +{ + "name": "--option_name", + "shortcut": "-o", + "accept_value": true, + "is_value_required": false, + "is_multiple": false, + "description": "option description", + "default": "INF" +} diff --git a/src/Symfony/Component/Console/Tests/Fixtures/input_option_with_default_inf_value.md b/src/Symfony/Component/Console/Tests/Fixtures/input_option_with_default_inf_value.md new file mode 100644 index 0000000000000..c27e30a0a3291 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/input_option_with_default_inf_value.md @@ -0,0 +1,8 @@ +#### `--option_name|-o` + +option description + +* Accept value: yes +* Is value required: no +* Is multiple: no +* Default: `INF` diff --git a/src/Symfony/Component/Console/Tests/Fixtures/input_option_with_default_inf_value.txt b/src/Symfony/Component/Console/Tests/Fixtures/input_option_with_default_inf_value.txt new file mode 100644 index 0000000000000..d467dcf42327b --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/input_option_with_default_inf_value.txt @@ -0,0 +1 @@ + -o, --option_name[=OPTION_NAME] option description [default: INF] diff --git a/src/Symfony/Component/Console/Tests/Fixtures/input_option_with_default_inf_value.xml b/src/Symfony/Component/Console/Tests/Fixtures/input_option_with_default_inf_value.xml new file mode 100644 index 0000000000000..5d1d21753e9dc --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/input_option_with_default_inf_value.xml @@ -0,0 +1,7 @@ + + diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index d6c4d10d2934a..e25b5a6dd8b2a 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -100,6 +100,8 @@ class ErrorHandler private static $stackedErrors = array(); private static $stackedErrorLevels = array(); private static $toStringException = null; + private static $silencedErrorCache = array(); + private static $silencedErrorCount = 0; private static $exitCode = 0; /** @@ -407,7 +409,24 @@ public function handleError($type, $message, $file, $line) $errorAsException = self::$toStringException; self::$toStringException = null; } elseif (!$throw && !($type & $level)) { - $errorAsException = new SilencedErrorContext($type, $file, $line); + if (isset(self::$silencedErrorCache[$message])) { + $lightTrace = null; + $errorAsException = self::$silencedErrorCache[$message]; + ++$errorAsException->count; + } else { + $lightTrace = $this->tracedErrors & $type ? $this->cleanTrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), $type, $file, $line, false) : array(); + $errorAsException = new SilencedErrorContext($type, $file, $line, $lightTrace); + } + + if (100 < ++self::$silencedErrorCount) { + self::$silencedErrorCache = $lightTrace = array(); + self::$silencedErrorCount = 1; + } + self::$silencedErrorCache[$message] = $errorAsException; + + if (null === $lightTrace) { + return; + } } else { if ($scope) { $errorAsException = new ContextErrorException($logMessage, 0, $type, $file, $line, $context); @@ -418,19 +437,7 @@ public function handleError($type, $message, $file, $line) // Clean the trace by removing function arguments and the first frames added by the error handler itself. if ($throw || $this->tracedErrors & $type) { $backtrace = $backtrace ?: $errorAsException->getTrace(); - $lightTrace = $backtrace; - - for ($i = 0; isset($backtrace[$i]); ++$i) { - if (isset($backtrace[$i]['file'], $backtrace[$i]['line']) && $backtrace[$i]['line'] === $line && $backtrace[$i]['file'] === $file) { - $lightTrace = array_slice($lightTrace, 1 + $i); - break; - } - } - if (!($throw || $this->scopedErrors & $type)) { - for ($i = 0; isset($lightTrace[$i]); ++$i) { - unset($lightTrace[$i]['args']); - } - } + $lightTrace = $this->cleanTrace($backtrace, $type, $file, $line, $throw); $this->traceReflector->setValue($errorAsException, $lightTrace); } else { $this->traceReflector->setValue($errorAsException, array()); @@ -687,4 +694,23 @@ protected function getFatalErrorHandlers() new ClassNotFoundFatalErrorHandler(), ); } + + private function cleanTrace($backtrace, $type, $file, $line, $throw) + { + $lightTrace = $backtrace; + + for ($i = 0; isset($backtrace[$i]); ++$i) { + if (isset($backtrace[$i]['file'], $backtrace[$i]['line']) && $backtrace[$i]['line'] === $line && $backtrace[$i]['file'] === $file) { + $lightTrace = array_slice($lightTrace, 1 + $i); + break; + } + } + if (!($throw || $this->scopedErrors & $type)) { + for ($i = 0; isset($lightTrace[$i]); ++$i) { + unset($lightTrace[$i]['args']); + } + } + + return $lightTrace; + } } diff --git a/src/Symfony/Component/Debug/Exception/SilencedErrorContext.php b/src/Symfony/Component/Debug/Exception/SilencedErrorContext.php index 0c3a0e1d9fb58..4be83491b9df7 100644 --- a/src/Symfony/Component/Debug/Exception/SilencedErrorContext.php +++ b/src/Symfony/Component/Debug/Exception/SilencedErrorContext.php @@ -18,15 +18,20 @@ */ class SilencedErrorContext implements \JsonSerializable { + public $count = 1; + private $severity; private $file; private $line; + private $trace; - public function __construct($severity, $file, $line) + public function __construct($severity, $file, $line, array $trace = array(), $count = 1) { $this->severity = $severity; $this->file = $file; $this->line = $line; + $this->trace = $trace; + $this->count = $count; } public function getSeverity() @@ -44,12 +49,19 @@ public function getLine() return $this->line; } + public function getTrace() + { + return $this->trace; + } + public function JsonSerialize() { return array( 'severity' => $this->severity, 'file' => $this->file, 'line' => $this->line, + 'trace' => $this->trace, + 'count' => $this->count, ); } } diff --git a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php index 2fccf2dbe7a9b..a14accf3df9a0 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -221,12 +221,17 @@ public function testHandleError() $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); - $logArgCheck = function ($level, $message, $context) { + $line = null; + $logArgCheck = function ($level, $message, $context) use (&$line) { $this->assertEquals('Notice: Undefined variable: undefVar', $message); $this->assertArrayHasKey('exception', $context); $exception = $context['exception']; $this->assertInstanceOf(SilencedErrorContext::class, $exception); $this->assertSame(E_NOTICE, $exception->getSeverity()); + $this->assertSame(__FILE__, $exception->getFile()); + $this->assertSame($line, $exception->getLine()); + $this->assertNotEmpty($exception->getTrace()); + $this->assertSame(1, $exception->count); }; $logger @@ -239,6 +244,7 @@ public function testHandleError() $handler->setDefaultLogger($logger, E_NOTICE); $handler->screamAt(E_NOTICE); unset($undefVar); + $line = __LINE__ + 1; @$undefVar++; restore_error_handler(); diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php index a2e62f4e536a5..82f13ed2d0e17 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php @@ -107,6 +107,9 @@ private function processDefinition(ContainerBuilder $container, $id, Definition while (0 <= --$i) { foreach ($instanceofTags[$i] as $k => $v) { foreach ($v as $v) { + if ($definition->hasTag($k) && in_array($v, $definition->getTag($k))) { + continue; + } $definition->addTag($k, $v); } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveParameterPlaceHoldersPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveParameterPlaceHoldersPass.php index 2444e441ee53e..69d48d9ef246b 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveParameterPlaceHoldersPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveParameterPlaceHoldersPass.php @@ -65,10 +65,14 @@ protected function processValue($value, $isRoot = false) if (isset($changes['file'])) { $value->setFile($this->bag->resolveValue($value->getFile())); } - $value->setProperties($this->bag->resolveValue($value->getProperties())); - $value->setMethodCalls($this->bag->resolveValue($value->getMethodCalls())); } - return parent::processValue($value, $isRoot); + $value = parent::processValue($value, $isRoot); + + if ($value && is_array($value)) { + $value = array_combine($this->bag->resolveValue(array_keys($value)), $value); + } + + return $value; } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveInstanceofConditionalsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveInstanceofConditionalsPassTest.php index c6c3681025606..164ab25941d64 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveInstanceofConditionalsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveInstanceofConditionalsPassTest.php @@ -130,6 +130,29 @@ public function testProcessUsesAutoconfiguredInstanceof() $this->assertSame(array('local_instanceof_tag' => array(array()), 'autoconfigured_tag' => array(array())), $def->getTags()); } + public function testAutoconfigureInstanceofDoesNotDuplicateTags() + { + $container = new ContainerBuilder(); + $def = $container->register('normal_service', self::class); + $def + ->addTag('duplicated_tag') + ->addTag('duplicated_tag', array('and_attributes' => 1)) + ; + $def->setInstanceofConditionals(array( + parent::class => (new ChildDefinition(''))->addTag('duplicated_tag'), + )); + $def->setAutoconfigured(true); + $container->registerForAutoconfiguration(parent::class) + ->addTag('duplicated_tag', array('and_attributes' => 1)) + ; + + (new ResolveInstanceofConditionalsPass())->process($container); + (new ResolveDefinitionTemplatesPass())->process($container); + + $def = $container->getDefinition('normal_service'); + $this->assertSame(array('duplicated_tag' => array(array(), array('and_attributes' => 1))), $def->getTags()); + } + public function testProcessDoesNotUseAutoconfiguredInstanceofIfNotEnabled() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveParameterPlaceHoldersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveParameterPlaceHoldersPassTest.php index 50be82d741119..b9459729e2e80 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveParameterPlaceHoldersPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveParameterPlaceHoldersPassTest.php @@ -41,12 +41,12 @@ public function testFactoryParametersShouldBeResolved() public function testArgumentParametersShouldBeResolved() { - $this->assertSame(array('bar', 'baz'), $this->fooDefinition->getArguments()); + $this->assertSame(array('bar', array('bar' => 'baz')), $this->fooDefinition->getArguments()); } public function testMethodCallParametersShouldBeResolved() { - $this->assertSame(array(array('foobar', array('bar', 'baz'))), $this->fooDefinition->getMethodCalls()); + $this->assertSame(array(array('foobar', array('bar', array('bar' => 'baz')))), $this->fooDefinition->getMethodCalls()); } public function testPropertyParametersShouldBeResolved() @@ -71,7 +71,7 @@ private function createContainerBuilder() $containerBuilder->setParameter('foo.class', 'Foo'); $containerBuilder->setParameter('foo.factory.class', 'FooFactory'); $containerBuilder->setParameter('foo.arg1', 'bar'); - $containerBuilder->setParameter('foo.arg2', 'baz'); + $containerBuilder->setParameter('foo.arg2', array('%foo.arg1%' => 'baz')); $containerBuilder->setParameter('foo.method', 'foobar'); $containerBuilder->setParameter('foo.property.name', 'bar'); $containerBuilder->setParameter('foo.property.value', 'baz'); @@ -80,7 +80,7 @@ private function createContainerBuilder() $fooDefinition = $containerBuilder->register('foo', '%foo.class%'); $fooDefinition->setFactory(array('%foo.factory.class%', 'getFoo')); - $fooDefinition->setArguments(array('%foo.arg1%', '%foo.arg2%')); + $fooDefinition->setArguments(array('%foo.arg1%', array('%foo.arg1%' => 'baz'))); $fooDefinition->addMethodCall('%foo.method%', array('%foo.arg1%', '%foo.arg2%')); $fooDefinition->setProperty('%foo.property.name%', '%foo.property.value%'); $fooDefinition->setFile('%foo.file%'); diff --git a/src/Symfony/Component/Dotenv/Dotenv.php b/src/Symfony/Component/Dotenv/Dotenv.php index 1e665fb537a70..00f27bb7b3cdd 100644 --- a/src/Symfony/Component/Dotenv/Dotenv.php +++ b/src/Symfony/Component/Dotenv/Dotenv.php @@ -168,69 +168,110 @@ private function lexValue() throw $this->createFormatException('Whitespace are not supported before the value'); } - $value = ''; - $singleQuoted = false; - $notQuoted = false; - if ("'" === $this->data[$this->cursor]) { - $singleQuoted = true; - ++$this->cursor; - while ("\n" !== $this->data[$this->cursor]) { - if ("'" === $this->data[$this->cursor]) { - if ($this->cursor + 1 === $this->end) { - break; - } - if ("'" !== $this->data[$this->cursor + 1]) { + $v = ''; + + do { + if ("'" === $this->data[$this->cursor]) { + $value = ''; + ++$this->cursor; + + while ("\n" !== $this->data[$this->cursor]) { + if ("'" === $this->data[$this->cursor]) { break; } - + $value .= $this->data[$this->cursor]; ++$this->cursor; - } - $value .= $this->data[$this->cursor]; - ++$this->cursor; - if ($this->cursor === $this->end) { + if ($this->cursor === $this->end) { + throw $this->createFormatException('Missing quote to end the value'); + } + } + if ("\n" === $this->data[$this->cursor]) { throw $this->createFormatException('Missing quote to end the value'); } - } - if ("\n" === $this->data[$this->cursor]) { - throw $this->createFormatException('Missing quote to end the value'); - } - ++$this->cursor; - } elseif ('"' === $this->data[$this->cursor]) { - ++$this->cursor; - while ('"' !== $this->data[$this->cursor] || ('\\' === $this->data[$this->cursor - 1] && '\\' !== $this->data[$this->cursor - 2])) { - $value .= $this->data[$this->cursor]; ++$this->cursor; + $v .= $value; + } elseif ('"' === $this->data[$this->cursor]) { + $value = ''; + ++$this->cursor; + + while ('"' !== $this->data[$this->cursor] || ('\\' === $this->data[$this->cursor - 1] && '\\' !== $this->data[$this->cursor - 2])) { + $value .= $this->data[$this->cursor]; + ++$this->cursor; - if ($this->cursor === $this->end) { + if ($this->cursor === $this->end) { + throw $this->createFormatException('Missing quote to end the value'); + } + } + if ("\n" === $this->data[$this->cursor]) { throw $this->createFormatException('Missing quote to end the value'); } - } - if ("\n" === $this->data[$this->cursor]) { - throw $this->createFormatException('Missing quote to end the value'); - } - ++$this->cursor; - $value = str_replace(array('\\\\', '\\"', '\r', '\n'), array('\\', '"', "\r", "\n"), $value); - } else { - $notQuoted = true; - $prevChr = $this->data[$this->cursor - 1]; - while ($this->cursor < $this->end && "\n" !== $this->data[$this->cursor] && !((' ' === $prevChr || "\t" === $prevChr) && '#' === $this->data[$this->cursor])) { - $value .= $prevChr = $this->data[$this->cursor]; ++$this->cursor; + $value = str_replace(array('\\\\', '\\"', '\r', '\n'), array('\\', '"', "\r", "\n"), $value); + $resolvedValue = $value; + $resolvedValue = $this->resolveVariables($resolvedValue); + $resolvedValue = $this->resolveCommands($resolvedValue); + $v .= $resolvedValue; + } else { + $value = ''; + $prevChr = $this->data[$this->cursor - 1]; + while ($this->cursor < $this->end && !in_array($this->data[$this->cursor], array("\n", '"', "'"), true) && !((' ' === $prevChr || "\t" === $prevChr) && '#' === $this->data[$this->cursor])) { + if ('\\' === $this->data[$this->cursor] && isset($this->data[$this->cursor + 1]) && ('"' === $this->data[$this->cursor + 1] || "'" === $this->data[$this->cursor + 1])) { + ++$this->cursor; + } + + $value .= $prevChr = $this->data[$this->cursor]; + + if ('$' === $this->data[$this->cursor] && isset($this->data[$this->cursor + 1]) && '(' === $this->data[$this->cursor + 1]) { + ++$this->cursor; + $value .= '('.$this->lexNestedExpression().')'; + } + + ++$this->cursor; + } + $value = rtrim($value); + $resolvedValue = $value; + $resolvedValue = $this->resolveVariables($resolvedValue); + $resolvedValue = $this->resolveCommands($resolvedValue); + + if ($resolvedValue === $value && preg_match('/\s+/', $value)) { + throw $this->createFormatException('A value containing spaces must be surrounded by quotes'); + } + + $v .= $resolvedValue; + + if ($this->cursor < $this->end && '#' === $this->data[$this->cursor]) { + break; + } } - $value = rtrim($value); - } + } while ($this->cursor < $this->end && "\n" !== $this->data[$this->cursor]); $this->skipEmptyLines(); - $currentValue = $value; - if (!$singleQuoted) { - $value = $this->resolveVariables($value); - $value = $this->resolveCommands($value); + return $v; + } + + private function lexNestedExpression() + { + ++$this->cursor; + $value = ''; + + while ("\n" !== $this->data[$this->cursor] && ')' !== $this->data[$this->cursor]) { + $value .= $this->data[$this->cursor]; + + if ('(' === $this->data[$this->cursor]) { + $value .= $this->lexNestedExpression().')'; + } + + ++$this->cursor; + + if ($this->cursor === $this->end) { + throw $this->createFormatException('Missing closing parenthesis.'); + } } - if ($notQuoted && $currentValue == $value && preg_match('/\s+/', $value)) { - throw $this->createFormatException('A value containing spaces must be surrounded by quotes'); + if ("\n" === $this->data[$this->cursor]) { + throw $this->createFormatException('Missing closing parenthesis.'); } return $value; diff --git a/src/Symfony/Component/Dotenv/Tests/DotenvTest.php b/src/Symfony/Component/Dotenv/Tests/DotenvTest.php index 85204e5c476ac..a7f211ecd6e37 100644 --- a/src/Symfony/Component/Dotenv/Tests/DotenvTest.php +++ b/src/Symfony/Component/Dotenv/Tests/DotenvTest.php @@ -85,7 +85,6 @@ public function getEnvData() array("FOO='bar'\n", array('FOO' => 'bar')), array("FOO='bar\"foo'\n", array('FOO' => 'bar"foo')), array("FOO=\"bar\\\"foo\"\n", array('FOO' => 'bar"foo')), - array("FOO='bar''foo'\n", array('FOO' => 'bar\'foo')), array('FOO="bar\nfoo"', array('FOO' => "bar\nfoo")), array('FOO="bar\rfoo"', array('FOO' => "bar\rfoo")), array('FOO=\'bar\nfoo\'', array('FOO' => 'bar\nfoo')), @@ -94,8 +93,15 @@ public function getEnvData() array('FOO=" "', array('FOO' => ' ')), array('PATH="c:\\\\"', array('PATH' => 'c:\\')), array("FOO=\"bar\nfoo\"", array('FOO' => "bar\nfoo")), + array('FOO=BAR\\"', array('FOO' => 'BAR"')), + array("FOO=BAR\\'BAZ", array('FOO' => "BAR'BAZ")), + array('FOO=\\"BAR', array('FOO' => '"BAR')), // concatenated values + array("FOO='bar''foo'\n", array('FOO' => 'barfoo')), + array("FOO='bar '' baz'", array('FOO' => 'bar baz')), + array("FOO=bar\nBAR='baz'\"\$FOO\"", array('FOO' => 'bar', 'BAR' => 'bazbar')), + array("FOO='bar '\\'' baz'", array('FOO' => "bar ' baz")), // comments array("#FOO=bar\nBAR=foo", array('BAR' => 'foo')), @@ -146,6 +152,38 @@ public function getEnvData() return $tests; } + public function testLoad() + { + unset($_ENV['FOO']); + unset($_ENV['BAR']); + unset($_SERVER['FOO']); + unset($_SERVER['BAR']); + putenv('FOO'); + putenv('BAR'); + + @mkdir($tmpdir = sys_get_temp_dir().'/dotenv'); + + $path1 = tempnam($tmpdir, 'sf-'); + $path2 = tempnam($tmpdir, 'sf-'); + + file_put_contents($path1, 'FOO=BAR'); + file_put_contents($path2, 'BAR=BAZ'); + + (new DotEnv())->load($path1, $path2); + + $foo = getenv('FOO'); + $bar = getenv('BAR'); + + putenv('FOO'); + putenv('BAR'); + unlink($path1); + unlink($path2); + rmdir($tmpdir); + + $this->assertSame('BAR', $foo); + $this->assertSame('BAZ', $bar); + } + /** * @expectedException \Symfony\Component\Dotenv\Exception\PathException */ diff --git a/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php b/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php index 4029883dd98c7..f7b0273e15aaa 100644 --- a/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php +++ b/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php @@ -15,7 +15,6 @@ use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\VarDumper\Caster\ClassStub; -use Symfony\Component\VarDumper\Cloner\VarCloner; /** * @author Fabien Potencier @@ -30,8 +29,7 @@ class WrappedListener private $dispatcher; private $pretty; private $stub; - - private static $cloner; + private static $hasClassStub; public function __construct($listener, $name, Stopwatch $stopwatch, EventDispatcherInterface $dispatcher = null) { @@ -58,8 +56,8 @@ public function __construct($listener, $name, Stopwatch $stopwatch, EventDispatc $this->name = $name; } - if (null === self::$cloner) { - self::$cloner = class_exists(ClassStub::class) ? new VarCloner() : false; + if (null === self::$hasClassStub) { + self::$hasClassStub = class_exists(ClassStub::class); } } @@ -86,7 +84,7 @@ public function getPretty() public function getInfo($eventName) { if (null === $this->stub) { - $this->stub = false === self::$cloner ? $this->pretty.'()' : new ClassStub($this->pretty.'()', $this->listener); + $this->stub = self::$hasClassStub ? new ClassStub($this->pretty.'()', $this->listener) : $this->pretty.'()'; } return array( diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index a550e8da14f56..320eb151166aa 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -276,6 +276,13 @@ public function rename($origin, $target, $overwrite = false) } if (true !== @rename($origin, $target)) { + if (is_dir($origin)) { + // See https://bugs.php.net/bug.php?id=54097 & http://php.net/manual/en/function.rename.php#113943 + $this->mirror($origin, $target, null, array('override' => $overwrite, 'delete' => $overwrite)); + $this->remove($origin); + + return; + } throw new IOException(sprintf('Cannot rename "%s" to "%s".', $origin, $target), 0, null, $target); } } diff --git a/src/Symfony/Component/Filesystem/Tests/LockHandlerTest.php b/src/Symfony/Component/Filesystem/Tests/LockHandlerTest.php index 0791cebc694b8..9ed871ea5a2a3 100644 --- a/src/Symfony/Component/Filesystem/Tests/LockHandlerTest.php +++ b/src/Symfony/Component/Filesystem/Tests/LockHandlerTest.php @@ -49,7 +49,7 @@ public function testErrorHandlingInLockIfLockPathBecomesUnwritable() $this->markTestSkipped('This test cannot run on Windows.'); } - $lockPath = sys_get_temp_dir().'/'.uniqid(); + $lockPath = sys_get_temp_dir().'/'.uniqid('', true); $e = null; $wrongMessage = null; diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php index 3398c98edd742..8d28376fd1bd2 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -54,7 +54,7 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, if (!is_callable($preferredChoices) && !empty($preferredChoices)) { $preferredChoices = function ($choice) use ($preferredChoices) { - return false !== array_search($choice, $preferredChoices, true); + return in_array($choice, $preferredChoices, true); }; } diff --git a/src/Symfony/Component/Form/FormBuilderInterface.php b/src/Symfony/Component/Form/FormBuilderInterface.php index bd7ce9b61e518..32582336aba9e 100644 --- a/src/Symfony/Component/Form/FormBuilderInterface.php +++ b/src/Symfony/Component/Form/FormBuilderInterface.php @@ -27,7 +27,7 @@ interface FormBuilderInterface extends \Traversable, \Countable, FormConfigBuild * @param string|null $type * @param array $options * - * @return $this + * @return self */ public function add($child, $type = null, array $options = array()); @@ -58,7 +58,7 @@ public function get($name); * * @param string $name * - * @return $this + * @return self */ public function remove($name); diff --git a/src/Symfony/Component/Form/FormRenderer.php b/src/Symfony/Component/Form/FormRenderer.php index 60f4c363d865e..2aa4b917d69d4 100644 --- a/src/Symfony/Component/Form/FormRenderer.php +++ b/src/Symfony/Component/Form/FormRenderer.php @@ -305,6 +305,6 @@ public function searchAndRenderBlock(FormView $view, $blockNameSuffix, array $va */ public function humanize($text) { - return ucfirst(trim(strtolower(preg_replace(array('/([A-Z])/', '/[_\s]+/'), array('_$1', ' '), $text)))); + return ucfirst(strtolower(trim(preg_replace(array('/([A-Z])/', '/[_\s]+/'), array('_$1', ' '), $text)))); } } diff --git a/src/Symfony/Component/Form/FormView.php b/src/Symfony/Component/Form/FormView.php index c1da5f8fc9bb1..8655bedf6e135 100644 --- a/src/Symfony/Component/Form/FormView.php +++ b/src/Symfony/Component/Form/FormView.php @@ -53,6 +53,8 @@ class FormView implements \ArrayAccess, \IteratorAggregate, \Countable */ private $rendered = false; + private $methodRendered = false; + public function __construct(FormView $parent = null) { $this->parent = $parent; @@ -90,6 +92,19 @@ public function setRendered() return $this; } + /** + * @return bool + */ + public function isMethodRendered() + { + return $this->methodRendered; + } + + public function setMethodRendered() + { + $this->methodRendered = true; + } + /** * Returns a child by name (implements \ArrayAccess). * diff --git a/src/Symfony/Component/HttpFoundation/IpUtils.php b/src/Symfony/Component/HttpFoundation/IpUtils.php index 28093be43403f..eba603b15df01 100644 --- a/src/Symfony/Component/HttpFoundation/IpUtils.php +++ b/src/Symfony/Component/HttpFoundation/IpUtils.php @@ -18,6 +18,8 @@ */ class IpUtils { + private static $checkedIps = array(); + /** * This class should not be instantiated. */ @@ -61,26 +63,31 @@ public static function checkIp($requestIp, $ips) */ public static function checkIp4($requestIp, $ip) { + $cacheKey = $requestIp.'-'.$ip; + if (isset(self::$checkedIps[$cacheKey])) { + return self::$checkedIps[$cacheKey]; + } + if (!filter_var($requestIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { - return false; + return self::$checkedIps[$cacheKey] = false; } if (false !== strpos($ip, '/')) { list($address, $netmask) = explode('/', $ip, 2); if ($netmask === '0') { - return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); + return self::$checkedIps[$cacheKey] = filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); } if ($netmask < 0 || $netmask > 32) { - return false; + return self::$checkedIps[$cacheKey] = false; } } else { $address = $ip; $netmask = 32; } - return 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask); + return self::$checkedIps[$cacheKey] = 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask); } /** @@ -100,6 +107,11 @@ public static function checkIp4($requestIp, $ip) */ public static function checkIp6($requestIp, $ip) { + $cacheKey = $requestIp.'-'.$ip; + if (isset(self::$checkedIps[$cacheKey])) { + return self::$checkedIps[$cacheKey]; + } + if (!((extension_loaded('sockets') && defined('AF_INET6')) || @inet_pton('::1'))) { throw new \RuntimeException('Unable to check Ipv6. Check that PHP was not compiled with option "disable-ipv6".'); } @@ -108,7 +120,7 @@ public static function checkIp6($requestIp, $ip) list($address, $netmask) = explode('/', $ip, 2); if ($netmask < 1 || $netmask > 128) { - return false; + return self::$checkedIps[$cacheKey] = false; } } else { $address = $ip; @@ -119,7 +131,7 @@ public static function checkIp6($requestIp, $ip) $bytesTest = unpack('n*', @inet_pton($requestIp)); if (!$bytesAddr || !$bytesTest) { - return false; + return self::$checkedIps[$cacheKey] = false; } for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) { @@ -127,10 +139,10 @@ public static function checkIp6($requestIp, $ip) $left = ($left <= 16) ? $left : 16; $mask = ~(0xffff >> $left) & 0xffff; if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) { - return false; + return self::$checkedIps[$cacheKey] = false; } } - return true; + return self::$checkedIps[$cacheKey] = true; } } diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index 13ff1e05528fe..6fd0707b059ff 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -672,7 +672,17 @@ public static function setTrustedHeaderName($key, $value) { @trigger_error(sprintf('The "%s()" method is deprecated since version 3.3 and will be removed in 4.0. Use the $trustedHeaderSet argument of the Request::setTrustedProxies() method instead.', __METHOD__), E_USER_DEPRECATED); - if (!array_key_exists($key, self::$trustedHeaders)) { + if ('forwarded' === $key) { + $key = self::HEADER_FORWARDED; + } elseif ('client_ip' === $key) { + $key = self::HEADER_CLIENT_IP; + } elseif ('client_host' === $key) { + $key = self::HEADER_CLIENT_HOST; + } elseif ('client_proto' === $key) { + $key = self::HEADER_CLIENT_PROTO; + } elseif ('client_port' === $key) { + $key = self::HEADER_CLIENT_PORT; + } elseif (!array_key_exists($key, self::$trustedHeaders)) { throw new \InvalidArgumentException(sprintf('Unable to set the trusted header name for key "%s".', $key)); } diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php b/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php index 0cc818eaad02a..97161b8d0f8a1 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php @@ -92,6 +92,10 @@ class NativeSessionStorage implements SessionStorageInterface * upload_progress.freq, "1%" * upload_progress.min-freq, "1" * url_rewriter.tags, "a=href,area=href,frame=src,form=,fieldset=" + * sid_length, "32" + * sid_bits_per_character, "5" + * trans_sid_hosts, $_SERVER['HTTP_HOST'] + * trans_sid_tags, "a=href,area=href,frame=src,form=" * * @param array $options Session configuration options * @param AbstractProxy|NativeSessionHandler|\SessionHandlerInterface|null $handler @@ -335,6 +339,7 @@ public function setOptions(array $options) 'use_only_cookies', 'use_trans_sid', 'upload_progress.enabled', 'upload_progress.cleanup', 'upload_progress.prefix', 'upload_progress.name', 'upload_progress.freq', 'upload_progress.min-freq', 'url_rewriter.tags', + 'sid_length', 'sid_bits_per_character', 'trans_sid_hosts', 'trans_sid_tags', )); foreach ($options as $key => $value) { diff --git a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php index 7ae1fbbd61537..f45f99718d318 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php @@ -49,10 +49,8 @@ public function collect(Request $request, Response $response, \Exception $except public function lateCollect() { if (null !== $this->logger) { - $this->data = $this->computeErrorsCount(); - $containerDeprecationLogs = $this->getContainerDeprecationLogs(); - $this->data['deprecation_count'] += count($containerDeprecationLogs); + $this->data = $this->computeErrorsCount($containerDeprecationLogs); $this->data['compiler_logs'] = $this->getContainerCompilerLogs(); $this->data['logs'] = $this->sanitizeLogs(array_merge($this->logger->getLogs(), $containerDeprecationLogs)); $this->data = $this->cloneVar($this->data); @@ -113,11 +111,10 @@ private function getContainerDeprecationLogs() return array(); } - $stubs = array(); $bootTime = filemtime($file); $logs = array(); foreach (unserialize(file_get_contents($file)) as $log) { - $log['context'] = array('exception' => new SilencedErrorContext($log['type'], $log['file'], $log['line'])); + $log['context'] = array('exception' => new SilencedErrorContext($log['type'], $log['file'], $log['line'], $log['trace'], $log['count'])); $log['timestamp'] = $bootTime; $log['priority'] = 100; $log['priorityName'] = 'DEBUG'; @@ -159,15 +156,34 @@ private function sanitizeLogs($logs) continue; } + $message = $log['message']; $exception = $log['context']['exception']; - $errorId = md5("{$exception->getSeverity()}/{$exception->getLine()}/{$exception->getFile()}\0{$log['message']}", true); + + if ($exception instanceof SilencedErrorContext) { + if (isset($silencedLogs[$h = spl_object_hash($exception)])) { + continue; + } + $silencedLogs[$h] = true; + + if (!isset($sanitizedLogs[$message])) { + $sanitizedLogs[$message] = $log + array( + 'errorCount' => 0, + 'scream' => true, + ); + } + $sanitizedLogs[$message]['errorCount'] += $exception->count; + + continue; + } + + $errorId = md5("{$exception->getSeverity()}/{$exception->getLine()}/{$exception->getFile()}\0{$message}", true); if (isset($sanitizedLogs[$errorId])) { ++$sanitizedLogs[$errorId]['errorCount']; } else { $log += array( 'errorCount' => 1, - 'scream' => $exception instanceof SilencedErrorContext, + 'scream' => false, ); $sanitizedLogs[$errorId] = $log; @@ -196,8 +212,9 @@ private function isSilencedOrDeprecationErrorLog(array $log) return false; } - private function computeErrorsCount() + private function computeErrorsCount(array $containerDeprecationLogs) { + $silencedLogs = array(); $count = array( 'error_count' => $this->logger->countErrors(), 'deprecation_count' => 0, @@ -220,14 +237,23 @@ private function computeErrorsCount() } if ($this->isSilencedOrDeprecationErrorLog($log)) { - if ($log['context']['exception'] instanceof SilencedErrorContext) { - ++$count['scream_count']; + $exception = $log['context']['exception']; + if ($exception instanceof SilencedErrorContext) { + if (isset($silencedLogs[$h = spl_object_hash($exception)])) { + continue; + } + $silencedLogs[$h] = true; + $count['scream_count'] += $exception->count; } else { ++$count['deprecation_count']; } } } + foreach ($containerDeprecationLogs as $deprecationLog) { + $count['deprecation_count'] += $deprecationLog['count']; + } + ksort($count['priorities']); return $count; diff --git a/src/Symfony/Component/HttpKernel/EventListener/SurrogateListener.php b/src/Symfony/Component/HttpKernel/EventListener/SurrogateListener.php index dc815a216f6c0..a207ab673171b 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/SurrogateListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/SurrogateListener.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\HttpCache\HttpCache; use Symfony\Component\HttpKernel\HttpCache\SurrogateInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -42,11 +43,24 @@ public function __construct(SurrogateInterface $surrogate = null) */ public function onKernelResponse(FilterResponseEvent $event) { - if (!$event->isMasterRequest() || null === $this->surrogate) { + if (!$event->isMasterRequest()) { return; } - $this->surrogate->addSurrogateControl($event->getResponse()); + $kernel = $event->getKernel(); + $surrogate = $this->surrogate; + if ($kernel instanceof HttpCache) { + $surrogate = $kernel->getSurrogate(); + if (null !== $this->surrogate && $this->surrogate->getName() !== $surrogate->getName()) { + $surrogate = $this->surrogate; + } + } + + if (null === $surrogate) { + return; + } + + $surrogate->addSurrogateControl($event->getResponse()); } public static function getSubscribedEvents() diff --git a/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php index 39a99e6966c22..027b2b1761334 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php @@ -39,7 +39,7 @@ class ResponseCacheStrategy implements ResponseCacheStrategyInterface */ public function add(Response $response) { - if ($response->isValidateable()) { + if (!$response->isFresh() || !$response->isCacheable()) { $this->cacheable = false; } else { $maxAge = $response->getMaxAge(); @@ -70,6 +70,9 @@ public function update(Response $response) if ($response->isValidateable()) { $response->setEtag(null); $response->setLastModified(null); + } + + if (!$response->isFresh()) { $this->cacheable = false; } diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 0cab79884c08c..955be15e9ea12 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -61,11 +61,11 @@ abstract class Kernel implements KernelInterface, TerminableInterface private $projectDir; - const VERSION = '3.3.2'; - const VERSION_ID = 30302; + const VERSION = '3.3.3'; + const VERSION_ID = 30303; const MAJOR_VERSION = 3; const MINOR_VERSION = 3; - const RELEASE_VERSION = 2; + const RELEASE_VERSION = 3; const EXTRA_VERSION = ''; const END_OF_MAINTENANCE = '01/2018'; @@ -545,11 +545,28 @@ protected function initializeContainer() return $previousHandler ? $previousHandler($type, $message, $file, $line) : false; } - $collectedLogs[] = array( + if (isset($collectedLogs[$message])) { + ++$collectedLogs[$message]['count']; + + return; + } + + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + // Clean the trace by removing first frames added by the error handler itself. + for ($i = 0; isset($backtrace[$i]); ++$i) { + if (isset($backtrace[$i]['file'], $backtrace[$i]['line']) && $backtrace[$i]['line'] === $line && $backtrace[$i]['file'] === $file) { + $backtrace = array_slice($backtrace, 1 + $i); + break; + } + } + + $collectedLogs[$message] = array( 'type' => $type, 'message' => $message, 'file' => $file, 'line' => $line, + 'trace' => $backtrace, + 'count' => 1, ); }); } @@ -562,7 +579,7 @@ protected function initializeContainer() if ($this->debug) { restore_error_handler(); - file_put_contents($this->getCacheDir().'/'.$class.'Deprecations.log', serialize($collectedLogs)); + file_put_contents($this->getCacheDir().'/'.$class.'Deprecations.log', serialize(array_values($collectedLogs))); file_put_contents($this->getCacheDir().'/'.$class.'Compiler.log', null !== $container ? implode("\n", $container->getCompiler()->getLog()) : ''); } } diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php index f30a3b6ad94ab..5e4c322223eb3 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php @@ -75,4 +75,148 @@ public function testSharedMaxAgeNotSetIfNotSetInMasterRequest() $this->assertFalse($response->headers->hasCacheControlDirective('s-maxage')); } + + public function testMasterResponseNotCacheableWhenEmbeddedResponseRequiresValidation() + { + $cacheStrategy = new ResponseCacheStrategy(); + + $embeddedResponse = new Response(); + $embeddedResponse->setLastModified(new \DateTime()); + $cacheStrategy->add($embeddedResponse); + + $masterResponse = new Response(); + $masterResponse->setSharedMaxAge(3600); + $cacheStrategy->update($masterResponse); + + $this->assertTrue($masterResponse->headers->hasCacheControlDirective('no-cache')); + $this->assertTrue($masterResponse->headers->hasCacheControlDirective('must-revalidate')); + $this->assertFalse($masterResponse->isFresh()); + } + + public function testValidationOnMasterResponseIsNotPossibleWhenItContainsEmbeddedResponses() + { + $cacheStrategy = new ResponseCacheStrategy(); + + // This master response uses the "validation" model + $masterResponse = new Response(); + $masterResponse->setLastModified(new \DateTime()); + $masterResponse->setEtag('foo'); + + // Embedded response uses "expiry" model + $embeddedResponse = new Response(); + $masterResponse->setSharedMaxAge(3600); + $cacheStrategy->add($embeddedResponse); + + $cacheStrategy->update($masterResponse); + + $this->assertFalse($masterResponse->isValidateable()); + $this->assertFalse($masterResponse->headers->has('Last-Modified')); + $this->assertFalse($masterResponse->headers->has('ETag')); + $this->assertTrue($masterResponse->headers->hasCacheControlDirective('no-cache')); + $this->assertTrue($masterResponse->headers->hasCacheControlDirective('must-revalidate')); + } + + public function testMasterResponseWithValidationIsUnchangedWhenThereIsNoEmbeddedResponse() + { + $cacheStrategy = new ResponseCacheStrategy(); + + $masterResponse = new Response(); + $masterResponse->setLastModified(new \DateTime()); + $cacheStrategy->update($masterResponse); + + $this->assertTrue($masterResponse->isValidateable()); + } + + public function testMasterResponseWithExpirationIsUnchangedWhenThereIsNoEmbeddedResponse() + { + $cacheStrategy = new ResponseCacheStrategy(); + + $masterResponse = new Response(); + $masterResponse->setSharedMaxAge(3600); + $cacheStrategy->update($masterResponse); + + $this->assertTrue($masterResponse->isFresh()); + } + + public function testMasterResponseIsNotCacheableWhenEmbeddedResponseIsNotCacheable() + { + $cacheStrategy = new ResponseCacheStrategy(); + + $masterResponse = new Response(); + $masterResponse->setSharedMaxAge(3600); // Public, cacheable + + /* This response has no validation or expiration information. + That makes it uncacheable, it is always stale. + (It does *not* make this private, though.) */ + $embeddedResponse = new Response(); + $this->assertFalse($embeddedResponse->isFresh()); // not fresh, as no lifetime is provided + + $cacheStrategy->add($embeddedResponse); + $cacheStrategy->update($masterResponse); + + $this->assertTrue($masterResponse->headers->hasCacheControlDirective('no-cache')); + $this->assertTrue($masterResponse->headers->hasCacheControlDirective('must-revalidate')); + $this->assertFalse($masterResponse->isFresh()); + } + + public function testEmbeddingPrivateResponseMakesMainResponsePrivate() + { + $cacheStrategy = new ResponseCacheStrategy(); + + $masterResponse = new Response(); + $masterResponse->setSharedMaxAge(3600); // public, cacheable + + // The embedded response might for example contain per-user data that remains valid for 60 seconds + $embeddedResponse = new Response(); + $embeddedResponse->setPrivate(); + $embeddedResponse->setMaxAge(60); // this would implicitly set "private" as well, but let's be explicit + + $cacheStrategy->add($embeddedResponse); + $cacheStrategy->update($masterResponse); + + $this->assertTrue($masterResponse->headers->hasCacheControlDirective('private')); + // Not sure if we should pass "max-age: 60" in this case, as long as the response is private and + // that's the more conservative of both the master and embedded response...? + } + + public function testResponseIsExiprableWhenEmbeddedResponseCombinesExpiryAndValidation() + { + /* When "expiration wins over validation" (https://symfony.com/doc/current/http_cache/validation.html) + * and both the main and embedded response provide s-maxage, then the more restricting value of both + * should be fine, regardless of whether the embedded response can be validated later on or must be + * completely regenerated. + */ + $cacheStrategy = new ResponseCacheStrategy(); + + $masterResponse = new Response(); + $masterResponse->setSharedMaxAge(3600); + + $embeddedResponse = new Response(); + $embeddedResponse->setSharedMaxAge(60); + $embeddedResponse->setEtag('foo'); + + $cacheStrategy->add($embeddedResponse); + $cacheStrategy->update($masterResponse); + + $this->assertSame('60', $masterResponse->headers->getCacheControlDirective('s-maxage')); + } + + public function testResponseIsExpirableButNotValidateableWhenMasterResponseCombinesExpirationAndValidation() + { + $cacheStrategy = new ResponseCacheStrategy(); + + $masterResponse = new Response(); + $masterResponse->setSharedMaxAge(3600); + $masterResponse->setEtag('foo'); + $masterResponse->setLastModified(new \DateTime()); + + $embeddedResponse = new Response(); + $embeddedResponse->setSharedMaxAge(60); + + $cacheStrategy->add($embeddedResponse); + $cacheStrategy->update($masterResponse); + + $this->assertSame('60', $masterResponse->headers->getCacheControlDirective('s-maxage')); + $this->assertFalse($masterResponse->isValidateable()); + } } diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php index ade11138e3b60..e4d084f44efdb 100644 --- a/src/Symfony/Component/Process/Tests/ProcessTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessTest.php @@ -429,6 +429,9 @@ public function testExitCodeCommandFailed() $this->assertGreaterThan(0, $process->getExitCode()); } + /** + * @group tty + */ public function testTTYCommand() { if ('\\' === DIRECTORY_SEPARATOR) { @@ -444,6 +447,9 @@ public function testTTYCommand() $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus()); } + /** + * @group tty + */ public function testTTYCommandExitCode() { if ('\\' === DIRECTORY_SEPARATOR) { diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 69d7fc0e9f1aa..d0cadcf65722c 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -244,6 +244,9 @@ public function setValue(&$objectOrArray, $propertyPath, $value) } } catch (\TypeError $e) { self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0); + + // It wasn't thrown in this class so rethrow it + throw $e; } finally { if (\PHP_VERSION_ID < 70000 && false !== self::$previousErrorHandler) { restore_error_handler(); @@ -266,7 +269,7 @@ public static function handleError($type, $message, $file, $line, $context) private static function throwInvalidArgumentException($message, $trace, $i) { - if (isset($trace[$i]['file']) && __FILE__ === $trace[$i]['file']) { + if (isset($trace[$i]['file']) && __FILE__ === $trace[$i]['file'] && isset($trace[$i]['args'][0])) { $pos = strpos($message, $delim = 'must be of the type ') ?: (strpos($message, $delim = 'must be an instance of ') ?: strpos($message, $delim = 'must implement interface ')); $pos += strlen($delim); $type = $trace[$i]['args'][0]; @@ -523,7 +526,7 @@ private function readProperty($zval, $property) */ private function getReadAccessInfo($class, $property) { - $key = $class.'..'.$property; + $key = (false !== strpos($class, '@') ? rawurlencode($class) : $class).'..'.$property; if (isset($this->readPropertyCache[$key])) { return $this->readPropertyCache[$key]; @@ -702,7 +705,7 @@ private function writeCollection($zval, $property, $collection, $addMethod, $rem */ private function getWriteAccessInfo($class, $property, $value) { - $key = $class.'..'.$property; + $key = (false !== strpos($class, '@') ? rawurlencode($class) : $class).'..'.$property; if (isset($this->writePropertyCache[$key])) { return $this->writePropertyCache[$key]; diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/ReturnTyped.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/ReturnTyped.php new file mode 100644 index 0000000000000..b6a9852715d79 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/ReturnTyped.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +/** + * @author Kévin Dunglas + */ +class ReturnTyped +{ + public function getFoos(): array + { + return 'It doesn\'t respect the return type on purpose'; + } + + public function addFoo(\DateTime $dateTime) + { + } + + public function removeFoo(\DateTime $dateTime) + { + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassTypeErrorInsideCall.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassTypeErrorInsideCall.php new file mode 100644 index 0000000000000..44a93900fae34 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TestClassTypeErrorInsideCall.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +class TestClassTypeErrorInsideCall +{ + public function expectsDateTime(\DateTime $date) + { + } + + public function getProperty() + { + } + + public function setProperty($property) + { + $this->expectsDateTime(null); // throws TypeError + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index e2ef9296ed50f..b8356500a5881 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -15,12 +15,14 @@ use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; use Symfony\Component\PropertyAccess\PropertyAccessor; +use Symfony\Component\PropertyAccess\Tests\Fixtures\ReturnTyped; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClass; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicCall; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassMagicGet; use Symfony\Component\PropertyAccess\Tests\Fixtures\Ticket5775Object; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassSetValue; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassIsWritable; +use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassTypeErrorInsideCall; use Symfony\Component\PropertyAccess\Tests\Fixtures\TypeHinted; class PropertyAccessorTest extends TestCase @@ -578,4 +580,87 @@ public function testThrowTypeErrorWithInterface() $this->propertyAccessor->setValue($object, 'countable', 'This is a string, \Countable expected.'); } + + /** + * @requires PHP 7.0 + */ + public function testAnonymousClassRead() + { + $value = 'bar'; + + $obj = $this->generateAnonymousClass($value); + + $propertyAccessor = new PropertyAccessor(false, false, new ArrayAdapter()); + + $this->assertEquals($value, $propertyAccessor->getValue($obj, 'foo')); + } + + /** + * @requires PHP 7.0 + */ + public function testAnonymousClassWrite() + { + $value = 'bar'; + + $obj = $this->generateAnonymousClass(''); + + $propertyAccessor = new PropertyAccessor(false, false, new ArrayAdapter()); + $propertyAccessor->setValue($obj, 'foo', $value); + + $this->assertEquals($value, $propertyAccessor->getValue($obj, 'foo')); + } + + private function generateAnonymousClass($value) + { + $obj = eval('return new class($value) + { + private $foo; + + public function __construct($foo) + { + $this->foo = $foo; + } + + /** + * @return mixed + */ + public function getFoo() + { + return $this->foo; + } + + /** + * @param mixed $foo + */ + public function setFoo($foo) + { + $this->foo = $foo; + } + };'); + + return $obj; + } + + /** + * @requires PHP 7.0 + * @expectedException \TypeError + */ + public function testThrowTypeErrorInsideSetterCall() + { + $object = new TestClassTypeErrorInsideCall(); + + $this->propertyAccessor->setValue($object, 'property', 'foo'); + } + + /** + * @requires PHP 7 + * + * @expectedException \TypeError + */ + public function testDoNotDiscardReturnTypeError() + { + $object = new ReturnTyped(); + + $this->propertyAccessor->setValue($object, 'foos', array(new \DateTime())); + } } diff --git a/src/Symfony/Component/Routing/Loader/XmlFileLoader.php b/src/Symfony/Component/Routing/Loader/XmlFileLoader.php index aff0ba27eecf8..8a6e8ce35989f 100644 --- a/src/Symfony/Component/Routing/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Routing/Loader/XmlFileLoader.php @@ -225,7 +225,7 @@ private function parseConfigs(\DOMElement $node, $path) $condition = trim($n->textContent); break; default: - throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "default", "requirement" or "option".', $n->localName, $path)); + throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "default", "requirement", "option" or "condition".', $n->localName, $path)); } } diff --git a/src/Symfony/Component/Routing/Matcher/RedirectableUrlMatcher.php b/src/Symfony/Component/Routing/Matcher/RedirectableUrlMatcher.php index 463bc0d056809..900c59fa37392 100644 --- a/src/Symfony/Component/Routing/Matcher/RedirectableUrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/RedirectableUrlMatcher.php @@ -49,7 +49,7 @@ public function match($pathinfo) protected function handleRouteRequirements($pathinfo, $name, Route $route) { // expression condition - if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), array('context' => $this->context, 'request' => $this->request))) { + if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), array('context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)))) { return array(self::REQUIREMENT_MISMATCH, null); } diff --git a/src/Symfony/Component/Routing/Matcher/TraceableUrlMatcher.php b/src/Symfony/Component/Routing/Matcher/TraceableUrlMatcher.php index cb1a35f4d3023..9085be0424886 100644 --- a/src/Symfony/Component/Routing/Matcher/TraceableUrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/TraceableUrlMatcher.php @@ -105,7 +105,7 @@ protected function matchCollection($pathinfo, RouteCollection $routes) // check condition if ($condition = $route->getCondition()) { - if (!$this->getExpressionLanguage()->evaluate($condition, array('context' => $this->context, 'request' => $this->request))) { + if (!$this->getExpressionLanguage()->evaluate($condition, array('context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)))) { $this->addTrace(sprintf('Condition "%s" does not evaluate to "true"', $condition), self::ROUTE_ALMOST_MATCHES, $name, $route); continue; diff --git a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php index 9786a9b42cc60..c646723b3ab08 100644 --- a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php @@ -207,7 +207,7 @@ protected function getAttributes(Route $route, $name, array $attributes) protected function handleRouteRequirements($pathinfo, $name, Route $route) { // expression condition - if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), array('context' => $this->context, 'request' => $this->request))) { + if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), array('context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)))) { return array(self::REQUIREMENT_MISMATCH, null); } @@ -248,4 +248,19 @@ protected function getExpressionLanguage() return $this->expressionLanguage; } + + /** + * @internal + */ + protected function createRequest($pathinfo) + { + if (!class_exists('Symfony\Component\HttpFoundation\Request')) { + return null; + } + + return Request::create($this->context->getScheme().'://'.$this->context->getHost().$this->context->getBaseUrl().$pathinfo, $this->context->getMethod(), $this->context->getParameters(), array(), array(), array( + 'SCRIPT_FILENAME' => $this->context->getBaseUrl(), + 'SCRIPT_NAME' => $this->context->getBaseUrl(), + )); + } } diff --git a/src/Symfony/Component/Routing/Route.php b/src/Symfony/Component/Routing/Route.php index 34b1b75765dc6..69a7cade10c33 100644 --- a/src/Symfony/Component/Routing/Route.php +++ b/src/Symfony/Component/Routing/Route.php @@ -116,11 +116,7 @@ public function serialize() */ public function unserialize($serialized) { - if (\PHP_VERSION_ID >= 70000) { - $data = unserialize($serialized, array('allowed_classes' => array(CompiledRoute::class))); - } else { - $data = unserialize($serialized); - } + $data = unserialize($serialized); $this->path = $data['path']; $this->host = $data['host']; $this->defaults = $data['defaults']; diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/CustomCompiledRoute.php b/src/Symfony/Component/Routing/Tests/Fixtures/CustomCompiledRoute.php new file mode 100644 index 0000000000000..0f6e198c923d9 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/CustomCompiledRoute.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Tests\Fixtures; + +use Symfony\Component\Routing\CompiledRoute; + +class CustomCompiledRoute extends CompiledRoute +{ +} diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/CustomRouteCompiler.php b/src/Symfony/Component/Routing/Tests/Fixtures/CustomRouteCompiler.php new file mode 100644 index 0000000000000..c2e2afd9afcd9 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/CustomRouteCompiler.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Tests\Fixtures; + +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCompiler; + +class CustomRouteCompiler extends RouteCompiler +{ + /** + * {@inheritdoc} + */ + public static function compile(Route $route) + { + return new CustomCompiledRoute('', '', array(), array()); + } +} diff --git a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php index 06a7779a02012..1eeb5d4360cde 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php @@ -337,6 +337,16 @@ public function testCondition() $matcher->match('/foo'); } + public function testRequestCondition() + { + $coll = new RouteCollection(); + $route = new Route('/foo/{bar}'); + $route->setCondition('request.getBaseUrl() == "/sub/front.php" and request.getPathInfo() == "/foo/bar"'); + $coll->add('foo', $route); + $matcher = new UrlMatcher($coll, new RequestContext('/sub/front.php')); + $this->assertEquals(array('bar' => 'bar', '_route' => 'foo'), $matcher->match('/foo/bar')); + } + public function testDecodeOnce() { $coll = new RouteCollection(); diff --git a/src/Symfony/Component/Routing/Tests/RouteTest.php b/src/Symfony/Component/Routing/Tests/RouteTest.php index b65dfb54085c1..ff7e320c5fc95 100644 --- a/src/Symfony/Component/Routing/Tests/RouteTest.php +++ b/src/Symfony/Component/Routing/Tests/RouteTest.php @@ -220,6 +220,24 @@ public function testSerializeWhenCompiled() $this->assertNotSame($route, $unserialized); } + /** + * Tests that unserialization does not fail when the compiled Route is of a + * class other than CompiledRoute, such as a subclass of it. + */ + public function testSerializeWhenCompiledWithClass() + { + $route = new Route('/', array(), array(), array('compiler_class' => '\Symfony\Component\Routing\Tests\Fixtures\CustomRouteCompiler')); + $this->assertInstanceOf('\Symfony\Component\Routing\Tests\Fixtures\CustomCompiledRoute', $route->compile(), '->compile() returned a proper route'); + + $serialized = serialize($route); + try { + $unserialized = unserialize($serialized); + $this->assertInstanceOf('\Symfony\Component\Routing\Tests\Fixtures\CustomCompiledRoute', $unserialized->compile(), 'the unserialized route compiled successfully'); + } catch (\Exception $e) { + $this->fail('unserializing a route which uses a custom compiled route class'); + } + } + /** * Tests that the serialized representation of a route in one symfony version * also works in later symfony versions, i.e. the unserialized route is in the diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php index 980a8139939c7..52e1126a07726 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php +++ b/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php @@ -25,7 +25,7 @@ class UsernamePasswordToken extends AbstractToken * Constructor. * * @param string|object $user The username (like a nickname, email address, etc.), or a UserInterface instance or an object implementing a __toString method - * @param string $credentials This usually is the password of the user + * @param mixed $credentials This usually is the password of the user * @param string $providerKey The provider key * @param (RoleInterface|string)[] $roles An array of roles * diff --git a/src/Symfony/Component/Security/Core/Validator/Constraints/UserPasswordValidator.php b/src/Symfony/Component/Security/Core/Validator/Constraints/UserPasswordValidator.php index 2dc7fee49992e..5f4c146cab469 100644 --- a/src/Symfony/Component/Security/Core/Validator/Constraints/UserPasswordValidator.php +++ b/src/Symfony/Component/Security/Core/Validator/Constraints/UserPasswordValidator.php @@ -39,6 +39,10 @@ public function validate($password, Constraint $constraint) throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\UserPassword'); } + if (null === $password || '' === $password) { + return; + } + $user = $this->tokenStorage->getToken()->getUser(); if (!$user instanceof UserInterface) { diff --git a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php index 2819018a8c2ae..fc279c0b3ab96 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php @@ -72,7 +72,7 @@ public function __construct(TokenStorageInterface $tokenStorage, AuthenticationT */ public function register(EventDispatcherInterface $dispatcher) { - $dispatcher->addListener(KernelEvents::EXCEPTION, array($this, 'onKernelException')); + $dispatcher->addListener(KernelEvents::EXCEPTION, array($this, 'onKernelException'), 1); } /** diff --git a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php index e9c3e4068d530..821bfae499378 100644 --- a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php @@ -161,7 +161,7 @@ private function attemptSwitchUser(Request $request) */ private function attemptExitUser(Request $request) { - if (false === $original = $this->getOriginalToken($this->tokenStorage->getToken())) { + if (null === ($currentToken = $this->tokenStorage->getToken()) || false === $original = $this->getOriginalToken($currentToken)) { throw new AuthenticationCredentialsNotFoundException('Could not find original Token object.'); } diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php index 43013520c36ba..6b6cb246c985f 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php @@ -65,6 +65,17 @@ public function testEventIsIgnoredIfUsernameIsNotPassedWithTheRequest() $this->assertNull($this->tokenStorage->getToken()); } + /** + * @expectedException \Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException + */ + public function testExitUserThrowsAuthenticationExceptionIfNoCurrentToken() + { + $this->tokenStorage->setToken(null); + $this->request->query->set('_switch_user', '_exit'); + $listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager); + $listener->handle($this->event); + } + /** * @expectedException \Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException */ diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 97d9a5c9f68a4..e618348019843 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -192,7 +192,7 @@ protected function handleCircularReference($object) return call_user_func($this->circularReferenceHandler, $object); } - throw new CircularReferenceException(sprintf('A circular reference has been detected (configured limit: %d).', $this->circularReferenceLimit)); + throw new CircularReferenceException(sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d)', get_class($object), $this->circularReferenceLimit)); } /** diff --git a/src/Symfony/Component/Translation/DataCollectorTranslator.php b/src/Symfony/Component/Translation/DataCollectorTranslator.php index cdfde100b8f9d..48b3c473c5670 100644 --- a/src/Symfony/Component/Translation/DataCollectorTranslator.php +++ b/src/Symfony/Component/Translation/DataCollectorTranslator.php @@ -97,7 +97,7 @@ public function getCatalogue($locale = null) */ public function getFallbackLocales() { - if ($this->translator instanceof Translator) { + if ($this->translator instanceof Translator || method_exists($this->translator, 'getFallbackLocales')) { return $this->translator->getFallbackLocales(); } diff --git a/src/Symfony/Component/Translation/Dumper/IcuResFileDumper.php b/src/Symfony/Component/Translation/Dumper/IcuResFileDumper.php index 77ce27b2aa4ca..42f159501a859 100644 --- a/src/Symfony/Component/Translation/Dumper/IcuResFileDumper.php +++ b/src/Symfony/Component/Translation/Dumper/IcuResFileDumper.php @@ -52,7 +52,7 @@ public function formatCatalogue(MessageCatalogue $messages, $domain, array $opti $resOffset = $this->getPosition($data); - $data .= pack('v', count($messages)) + $data .= pack('v', count($messages->all($domain))) .$indexes .$this->writePadding($data) .$resources @@ -63,11 +63,11 @@ public function formatCatalogue(MessageCatalogue $messages, $domain, array $opti $root = pack('V7', $resOffset + (2 << 28), // Resource Offset + Resource Type 6, // Index length - $keyTop, // Index keys top - $bundleTop, // Index resources top - $bundleTop, // Index bundle top - count($messages), // Index max table length - 0 // Index attributes + $keyTop, // Index keys top + $bundleTop, // Index resources top + $bundleTop, // Index bundle top + count($messages->all($domain)), // Index max table length + 0 // Index attributes ); $header = pack('vC2v4C12@32', diff --git a/src/Symfony/Component/Translation/Loader/FileLoader.php b/src/Symfony/Component/Translation/Loader/FileLoader.php index a7f24f41a6535..1885963a99550 100644 --- a/src/Symfony/Component/Translation/Loader/FileLoader.php +++ b/src/Symfony/Component/Translation/Loader/FileLoader.php @@ -54,7 +54,7 @@ public function load($resource, $locale, $domain = 'messages') return $catalogue; } - /* + /** * @param string $resource * * @return array diff --git a/src/Symfony/Component/Translation/LoggingTranslator.php b/src/Symfony/Component/Translation/LoggingTranslator.php index 469b3d133b9f7..194e554ad9106 100644 --- a/src/Symfony/Component/Translation/LoggingTranslator.php +++ b/src/Symfony/Component/Translation/LoggingTranslator.php @@ -96,7 +96,7 @@ public function getCatalogue($locale = null) */ public function getFallbackLocales() { - if ($this->translator instanceof Translator) { + if ($this->translator instanceof Translator || method_exists($this->translator, 'getFallbackLocales')) { return $this->translator->getFallbackLocales(); } diff --git a/src/Symfony/Component/Validator/Constraints/AbstractComparison.php b/src/Symfony/Component/Validator/Constraints/AbstractComparison.php index 78db943f74f14..e20a8f3fb7b05 100644 --- a/src/Symfony/Component/Validator/Constraints/AbstractComparison.php +++ b/src/Symfony/Component/Validator/Constraints/AbstractComparison.php @@ -30,6 +30,10 @@ abstract class AbstractComparison extends Constraint */ public function __construct($options = null) { + if (null === $options) { + $options = array(); + } + if (is_array($options) && !isset($options['value'])) { throw new ConstraintDefinitionException(sprintf( 'The %s constraint requires the "value" option to be set.', diff --git a/src/Symfony/Component/Validator/Constraints/UrlValidator.php b/src/Symfony/Component/Validator/Constraints/UrlValidator.php index 118a8defc1468..5d6780e4122e3 100644 --- a/src/Symfony/Component/Validator/Constraints/UrlValidator.php +++ b/src/Symfony/Component/Validator/Constraints/UrlValidator.php @@ -47,7 +47,7 @@ public function validate($value, Constraint $constraint) throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\Url'); } - if (null === $value) { + if (null === $value || '' === $value) { return; } diff --git a/src/Symfony/Component/Validator/DependencyInjection/AddConstraintValidatorsPass.php b/src/Symfony/Component/Validator/DependencyInjection/AddConstraintValidatorsPass.php index ec68bc0a8c828..207a1ad49d487 100644 --- a/src/Symfony/Component/Validator/DependencyInjection/AddConstraintValidatorsPass.php +++ b/src/Symfony/Component/Validator/DependencyInjection/AddConstraintValidatorsPass.php @@ -49,7 +49,7 @@ public function process(ContainerBuilder $container) } $container - ->getDefinition('validator.validator_factory') + ->getDefinition($this->validatorFactoryServiceId) ->replaceArgument(0, ServiceLocatorTagPass::register($container, $validators)) ; } diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf index 72d4d92973fdc..30a883bbf239d 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf @@ -76,7 +76,7 @@ This value is too long. It should have {{ limit }} character or less.|This value is too long. It should have {{ limit }} characters or less. - Cette chaine est trop longue. Elle doit avoir au maximum {{ limit }} caractère.|Cette chaine est trop longue. Elle doit avoir au maximum {{ limit }} caractères. + Cette chaîne est trop longue. Elle doit avoir au maximum {{ limit }} caractère.|Cette chaîne est trop longue. Elle doit avoir au maximum {{ limit }} caractères. This value should be {{ limit }} or more. @@ -84,7 +84,7 @@ This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more. - Cette chaine est trop courte. Elle doit avoir au minimum {{ limit }} caractère.|Cette chaine est trop courte. Elle doit avoir au minimum {{ limit }} caractères. + Cette chaîne est trop courte. Elle doit avoir au minimum {{ limit }} caractère.|Cette chaîne est trop courte. Elle doit avoir au minimum {{ limit }} caractères. This value should not be blank. @@ -180,7 +180,7 @@ This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters. - Cette chaine doit avoir exactement {{ limit }} caractère.|Cette chaine doit avoir exactement {{ limit }} caractères. + Cette chaîne doit avoir exactement {{ limit }} caractère.|Cette chaîne doit avoir exactement {{ limit }} caractères. The file was only partially uploaded. diff --git a/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php b/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php index 437c7453cfe83..a613d18ead3fb 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php @@ -65,14 +65,21 @@ protected static function addPhp5Dot5Comparisons(array $comparisons) return $result; } + public function provideInvalidConstraintOptions() + { + return array( + array(null), + array(array()), + ); + } + /** + * @dataProvider provideInvalidConstraintOptions * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException */ - public function testThrowsConstraintExceptionIfNoValueOrProperty() + public function testThrowsConstraintExceptionIfNoValueOrProperty($options) { - $comparison = $this->createConstraint(array()); - - $this->validator->validate('some value', $comparison); + $this->createConstraint($options); } /** @@ -163,11 +170,11 @@ public function provideAllInvalidComparisons() abstract public function provideInvalidComparisons(); /** - * @param array $options Options for the constraint + * @param array|null $options Options for the constraint * * @return Constraint */ - abstract protected function createConstraint(array $options); + abstract protected function createConstraint(array $options = null); /** * @return string|null diff --git a/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php index ad3f0d7737f30..5e079ce525058 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php @@ -24,7 +24,7 @@ protected function createValidator() return new EqualToValidator(); } - protected function createConstraint(array $options) + protected function createConstraint(array $options = null) { return new EqualTo($options); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorTest.php index 0fe3001d5ed09..f13cbd9d396ac 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanOrEqualValidatorTest.php @@ -24,7 +24,7 @@ protected function createValidator() return new GreaterThanOrEqualValidator(); } - protected function createConstraint(array $options) + protected function createConstraint(array $options = null) { return new GreaterThanOrEqual($options); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php index 6742fcb9b9ec8..4347473fe006e 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php @@ -24,7 +24,7 @@ protected function createValidator() return new GreaterThanValidator(); } - protected function createConstraint(array $options) + protected function createConstraint(array $options = null) { return new GreaterThan($options); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php index 0a23db7e046dc..100600d04bb3b 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php @@ -24,7 +24,7 @@ protected function createValidator() return new IdenticalToValidator(); } - protected function createConstraint(array $options) + protected function createConstraint(array $options = null) { return new IdenticalTo($options); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php index 3d0cc902c8219..bbd0604f84e1c 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php @@ -24,7 +24,7 @@ protected function createValidator() return new LessThanOrEqualValidator(); } - protected function createConstraint(array $options) + protected function createConstraint(array $options = null) { return new LessThanOrEqual($options); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php index 807c43adaf46b..d5a5c7378d8da 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php @@ -24,7 +24,7 @@ protected function createValidator() return new LessThanValidator(); } - protected function createConstraint(array $options) + protected function createConstraint(array $options = null) { return new LessThan($options); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php index ed3568b8f50db..fb9aa9f6e8b86 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php @@ -24,7 +24,7 @@ protected function createValidator() return new NotEqualToValidator(); } - protected function createConstraint(array $options) + protected function createConstraint(array $options = null) { return new NotEqualTo($options); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php index d9a3d16f8bfe7..7938b0a7e3899 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php @@ -24,7 +24,7 @@ protected function createValidator() return new NotIdenticalToValidator(); } - protected function createConstraint(array $options) + protected function createConstraint(array $options = null) { return new NotIdenticalTo($options); } diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json index 7c7214b54878a..ca441954b894f 100644 --- a/src/Symfony/Component/Validator/composer.json +++ b/src/Symfony/Component/Validator/composer.json @@ -46,7 +46,6 @@ "symfony/yaml": "", "symfony/config": "", "egulias/email-validator": "Strict (RFC compliant) email validation", - "symfony/property-access": "For using the Expression validator", "symfony/expression-language": "For using the Expression validator" }, "autoload": { diff --git a/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php b/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php index f52972a0efea2..f1f58c0a87263 100644 --- a/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php @@ -83,7 +83,6 @@ public static function castThrowingCasterException(ThrowingCasterException $e, a public static function castSilencedErrorContext(SilencedErrorContext $e, array $a, Stub $stub, $isNested) { $sPrefix = "\0".SilencedErrorContext::class."\0"; - $xPrefix = "\0Exception\0"; if (!isset($a[$s = $sPrefix.'severity'])) { return $a; @@ -93,12 +92,17 @@ public static function castSilencedErrorContext(SilencedErrorContext $e, array $ $a[$s] = new ConstStub(self::$errorTypes[$a[$s]], $a[$s]); } - $trace = array( + $trace = array(array( 'file' => $a[$sPrefix.'file'], 'line' => $a[$sPrefix.'line'], - ); - unset($a[$sPrefix.'file'], $a[$sPrefix.'line']); - $a[Caster::PREFIX_VIRTUAL.'trace'] = new TraceStub(array($trace)); + )); + + if (isset($a[$sPrefix.'trace'])) { + $trace = array_merge($trace, $a[$sPrefix.'trace']); + } + + unset($a[$sPrefix.'file'], $a[$sPrefix.'line'], $a[$sPrefix.'trace']); + $a[Caster::PREFIX_VIRTUAL.'trace'] = new TraceStub($trace); return $a; } diff --git a/src/Symfony/Component/VarDumper/Caster/ResourceCaster.php b/src/Symfony/Component/VarDumper/Caster/ResourceCaster.php index bc9cb115264fb..3cdb27c30879b 100644 --- a/src/Symfony/Component/VarDumper/Caster/ResourceCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/ResourceCaster.php @@ -50,7 +50,7 @@ public static function castStream($stream, array $a, Stub $stub, $isNested) public static function castStreamContext($stream, array $a, Stub $stub, $isNested) { - return stream_context_get_params($stream); + return @stream_context_get_params($stream) ?: $a; } public static function castGd($gd, array $a, Stub $stub, $isNested) diff --git a/src/Symfony/Component/VarDumper/Caster/SplCaster.php b/src/Symfony/Component/VarDumper/Caster/SplCaster.php index cbeb679543651..75ed81f9985a9 100644 --- a/src/Symfony/Component/VarDumper/Caster/SplCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/SplCaster.php @@ -184,7 +184,7 @@ public static function castObjectStorage(\SplObjectStorage $c, array $a, Stub $s $storage = array(); unset($a[Caster::PREFIX_DYNAMIC."\0gcdata"]); // Don't hit https://bugs.php.net/65967 - foreach ($c as $obj) { + foreach (clone $c as $obj) { $storage[spl_object_hash($obj)] = array( 'object' => $obj, 'info' => $c->getInfo(), diff --git a/src/Symfony/Component/Workflow/Tests/WorkflowTest.php b/src/Symfony/Component/Workflow/Tests/WorkflowTest.php index 6c48f44154d25..212bd5f3f9411 100644 --- a/src/Symfony/Component/Workflow/Tests/WorkflowTest.php +++ b/src/Symfony/Component/Workflow/Tests/WorkflowTest.php @@ -263,6 +263,8 @@ public function testApplyWithEventDispatcher() 'workflow.workflow_name.entered.b', 'workflow.workflow_name.entered.c', // Following events are fired because of announce() method + 'workflow.announce', + 'workflow.workflow_name.announce', 'workflow.guard', 'workflow.workflow_name.guard', 'workflow.workflow_name.guard.t2', diff --git a/src/Symfony/Component/Workflow/Workflow.php b/src/Symfony/Component/Workflow/Workflow.php index 6397b9147e784..1791b568d79be 100644 --- a/src/Symfony/Component/Workflow/Workflow.php +++ b/src/Symfony/Component/Workflow/Workflow.php @@ -300,6 +300,9 @@ private function announce($subject, Transition $initialTransition, Marking $mark $event = new Event($subject, $marking, $initialTransition); + $this->dispatcher->dispatch('workflow.announce', $event); + $this->dispatcher->dispatch(sprintf('workflow.%s.announce', $this->name), $event); + foreach ($this->getEnabledTransitions($subject) as $transition) { $this->dispatcher->dispatch(sprintf('workflow.%s.announce.%s', $this->name, $transition->getName()), $event); } diff --git a/src/Symfony/Component/Workflow/composer.json b/src/Symfony/Component/Workflow/composer.json index cba7243523f7c..2e5825f48d0fb 100644 --- a/src/Symfony/Component/Workflow/composer.json +++ b/src/Symfony/Component/Workflow/composer.json @@ -2,7 +2,7 @@ "name": "symfony/workflow", "type": "library", "description": "Symfony Workflow Component", - "keywords": ["workflow", "petrinet", "place", "transition"], + "keywords": ["workflow", "petrinet", "place", "transition", "statemachine", "state"], "homepage": "http://symfony.com", "license": "MIT", "authors": [ diff --git a/src/Symfony/Component/Yaml/Command/LintCommand.php b/src/Symfony/Component/Yaml/Command/LintCommand.php index 48f6d94a03915..271f2d8474109 100644 --- a/src/Symfony/Component/Yaml/Command/LintCommand.php +++ b/src/Symfony/Component/Yaml/Command/LintCommand.php @@ -18,6 +18,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Parser; +use Symfony\Component\Yaml\Yaml; /** * Validates YAML files syntax and outputs encountered errors. @@ -111,7 +112,7 @@ private function validate($content, $file = null) }); try { - $this->getParser()->parse($content); + $this->getParser()->parse($content, Yaml::PARSE_CONSTANT); } catch (ParseException $e) { return array('file' => $file, 'valid' => false, 'message' => $e->getMessage()); } finally { diff --git a/src/Symfony/Component/Yaml/Inline.php b/src/Symfony/Component/Yaml/Inline.php index 7ad53692eb000..5aac7cb89490a 100644 --- a/src/Symfony/Component/Yaml/Inline.php +++ b/src/Symfony/Component/Yaml/Inline.php @@ -493,8 +493,8 @@ private static function parseMapping($mapping, $flags, &$i = 0, $references = ar if (!(Yaml::PARSE_KEYS_AS_STRINGS & $flags)) { $evaluatedKey = self::evaluateScalar($key, $flags, $references); - if ('' !== $key && $evaluatedKey !== $key && !is_string($evaluatedKey)) { - @trigger_error('Implicit casting of incompatible mapping keys to strings is deprecated since version 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Pass the PARSE_KEYS_AS_STRING flag to explicitly enable the type casts.', E_USER_DEPRECATED); + if ('' !== $key && $evaluatedKey !== $key && !is_string($evaluatedKey) && !is_int($evaluatedKey)) { + @trigger_error('Implicit casting of incompatible mapping keys to strings is deprecated since version 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Pass the PARSE_KEYS_AS_STRINGS flag to explicitly enable the type casts.', E_USER_DEPRECATED); } } @@ -519,7 +519,7 @@ private static function parseMapping($mapping, $flags, &$i = 0, $references = ar // Parser cannot abort this mapping earlier, since lines // are processed sequentially. if (isset($output[$key])) { - @trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, self::$parsedLineNumber + 1), E_USER_DEPRECATED); + @trigger_error(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key), E_USER_DEPRECATED); $duplicate = true; } break; @@ -530,7 +530,7 @@ private static function parseMapping($mapping, $flags, &$i = 0, $references = ar // Parser cannot abort this mapping earlier, since lines // are processed sequentially. if (isset($output[$key])) { - @trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, self::$parsedLineNumber + 1), E_USER_DEPRECATED); + @trigger_error(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key), E_USER_DEPRECATED); $duplicate = true; } break; @@ -540,7 +540,7 @@ private static function parseMapping($mapping, $flags, &$i = 0, $references = ar // Parser cannot abort this mapping earlier, since lines // are processed sequentially. if (isset($output[$key])) { - @trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, self::$parsedLineNumber + 1), E_USER_DEPRECATED); + @trigger_error(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key), E_USER_DEPRECATED); $duplicate = true; } --$i; diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php index 4a5f2b91e06bf..887e2184a0efb 100644 --- a/src/Symfony/Component/Yaml/Parser.php +++ b/src/Symfony/Component/Yaml/Parser.php @@ -236,9 +236,9 @@ private function doParse($value, $flags) throw $e; } - if (!(Yaml::PARSE_KEYS_AS_STRINGS & $flags) && !is_string($key)) { - $keyType = is_numeric($key) ? 'numeric key' : 'incompatible key type'; - @trigger_error(sprintf('Implicit casting of %s to string on line %d is deprecated since version 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Pass the PARSE_KEYS_AS_STRING flag to explicitly enable the type casts.', $keyType, $this->getRealCurrentLineNb()), E_USER_DEPRECATED); + if (!(Yaml::PARSE_KEYS_AS_STRINGS & $flags) && !is_string($key) && !is_int($key)) { + $keyType = is_numeric($key) ? 'numeric key' : 'non-string key'; + @trigger_error(sprintf('Implicit casting of %s to string is deprecated since version 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Pass the PARSE_KEYS_AS_STRINGS flag to explicitly enable the type casts.', $keyType), E_USER_DEPRECATED); } // Convert float keys to strings, to avoid being converted to integers by PHP @@ -312,7 +312,7 @@ private function doParse($value, $flags) $data[$key] = null; } } else { - @trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED); + @trigger_error(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key), E_USER_DEPRECATED); } } else { // remember the parsed line number here in case we need it to provide some contexts in error messages below @@ -327,7 +327,7 @@ private function doParse($value, $flags) $data[$key] = $value; } } else { - @trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, $realCurrentLineNbKey + 1), E_USER_DEPRECATED); + @trigger_error(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key), E_USER_DEPRECATED); } } } else { @@ -337,7 +337,7 @@ private function doParse($value, $flags) if ($allowOverwrite || !isset($data[$key])) { $data[$key] = $value; } else { - @trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED); + @trigger_error(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key), E_USER_DEPRECATED); } } if ($isRef) { diff --git a/src/Symfony/Component/Yaml/Tests/Command/LintCommandTest.php b/src/Symfony/Component/Yaml/Tests/Command/LintCommandTest.php index ce6ad480cf2b6..75aa067f22890 100644 --- a/src/Symfony/Component/Yaml/Tests/Command/LintCommandTest.php +++ b/src/Symfony/Component/Yaml/Tests/Command/LintCommandTest.php @@ -51,6 +51,15 @@ public function testLintIncorrectFile() $this->assertContains('Unable to parse at line 3 (near "bar").', trim($tester->getDisplay())); } + public function testConstantAsKey() + { + $yaml = <<createCommandTester()->execute(array('filename' => $this->createFile($yaml)), array('verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false)); + $this->assertSame(0, $ret, 'lint:yaml exits with code 0 in case of success'); + } + /** * @expectedException \RuntimeException */ @@ -105,3 +114,8 @@ protected function tearDown() rmdir(sys_get_temp_dir().'/framework-yml-lint-test'); } } + +class Foo +{ + const TEST = 'foo'; +} diff --git a/src/Symfony/Component/Yaml/Tests/InlineTest.php b/src/Symfony/Component/Yaml/Tests/InlineTest.php index 647904a91632b..89950251cb242 100644 --- a/src/Symfony/Component/Yaml/Tests/InlineTest.php +++ b/src/Symfony/Component/Yaml/Tests/InlineTest.php @@ -730,7 +730,7 @@ public function testTheEmptyStringIsAValidMappingKey() /** * @group legacy - * @expectedDeprecation Implicit casting of incompatible mapping keys to strings is deprecated since version 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Pass the PARSE_KEYS_AS_STRING flag to explicitly enable the type casts. + * @expectedDeprecation Implicit casting of incompatible mapping keys to strings is deprecated since version 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Pass the PARSE_KEYS_AS_STRINGS flag to explicitly enable the type casts. * @dataProvider getNotPhpCompatibleMappingKeyData */ public function testImplicitStringCastingOfMappingKeysIsDeprecated($yaml, $expected) diff --git a/src/Symfony/Component/Yaml/Tests/ParserTest.php b/src/Symfony/Component/Yaml/Tests/ParserTest.php index a6c7b6ad12aff..c1ce9a161fdab 100644 --- a/src/Symfony/Component/Yaml/Tests/ParserTest.php +++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php @@ -853,7 +853,7 @@ public function testMappingDuplicateKeyFlow() /** * @group legacy * @dataProvider getParseExceptionOnDuplicateData - * @expectedDeprecation Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated %s. + * @expectedDeprecation Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated %s. * throws \Symfony\Component\Yaml\Exception\ParseException in 4.0 */ public function testParseExceptionOnDuplicate($input, $duplicateKey, $lineNumber) @@ -1081,7 +1081,7 @@ public function testYamlDirective() /** * @group legacy - * @expectedDeprecation Implicit casting of numeric key to string on line 1 is deprecated since version 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Pass the PARSE_KEYS_AS_STRING flag to explicitly enable the type casts. + * @expectedDeprecation Implicit casting of numeric key to string is deprecated since version 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Pass the PARSE_KEYS_AS_STRINGS flag to explicitly enable the type casts. */ public function testFloatKeys() { @@ -1103,7 +1103,7 @@ public function testFloatKeys() /** * @group legacy - * @expectedDeprecation Implicit casting of incompatible key type to string on line 1 is deprecated since version 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Pass the PARSE_KEYS_AS_STRING flag to explicitly enable the type casts. + * @expectedDeprecation Implicit casting of non-string key to string is deprecated since version 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Pass the PARSE_KEYS_AS_STRINGS flag to explicitly enable the type casts. */ public function testBooleanKeys() {