diff --git a/CHANGELOG-4.3.md b/CHANGELOG-4.3.md index df5f4ef4396b6..a5b9b78847b5c 100644 --- a/CHANGELOG-4.3.md +++ b/CHANGELOG-4.3.md @@ -7,6 +7,16 @@ in 4.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/v4.3.0...v4.3.1 +* 4.3.8 (2019-11-13) + + * bug #34344 [Console] Constant STDOUT might be undefined (nicolas-grekas) + * security #cve-2019-18886 [Security\Core] throw AccessDeniedException when switch user fails (nicolas-grekas) + * security #cve-2019-18888 [Mime] fix guessing mime-types of files with leading dash (nicolas-grekas) + * security #cve-2019-11325 [VarExporter] fix exporting some strings (nicolas-grekas) + * security #cve-2019-18889 [Cache] forbid serializing AbstractAdapter and TagAwareAdapter instances (nicolas-grekas) + * security #cve-2019-18888 [HttpFoundation] fix guessing mime-types of files with leading dash (nicolas-grekas) + * security #cve-2019-18887 [HttpKernel] Use constant time comparison in UriSigner (stof) + * 4.3.7 (2019-11-11) * bug #34294 [Workflow] Fix error when we use ValueObject for the marking property (FabienSalles) diff --git a/CHANGELOG-4.4.md b/CHANGELOG-4.4.md index facb38ff1c67b..0f975d9e83dd5 100644 --- a/CHANGELOG-4.4.md +++ b/CHANGELOG-4.4.md @@ -7,6 +7,30 @@ in 4.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/v4.4.0...v4.4.1 +* 4.4.0-RC1 (2019-11-17) + + * bug #34419 [Cache] Disable igbinary on PHP >= 7.4 (nicolas-grekas) + * bug #34347 [Messenger] Perform no deep merging of bus middleware (vudaltsov) + * bug #34366 [HttpFoundation] Allow redirecting to URLs that contain a semicolon (JayBizzle) + * feature #34405 [HttpFoundation] Added possibility to configure expiration time in redis session handler (mantulo) + * bug #34397 [FrameworkBundle] Remove project dir from Translator cache vary scanned directories (fancyweb) + * bug #34384 [DoctrineBridge] Improve queries parameters display in Profiler (fancyweb) + * bug #34408 [Cache] catch exceptions when using PDO directly (xabbuh) + * bug #34411 [HttpKernel] Flatten "exception" controller argument if not typed (chalasr) + * bug #34410 [HttpFoundation] Fix MySQL column type definition. (jbroutier) + * bug #34403 [Cache] Redis Tag Aware warn on wrong eviction policy (andrerom) + * bug #34400 [HttpKernel] collect bundle classes, not paths (nicolas-grekas) + * bug #34398 [Config] fix id-generation for GlobResource (nicolas-grekas) + * bug #34404 [HttpClient] fix HttpClientDataCollector (nicolas-grekas) + * bug #34396 [Finder] Allow ssh2 stream wrapper for sftp (damienalexandre) + * bug #34383 [DI] Use reproducible entropy to generate env placeholders (nicolas-grekas) + * bug #34389 [WebProfilerBundle] add FrameworkBundle requirement (xabbuh) + * bug #34381 [WebProfilerBundle] Require symfony/twig-bundle (fancyweb) + * bug #34358 [Security] always check the token on non-lazy firewalls (nicolas-grekas, lyrixx) + * bug #34390 [FrameworkBundle] fix wiring of httplug client (nicolas-grekas) + * bug #34369 [FrameworkBundle] Disallow WebProfilerBundle < 4.4 (derrabus) + * bug #34370 [DI] fix detecting singly implemented interfaces (nicolas-grekas) + * 4.4.0-BETA2 (2019-11-13) * bug #34344 [Console] Constant STDOUT might be undefined (nicolas-grekas) diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md index db5cdf158a92f..fa37fba3a1211 100644 --- a/UPGRADE-4.0.md +++ b/UPGRADE-4.0.md @@ -26,8 +26,8 @@ file and directory structure of your application: Then, upgrade the contents of your console script and your front controller: -* `bin/console`: https://github.com/symfony/recipes/blob/master/symfony/console/3.3/bin/console -* `public/index.php`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/3.3/public/index.php +* `bin/console`: https://github.com/symfony/recipes/blob/master/symfony/console/4.4/bin/console +* `public/index.php`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/4.4/public/index.php Lastly, read the following article to add Symfony Flex to your application and upgrade the configuration files: https://symfony.com/doc/current/setup/flex.html diff --git a/UPGRADE-4.4.md b/UPGRADE-4.4.md index aef34c47bd61d..1d1863e4f84a2 100644 --- a/UPGRADE-4.4.md +++ b/UPGRADE-4.4.md @@ -164,6 +164,11 @@ Lock * Deprecated services `lock.store.flock`, `lock.store.semaphore`, `lock.store.memcached.abstract` and `lock.store.redis.abstract`, use `StoreFactory::createStore` instead. +Mailer +------ + + * [BC BREAK] Changed the DSN to use for disabling delivery (using the `NullTransport`) from `smtp://null` to `null://null` (host doesn't matter). + Messenger --------- diff --git a/composer.json b/composer.json index a432a159c07fe..a8d1e6b54c43d 100644 --- a/composer.json +++ b/composer.json @@ -127,6 +127,7 @@ }, "conflict": { "masterminds/html5": "<2.6", + "monolog/monolog": ">=2", "phpdocumentor/reflection-docblock": "<3.0||>=3.2.0,<3.2.2", "phpdocumentor/type-resolver": "<0.3.0", "ocramius/proxy-manager": "<2.1", diff --git a/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php b/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php index 871eeee97dfc4..b184a7163d45a 100644 --- a/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php +++ b/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php @@ -18,6 +18,8 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Component\VarDumper\Caster\Caster; +use Symfony\Component\VarDumper\Cloner\Stub; /** * DoctrineDataCollector. @@ -121,6 +123,38 @@ public function getName() return 'db'; } + /** + * {@inheritdoc} + */ + protected function getCasters() + { + return parent::getCasters() + [ + ObjectParameter::class => static function (ObjectParameter $o, array $a, Stub $s): array { + $s->class = $o->getClass(); + $s->value = $o->getObject(); + + $r = new \ReflectionClass($o->getClass()); + if ($f = $r->getFileName()) { + $s->attr['file'] = $f; + $s->attr['line'] = $r->getStartLine(); + } else { + unset($s->attr['file']); + unset($s->attr['line']); + } + + if ($error = $o->getError()) { + return [Caster::PREFIX_VIRTUAL.'⚠' => $error->getMessage()]; + } + + if ($o->isStringable()) { + return [Caster::PREFIX_VIRTUAL.'__toString()' => (string) $o->getObject()]; + } + + return [Caster::PREFIX_VIRTUAL.'⚠' => sprintf('Object of class "%s" could not be converted to string.', $o->getClass())]; + }, + ]; + } + private function sanitizeQueries(string $connectionName, array $queries): array { foreach ($queries as $i => $query) { @@ -133,6 +167,7 @@ private function sanitizeQueries(string $connectionName, array $queries): array private function sanitizeQuery(string $connectionName, array $query): array { $query['explainable'] = true; + $query['runnable'] = true; if (null === $query['params']) { $query['params'] = []; } @@ -143,6 +178,7 @@ private function sanitizeQuery(string $connectionName, array $query): array $query['types'] = []; } foreach ($query['params'] as $j => $param) { + $e = null; if (isset($query['types'][$j])) { // Transform the param according to the type $type = $query['types'][$j]; @@ -154,18 +190,19 @@ private function sanitizeQuery(string $connectionName, array $query): array try { $param = $type->convertToDatabaseValue($param, $this->registry->getConnection($connectionName)->getDatabasePlatform()); } catch (\TypeError $e) { - // Error thrown while processing params, query is not explainable. - $query['explainable'] = false; } catch (ConversionException $e) { - $query['explainable'] = false; } } } - list($query['params'][$j], $explainable) = $this->sanitizeParam($param); + list($query['params'][$j], $explainable, $runnable) = $this->sanitizeParam($param, $e); if (!$explainable) { $query['explainable'] = false; } + + if (!$runnable) { + $query['runnable'] = false; + } } $query['params'] = $this->cloneVar($query['params']); @@ -180,32 +217,33 @@ private function sanitizeQuery(string $connectionName, array $query): array * indicating if the original value was kept (allowing to use the sanitized * value to explain the query). */ - private function sanitizeParam($var): array + private function sanitizeParam($var, ?\Throwable $error): array { if (\is_object($var)) { - $className = \get_class($var); + return [$o = new ObjectParameter($var, $error), false, $o->isStringable() && !$error]; + } - return method_exists($var, '__toString') ? - [sprintf('/* Object(%s): */"%s"', $className, $var->__toString()), false] : - [sprintf('/* Object(%s) */', $className), false]; + if ($error) { + return ['⚠ '.$error->getMessage(), false, false]; } if (\is_array($var)) { $a = []; - $original = true; + $explainable = $runnable = true; foreach ($var as $k => $v) { - list($value, $orig) = $this->sanitizeParam($v); - $original = $original && $orig; + list($value, $e, $r) = $this->sanitizeParam($v, null); + $explainable = $explainable && $e; + $runnable = $runnable && $r; $a[$k] = $value; } - return [$a, $original]; + return [$a, $explainable, $runnable]; } if (\is_resource($var)) { - return [sprintf('/* Resource(%s) */', get_resource_type($var)), false]; + return [sprintf('/* Resource(%s) */', get_resource_type($var)), false, false]; } - return [$var, true]; + return [$var, true, true]; } } diff --git a/src/Symfony/Bridge/Doctrine/DataCollector/ObjectParameter.php b/src/Symfony/Bridge/Doctrine/DataCollector/ObjectParameter.php new file mode 100644 index 0000000000000..26bdb7ff267d0 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/DataCollector/ObjectParameter.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\DataCollector; + +final class ObjectParameter +{ + private $object; + private $error; + private $stringable; + private $class; + + /** + * @param object $object + */ + public function __construct($object, ?\Throwable $error) + { + $this->object = $object; + $this->error = $error; + $this->stringable = \is_callable([$object, '__toString']); + $this->class = \get_class($object); + } + + /** + * @return object + */ + public function getObject() + { + return $this->object; + } + + public function getError(): ?\Throwable + { + return $this->error; + } + + public function isStringable(): bool + { + return $this->stringable; + } + + public function getClass(): string + { + return $this->class; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php b/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php index e6547f3844132..13a6c70669eeb 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php @@ -18,6 +18,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\VarDumper\Cloner\Data; +use Symfony\Component\VarDumper\Dumper\CliDumper; class DoctrineDataCollectorTest extends TestCase { @@ -74,7 +75,7 @@ public function testCollectTime() /** * @dataProvider paramProvider */ - public function testCollectQueries($param, $types, $expected, $explainable) + public function testCollectQueries($param, $types, $expected, $explainable, bool $runnable = true) { $queries = [ ['sql' => 'SELECT * FROM table1 WHERE field1 = ?1', 'params' => [$param], 'types' => $types, 'executionMS' => 1], @@ -83,8 +84,19 @@ public function testCollectQueries($param, $types, $expected, $explainable) $c->collect(new Request(), new Response()); $collectedQueries = $c->getQueries(); - $this->assertEquals($expected, $collectedQueries['default'][0]['params'][0]); + + $collectedParam = $collectedQueries['default'][0]['params'][0]; + if ($collectedParam instanceof Data) { + $dumper = new CliDumper($out = fopen('php://memory', 'r+b')); + $dumper->setColors(false); + $collectedParam->dump($dumper); + $this->assertStringMatchesFormat($expected, print_r(stream_get_contents($out, -1, 0), true)); + } else { + $this->assertEquals($expected, $collectedParam); + } + $this->assertEquals($explainable, $collectedQueries['default'][0]['explainable']); + $this->assertSame($runnable, $collectedQueries['default'][0]['runnable']); } public function testCollectQueryWithNoParams() @@ -100,9 +112,11 @@ public function testCollectQueryWithNoParams() $this->assertInstanceOf(Data::class, $collectedQueries['default'][0]['params']); $this->assertEquals([], $collectedQueries['default'][0]['params']->getValue()); $this->assertTrue($collectedQueries['default'][0]['explainable']); + $this->assertTrue($collectedQueries['default'][0]['runnable']); $this->assertInstanceOf(Data::class, $collectedQueries['default'][1]['params']); $this->assertEquals([], $collectedQueries['default'][1]['params']->getValue()); $this->assertTrue($collectedQueries['default'][1]['explainable']); + $this->assertTrue($collectedQueries['default'][1]['runnable']); } public function testCollectQueryWithNoTypes() @@ -134,7 +148,7 @@ public function testReset() /** * @dataProvider paramProvider */ - public function testSerialization($param, $types, $expected, $explainable) + public function testSerialization($param, $types, $expected, $explainable, bool $runnable = true) { $queries = [ ['sql' => 'SELECT * FROM table1 WHERE field1 = ?1', 'params' => [$param], 'types' => $types, 'executionMS' => 1], @@ -144,8 +158,19 @@ public function testSerialization($param, $types, $expected, $explainable) $c = unserialize(serialize($c)); $collectedQueries = $c->getQueries(); - $this->assertEquals($expected, $collectedQueries['default'][0]['params'][0]); + + $collectedParam = $collectedQueries['default'][0]['params'][0]; + if ($collectedParam instanceof Data) { + $dumper = new CliDumper($out = fopen('php://memory', 'r+b')); + $dumper->setColors(false); + $collectedParam->dump($dumper); + $this->assertStringMatchesFormat($expected, print_r(stream_get_contents($out, -1, 0), true)); + } else { + $this->assertEquals($expected, $collectedParam); + } + $this->assertEquals($explainable, $collectedQueries['default'][0]['explainable']); + $this->assertSame($runnable, $collectedQueries['default'][0]['runnable']); } public function paramProvider() @@ -156,19 +181,46 @@ public function paramProvider() [true, [], true, true], [null, [], null, true], [new \DateTime('2011-09-11'), ['date'], '2011-09-11', true], - [fopen(__FILE__, 'r'), [], '/* Resource(stream) */', false], - [new \stdClass(), [], '/* Object(stdClass) */', false], + [fopen(__FILE__, 'r'), [], '/* Resource(stream) */', false, false], + [ + new \stdClass(), + [], + <<=')) { - $tests[] = ['this is not a date', ['date'], 'this is not a date', false]; - $tests[] = [new \stdClass(), ['date'], '/* Object(stdClass) */', false]; + $tests[] = ['this is not a date', ['date'], "⚠ Could not convert PHP value 'this is not a date' of type 'string' to type 'date'. Expected one of the following types: null, DateTime", false, false]; + $tests[] = [ + new \stdClass(), + ['date'], + <<.middleware` config key is not deeply merged anymore. 4.3.0 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 405aff41d7fed..c99106299a588 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -1293,6 +1293,7 @@ function ($a) { ->defaultTrue() ->end() ->arrayNode('middleware') + ->performNoDeepMerging() ->beforeNormalization() ->ifTrue(function ($v) { return \is_string($v) || (\is_array($v) && !\is_int(key($v))); }) ->then(function ($v) { return [$v]; }) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index ede13acf47315..a56a05e741241 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1234,11 +1234,18 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder $files[$locale][] = (string) $file; } + $projectDir = $container->getParameter('kernel.project_dir'); + $options = array_merge( $translator->getArgument(4), [ 'resource_files' => $files, - 'scanned_directories' => array_merge($dirs, $nonExistingDirs), + 'scanned_directories' => $scannedDirectories = array_merge($dirs, $nonExistingDirs), + 'cache_vary' => [ + 'scanned_directories' => array_map(static function (string $dir) use ($projectDir): string { + return 0 === strpos($dir, $projectDir.'/') ? substr($dir, 1 + \strlen($projectDir)) : $dir; + }, $scannedDirectories), + ], ] ); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml index 766e9f6d33d31..10256b69d5e96 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml @@ -26,8 +26,8 @@ - - + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index f79897a6badd0..a95e649c80c91 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -271,6 +271,64 @@ public function testItShowANiceMessageIfTwoMessengerBusesAreConfiguredButNoDefau ]); } + public function testBusMiddlewareDontMerge() + { + $processor = new Processor(); + $configuration = new Configuration(true); + $config = $processor->processConfiguration($configuration, [ + [ + 'messenger' => [ + 'default_bus' => 'existing_bus', + 'buses' => [ + 'existing_bus' => [ + 'middleware' => 'existing_bus.middleware', + ], + 'common_bus' => [ + 'default_middleware' => false, + 'middleware' => 'common_bus.old_middleware', + ], + ], + ], + ], + [ + 'messenger' => [ + 'buses' => [ + 'common_bus' => [ + 'middleware' => 'common_bus.new_middleware', + ], + 'new_bus' => [ + 'middleware' => 'new_bus.middleware', + ], + ], + ], + ], + ]); + + $this->assertEquals( + [ + 'existing_bus' => [ + 'default_middleware' => true, + 'middleware' => [ + ['id' => 'existing_bus.middleware', 'arguments' => []], + ], + ], + 'common_bus' => [ + 'default_middleware' => false, + 'middleware' => [ + ['id' => 'common_bus.new_middleware', 'arguments' => []], + ], + ], + 'new_bus' => [ + 'default_middleware' => true, + 'middleware' => [ + ['id' => 'new_bus.middleware', 'arguments' => []], + ], + ], + ], + $config['messenger']['buses'] + ); + } + protected static function getBundleDefaultConfig() { return [ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 860555cfc730a..8366faf918b5b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -838,6 +838,8 @@ function ($directory) { ); $this->assertNotEmpty($nonExistingDirectories, 'FrameworkBundle should pass non existing directories to Translator'); + + $this->assertSame('Fixtures/translations', $options['cache_vary']['scanned_directories'][3]); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php index 9f85b18d01ec9..52e3ef05e4f7c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php @@ -255,8 +255,10 @@ public function testCachedCatalogueIsReDumpedWhenScannedDirectoriesChange() __DIR__.'/../Fixtures/Resources/translations/messages.fr.yml', ], ], - 'scanned_directories' => [ - __DIR__.'/../Fixtures/Resources/translations/', + 'cache_vary' => [ + 'scanned_directories' => [ + '/Fixtures/Resources/translations/', + ], ], ], 'yml'); @@ -271,9 +273,11 @@ public function testCachedCatalogueIsReDumpedWhenScannedDirectoriesChange() __DIR__.'/../Fixtures/Resources/translations2/ccc.fr.yml', ], ], - 'scanned_directories' => [ - __DIR__.'/../Fixtures/Resources/translations/', - __DIR__.'/../Fixtures/Resources/translations2/', + 'cache_vary' => [ + 'scanned_directories' => [ + '/Fixtures/Resources/translations/', + '/Fixtures/Resources/translations2/', + ], ], ], 'yml'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php index 95d9ce564c14e..ec6a5febaaa74 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php +++ b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php @@ -34,6 +34,7 @@ class Translator extends BaseTranslator implements WarmableInterface 'debug' => false, 'resource_files' => [], 'scanned_directories' => [], + 'cache_vary' => [], ]; /** @@ -61,9 +62,10 @@ class Translator extends BaseTranslator implements WarmableInterface * * Available options: * - * * cache_dir: The cache directory (or null to disable caching) - * * debug: Whether to enable debugging or not (false by default) + * * cache_dir: The cache directory (or null to disable caching) + * * debug: Whether to enable debugging or not (false by default) * * resource_files: List of translation resources available grouped by locale. + * * cache_vary: An array of data that is serialized to generate the cached catalogue name. * * @throws InvalidArgumentException */ @@ -82,9 +84,7 @@ public function __construct(ContainerInterface $container, MessageFormatterInter $this->resourceFiles = $this->options['resource_files']; $this->scannedDirectories = $this->options['scanned_directories']; - parent::__construct($defaultLocale, $formatter, $this->options['cache_dir'], $this->options['debug'], [ - 'scanned_directories' => $this->scannedDirectories, - ]); + parent::__construct($defaultLocale, $formatter, $this->options['cache_dir'], $this->options['debug'], $this->options['cache_vary']); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index cb778933005df..7022e25a21b88 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -85,6 +85,7 @@ "symfony/twig-bridge": "<4.1.1", "symfony/twig-bundle": "<4.4", "symfony/validator": "<4.4", + "symfony/web-profiler-bundle": "<4.4", "symfony/workflow": "<4.3.6" }, "suggest": { diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AnonymousTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AnonymousTest.php new file mode 100644 index 0000000000000..fdee9bce9b06a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AnonymousTest.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\Bundle\SecurityBundle\Tests\Functional; + +class AnonymousTest extends AbstractWebTestCase +{ + public function testAnonymous() + { + $client = $this->createClient(['test_case' => 'Anonymous', 'root_config' => 'config.yml']); + + $client->request('GET', '/'); + + $this->assertSame(401, $client->getResponse()->getStatusCode()); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AnonymousBundle/AppCustomAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AnonymousBundle/AppCustomAuthenticator.php new file mode 100644 index 0000000000000..5069fa9cc7fa9 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AnonymousBundle/AppCustomAuthenticator.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AnonymousBundle; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; + +class AppCustomAuthenticator extends AbstractGuardAuthenticator +{ + public function supports(Request $request) + { + return false; + } + + public function getCredentials(Request $request) + { + } + + public function getUser($credentials, UserProviderInterface $userProvider) + { + } + + public function checkCredentials($credentials, UserInterface $user) + { + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + { + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + { + } + + public function start(Request $request, AuthenticationException $authException = null) + { + return new Response($authException->getMessage(), Response::HTTP_UNAUTHORIZED); + } + + public function supportsRememberMe() + { + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/bundles.php new file mode 100644 index 0000000000000..d1e9eb7e0d36a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/bundles.php @@ -0,0 +1,15 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + new Symfony\Bundle\SecurityBundle\SecurityBundle(), +]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/config.yml new file mode 100644 index 0000000000000..8ee417ab3a17d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/config.yml @@ -0,0 +1,24 @@ +framework: + secret: test + router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml" } + validation: { enabled: true, enable_annotations: true } + csrf_protection: true + form: true + test: ~ + default_locale: en + session: + storage_id: session.storage.mock_file + profiler: { only_exceptions: false } + +services: + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AnonymousBundle\AppCustomAuthenticator: ~ + +security: + firewalls: + secure: + pattern: ^/ + anonymous: false + stateless: true + guard: + authenticators: + - Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AnonymousBundle\AppCustomAuthenticator diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/routing.yml new file mode 100644 index 0000000000000..4d11154375219 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/routing.yml @@ -0,0 +1,5 @@ +main: + path: / + defaults: + _controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction + path: /app diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/config.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/config.html.twig index bfed460ab7c99..c085303213897 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/config.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/config.html.twig @@ -210,7 +210,7 @@ Name - Path + Class diff --git a/src/Symfony/Bundle/WebProfilerBundle/composer.json b/src/Symfony/Bundle/WebProfilerBundle/composer.json index 30c4e7a732bf1..11e3f5b68301e 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/composer.json +++ b/src/Symfony/Bundle/WebProfilerBundle/composer.json @@ -18,6 +18,7 @@ "require": { "php": "^7.1.3", "symfony/config": "^4.2|^5.0", + "symfony/framework-bundle": "^4.4|^5.0", "symfony/http-kernel": "^4.4", "symfony/routing": "^3.4|^4.0|^5.0", "symfony/twig-bundle": "^4.2|^5.0", diff --git a/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php index d52c08d642c6f..1dbce4b0db7d4 100644 --- a/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php @@ -14,6 +14,7 @@ use Predis\Connection\Aggregate\ClusterInterface; use Predis\Connection\Aggregate\PredisCluster; use Predis\Response\Status; +use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\Exception\InvalidArgumentException; use Symfony\Component\Cache\Marshaller\DeflateMarshaller; use Symfony\Component\Cache\Marshaller\MarshallerInterface; @@ -58,6 +59,11 @@ class RedisTagAwareAdapter extends AbstractTagAwareAdapter */ private const DEFAULT_CACHE_TTL = 8640000; + /** + * @var string|null detected eviction policy used on Redis server + */ + private $redisEvictionPolicy; + /** * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface $redisClient The redis client * @param string $namespace The default namespace @@ -87,6 +93,13 @@ public function __construct($redisClient, string $namespace = '', int $defaultLi */ protected function doSave(array $values, ?int $lifetime, array $addTagData = [], array $delTagData = []): array { + $eviction = $this->getRedisEvictionPolicy(); + if ('noeviction' !== $eviction && 0 !== strpos($eviction, 'volatile-')) { + CacheItem::log($this->logger, sprintf('Redis maxmemory-policy setting "%s" is *not* supported by RedisTagAwareAdapter, use "noeviction" or "volatile-*" eviction policies', $eviction)); + + return false; + } + // serialize values if (!$serialized = $this->marshaller->marshall($values, $failed)) { return $failed; @@ -260,4 +273,20 @@ private function renameKeys($redis, array $ids): array return $newIds; } + + private function getRedisEvictionPolicy(): string + { + if (null !== $this->redisEvictionPolicy) { + return $this->redisEvictionPolicy; + } + + foreach ($this->getHosts() as $host) { + $info = $host->info('Memory'); + $info = isset($info['Memory']) ? $info['Memory'] : $info; + + return $this->redisEvictionPolicy = $info['maxmemory_policy']; + } + + return $this->redisEvictionPolicy = ''; + } } diff --git a/src/Symfony/Component/Cache/Marshaller/DefaultMarshaller.php b/src/Symfony/Component/Cache/Marshaller/DefaultMarshaller.php index 196fd625f1fb9..8056090110c41 100644 --- a/src/Symfony/Component/Cache/Marshaller/DefaultMarshaller.php +++ b/src/Symfony/Component/Cache/Marshaller/DefaultMarshaller.php @@ -25,9 +25,9 @@ class DefaultMarshaller implements MarshallerInterface public function __construct(bool $useIgbinarySerialize = null) { if (null === $useIgbinarySerialize) { - $useIgbinarySerialize = \extension_loaded('igbinary') && \PHP_VERSION_ID !== 70400; - } elseif ($useIgbinarySerialize && (!\extension_loaded('igbinary') || \PHP_VERSION_ID === 70400)) { - throw new CacheException('The "igbinary" PHP extension is not '.(\PHP_VERSION_ID === 70400 ? 'compatible with PHP 7.4.0.' : 'loaded.')); + $useIgbinarySerialize = \extension_loaded('igbinary') && \PHP_VERSION_ID < 70400; + } elseif ($useIgbinarySerialize && (!\extension_loaded('igbinary') || \PHP_VERSION_ID >= 70400)) { + throw new CacheException('The "igbinary" PHP extension is not '.(\PHP_VERSION_ID >= 70400 ? 'compatible with PHP 7.4.' : 'loaded.')); } $this->useIgbinarySerialize = $useIgbinarySerialize; } @@ -66,7 +66,7 @@ public function unmarshall(string $value) return null; } static $igbinaryNull; - if ($value === ($igbinaryNull ?? $igbinaryNull = \extension_loaded('igbinary') && \PHP_VERSION_ID !== 70400 ? igbinary_serialize(null) : false)) { + if ($value === ($igbinaryNull ?? $igbinaryNull = \extension_loaded('igbinary') && \PHP_VERSION_ID < 70400 ? igbinary_serialize(null) : false)) { return null; } $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback'); diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/DefaultMarshallerTest.php b/src/Symfony/Component/Cache/Tests/Marshaller/DefaultMarshallerTest.php index 141291d6e448b..cc94ad15237a0 100644 --- a/src/Symfony/Component/Cache/Tests/Marshaller/DefaultMarshallerTest.php +++ b/src/Symfony/Component/Cache/Tests/Marshaller/DefaultMarshallerTest.php @@ -24,7 +24,7 @@ public function testSerialize() 'b' => function () {}, ]; - $expected = ['a' => \extension_loaded('igbinary') && \PHP_VERSION_ID !== 70400 ? igbinary_serialize(123) : serialize(123)]; + $expected = ['a' => \extension_loaded('igbinary') && \PHP_VERSION_ID < 70400 ? igbinary_serialize(123) : serialize(123)]; $this->assertSame($expected, $marshaller->marshall($values, $failed)); $this->assertSame(['b'], $failed); } @@ -43,8 +43,8 @@ public function testNativeUnserialize() */ public function testIgbinaryUnserialize() { - if (\PHP_VERSION_ID === 70400) { - $this->markTestSkipped('igbinary is not compatible with PHP 7.4.0.'); + if (\PHP_VERSION_ID >= 70400) { + $this->markTestSkipped('igbinary is not compatible with PHP 7.4.'); } $marshaller = new DefaultMarshaller(); @@ -67,8 +67,8 @@ public function testNativeUnserializeNotFoundClass() */ public function testIgbinaryUnserializeNotFoundClass() { - if (\PHP_VERSION_ID === 70400) { - $this->markTestSkipped('igbinary is not compatible with PHP 7.4.0.'); + if (\PHP_VERSION_ID >= 70400) { + $this->markTestSkipped('igbinary is not compatible with PHP 7.4.'); } $this->expectException('DomainException'); @@ -95,8 +95,8 @@ public function testNativeUnserializeInvalid() */ public function testIgbinaryUnserializeInvalid() { - if (\PHP_VERSION_ID === 70400) { - $this->markTestSkipped('igbinary is not compatible with PHP 7.4.0'); + if (\PHP_VERSION_ID >= 70400) { + $this->markTestSkipped('igbinary is not compatible with PHP 7.4.'); } $this->expectException('DomainException'); diff --git a/src/Symfony/Component/Cache/Traits/PdoTrait.php b/src/Symfony/Component/Cache/Traits/PdoTrait.php index 3f0b4fc6ac737..3ee1f8cf60331 100644 --- a/src/Symfony/Component/Cache/Traits/PdoTrait.php +++ b/src/Symfony/Component/Cache/Traits/PdoTrait.php @@ -160,6 +160,8 @@ public function prune() $delete = $this->getConnection()->prepare($deleteSql); } catch (TableNotFoundException $e) { return true; + } catch (\PDOException $e) { + return true; } $delete->bindValue(':time', time(), \PDO::PARAM_INT); @@ -170,6 +172,8 @@ public function prune() return $delete->execute(); } catch (TableNotFoundException $e) { return true; + } catch (\PDOException $e) { + return true; } } @@ -245,6 +249,7 @@ protected function doClear($namespace) try { $conn->exec($sql); } catch (TableNotFoundException $e) { + } catch (\PDOException $e) { } return true; @@ -261,6 +266,7 @@ protected function doDelete(array $ids) $stmt = $this->getConnection()->prepare($sql); $stmt->execute(array_values($ids)); } catch (TableNotFoundException $e) { + } catch (\PDOException $e) { } return true; @@ -317,6 +323,11 @@ protected function doSave(array $values, $lifetime) $this->createTable(); } $stmt = $conn->prepare($sql); + } catch (\PDOException $e) { + if (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true)) { + $this->createTable(); + } + $stmt = $conn->prepare($sql); } if ('sqlsrv' === $driver || 'oci' === $driver) { @@ -351,6 +362,11 @@ protected function doSave(array $values, $lifetime) $this->createTable(); } $stmt->execute(); + } catch (\PDOException $e) { + if (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true)) { + $this->createTable(); + } + $stmt->execute(); } if (null === $driver && !$stmt->rowCount()) { try { diff --git a/src/Symfony/Component/Config/Resource/GlobResource.php b/src/Symfony/Component/Config/Resource/GlobResource.php index 236ce3d0540eb..52c1c93f8ee43 100644 --- a/src/Symfony/Component/Config/Resource/GlobResource.php +++ b/src/Symfony/Component/Config/Resource/GlobResource.php @@ -41,6 +41,7 @@ class GlobResource implements \IteratorAggregate, SelfCheckingResourceInterface */ public function __construct(string $prefix, string $pattern, bool $recursive, bool $forExclusion = false, array $excludedPrefixes = []) { + ksort($excludedPrefixes); $this->prefix = realpath($prefix) ?: (file_exists($prefix) ? $prefix : false); $this->pattern = $pattern; $this->recursive = $recursive; @@ -62,7 +63,7 @@ public function getPrefix() */ public function __toString() { - return 'glob.'.$this->prefix.$this->pattern.(int) $this->recursive; + return 'glob.'.$this->prefix.(int) $this->recursive.$this->pattern.(int) $this->forExclusion.implode("\0", $this->excludedPrefixes); } /** diff --git a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php index 3d0a076674941..a474613ef7bb0 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php @@ -112,7 +112,7 @@ public function registerClasses(Definition $prototype, $namespace, $resource, $e continue; } foreach (class_implements($class, false) as $interface) { - $this->singlyImplemented[$interface] = isset($this->singlyImplemented[$interface]) ? false : $class; + $this->singlyImplemented[$interface] = ($this->singlyImplemented[$interface] ?? $class) !== $class ? false : $class; } } } diff --git a/src/Symfony/Component/DependencyInjection/ParameterBag/EnvPlaceholderParameterBag.php b/src/Symfony/Component/DependencyInjection/ParameterBag/EnvPlaceholderParameterBag.php index fe27324e54f11..872408690b926 100644 --- a/src/Symfony/Component/DependencyInjection/ParameterBag/EnvPlaceholderParameterBag.php +++ b/src/Symfony/Component/DependencyInjection/ParameterBag/EnvPlaceholderParameterBag.php @@ -24,6 +24,8 @@ class EnvPlaceholderParameterBag extends ParameterBag private $unusedEnvPlaceholders = []; private $providedTypes = []; + private static $counter = 0; + /** * {@inheritdoc} */ @@ -57,7 +59,7 @@ public function get($name) } } - $uniqueName = md5($name.uniqid(mt_rand(), true)); + $uniqueName = md5($name.'_'.self::$counter++); $placeholder = sprintf('%s_%s_%s', $this->getEnvPlaceholderUniquePrefix(), str_replace(':', '_', $env), $uniqueName); $this->envPlaceholders[$env][$placeholder] = $placeholder; @@ -72,7 +74,13 @@ public function get($name) */ public function getEnvPlaceholderUniquePrefix(): string { - return $this->envPlaceholderUniquePrefix ?? $this->envPlaceholderUniquePrefix = 'env_'.bin2hex(random_bytes(8)); + if (null === $this->envPlaceholderUniquePrefix) { + $reproducibleEntropy = unserialize(serialize($this->parameters)); + array_walk_recursive($reproducibleEntropy, function (&$v) { $v = null; }); + $this->envPlaceholderUniquePrefix = 'env_'.substr(md5(serialize($reproducibleEntropy)), -16); + } + + return $this->envPlaceholderUniquePrefix; } /** diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php index b9ea798d2c5cd..970cf416b3442 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php @@ -91,6 +91,8 @@ public function testRegisterClasses() $loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures')); $loader->registerClasses(new Definition(), 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\\', 'Prototype/%sub_dir%/*'); + $loader->registerClasses(new Definition(), 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\\', 'Prototype/%sub_dir%/*'); // loading twice should not be an issue + $loader->registerAliasesForSinglyImplementedInterfaces(); $this->assertEquals( ['service_container', Bar::class], @@ -119,6 +121,7 @@ public function testRegisterClassesWithExclude() // load everything, except OtherDir/AnotherSub & Foo.php 'Prototype/{%other_dir%/AnotherSub,Foo.php}' ); + $loader->registerAliasesForSinglyImplementedInterfaces(); $this->assertTrue($container->has(Bar::class)); $this->assertTrue($container->has(Baz::class)); @@ -148,6 +151,8 @@ public function testRegisterClassesWithExcludeAsArray() 'Prototype/OtherDir/AnotherSub/DeeperBaz.php', ] ); + $loader->registerAliasesForSinglyImplementedInterfaces(); + $this->assertTrue($container->has(Foo::class)); $this->assertTrue($container->has(Baz::class)); $this->assertFalse($container->has(Bar::class)); @@ -162,6 +167,7 @@ public function testNestedRegisterClasses() $prototype = new Definition(); $prototype->setPublic(true)->setPrivate(true); $loader->registerClasses($prototype, 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\\', 'Prototype/*'); + $loader->registerAliasesForSinglyImplementedInterfaces(); $this->assertTrue($container->has(Bar::class)); $this->assertTrue($container->has(Baz::class)); @@ -193,6 +199,7 @@ public function testMissingParentClass() 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\BadClasses\\', 'Prototype/%bad_classes_dir%/*' ); + $loader->registerAliasesForSinglyImplementedInterfaces(); $this->assertTrue($container->has(MissingParent::class)); @@ -211,6 +218,7 @@ public function testRegisterClassesWithBadPrefix() // the Sub is missing from namespace prefix $loader->registerClasses(new Definition(), 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\\', 'Prototype/Sub/*'); + $loader->registerAliasesForSinglyImplementedInterfaces(); } public function testRegisterClassesWithIncompatibleExclude() @@ -226,6 +234,7 @@ public function testRegisterClassesWithIncompatibleExclude() 'Prototype/*', 'yaml/*' ); + $loader->registerAliasesForSinglyImplementedInterfaces(); } } @@ -240,10 +249,4 @@ public function supports($resource, $type = null): bool { return false; } - - public function registerClasses(Definition $prototype, $namespace, $resource, $exclude = null) - { - parent::registerClasses($prototype, $namespace, $resource, $exclude); - $this->registerAliasesForSinglyImplementedInterfaces(); - } } diff --git a/src/Symfony/Component/Finder/Finder.php b/src/Symfony/Component/Finder/Finder.php index c327922f4d7ac..2a0b474dd6a90 100644 --- a/src/Symfony/Component/Finder/Finder.php +++ b/src/Symfony/Component/Finder/Finder.php @@ -799,7 +799,7 @@ private function normalizeDir(string $dir): string { $dir = rtrim($dir, '/'.\DIRECTORY_SEPARATOR); - if (preg_match('#^s?ftp://#', $dir)) { + if (preg_match('#^(ssh2\.)?s?ftp://#', $dir)) { $dir .= '/'; } diff --git a/src/Symfony/Component/Form/composer.json b/src/Symfony/Component/Form/composer.json index b6309a0012e6e..a3dbf875006ca 100644 --- a/src/Symfony/Component/Form/composer.json +++ b/src/Symfony/Component/Form/composer.json @@ -23,7 +23,7 @@ "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", "symfony/property-access": "^3.4|^4.0|^5.0", - "symfony/service-contracts": "~1.1" + "symfony/service-contracts": "^1.1|^2" }, "require-dev": { "doctrine/collections": "~1.0", diff --git a/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php b/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php index 4fdd4a671fb0f..b3b0959465cfe 100644 --- a/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php +++ b/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php @@ -135,6 +135,7 @@ private function collectOnClient(TraceableHttpClient $client): array $debugInfo = array_diff_key($info, $baseInfo); $info = array_diff_key($info, $debugInfo) + ['debug_info' => $debugInfo]; + unset($traces[$i]['info']); // break PHP reference used by TraceableHttpClient $traces[$i]['info'] = $this->cloneVar($info); $traces[$i]['options'] = $this->cloneVar($trace['options']); } diff --git a/src/Symfony/Component/HttpClient/DependencyInjection/HttpClientPass.php b/src/Symfony/Component/HttpClient/DependencyInjection/HttpClientPass.php old mode 100755 new mode 100644 diff --git a/src/Symfony/Component/HttpFoundation/RedirectResponse.php b/src/Symfony/Component/HttpFoundation/RedirectResponse.php index 5f9d072785fb6..687bc04d9b60a 100644 --- a/src/Symfony/Component/HttpFoundation/RedirectResponse.php +++ b/src/Symfony/Component/HttpFoundation/RedirectResponse.php @@ -98,7 +98,7 @@ public function setTargetUrl($url) - + Redirecting to %1$s diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php index 3c6b0d46120d6..7942e0a579b41 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php @@ -219,7 +219,7 @@ public function createTable() // - trailing space removal // - case-insensitivity // - language processing like é == e - $sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol MEDIUMINT NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8_bin, ENGINE = InnoDB"; + $sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8_bin, ENGINE = InnoDB"; break; case 'sqlite': $sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/RedisSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/RedisSessionHandler.php index 40c209341eb42..d8c1f8cb9e711 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/RedisSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/RedisSessionHandler.php @@ -30,11 +30,17 @@ class RedisSessionHandler extends AbstractSessionHandler */ private $prefix; + /** + * @var int Time to live in seconds + */ + private $ttl; + /** * List of available options: - * * prefix: The prefix to use for the keys in order to avoid collision on the Redis server. + * * prefix: The prefix to use for the keys in order to avoid collision on the Redis server + * * ttl: The time to live in seconds. * - * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy $redis + * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis * * @throws \InvalidArgumentException When unsupported client or options are passed */ @@ -51,12 +57,13 @@ public function __construct($redis, array $options = []) throw new \InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\ClientInterface, %s given', __METHOD__, \is_object($redis) ? \get_class($redis) : \gettype($redis))); } - if ($diff = array_diff(array_keys($options), ['prefix'])) { + if ($diff = array_diff(array_keys($options), ['prefix', 'ttl'])) { throw new \InvalidArgumentException(sprintf('The following options are not supported "%s"', implode(', ', $diff))); } $this->redis = $redis; $this->prefix = $options['prefix'] ?? 'sf_s'; + $this->ttl = $options['ttl'] ?? (int) ini_get('session.gc_maxlifetime'); } /** @@ -72,7 +79,7 @@ protected function doRead($sessionId): string */ protected function doWrite($sessionId, $data): bool { - $result = $this->redis->setEx($this->prefix.$sessionId, (int) ini_get('session.gc_maxlifetime'), $data); + $result = $this->redis->setEx($this->prefix.$sessionId, $this->ttl, $data); return $result && !$result instanceof ErrorInterface; } @@ -108,6 +115,6 @@ public function gc($maxlifetime): bool */ public function updateTimestamp($sessionId, $data) { - return (bool) $this->redis->expire($this->prefix.$sessionId, (int) ini_get('session.gc_maxlifetime')); + return (bool) $this->redis->expire($this->prefix.$sessionId, $this->ttl); } } diff --git a/src/Symfony/Component/HttpFoundation/Tests/RedirectResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/RedirectResponseTest.php index d7fff33cf069c..9d9797f7cfd3f 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/RedirectResponseTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/RedirectResponseTest.php @@ -20,10 +20,7 @@ public function testGenerateMetaRedirect() { $response = new RedirectResponse('foo.bar'); - $this->assertEquals(1, preg_match( - '##', - preg_replace(['/\s+/', '/\'/'], [' ', '"'], $response->getContent()) - )); + $this->assertRegExp('##', preg_replace('/\s+/', ' ', $response->getContent())); } public function testRedirectResponseConstructorEmptyUrl() diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php index 6a15a06873e25..8828be666f2dc 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php @@ -139,7 +139,37 @@ public function getOptionFixtures(): array { return [ [['prefix' => 'session'], true], + [['ttl' => 1000], true], + [['prefix' => 'sfs', 'ttl' => 1000], true], [['prefix' => 'sfs', 'foo' => 'bar'], false], + [['ttl' => 'sfs', 'foo' => 'bar'], false], + ]; + } + + /** + * @dataProvider getTtlFixtures + */ + public function testUseTtlOption(int $ttl) + { + $options = [ + 'prefix' => self::PREFIX, + 'ttl' => $ttl, + ]; + + $handler = new RedisSessionHandler($this->redisClient, $options); + $handler->write('id', 'data'); + $redisTtl = $this->redisClient->ttl(self::PREFIX.'id'); + + $this->assertLessThan($redisTtl, $ttl - 5); + $this->assertGreaterThan($redisTtl, $ttl + 5); + } + + public function getTtlFixtures(): array + { + return [ + ['ttl' => 5000], + ['ttl' => 120], + ['ttl' => 60], ]; } } diff --git a/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php index 3b63f3a99cc83..266d97626f732 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php @@ -15,7 +15,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\KernelInterface; -use Symfony\Component\VarDumper\Caster\LinkStub; +use Symfony\Component\VarDumper\Caster\ClassStub; /** * @author Fabien Potencier @@ -30,7 +30,6 @@ class ConfigDataCollector extends DataCollector implements LateDataCollectorInte private $kernel; private $name; private $version; - private $hasVarDumper; public function __construct(string $name = null, string $version = null) { @@ -43,7 +42,6 @@ public function __construct(string $name = null, string $version = null) $this->name = $name; $this->version = $version; - $this->hasVarDumper = class_exists(LinkStub::class); } /** @@ -82,7 +80,7 @@ public function collect(Request $request, Response $response/*, \Throwable $exce if (isset($this->kernel)) { foreach ($this->kernel->getBundles() as $name => $bundle) { - $this->data['bundles'][$name] = $this->hasVarDumper ? new LinkStub($bundle->getPath()) : $bundle->getPath(); + $this->data['bundles'][$name] = new ClassStub(\get_class($bundle)); } $this->data['symfony_state'] = $this->determineSymfonyState(); diff --git a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php index d8152edd22de7..77da3ce3e7a86 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php @@ -99,7 +99,7 @@ public function onControllerArguments(ControllerArgumentsEvent $event) $r = new \ReflectionFunction(\Closure::fromCallable($event->getController())); $r = $r->getParameters()[$k] ?? null; - if ($r && $r->hasType() && FlattenException::class === $r->getType()->getName()) { + if ($r && (!$r->hasType() || FlattenException::class === $r->getType()->getName())) { $arguments = $event->getArguments(); $arguments[$k] = FlattenException::createFromThrowable($e); $event->setArguments($arguments); diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 2b43c038a4d53..4b1ee5ea0aaed 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -74,12 +74,14 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private $requestStackSize = 0; private $resetServices = false; - const VERSION = '4.4.0-BETA2'; + private static $freshCache = []; + + const VERSION = '4.4.0-RC1'; const VERSION_ID = 40400; const MAJOR_VERSION = 4; const MINOR_VERSION = 4; const RELEASE_VERSION = 0; - const EXTRA_VERSION = 'BETA2'; + const EXTRA_VERSION = 'RC1'; const END_OF_MAINTENANCE = '11/2022'; const END_OF_LIFE = '11/2023'; @@ -511,7 +513,9 @@ protected function initializeContainer() $errorLevel = error_reporting(\E_ALL ^ \E_WARNING); try { - if (file_exists($cachePath) && \is_object($this->container = include $cachePath) && (!$this->debug || $cache->isFresh())) { + if (file_exists($cachePath) && \is_object($this->container = include $cachePath) + && (!$this->debug || (self::$freshCache[$k = $cachePath.'.'.$this->environment] ?? self::$freshCache[$k] = $cache->isFresh())) + ) { $this->container->set('kernel', $this); error_reporting($errorLevel); diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php index ae7149199f963..cdf6874f359b2 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php @@ -13,8 +13,8 @@ use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; -use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\ErrorHandler\Exception\FlattenException; +use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; @@ -158,12 +158,11 @@ public function testCSPHeaderIsRemoved() $this->assertFalse($dispatcher->hasListeners(KernelEvents::RESPONSE), 'CSP removal listener has been removed'); } - public function testOnControllerArguments() + /** + * @dataProvider controllerProvider + */ + public function testOnControllerArguments(callable $controller) { - $controller = function (FlattenException $exception) { - return new Response('OK: '.$exception->getMessage()); - }; - $listener = new ErrorListener($controller, $this->createMock(LoggerInterface::class), true); $kernel = $this->createMock(HttpKernelInterface::class); @@ -181,6 +180,23 @@ public function testOnControllerArguments() $this->assertSame('OK: foo', $event->getResponse()->getContent()); } + + public function controllerProvider() + { + yield [function (FlattenException $exception) { + return new Response('OK: '.$exception->getMessage()); + }]; + + yield [function ($exception) { + $this->assertInstanceOf(FlattenException::class, $exception); + + return new Response('OK: '.$exception->getMessage()); + }]; + + yield [function (\Throwable $exception) { + return new Response('OK: '.$exception->getMessage()); + }]; + } } class TestLogger extends Logger implements DebugLoggerInterface diff --git a/src/Symfony/Component/Mailer/CHANGELOG.md b/src/Symfony/Component/Mailer/CHANGELOG.md index 060d333d74824..051984f00160c 100644 --- a/src/Symfony/Component/Mailer/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 4.4.0 ----- + * [BC BREAK] changed the `NullTransport` DSN from `smtp://null` to `null://null` * [BC BREAK] renamed `SmtpEnvelope` to `Envelope`, renamed `DelayedSmtpEnvelope` to `DelayedEnvelope` * [BC BREAK] changed the syntax for failover and roundrobin DSNs diff --git a/src/Symfony/Component/Security/Http/Firewall/AccessListener.php b/src/Symfony/Component/Security/Http/Firewall/AccessListener.php index cecb18568b300..6164adde5db02 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AccessListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AccessListener.php @@ -18,6 +18,7 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException; use Symfony\Component\Security\Http\AccessMapInterface; +use Symfony\Component\Security\Http\Event\LazyResponseEvent; /** * AccessListener enforces access control rules. @@ -51,6 +52,10 @@ public function __construct(TokenStorageInterface $tokenStorage, AccessDecisionM */ public function __invoke(RequestEvent $event) { + if (!$event instanceof LazyResponseEvent && null === $token = $this->tokenStorage->getToken()) { + throw new AuthenticationCredentialsNotFoundException('A Token was not found in the TokenStorage.'); + } + $request = $event->getRequest(); list($attributes) = $this->map->getPatterns($request); @@ -59,7 +64,7 @@ public function __invoke(RequestEvent $event) return; } - if (null === $token = $this->tokenStorage->getToken()) { + if ($event instanceof LazyResponseEvent && null === $token = $this->tokenStorage->getToken()) { throw new AuthenticationCredentialsNotFoundException('A Token was not found in the TokenStorage.'); } diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php index 08515164971c3..1dff48dfda84f 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; use Symfony\Component\Security\Http\AccessMapInterface; +use Symfony\Component\Security\Http\Event\LazyResponseEvent; use Symfony\Component\Security\Http\Firewall\AccessListener; class AccessListenerTest extends TestCase @@ -219,7 +220,7 @@ public function testHandleWhenAccessMapReturnsEmptyAttributes() ->willReturn($request) ; - $listener($event); + $listener(new LazyResponseEvent($event)); } public function testHandleWhenTheSecurityTokenStorageHasNoToken()