diff --git a/CHANGELOG-3.3.md b/CHANGELOG-3.3.md index 7619e264d1a01..310284444011e 100644 --- a/CHANGELOG-3.3.md +++ b/CHANGELOG-3.3.md @@ -7,6 +7,19 @@ 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.13 (2017-11-16) + + * security #24995 Validate redirect targets using the session cookie domain (nicolas-grekas) + * security #24994 Prevent bundle readers from breaking out of paths (xabbuh) + * security #24993 Ensure that submitted data are uploaded files (xabbuh) + * security #24992 Namespace generated CSRF tokens depending of the current scheme (dunglas) + +* 3.3.12 (2017-11-13) + + * bug #24954 [DI] Fix dumping with custom base class (nicolas-grekas) + * bug #24952 [HttpFoundation] Fix session-related BC break (nicolas-grekas, sroze) + * bug #24929 [Console] Fix traversable autocomplete values (ro0NL) + * 3.3.11 (2017-11-10) * bug #24888 [FrameworkBundle] Specifically inject the debug dispatcher in the collector (ogizanagi) diff --git a/CHANGELOG-3.4.md b/CHANGELOG-3.4.md index fdefa9a3cd435..ff448f9565412 100644 --- a/CHANGELOG-3.4.md +++ b/CHANGELOG-3.4.md @@ -7,6 +7,38 @@ in 3.4 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v3.4.0...v3.4.1 +* 3.4.0-RC1 (2017-11-21) + + * bug #25077 [Bridge/Twig] Let getFlashes starts the session (MatTheCat) + * bug #25082 [HttpKernel] Disable container inlining when legacy inlining has been used (nicolas-grekas) + * bug #25072 [Bridge/PhpUnit] Remove trailing "\n" from ClockMock::microtime(false) (joky) + * bug #25069 [Debug] Fix undefined variable $lightTrace (nicolas-grekas) + * bug #25053 [Serializer] Fixing PropertyNormalizer supports parent properties (Christopher Hertel) + * bug #25055 [DI] Analyze setter-circular deps more precisely (nicolas-grekas) + * feature #25056 [Bridge/PhpUnit] Sync the bridge version installed in vendor/ and in phpunit clone (nicolas-grekas) + * bug #25045 [SecurityBundle] Don't trigger auto-picking notice if provider is set per listener (chalasr) + * bug #25033 [FrameworkBundle] Dont create empty bundles directory by default (ro0NL) + * bug #25037 [DI] Skip hot_path tag for deprecated services as their class might also be (nicolas-grekas) + * bug #25038 [Cache] Memcached options should ignore "lazy" (nicolas-grekas) + * bug #25014 Move deprecation under use statements (greg0ire) + * bug #25030 [Console] Fix ability to disable lazy commands (chalasr) + * bug #25032 [Bridge\PhpUnit] Disable broken auto-require mechanism of phpunit (nicolas-grekas) + * bug #25027 [FrameworkBundle] Hide server:log command based on deps (sroze) + * bug #24991 [DependencyInjection] Single typed argument can be applied on multiple parameters (nicolas-grekas, sroze) + * bug #24983 [Validator] enter the context in which to validate (xabbuh) + * bug #24956 Fix ambiguous pattern (weltling) + * bug #24732 [DependencyInjection] Prevent service:method factory notation in PHP config (vudaltsov) + * bug #24979 [HttpKernel] remove services resetter even when it's an alias (xabbuh) + * bug #24972 [HttpKernel] Fix service arg resolver for controllers as array callables (sroze, nicolas-grekas) + * bug #24971 [FrameworkBundle] Empty event dispatcher earlier in CacheClearCommand (nicolas-grekas) + * security #24995 Validate redirect targets using the session cookie domain (nicolas-grekas) + * security #24994 Prevent bundle readers from breaking out of paths (xabbuh) + * security #24993 Ensure that submitted data are uploaded files (xabbuh) + * security #24992 Namespace generated CSRF tokens depending of the current scheme (dunglas) + * bug #24954 [DI] Fix dumping with custom base class (nicolas-grekas) + * bug #24952 [HttpFoundation] Fix session-related BC break (nicolas-grekas, sroze) + * bug #24943 [FrameworkBundle] Wire the translation.reader service instead of deprecated translation.loader in commands (ogizanagi) + * 3.4.0-BETA4 (2017-11-12) * bug #24874 [TwigBridge] Fixed the .form-check-input class in the bs4 templates (vudaltsov) diff --git a/UPGRADE-3.4.md b/UPGRADE-3.4.md index b3058828ce3f6..77d0a143f84d3 100644 --- a/UPGRADE-3.4.md +++ b/UPGRADE-3.4.md @@ -116,7 +116,7 @@ Form ```php class MyTimezoneType extends AbstractType { - public function. getParent() + public function getParent() { return TimezoneType::class; } diff --git a/composer.json b/composer.json index 7367a90ea8ea4..977024f6cd5bc 100644 --- a/composer.json +++ b/composer.json @@ -98,7 +98,7 @@ "ocramius/proxy-manager": "~0.4|~1.0|~2.0", "predis/predis": "~1.0", "egulias/email-validator": "~1.2,>=1.2.8|~2.0", - "symfony/phpunit-bridge": "~3.2", + "symfony/phpunit-bridge": "~3.4|~4.0", "symfony/security-acl": "~2.8|~3.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0" }, diff --git a/link b/link new file mode 100755 index 0000000000000..f4070998e72b5 --- /dev/null +++ b/link @@ -0,0 +1,59 @@ +#!/usr/bin/env php + +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +require __DIR__.'/src/Symfony/Component/Filesystem/Exception/ExceptionInterface.php'; +require __DIR__.'/src/Symfony/Component/Filesystem/Exception/IOExceptionInterface.php'; +require __DIR__.'/src/Symfony/Component/Filesystem/Exception/IOException.php'; +require __DIR__.'/src/Symfony/Component/Filesystem/Filesystem.php'; + +use Symfony\Component\Filesystem\Filesystem; + +/** + * Links dependencies to components to a local clone of the main symfony/symfony GitHub repository. + * + * @author Kévin Dunglas + */ + +if (2 !== $argc) { + echo 'Link dependencies to components to a local clone of the main symfony/symfony GitHub repository.'.PHP_EOL.PHP_EOL; + echo "Usage: $argv[0] /path/to/the/project".PHP_EOL; + exit(1); +} + +if (!is_dir("$argv[1]/vendor/symfony")) { + echo "The directory \"$argv[1]\" does not exist or the dependencies are not installed, did you forget to run \"composer install\" in your project?".PHP_EOL; + exit(1); +} + +$sfPackages = array('symfony/symfony' => __DIR__); +foreach (glob(__DIR__.'/src/Symfony/{Bundle,Bridge,Component,Component/Security}/*', GLOB_BRACE | GLOB_ONLYDIR | GLOB_NOSORT) as $dir) { + $sfPackages[json_decode(file_get_contents("$dir/composer.json"))->name] = $dir; +} + +$filesystem = new Filesystem(); +foreach (glob("$argv[1]/vendor/symfony/*", GLOB_ONLYDIR | GLOB_NOSORT) as $dir) { + $package = 'symfony/'.basename($dir); + if (is_link($dir)) { + echo "\"$package\" is already a symlink, skipping.".PHP_EOL; + continue; + } + + if (!isset($sfPackages[$package])) { + continue; + } + + $sfDir = '\\' === DIRECTORY_SEPARATOR ? $sfPackages[$package] : $filesystem->makePathRelative($sfPackages[$package], dirname(realpath($dir))); + + $filesystem->remove($dir); + $filesystem->symlink($sfDir, $dir); + echo "\"$package\" has been linked to \"$sfPackages[$package]\".".PHP_EOL; +} diff --git a/phpunit b/phpunit index 53e1a8dc31dbf..86f4cdd5ae0a2 100755 --- a/phpunit +++ b/phpunit @@ -1,7 +1,7 @@ #!/usr/bin/env php + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit; + +/** + * Utility class replacing PHPUnit's implementation of the same class. + * + * All files are blacklisted so that process-isolated tests don't start with broken + * "require_once" statements. Composer is the only supported way to load code there. + */ +class Blacklist +{ + public static $blacklistedClassNames = array(); + + public function getBlacklistedDirectories() + { + $blacklist = array(); + + foreach (get_declared_classes() as $class) { + if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) { + $r = new \ReflectionClass($class); + $v = dirname(dirname($r->getFileName())); + if (file_exists($v.'/composer/installed.json')) { + $blacklist[] = $v; + } + } + } + + return $blacklist; + } + + public function isBlacklisted($file) + { + return true; + } +} + +if (class_exists('PHPUnit\Util\Test')) { + class_alias('Symfony\Bridge\PhpUnit\Blacklist', 'PHPUnit\Util\Blacklist'); +} +if (class_exists('PHPUnit_Util_Test')) { + class_alias('Symfony\Bridge\PhpUnit\Blacklist', 'PHPUnit_Util_Blacklist'); +} diff --git a/src/Symfony/Bridge/PhpUnit/ClockMock.php b/src/Symfony/Bridge/PhpUnit/ClockMock.php index 60d19ffc5f0c7..40ed8ecc0e454 100644 --- a/src/Symfony/Bridge/PhpUnit/ClockMock.php +++ b/src/Symfony/Bridge/PhpUnit/ClockMock.php @@ -66,7 +66,7 @@ public static function microtime($asFloat = false) return self::$now; } - return sprintf("%0.6f %d\n", self::$now - (int) self::$now, (int) self::$now); + return sprintf('%0.6f %d', self::$now - (int) self::$now, (int) self::$now); } public static function register($class) diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php index b114c69c5176c..498919c09991b 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php @@ -15,7 +15,6 @@ use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestSuite; -use PHPUnit\Util\Blacklist; use Symfony\Bridge\PhpUnit\ClockMock; use Symfony\Bridge\PhpUnit\DnsMock; @@ -46,17 +45,6 @@ class SymfonyTestsListenerTrait */ public function __construct(array $mockedNamespaces = array()) { - if (class_exists('PHPUnit_Util_Blacklist')) { - \PHPUnit_Util_Blacklist::$blacklistedClassNames['\Symfony\Bridge\PhpUnit\DeprecationErrorHandler'] = 1; - \PHPUnit_Util_Blacklist::$blacklistedClassNames['\Symfony\Bridge\PhpUnit\SymfonyTestsListener'] = 1; - \PHPUnit_Util_Blacklist::$blacklistedClassNames['\Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListener'] = 1; - \PHPUnit_Util_Blacklist::$blacklistedClassNames['\Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait'] = 1; - } else { - Blacklist::$blacklistedClassNames['\Symfony\Bridge\PhpUnit\DeprecationErrorHandler'] = 1; - Blacklist::$blacklistedClassNames['\Symfony\Bridge\PhpUnit\SymfonyTestsListener'] = 1; - Blacklist::$blacklistedClassNames['\Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait'] = 1; - } - $warn = false; foreach ($mockedNamespaces as $type => $namespaces) { if (!is_array($namespaces)) { @@ -103,7 +91,7 @@ public function globalListenerDisabled() public function startTestSuite($suite) { - if (class_exists('PHPUnit_Util_Blacklist', false)) { + if (class_exists('PHPUnit_Util_Test', false)) { $Test = 'PHPUnit_Util_Test'; } else { $Test = 'PHPUnit\Util\Test'; @@ -190,7 +178,7 @@ public function startTest($test) putenv('SYMFONY_DEPRECATIONS_SERIALIZE='.$this->runsInSeparateProcess); } - if (class_exists('PHPUnit_Util_Blacklist', false)) { + if (class_exists('PHPUnit_Util_Test', false)) { $Test = 'PHPUnit_Util_Test'; $AssertionFailedError = 'PHPUnit_Framework_AssertionFailedError'; } else { @@ -236,7 +224,7 @@ public function addWarning($test, $e, $time) public function endTest($test, $time) { - if (class_exists('PHPUnit_Util_Blacklist', false)) { + if (class_exists('PHPUnit_Util_Test', false)) { $Test = 'PHPUnit_Util_Test'; $BaseTestRunner = 'PHPUnit_Runner_BaseTestRunner'; $Warning = 'PHPUnit_Framework_Warning'; diff --git a/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php b/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php index 64984ee0b962c..b07effe3d27dc 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php @@ -16,20 +16,24 @@ public function test() $this->markTestSkipped('This test cannot be run on HHVM.'); } - exec('php --ri xdebug -d zend_extension=xdebug.so 2> /dev/null', $output, $returnCode); - if (0 !== $returnCode) { - $this->markTestSkipped('Xdebug is required to run this test.'); + if (\PHP_VERSION_ID >= 70000) { + $php = 'phpdbg -qrr'; + } else { + exec('php --ri xdebug -d zend_extension=xdebug.so 2> /dev/null', $output, $returnCode); + if (0 !== $returnCode) { + $this->markTestSkipped('Xdebug is required to run this test.'); + } + $php = 'php -d zend_extension=xdebug.so'; } $dir = __DIR__.'/../Tests/Fixtures/coverage'; - $php = PHP_BINARY; $phpunit = $_SERVER['argv'][0]; - exec("$php -d zend_extension=xdebug.so $phpunit -c $dir/phpunit-without-listener.xml.dist $dir/tests/ --coverage-text", $output); + exec("$php $phpunit -c $dir/phpunit-without-listener.xml.dist $dir/tests/ --coverage-text", $output); $output = implode("\n", $output); $this->assertContains('FooCov', $output); - exec("$php -d zend_extension=xdebug.so $phpunit -c $dir/phpunit-with-listener.xml.dist $dir/tests/ --coverage-text", $output); + exec("$php $phpunit -c $dir/phpunit-with-listener.xml.dist $dir/tests/ --coverage-text", $output); $output = implode("\n", $output); $this->assertNotContains('FooCov', $output); $this->assertContains("SutNotFoundTest::test\nCould not find the tested class.", $output); diff --git a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit index ac358dd4bed8e..3a89787f4f797 100755 --- a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit +++ b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit @@ -26,8 +26,16 @@ if (PHP_VERSION_ID >= 70200) { $PHPUNIT_VERSION = '4.8'; } +$root = __DIR__; +while (!file_exists($root.'/composer.json') || file_exists($root.'/bin/simple-phpunit')) { + if ($root === dirname($root)) { + break; + } + $root = dirname($root); +} + $oldPwd = getcwd(); -$PHPUNIT_DIR = getenv('SYMFONY_PHPUNIT_DIR') ?: (__DIR__.'/.phpunit'); +$PHPUNIT_DIR = getenv('SYMFONY_PHPUNIT_DIR') ?: ($root.'/vendor/bin/.phpunit'); $PHP = defined('PHP_BINARY') ? PHP_BINARY : 'php'; $PHP = escapeshellarg($PHP); if ('phpdbg' === PHP_SAPI) { @@ -68,7 +76,15 @@ if (!file_exists("$PHPUNIT_DIR/phpunit-$PHPUNIT_VERSION/phpunit") || md5_file(__ if (5.1 <= $PHPUNIT_VERSION && $PHPUNIT_VERSION < 5.4) { passthru("$COMPOSER require --no-update phpunit/phpunit-mock-objects \"~3.1.0\""); } - passthru("$COMPOSER require --no-update symfony/phpunit-bridge \"~3.3.11@dev|~3.4.0-beta2@dev|^4.0.0-beta2@dev\""); + if (file_exists($path = $root.'/vendor/symfony/phpunit-bridge')) { + passthru("$COMPOSER require --no-update symfony/phpunit-bridge \"*@dev\""); + passthru("$COMPOSER config repositories.phpunit-bridge path ".escapeshellarg(str_replace('/', DIRECTORY_SEPARATOR, $path))); + if ('\\' === DIRECTORY_SEPARATOR) { + file_put_contents('composer.json', preg_replace('/^( {8})"phpunit-bridge": \{$/m', "$0\n$1 ".'"options": {"symlink": false},', file_get_contents('composer.json'))); + } + } else { + passthru("$COMPOSER require --no-update symfony/phpunit-bridge \"*\""); + } $prevRoot = getenv('COMPOSER_ROOT_VERSION'); putenv("COMPOSER_ROOT_VERSION=$PHPUNIT_VERSION.99"); $exit = proc_close(proc_open("$COMPOSER install --no-dev --prefer-dist --no-progress --ansi", array(), $p, getcwd(), null, array('bypass_shell' => true))); @@ -82,17 +98,6 @@ if (!file_exists("$PHPUNIT_DIR/phpunit-$PHPUNIT_VERSION/phpunit") || md5_file(__ define('PHPUNIT_COMPOSER_INSTALL', __DIR__.'/vendor/autoload.php'); require PHPUNIT_COMPOSER_INSTALL; -if (!class_exists('SymfonyBlacklistPhpunit', false)) { - class SymfonyBlacklistPhpunit {} -} -if (class_exists('PHPUnit_Util_Blacklist')) { - PHPUnit_Util_Blacklist::$blacklistedClassNames['SymfonyBlacklistPhpunit'] = 1; - PHPUnit_Util_Blacklist::$blacklistedClassNames['SymfonyBlacklistSimplePhpunit'] = 1; -} else { - PHPUnit\Util\Blacklist::$blacklistedClassNames['SymfonyBlacklistPhpunit'] = 1; - PHPUnit\Util\Blacklist::$blacklistedClassNames['SymfonyBlacklistSimplePhpunit'] = 1; -} - Symfony\Bridge\PhpUnit\TextUI\Command::main(); EOPHP @@ -211,9 +216,6 @@ if ($components) { } } } elseif (!isset($argv[1]) || 'install' !== $argv[1] || file_exists('install')) { - if (!class_exists('SymfonyBlacklistSimplePhpunit', false)) { - class SymfonyBlacklistSimplePhpunit {} - } array_splice($argv, 1, 0, array('--colors=always')); $_SERVER['argv'] = $argv; $_SERVER['argc'] = ++$argc; diff --git a/src/Symfony/Bridge/PhpUnit/bootstrap.php b/src/Symfony/Bridge/PhpUnit/bootstrap.php index a265a129e6fdc..8e85feaa34269 100644 --- a/src/Symfony/Bridge/PhpUnit/bootstrap.php +++ b/src/Symfony/Bridge/PhpUnit/bootstrap.php @@ -12,6 +12,11 @@ use Doctrine\Common\Annotations\AnnotationRegistry; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler; +// Replace the native phpunit Blacklist, it's a broken artifact from the past +if (!class_exists('Symfony\Bridge\PhpUnit\Blacklist', false)) { + require_once __DIR__.'/Blacklist.php'; +} + // Detect if we need to serialize deprecations to a file. if ($file = getenv('SYMFONY_DEPRECATIONS_SERIALIZE')) { DeprecationErrorHandler::collectDeprecations($file); diff --git a/src/Symfony/Bridge/Twig/AppVariable.php b/src/Symfony/Bridge/Twig/AppVariable.php index e8e69d0ee3d68..54a785c0a4336 100644 --- a/src/Symfony/Bridge/Twig/AppVariable.php +++ b/src/Symfony/Bridge/Twig/AppVariable.php @@ -156,10 +156,9 @@ public function getDebug() */ public function getFlashes($types = null) { - // needed to avoid starting the session automatically when looking for flash messages try { $session = $this->getSession(); - if (null === $session || !$session->isStarted()) { + if (null === $session) { return array(); } } catch (\RuntimeException $e) { diff --git a/src/Symfony/Bridge/Twig/Form/TwigRenderer.php b/src/Symfony/Bridge/Twig/Form/TwigRenderer.php index 696738b5a70b7..82521f70cd10b 100644 --- a/src/Symfony/Bridge/Twig/Form/TwigRenderer.php +++ b/src/Symfony/Bridge/Twig/Form/TwigRenderer.php @@ -11,12 +11,12 @@ namespace Symfony\Bridge\Twig\Form; -@trigger_error(sprintf('The %s class is deprecated since version 3.4 and will be removed in 4.0. Use %s instead.', TwigRenderer::class, FormRenderer::class), E_USER_DEPRECATED); - use Symfony\Component\Form\FormRenderer; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Twig\Environment; +@trigger_error(sprintf('The %s class is deprecated since version 3.4 and will be removed in 4.0. Use %s instead.', TwigRenderer::class, FormRenderer::class), E_USER_DEPRECATED); + /** * @author Bernhard Schussek * diff --git a/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php b/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php index f6c678a918072..27eca0c44a74a 100644 --- a/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php +++ b/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php @@ -174,13 +174,8 @@ public function testGetFlashesWithNoRequest() */ public function testGetFlashesWithNoSessionStarted() { - $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); - $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->getMock(); - $request->method('getSession')->willReturn($session); - - $this->setRequestStack($request); - - $this->assertEquals(array(), $this->appVariable->getFlashes()); + $flashMessages = $this->setFlashMessages(false); + $this->assertEquals($flashMessages, $this->appVariable->getFlashes()); } /** @@ -258,7 +253,7 @@ protected function setTokenStorage($user) $token->method('getUser')->willReturn($user); } - private function setFlashMessages() + private function setFlashMessages($sessionHasStarted = true) { $flashMessages = array( 'notice' => array('Notice #1 message'), @@ -269,7 +264,7 @@ private function setFlashMessages() $flashBag->initialize($flashMessages); $session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\Session')->disableOriginalConstructor()->getMock(); - $session->method('isStarted')->willReturn(true); + $session->method('isStarted')->willReturn($sessionHasStarted); $session->method('getFlashBag')->willReturn($flashBag); $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->getMock(); diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php index a674e731f50e7..0f5e072378969 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php @@ -120,9 +120,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } } - // Create the bundles directory otherwise symlink will fail. $bundlesDir = $targetArg.'/bundles/'; - $this->filesystem->mkdir($bundlesDir, 0777); $io = new SymfonyStyle($input, $output); $io->newLine(); @@ -186,10 +184,14 @@ protected function execute(InputInterface $input, OutputInterface $output) } } // remove the assets of the bundles that no longer exist - $dirsToRemove = Finder::create()->depth(0)->directories()->exclude($validAssetDirs)->in($bundlesDir); - $this->filesystem->remove($dirsToRemove); + if (is_dir($bundlesDir)) { + $dirsToRemove = Finder::create()->depth(0)->directories()->exclude($validAssetDirs)->in($bundlesDir); + $this->filesystem->remove($dirsToRemove); + } - $io->table(array('', 'Bundle', 'Method / Error'), $rows); + if ($rows) { + $io->table(array('', 'Bundle', 'Method / Error'), $rows); + } if (0 !== $exitCode) { $io->error('Some errors occurred while installing assets.'); @@ -197,7 +199,7 @@ protected function execute(InputInterface $input, OutputInterface $output) if ($copyUsed) { $io->note('Some assets were installed via copy. If you make changes to these assets you have to run this command again.'); } - $io->success('All assets were successfully installed.'); + $io->success($rows ? 'All assets were successfully installed.' : 'No assets were provided by any bundle.'); } return $exitCode; diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php index 4080d2a3d2de6..f252014e266e6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -16,6 +16,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface; use Symfony\Component\HttpKernel\KernelInterface; @@ -111,6 +112,9 @@ protected function execute(InputInterface $input, OutputInterface $output) $io->comment(sprintf('Clearing the cache for the %s environment with debug %s', $kernel->getEnvironment(), var_export($kernel->isDebug(), true))); $this->cacheClearer->clear($realCacheDir); + // The current event dispatcher is stale, let's not use it anymore + $this->getApplication()->setDispatcher(new EventDispatcher()); + if ($input->getOption('no-warmup')) { $this->filesystem->rename($realCacheDir, $oldCacheDir); } else { @@ -127,10 +131,11 @@ protected function execute(InputInterface $input, OutputInterface $output) $io->comment('Removing old cache directory...'); } - $this->filesystem->remove($oldCacheDir); - - // The current event dispatcher is stale, let's not use it anymore - $this->getApplication()->setDispatcher(new EventDispatcher()); + try { + $this->filesystem->remove($oldCacheDir); + } catch (IOException $e) { + $io->warning($e->getMessage()); + } if ($output->isVerbose()) { $io->comment('Finished'); diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index cba3f2db01118..d5030fc9fe7cf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -679,7 +679,7 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode) ->scalarNode('formatter')->defaultValue('translator.formatter.default')->end() ->scalarNode('default_path') ->info('The default path used to load translations') - ->defaultValue('%kernel.project_dir%/config/translations') + ->defaultValue('%kernel.project_dir%/translations') ->end() ->arrayNode('paths') ->prototype('scalar')->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index fc9390379ea06..e2848217cee6c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -13,13 +13,6 @@ use Doctrine\Common\Annotations\Reader; use Symfony\Bridge\Monolog\Processor\DebugProcessor; -use Symfony\Bundle\FrameworkBundle\Command\RouterDebugCommand; -use Symfony\Bundle\FrameworkBundle\Command\RouterMatchCommand; -use Symfony\Bundle\FrameworkBundle\Command\TranslationDebugCommand; -use Symfony\Bundle\FrameworkBundle\Command\TranslationUpdateCommand; -use Symfony\Bundle\FrameworkBundle\Command\WorkflowDumpCommand; -use Symfony\Bundle\FrameworkBundle\Command\XliffLintCommand; -use Symfony\Bundle\FrameworkBundle\Command\YamlLintCommand; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Routing\AnnotatedRouteControllerLoader; @@ -152,10 +145,10 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('console.xml'); if (!class_exists(BaseXliffLintCommand::class)) { - $container->removeDefinition(XliffLintCommand::class); + $container->removeDefinition('console.command.xliff_lint'); } if (!class_exists(BaseYamlLintCommand::class)) { - $container->removeDefinition(YamlLintCommand::class); + $container->removeDefinition('console.command.yaml_lint'); } } @@ -251,7 +244,7 @@ public function load(array $configs, ContainerBuilder $container) $container->removeDefinition('form.type_guesser.validator'); } } else { - $container->removeDefinition('Symfony\Component\Form\Command\DebugCommand'); + $container->removeDefinition('console.command.form_debug'); } $this->registerSecurityCsrfConfiguration($config['csrf_protection'], $container, $loader); @@ -572,7 +565,7 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ private function registerWorkflowConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) { if (!$config['enabled']) { - $container->removeDefinition(WorkflowDumpCommand::class); + $container->removeDefinition('console.command.workflow_dump'); return; } @@ -757,8 +750,8 @@ private function registerDebugConfiguration(array $config, ContainerBuilder $con private function registerRouterConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) { if (!$this->isConfigEnabled($container, $config)) { - $container->removeDefinition(RouterDebugCommand::class); - $container->removeDefinition(RouterMatchCommand::class); + $container->removeDefinition('console.command.router_debug'); + $container->removeDefinition('console.command.router_match'); return; } @@ -1087,8 +1080,8 @@ private function createVersion(ContainerBuilder $container, $version, $format, $ private function registerTranslatorConfiguration(array $config, ContainerBuilder $container, LoaderInterface $loader) { if (!$this->isConfigEnabled($container, $config)) { - $container->removeDefinition(TranslationDebugCommand::class); - $container->removeDefinition(TranslationUpdateCommand::class); + $container->removeDefinition('console.command.translation_debug'); + $container->removeDefinition('console.command.translation_update'); return; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml index cebf6014f0462..9484b985bfad4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml @@ -13,95 +13,95 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + %kernel.default_locale% - + - + - + - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.xml index 7a7fa2a38a03e..8a3ffac2da0ea 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.xml @@ -18,6 +18,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml index e33382ff330a1..f5c44432a3272 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml @@ -68,12 +68,12 @@ - + - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index f86951e04837c..085911b72abf6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -257,7 +257,7 @@ protected static function getBundleDefaultConfig() 'logging' => true, 'formatter' => 'translator.formatter.default', 'paths' => array(), - 'default_path' => '%kernel.project_dir%/config/translations', + 'default_path' => '%kernel.project_dir%/translations', ), 'validation' => array( 'enabled' => !class_exists(FullStack::class), 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 2c5bbae850e9a..116062e1369a7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml @@ -42,7 +42,7 @@ framework: translator: enabled: true fallback: fr - default_path: '%kernel.root_dir%/config/translations' + default_path: '%kernel.root_dir%/translations' paths: ['%kernel.root_dir%/Fixtures/translations'] validation: enabled: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index de998d1bcc36a..ae75b3727c6d8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -12,7 +12,6 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection; use Doctrine\Common\Annotations\Annotation; -use Symfony\Bundle\FrameworkBundle\Command\WorkflowDumpCommand; use Symfony\Bundle\FullStack; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddAnnotationsCachedReaderPass; @@ -314,7 +313,7 @@ public function testWorkflowServicesCanBeEnabled() $container = $this->createContainerFromFile('workflows_enabled'); $this->assertTrue($container->has(Registry::class)); - $this->assertTrue($container->hasDefinition(WorkflowDumpCommand::class)); + $this->assertTrue($container->hasDefinition('console.command.workflow_dump')); } public function testRouter() @@ -504,7 +503,7 @@ public function testTranslator() '->registerTranslatorConfiguration() finds translation resources in custom paths' ); $this->assertContains( - strtr(__DIR__.'/config/translations/test_default.en.xlf', '/', DIRECTORY_SEPARATOR), + strtr(__DIR__.'/translations/test_default.en.xlf', '/', DIRECTORY_SEPARATOR), $files, '->registerTranslatorConfiguration() finds translation resources in default path' ); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/config/translations/test_default.en.xlf b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/translations/test_default.en.xlf similarity index 100% rename from src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/config/translations/test_default.en.xlf rename to src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/translations/test_default.en.xlf diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Validation/Article.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Validation/Article.php index 5f28d0648f388..a1ecee6d0b375 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Validation/Article.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Validation/Article.php @@ -2,9 +2,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Validation; -if (!function_exists('__phpunit_run_isolated_test')) { - class Article implements NotExistingInterface - { - public $category; - } +class Article implements NotExistingInterface +{ + public $category; } diff --git a/src/Symfony/Bundle/SecurityBundle/Command/InitAclCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/InitAclCommand.php index 42e4f8825d888..09947b0c7e209 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/InitAclCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/InitAclCommand.php @@ -11,8 +11,6 @@ namespace Symfony\Bundle\SecurityBundle\Command; -@trigger_error(sprintf('Class "%s" is deprecated since version 3.4 and will be removed in 4.0. Use Symfony\Bundle\AclBundle\Command\SetAclCommand instead.', SetAclCommand::class), E_USER_DEPRECATED); - use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; @@ -22,6 +20,8 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Schema\SchemaException; +@trigger_error(sprintf('Class "%s" is deprecated since version 3.4 and will be removed in 4.0. Use Symfony\Bundle\AclBundle\Command\InitAclCommand instead.', InitAclCommand::class), E_USER_DEPRECATED); + /** * Installs the tables required by the ACL system. * diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSessionDomainConstraintPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSessionDomainConstraintPass.php new file mode 100644 index 0000000000000..3dd18944de9f3 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSessionDomainConstraintPass.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; + +/** + * Uses the session domain to restrict allowed redirection targets. + * + * @author Nicolas Grekas + */ +class AddSessionDomainConstraintPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasParameter('session.storage.options') || !$container->has('security.http_utils')) { + return; + } + + $sessionOptions = $container->getParameter('session.storage.options'); + $domainRegexp = empty($sessionOptions['cookie_domain']) ? '%s' : sprintf('(?:%%s|(?:.+\.)?%s)', preg_quote(trim($sessionOptions['cookie_domain'], '.'))); + $domainRegexp = (empty($sessionOptions['cookie_secure']) ? 'https?://' : 'https://').$domainRegexp; + + $container->findDefinition('security.http_utils')->addArgument(sprintf('{^%s$}i', $domainRegexp)); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 8e82e29e88f17..f777ad1d68344 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -11,9 +11,6 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection; -use Symfony\Bundle\SecurityBundle\Command\InitAclCommand; -use Symfony\Bundle\SecurityBundle\Command\SetAclCommand; -use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; @@ -122,15 +119,15 @@ public function load(array $configs, ContainerBuilder $container) if (class_exists(Application::class)) { $loader->load('console.xml'); - $container->getDefinition(UserPasswordEncoderCommand::class)->replaceArgument(1, array_keys($config['encoders'])); + $container->getDefinition('security.command.user_password_encoder')->replaceArgument(1, array_keys($config['encoders'])); } // load ACL if (isset($config['acl'])) { $this->aclLoad($config['acl'], $container); } else { - $container->removeDefinition(InitAclCommand::class); - $container->removeDefinition(SetAclCommand::class); + $container->removeDefinition('security.command.init_acl'); + $container->removeDefinition('security.command.set_acl'); } $container->registerForAutoconfiguration(VoterInterface::class) @@ -346,17 +343,14 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a $config->replaceArgument(4, $firewall['stateless']); // Provider id (take the first registered provider if none defined) + $defaultProvider = null; if (isset($firewall['provider'])) { if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall['provider'])])) { throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall['provider'])); } $defaultProvider = $providerIds[$normalizedName]; - } else { + } elseif (1 === count($providerIds)) { $defaultProvider = reset($providerIds); - - if (count($providerIds) > 1) { - @trigger_error(sprintf('Firewall "%s" has no "provider" set but multiple providers exist. Using the first configured provider (%s) is deprecated since 3.4 and will throw an exception in 4.0, set the "provider" key on the firewall instead.', $id, key($providerIds)), E_USER_DEPRECATED); - } } $config->replaceArgument(5, $defaultProvider); @@ -500,7 +494,7 @@ private function createContextListener($container, $contextKey) return $this->contextListeners[$contextKey] = $listenerId; } - private function createAuthenticationListeners($container, $id, $firewall, &$authenticationProviders, $defaultProvider, array $providerIds, $defaultEntryPoint) + private function createAuthenticationListeners($container, $id, $firewall, &$authenticationProviders, $defaultProvider = null, array $providerIds, $defaultEntryPoint) { $listeners = array(); $hasListeners = false; @@ -516,7 +510,7 @@ private function createAuthenticationListeners($container, $id, $firewall, &$aut } $userProvider = $providerIds[$normalizedName]; } else { - $userProvider = $defaultProvider; + $userProvider = $defaultProvider ?: $this->getFirstProvider($id, $key, $providerIds); } list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint); @@ -699,7 +693,7 @@ private function createExceptionListener($container, $config, $id, $defaultEntry private function createSwitchUserListener($container, $id, $config, $defaultProvider, $stateless) { - $userProvider = isset($config['provider']) ? $this->getUserProviderId($config['provider']) : $defaultProvider; + $userProvider = isset($config['provider']) ? $this->getUserProviderId($config['provider']) : ($defaultProvider ?: $this->getFirstProvider($id, 'switch_user', $providerIds)); // in 4.0, ignore the `switch_user.stateless` key if $stateless is `true` if ($stateless && false === $config['stateless']) { @@ -803,4 +797,14 @@ private function getExpressionLanguage() return $this->expressionLanguage; } + + /** + * @deprecated since version 3.4, to be removed in 4.0 + */ + private function getFirstProvider($firewallName, $listenerName, array $providerIds) + { + @trigger_error(sprintf('Listener "%s" on firewall "%s" has no "provider" set but multiple providers exist. Using the first configured provider (%s) is deprecated since 3.4 and will throw an exception in 4.0, set the "provider" key on the firewall instead.', $listenerName, $firewallName, $first = array_keys($providerIds)[0]), E_USER_DEPRECATED); + + return $providerIds[$first]; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/console.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/console.xml index b375d95effe5c..a8f9b510a9e00 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/console.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/console.xml @@ -7,18 +7,18 @@ - + - + - + diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php index 4f17e12236659..0304c165a4bca 100644 --- a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php @@ -14,8 +14,10 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\JsonLoginFactory; use Symfony\Component\Console\Application; use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSecurityVotersPass; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSessionDomainConstraintPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginLdapFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpBasicFactory; @@ -58,6 +60,7 @@ public function build(ContainerBuilder $container) $extension->addUserProviderFactory(new InMemoryFactory()); $extension->addUserProviderFactory(new LdapFactory()); $container->addCompilerPass(new AddSecurityVotersPass()); + $container->addCompilerPass(new AddSessionDomainConstraintPass(), PassConfig::TYPE_AFTER_REMOVING); } public function registerCommands(Application $application) diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSessionDomainConstraintPassTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSessionDomainConstraintPassTest.php new file mode 100644 index 0000000000000..5238eb3f842c3 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSessionDomainConstraintPassTest.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSessionDomainConstraintPass; +use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpFoundation\Request; + +class AddSessionDomainConstraintPassTest extends TestCase +{ + public function testSessionCookie() + { + $container = $this->createContainer(array('cookie_domain' => '.symfony.com.', 'cookie_secure' => true)); + + $utils = $container->get('security.http_utils'); + $request = Request::create('/', 'get'); + + $this->assertTrue($utils->createRedirectResponse($request, 'https://symfony.com/blog')->isRedirect('https://symfony.com/blog')); + $this->assertTrue($utils->createRedirectResponse($request, 'https://www.symfony.com/blog')->isRedirect('https://www.symfony.com/blog')); + $this->assertTrue($utils->createRedirectResponse($request, 'https://localhost/foo')->isRedirect('https://localhost/foo')); + $this->assertTrue($utils->createRedirectResponse($request, 'https://www.localhost/foo')->isRedirect('http://localhost/')); + $this->assertTrue($utils->createRedirectResponse($request, 'http://symfony.com/blog')->isRedirect('http://localhost/')); + $this->assertTrue($utils->createRedirectResponse($request, 'http://pirate.com/foo')->isRedirect('http://localhost/')); + } + + public function testSessionNoDomain() + { + $container = $this->createContainer(array('cookie_secure' => true)); + + $utils = $container->get('security.http_utils'); + $request = Request::create('/', 'get'); + + $this->assertTrue($utils->createRedirectResponse($request, 'https://symfony.com/blog')->isRedirect('http://localhost/')); + $this->assertTrue($utils->createRedirectResponse($request, 'https://www.symfony.com/blog')->isRedirect('http://localhost/')); + $this->assertTrue($utils->createRedirectResponse($request, 'https://localhost/foo')->isRedirect('https://localhost/foo')); + $this->assertTrue($utils->createRedirectResponse($request, 'https://www.localhost/foo')->isRedirect('http://localhost/')); + $this->assertTrue($utils->createRedirectResponse($request, 'http://symfony.com/blog')->isRedirect('http://localhost/')); + $this->assertTrue($utils->createRedirectResponse($request, 'http://pirate.com/foo')->isRedirect('http://localhost/')); + } + + public function testSessionNoSecure() + { + $container = $this->createContainer(array('cookie_domain' => '.symfony.com.')); + + $utils = $container->get('security.http_utils'); + $request = Request::create('/', 'get'); + + $this->assertTrue($utils->createRedirectResponse($request, 'https://symfony.com/blog')->isRedirect('https://symfony.com/blog')); + $this->assertTrue($utils->createRedirectResponse($request, 'https://www.symfony.com/blog')->isRedirect('https://www.symfony.com/blog')); + $this->assertTrue($utils->createRedirectResponse($request, 'https://localhost/foo')->isRedirect('https://localhost/foo')); + $this->assertTrue($utils->createRedirectResponse($request, 'https://www.localhost/foo')->isRedirect('http://localhost/')); + $this->assertTrue($utils->createRedirectResponse($request, 'http://symfony.com/blog')->isRedirect('http://symfony.com/blog')); + $this->assertTrue($utils->createRedirectResponse($request, 'http://pirate.com/foo')->isRedirect('http://localhost/')); + } + + public function testSessionNoSecureAndNoDomain() + { + $container = $this->createContainer(array()); + + $utils = $container->get('security.http_utils'); + $request = Request::create('/', 'get'); + + $this->assertTrue($utils->createRedirectResponse($request, 'https://symfony.com/blog')->isRedirect('http://localhost/')); + $this->assertTrue($utils->createRedirectResponse($request, 'https://www.symfony.com/blog')->isRedirect('http://localhost/')); + $this->assertTrue($utils->createRedirectResponse($request, 'https://localhost/foo')->isRedirect('https://localhost/foo')); + $this->assertTrue($utils->createRedirectResponse($request, 'http://localhost/foo')->isRedirect('http://localhost/foo')); + $this->assertTrue($utils->createRedirectResponse($request, 'https://www.localhost/foo')->isRedirect('http://localhost/')); + $this->assertTrue($utils->createRedirectResponse($request, 'http://symfony.com/blog')->isRedirect('http://localhost/')); + $this->assertTrue($utils->createRedirectResponse($request, 'http://pirate.com/foo')->isRedirect('http://localhost/')); + } + + public function testNoSession() + { + $container = $this->createContainer(null); + + $utils = $container->get('security.http_utils'); + $request = Request::create('/', 'get'); + + $this->assertTrue($utils->createRedirectResponse($request, 'https://symfony.com/blog')->isRedirect('https://symfony.com/blog')); + $this->assertTrue($utils->createRedirectResponse($request, 'https://www.symfony.com/blog')->isRedirect('https://www.symfony.com/blog')); + $this->assertTrue($utils->createRedirectResponse($request, 'https://localhost/foo')->isRedirect('https://localhost/foo')); + $this->assertTrue($utils->createRedirectResponse($request, 'https://www.localhost/foo')->isRedirect('https://www.localhost/foo')); + $this->assertTrue($utils->createRedirectResponse($request, 'http://symfony.com/blog')->isRedirect('http://symfony.com/blog')); + $this->assertTrue($utils->createRedirectResponse($request, 'http://pirate.com/foo')->isRedirect('http://pirate.com/foo')); + } + + private function createContainer($sessionStorageOptions) + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.bundles_metadata', array()); + $container->setParameter('kernel.cache_dir', __DIR__); + $container->setParameter('kernel.charset', 'UTF-8'); + $container->setParameter('kernel.container_class', 'cc'); + $container->setParameter('kernel.debug', true); + $container->setParameter('kernel.project_dir', __DIR__); + $container->setParameter('kernel.root_dir', __DIR__); + $container->setParameter('kernel.secret', __DIR__); + if (null !== $sessionStorageOptions) { + $container->setParameter('session.storage.options', $sessionStorageOptions); + } + $container->setParameter('request_listener.http_port', 80); + $container->setParameter('request_listener.https_port', 443); + + $config = array( + 'security' => array( + 'providers' => array('some_provider' => array('id' => 'foo')), + 'firewalls' => array('some_firewall' => array('security' => false)), + ), + ); + + $ext = new FrameworkExtension(); + $ext->load(array(), $container); + + $ext = new SecurityExtension(); + $ext->load($config, $container); + + $pass = new AddSessionDomainConstraintPass(); + $pass->process($container); + + return $container; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php index ad87bcb112904..9918dc7e48414 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php @@ -12,7 +12,6 @@ namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; -use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Reference; use Symfony\Bundle\SecurityBundle\SecurityBundle; @@ -521,7 +520,7 @@ public function testUserCheckerConfigWithNoCheckers() public function testUserPasswordEncoderCommandIsRegistered() { - $this->assertTrue($this->getContainer('remember_me_options')->has(UserPasswordEncoderCommand::class)); + $this->assertTrue($this->getContainer('remember_me_options')->has('security.command.user_password_encoder')); } public function testDefaultAccessDecisionManagerStrategyIsAffirmative() diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index 49de7529e7f02..0b339188e7590 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -176,7 +176,7 @@ public function testSwitchUserNotStatelessOnStatelessFirewall() /** * @group legacy - * @expectedDeprecation Firewall "default" has no "provider" set but multiple providers exist. Using the first configured provider (first) is deprecated since 3.4 and will throw an exception in 4.0, set the "provider" key on the firewall instead. + * @expectedDeprecation Listener "http_basic" on firewall "default" has no "provider" set but multiple providers exist. Using the first configured provider (first) is deprecated since 3.4 and will throw an exception in 4.0, set the "provider" key on the firewall instead. */ public function testDeprecationForAmbiguousProvider() { @@ -199,6 +199,27 @@ public function testDeprecationForAmbiguousProvider() $container->compile(); } + public function testPerListenerProvider() + { + $container = $this->getRawContainer(); + $container->loadFromExtension('security', array( + 'providers' => array( + 'first' => array('id' => 'foo'), + 'second' => array('id' => 'bar'), + ), + + 'firewalls' => array( + 'default' => array( + 'http_basic' => array('provider' => 'second'), + 'logout_on_user_change' => true, + ), + ), + )); + + $container->compile(); + $this->addToAssertionCount(1); + } + protected function getRawContainer() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 3e7caac2ec5f9..d621f66f3ad75 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -18,7 +18,7 @@ "require": { "php": "^5.5.9|>=7.0.8", "ext-xml": "*", - "symfony/security": "~3.4|~4.0", + "symfony/security": "~3.4-beta5|~4.0-beta5", "symfony/dependency-injection": "~3.4|~4.0", "symfony/http-kernel": "~3.3|~4.0", "symfony/polyfill-php70": "~1.0" diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/console.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/console.xml index d716f309f451d..6f4b6a41319ca 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/console.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/console.xml @@ -7,13 +7,13 @@ - + %kernel.project_dir% - + diff --git a/src/Symfony/Bundle/WebServerBundle/DependencyInjection/WebServerExtension.php b/src/Symfony/Bundle/WebServerBundle/DependencyInjection/WebServerExtension.php index b26236bcccef9..4285be0a9db05 100644 --- a/src/Symfony/Bundle/WebServerBundle/DependencyInjection/WebServerExtension.php +++ b/src/Symfony/Bundle/WebServerBundle/DependencyInjection/WebServerExtension.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\WebServerBundle\DependencyInjection; +use Symfony\Bridge\Monolog\Formatter\ConsoleFormatter; use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -25,5 +26,9 @@ public function load(array $configs, ContainerBuilder $container) { $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('webserver.xml'); + + if (!class_exists(ConsoleFormatter::class)) { + $container->removeDefinition('web_server.command.server_log'); + } } } diff --git a/src/Symfony/Bundle/WebServerBundle/Tests/DependencyInjection/WebServerExtensionTest.php b/src/Symfony/Bundle/WebServerBundle/Tests/DependencyInjection/WebServerExtensionTest.php index 17c41139fe649..ebc0c9421fdb9 100644 --- a/src/Symfony/Bundle/WebServerBundle/Tests/DependencyInjection/WebServerExtensionTest.php +++ b/src/Symfony/Bundle/WebServerBundle/Tests/DependencyInjection/WebServerExtensionTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\WebServerBundle\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Monolog\Formatter\ConsoleFormatter; use Symfony\Bundle\WebServerBundle\DependencyInjection\WebServerExtension; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -26,6 +27,6 @@ public function testLoad() $this->assertTrue($container->hasDefinition('web_server.command.server_start')); $this->assertTrue($container->hasDefinition('web_server.command.server_stop')); $this->assertTrue($container->hasDefinition('web_server.command.server_status')); - $this->assertTrue($container->hasDefinition('web_server.command.server_log')); + $this->assertSame(class_exists(ConsoleFormatter::class), $container->hasDefinition('web_server.command.server_log')); } } diff --git a/src/Symfony/Component/Cache/Traits/MemcachedTrait.php b/src/Symfony/Component/Cache/Traits/MemcachedTrait.php index 29b92647c9bde..82344fceaeba7 100644 --- a/src/Symfony/Component/Cache/Traits/MemcachedTrait.php +++ b/src/Symfony/Component/Cache/Traits/MemcachedTrait.php @@ -130,7 +130,7 @@ public static function createConnection($servers, array $options = array()) } // set client's options - unset($options['persistent_id'], $options['username'], $options['password'], $options['weight']); + unset($options['persistent_id'], $options['username'], $options['password'], $options['weight'], $options['lazy']); $options = array_change_key_case($options, CASE_UPPER); $client->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); $client->setOption(\Memcached::OPT_NO_BLOCK, true); diff --git a/src/Symfony/Component/Config/Tests/Fixtures/BadParent.php b/src/Symfony/Component/Config/Tests/Fixtures/BadParent.php index 3f9720bb8a962..68d7296ed8696 100644 --- a/src/Symfony/Component/Config/Tests/Fixtures/BadParent.php +++ b/src/Symfony/Component/Config/Tests/Fixtures/BadParent.php @@ -2,8 +2,6 @@ namespace Symfony\Component\Config\Tests\Fixtures; -if (!function_exists('__phpunit_run_isolated_test')) { - class BadParent extends MissingParent - { - } +class BadParent extends MissingParent +{ } diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index f821ff68f8de8..b98365c059332 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -127,15 +127,12 @@ public function run(InputInterface $input = null, OutputInterface $output = null try { $e = null; $exitCode = $this->doRun($input, $output); - } catch (\Exception $x) { - $e = $x; - } catch (\Throwable $x) { - $e = new FatalThrowableError($x); + } catch (\Exception $e) { } if (null !== $e) { - if (!$this->catchExceptions || !$x instanceof \Exception) { - throw $x; + if (!$this->catchExceptions) { + throw $e; } if ($output instanceof ConsoleOutputInterface) { @@ -457,15 +454,12 @@ public function get($name) { $this->init(); - if (isset($this->commands[$name])) { - $command = $this->commands[$name]; - } elseif ($this->commandLoader && $this->commandLoader->has($name)) { - $command = $this->commandLoader->get($name); - $this->add($command); - } else { + if (!$this->has($name)) { throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name)); } + $command = $this->commands[$name]; + if ($this->wantHelps) { $this->wantHelps = false; @@ -489,7 +483,7 @@ public function has($name) { $this->init(); - return isset($this->commands[$name]) || ($this->commandLoader && $this->commandLoader->has($name)); + return isset($this->commands[$name]) || ($this->commandLoader && $this->commandLoader->has($name) && $this->add($this->commandLoader->get($name))); } /** @@ -651,7 +645,7 @@ public function all($namespace = null) $commands = $this->commands; foreach ($this->commandLoader->getNames() as $name) { - if (!isset($commands[$name])) { + if (!isset($commands[$name]) && $this->has($name)) { $commands[$name] = $this->get($name); } } @@ -668,7 +662,7 @@ public function all($namespace = null) if ($this->commandLoader) { foreach ($this->commandLoader->getNames() as $name) { - if (!isset($commands[$name]) && $namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) { + if (!isset($commands[$name]) && $namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1) && $this->has($name)) { $commands[$name] = $this->get($name); } } @@ -1064,8 +1058,8 @@ public function extractNamespace($name, $limit = null) * Finds alternative of $name among $collection, * if nothing is found in $collection, try in $abbrevs. * - * @param string $name The string - * @param array|\Traversable $collection The collection + * @param string $name The string + * @param iterable $collection The collection * * @return string[] A sorted array of similar string */ diff --git a/src/Symfony/Component/Console/Question/Question.php b/src/Symfony/Component/Console/Question/Question.php index f750183799414..63afbc4c0e4d0 100644 --- a/src/Symfony/Component/Console/Question/Question.php +++ b/src/Symfony/Component/Console/Question/Question.php @@ -117,7 +117,7 @@ public function setHiddenFallback($fallback) /** * Gets values for the autocompleter. * - * @return null|array|\Traversable + * @return null|iterable */ public function getAutocompleterValues() { @@ -127,7 +127,7 @@ public function getAutocompleterValues() /** * Sets values for the autocompleter. * - * @param null|array|\Traversable $values + * @param null|iterable $values * * @return $this * diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 396c35c733d0e..4e1f8811b2073 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -1532,6 +1532,30 @@ public function testRunLazyCommandService() $this->assertSame(array('lazy:alias', 'lazy:alias2'), $command->getAliases()); } + /** + * @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException + */ + public function testGetDisabledLazyCommand() + { + $application = new Application(); + $application->setCommandLoader(new FactoryCommandLoader(array('disabled' => function () { return new DisabledCommand(); }))); + $application->get('disabled'); + } + + public function testHasReturnsFalseForDisabledLazyCommand() + { + $application = new Application(); + $application->setCommandLoader(new FactoryCommandLoader(array('disabled' => function () { return new DisabledCommand(); }))); + $this->assertFalse($application->has('disabled')); + } + + public function testAllExcludesDisabledLazyCommand() + { + $application = new Application(); + $application->setCommandLoader(new FactoryCommandLoader(array('disabled' => function () { return new DisabledCommand(); }))); + $this->assertArrayNotHasKey('disabled', $application->all()); + } + protected function getDispatcher($skipCommand = false) { $dispatcher = new EventDispatcher(); @@ -1634,3 +1658,11 @@ public function execute(InputInterface $input, OutputInterface $output) $output->writeln('lazy-command called'); } } + +class DisabledCommand extends Command +{ + public function isEnabled() + { + return false; + } +} diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index a71aae3eff5de..61a25e1123344 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -417,6 +417,7 @@ public function handleError($type, $message, $file, $line) $errorAsException = self::$silencedErrorCache[$id][$message]; ++$errorAsException->count; } else { + $lightTrace = array(); $errorAsException = null; } diff --git a/src/Symfony/Component/Debug/Tests/Fixtures/Throwing.php b/src/Symfony/Component/Debug/Tests/Fixtures/Throwing.php index d338ca9d5752b..21e0aba17d358 100644 --- a/src/Symfony/Component/Debug/Tests/Fixtures/Throwing.php +++ b/src/Symfony/Component/Debug/Tests/Fixtures/Throwing.php @@ -1,5 +1,3 @@ checkedNodes[$id])) { // Don't check circular references for lazy edges - if (!$node->getValue() || !$edge->isLazy()) { + if (!$node->getValue() || (!$edge->isLazy() && !$edge->isWeak())) { $searchKey = array_search($id, $this->currentPath); $this->currentPath[] = $id; diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveHotPathPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveHotPathPass.php index e9147f52421b8..38e96d06f1a76 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveHotPathPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveHotPathPass.php @@ -52,12 +52,12 @@ protected function processValue($value, $isRoot = false) if ($value instanceof ArgumentInterface) { return $value; } - if ($value instanceof Definition && $isRoot && (isset($this->resolvedIds[$this->currentId]) || !$value->hasTag($this->tagName))) { - return $value; + if ($value instanceof Definition && $isRoot && (isset($this->resolvedIds[$this->currentId]) || !$value->hasTag($this->tagName) || $value->isDeprecated())) { + return $value->isDeprecated() ? $value->clearTag($this->tagName) : $value; } if ($value instanceof Reference && ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE !== $value->getInvalidBehavior() && $this->container->has($id = (string) $value)) { $definition = $this->container->findDefinition($id); - if (!$definition->hasTag($this->tagName)) { + if (!$definition->hasTag($this->tagName) && !$definition->isDeprecated()) { $this->resolvedIds[$id] = true; $definition->addTag($this->tagName); parent::processValue($definition, false); diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveNamedArgumentsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveNamedArgumentsPass.php index a96f981d074b4..cd6bb6fe8c58a 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveNamedArgumentsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveNamedArgumentsPass.php @@ -68,15 +68,17 @@ protected function processValue($value, $isRoot = false) throw new InvalidArgumentException(sprintf('Invalid service "%s": the value of argument "%s" of method "%s()" must be null, an instance of %s or an instance of %s, %s given.', $this->currentId, $key, $class !== $this->currentId ? $class.'::'.$method : $method, Reference::class, Definition::class, gettype($argument))); } + $typeFound = false; foreach ($parameters as $j => $p) { - if (ProxyHelper::getTypeHint($r, $p, true) === $key) { + if (!array_key_exists($j, $resolvedArguments) && ProxyHelper::getTypeHint($r, $p, true) === $key) { $resolvedArguments[$j] = $argument; - - continue 2; + $typeFound = true; } } - throw new InvalidArgumentException(sprintf('Invalid service "%s": method "%s()" has no argument type-hinted as "%s". Check your service definition.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method, $key)); + if (!$typeFound) { + throw new InvalidArgumentException(sprintf('Invalid service "%s": method "%s()" has no argument type-hinted as "%s". Check your service definition.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method, $key)); + } } if ($resolvedArguments !== $call[1]) { diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 5234aa354fe53..7a6ef9851ec99 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -16,6 +16,7 @@ use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Variable; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Compiler\AnalyzeServiceReferencesPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -66,6 +67,7 @@ class PhpDumper extends Dumper private $hotPathTag; private $inlineRequires; private $inlinedRequires = array(); + private $circularReferences = array(); /** * @var ProxyDumper @@ -118,15 +120,28 @@ public function dump(array $options = array()) 'namespace' => '', 'as_files' => false, 'debug' => true, - 'hot_path_tag' => null, + 'hot_path_tag' => 'container.hot_path', 'inline_class_loader_parameter' => 'container.dumper.inline_class_loader', ), $options); $this->namespace = $options['namespace']; $this->asFiles = $options['as_files']; $this->hotPathTag = $options['hot_path_tag']; - $this->inlineRequires = $this->container->hasParameter($options['inline_class_loader_parameter']) && $this->container->getParameter($options['inline_class_loader_parameter']); - $this->initializeMethodNamesMap($options['base_class']); + $this->inlineRequires = $options['inline_class_loader_parameter'] && $this->container->hasParameter($options['inline_class_loader_parameter']) && $this->container->getParameter($options['inline_class_loader_parameter']); + + if (0 !== strpos($baseClass = $options['base_class'], '\\') && 'Container' !== $baseClass) { + $baseClass = sprintf('%s\%s', $options['namespace'] ? '\\'.$options['namespace'] : '', $baseClass); + } + + $this->initializeMethodNamesMap('Container' === $baseClass ? Container::class : $baseClass); + + (new AnalyzeServiceReferencesPass())->process($this->container); + $this->circularReferences = array(); + $checkedNodes = array(); + foreach ($this->container->getCompiler()->getServiceReferenceGraph()->getNodes() as $id => $node) { + $currentPath = array($id => $id); + $this->analyzeCircularReferences($node->getOutEdges(), $checkedNodes, $currentPath); + } $this->docStar = $options['debug'] ? '*' : ''; @@ -156,7 +171,7 @@ public function dump(array $options = array()) } $code = - $this->startClass($options['class'], $options['base_class']). + $this->startClass($options['class'], $baseClass). $this->addServices(). $this->addDefaultParametersMethod(). $this->endClass() @@ -223,6 +238,7 @@ class_alias(\\Container{$hash}\\{$options['class']}::class, {$options['class']}: $this->targetDirRegex = null; $this->inlinedRequires = array(); + $this->circularReferences = array(); $unusedEnvs = array(); foreach ($this->container->getEnvCounters() as $env => $use) { @@ -266,19 +282,19 @@ private function addServiceLocalTempVariables($cId, Definition $definition, arra array_unshift($inlinedDefinitions, $definition); - $collectLineage = $this->inlineRequires && !($this->hotPathTag && $definition->hasTag($this->hotPathTag)); - $isNonLazyShared = !$this->getProxyDumper()->isProxyCandidate($definition) && $definition->isShared(); + $collectLineage = $this->inlineRequires && !$this->isHotPath($definition); + $isNonLazyShared = isset($this->circularReferences[$cId]) && !$this->getProxyDumper()->isProxyCandidate($definition) && $definition->isShared(); $lineage = $calls = $behavior = array(); foreach ($inlinedDefinitions as $iDefinition) { - if ($collectLineage && $class = is_array($factory = $iDefinition->getFactory()) && is_string($factory[0]) ? $factory[0] : $iDefinition->getClass()) { + if ($collectLineage && !$iDefinition->isDeprecated() && $class = is_array($factory = $iDefinition->getFactory()) && is_string($factory[0]) ? $factory[0] : $iDefinition->getClass()) { $this->collectLineage($class, $lineage); } - $this->getServiceCallsFromArguments($iDefinition->getArguments(), $calls, $behavior, $isNonLazyShared); + $this->getServiceCallsFromArguments($iDefinition->getArguments(), $calls, $behavior, $isNonLazyShared, $cId); $isPreInstantiation = $isNonLazyShared && $iDefinition !== $definition && !$this->hasReference($cId, $iDefinition->getMethodCalls(), true) && !$this->hasReference($cId, $iDefinition->getProperties(), true); - $this->getServiceCallsFromArguments($iDefinition->getMethodCalls(), $calls, $behavior, $isPreInstantiation); - $this->getServiceCallsFromArguments($iDefinition->getProperties(), $calls, $behavior, $isPreInstantiation); - $this->getServiceCallsFromArguments(array($iDefinition->getConfigurator()), $calls, $behavior, $isPreInstantiation); - $this->getServiceCallsFromArguments(array($iDefinition->getFactory()), $calls, $behavior, $isNonLazyShared); + $this->getServiceCallsFromArguments($iDefinition->getMethodCalls(), $calls, $behavior, $isPreInstantiation, $cId); + $this->getServiceCallsFromArguments($iDefinition->getProperties(), $calls, $behavior, $isPreInstantiation, $cId); + $this->getServiceCallsFromArguments(array($iDefinition->getConfigurator()), $calls, $behavior, $isPreInstantiation, $cId); + $this->getServiceCallsFromArguments(array($iDefinition->getFactory()), $calls, $behavior, $isNonLazyShared, $cId); } $code = ''; @@ -331,6 +347,33 @@ private function addServiceLocalTempVariables($cId, Definition $definition, arra return $code; } + private function analyzeCircularReferences(array $edges, &$checkedNodes, &$currentPath) + { + foreach ($edges as $edge) { + $node = $edge->getDestNode(); + $id = $node->getId(); + + if (isset($checkedNodes[$id])) { + continue; + } + + if ($node->getValue() && ($edge->isLazy() || $edge->isWeak())) { + // no-op + } elseif (isset($currentPath[$id])) { + foreach (array_reverse($currentPath) as $parentId) { + $this->circularReferences[$parentId][$id] = $id; + $id = $parentId; + } + } else { + $currentPath[$id] = $id; + $this->analyzeCircularReferences($node->getOutEdges(), $checkedNodes, $currentPath); + } + + $checkedNodes[$id] = true; + array_pop($currentPath); + } + } + private function collectLineage($class, array &$lineage) { if (isset($lineage[$class])) { @@ -557,7 +600,7 @@ private function isTrivialInstance(Definition $definition) } foreach ($definition->getArguments() as $arg) { - if (!$arg || ($arg instanceof Reference && 'service_container' === (string) $arg)) { + if (!$arg) { continue; } if (is_array($arg) && 3 >= count($arg)) { @@ -565,7 +608,7 @@ private function isTrivialInstance(Definition $definition) if ($this->dumpValue($k) !== $this->dumpValue($k, false)) { return false; } - if (!$v || ($v instanceof Reference && 'service_container' === (string) $v)) { + if (!$v) { continue; } if ($v instanceof Reference && $this->container->has($id = (string) $v) && $this->container->findDefinition($id)->isSynthetic()) { @@ -758,7 +801,7 @@ private function addService($id, Definition $definition, &$file = null) $lazyInitialization = ''; } - $asFile = $this->asFiles && $definition->isShared() && !($this->hotPathTag && $definition->hasTag($this->hotPathTag)); + $asFile = $this->asFiles && $definition->isShared() && !$this->isHotPath($definition); $methodName = $this->generateMethodName($id); if ($asFile) { $file = $methodName.'.php'; @@ -824,7 +867,7 @@ private function addServices() $definitions = $this->container->getDefinitions(); ksort($definitions); foreach ($definitions as $id => $definition) { - if ($definition->isSynthetic() || ($this->asFiles && $definition->isShared() && !($this->hotPathTag && $definition->hasTag($this->hotPathTag)))) { + if ($definition->isSynthetic() || ($this->asFiles && $definition->isShared() && !$this->isHotPath($definition))) { continue; } if ($definition->isPublic()) { @@ -842,7 +885,7 @@ private function generateServiceFiles() $definitions = $this->container->getDefinitions(); ksort($definitions); foreach ($definitions as $id => $definition) { - if (!$definition->isSynthetic() && $definition->isShared() && !($this->hotPathTag && $definition->hasTag($this->hotPathTag))) { + if (!$definition->isSynthetic() && $definition->isShared() && !$this->isHotPath($definition)) { $code = $this->addService($id, $definition, $file); yield $file => $code; } @@ -1115,7 +1158,7 @@ private function addMethodMap() $definitions = $this->container->getDefinitions(); ksort($definitions); foreach ($definitions as $id => $definition) { - if (!$definition->isSynthetic() && (!$this->asFiles || !$definition->isShared() || ($this->hotPathTag && $definition->hasTag($this->hotPathTag)))) { + if (!$definition->isSynthetic() && (!$this->asFiles || !$definition->isShared() || $this->isHotPath($definition))) { $code .= ' '.$this->export($id).' => '.$this->export($this->generateMethodName($id)).",\n"; } } @@ -1134,7 +1177,7 @@ private function addFileMap() $definitions = $this->container->getDefinitions(); ksort($definitions); foreach ($definitions as $id => $definition) { - if (!$definition->isSynthetic() && $definition->isShared() && !($this->hotPathTag && $definition->hasTag($this->hotPathTag))) { + if (!$definition->isSynthetic() && $definition->isShared() && !$this->isHotPath($definition)) { $code .= sprintf(" %s => __DIR__.'/%s.php',\n", $this->export($id), $this->generateMethodName($id)); } } @@ -1496,16 +1539,16 @@ private function getServiceConditionals($value) /** * Builds service calls from arguments. */ - private function getServiceCallsFromArguments(array $arguments, array &$calls, array &$behavior, $isPreInstantiation) + private function getServiceCallsFromArguments(array $arguments, array &$calls, array &$behavior, $isPreInstantiation, $callerId) { foreach ($arguments as $argument) { if (is_array($argument)) { - $this->getServiceCallsFromArguments($argument, $calls, $behavior, $isPreInstantiation); + $this->getServiceCallsFromArguments($argument, $calls, $behavior, $isPreInstantiation, $callerId); } elseif ($argument instanceof Reference) { $id = (string) $argument; if (!isset($calls[$id])) { - $calls[$id] = (int) ($isPreInstantiation && $this->container->has($id) && !$this->container->findDefinition($id)->isSynthetic()); + $calls[$id] = (int) ($isPreInstantiation && isset($this->circularReferences[$callerId][$id])); } if (!isset($behavior[$id])) { $behavior[$id] = $argument->getInvalidBehavior(); @@ -1577,6 +1620,10 @@ private function getDefinitionsFromArguments(array $arguments) */ private function hasReference($id, array $arguments, $deep = false, array &$visited = array()) { + if (!isset($this->circularReferences[$id])) { + return false; + } + foreach ($arguments as $argument) { if (is_array($argument)) { if ($this->hasReference($id, $argument, $deep, $visited)) { @@ -1590,7 +1637,7 @@ private function hasReference($id, array $arguments, $deep = false, array &$visi return true; } - if (!$deep || isset($visited[$argumentId]) || 'service_container' === $argumentId) { + if (!$deep || isset($visited[$argumentId]) || !isset($this->circularReferences[$id][$argumentId])) { continue; } @@ -1851,7 +1898,7 @@ private function getServiceCall($id, Reference $reference = null) if ($definition->isShared()) { $code = sprintf('$this->services[\'%s\'] = %s', $id, $code); } - } elseif ($this->asFiles && $definition->isShared() && !($this->hotPathTag && $definition->hasTag($this->hotPathTag))) { + } elseif ($this->asFiles && $definition->isShared() && !$this->isHotPath($definition)) { $code = sprintf("\$this->load(__DIR__.'/%s.php')", $this->generateMethodName($id)); } else { $code = sprintf('$this->%s()', $this->generateMethodName($id)); @@ -1983,6 +2030,11 @@ private function getExpressionLanguage() return $this->expressionLanguage; } + private function isHotPath(Definition $definition) + { + return $this->hotPathTag && $definition->hasTag($this->hotPathTag) && !$definition->isDeprecated(); + } + private function export($value) { if (null !== $this->targetDirRegex && is_string($value) && preg_match($this->targetDirRegex, $value, $matches, PREG_OFFSET_CAPTURE)) { diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FactoryTrait.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FactoryTrait.php index 71c15906cc9af..12e8859ca1bb9 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FactoryTrait.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FactoryTrait.php @@ -11,6 +11,8 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; + trait FactoryTrait { /** @@ -22,6 +24,12 @@ trait FactoryTrait */ final public function factory($factory) { + if (is_string($factory) && 1 === substr_count($factory, ':')) { + $factoryParts = explode(':', $factory); + + throw new InvalidArgumentException(sprintf('Invalid factory "%s": the `service:method` notation is not available when using PHP-based DI configuration. Use "[ref(\'%s\'), \'%s\']" instead.', $factory, $factoryParts[0], $factoryParts[1])); + } + $this->definition->setFactory(static::processValue($factory, true)); return $this; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/OptionalServiceClass.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/OptionalServiceClass.php index 33372695903e9..7e9238f22301b 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/OptionalServiceClass.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/OptionalServiceClass.php @@ -13,8 +13,6 @@ use Symfony\Bug\NotExistClass; -if (!function_exists('__phpunit_run_isolated_test')) { - class OptionalServiceClass extends NotExistClass - { - } +class OptionalServiceClass extends NotExistClass +{ } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveHotPathPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveHotPathPassTest.php index 33176159cc945..d2ba590047caa 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveHotPathPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveHotPathPassTest.php @@ -34,11 +34,15 @@ public function testProcess() ; $container->register('lazy'); - $container->register('bar'); - $container->register('bar')->addArgument(new Reference('buz')); - $container->register('baz')->addArgument(new Reference('lazy')); - $container->register('baz')->addArgument(new Reference('lazy')); + $container->register('bar') + ->addArgument(new Reference('buz')) + ->addArgument(new Reference('deprec_ref_notag')); + $container->register('baz') + ->addArgument(new Reference('lazy')) + ->addArgument(new Reference('lazy')); $container->register('buz'); + $container->register('deprec_with_tag')->setDeprecated()->addTag('container.hot_path'); + $container->register('deprec_ref_notag')->setDeprecated(); (new ResolveHotPathPass())->process($container); @@ -47,5 +51,7 @@ public function testProcess() $this->assertTrue($container->getDefinition('buz')->hasTag('container.hot_path')); $this->assertFalse($container->getDefinition('baz')->hasTag('container.hot_path')); $this->assertFalse($container->getDefinition('service_container')->hasTag('container.hot_path')); + $this->assertFalse($container->getDefinition('deprec_with_tag')->hasTag('container.hot_path')); + $this->assertFalse($container->getDefinition('deprec_ref_notag')->hasTag('container.hot_path')); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveNamedArgumentsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveNamedArgumentsPassTest.php index fac05f070b05c..fe681b41df788 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveNamedArgumentsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveNamedArgumentsPassTest.php @@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy; +use Symfony\Component\DependencyInjection\Tests\Fixtures\SimilarArgumentsDummy; /** * @author Kévin Dunglas @@ -125,6 +126,32 @@ public function testTypedArgument() $this->assertEquals(array(new Reference('foo'), '123'), $definition->getArguments()); } + + public function testResolvesMultipleArgumentsOfTheSameType() + { + $container = new ContainerBuilder(); + + $definition = $container->register(SimilarArgumentsDummy::class, SimilarArgumentsDummy::class); + $definition->setArguments(array(CaseSensitiveClass::class => new Reference('foo'), '$token' => 'qwerty')); + + $pass = new ResolveNamedArgumentsPass(); + $pass->process($container); + + $this->assertEquals(array(new Reference('foo'), 'qwerty', new Reference('foo')), $definition->getArguments()); + } + + public function testResolvePrioritizeNamedOverType() + { + $container = new ContainerBuilder(); + + $definition = $container->register(SimilarArgumentsDummy::class, SimilarArgumentsDummy::class); + $definition->setArguments(array(CaseSensitiveClass::class => new Reference('foo'), '$token' => 'qwerty', '$class1' => new Reference('bar'))); + + $pass = new ResolveNamedArgumentsPass(); + $pass->process($container); + + $this->assertEquals(array(new Reference('bar'), 'qwerty', new Reference('foo')), $definition->getArguments()); + } } class NoConstructor diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index ac73138a9dfd9..d3adfe7e69aa7 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -32,6 +32,7 @@ use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\Loader\ClosureLoader; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\Tests\Fixtures\SimilarArgumentsDummy; use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; @@ -1270,6 +1271,30 @@ public function testParameterWithMixedCase() $this->assertSame('bar', $container->get('foo')->foo); } + + public function testArgumentsHaveHigherPriorityThanBindings() + { + $container = new ContainerBuilder(); + $container->register('class.via.bindings', CaseSensitiveClass::class)->setArguments(array( + 'via-bindings', + )); + $container->register('class.via.argument', CaseSensitiveClass::class)->setArguments(array( + 'via-argument', + )); + $container->register('foo', SimilarArgumentsDummy::class)->setPublic(true)->setBindings(array( + CaseSensitiveClass::class => new Reference('class.via.bindings'), + '$token' => '1234', + ))->setArguments(array( + '$class1' => new Reference('class.via.argument'), + )); + + $this->assertSame(array('service_container', 'class.via.bindings', 'class.via.argument', 'foo', 'Psr\Container\ContainerInterface', 'Symfony\Component\DependencyInjection\ContainerInterface'), $container->getServiceIds()); + + $container->compile(); + + $this->assertSame('via-argument', $container->get('foo')->class1->identifier); + $this->assertSame('via-bindings', $container->get('foo')->class2->identifier); + } } class FooClass diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CaseSensitiveClass.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CaseSensitiveClass.php index 87b258429e6a3..7c6f0ff4cb022 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CaseSensitiveClass.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CaseSensitiveClass.php @@ -13,4 +13,10 @@ class CaseSensitiveClass { + public $identifier; + + public function __construct($identifier = null) + { + $this->identifier = $identifier; + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/SimilarArgumentsDummy.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/SimilarArgumentsDummy.php new file mode 100644 index 0000000000000..2600389bdd6a7 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/SimilarArgumentsDummy.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +class SimilarArgumentsDummy +{ + public $class1; + public $class2; + + public function __construct(CaseSensitiveClass $class1, $token, CaseSensitiveClass $class2) + { + $this->class1 = $class1; + $this->class2 = $class2; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/factory_short_notation.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/factory_short_notation.php new file mode 100644 index 0000000000000..5b37793ea24af --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/factory_short_notation.php @@ -0,0 +1,9 @@ +services() + ->set('service', \stdClass::class) + ->factory('factory:method'); +}; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/container_inline_requires.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/container_inline_requires.php index d3d822a27da86..d3d94290fd6a9 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/container_inline_requires.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/container_inline_requires.php @@ -95,13 +95,7 @@ protected function getC2Service() require_once $this->targetDirs[1].'/includes/HotPath/C2.php'; require_once $this->targetDirs[1].'/includes/HotPath/C3.php'; - $a = ${($_ = isset($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C3']) ? $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C3'] : $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C3'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C3()) && false ?: '_'}; - - if (isset($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C2'])) { - return $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C2']; - } - - return $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C2'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C2($a); + return $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C2'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C2(${($_ = isset($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C3']) ? $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C3'] : $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C3'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C3()) && false ?: '_'}); } /** diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services1-1.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services1-1.php index 349358c384549..88518409038f9 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services1-1.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services1-1.php @@ -16,7 +16,7 @@ * * @final since Symfony 3.3 */ -class Container extends AbstractContainer +class Container extends \Symfony\Component\DependencyInjection\Dump\AbstractContainer { private $parameters; private $targetDirs = array(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php index 3a1c8bc301a57..30b94d456ef6f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php @@ -82,10 +82,6 @@ protected function getBarService() { $a = ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->getFoo_BazService()) && false ?: '_'}; - if (isset($this->services['bar'])) { - return $this->services['bar']; - } - $this->services['bar'] = $instance = new \Bar\FooClass('foo', $a, $this->getParameter('foo_bar')); $a->configure($instance); @@ -186,13 +182,7 @@ protected function getDeprecatedServiceService() */ protected function getFactoryServiceService() { - $a = ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->getFoo_BazService()) && false ?: '_'}; - - if (isset($this->services['factory_service'])) { - return $this->services['factory_service']; - } - - return $this->services['factory_service'] = $a->getInstance(); + return $this->services['factory_service'] = ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->getFoo_BazService()) && false ?: '_'}->getInstance(); } /** @@ -202,13 +192,7 @@ protected function getFactoryServiceService() */ protected function getFactoryServiceSimpleService() { - $a = ${($_ = isset($this->services['factory_simple']) ? $this->services['factory_simple'] : $this->getFactorySimpleService()) && false ?: '_'}; - - if (isset($this->services['factory_service_simple'])) { - return $this->services['factory_service_simple']; - } - - return $this->services['factory_service_simple'] = $a->getInstance(); + return $this->services['factory_service_simple'] = ${($_ = isset($this->services['factory_simple']) ? $this->services['factory_simple'] : $this->getFactorySimpleService()) && false ?: '_'}->getInstance(); } /** @@ -220,10 +204,6 @@ protected function getFooService() { $a = ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->getFoo_BazService()) && false ?: '_'}; - if (isset($this->services['foo'])) { - return $this->services['foo']; - } - $this->services['foo'] = $instance = \Bar\FooClass::getInstance('foo', $a, array($this->getParameter('foo') => 'foo is '.$this->getParameter('foo').'', 'foobar' => $this->getParameter('foo')), true, $this); $instance->foo = 'bar'; @@ -341,13 +321,7 @@ protected function getMethodCall1Service() */ protected function getNewFactoryServiceService() { - $a = ${($_ = isset($this->services['new_factory']) ? $this->services['new_factory'] : $this->getNewFactoryService()) && false ?: '_'}; - - if (isset($this->services['new_factory_service'])) { - return $this->services['new_factory_service']; - } - - $this->services['new_factory_service'] = $instance = $a->getInstance(); + $this->services['new_factory_service'] = $instance = ${($_ = isset($this->services['new_factory']) ? $this->services['new_factory'] : $this->getNewFactoryService()) && false ?: '_'}->getInstance(); $instance->foo = 'bar'; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt index cadcdbbb916d5..8c76d4c7636f3 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt @@ -33,18 +33,12 @@ use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; // This file has been auto-generated by the Symfony Dependency Injection Component for internal use. // Returns the public 'configured_service' shared service. -$a = ${($_ = isset($this->services['baz']) ? $this->services['baz'] : $this->load(__DIR__.'/getBazService.php')) && false ?: '_'}; - -if (isset($this->services['configured_service'])) { - return $this->services['configured_service']; -} - -$b = new \ConfClass(); -$b->setFoo($a); +$a = new \ConfClass(); +$a->setFoo(${($_ = isset($this->services['baz']) ? $this->services['baz'] : $this->load(__DIR__.'/getBazService.php')) && false ?: '_'}); $this->services['configured_service'] = $instance = new \stdClass(); -$b->configureStdClass($instance); +$a->configureStdClass($instance); return $instance; @@ -97,13 +91,7 @@ use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; // This file has been auto-generated by the Symfony Dependency Injection Component for internal use. // Returns the public 'factory_service' shared service. -$a = ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->load(__DIR__.'/getFoo_BazService.php')) && false ?: '_'}; - -if (isset($this->services['factory_service'])) { - return $this->services['factory_service']; -} - -return $this->services['factory_service'] = $a->getInstance(); +return $this->services['factory_service'] = ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->load(__DIR__.'/getFoo_BazService.php')) && false ?: '_'}->getInstance(); [Container%s/getFactoryServiceSimpleService.php] => services['factory_simple']) ? $this->services['factory_simple'] : $this->load(__DIR__.'/getFactorySimpleService.php')) && false ?: '_'}; - -if (isset($this->services['factory_service_simple'])) { - return $this->services['factory_service_simple']; -} - -return $this->services['factory_service_simple'] = $a->getInstance(); +return $this->services['factory_service_simple'] = ${($_ = isset($this->services['factory_simple']) ? $this->services['factory_simple'] : $this->load(__DIR__.'/getFactorySimpleService.php')) && false ?: '_'}->getInstance(); [Container%s/getFactorySimpleService.php] => services['foo.baz']) ? $this->services['foo.baz'] : $this->load(__DIR__.'/getFoo_BazService.php')) && false ?: '_'}; -if (isset($this->services['foo'])) { - return $this->services['foo']; -} - $this->services['foo'] = $instance = \Bar\FooClass::getInstance('foo', $a, array('bar' => 'foo is bar', 'foobar' => 'bar'), true, $this); $instance->foo = 'bar'; @@ -383,10 +361,6 @@ class ProjectServiceContainer extends Container { $a = ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->load(__DIR__.'/getFoo_BazService.php')) && false ?: '_'}; - if (isset($this->services['bar'])) { - return $this->services['bar']; - } - $this->services['bar'] = $instance = new \Bar\FooClass('foo', $a, $this->getParameter('foo_bar')); $a->configure($instance); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php index f0615f5e56d56..403eb12954fe1 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php @@ -101,10 +101,6 @@ protected function getBarService() { $a = ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->getFoo_BazService()) && false ?: '_'}; - if (isset($this->services['bar'])) { - return $this->services['bar']; - } - $this->services['bar'] = $instance = new \Bar\FooClass('foo', $a, $this->getParameter('foo_bar')); $a->configure($instance); @@ -133,18 +129,12 @@ protected function getBazService() */ protected function getConfiguredServiceService() { - $a = ${($_ = isset($this->services['baz']) ? $this->services['baz'] : $this->getBazService()) && false ?: '_'}; - - if (isset($this->services['configured_service'])) { - return $this->services['configured_service']; - } - - $b = new \ConfClass(); - $b->setFoo($a); + $a = new \ConfClass(); + $a->setFoo(${($_ = isset($this->services['baz']) ? $this->services['baz'] : $this->getBazService()) && false ?: '_'}); $this->services['configured_service'] = $instance = new \stdClass(); - $b->configureStdClass($instance); + $a->configureStdClass($instance); return $instance; } @@ -204,13 +194,7 @@ protected function getDeprecatedServiceService() */ protected function getFactoryServiceService() { - $a = ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->getFoo_BazService()) && false ?: '_'}; - - if (isset($this->services['factory_service'])) { - return $this->services['factory_service']; - } - - return $this->services['factory_service'] = $a->getInstance(); + return $this->services['factory_service'] = ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->getFoo_BazService()) && false ?: '_'}->getInstance(); } /** @@ -220,13 +204,7 @@ protected function getFactoryServiceService() */ protected function getFactoryServiceSimpleService() { - $a = ${($_ = isset($this->services['factory_simple']) ? $this->services['factory_simple'] : $this->getFactorySimpleService()) && false ?: '_'}; - - if (isset($this->services['factory_service_simple'])) { - return $this->services['factory_service_simple']; - } - - return $this->services['factory_service_simple'] = $a->getInstance(); + return $this->services['factory_service_simple'] = ${($_ = isset($this->services['factory_simple']) ? $this->services['factory_simple'] : $this->getFactorySimpleService()) && false ?: '_'}->getInstance(); } /** @@ -238,10 +216,6 @@ protected function getFooService() { $a = ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->getFoo_BazService()) && false ?: '_'}; - if (isset($this->services['foo'])) { - return $this->services['foo']; - } - $this->services['foo'] = $instance = \Bar\FooClass::getInstance('foo', $a, array('bar' => 'foo is bar', 'foobar' => 'bar'), true, $this); $instance->foo = 'bar'; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_legacy_privates.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_legacy_privates.php index 50e29ae4ec3bc..f8c33ae59966a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_legacy_privates.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_legacy_privates.php @@ -90,13 +90,7 @@ public function isFrozen() */ protected function getBarService() { - $a = ${($_ = isset($this->services['private_not_inlined']) ? $this->services['private_not_inlined'] : $this->services['private_not_inlined'] = new \stdClass()) && false ?: '_'}; - - if (isset($this->services['bar'])) { - return $this->services['bar']; - } - - return $this->services['bar'] = new \stdClass($a); + return $this->services['bar'] = new \stdClass(${($_ = isset($this->services['private_not_inlined']) ? $this->services['private_not_inlined'] : $this->services['private_not_inlined'] = new \stdClass()) && false ?: '_'}); } /** diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_locator.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_locator.php index af794f49cfe17..f7e751113cfd3 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_locator.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_locator.php @@ -75,13 +75,7 @@ public function isFrozen() */ protected function getBarServiceService() { - $a = ${($_ = isset($this->services['baz_service']) ? $this->services['baz_service'] : $this->services['baz_service'] = new \stdClass()) && false ?: '_'}; - - if (isset($this->services['bar_service'])) { - return $this->services['bar_service']; - } - - return $this->services['bar_service'] = new \stdClass($a); + return $this->services['bar_service'] = new \stdClass(${($_ = isset($this->services['baz_service']) ? $this->services['baz_service'] : $this->services['baz_service'] = new \stdClass()) && false ?: '_'}); } /** @@ -167,10 +161,6 @@ protected function getTranslator3Service() { $a = ${($_ = isset($this->services['translator.loader_3']) ? $this->services['translator.loader_3'] : $this->services['translator.loader_3'] = new \stdClass()) && false ?: '_'}; - if (isset($this->services['translator_3'])) { - return $this->services['translator_3']; - } - $this->services['translator_3'] = $instance = new \Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator(new \Symfony\Component\DependencyInjection\ServiceLocator(array('translator.loader_3' => function () { return ${($_ = isset($this->services['translator.loader_3']) ? $this->services['translator.loader_3'] : $this->services['translator.loader_3'] = new \stdClass()) && false ?: '_'}; }))); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_private_frozen.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_private_frozen.php index b71508c9d3f5b..59857ecac3ef8 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_private_frozen.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_private_frozen.php @@ -66,13 +66,7 @@ public function isFrozen() */ protected function getBarServiceService() { - $a = ${($_ = isset($this->services['baz_service']) ? $this->services['baz_service'] : $this->services['baz_service'] = new \stdClass()) && false ?: '_'}; - - if (isset($this->services['bar_service'])) { - return $this->services['bar_service']; - } - - return $this->services['bar_service'] = new \stdClass($a); + return $this->services['bar_service'] = new \stdClass(${($_ = isset($this->services['baz_service']) ? $this->services['baz_service'] : $this->services['baz_service'] = new \stdClass()) && false ?: '_'}); } /** @@ -82,13 +76,7 @@ protected function getBarServiceService() */ protected function getFooServiceService() { - $a = ${($_ = isset($this->services['baz_service']) ? $this->services['baz_service'] : $this->services['baz_service'] = new \stdClass()) && false ?: '_'}; - - if (isset($this->services['foo_service'])) { - return $this->services['foo_service']; - } - - return $this->services['foo_service'] = new \stdClass($a); + return $this->services['foo_service'] = new \stdClass(${($_ = isset($this->services['baz_service']) ? $this->services['baz_service'] : $this->services['baz_service'] = new \stdClass()) && false ?: '_'}); } /** diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php index a8f10d9ef0461..8c84dc7e8041c 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php @@ -89,4 +89,17 @@ public function testAutoConfigureAndChildDefinitionNotAllowed() $loader->load($fixtures.'/config/services_autoconfigure_with_parent.php'); $container->compile(); } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid factory "factory:method": the `service:method` notation is not available when using PHP-based DI configuration. Use "[ref('factory'), 'method']" instead. + */ + public function testFactoryShortNotationNotAllowed() + { + $fixtures = realpath(__DIR__.'/../Fixtures'); + $container = new ContainerBuilder(); + $loader = new PhpFileLoader($container, new FileLocator()); + $loader->load($fixtures.'/config/factory_short_notation.php'); + $container->compile(); + } } diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index 0354a49d3d105..0376ff73c091f 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -83,14 +83,14 @@ public function copy($originFile, $targetFile, $overwriteNewerFiles = false) /** * Creates a directory recursively. * - * @param string|array|\Traversable $dirs The directory path - * @param int $mode The directory mode + * @param string|iterable $dirs The directory path + * @param int $mode The directory mode * * @throws IOException On any directory creation failure */ public function mkdir($dirs, $mode = 0777) { - foreach ($this->toIterator($dirs) as $dir) { + foreach ($this->toIterable($dirs) as $dir) { if (is_dir($dir)) { continue; } @@ -111,7 +111,7 @@ public function mkdir($dirs, $mode = 0777) /** * Checks the existence of files or directories. * - * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to check + * @param string|iterable $files A filename, an array of files, or a \Traversable instance to check * * @return bool true if the file exists, false otherwise */ @@ -119,7 +119,7 @@ public function exists($files) { $maxPathLength = PHP_MAXPATHLEN - 2; - foreach ($this->toIterator($files) as $file) { + foreach ($this->toIterable($files) as $file) { if (strlen($file) > $maxPathLength) { throw new IOException(sprintf('Could not check if file exist because path length exceeds %d characters.', $maxPathLength), 0, null, $file); } @@ -135,15 +135,15 @@ public function exists($files) /** * Sets access and modification time of file. * - * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to create - * @param int $time The touch time as a Unix timestamp - * @param int $atime The access time as a Unix timestamp + * @param string|iterable $files A filename, an array of files, or a \Traversable instance to create + * @param int $time The touch time as a Unix timestamp + * @param int $atime The access time as a Unix timestamp * * @throws IOException When touch fails */ public function touch($files, $time = null, $atime = null) { - foreach ($this->toIterator($files) as $file) { + foreach ($this->toIterable($files) as $file) { $touch = $time ? @touch($file, $time, $atime) : @touch($file); if (true !== $touch) { throw new IOException(sprintf('Failed to touch "%s".', $file), 0, null, $file); @@ -154,7 +154,7 @@ public function touch($files, $time = null, $atime = null) /** * Removes files or directories. * - * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to remove + * @param string|iterable $files A filename, an array of files, or a \Traversable instance to remove * * @throws IOException When removal fails */ @@ -190,16 +190,16 @@ public function remove($files) /** * Change mode for an array of files or directories. * - * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change mode - * @param int $mode The new mode (octal) - * @param int $umask The mode mask (octal) - * @param bool $recursive Whether change the mod recursively or not + * @param string|iterable $files A filename, an array of files, or a \Traversable instance to change mode + * @param int $mode The new mode (octal) + * @param int $umask The mode mask (octal) + * @param bool $recursive Whether change the mod recursively or not * * @throws IOException When the change fail */ public function chmod($files, $mode, $umask = 0000, $recursive = false) { - foreach ($this->toIterator($files) as $file) { + foreach ($this->toIterable($files) as $file) { if (true !== @chmod($file, $mode & ~$umask)) { throw new IOException(sprintf('Failed to chmod file "%s".', $file), 0, null, $file); } @@ -212,15 +212,15 @@ public function chmod($files, $mode, $umask = 0000, $recursive = false) /** * Change the owner of an array of files or directories. * - * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change owner - * @param string $user The new owner user name - * @param bool $recursive Whether change the owner recursively or not + * @param string|iterable $files A filename, an array of files, or a \Traversable instance to change owner + * @param string $user The new owner user name + * @param bool $recursive Whether change the owner recursively or not * * @throws IOException When the change fail */ public function chown($files, $user, $recursive = false) { - foreach ($this->toIterator($files) as $file) { + foreach ($this->toIterable($files) as $file) { if ($recursive && is_dir($file) && !is_link($file)) { $this->chown(new \FilesystemIterator($file), $user, true); } @@ -239,15 +239,15 @@ public function chown($files, $user, $recursive = false) /** * Change the group of an array of files or directories. * - * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change group - * @param string $group The group name - * @param bool $recursive Whether change the group recursively or not + * @param string|iterable $files A filename, an array of files, or a \Traversable instance to change group + * @param string $group The group name + * @param bool $recursive Whether change the group recursively or not * * @throws IOException When the change fail */ public function chgrp($files, $group, $recursive = false) { - foreach ($this->toIterator($files) as $file) { + foreach ($this->toIterable($files) as $file) { if ($recursive && is_dir($file) && !is_link($file)) { $this->chgrp(new \FilesystemIterator($file), $group, true); } @@ -369,7 +369,7 @@ public function hardlink($originFile, $targetFiles) throw new FileNotFoundException(sprintf('Origin file "%s" is not a file', $originFile)); } - foreach ($this->toIterator($targetFiles) as $targetFile) { + foreach ($this->toIterable($targetFiles) as $targetFile) { if (is_file($targetFile)) { if (fileinode($originFile) === fileinode($targetFile)) { continue; @@ -722,15 +722,11 @@ public function appendToFile($filename, $content) /** * @param mixed $files * - * @return \Traversable + * @return array|\Traversable */ - private function toIterator($files) + private function toIterable($files) { - if (!$files instanceof \Traversable) { - $files = new \ArrayObject(is_array($files) ? $files : array($files)); - } - - return $files; + return is_array($files) || $files instanceof \Traversable ? $files : array($files); } /** diff --git a/src/Symfony/Component/Filesystem/LockHandler.php b/src/Symfony/Component/Filesystem/LockHandler.php index f1bb8d0ebf5bc..6e5dfdd506af8 100644 --- a/src/Symfony/Component/Filesystem/LockHandler.php +++ b/src/Symfony/Component/Filesystem/LockHandler.php @@ -11,12 +11,12 @@ namespace Symfony\Component\Filesystem; -@trigger_error(sprintf('The %s class is deprecated since version 3.4 and will be removed in 4.0. Use %s or %s instead.', LockHandler::class, SemaphoreStore::class, FlockStore::class), E_USER_DEPRECATED); - use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Lock\Store\FlockStore; use Symfony\Component\Lock\Store\SemaphoreStore; +@trigger_error(sprintf('The %s class is deprecated since version 3.4 and will be removed in 4.0. Use %s or %s instead.', LockHandler::class, SemaphoreStore::class, FlockStore::class), E_USER_DEPRECATED); + /** * LockHandler class provides a simple abstraction to lock anything by means of * a file lock. diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 19105b9e4db98..123ec4fbe8781 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -57,6 +57,11 @@ CHANGELOG * moved data trimming logic of TrimListener into StringUtil * [BC BREAK] When registering a type extension through the DI extension, the tag alias has to match the actual extended type. +2.7.38 +------ + + * [BC BREAK] the `isFileUpload()` method was added to the `RequestHandlerInterface` + 2.7.0 ----- diff --git a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php index 51c43bf07c3ef..1953980c29716 100644 --- a/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php +++ b/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php @@ -56,11 +56,11 @@ class ArrayChoiceList implements ChoiceListInterface * * The given choice array must have the same array keys as the value array. * - * @param array|\Traversable $choices The selectable choices - * @param callable|null $value The callable for creating the value - * for a choice. If `null` is passed, - * incrementing integers are used as - * values + * @param iterable $choices The selectable choices + * @param callable|null $value The callable for creating the value + * for a choice. If `null` is passed, + * incrementing integers are used as + * values */ public function __construct($choices, callable $value = null) { diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php b/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php index 1c6f24d6986f8..5abbeac491601 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/ChoiceListFactoryInterface.php @@ -31,9 +31,9 @@ interface ChoiceListFactoryInterface * The callable receives the choice as first and the array key as the second * argument. * - * @param array|\Traversable $choices The choices - * @param null|callable $value The callable generating the choice - * values + * @param iterable $choices The choices + * @param null|callable $value The callable generating the choice + * values * * @return ChoiceListInterface The choice list */ diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php index 3e6f1c177b258..8bed7ca8349a1 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php @@ -63,7 +63,7 @@ public function getDecoratedFactory() /** * {@inheritdoc} * - * @param array|\Traversable $choices The choices + * @param iterable $choices The choices * @param null|callable|string|PropertyPath $value The callable or path for * generating the choice values * diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FileType.php b/src/Symfony/Component/Form/Extension/Core/Type/FileType.php index c882c8eda31d1..b801101cbae7f 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FileType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FileType.php @@ -27,20 +27,35 @@ class FileType extends AbstractType */ public function buildForm(FormBuilderInterface $builder, array $options) { - if ($options['multiple']) { - $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { - $form = $event->getForm(); - $data = $event->getData(); + $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) { + $form = $event->getForm(); + $requestHandler = $form->getConfig()->getRequestHandler(); + $data = null; + + if ($options['multiple']) { + $data = array(); + + foreach ($event->getData() as $file) { + if ($requestHandler->isFileUpload($file)) { + $data[] = $file; + } + } // submitted data for an input file (not required) without choosing any file - if (array(null) === $data) { + if (array(null) === $data || array() === $data) { $emptyData = $form->getConfig()->getEmptyData(); $data = is_callable($emptyData) ? call_user_func($emptyData, $form, $data) : $emptyData; - $event->setData($data); } - }); - } + + $event->setData($data); + } elseif (!$requestHandler->isFileUpload($event->getData())) { + $emptyData = $form->getConfig()->getEmptyData(); + + $data = is_callable($emptyData) ? call_user_func($emptyData, $form, $data) : $emptyData; + $event->setData($data); + } + }); } /** diff --git a/src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php b/src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php index f65ee33f87a0c..571db0eb41dfc 100644 --- a/src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php +++ b/src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php @@ -16,6 +16,7 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\RequestHandlerInterface; use Symfony\Component\Form\Util\ServerParams; +use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\Request; /** @@ -28,9 +29,6 @@ class HttpFoundationRequestHandler implements RequestHandlerInterface { private $serverParams; - /** - * {@inheritdoc} - */ public function __construct(ServerParams $serverParams = null) { $this->serverParams = $serverParams ?: new ServerParams(); @@ -109,4 +107,12 @@ public function handleRequest(FormInterface $form, $request = null) $form->submit($data, 'PATCH' !== $method); } + + /** + * {@inheritdoc} + */ + public function isFileUpload($data) + { + return $data instanceof File; + } } diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index c32f8ed0fbc7b..d7734e3f4fe63 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -49,7 +49,7 @@ * either as "Y-m-d" string or as timestamp. Internally we still want to * use a DateTime object for processing. To convert the data from string/integer * to DateTime you can set a normalization transformer by calling - * addNormTransformer(). The normalized data is then converted to the displayed + * addModelTransformer(). The normalized data is then converted to the displayed * data as described before. * * The conversions (1) -> (2) -> (3) use the transform methods of the transformers. diff --git a/src/Symfony/Component/Form/FormConfigBuilder.php b/src/Symfony/Component/Form/FormConfigBuilder.php index 801393b6cf9ca..f6cdff5310494 100644 --- a/src/Symfony/Component/Form/FormConfigBuilder.php +++ b/src/Symfony/Component/Form/FormConfigBuilder.php @@ -32,7 +32,7 @@ class FormConfigBuilder implements FormConfigBuilderInterface * * @var NativeRequestHandler */ - private static $nativeRequestProcessor; + private static $nativeRequestHandler; /** * The accepted request methods. @@ -496,10 +496,10 @@ public function getMethod() public function getRequestHandler() { if (null === $this->requestHandler) { - if (null === self::$nativeRequestProcessor) { - self::$nativeRequestProcessor = new NativeRequestHandler(); + if (null === self::$nativeRequestHandler) { + self::$nativeRequestHandler = new NativeRequestHandler(); } - $this->requestHandler = self::$nativeRequestProcessor; + $this->requestHandler = self::$nativeRequestHandler; } return $this->requestHandler; diff --git a/src/Symfony/Component/Form/NativeRequestHandler.php b/src/Symfony/Component/Form/NativeRequestHandler.php index e28c2b4b102cf..37e1c99a74505 100644 --- a/src/Symfony/Component/Form/NativeRequestHandler.php +++ b/src/Symfony/Component/Form/NativeRequestHandler.php @@ -23,14 +23,6 @@ class NativeRequestHandler implements RequestHandlerInterface { private $serverParams; - /** - * {@inheritdoc} - */ - public function __construct(ServerParams $params = null) - { - $this->serverParams = $params ?: new ServerParams(); - } - /** * The allowed keys of the $_FILES array. */ @@ -42,6 +34,11 @@ public function __construct(ServerParams $params = null) 'type', ); + public function __construct(ServerParams $params = null) + { + $this->serverParams = $params ?: new ServerParams(); + } + /** * {@inheritdoc} */ @@ -121,6 +118,17 @@ public function handleRequest(FormInterface $form, $request = null) $form->submit($data, 'PATCH' !== $method); } + /** + * {@inheritdoc} + */ + public function isFileUpload($data) + { + // POST data will always be strings or arrays of strings. Thus, we can be sure + // that the submitted data is a file upload if the "error" value is an integer + // (this value must have been injected by PHP itself). + return is_array($data) && isset($data['error']) && is_int($data['error']); + } + /** * Returns the method used to submit the request to the server. * diff --git a/src/Symfony/Component/Form/RequestHandlerInterface.php b/src/Symfony/Component/Form/RequestHandlerInterface.php index 598a7bb17438c..3d7b45d5064af 100644 --- a/src/Symfony/Component/Form/RequestHandlerInterface.php +++ b/src/Symfony/Component/Form/RequestHandlerInterface.php @@ -25,4 +25,13 @@ interface RequestHandlerInterface * @param mixed $request The current request */ public function handleRequest(FormInterface $form, $request = null); + + /** + * Returns true if the given data is a file upload. + * + * @param mixed $data The form field data + * + * @return bool + */ + public function isFileUpload($data); } diff --git a/src/Symfony/Component/Form/Tests/AbstractRequestHandlerTest.php b/src/Symfony/Component/Form/Tests/AbstractRequestHandlerTest.php index f9aa7a5341cb4..7bc1a65219b98 100644 --- a/src/Symfony/Component/Form/Tests/AbstractRequestHandlerTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractRequestHandlerTest.php @@ -353,12 +353,24 @@ public function getPostMaxSizeFixtures() ); } + public function testUploadedFilesAreAccepted() + { + $this->assertTrue($this->requestHandler->isFileUpload($this->getMockFile())); + } + + public function testInvalidFilesAreRejected() + { + $this->assertFalse($this->requestHandler->isFileUpload($this->getInvalidFile())); + } + abstract protected function setRequestData($method, $data, $files = array()); abstract protected function getRequestHandler(); abstract protected function getMockFile($suffix = ''); + abstract protected function getInvalidFile(); + protected function getMockForm($name, $method = null, $compound = true) { $config = $this->getMockBuilder('Symfony\Component\Form\FormConfigInterface')->getMock(); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FileTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FileTypeTest.php index 1545e8ae9d4b1..8ac62c8dfb1da 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FileTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FileTypeTest.php @@ -11,6 +11,11 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; +use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler; +use Symfony\Component\Form\NativeRequestHandler; +use Symfony\Component\Form\RequestHandlerInterface; +use Symfony\Component\HttpFoundation\File\UploadedFile; + class FileTypeTest extends BaseTypeTest { const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\FileType'; @@ -29,40 +34,49 @@ public function testSetData() $this->assertSame($data, $form->getData()); } - public function testSubmit() + /** + * @dataProvider requestHandlerProvider + */ + public function testSubmit(RequestHandlerInterface $requestHandler) { - $form = $this->factory->createBuilder(static::TESTED_TYPE)->getForm(); - $data = $this->createUploadedFileMock('abcdef', 'original.jpg', true); + $form = $this->factory->createBuilder(static::TESTED_TYPE)->setRequestHandler($requestHandler)->getForm(); + $data = $this->createUploadedFileMock($requestHandler, __DIR__.'/../../../Fixtures/foo', 'foo.jpg'); $form->submit($data); $this->assertSame($data, $form->getData()); } - public function testSetDataMultiple() + /** + * @dataProvider requestHandlerProvider + */ + public function testSetDataMultiple(RequestHandlerInterface $requestHandler) { $form = $this->factory->createBuilder(static::TESTED_TYPE, null, array( 'multiple' => true, - ))->getForm(); + ))->setRequestHandler($requestHandler)->getForm(); $data = array( - $this->createUploadedFileMock('abcdef', 'first.jpg', true), - $this->createUploadedFileMock('zyxwvu', 'second.jpg', true), + $this->createUploadedFileMock($requestHandler, __DIR__.'/../../../Fixtures/foo', 'foo.jpg'), + $this->createUploadedFileMock($requestHandler, __DIR__.'/../../../Fixtures/foo2', 'foo2.jpg'), ); $form->setData($data); $this->assertSame($data, $form->getData()); } - public function testSubmitMultiple() + /** + * @dataProvider requestHandlerProvider + */ + public function testSubmitMultiple(RequestHandlerInterface $requestHandler) { $form = $this->factory->createBuilder(static::TESTED_TYPE, null, array( 'multiple' => true, - ))->getForm(); + ))->setRequestHandler($requestHandler)->getForm(); $data = array( - $this->createUploadedFileMock('abcdef', 'first.jpg', true), - $this->createUploadedFileMock('zyxwvu', 'second.jpg', true), + $this->createUploadedFileMock($requestHandler, __DIR__.'/../../../Fixtures/foo', 'foo.jpg'), + $this->createUploadedFileMock($requestHandler, __DIR__.'/../../../Fixtures/foo2', 'foo2.jpg'), ); $form->submit($data); @@ -73,11 +87,14 @@ public function testSubmitMultiple() $this->assertArrayHasKey('multiple', $view->vars['attr']); } - public function testDontPassValueToView() + /** + * @dataProvider requestHandlerProvider + */ + public function testDontPassValueToView(RequestHandlerInterface $requestHandler) { - $form = $this->factory->create(static::TESTED_TYPE); + $form = $this->factory->createBuilder(static::TESTED_TYPE)->setRequestHandler($requestHandler)->getForm(); $form->submit(array( - 'file' => $this->createUploadedFileMock('abcdef', 'original.jpg', true), + 'file' => $this->createUploadedFileMock($requestHandler, __DIR__.'/../../../Fixtures/foo', 'foo.jpg'), )); $this->assertEquals('', $form->createView()->vars['value']); @@ -109,29 +126,59 @@ public function testSubmitNullWhenMultiple() $this->assertSame(array(), $form->getViewData()); } - private function createUploadedFileMock($name, $originalName, $valid) + /** + * @dataProvider requestHandlerProvider + */ + public function testSubmittedFilePathsAreDropped(RequestHandlerInterface $requestHandler) { - $file = $this - ->getMockBuilder('Symfony\Component\HttpFoundation\File\UploadedFile') - ->setConstructorArgs(array(__DIR__.'/../../../Fixtures/foo', 'foo')) - ->getMock() - ; - $file - ->expects($this->any()) - ->method('getBasename') - ->will($this->returnValue($name)) - ; - $file - ->expects($this->any()) - ->method('getClientOriginalName') - ->will($this->returnValue($originalName)) - ; - $file - ->expects($this->any()) - ->method('isValid') - ->will($this->returnValue($valid)) - ; - - return $file; + $form = $this->factory->createBuilder(static::TESTED_TYPE)->setRequestHandler($requestHandler)->getForm(); + $form->submit('file:///etc/passwd'); + + $this->assertNull($form->getData()); + $this->assertNull($form->getNormData()); + $this->assertSame('', $form->getViewData()); + } + + /** + * @dataProvider requestHandlerProvider + */ + public function testMultipleSubmittedFilePathsAreDropped(RequestHandlerInterface $requestHandler) + { + $form = $this->factory + ->createBuilder(static::TESTED_TYPE, null, array( + 'multiple' => true, + )) + ->setRequestHandler($requestHandler) + ->getForm(); + $form->submit(array( + 'file:///etc/passwd', + $this->createUploadedFileMock(new HttpFoundationRequestHandler(), __DIR__.'/../../../Fixtures/foo', 'foo.jpg'), + $this->createUploadedFileMock(new NativeRequestHandler(), __DIR__.'/../../../Fixtures/foo2', 'foo2.jpg'), + )); + + $this->assertCount(1, $form->getData()); + } + + public function requestHandlerProvider() + { + return array( + array(new HttpFoundationRequestHandler()), + array(new NativeRequestHandler()), + ); + } + + private function createUploadedFileMock(RequestHandlerInterface $requestHandler, $path, $originalName) + { + if ($requestHandler instanceof HttpFoundationRequestHandler) { + return new UploadedFile($path, $originalName, null, 10, null, true); + } + + return array( + 'name' => $originalName, + 'error' => 0, + 'type' => 'text/plain', + 'tmp_name' => $path, + 'size' => 10, + ); } } diff --git a/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/HttpFoundationRequestHandlerTest.php b/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/HttpFoundationRequestHandlerTest.php index e71a1fdfd8058..9fbe12258311a 100644 --- a/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/HttpFoundationRequestHandlerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/HttpFoundationRequestHandlerTest.php @@ -51,4 +51,9 @@ protected function getMockFile($suffix = '') { return new UploadedFile(__DIR__.'/../../Fixtures/foo'.$suffix, 'foo'.$suffix); } + + protected function getInvalidFile() + { + return 'file:///etc/passwd'; + } } diff --git a/src/Symfony/Component/Form/Tests/NativeRequestHandlerTest.php b/src/Symfony/Component/Form/Tests/NativeRequestHandlerTest.php index 38580063e7195..aba417288a948 100644 --- a/src/Symfony/Component/Form/Tests/NativeRequestHandlerTest.php +++ b/src/Symfony/Component/Form/Tests/NativeRequestHandlerTest.php @@ -216,4 +216,15 @@ protected function getMockFile($suffix = '') 'size' => 100, ); } + + protected function getInvalidFile() + { + return array( + 'name' => 'upload.txt', + 'type' => 'text/plain', + 'tmp_name' => 'owfdskjasdfsa', + 'error' => '0', + 'size' => '100', + ); + } } diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php b/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php index 612b6bd3edb7a..0dfad9acb3532 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php @@ -100,12 +100,6 @@ class NativeSessionStorage implements SessionStorageInterface */ public function __construct(array $options = array(), $handler = null, MetadataBag $metaBag = null) { - $this->setMetadataBag($metaBag); - - if (\PHP_SESSION_ACTIVE === session_status()) { - return; - } - $options += array( 'cache_limiter' => '', 'cache_expire' => 0, @@ -115,6 +109,7 @@ public function __construct(array $options = array(), $handler = null, MetadataB session_register_shutdown(); + $this->setMetadataBag($metaBag); $this->setOptions($options); $this->setSaveHandler($handler); } @@ -349,7 +344,7 @@ public function isStarted() */ public function setOptions(array $options) { - if (headers_sent()) { + if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { return; } @@ -403,10 +398,6 @@ public function setSaveHandler($saveHandler = null) throw new \InvalidArgumentException('Must be instance of AbstractProxy; implement \SessionHandlerInterface; or be null.'); } - if (headers_sent($file, $line)) { - throw new \RuntimeException(sprintf('Failed to set the session handler because headers have already been sent by "%s" at line %d.', $file, $line)); - } - // Wrap $saveHandler in proxy and prevent double wrapping of proxy if (!$saveHandler instanceof AbstractProxy && $saveHandler instanceof \SessionHandlerInterface) { $saveHandler = new SessionHandlerProxy($saveHandler); @@ -415,6 +406,10 @@ public function setSaveHandler($saveHandler = null) } $this->saveHandler = $saveHandler; + if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { + return; + } + if ($this->saveHandler instanceof SessionHandlerProxy) { session_set_save_handler($this->saveHandler->getHandler(), false); } elseif ($this->saveHandler instanceof \SessionHandlerInterface) { diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/NativeSessionStorageTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/NativeSessionStorageTest.php index ec094a6169e3a..f1442e8edb582 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/NativeSessionStorageTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/NativeSessionStorageTest.php @@ -262,4 +262,16 @@ public function testSetSessionOptionsOnceSessionStartedIsIgnored() // Assert no exception has been thrown by `getStorage()` $this->addToAssertionCount(1); } + + public function testGetBagsOnceSessionStartedIsIgnored() + { + session_start(); + $bag = new AttributeBag(); + $bag->setName('flashes'); + + $storage = $this->getStorage(); + $storage->registerBag($bag); + + $this->assertEquals($storage->getBag('flashes'), $bag); + } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ServiceValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ServiceValueResolver.php index b1da6a9a75938..c55564c0467ef 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ServiceValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ServiceValueResolver.php @@ -35,7 +35,13 @@ public function __construct(ContainerInterface $container) */ public function supports(Request $request, ArgumentMetadata $argument) { - return is_string($controller = $request->attributes->get('_controller')) && $this->container->has($controller) && $this->container->get($controller)->has($argument->getName()); + $controller = $request->attributes->get('_controller'); + + if (\is_array($controller) && \is_callable($controller, true) && \is_string($controller[0])) { + $controller = $controller[0].'::'.$controller[1]; + } + + return \is_string($controller) && $this->container->has($controller) && $this->container->get($controller)->has($argument->getName()); } /** @@ -43,6 +49,10 @@ public function supports(Request $request, ArgumentMetadata $argument) */ public function resolve(Request $request, ArgumentMetadata $argument) { - yield $this->container->get($request->attributes->get('_controller'))->get($argument->getName()); + if (\is_array($controller = $request->attributes->get('_controller'))) { + $controller = $controller[0].'::'.$controller[1]; + } + + yield $this->container->get($controller)->get($argument->getName()); } } diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php index 52cc3a4a8383f..29433a6d5b195 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php @@ -53,6 +53,7 @@ public function process(ContainerBuilder $container) } if (empty($services)) { + $container->removeAlias('services_resetter'); $container->removeDefinition('services_resetter'); return; diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 13eafcdff6f1c..c86d622cbb800 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -67,12 +67,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private $requestStackSize = 0; private $resetServices = false; - const VERSION = '3.4.0-BETA4'; + const VERSION = '3.4.0-RC1'; const VERSION_ID = 30400; const MAJOR_VERSION = 3; const MINOR_VERSION = 4; const RELEASE_VERSION = 0; - const EXTRA_VERSION = 'BETA4'; + const EXTRA_VERSION = 'RC1'; const END_OF_MAINTENANCE = '11/2020'; const END_OF_LIFE = '11/2021'; @@ -818,7 +818,7 @@ protected function dumpContainer(ConfigCache $cache, ContainerBuilder $container 'file' => $cache->getPath(), 'as_files' => true, 'debug' => $this->debug, - 'hot_path_tag' => !$this->loadClassCache ? 'container.hot_path' : null, + 'inline_class_loader_parameter' => !$this->loadClassCache && !class_exists(ClassCollectionLoader::class, false) ? 'container.dumper.inline_class_loader' : null, )); $rootCode = array_pop($content); diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php new file mode 100644 index 0000000000000..b05828f5bf6d2 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ServiceValueResolver; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; + +class ServiceValueResolverTest extends TestCase +{ + public function testDoNotSupportWhenControllerDoNotExists() + { + $resolver = new ServiceValueResolver(new ServiceLocator(array())); + $argument = new ArgumentMetadata('dummy', DummyService::class, false, false, null); + $request = $this->requestWithAttributes(array('_controller' => 'my_controller')); + + $this->assertFalse($resolver->supports($request, $argument)); + } + + public function testExistingController() + { + $resolver = new ServiceValueResolver(new ServiceLocator(array( + 'App\\Controller\\Mine::method' => function () { + return new ServiceLocator(array( + 'dummy' => function () { + return new DummyService(); + }, + )); + }, + ))); + + $request = $this->requestWithAttributes(array('_controller' => 'App\\Controller\\Mine::method')); + $argument = new ArgumentMetadata('dummy', DummyService::class, false, false, null); + + $this->assertTrue($resolver->supports($request, $argument)); + $this->assertYieldEquals(array(new DummyService()), $resolver->resolve($request, $argument)); + } + + public function testControllerNameIsAnArray() + { + $resolver = new ServiceValueResolver(new ServiceLocator(array( + 'App\\Controller\\Mine::method' => function () { + return new ServiceLocator(array( + 'dummy' => function () { + return new DummyService(); + }, + )); + }, + ))); + + $request = $this->requestWithAttributes(array('_controller' => array('App\\Controller\\Mine', 'method'))); + $argument = new ArgumentMetadata('dummy', DummyService::class, false, false, null); + + $this->assertTrue($resolver->supports($request, $argument)); + $this->assertYieldEquals(array(new DummyService()), $resolver->resolve($request, $argument)); + } + + private function requestWithAttributes(array $attributes) + { + $request = Request::create('/'); + + foreach ($attributes as $name => $value) { + $request->attributes->set($name, $value); + } + + return $request; + } + + private function assertYieldEquals(array $expected, \Generator $generator) + { + $args = array(); + foreach ($generator as $arg) { + $args[] = $arg; + } + + $this->assertEquals($expected, $args); + } +} + +class DummyService +{ +} diff --git a/src/Symfony/Component/Intl/Data/Bundle/Reader/JsonBundleReader.php b/src/Symfony/Component/Intl/Data/Bundle/Reader/JsonBundleReader.php index 4521b06d43936..17a9df32c21a9 100644 --- a/src/Symfony/Component/Intl/Data/Bundle/Reader/JsonBundleReader.php +++ b/src/Symfony/Component/Intl/Data/Bundle/Reader/JsonBundleReader.php @@ -30,6 +30,11 @@ public function read($path, $locale) { $fileName = $path.'/'.$locale.'.json'; + // prevent directory traversal attacks + if (dirname($fileName) !== $path) { + throw new ResourceBundleNotFoundException(sprintf('The resource bundle "%s" does not exist.', $fileName)); + } + if (!file_exists($fileName)) { throw new ResourceBundleNotFoundException(sprintf( 'The resource bundle "%s" does not exist.', diff --git a/src/Symfony/Component/Intl/Data/Bundle/Reader/PhpBundleReader.php b/src/Symfony/Component/Intl/Data/Bundle/Reader/PhpBundleReader.php index 57391ce010077..0b66bb1a557c3 100644 --- a/src/Symfony/Component/Intl/Data/Bundle/Reader/PhpBundleReader.php +++ b/src/Symfony/Component/Intl/Data/Bundle/Reader/PhpBundleReader.php @@ -30,6 +30,11 @@ public function read($path, $locale) { $fileName = $path.'/'.$locale.'.php'; + // prevent directory traversal attacks + if (dirname($fileName) !== $path) { + throw new ResourceBundleNotFoundException(sprintf('The resource bundle "%s" does not exist.', $fileName)); + } + if (!file_exists($fileName)) { throw new ResourceBundleNotFoundException(sprintf( 'The resource bundle "%s/%s.php" does not exist.', diff --git a/src/Symfony/Component/Intl/Data/Bundle/Writer/TextBundleWriter.php b/src/Symfony/Component/Intl/Data/Bundle/Writer/TextBundleWriter.php index 3a444fd350edb..20fad5347aed2 100644 --- a/src/Symfony/Component/Intl/Data/Bundle/Writer/TextBundleWriter.php +++ b/src/Symfony/Component/Intl/Data/Bundle/Writer/TextBundleWriter.php @@ -195,11 +195,11 @@ private function writeArray($file, array $value, $indentation) /** * Writes a "table" node. * - * @param resource $file The file handle to write to - * @param array|\Traversable $value The value of the node - * @param int $indentation The number of levels to indent - * @param bool $fallback Whether the table should be merged - * with the fallback locale + * @param resource $file The file handle to write to + * @param iterable $value The value of the node + * @param int $indentation The number of levels to indent + * @param bool $fallback Whether the table should be merged + * with the fallback locale * * @throws UnexpectedTypeException when $value is not an array and not a * \Traversable instance diff --git a/src/Symfony/Component/Intl/Tests/Data/Bundle/Reader/Fixtures/invalid_directory/en.json b/src/Symfony/Component/Intl/Tests/Data/Bundle/Reader/Fixtures/invalid_directory/en.json new file mode 100644 index 0000000000000..16ea32adf74ae --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/Data/Bundle/Reader/Fixtures/invalid_directory/en.json @@ -0,0 +1 @@ +{"Foo":"Bar"} diff --git a/src/Symfony/Component/Intl/Tests/Data/Bundle/Reader/Fixtures/invalid_directory/en.php b/src/Symfony/Component/Intl/Tests/Data/Bundle/Reader/Fixtures/invalid_directory/en.php new file mode 100644 index 0000000000000..f2b06a91ad32e --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/Data/Bundle/Reader/Fixtures/invalid_directory/en.php @@ -0,0 +1,14 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return array( + 'Foo' => 'Bar', +); diff --git a/src/Symfony/Component/Intl/Tests/Data/Bundle/Reader/JsonBundleReaderTest.php b/src/Symfony/Component/Intl/Tests/Data/Bundle/Reader/JsonBundleReaderTest.php index a8ccabe07bb75..2b6e6c4169e7f 100644 --- a/src/Symfony/Component/Intl/Tests/Data/Bundle/Reader/JsonBundleReaderTest.php +++ b/src/Symfony/Component/Intl/Tests/Data/Bundle/Reader/JsonBundleReaderTest.php @@ -69,4 +69,12 @@ public function testReadFailsIfInvalidJson() { $this->reader->read(__DIR__.'/Fixtures/json', 'en_Invalid'); } + + /** + * @expectedException \Symfony\Component\Intl\Exception\ResourceBundleNotFoundException + */ + public function testReaderDoesNotBreakOutOfGivenPath() + { + $this->reader->read(__DIR__.'/Fixtures/json', '../invalid_directory/en'); + } } diff --git a/src/Symfony/Component/Intl/Tests/Data/Bundle/Reader/PhpBundleReaderTest.php b/src/Symfony/Component/Intl/Tests/Data/Bundle/Reader/PhpBundleReaderTest.php index 51898cb2be5d0..954e2f04237c8 100644 --- a/src/Symfony/Component/Intl/Tests/Data/Bundle/Reader/PhpBundleReaderTest.php +++ b/src/Symfony/Component/Intl/Tests/Data/Bundle/Reader/PhpBundleReaderTest.php @@ -61,4 +61,12 @@ public function testReadFailsIfNotAFile() { $this->reader->read(__DIR__.'/Fixtures/NotAFile', 'en'); } + + /** + * @expectedException \Symfony\Component\Intl\Exception\ResourceBundleNotFoundException + */ + public function testReaderDoesNotBreakOutOfGivenPath() + { + $this->reader->read(__DIR__.'/Fixtures/php', '../invalid_directory/en'); + } } diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php index 1ad601c715aaf..5a925e7f0d1b7 100644 --- a/src/Symfony/Component/Process/Tests/ProcessTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessTest.php @@ -1265,7 +1265,7 @@ public function testSimpleInputStream() { $input = new InputStream(); - $process = $this->getProcessForCode('echo \'ping\'; stream_copy_to_stream(STDIN, STDOUT);'); + $process = $this->getProcessForCode('echo \'ping\'; echo fread(STDIN, 4); echo fread(STDIN, 4);'); $process->setInput($input); $process->start(function ($type, $data) use ($input) { diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 2309cc63b4ed7..c23229dd86ca2 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -645,11 +645,11 @@ private function writeProperty($zval, $property, $value) /** * Adjusts a collection-valued property by calling add*() and remove*() methods. * - * @param array $zval The array containing the object to write to - * @param string $property The property to write - * @param array|\Traversable $collection The collection to write - * @param string $addMethod The add*() method - * @param string $removeMethod The remove*() method + * @param array $zval The array containing the object to write to + * @param string $property The property to write + * @param iterable $collection The collection to write + * @param string $addMethod The add*() method + * @param string $removeMethod The remove*() method */ private function writeCollection($zval, $property, $collection, $addMethod, $removeMethod) { diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/validresource.php b/src/Symfony/Component/Routing/Tests/Fixtures/validresource.php index f59a234d6589a..482c80b29e919 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/validresource.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/validresource.php @@ -1,8 +1,5 @@ import('validpattern.php'); diff --git a/src/Symfony/Component/Security/Csrf/CsrfTokenManager.php b/src/Symfony/Component/Security/Csrf/CsrfTokenManager.php index 487df7558f95c..332880fda518f 100644 --- a/src/Symfony/Component/Security/Csrf/CsrfTokenManager.php +++ b/src/Symfony/Component/Security/Csrf/CsrfTokenManager.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Security\Csrf; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Security\Core\Exception\InvalidArgumentException; use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator; use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface; use Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage; @@ -20,16 +22,45 @@ * Default implementation of {@link CsrfTokenManagerInterface}. * * @author Bernhard Schussek + * @author Kévin Dunglas */ class CsrfTokenManager implements CsrfTokenManagerInterface { private $generator; private $storage; + private $namespace; - public function __construct(TokenGeneratorInterface $generator = null, TokenStorageInterface $storage = null) + /** + * @param null|string|RequestStack|callable $namespace + * * null: generates a namespace using $_SERVER['HTTPS'] + * * string: uses the given string + * * RequestStack: generates a namespace using the current master request + * * callable: uses the result of this callable (must return a string) + */ + public function __construct(TokenGeneratorInterface $generator = null, TokenStorageInterface $storage = null, $namespace = null) { $this->generator = $generator ?: new UriSafeTokenGenerator(); $this->storage = $storage ?: new NativeSessionTokenStorage(); + + $superGlobalNamespaceGenerator = function () { + return !empty($_SERVER['HTTPS']) && 'off' !== strtolower($_SERVER['HTTPS']) ? 'https-' : ''; + }; + + if (null === $namespace) { + $this->namespace = $superGlobalNamespaceGenerator; + } elseif ($namespace instanceof RequestStack) { + $this->namespace = function () use ($namespace, $superGlobalNamespaceGenerator) { + if ($request = $namespace->getMasterRequest()) { + return $request->isSecure() ? 'https-' : ''; + } + + return $superGlobalNamespaceGenerator(); + }; + } elseif (is_callable($namespace) || is_string($namespace)) { + $this->namespace = $namespace; + } else { + throw new InvalidArgumentException(sprintf('$namespace must be a string, a callable returning a string, null or an instance of "RequestStack". "%s" given.', gettype($namespace))); + } } /** @@ -37,12 +68,13 @@ public function __construct(TokenGeneratorInterface $generator = null, TokenStor */ public function getToken($tokenId) { - if ($this->storage->hasToken($tokenId)) { - $value = $this->storage->getToken($tokenId); + $namespacedId = $this->getNamespace().$tokenId; + if ($this->storage->hasToken($namespacedId)) { + $value = $this->storage->getToken($namespacedId); } else { $value = $this->generator->generateToken(); - $this->storage->setToken($tokenId, $value); + $this->storage->setToken($namespacedId, $value); } return new CsrfToken($tokenId, $value); @@ -53,9 +85,10 @@ public function getToken($tokenId) */ public function refreshToken($tokenId) { + $namespacedId = $this->getNamespace().$tokenId; $value = $this->generator->generateToken(); - $this->storage->setToken($tokenId, $value); + $this->storage->setToken($namespacedId, $value); return new CsrfToken($tokenId, $value); } @@ -65,7 +98,7 @@ public function refreshToken($tokenId) */ public function removeToken($tokenId) { - return $this->storage->removeToken($tokenId); + return $this->storage->removeToken($this->getNamespace().$tokenId); } /** @@ -73,10 +106,16 @@ public function removeToken($tokenId) */ public function isTokenValid(CsrfToken $token) { - if (!$this->storage->hasToken($token->getId())) { + $namespacedId = $this->getNamespace().$token->getId(); + if (!$this->storage->hasToken($namespacedId)) { return false; } - return hash_equals($this->storage->getToken($token->getId()), $token->getValue()); + return hash_equals($this->storage->getToken($namespacedId), $token->getValue()); + } + + private function getNamespace() + { + return is_callable($ns = $this->namespace) ? $ns() : $ns; } } diff --git a/src/Symfony/Component/Security/Csrf/Tests/CsrfTokenManagerTest.php b/src/Symfony/Component/Security/Csrf/Tests/CsrfTokenManagerTest.php index 9ce35f4dea123..214a189e99b17 100644 --- a/src/Symfony/Component/Security/Csrf/Tests/CsrfTokenManagerTest.php +++ b/src/Symfony/Component/Security/Csrf/Tests/CsrfTokenManagerTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Security\Csrf\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfTokenManager; @@ -21,145 +23,202 @@ class CsrfTokenManagerTest extends TestCase { /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @dataProvider getManagerGeneratorAndStorage */ - private $generator; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $storage; - - /** - * @var CsrfTokenManager - */ - private $manager; - - protected function setUp() - { - $this->generator = $this->getMockBuilder('Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface')->getMock(); - $this->storage = $this->getMockBuilder('Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface')->getMock(); - $this->manager = new CsrfTokenManager($this->generator, $this->storage); - } - - protected function tearDown() - { - $this->generator = null; - $this->storage = null; - $this->manager = null; - } - - public function testGetNonExistingToken() + public function testGetNonExistingToken($namespace, $manager, $storage, $generator) { - $this->storage->expects($this->once()) + $storage->expects($this->once()) ->method('hasToken') - ->with('token_id') + ->with($namespace.'token_id') ->will($this->returnValue(false)); - $this->generator->expects($this->once()) + $generator->expects($this->once()) ->method('generateToken') ->will($this->returnValue('TOKEN')); - $this->storage->expects($this->once()) + $storage->expects($this->once()) ->method('setToken') - ->with('token_id', 'TOKEN'); + ->with($namespace.'token_id', 'TOKEN'); - $token = $this->manager->getToken('token_id'); + $token = $manager->getToken('token_id'); $this->assertInstanceOf('Symfony\Component\Security\Csrf\CsrfToken', $token); $this->assertSame('token_id', $token->getId()); $this->assertSame('TOKEN', $token->getValue()); } - public function testUseExistingTokenIfAvailable() + /** + * @dataProvider getManagerGeneratorAndStorage + */ + public function testUseExistingTokenIfAvailable($namespace, $manager, $storage) { - $this->storage->expects($this->once()) + $storage->expects($this->once()) ->method('hasToken') - ->with('token_id') + ->with($namespace.'token_id') ->will($this->returnValue(true)); - $this->storage->expects($this->once()) + $storage->expects($this->once()) ->method('getToken') - ->with('token_id') + ->with($namespace.'token_id') ->will($this->returnValue('TOKEN')); - $token = $this->manager->getToken('token_id'); + $token = $manager->getToken('token_id'); $this->assertInstanceOf('Symfony\Component\Security\Csrf\CsrfToken', $token); $this->assertSame('token_id', $token->getId()); $this->assertSame('TOKEN', $token->getValue()); } - public function testRefreshTokenAlwaysReturnsNewToken() + /** + * @dataProvider getManagerGeneratorAndStorage + */ + public function testRefreshTokenAlwaysReturnsNewToken($namespace, $manager, $storage, $generator) { - $this->storage->expects($this->never()) + $storage->expects($this->never()) ->method('hasToken'); - $this->generator->expects($this->once()) + $generator->expects($this->once()) ->method('generateToken') ->will($this->returnValue('TOKEN')); - $this->storage->expects($this->once()) + $storage->expects($this->once()) ->method('setToken') - ->with('token_id', 'TOKEN'); + ->with($namespace.'token_id', 'TOKEN'); - $token = $this->manager->refreshToken('token_id'); + $token = $manager->refreshToken('token_id'); $this->assertInstanceOf('Symfony\Component\Security\Csrf\CsrfToken', $token); $this->assertSame('token_id', $token->getId()); $this->assertSame('TOKEN', $token->getValue()); } - public function testMatchingTokenIsValid() + /** + * @dataProvider getManagerGeneratorAndStorage + */ + public function testMatchingTokenIsValid($namespace, $manager, $storage) { - $this->storage->expects($this->once()) + $storage->expects($this->once()) ->method('hasToken') - ->with('token_id') + ->with($namespace.'token_id') ->will($this->returnValue(true)); - $this->storage->expects($this->once()) + $storage->expects($this->once()) ->method('getToken') - ->with('token_id') + ->with($namespace.'token_id') ->will($this->returnValue('TOKEN')); - $this->assertTrue($this->manager->isTokenValid(new CsrfToken('token_id', 'TOKEN'))); + $this->assertTrue($manager->isTokenValid(new CsrfToken('token_id', 'TOKEN'))); } - public function testNonMatchingTokenIsNotValid() + /** + * @dataProvider getManagerGeneratorAndStorage + */ + public function testNonMatchingTokenIsNotValid($namespace, $manager, $storage) { - $this->storage->expects($this->once()) + $storage->expects($this->once()) ->method('hasToken') - ->with('token_id') + ->with($namespace.'token_id') ->will($this->returnValue(true)); - $this->storage->expects($this->once()) + $storage->expects($this->once()) ->method('getToken') - ->with('token_id') + ->with($namespace.'token_id') ->will($this->returnValue('TOKEN')); - $this->assertFalse($this->manager->isTokenValid(new CsrfToken('token_id', 'FOOBAR'))); + $this->assertFalse($manager->isTokenValid(new CsrfToken('token_id', 'FOOBAR'))); } - public function testNonExistingTokenIsNotValid() + /** + * @dataProvider getManagerGeneratorAndStorage + */ + public function testNonExistingTokenIsNotValid($namespace, $manager, $storage) { - $this->storage->expects($this->once()) + $storage->expects($this->once()) ->method('hasToken') - ->with('token_id') + ->with($namespace.'token_id') ->will($this->returnValue(false)); - $this->storage->expects($this->never()) + $storage->expects($this->never()) ->method('getToken'); - $this->assertFalse($this->manager->isTokenValid(new CsrfToken('token_id', 'FOOBAR'))); + $this->assertFalse($manager->isTokenValid(new CsrfToken('token_id', 'FOOBAR'))); } - public function testRemoveToken() + /** + * @dataProvider getManagerGeneratorAndStorage + */ + public function testRemoveToken($namespace, $manager, $storage) { - $this->storage->expects($this->once()) + $storage->expects($this->once()) ->method('removeToken') - ->with('token_id') + ->with($namespace.'token_id') ->will($this->returnValue('REMOVED_TOKEN')); - $this->assertSame('REMOVED_TOKEN', $this->manager->removeToken('token_id')); + $this->assertSame('REMOVED_TOKEN', $manager->removeToken('token_id')); + } + + public function testNamespaced() + { + $generator = $this->getMockBuilder('Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface')->getMock(); + $storage = $this->getMockBuilder('Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface')->getMock(); + + $requestStack = new RequestStack(); + $requestStack->push(new Request(array(), array(), array(), array(), array(), array('HTTPS' => 'on'))); + + $manager = new CsrfTokenManager($generator, $storage, null, $requestStack); + + $token = $manager->getToken('foo'); + $this->assertSame('foo', $token->getId()); + } + + public function getManagerGeneratorAndStorage() + { + $data = array(); + + list($generator, $storage) = $this->getGeneratorAndStorage(); + $data[] = array('', new CsrfTokenManager($generator, $storage, ''), $storage, $generator); + + list($generator, $storage) = $this->getGeneratorAndStorage(); + $data[] = array('https-', new CsrfTokenManager($generator, $storage), $storage, $generator); + + list($generator, $storage) = $this->getGeneratorAndStorage(); + $data[] = array('aNamespace-', new CsrfTokenManager($generator, $storage, 'aNamespace-'), $storage, $generator); + + $requestStack = new RequestStack(); + $requestStack->push(new Request(array(), array(), array(), array(), array(), array('HTTPS' => 'on'))); + list($generator, $storage) = $this->getGeneratorAndStorage(); + $data[] = array('https-', new CsrfTokenManager($generator, $storage, $requestStack), $storage, $generator); + + list($generator, $storage) = $this->getGeneratorAndStorage(); + $data[] = array('generated-', new CsrfTokenManager($generator, $storage, function () { + return 'generated-'; + }), $storage, $generator); + + $requestStack = new RequestStack(); + $requestStack->push(new Request()); + list($generator, $storage) = $this->getGeneratorAndStorage(); + $data[] = array('', new CsrfTokenManager($generator, $storage, $requestStack), $storage, $generator); + + return $data; + } + + private function getGeneratorAndStorage() + { + return array( + $this->getMockBuilder('Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface')->getMock(), + $this->getMockBuilder('Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface')->getMock(), + ); + } + + public function setUp() + { + $_SERVER['HTTPS'] = 'on'; + } + + public function tearDown() + { + parent::tearDown(); + + unset($_SERVER['HTTPS']); } } diff --git a/src/Symfony/Component/Security/Csrf/composer.json b/src/Symfony/Component/Security/Csrf/composer.json index 695bb1cf9f0d2..72bef3805e2f6 100644 --- a/src/Symfony/Component/Security/Csrf/composer.json +++ b/src/Symfony/Component/Security/Csrf/composer.json @@ -22,7 +22,10 @@ "symfony/security-core": "~2.8|~3.0|~4.0" }, "require-dev": { - "symfony/http-foundation": "~2.8|~3.0|~4.0" + "symfony/http-foundation": "^2.8.31|~3.3.13|~3.4-beta5|~4.0-beta5" + }, + "conflict": { + "symfony/http-foundation": "<2.8.31|~3.3,<3.3.13|~3.4,<3.4-beta5|~4.0,<4.0-beta5" }, "suggest": { "symfony/http-foundation": "For using the class SessionTokenStorage." diff --git a/src/Symfony/Component/Security/Http/HttpUtils.php b/src/Symfony/Component/Security/Http/HttpUtils.php index f9551370fcd1f..a55421340d495 100644 --- a/src/Symfony/Component/Security/Http/HttpUtils.php +++ b/src/Symfony/Component/Security/Http/HttpUtils.php @@ -29,20 +29,23 @@ class HttpUtils { private $urlGenerator; private $urlMatcher; + private $domainRegexp; /** * @param UrlGeneratorInterface $urlGenerator A UrlGeneratorInterface instance * @param UrlMatcherInterface|RequestMatcherInterface $urlMatcher The URL or Request matcher + * @param string|null $domainRegexp A regexp that the target of HTTP redirections must match, scheme included * * @throws \InvalidArgumentException */ - public function __construct(UrlGeneratorInterface $urlGenerator = null, $urlMatcher = null) + public function __construct(UrlGeneratorInterface $urlGenerator = null, $urlMatcher = null, $domainRegexp = null) { $this->urlGenerator = $urlGenerator; if (null !== $urlMatcher && !$urlMatcher instanceof UrlMatcherInterface && !$urlMatcher instanceof RequestMatcherInterface) { throw new \InvalidArgumentException('Matcher must either implement UrlMatcherInterface or RequestMatcherInterface.'); } $this->urlMatcher = $urlMatcher; + $this->domainRegexp = $domainRegexp; } /** @@ -56,6 +59,10 @@ public function __construct(UrlGeneratorInterface $urlGenerator = null, $urlMatc */ public function createRedirectResponse(Request $request, $path, $status = 302) { + if (null !== $this->domainRegexp && preg_match('#^https?://[^/]++#i', $path, $host) && !preg_match(sprintf($this->domainRegexp, preg_quote($request->getHttpHost())), $host[0])) { + $path = '/'; + } + return new RedirectResponse($this->generateUri($request, $path), $status); } diff --git a/src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php b/src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php index 457588c180529..98476f2ff3ec7 100644 --- a/src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php +++ b/src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php @@ -38,6 +38,38 @@ public function testCreateRedirectResponseWithAbsoluteUrl() $this->assertTrue($response->isRedirect('http://symfony.com/')); } + public function testCreateRedirectResponseWithDomainRegexp() + { + $utils = new HttpUtils($this->getUrlGenerator(), null, '#^https?://symfony\.com$#i'); + $response = $utils->createRedirectResponse($this->getRequest(), 'http://symfony.com/blog'); + + $this->assertTrue($response->isRedirect('http://symfony.com/blog')); + } + + public function testCreateRedirectResponseWithRequestsDomain() + { + $utils = new HttpUtils($this->getUrlGenerator(), null, '#^https?://%s$#i'); + $response = $utils->createRedirectResponse($this->getRequest(), 'http://localhost/blog'); + + $this->assertTrue($response->isRedirect('http://localhost/blog')); + } + + public function testCreateRedirectResponseWithBadRequestsDomain() + { + $utils = new HttpUtils($this->getUrlGenerator(), null, '#^https?://%s$#i'); + $response = $utils->createRedirectResponse($this->getRequest(), 'http://pirate.net/foo'); + + $this->assertTrue($response->isRedirect('http://localhost/')); + } + + public function testCreateRedirectResponseWithProtocolRelativeTarget() + { + $utils = new HttpUtils($this->getUrlGenerator(), null, '#^https?://%s$#i'); + $response = $utils->createRedirectResponse($this->getRequest(), '//evil.com/do-bad-things'); + + $this->assertTrue($response->isRedirect('http://localhost//evil.com/do-bad-things'), 'Protocol-relative redirection should not be supported for security reasons'); + } + public function testCreateRedirectResponseWithRouteName() { $utils = new HttpUtils($urlGenerator = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock()); diff --git a/src/Symfony/Component/Security/composer.json b/src/Symfony/Component/Security/composer.json index bc72e8469bf0e..7fee5322a24b9 100644 --- a/src/Symfony/Component/Security/composer.json +++ b/src/Symfony/Component/Security/composer.json @@ -18,7 +18,7 @@ "require": { "php": "^5.5.9|>=7.0.8", "symfony/event-dispatcher": "~2.8|~3.0|~4.0", - "symfony/http-foundation": "~2.8|~3.0|~4.0", + "symfony/http-foundation": "^2.8.31|~3.3.13|~3.4-beta5|~4.0-beta5", "symfony/http-kernel": "~3.3|~4.0", "symfony/polyfill-php56": "~1.0", "symfony/polyfill-php70": "~1.0", diff --git a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php index 2921e5baf9532..52dc4f4a52dcc 100644 --- a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php @@ -60,11 +60,13 @@ private function supports($class) $class = new \ReflectionClass($class); // We look for at least one non-static property - foreach ($class->getProperties() as $property) { - if (!$property->isStatic()) { - return true; + do { + foreach ($class->getProperties() as $property) { + if (!$property->isStatic()) { + return true; + } } - } + } while ($class = $class->getParentClass()); return false; } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php index 954de0b03e323..29c2127a27973 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php @@ -441,6 +441,11 @@ public function testMaxDepth() $this->assertEquals($expected, $result); } + + public function testInheritedPropertiesSupport() + { + $this->assertTrue($this->normalizer->supportsNormalization(new PropertyChildDummy())); + } } class PropertyDummy @@ -509,3 +514,12 @@ class StaticPropertyDummy { private static $property = 'value'; } + +class PropertyParentDummy +{ + private $foo = 'bar'; +} + +class PropertyChildDummy extends PropertyParentDummy +{ +} diff --git a/src/Symfony/Component/Validator/Constraints/UrlValidator.php b/src/Symfony/Component/Validator/Constraints/UrlValidator.php index 216759cda9edf..222597ed10f41 100644 --- a/src/Symfony/Component/Validator/Constraints/UrlValidator.php +++ b/src/Symfony/Component/Validator/Constraints/UrlValidator.php @@ -25,7 +25,7 @@ class UrlValidator extends ConstraintValidator (%s):// # protocol (([\.\pL\pN-]+:)?([\.\pL\pN-]+)@)? # basic auth ( - ([\pL\pN\pS-\.])+(\.?([\pL\pN]|xn\-\-[\pL\pN-]+)+\.?) # a domain name + ([\pL\pN\pS\-\.])+(\.?([\pL\pN]|xn\-\-[\pL\pN-]+)+\.?) # a domain name | # or \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} # an IP address | # or diff --git a/src/Symfony/Component/Validator/Constraints/ValidValidator.php b/src/Symfony/Component/Validator/Constraints/ValidValidator.php index 45ac69d91d196..b2f1f1c5a06b9 100644 --- a/src/Symfony/Component/Validator/Constraints/ValidValidator.php +++ b/src/Symfony/Component/Validator/Constraints/ValidValidator.php @@ -26,12 +26,9 @@ public function validate($value, Constraint $constraint) throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\Valid'); } - $violations = $this->context->getValidator()->validate($value, null, array($this->context->getGroup())); - - foreach ($violations as $violation) { - $this->context->buildViolation($violation->getMessage(), $violation->getParameters()) - ->atPath($violation->getPropertyPath()) - ->addViolation(); - } + $this->context + ->getValidator() + ->inContext($this->context) + ->validate($value, null, array($this->context->getGroup())); } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/ValidValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/ValidValidatorTest.php new file mode 100644 index 0000000000000..f95650d359b3b --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/ValidValidatorTest.php @@ -0,0 +1,61 @@ +enableAnnotationMapping()->getValidator(); + + $violations = $validator->validate(new Foo(), null, array('nested')); + + $this->assertCount(1, $violations); + $this->assertSame('fooBar.fooBarBaz.foo', $violations->get(0)->getPropertyPath()); + } + + protected function createValidator() + { + return new ValidValidator(); + } +} + +class Foo +{ + /** + * @Assert\Valid(groups={"nested"}) + */ + public $fooBar; + + public function __construct() + { + $this->fooBar = new FooBar(); + } +} + +class FooBar +{ + /** + * @Assert\Valid(groups={"nested"}) + */ + public $fooBarBaz; + + public function __construct() + { + $this->fooBarBaz = new FooBarBaz(); + } +} + +class FooBarBaz +{ + /** + * @Assert\NotBlank(groups={"nested"}) + */ + public $foo; +} diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index 5ac343bfbc9af..ca3e57a75266b 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -374,7 +374,7 @@ private function validateObject($object, $propertyPath, array $groups, $traversa * objects are iterated as well. Nested arrays are always iterated, * regardless of the value of $recursive. * - * @param array|\Traversable $collection The collection + * @param iterable $collection The collection * @param string $propertyPath The current property path * @param string[] $groups The validated groups * @param ExecutionContextInterface $context The current execution context