diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services8.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services8.yml
index a17a71cc8ecf5..8c9f7a2902fc2 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services8.yml
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services8.yml
@@ -6,6 +6,17 @@ parameters:
values: [true, false, null, 0, 1000.3, 'true', 'false', 'null']
binary: !!binary 8PDw8A==
binary-control-char: !!binary VGhpcyBpcyBhIEJlbGwgY2hhciAH
+ null string: 'null'
+ string of digits: '123'
+ string of digits prefixed with minus character: '-123'
+ true string: 'true'
+ false string: 'false'
+ binary number string: '0b0110'
+ numeric string: '-1.2E2'
+ hexadecimal number string: '0xFF'
+ float string: '10100.1'
+ positive float string: '+10100.1'
+ negative float string: '-10100.1'
services:
service_container:
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_bindings.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_bindings.yml
index 0eba120b586e2..6b3b3607a9e05 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_bindings.yml
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_bindings.yml
@@ -12,6 +12,7 @@ services:
bind:
Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface: '@Symfony\Component\DependencyInjection\Tests\Fixtures\Bar'
$foo: [ ~ ]
+ iterable $baz: !tagged_iterator { tag: bar }
Symfony\Component\DependencyInjection\Tests\Fixtures\Bar:
factory: [ ~, 'create' ]
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php
index 970cf416b3442..e0e5928914e6b 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php
@@ -89,6 +89,7 @@ public function testRegisterClasses()
$container = new ContainerBuilder();
$container->setParameter('sub_dir', 'Sub');
$loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures'));
+ $loader->autoRegisterAliasesForSinglyImplementedInterfaces = false;
$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
@@ -121,7 +122,6 @@ 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));
@@ -151,7 +151,6 @@ public function testRegisterClassesWithExcludeAsArray()
'Prototype/OtherDir/AnotherSub/DeeperBaz.php',
]
);
- $loader->registerAliasesForSinglyImplementedInterfaces();
$this->assertTrue($container->has(Foo::class));
$this->assertTrue($container->has(Baz::class));
@@ -167,7 +166,6 @@ 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));
@@ -199,7 +197,6 @@ public function testMissingParentClass()
'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\BadClasses\\',
'Prototype/%bad_classes_dir%/*'
);
- $loader->registerAliasesForSinglyImplementedInterfaces();
$this->assertTrue($container->has(MissingParent::class));
@@ -218,7 +215,6 @@ 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()
@@ -234,12 +230,13 @@ public function testRegisterClassesWithIncompatibleExclude()
'Prototype/*',
'yaml/*'
);
- $loader->registerAliasesForSinglyImplementedInterfaces();
}
}
class TestFileLoader extends FileLoader
{
+ public $autoRegisterAliasesForSinglyImplementedInterfaces = true;
+
public function load($resource, $type = null)
{
return $resource;
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php
index 2c5386572a439..14ba4c7f6e567 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php
@@ -879,12 +879,14 @@ public function testBindings()
'$foo' => [null],
'$quz' => 'quz',
'$factory' => 'factory',
+ 'iterable $baz' => new TaggedIteratorArgument('bar'),
], array_map(function (BoundArgument $v) { return $v->getValues()[0]; }, $definition->getBindings()));
$this->assertEquals([
'quz',
null,
new Reference(Bar::class),
[null],
+ new TaggedIteratorArgument('bar'),
], $definition->getArguments());
$definition = $container->getDefinition(Bar::class);
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php
index 2010e36ababb5..fc827df18cd4f 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php
@@ -406,6 +406,9 @@ public function testParsesIteratorArgument()
$lazyDefinition = $container->getDefinition('lazy_context');
$this->assertEquals([new IteratorArgument(['k1' => new Reference('foo.baz'), 'k2' => new Reference('service_container')]), new IteratorArgument([])], $lazyDefinition->getArguments(), '->load() parses lazy arguments');
+
+ $message = 'The "deprecated_service" service is deprecated. You should stop using it, as it will be removed in the future.';
+ $this->assertSame($message, $container->getDefinition('deprecated_service')->getDeprecationMessage('deprecated_service'));
}
public function testAutowire()
@@ -798,12 +801,14 @@ public function testBindings()
'$foo' => [null],
'$quz' => 'quz',
'$factory' => 'factory',
+ 'iterable $baz' => new TaggedIteratorArgument('bar'),
], array_map(function (BoundArgument $v) { return $v->getValues()[0]; }, $definition->getBindings()));
$this->assertEquals([
'quz',
null,
new Reference(Bar::class),
[null],
+ new TaggedIteratorArgument('bar'),
], $definition->getArguments());
$definition = $container->getDefinition(Bar::class);
diff --git a/src/Symfony/Component/Dotenv/Dotenv.php b/src/Symfony/Component/Dotenv/Dotenv.php
index de40ade592d2e..81bb08810eaac 100644
--- a/src/Symfony/Component/Dotenv/Dotenv.php
+++ b/src/Symfony/Component/Dotenv/Dotenv.php
@@ -466,7 +466,7 @@ private function resolveVariables(string $value, array $loadedVars): string
$value = '';
}
- if ('' === $value && isset($matches['default_value'])) {
+ if ('' === $value && isset($matches['default_value']) && '' !== $matches['default_value']) {
$unsupportedChars = strpbrk($matches['default_value'], '\'"{$');
if (false !== $unsupportedChars) {
throw $this->createFormatException(sprintf('Unsupported character "%s" found in the default value of variable "$%s".', $unsupportedChars[0], $name));
diff --git a/src/Symfony/Component/Dotenv/Tests/DotenvTest.php b/src/Symfony/Component/Dotenv/Tests/DotenvTest.php
index 3f232698aebf7..d4c7c53dcd2ba 100644
--- a/src/Symfony/Component/Dotenv/Tests/DotenvTest.php
+++ b/src/Symfony/Component/Dotenv/Tests/DotenvTest.php
@@ -172,6 +172,7 @@ public function getEnvData()
["FOO=BAR\nBAR=\${NOTDEFINED:=TEST}", ['FOO' => 'BAR', 'NOTDEFINED' => 'TEST', 'BAR' => 'TEST']],
["FOO=\nBAR=\${FOO:=TEST}", ['FOO' => 'TEST', 'BAR' => 'TEST']],
["FOO=\nBAR=\$FOO:=TEST}", ['FOO' => 'TEST', 'BAR' => 'TEST}']],
+ ["FOO=foo\nFOOBAR=\${FOO}\${BAR}", ['FOO' => 'foo', 'FOOBAR' => 'foo']],
];
if ('\\' !== \DIRECTORY_SEPARATOR) {
diff --git a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php
index a09d8d57c6357..cd419098cc828 100644
--- a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php
+++ b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php
@@ -724,7 +724,11 @@ private function darwinRealpath(string $real): string
$real = self::$darwinCache[$kDir][0];
} else {
$dir = getcwd();
- chdir($real);
+
+ if (!@chdir($real)) {
+ return $real.$file;
+ }
+
$real = getcwd().'/';
chdir($dir);
diff --git a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php
index 84cb9da42413f..883a94f68968f 100644
--- a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php
+++ b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php
@@ -36,16 +36,28 @@ class HtmlErrorRenderer implements ErrorRendererInterface
private $charset;
private $fileLinkFormat;
private $projectDir;
- private $requestStack;
+ private $outputBuffer;
private $logger;
- public function __construct(bool $debug = false, string $charset = null, $fileLinkFormat = null, string $projectDir = null, RequestStack $requestStack = null, LoggerInterface $logger = null)
+ /**
+ * @param bool|callable $debug The debugging mode as a boolean or a callable that should return it
+ * @param bool|callable $outputBuffer The output buffer as a string or a callable that should return it
+ */
+ public function __construct($debug = false, string $charset = null, $fileLinkFormat = null, string $projectDir = null, $outputBuffer = '', LoggerInterface $logger = null)
{
+ if (!\is_bool($debug) && !\is_callable($debug)) {
+ throw new \TypeError(sprintf('Argument 1 passed to %s() must be a boolean or a callable, %s given.', __METHOD__, \is_object($debug) ? \get_class($debug) : \gettype($debug)));
+ }
+
+ if (!\is_string($outputBuffer) && !\is_callable($outputBuffer)) {
+ throw new \TypeError(sprintf('Argument 5 passed to %s() must be a string or a callable, %s given.', __METHOD__, \is_object($outputBuffer) ? \get_class($outputBuffer) : \gettype($outputBuffer)));
+ }
+
$this->debug = $debug;
$this->charset = $charset ?: (ini_get('default_charset') ?: 'UTF-8');
$this->fileLinkFormat = $fileLinkFormat ?: (ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'));
$this->projectDir = $projectDir;
- $this->requestStack = $requestStack;
+ $this->outputBuffer = $outputBuffer;
$this->logger = $logger;
}
@@ -57,7 +69,7 @@ public function render(\Throwable $exception): FlattenException
$exception = FlattenException::createFromThrowable($exception, null, [
'Content-Type' => 'text/html; charset='.$this->charset,
]);
-
+
return $exception->setAsString($this->renderException($exception));
}
@@ -81,12 +93,43 @@ public function getStylesheet(): string
return $this->include('assets/css/exception.css');
}
+ public static function isDebug(RequestStack $requestStack, bool $debug): \Closure
+ {
+ return static function () use ($requestStack, $debug): bool {
+ if (!$request = $requestStack->getCurrentRequest()) {
+ return $debug;
+ }
+
+ return $debug && $request->attributes->getBoolean('showException', true);
+ };
+ }
+
+ public static function getAndCleanOutputBuffer(RequestStack $requestStack): \Closure
+ {
+ return static function () use ($requestStack): string {
+ if (!$request = $requestStack->getCurrentRequest()) {
+ return '';
+ }
+
+ $startObLevel = $request->headers->get('X-Php-Ob-Level', -1);
+
+ if (ob_get_level() <= $startObLevel) {
+ return '';
+ }
+
+ Response::closeOutputBuffers($startObLevel + 1, true);
+
+ return ob_get_clean();
+ };
+ }
+
private function renderException(FlattenException $exception, string $debugTemplate = 'views/exception_full.html.php'): string
{
+ $debug = \is_bool($this->debug) ? $this->debug : ($this->debug)($exception);
$statusText = $this->escape($exception->getStatusText());
$statusCode = $this->escape($exception->getStatusCode());
- if (!$this->debug) {
+ if (!$debug) {
return $this->include('views/error.html.php', [
'statusText' => $statusText,
'statusCode' => $statusCode,
@@ -94,7 +137,6 @@ private function renderException(FlattenException $exception, string $debugTempl
}
$exceptionMessage = $this->escape($exception->getMessage());
- $request = $this->requestStack ? $this->requestStack->getCurrentRequest() : null;
return $this->include($debugTemplate, [
'exception' => $exception,
@@ -102,21 +144,10 @@ private function renderException(FlattenException $exception, string $debugTempl
'statusText' => $statusText,
'statusCode' => $statusCode,
'logger' => $this->logger instanceof DebugLoggerInterface ? $this->logger : null,
- 'currentContent' => $request ? $this->getAndCleanOutputBuffering($request->headers->get('X-Php-Ob-Level', -1)) : '',
+ 'currentContent' => \is_string($this->outputBuffer) ? $this->outputBuffer : ($this->outputBuffer)(),
]);
}
- private function getAndCleanOutputBuffering(int $startObLevel): string
- {
- if (ob_get_level() <= $startObLevel) {
- return '';
- }
-
- Response::closeOutputBuffers($startObLevel + 1, true);
-
- return ob_get_clean();
- }
-
/**
* Formats an array as a string.
*/
@@ -312,7 +343,7 @@ private function include(string $name, array $context = []): string
{
extract($context, EXTR_SKIP);
ob_start();
- include __DIR__ . '/../Resources/' .$name;
+ include __DIR__.'/../Resources/'.$name;
return trim(ob_get_clean());
}
diff --git a/src/Symfony/Component/ErrorHandler/ErrorRenderer/SerializerErrorRenderer.php b/src/Symfony/Component/ErrorHandler/ErrorRenderer/SerializerErrorRenderer.php
index c055bc3b65db3..6cc363d0d9f1e 100644
--- a/src/Symfony/Component/ErrorHandler/ErrorRenderer/SerializerErrorRenderer.php
+++ b/src/Symfony/Component/ErrorHandler/ErrorRenderer/SerializerErrorRenderer.php
@@ -26,19 +26,26 @@ class SerializerErrorRenderer implements ErrorRendererInterface
private $serializer;
private $format;
private $fallbackErrorRenderer;
+ private $debug;
/**
* @param string|callable(FlattenException) $format The format as a string or a callable that should return it
+ * @param bool|callable $debug The debugging mode as a boolean or a callable that should return it
*/
- public function __construct(SerializerInterface $serializer, $format, ErrorRendererInterface $fallbackErrorRenderer = null)
+ public function __construct(SerializerInterface $serializer, $format, ErrorRendererInterface $fallbackErrorRenderer = null, $debug = false)
{
if (!\is_string($format) && !\is_callable($format)) {
throw new \TypeError(sprintf('Argument 2 passed to %s() must be a string or a callable, %s given.', __METHOD__, \is_object($format) ? \get_class($format) : \gettype($format)));
}
+ if (!\is_bool($debug) && !\is_callable($debug)) {
+ throw new \TypeError(sprintf('Argument 4 passed to %s() must be a boolean or a callable, %s given.', __METHOD__, \is_object($debug) ? \get_class($debug) : \gettype($debug)));
+ }
+
$this->serializer = $serializer;
$this->format = $format;
$this->fallbackErrorRenderer = $fallbackErrorRenderer ?? new HtmlErrorRenderer();
+ $this->debug = $debug;
}
/**
@@ -51,7 +58,10 @@ public function render(\Throwable $exception): FlattenException
try {
$format = \is_string($this->format) ? $this->format : ($this->format)($flattenException);
- return $flattenException->setAsString($this->serializer->serialize($flattenException, $format, ['exception' => $exception]));
+ return $flattenException->setAsString($this->serializer->serialize($flattenException, $format, [
+ 'exception' => $exception,
+ 'debug' => \is_bool($this->debug) ? $this->debug : ($this->debug)($exception),
+ ]));
} catch (NotEncodableValueException $e) {
return $this->fallbackErrorRenderer->render($exception);
}
diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/HtmlErrorRendererTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/HtmlErrorRendererTest.php
index b140ca6e52f74..f292d0f79618f 100644
--- a/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/HtmlErrorRendererTest.php
+++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/HtmlErrorRendererTest.php
@@ -13,7 +13,6 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
-use Symfony\Component\ErrorHandler\Exception\FlattenException;
class HtmlErrorRendererTest extends TestCase
{
diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/SerializerErrorRendererTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/SerializerErrorRendererTest.php
index a1698e0a88cd9..77a28ecde69d6 100644
--- a/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/SerializerErrorRendererTest.php
+++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/SerializerErrorRendererTest.php
@@ -13,7 +13,6 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorHandler\ErrorRenderer\SerializerErrorRenderer;
-use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ProblemNormalizer;
use Symfony\Component\Serializer\Serializer;
diff --git a/src/Symfony/Component/EventDispatcher/EventDispatcher.php b/src/Symfony/Component/EventDispatcher/EventDispatcher.php
index 816a0ea904cb4..9a2459cdc6ca7 100644
--- a/src/Symfony/Component/EventDispatcher/EventDispatcher.php
+++ b/src/Symfony/Component/EventDispatcher/EventDispatcher.php
@@ -54,7 +54,7 @@ public function dispatch($event/*, string $eventName = null*/)
if (\is_object($event)) {
$eventName = $eventName ?? \get_class($event);
- } elseif (\is_string($event) && (null === $eventName || $eventName instanceof Event)) {
+ } elseif (\is_string($event) && (null === $eventName || $eventName instanceof ContractsEvent || $eventName instanceof Event)) {
@trigger_error(sprintf('Calling the "%s::dispatch()" method with the event name as the first argument is deprecated since Symfony 4.3, pass it as the second argument and provide the event object as the first argument instead.', EventDispatcherInterface::class), E_USER_DEPRECATED);
$swap = $event;
$event = $eventName ?? new Event();
diff --git a/src/Symfony/Component/EventDispatcher/LegacyEventDispatcherProxy.php b/src/Symfony/Component/EventDispatcher/LegacyEventDispatcherProxy.php
index cc1a4b93606a1..513e260e98106 100644
--- a/src/Symfony/Component/EventDispatcher/LegacyEventDispatcherProxy.php
+++ b/src/Symfony/Component/EventDispatcher/LegacyEventDispatcherProxy.php
@@ -59,7 +59,7 @@ public function dispatch($event/*, string $eventName = null*/)
if (\is_object($event)) {
$eventName = $eventName ?? \get_class($event);
- } elseif (\is_string($event) && (null === $eventName || $eventName instanceof Event)) {
+ } elseif (\is_string($event) && (null === $eventName || $eventName instanceof ContractsEvent || $eventName instanceof Event)) {
@trigger_error(sprintf('Calling the "%s::dispatch()" method with the event name as the first argument is deprecated since Symfony 4.3, pass it as the second argument and provide the event object as the first argument instead.', ContractsEventDispatcherInterface::class), E_USER_DEPRECATED);
$swap = $event;
$event = $eventName ?? new Event();
diff --git a/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php b/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php
index 215afcd5b82eb..7c8b5dc9b20fe 100644
--- a/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php
+++ b/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php
@@ -440,10 +440,12 @@ public function testLegacySignatureWithEvent()
$this->dispatcher->dispatch('foo', new Event());
}
+ /**
+ * @group legacy
+ * @expectedDeprecation Calling the "Symfony\Component\EventDispatcher\EventDispatcherInterface::dispatch()" method with the event name as the first argument is deprecated since Symfony 4.3, pass it as the second argument and provide the event object as the first argument instead.
+ */
public function testLegacySignatureWithNewEventObject()
{
- $this->expectException('TypeError');
- $this->expectExceptionMessage('Argument 1 passed to "Symfony\Component\EventDispatcher\EventDispatcherInterface::dispatch()" must be an object, string given.');
$this->dispatcher->dispatch('foo', new ContractsEvent());
}
}
diff --git a/src/Symfony/Component/EventDispatcher/Tests/LegacyEventDispatcherProxyTest.php b/src/Symfony/Component/EventDispatcher/Tests/LegacyEventDispatcherProxyTest.php
index 3b7721d2a37c8..f0469f9538471 100644
--- a/src/Symfony/Component/EventDispatcher/Tests/LegacyEventDispatcherProxyTest.php
+++ b/src/Symfony/Component/EventDispatcher/Tests/LegacyEventDispatcherProxyTest.php
@@ -41,10 +41,14 @@ public function testLegacySignatureWithEvent()
$this->createEventDispatcher()->dispatch('foo', new Event());
}
+ /**
+ * @group legacy
+ * @expectedDeprecation The signature of the "Symfony\Component\EventDispatcher\Tests\TestLegacyEventDispatcher::dispatch()" method should be updated to "dispatch($event, string $eventName = null)", not doing so is deprecated since Symfony 4.3.
+ * @expectedDeprecation The signature of the "Symfony\Component\EventDispatcher\Tests\TestLegacyEventDispatcher::dispatch()" method should be updated to "dispatch($event, string $eventName = null)", not doing so is deprecated since Symfony 4.3.
+ * @expectedDeprecation Calling the "Symfony\Contracts\EventDispatcher\EventDispatcherInterface::dispatch()" method with the event name as the first argument is deprecated since Symfony 4.3, pass it as the second argument and provide the event object as the first argument instead.
+ */
public function testLegacySignatureWithNewEventObject()
{
- $this->expectException('TypeError');
- $this->expectExceptionMessage('Argument 1 passed to "Symfony\Contracts\EventDispatcher\EventDispatcherInterface::dispatch()" must be an object, string given.');
$this->createEventDispatcher()->dispatch('foo', new ContractsEvent());
}
diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php
index 94e1024af504b..c85860be7f0f0 100644
--- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php
+++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php
@@ -797,7 +797,7 @@ public function testSymlink()
$file = $this->workspace.\DIRECTORY_SEPARATOR.'file';
$link = $this->workspace.\DIRECTORY_SEPARATOR.'link';
- // $file does not exists right now: creating "broken" links is a wanted feature
+ // $file does not exist right now: creating "broken" links is a wanted feature
$this->filesystem->symlink($file, $link);
$this->assertTrue(is_link($link));
diff --git a/src/Symfony/Component/Form/ButtonBuilder.php b/src/Symfony/Component/Form/ButtonBuilder.php
index b82314c0184cf..2af869c66d0d4 100644
--- a/src/Symfony/Component/Form/ButtonBuilder.php
+++ b/src/Symfony/Component/Form/ButtonBuilder.php
@@ -63,7 +63,7 @@ public function __construct(?string $name, array $options = [])
if (preg_match('/^([^a-zA-Z0-9_].*)?(.*[^a-zA-Z0-9_\-:].*)?$/D', $name, $matches)) {
if (isset($matches[1])) {
- @trigger_error(sprintf('Using names for buttons that do not start with a lowercase letter, a digit, or an underscore is deprecated since Symfony 4.3 and will throw an exception in 5.0 ("%s" given).', $name), E_USER_DEPRECATED);
+ @trigger_error(sprintf('Using names for buttons that do not start with a letter, a digit, or an underscore is deprecated since Symfony 4.3 and will throw an exception in 5.0 ("%s" given).', $name), E_USER_DEPRECATED);
}
if (isset($matches[2])) {
@trigger_error(sprintf('Using names for buttons that do not contain only letters, digits, underscores ("_"), hyphens ("-") and colons (":") ("%s" given) is deprecated since Symfony 4.3 and will throw an exception in 5.0.', $name), E_USER_DEPRECATED);
diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php
index 7638c104d465a..e401351660c1f 100644
--- a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php
+++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php
@@ -105,6 +105,14 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null,
unset($otherViews[$key]);
}
}
+
+ foreach ($preferredViewsOrder as $key => $groupViewsOrder) {
+ if ($groupViewsOrder) {
+ $preferredViewsOrder[$key] = min($groupViewsOrder);
+ } else {
+ unset($preferredViewsOrder[$key]);
+ }
+ }
} else {
// Otherwise use the original structure of the choices
self::addChoiceViewsFromStructuredValues(
@@ -249,6 +257,9 @@ private static function addChoiceViewsGroupedByCallable(callable $groupBy, $choi
$preferredViews[$groupLabel] = new ChoiceGroupView($groupLabel);
$otherViews[$groupLabel] = new ChoiceGroupView($groupLabel);
}
+ if (!isset($preferredViewsOrder[$groupLabel])) {
+ $preferredViewsOrder[$groupLabel] = [];
+ }
self::addChoiceView(
$choice,
@@ -259,7 +270,7 @@ private static function addChoiceViewsGroupedByCallable(callable $groupBy, $choi
$attr,
$isPreferred,
$preferredViews[$groupLabel]->choices,
- $preferredViewsOrder,
+ $preferredViewsOrder[$groupLabel],
$otherViews[$groupLabel]->choices
);
}
diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php
index 4472bd06c9403..7c717f441b426 100644
--- a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php
+++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php
@@ -256,6 +256,37 @@ public function testCreateViewFlatPreferredChoicesSameOrder()
);
}
+ public function testCreateViewFlatPreferredChoiceGroupsSameOrder()
+ {
+ $view = $this->factory->createView(
+ $this->list,
+ [$this->obj4, $this->obj2, $this->obj1, $this->obj3],
+ null, // label
+ null, // index
+ [$this, 'getGroup']
+ );
+
+ $preferredLabels = array_map(static function (ChoiceGroupView $groupView): array {
+ return array_map(static function (ChoiceView $view): string {
+ return $view->label;
+ }, $groupView->choices);
+ }, $view->preferredChoices);
+
+ $this->assertEquals(
+ [
+ 'Group 2' => [
+ 2 => 'C',
+ 3 => 'D',
+ ],
+ 'Group 1' => [
+ 0 => 'A',
+ 1 => 'B',
+ ],
+ ],
+ $preferredLabels
+ );
+ }
+
public function testCreateViewFlatPreferredChoicesEmptyArray()
{
$view = $this->factory->createView(
diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php
index 5e9fe4221beb7..0f872dfa97175 100644
--- a/src/Symfony/Component/HttpClient/CurlHttpClient.php
+++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php
@@ -23,6 +23,7 @@
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
+use Symfony\Contracts\Service\ResetInterface;
/**
* A performant implementation of the HttpClientInterface contracts based on the curl extension.
@@ -32,7 +33,7 @@
*
* @author Nicolas Grekas
*/
-final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
+final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
{
use HttpClientTrait;
use LoggerAwareTrait;
@@ -324,9 +325,17 @@ public function stream($responses, float $timeout = null): ResponseStreamInterfa
return new ResponseStream(CurlResponse::stream($responses, $timeout));
}
- public function __destruct()
+ public function reset()
{
+ if ($this->logger) {
+ foreach ($this->multi->pushedResponses as $url => $response) {
+ $this->logger->debug(sprintf('Unused pushed response: "%s"', $url));
+ }
+ }
+
$this->multi->pushedResponses = [];
+ $this->multi->dnsCache->evictions = $this->multi->dnsCache->evictions ?: $this->multi->dnsCache->removals;
+ $this->multi->dnsCache->removals = $this->multi->dnsCache->hostnames = [];
if (\is_resource($this->multi->handle)) {
if (\defined('CURLMOPT_PUSHFUNCTION')) {
@@ -344,6 +353,11 @@ public function __destruct()
}
}
+ public function __destruct()
+ {
+ $this->reset();
+ }
+
private static function handlePush($parent, $pushed, array $requestHeaders, CurlClientState $multi, int $maxPendingPushes, ?LoggerInterface $logger): int
{
$headers = [];
@@ -363,12 +377,6 @@ private static function handlePush($parent, $pushed, array $requestHeaders, Curl
$url = $headers[':scheme'][0].'://'.$headers[':authority'][0];
- if ($maxPendingPushes <= \count($multi->pushedResponses)) {
- $logger && $logger->debug(sprintf('Rejecting pushed response from "%s" for "%s": the queue is full', $origin, $url));
-
- return CURL_PUSH_DENY;
- }
-
// curl before 7.65 doesn't validate the pushed ":authority" header,
// but this is a MUST in the HTTP/2 RFC; let's restrict pushes to the original host,
// ignoring domains mentioned as alt-name in the certificate for now (same as curl).
@@ -378,6 +386,12 @@ private static function handlePush($parent, $pushed, array $requestHeaders, Curl
return CURL_PUSH_DENY;
}
+ if ($maxPendingPushes <= \count($multi->pushedResponses)) {
+ $fifoUrl = key($multi->pushedResponses);
+ unset($multi->pushedResponses[$fifoUrl]);
+ $logger && $logger->debug(sprintf('Evicting oldest pushed response: "%s"', $fifoUrl));
+ }
+
$url .= $headers[':path'][0];
$logger && $logger->debug(sprintf('Queueing pushed response: "%s"', $url));
diff --git a/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php b/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php
index b3b0959465cfe..96bbe695b92ea 100644
--- a/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php
+++ b/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php
@@ -15,11 +15,12 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
+use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
/**
* @author Jérémy Romey
*/
-final class HttpClientDataCollector extends DataCollector
+final class HttpClientDataCollector extends DataCollector implements LateDataCollectorInterface
{
/**
* @var TraceableHttpClient[]
@@ -38,7 +39,7 @@ public function registerClient(string $name, TraceableHttpClient $client)
*/
public function collect(Request $request, Response $response/*, \Throwable $exception = null*/)
{
- $this->initData();
+ $this->reset();
foreach ($this->clients as $name => $client) {
[$errorCount, $traces] = $this->collectOnClient($client);
@@ -53,6 +54,13 @@ public function collect(Request $request, Response $response/*, \Throwable $exce
}
}
+ public function lateCollect()
+ {
+ foreach ($this->clients as $client) {
+ $client->reset();
+ }
+ }
+
public function getClients(): array
{
return $this->data['clients'] ?? [];
@@ -68,17 +76,6 @@ public function getErrorCount(): int
return $this->data['error_count'] ?? 0;
}
- /**
- * {@inheritdoc}
- */
- public function reset()
- {
- $this->initData();
- foreach ($this->clients as $client) {
- $client->reset();
- }
- }
-
/**
* {@inheritdoc}
*/
@@ -87,7 +84,7 @@ public function getName(): string
return 'http_client';
}
- private function initData()
+ public function reset()
{
$this->data = [
'clients' => [],
diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php
index e06f9a58bab08..618858728839f 100644
--- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php
+++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php
@@ -228,15 +228,7 @@ public function __destruct()
} finally {
$this->close();
- // Clear local caches when the only remaining handles are about pushed responses
if (!$this->multi->openHandles) {
- if ($this->logger) {
- foreach ($this->multi->pushedResponses as $url => $response) {
- $this->logger->debug(sprintf('Unused pushed response: "%s"', $url));
- }
- }
-
- $this->multi->pushedResponses = [];
// Schedule DNS cache eviction for the next request
$this->multi->dnsCache->evictions = $this->multi->dnsCache->evictions ?: $this->multi->dnsCache->removals;
$this->multi->dnsCache->removals = $this->multi->dnsCache->hostnames = [];
diff --git a/src/Symfony/Component/HttpClient/Response/NativeResponse.php b/src/Symfony/Component/HttpClient/Response/NativeResponse.php
index 2e96bd411d589..2e8bce58af198 100644
--- a/src/Symfony/Component/HttpClient/Response/NativeResponse.php
+++ b/src/Symfony/Component/HttpClient/Response/NativeResponse.php
@@ -113,11 +113,18 @@ public function __destruct()
private function open(): void
{
- set_error_handler(function ($type, $msg) { throw new TransportException($msg); });
+ $url = $this->url;
+
+ set_error_handler(function ($type, $msg) use (&$url) {
+ if (E_NOTICE !== $type || 'fopen(): Content-type not specified assuming application/x-www-form-urlencoded' !== $msg) {
+ throw new TransportException($msg);
+ }
+
+ $this->logger && $this->logger->info(sprintf('%s for "%s".', $msg, $url ?? $this->url));
+ });
try {
$this->info['start_time'] = microtime(true);
- $url = $this->url;
while (true) {
$context = stream_context_get_options($this->context);
diff --git a/src/Symfony/Component/HttpClient/ScopingHttpClient.php b/src/Symfony/Component/HttpClient/ScopingHttpClient.php
index 3f071720f0573..a55d011953086 100644
--- a/src/Symfony/Component/HttpClient/ScopingHttpClient.php
+++ b/src/Symfony/Component/HttpClient/ScopingHttpClient.php
@@ -15,13 +15,14 @@
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
+use Symfony\Contracts\Service\ResetInterface;
/**
* Auto-configure the default options based on the requested URL.
*
* @author Anthony Martin
*/
-class ScopingHttpClient implements HttpClientInterface
+class ScopingHttpClient implements HttpClientInterface, ResetInterface
{
use HttpClientTrait;
@@ -90,4 +91,11 @@ public function stream($responses, float $timeout = null): ResponseStreamInterfa
{
return $this->client->stream($responses, $timeout);
}
+
+ public function reset()
+ {
+ if ($this->client instanceof ResetInterface) {
+ $this->client->reset();
+ }
+ }
}
diff --git a/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php
index 4fd367fd9d169..f83edf91b6f39 100644
--- a/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php
+++ b/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php
@@ -13,13 +13,23 @@
use Psr\Log\AbstractLogger;
use Symfony\Component\HttpClient\CurlHttpClient;
+use Symfony\Component\Process\Exception\ProcessFailedException;
+use Symfony\Component\Process\Process;
use Symfony\Contracts\HttpClient\HttpClientInterface;
+/*
+Tests for HTTP2 Push need a recent version of both PHP and curl. This docker command should run them:
+docker run -it --rm -v $(pwd):/app -v /path/to/vulcain:/usr/local/bin/vulcain -w /app php:7.3-alpine ./phpunit src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php --filter testHttp2Push
+The vulcain binary can be found at https://github.com/symfony/binary-utils/releases/download/v0.1/vulcain_0.1.3_Linux_x86_64.tar.gz - see https://github.com/dunglas/vulcain for source
+*/
+
/**
* @requires extension curl
*/
class CurlHttpClientTest extends HttpClientTestCase
{
+ private static $vulcainStarted = false;
+
protected function getHttpClient(string $testCase): HttpClientInterface
{
return new CurlHttpClient();
@@ -28,7 +38,81 @@ protected function getHttpClient(string $testCase): HttpClientInterface
/**
* @requires PHP 7.2.17
*/
- public function testHttp2Push()
+ public function testHttp2PushVulcain()
+ {
+ $client = $this->getVulcainClient();
+ $logger = new TestLogger();
+ $client->setLogger($logger);
+
+ $responseAsArray = $client->request('GET', 'https://127.0.0.1:3000/json', [
+ 'headers' => [
+ 'Preload' => '/documents/*/id',
+ ],
+ ])->toArray();
+
+ foreach ($responseAsArray['documents'] as $document) {
+ $client->request('GET', 'https://127.0.0.1:3000'.$document['id'])->toArray();
+ }
+
+ $client->reset();
+
+ $expected = [
+ 'Request: "GET https://127.0.0.1:3000/json"',
+ 'Queueing pushed response: "https://127.0.0.1:3000/json/1"',
+ 'Queueing pushed response: "https://127.0.0.1:3000/json/2"',
+ 'Queueing pushed response: "https://127.0.0.1:3000/json/3"',
+ 'Response: "200 https://127.0.0.1:3000/json"',
+ 'Accepting pushed response: "GET https://127.0.0.1:3000/json/1"',
+ 'Response: "200 https://127.0.0.1:3000/json/1"',
+ 'Accepting pushed response: "GET https://127.0.0.1:3000/json/2"',
+ 'Response: "200 https://127.0.0.1:3000/json/2"',
+ 'Accepting pushed response: "GET https://127.0.0.1:3000/json/3"',
+ 'Response: "200 https://127.0.0.1:3000/json/3"',
+ ];
+ $this->assertSame($expected, $logger->logs);
+ }
+
+ /**
+ * @requires PHP 7.2.17
+ */
+ public function testHttp2PushVulcainWithUnusedResponse()
+ {
+ $client = $this->getVulcainClient();
+ $logger = new TestLogger();
+ $client->setLogger($logger);
+
+ $responseAsArray = $client->request('GET', 'https://127.0.0.1:3000/json', [
+ 'headers' => [
+ 'Preload' => '/documents/*/id',
+ ],
+ ])->toArray();
+
+ $i = 0;
+ foreach ($responseAsArray['documents'] as $document) {
+ $client->request('GET', 'https://127.0.0.1:3000'.$document['id'])->toArray();
+ if (++$i >= 2) {
+ break;
+ }
+ }
+
+ $client->reset();
+
+ $expected = [
+ 'Request: "GET https://127.0.0.1:3000/json"',
+ 'Queueing pushed response: "https://127.0.0.1:3000/json/1"',
+ 'Queueing pushed response: "https://127.0.0.1:3000/json/2"',
+ 'Queueing pushed response: "https://127.0.0.1:3000/json/3"',
+ 'Response: "200 https://127.0.0.1:3000/json"',
+ 'Accepting pushed response: "GET https://127.0.0.1:3000/json/1"',
+ 'Response: "200 https://127.0.0.1:3000/json/1"',
+ 'Accepting pushed response: "GET https://127.0.0.1:3000/json/2"',
+ 'Response: "200 https://127.0.0.1:3000/json/2"',
+ 'Unused pushed response: "https://127.0.0.1:3000/json/3"',
+ ];
+ $this->assertSame($expected, $logger->logs);
+ }
+
+ private function getVulcainClient(): CurlHttpClient
{
if (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304) {
$this->markTestSkipped('PHP 7.3.0 to 7.3.3 don\'t support HTTP/2 PUSH');
@@ -38,32 +122,44 @@ public function testHttp2Push()
$this->markTestSkipped('curl <7.61 is used or it is not compiled with support for HTTP/2 PUSH');
}
- $logger = new class() extends AbstractLogger {
- public $logs = [];
+ $client = new CurlHttpClient(['verify_peer' => false, 'verify_host' => false]);
- public function log($level, $message, array $context = []): void
- {
- $this->logs[] = $message;
- }
- };
+ if (static::$vulcainStarted) {
+ return $client;
+ }
- $client = new CurlHttpClient([], 6, 2);
- $client->setLogger($logger);
+ if (['application/json'] !== $client->request('GET', 'http://127.0.0.1:8057/json')->getHeaders()['content-type']) {
+ $this->markTestSkipped('symfony/http-client-contracts >= 2.0.1 required');
+ }
- $index = $client->request('GET', 'https://http2.akamai.com/');
- $index->getContent();
+ $process = new Process(['vulcain'], null, [
+ 'DEBUG' => 1,
+ 'UPSTREAM' => 'http://127.0.0.1:8057',
+ 'ADDR' => ':3000',
+ 'KEY_FILE' => __DIR__.'/Fixtures/tls/server.key',
+ 'CERT_FILE' => __DIR__.'/Fixtures/tls/server.crt',
+ ]);
+ $process->start();
- $css = $client->request('GET', 'https://http2.akamai.com/resources/push.css');
+ register_shutdown_function([$process, 'stop']);
+ sleep('\\' === \DIRECTORY_SEPARATOR ? 10 : 1);
- $css->getHeaders();
+ if (!$process->isRunning()) {
+ throw new ProcessFailedException($process);
+ }
- $expected = [
- 'Request: "GET https://http2.akamai.com/"',
- 'Queueing pushed response: "https://http2.akamai.com/resources/push.css"',
- 'Response: "200 https://http2.akamai.com/"',
- 'Accepting pushed response: "GET https://http2.akamai.com/resources/push.css"',
- 'Response: "200 https://http2.akamai.com/resources/push.css"',
- ];
- $this->assertSame($expected, $logger->logs);
+ static::$vulcainStarted = true;
+
+ return $client;
+ }
+}
+
+class TestLogger extends AbstractLogger
+{
+ public $logs = [];
+
+ public function log($level, $message, array $context = []): void
+ {
+ $this->logs[] = $message;
}
}
diff --git a/src/Symfony/Component/HttpClient/Tests/Fixtures/tls/server.crt b/src/Symfony/Component/HttpClient/Tests/Fixtures/tls/server.crt
new file mode 100644
index 0000000000000..3903667223308
--- /dev/null
+++ b/src/Symfony/Component/HttpClient/Tests/Fixtures/tls/server.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDPjCCAiYCCQDpVvfmCZt2GzANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJV
+UzEUMBIGA1UEBwwLR290aGFtIENpdHkxEjAQBgNVBAMMCWxvY2FsaG9zdDEoMCYG
+CSqGSIb3DQEJARYZZHVuZ2xhcyttZXJjdXJlQGdtYWlsLmNvbTAeFw0xOTAxMjMx
+NTUzMzlaFw0yOTAxMjAxNTUzMzlaMGExCzAJBgNVBAYTAlVTMRQwEgYDVQQHDAtH
+b3RoYW0gQ2l0eTESMBAGA1UEAwwJbG9jYWxob3N0MSgwJgYJKoZIhvcNAQkBFhlk
+dW5nbGFzK21lcmN1cmVAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAuKnXkBSJwOwkKfR58wP/yLYW9QFX2THoqN8iffangRmZwc5KLE6F
+1S8jYMv3JGiJ95Ij3MezAfuBCdgPqqP8JrR1XwjR1RFZMOL/4U9R9OuMVng04PLw
+L6TzKoEtZuExHUWFP0+5AYblgno2hoN/HVuox8m6zQrBNcbhTgDIjP5Hn491d9od
+MtS3OxksDLr1UIOUGUWF7MQMN7lsN7rgT5qxoCkcAGAB4GPOA23HMt2zt4afDiI7
+lAmuv8MKkTmBCcFe+q+U7o6wMxkjGstzAWRibtwzR4ejPwdO7se23MXCWGPvF16Z
+tu1ip+e+waRus9o5UnyGaVPFAw8iCTC/KwIDAQABMA0GCSqGSIb3DQEBCwUAA4IB
+AQB42AW7E57yOky8GpsKLoa9u7okwvvg8CQJ117X8a2MElBGnmMd9tjLa/pXAx2I
+bN7jSTSadXiPNYCx4ueiJa4Dwy+C8YkwUbhRf3+mc7Chnz0SXouTjh7OUeeA06jS
+W2VAR2pKB0pdJtAkXxIy21Juu8KF5uZqVq1oimgKw2lRUIMdKaqsrVwESk6u5Ojj
+3DS40q9DzFnwKGCuZpspvMdWYLscotzLrCbnHp+guWDigEHS3CKzKbNo327nVg6X
+7UjqqtPZ2mCsnUx3QTDJsr3gcSqhzmB+Q6I/0Q2Nx/aMmbsNegu+LC3GjFtL59Bv
+B8pB/MxID0j47SwPKQghZvb3
+-----END CERTIFICATE-----
diff --git a/src/Symfony/Component/HttpClient/Tests/Fixtures/tls/server.key b/src/Symfony/Component/HttpClient/Tests/Fixtures/tls/server.key
new file mode 100644
index 0000000000000..8c278f843df24
--- /dev/null
+++ b/src/Symfony/Component/HttpClient/Tests/Fixtures/tls/server.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAuKnXkBSJwOwkKfR58wP/yLYW9QFX2THoqN8iffangRmZwc5K
+LE6F1S8jYMv3JGiJ95Ij3MezAfuBCdgPqqP8JrR1XwjR1RFZMOL/4U9R9OuMVng0
+4PLwL6TzKoEtZuExHUWFP0+5AYblgno2hoN/HVuox8m6zQrBNcbhTgDIjP5Hn491
+d9odMtS3OxksDLr1UIOUGUWF7MQMN7lsN7rgT5qxoCkcAGAB4GPOA23HMt2zt4af
+DiI7lAmuv8MKkTmBCcFe+q+U7o6wMxkjGstzAWRibtwzR4ejPwdO7se23MXCWGPv
+F16Ztu1ip+e+waRus9o5UnyGaVPFAw8iCTC/KwIDAQABAoIBAQCczVNGe7oRADMh
+EP/wM4ghhUTvHAndWrzFkFs4fJX1UKi34ZQoFTEdOZ6f1fHwj3f/qa8cDNJar5X9
+puJ+siotL3Suks2iT83dbhN63SCpiM2sqvuzu3Xp7vWwNOo5fqR2x46CmQ5uVn5S
+EbZ09/mbEza5FvmwnB49rLepxY6F8P+vK5ZnCZYS2SHpOxv3U9wG8gmcHRI9ejbC
+X9rwuu3oT23bfbJ0tn6Qh8O3R1kXZUUXqnxsn554cZZrXg5+ygbt4HfDVWMLpqy/
+5wG0FCpU8QvjF4L8qErP7TZRrWGFtti1RtACbu9LrWvO/74v54td5V28U6kqlDJR
+ff4Mi4whAoGBAOBzReQIxGwoYApPyhF+ohvF39JEEXYfkzk94t6hbgyBFBFvqdFY
+shT59im2P9LyDvTd5DnCIo52Sj7vM9H80tRjAA0A8okGOczk31ABbH8aZ2orU/0G
+EJe4PV4r3bpLO6DKTYsicgRpXI3aHHLvYFXOVNrQKfrKCQ+GFMVuhDdRAoGBANKe
+3Dn3XOq7EW42GZey1xUxrfQRJp491KXHvjYt7z7zSiUzqN+mqIqz6ngCjJWbyQsl
+Ud9N9U+4rNfYYLHQ0resjxGQRtmooOHlLhT6pEplXDgQb2SmCg2u22SKkkXA7zOV
+OFbNryXgkYThsA6ih8LiKM8aFn7zttRSEeTpfye7AoGBALhIzRyiuiuXpuswgdeF
+YrJs8A1jB/c1i5qXHlvurT2lCYYbaZHSQj0I0r2CvrqDNhaEzStDIz5XDzTHD4Qd
+EjmBo3wJyBkLPI/nZxb4ZE2jrz8znf0EasE3a2OTnrSjqqylDa/sMzM+EtkBORSB
+SFaLV45lFeKs2W2eiBVmXTZRAoGAJoA7qaz6Iz6G9SqWixB6GLm4HsFz2cFbueJF
+dwn2jf9TMnG7EQcaECDLX5y3rjGIEq2DxdouWaBcmChJpLeTjVfR31gMW4Vjw2dt
+gRBAMAlPTkBS3Ictl0q7eCmMi4u1Liy828FFnxrp/uxyjnpPbuSAqTsPma1bYnyO
+INY+FDkCgYAe9e39/vXe7Un3ysjqDUW+0OMM+kg4ulhiopzKY+QbHiSWmUUDtvcN
+asqrYiX1d59e2ZNiqrlBn86I8549St81bWSrRMNf7R+WVb79RApsABeUaEoyo3lq
+0UgOBM8Nt558kaja/YfJf/jwNC1DPuu5x5t38ZcqAkqrZ/HEPkFdGQ==
+-----END RSA PRIVATE KEY-----
diff --git a/src/Symfony/Component/HttpClient/TraceableHttpClient.php b/src/Symfony/Component/HttpClient/TraceableHttpClient.php
index 4acbc8ee42df8..d60d0849cd95e 100644
--- a/src/Symfony/Component/HttpClient/TraceableHttpClient.php
+++ b/src/Symfony/Component/HttpClient/TraceableHttpClient.php
@@ -14,11 +14,12 @@
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
+use Symfony\Contracts\Service\ResetInterface;
/**
* @author Jérémy Romey
*/
-final class TraceableHttpClient implements HttpClientInterface
+final class TraceableHttpClient implements HttpClientInterface, ResetInterface
{
private $client;
private $tracedRequests = [];
@@ -68,6 +69,10 @@ public function getTracedRequests(): array
public function reset()
{
+ if ($this->client instanceof ResetInterface) {
+ $this->client->reset();
+ }
+
$this->tracedRequests = [];
}
}
diff --git a/src/Symfony/Component/HttpClient/composer.json b/src/Symfony/Component/HttpClient/composer.json
index e734b21554df6..68169d9da90fb 100644
--- a/src/Symfony/Component/HttpClient/composer.json
+++ b/src/Symfony/Component/HttpClient/composer.json
@@ -24,7 +24,8 @@
"php": "^7.1.3",
"psr/log": "^1.0",
"symfony/http-client-contracts": "^1.1.8|^2",
- "symfony/polyfill-php73": "^1.11"
+ "symfony/polyfill-php73": "^1.11",
+ "symfony/service-contracts": "^1.0|^2"
},
"require-dev": {
"guzzlehttp/promises": "^1.3.1",
@@ -33,11 +34,7 @@
"psr/http-client": "^1.0",
"symfony/dependency-injection": "^4.3|^5.0",
"symfony/http-kernel": "^4.4",
- "symfony/process": "^4.2|^5.0",
- "symfony/service-contracts": "^1.0|^2"
- },
- "conflict": {
- "symfony/http-kernel": "<4.4"
+ "symfony/process": "^4.2|^5.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\HttpClient\\": "" },
diff --git a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php
index f02075f63021c..64800b3fa4539 100644
--- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php
+++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php
@@ -343,7 +343,7 @@ public static function trustXSendfileTypeHeader()
}
/**
- * If this is set to true, the file will be unlinked after the request is send
+ * If this is set to true, the file will be unlinked after the request is sent
* Note: If the X-Sendfile header is used, the deleteFileAfterSend setting will not be used.
*
* @param bool $shouldDelete
diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md
index e33b33ccbe4c7..3fa73a26a9074 100644
--- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md
+++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md
@@ -7,6 +7,9 @@ CHANGELOG
* passing arguments to `Request::isMethodSafe()` is deprecated.
* `ApacheRequest` is deprecated, use the `Request` class instead.
* passing a third argument to `HeaderBag::get()` is deprecated, use method `all()` instead
+ * [BC BREAK] `PdoSessionHandler` with MySQL changed the type of the lifetime column,
+ make sure to run `ALTER TABLE sessions MODIFY sess_lifetime INTEGER UNSIGNED NOT NULL` to
+ update your database.
* `PdoSessionHandler` now precalculates the expiry timestamp in the lifetime column,
make sure to run `CREATE INDEX EXPIRY ON sessions (sess_lifetime)` to update your database
to speed up garbage collection of expired sessions.
diff --git a/src/Symfony/Component/HttpFoundation/ServerBag.php b/src/Symfony/Component/HttpFoundation/ServerBag.php
index 25da35ec595e0..02c70911c19f1 100644
--- a/src/Symfony/Component/HttpFoundation/ServerBag.php
+++ b/src/Symfony/Component/HttpFoundation/ServerBag.php
@@ -43,13 +43,13 @@ public function getHeaders()
/*
* php-cgi under Apache does not pass HTTP Basic user/pass to PHP by default
* For this workaround to work, add these lines to your .htaccess file:
- * RewriteCond %{HTTP:Authorization} ^(.+)$
- * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
+ * RewriteCond %{HTTP:Authorization} .+
+ * RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
*
* A sample .htaccess file:
* RewriteEngine On
- * RewriteCond %{HTTP:Authorization} ^(.+)$
- * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
+ * RewriteCond %{HTTP:Authorization} .+
+ * RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
* RewriteCond %{REQUEST_FILENAME} !-f
* RewriteRule ^(.*)$ app.php [QSA,L]
*/
diff --git a/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php
index 59758428c693b..7ffcdab41dca4 100644
--- a/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php
+++ b/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php
@@ -93,7 +93,10 @@ public function getName()
return 'memory';
}
- private function convertToBytes(string $memoryLimit): int
+ /**
+ * @return int|float
+ */
+ private function convertToBytes(string $memoryLimit)
{
if ('-1' === $memoryLimit) {
return -1;
diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php
index c1199f639edf8..b5e46106a7422 100644
--- a/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php
+++ b/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php
@@ -43,16 +43,21 @@ public function process(ContainerBuilder $container)
foreach ($container->findTaggedServiceIds($this->tagName, true) as $id => $tags) {
$services[$id] = new Reference($id, ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE);
- $attributes = $tags[0];
- if (!isset($attributes['method'])) {
- throw new RuntimeException(sprintf('Tag %s requires the "method" attribute to be set.', $this->tagName));
- }
+ foreach ($tags as $attributes) {
+ if (!isset($attributes['method'])) {
+ throw new RuntimeException(sprintf('Tag "%s" requires the "method" attribute to be set.', $this->tagName));
+ }
+
+ if (!isset($methods[$id])) {
+ $methods[$id] = [];
+ }
- $methods[$id] = $attributes['method'];
+ $methods[$id][] = $attributes['method'];
+ }
}
- if (empty($services)) {
+ if (!$services) {
$container->removeAlias('services_resetter');
$container->removeDefinition('services_resetter');
diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php b/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php
index 734fadbd74176..d9e0028ce1227 100644
--- a/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php
+++ b/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php
@@ -35,7 +35,9 @@ public function __construct(\Traversable $resettableServices, array $resetMethod
public function reset()
{
foreach ($this->resettableServices as $id => $service) {
- $service->{$this->resetMethods[$id]}();
+ foreach ((array) $this->resetMethods[$id] as $resetMethod) {
+ $service->$resetMethod();
+ }
}
}
}
diff --git a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php
index 4d855b724e349..26c361f754e53 100644
--- a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php
+++ b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\HttpKernel\EventListener;
use Psr\Log\LoggerInterface;
+use Symfony\Component\Debug\Exception\FlattenException as LegacyFlattenException;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -98,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() || \in_array($r->getType()->getName(), [FlattenException::class, LegacyFlattenException::class], true))) {
$arguments = $event->getArguments();
$arguments[$k] = FlattenException::createFromThrowable($e);
$event->setArguments($arguments);
diff --git a/src/Symfony/Component/HttpKernel/EventListener/LocaleAwareListener.php b/src/Symfony/Component/HttpKernel/EventListener/LocaleAwareListener.php
index b93c4b0f03b46..62d03026a1c25 100644
--- a/src/Symfony/Component/HttpKernel/EventListener/LocaleAwareListener.php
+++ b/src/Symfony/Component/HttpKernel/EventListener/LocaleAwareListener.php
@@ -12,7 +12,6 @@
namespace Symfony\Component\HttpKernel\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
use Symfony\Component\HttpKernel\Event\RequestEvent;
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
index 06218e002cbd8..716dcac09bca7 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
@@ -377,7 +377,9 @@ protected function validate(Request $request, Response $entry, $catch = false)
}
// add our cached last-modified validator
- $subRequest->headers->set('if_modified_since', $entry->headers->get('Last-Modified'));
+ if ($entry->headers->has('Last-Modified')) {
+ $subRequest->headers->set('if_modified_since', $entry->headers->get('Last-Modified'));
+ }
// Add our cached etag validator to the environment.
// We keep the etags from the client to handle the case when the client
diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php
index d965080ae45e9..737e2777f936b 100644
--- a/src/Symfony/Component/HttpKernel/Kernel.php
+++ b/src/Symfony/Component/HttpKernel/Kernel.php
@@ -76,11 +76,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
private static $freshCache = [];
- const VERSION = '4.4.0';
- const VERSION_ID = 40400;
+ const VERSION = '4.4.1';
+ const VERSION_ID = 40401;
const MAJOR_VERSION = 4;
const MINOR_VERSION = 4;
- const RELEASE_VERSION = 0;
+ const RELEASE_VERSION = 1;
const EXTRA_VERSION = '';
const END_OF_MAINTENANCE = '11/2022';
@@ -514,8 +514,9 @@ protected function initializeContainer()
try {
if (file_exists($cachePath) && \is_object($this->container = include $cachePath)
- && (!$this->debug || (self::$freshCache[$k = $cachePath.'.'.$this->environment] ?? self::$freshCache[$k] = $cache->isFresh()))
+ && (!$this->debug || (self::$freshCache[$cachePath] ?? $cache->isFresh()))
) {
+ self::$freshCache[$cachePath] = true;
$this->container->set('kernel', $this);
error_reporting($errorLevel);
diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php
index a857615f1c3d3..fadd820ea66c7 100644
--- a/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php
@@ -13,7 +13,6 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
-use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ErrorController;
diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ResettableServicePassTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ResettableServicePassTest.php
index d3c6869320910..9dbc2b08a4f26 100644
--- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ResettableServicePassTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ResettableServicePassTest.php
@@ -10,6 +10,7 @@
use Symfony\Component\HttpKernel\DependencyInjection\ResettableServicePass;
use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter;
use Symfony\Component\HttpKernel\Tests\Fixtures\ClearableService;
+use Symfony\Component\HttpKernel\Tests\Fixtures\MultiResettableService;
use Symfony\Component\HttpKernel\Tests\Fixtures\ResettableService;
class ResettableServicePassTest extends TestCase
@@ -23,6 +24,10 @@ public function testCompilerPass()
$container->register('two', ClearableService::class)
->setPublic(true)
->addTag('kernel.reset', ['method' => 'clear']);
+ $container->register('three', MultiResettableService::class)
+ ->setPublic(true)
+ ->addTag('kernel.reset', ['method' => 'resetFirst'])
+ ->addTag('kernel.reset', ['method' => 'resetSecond']);
$container->register('services_resetter', ServicesResetter::class)
->setPublic(true)
@@ -38,10 +43,12 @@ public function testCompilerPass()
new IteratorArgument([
'one' => new Reference('one', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE),
'two' => new Reference('two', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE),
+ 'three' => new Reference('three', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE),
]),
[
- 'one' => 'reset',
- 'two' => 'clear',
+ 'one' => ['reset'],
+ 'two' => ['clear'],
+ 'three' => ['resetFirst', 'resetSecond'],
],
],
$definition->getArguments()
@@ -51,7 +58,7 @@ public function testCompilerPass()
public function testMissingMethod()
{
$this->expectException('Symfony\Component\DependencyInjection\Exception\RuntimeException');
- $this->expectExceptionMessage('Tag kernel.reset requires the "method" attribute to be set.');
+ $this->expectExceptionMessage('Tag "kernel.reset" requires the "method" attribute to be set.');
$container = new ContainerBuilder();
$container->register(ResettableService::class)
->addTag('kernel.reset');
diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php
index 5be6026c90a67..604d2b0d13b82 100644
--- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php
@@ -14,6 +14,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter;
use Symfony\Component\HttpKernel\Tests\Fixtures\ClearableService;
+use Symfony\Component\HttpKernel\Tests\Fixtures\MultiResettableService;
use Symfony\Component\HttpKernel\Tests\Fixtures\ResettableService;
class ServicesResetterTest extends TestCase
@@ -22,6 +23,8 @@ protected function setUp(): void
{
ResettableService::$counter = 0;
ClearableService::$counter = 0;
+ MultiResettableService::$resetFirstCounter = 0;
+ MultiResettableService::$resetSecondCounter = 0;
}
public function testResetServices()
@@ -29,14 +32,18 @@ public function testResetServices()
$resetter = new ServicesResetter(new \ArrayIterator([
'id1' => new ResettableService(),
'id2' => new ClearableService(),
+ 'id3' => new MultiResettableService(),
]), [
- 'id1' => 'reset',
- 'id2' => 'clear',
+ 'id1' => ['reset'],
+ 'id2' => ['clear'],
+ 'id3' => ['resetFirst', 'resetSecond'],
]);
$resetter->reset();
- $this->assertEquals(1, ResettableService::$counter);
- $this->assertEquals(1, ClearableService::$counter);
+ $this->assertSame(1, ResettableService::$counter);
+ $this->assertSame(1, ClearableService::$counter);
+ $this->assertSame(1, MultiResettableService::$resetFirstCounter);
+ $this->assertSame(1, MultiResettableService::$resetSecondCounter);
}
}
diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/MultiResettableService.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/MultiResettableService.php
new file mode 100644
index 0000000000000..4930fd6a30c19
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/MultiResettableService.php
@@ -0,0 +1,19 @@
+setNextResponse(200, [], 'Hello World', function ($request, $response) {
+ $this->assertFalse($request->headers->has('If-Modified-Since'));
$response->headers->set('Cache-Control', 'public');
$response->headers->set('ETag', '"12345"');
if ($response->getETag() == $request->headers->get('IF_NONE_MATCH')) {
diff --git a/src/Symfony/Component/HttpKernel/Tests/KernelTest.php b/src/Symfony/Component/HttpKernel/Tests/KernelTest.php
index 1f6811639b94c..deac82b72d5fc 100644
--- a/src/Symfony/Component/HttpKernel/Tests/KernelTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/KernelTest.php
@@ -30,7 +30,7 @@
class KernelTest extends TestCase
{
- public static function tearDownAfterClass(): void
+ protected function tearDown(): void
{
$fs = new Filesystem();
$fs->remove(__DIR__.'/Fixtures/var');
diff --git a/src/Symfony/Component/Intl/Currencies.php b/src/Symfony/Component/Intl/Currencies.php
index 7459d633cba9e..c155c8f09f425 100644
--- a/src/Symfony/Component/Intl/Currencies.php
+++ b/src/Symfony/Component/Intl/Currencies.php
@@ -46,7 +46,7 @@ public static function exists(string $currency): bool
}
/**
- * @throws MissingResourceException if the currency code does not exists
+ * @throws MissingResourceException if the currency code does not exist
*/
public static function getName(string $currency, string $displayLocale = null): string
{
@@ -78,7 +78,7 @@ public static function getNames(string $displayLocale = null): array
}
/**
- * @throws MissingResourceException if the currency code does not exists
+ * @throws MissingResourceException if the currency code does not exist
*/
public static function getSymbol(string $currency, string $displayLocale = null): string
{
@@ -115,7 +115,7 @@ public static function getNumericCode(string $currency): int
}
/**
- * @throws MissingResourceException if the numeric code does not exists
+ * @throws MissingResourceException if the numeric code does not exist
*/
public static function forNumericCode(int $numericCode): array
{
diff --git a/src/Symfony/Component/Intl/Locales.php b/src/Symfony/Component/Intl/Locales.php
index aee16beb16678..1b2d0d2adf258 100644
--- a/src/Symfony/Component/Intl/Locales.php
+++ b/src/Symfony/Component/Intl/Locales.php
@@ -49,7 +49,7 @@ public static function exists(string $locale): bool
}
/**
- * @throws MissingResourceException if the locale does not exists
+ * @throws MissingResourceException if the locale does not exist
*/
public static function getName(string $locale, string $displayLocale = null): string
{
diff --git a/src/Symfony/Component/Intl/Scripts.php b/src/Symfony/Component/Intl/Scripts.php
index 943ef8b46919f..bb29f0095bee3 100644
--- a/src/Symfony/Component/Intl/Scripts.php
+++ b/src/Symfony/Component/Intl/Scripts.php
@@ -41,7 +41,7 @@ public static function exists(string $script): bool
}
/**
- * @throws MissingResourceException if the script code does not exists
+ * @throws MissingResourceException if the script code does not exist
*/
public static function getName(string $script, string $displayLocale = null): string
{
diff --git a/src/Symfony/Component/Ldap/Ldap.php b/src/Symfony/Component/Ldap/Ldap.php
index dc7d3ae304aef..bb999bf201389 100644
--- a/src/Symfony/Component/Ldap/Ldap.php
+++ b/src/Symfony/Component/Ldap/Ldap.php
@@ -75,11 +75,7 @@ public function escape($subject, $ignore = '', $flags = 0): string
public static function create($adapter, array $config = []): self
{
if (!isset(self::$adapterMap[$adapter])) {
- throw new DriverNotFoundException(sprintf(
- 'Adapter "%s" not found. You should use one of: %s',
- $adapter,
- implode(', ', self::$adapterMap)
- ));
+ throw new DriverNotFoundException(sprintf('Adapter "%s" not found. You should use one of: %s', $adapter, implode(', ', self::$adapterMap)));
}
$class = self::$adapterMap[$adapter];
diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHttpTransport.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHttpTransport.php
index 2c90472fc6dc6..17f7a7fcf3364 100644
--- a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHttpTransport.php
+++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHttpTransport.php
@@ -60,7 +60,7 @@ protected function doSendHttp(SentMessage $message): ResponseInterface
throw new HttpTransportException(sprintf('Unable to send an email (code %s).', $result['code']), $response);
}
- $message->setMessageId($result['_id']);
+ $message->setMessageId($result[0]['_id']);
return $response;
}
diff --git a/src/Symfony/Component/Mailer/Transport/AbstractTransport.php b/src/Symfony/Component/Mailer/Transport/AbstractTransport.php
index f8dd7d4c574ef..1bc3fa12a5616 100644
--- a/src/Symfony/Component/Mailer/Transport/AbstractTransport.php
+++ b/src/Symfony/Component/Mailer/Transport/AbstractTransport.php
@@ -13,8 +13,8 @@
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
-use Symfony\Component\Mailer\Envelope;
use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy;
+use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Event\MessageEvent;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mime\Address;
diff --git a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php
index 1a40e0c5327a0..e6d107eeaf1d1 100644
--- a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php
+++ b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php
@@ -19,6 +19,7 @@
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp;
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
+use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
/**
* @group time-sensitive
@@ -94,4 +95,101 @@ public function testMultipleRedeliveryFails()
$redeliveryStamp2->getRedeliveredAt()->format('Y-m-d H:i:s')),
$tester->getDisplay(true));
}
+
+ public function testReceiverShouldBeListable()
+ {
+ $receiver = $this->createMock(ReceiverInterface::class);
+ $command = new FailedMessagesShowCommand(
+ 'failure_receiver',
+ $receiver
+ );
+
+ $this->expectExceptionMessage('The "failure_receiver" receiver does not support listing or showing specific messages.');
+
+ $tester = new CommandTester($command);
+ $tester->execute(['id' => 15]);
+ }
+
+ public function testListMessages()
+ {
+ $sentToFailureStamp = new SentToFailureTransportStamp('async');
+ $redeliveryStamp = new RedeliveryStamp(0, 'Things are bad!');
+ $envelope = new Envelope(new \stdClass(), [
+ new TransportMessageIdStamp(15),
+ $sentToFailureStamp,
+ $redeliveryStamp,
+ ]);
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+ $receiver->expects($this->once())->method('all')->with()->willReturn([$envelope]);
+
+ $command = new FailedMessagesShowCommand(
+ 'failure_receiver',
+ $receiver
+ );
+
+ $tester = new CommandTester($command);
+ $tester->execute([]);
+ $this->assertStringContainsString(sprintf(<<getRedeliveredAt()->format('Y-m-d H:i:s')),
+ $tester->getDisplay(true));
+ }
+
+ public function testListMessagesReturnsNoMessagesFound()
+ {
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+ $receiver->expects($this->once())->method('all')->with()->willReturn([]);
+
+ $command = new FailedMessagesShowCommand(
+ 'failure_receiver',
+ $receiver
+ );
+
+ $tester = new CommandTester($command);
+ $tester->execute([]);
+ $this->assertStringContainsString('[OK] No failed messages were found.', $tester->getDisplay(true));
+ }
+
+ public function testListMessagesReturnsPaginatedMessages()
+ {
+ $sentToFailureStamp = new SentToFailureTransportStamp('async');
+ $envelope = new Envelope(new \stdClass(), [
+ new TransportMessageIdStamp(15),
+ $sentToFailureStamp,
+ new RedeliveryStamp(0, 'Things are bad!'),
+ ]);
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+ $receiver->expects($this->once())->method('all')->with()->willReturn([$envelope]);
+
+ $command = new FailedMessagesShowCommand(
+ 'failure_receiver',
+ $receiver
+ );
+
+ $tester = new CommandTester($command);
+ $tester->execute(['--max' => 1]);
+ $this->assertStringContainsString('Showing first 1 messages.', $tester->getDisplay(true));
+ }
+
+ public function testInvalidMessagesThrowsException()
+ {
+ $sentToFailureStamp = new SentToFailureTransportStamp('async');
+ $envelope = new Envelope(new \stdClass(), [
+ new TransportMessageIdStamp(15),
+ $sentToFailureStamp,
+ ]);
+ $receiver = $this->createMock(ListableReceiverInterface::class);
+
+ $command = new FailedMessagesShowCommand(
+ 'failure_receiver',
+ $receiver
+ );
+
+ $this->expectExceptionMessage('The message "15" was not found.');
+
+ $tester = new CommandTester($command);
+ $tester->execute(['id' => 15]);
+ }
}
diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportFactoryTest.php b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportFactoryTest.php
index 60d5e806e357c..b3cb7a6dc8261 100644
--- a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportFactoryTest.php
+++ b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportFactoryTest.php
@@ -28,6 +28,9 @@ public function testSupportsOnlyAmqpTransports()
$this->assertFalse($factory->supports('invalid-dsn', []));
}
+ /**
+ * @requires extension amqp
+ */
public function testItCreatesTheTransport()
{
$factory = new AmqpTransportFactory();
diff --git a/src/Symfony/Component/Messenger/Tests/Transport/InMemoryTransportTest.php b/src/Symfony/Component/Messenger/Tests/Transport/InMemoryTransportTest.php
index 22149f8a391aa..6fddc3fbbc3e5 100644
--- a/src/Symfony/Component/Messenger/Tests/Transport/InMemoryTransportTest.php
+++ b/src/Symfony/Component/Messenger/Tests/Transport/InMemoryTransportTest.php
@@ -13,6 +13,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Envelope;
+use Symfony\Component\Messenger\Tests\Fixtures\AnEnvelopeStamp;
use Symfony\Component\Messenger\Transport\InMemoryTransport;
/**
@@ -50,6 +51,19 @@ public function testQueue()
$this->assertSame([], $this->transport->get());
}
+ public function testAcknowledgeSameMessageWithDifferentStamps()
+ {
+ $envelope1 = new Envelope(new \stdClass(), [new AnEnvelopeStamp()]);
+ $this->transport->send($envelope1);
+ $envelope2 = new Envelope(new \stdClass(), [new AnEnvelopeStamp()]);
+ $this->transport->send($envelope2);
+ $this->assertSame([$envelope1, $envelope2], $this->transport->get());
+ $this->transport->ack($envelope1->with(new AnEnvelopeStamp()));
+ $this->assertSame([$envelope2], $this->transport->get());
+ $this->transport->reject($envelope2->with(new AnEnvelopeStamp()));
+ $this->assertSame([], $this->transport->get());
+ }
+
public function testAck()
{
$envelope = new Envelope(new \stdClass());
diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php
index 8e37e3873f486..6d4db85b0b259 100644
--- a/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php
+++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\Messenger\Transport\AmqpExt;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
+use Symfony\Component\Messenger\Exception\LogicException;
/**
* An AMQP connection.
@@ -58,6 +59,10 @@ class Connection
public function __construct(array $connectionOptions, array $exchangeOptions, array $queuesOptions, AmqpFactory $amqpFactory = null)
{
+ if (!\extension_loaded('amqp')) {
+ throw new LogicException(sprintf('You cannot use the "%s" as the "amqp" extension is not installed.', __CLASS__));
+ }
+
$this->connectionOptions = array_replace_recursive([
'delay' => [
'exchange_name' => 'delays',
diff --git a/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php b/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php
index 4541a1095db43..50e95ef5e2893 100644
--- a/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php
+++ b/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\Messenger\Transport\Doctrine;
use Doctrine\Common\Persistence\ConnectionRegistry;
+use Symfony\Bridge\Doctrine\RegistryInterface;
use Symfony\Component\Messenger\Exception\TransportException;
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
use Symfony\Component\Messenger\Transport\TransportFactoryInterface;
@@ -24,8 +25,12 @@ class DoctrineTransportFactory implements TransportFactoryInterface
{
private $registry;
- public function __construct(ConnectionRegistry $registry)
+ public function __construct($registry)
{
+ if (!$registry instanceof RegistryInterface && !$registry instanceof ConnectionRegistry) {
+ throw new \TypeError(sprintf('Expected an instance of %s or %s, but got %s.', RegistryInterface::class, ConnectionRegistry::class, \is_object($registry) ? \get_class($registry) : \gettype($registry)));
+ }
+
$this->registry = $registry;
}
diff --git a/src/Symfony/Component/Messenger/Transport/InMemoryTransport.php b/src/Symfony/Component/Messenger/Transport/InMemoryTransport.php
index 354bb601a140a..09cbb31a041fd 100644
--- a/src/Symfony/Component/Messenger/Transport/InMemoryTransport.php
+++ b/src/Symfony/Component/Messenger/Transport/InMemoryTransport.php
@@ -55,7 +55,7 @@ public function get(): iterable
public function ack(Envelope $envelope): void
{
$this->acknowledged[] = $envelope;
- $id = spl_object_hash($envelope);
+ $id = spl_object_hash($envelope->getMessage());
unset($this->queue[$id]);
}
@@ -65,7 +65,7 @@ public function ack(Envelope $envelope): void
public function reject(Envelope $envelope): void
{
$this->rejected[] = $envelope;
- $id = spl_object_hash($envelope);
+ $id = spl_object_hash($envelope->getMessage());
unset($this->queue[$id]);
}
@@ -75,7 +75,7 @@ public function reject(Envelope $envelope): void
public function send(Envelope $envelope): Envelope
{
$this->sent[] = $envelope;
- $id = spl_object_hash($envelope);
+ $id = spl_object_hash($envelope->getMessage());
$this->queue[$id] = $envelope;
return $envelope;
diff --git a/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php b/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php
index 88aa1a316a786..6838620325668 100644
--- a/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php
+++ b/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php
@@ -56,11 +56,20 @@ public function getParts(): array
private function prepareFields(array $fields): array
{
$values = [];
- array_walk_recursive($fields, function ($item, $key) use (&$values) {
- if (!\is_array($item)) {
- $values[] = $this->preparePart($key, $item);
+
+ $prepare = function ($item, $key, $root = null) use (&$values, &$prepare) {
+ $fieldName = $root ? sprintf('%s[%s]', $root, $key) : $key;
+
+ if (\is_array($item)) {
+ array_walk($item, $prepare, $fieldName);
+
+ return;
}
- });
+
+ $values[] = $this->preparePart($fieldName, $item);
+ };
+
+ array_walk($fields, $prepare);
return $values;
}
diff --git a/src/Symfony/Component/Mime/Tests/Part/Multipart/FormDataPartTest.php b/src/Symfony/Component/Mime/Tests/Part/Multipart/FormDataPartTest.php
index 71a03e6863d8a..a74ecea316ec9 100644
--- a/src/Symfony/Component/Mime/Tests/Part/Multipart/FormDataPartTest.php
+++ b/src/Symfony/Component/Mime/Tests/Part/Multipart/FormDataPartTest.php
@@ -47,6 +47,34 @@ public function testConstructor()
$this->assertEquals([$t, $b, $c], $f->getParts());
}
+ public function testNestedArrayParts()
+ {
+ $p1 = new TextPart('content', 'utf-8', 'plain', '8bit');
+ $f = new FormDataPart([
+ 'foo' => clone $p1,
+ 'bar' => [
+ 'baz' => [
+ clone $p1,
+ 'qux' => clone $p1,
+ ],
+ ],
+ ]);
+
+ $this->assertEquals('multipart', $f->getMediaType());
+ $this->assertEquals('form-data', $f->getMediaSubtype());
+
+ $p1->setName('foo');
+ $p1->setDisposition('form-data');
+
+ $p2 = clone $p1;
+ $p2->setName('bar[baz][0]');
+
+ $p3 = clone $p1;
+ $p3->setName('bar[baz][qux]');
+
+ $this->assertEquals([$p1, $p2, $p3], $f->getParts());
+ }
+
public function testToString()
{
$p = DataPart::fromPath($file = __DIR__.'/../../Fixtures/mimetypes/test.gif');
diff --git a/src/Symfony/Component/Process/Tests/PhpExecutableFinderTest.php b/src/Symfony/Component/Process/Tests/PhpExecutableFinderTest.php
index 338bb4e0c6eb9..6c3decc53cfda 100644
--- a/src/Symfony/Component/Process/Tests/PhpExecutableFinderTest.php
+++ b/src/Symfony/Component/Process/Tests/PhpExecutableFinderTest.php
@@ -46,4 +46,16 @@ public function testFindArguments()
$this->assertEquals($f->findArguments(), [], '::findArguments() returns no arguments');
}
}
+
+ public function testNotExitsBinaryFile()
+ {
+ $f = new PhpExecutableFinder();
+ $phpBinaryEnv = PHP_BINARY;
+ putenv('PHP_BINARY=/usr/local/php/bin/php-invalid');
+
+ $this->assertFalse($f->find(), '::find() returns false because of not exist file');
+ $this->assertFalse($f->find(false), '::find(false) returns false because of not exist file');
+
+ putenv('PHP_BINARY='.$phpBinaryEnv);
+ }
}
diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
index 14be8444b2b74..c376903287544 100644
--- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
+++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
@@ -188,13 +188,14 @@ private static function throwInvalidArgumentException(string $message, array $tr
return;
}
- if (isset($trace[$i]['file']) && __FILE__ === $trace[$i]['file'] && \array_key_exists(0, $trace[$i]['args'])) {
+ if (isset($trace[$i]['file']) && __FILE__ === $trace[$i]['file']) {
$pos = strpos($message, $delim = 'must be of the type ') ?: (strpos($message, $delim = 'must be an instance of ') ?: strpos($message, $delim = 'must implement interface '));
$pos += \strlen($delim);
- $type = $trace[$i]['args'][0];
- $type = \is_object($type) ? \get_class($type) : \gettype($type);
+ $j = strpos($message, ',', $pos);
+ $type = substr($message, 2 + $j, strpos($message, ' given', $j) - $j - 2);
+ $message = substr($message, $pos, $j - $pos);
- throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given at property path "%s".', substr($message, $pos, strpos($message, ',', $pos) - $pos), $type, $propertyPath));
+ throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given at property path "%s".', $message, 'NULL' === $type ? 'null' : $type, $propertyPath));
}
}
diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php
index 1b0e47b1063c5..218f18730f162 100644
--- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php
+++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php
@@ -546,7 +546,7 @@ public function testThrowTypeError()
public function testThrowTypeErrorWithNullArgument()
{
$this->expectException('Symfony\Component\PropertyAccess\Exception\InvalidArgumentException');
- $this->expectExceptionMessage('Expected argument of type "DateTime", "NULL" given');
+ $this->expectExceptionMessage('Expected argument of type "DateTime", "null" given');
$object = new TypeHinted();
$this->propertyAccessor->setValue($object, 'date', null);
diff --git a/src/Symfony/Component/Routing/Loader/ObjectLoader.php b/src/Symfony/Component/Routing/Loader/ObjectLoader.php
index c391ad6648aa8..16dd6c9a36fb2 100644
--- a/src/Symfony/Component/Routing/Loader/ObjectLoader.php
+++ b/src/Symfony/Component/Routing/Loader/ObjectLoader.php
@@ -42,10 +42,15 @@ abstract protected function getObject(string $id);
*/
public function load($resource, $type = null)
{
- if (!preg_match('/^[^\:]+(?:::(?:[^\:]+))?$/', $resource)) {
+ if (!preg_match('/^[^\:]+(?:::?(?:[^\:]+))?$/', $resource)) {
throw new \InvalidArgumentException(sprintf('Invalid resource "%s" passed to the %s route loader: use the format "object_id::method" or "object_id" if your object class has an "__invoke" method.', $resource, \is_string($type) ? '"'.$type.'"' : 'object'));
}
+ if (1 === substr_count($resource, ':')) {
+ $resource = str_replace(':', '::', $resource);
+ @trigger_error(sprintf('Referencing object route loaders with a single colon is deprecated since Symfony 4.1. Use %s instead.', $resource), E_USER_DEPRECATED);
+ }
+
$parts = explode('::', $resource);
$method = $parts[1] ?? '__invoke';
diff --git a/src/Symfony/Component/Routing/Loader/ObjectRouteLoader.php b/src/Symfony/Component/Routing/Loader/ObjectRouteLoader.php
index 2bed560322145..39833d9ce6f93 100644
--- a/src/Symfony/Component/Routing/Loader/ObjectRouteLoader.php
+++ b/src/Symfony/Component/Routing/Loader/ObjectRouteLoader.php
@@ -11,8 +11,6 @@
namespace Symfony\Component\Routing\Loader;
-use Symfony\Component\Routing\RouteCollection;
-
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ObjectRouteLoader::class, ObjectLoader::class), E_USER_DEPRECATED);
/**
@@ -36,28 +34,6 @@ abstract class ObjectRouteLoader extends ObjectLoader
*/
abstract protected function getServiceObject($id);
- /**
- * Calls the service that will load the routes.
- *
- * @param string $resource Some value that will resolve to a callable
- * @param string|null $type The resource type
- *
- * @return RouteCollection
- */
- public function load($resource, $type = null)
- {
- if (!preg_match('/^[^\:]+(?:::?(?:[^\:]+))?$/', $resource)) {
- throw new \InvalidArgumentException(sprintf('Invalid resource "%s" passed to the "service" route loader: use the format "service::method" or "service" if your service has an "__invoke" method.', $resource));
- }
-
- if (1 === substr_count($resource, ':')) {
- $resource = str_replace(':', '::', $resource);
- @trigger_error(sprintf('Referencing service route loaders with a single colon is deprecated since Symfony 4.1. Use %s instead.', $resource), E_USER_DEPRECATED);
- }
-
- return parent::load($resource, $type);
- }
-
/**
* {@inheritdoc}
*/
diff --git a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php
index f471ce4bc9881..b44dfa1b62cfe 100644
--- a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php
+++ b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php
@@ -93,9 +93,7 @@ public function match($pathinfo)
throw new NoConfigurationException();
}
- throw 0 < \count($this->allow)
- ? new MethodNotAllowedException(array_unique($this->allow))
- : new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo));
+ throw 0 < \count($this->allow) ? new MethodNotAllowedException(array_unique($this->allow)) : new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo));
}
/**
diff --git a/src/Symfony/Component/Routing/RequestContext.php b/src/Symfony/Component/Routing/RequestContext.php
index 591dd15938695..1d68b17e39321 100644
--- a/src/Symfony/Component/Routing/RequestContext.php
+++ b/src/Symfony/Component/Routing/RequestContext.php
@@ -57,8 +57,8 @@ public function fromRequest(Request $request)
$this->setMethod($request->getMethod());
$this->setHost($request->getHost());
$this->setScheme($request->getScheme());
- $this->setHttpPort($request->isSecure() ? $this->httpPort : $request->getPort());
- $this->setHttpsPort($request->isSecure() ? $request->getPort() : $this->httpsPort);
+ $this->setHttpPort($request->isSecure() || null === $request->getPort() ? $this->httpPort : $request->getPort());
+ $this->setHttpsPort($request->isSecure() && null !== $request->getPort() ? $request->getPort() : $this->httpsPort);
$this->setQueryString($request->server->get('QUERY_STRING', ''));
return $this;
diff --git a/src/Symfony/Component/Routing/Tests/Loader/ObjectRouteLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/ObjectRouteLoaderTest.php
index cc0aa5d50711e..3436d60693730 100644
--- a/src/Symfony/Component/Routing/Tests/Loader/ObjectRouteLoaderTest.php
+++ b/src/Symfony/Component/Routing/Tests/Loader/ObjectRouteLoaderTest.php
@@ -22,7 +22,7 @@
class ObjectRouteLoaderTest extends TestCase
{
/**
- * @expectedDeprecation Referencing service route loaders with a single colon is deprecated since Symfony 4.1. Use my_route_provider_service::loadRoutes instead.
+ * @expectedDeprecation Referencing object route loaders with a single colon is deprecated since Symfony 4.1. Use my_route_provider_service::loadRoutes instead.
*/
public function testLoadCallsServiceAndReturnsCollectionWithLegacyNotation()
{
diff --git a/src/Symfony/Component/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md
index cc30a5608fffa..0877880d880c1 100644
--- a/src/Symfony/Component/Security/CHANGELOG.md
+++ b/src/Symfony/Component/Security/CHANGELOG.md
@@ -14,6 +14,7 @@ CHANGELOG
* Deprecated returning a non-boolean value when implementing `Guard\AuthenticatorInterface::checkCredentials()`.
* Deprecated passing more than one attribute to `AccessDecisionManager::decide()` and `AuthorizationChecker::isGranted()`
* Added new `argon2id` encoder, undeprecated the `bcrypt` and `argon2i` ones (using `auto` is still recommended by default.)
+ * Added `AbstractListener` which replaces the deprecated `ListenerInterface`
4.3.0
-----
diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php
index bcfa30dc585bd..83f0c5d2bc163 100644
--- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php
+++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php
@@ -21,6 +21,7 @@
use Symfony\Component\Security\Guard\AuthenticatorInterface;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken;
+use Symfony\Component\Security\Http\Firewall\AbstractListener;
use Symfony\Component\Security\Http\Firewall\LegacyListenerTrait;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
@@ -33,7 +34,7 @@
*
* @final since Symfony 4.3
*/
-class GuardAuthenticationListener implements ListenerInterface
+class GuardAuthenticationListener extends AbstractListener implements ListenerInterface
{
use LegacyListenerTrait;
@@ -62,9 +63,9 @@ public function __construct(GuardAuthenticatorHandler $guardHandler, Authenticat
}
/**
- * Iterates over each authenticator to see if each wants to authenticate the request.
+ * {@inheritdoc}
*/
- public function __invoke(RequestEvent $event)
+ public function supports(Request $request): ?bool
{
if (null !== $this->logger) {
$context = ['firewall_key' => $this->providerKey];
@@ -76,7 +77,39 @@ public function __invoke(RequestEvent $event)
$this->logger->debug('Checking for guard authentication credentials.', $context);
}
+ $guardAuthenticators = [];
+
foreach ($this->guardAuthenticators as $key => $guardAuthenticator) {
+ if (null !== $this->logger) {
+ $this->logger->debug('Checking support on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]);
+ }
+
+ if ($guardAuthenticator->supports($request)) {
+ $guardAuthenticators[$key] = $guardAuthenticator;
+ } elseif (null !== $this->logger) {
+ $this->logger->debug('Guard authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]);
+ }
+ }
+
+ if (!$guardAuthenticators) {
+ return false;
+ }
+
+ $request->attributes->set('_guard_authenticators', $guardAuthenticators);
+
+ return true;
+ }
+
+ /**
+ * Iterates over each authenticator to see if each wants to authenticate the request.
+ */
+ public function authenticate(RequestEvent $event)
+ {
+ $request = $event->getRequest();
+ $guardAuthenticators = $request->attributes->get('_guard_authenticators');
+ $request->attributes->remove('_guard_authenticators');
+
+ foreach ($guardAuthenticators as $key => $guardAuthenticator) {
// get a key that's unique to *this* guard authenticator
// this MUST be the same as GuardAuthenticationProvider
$uniqueGuardKey = $this->providerKey.'_'.$key;
@@ -97,19 +130,6 @@ private function executeGuardAuthenticator(string $uniqueGuardKey, Authenticator
{
$request = $event->getRequest();
try {
- if (null !== $this->logger) {
- $this->logger->debug('Checking support on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]);
- }
-
- // abort the execution of the authenticator if it doesn't support the request
- if (!$guardAuthenticator->supports($request)) {
- if (null !== $this->logger) {
- $this->logger->debug('Guard authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]);
- }
-
- return;
- }
-
if (null !== $this->logger) {
$this->logger->debug('Calling getCredentials() on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]);
}
diff --git a/src/Symfony/Component/Security/Guard/composer.json b/src/Symfony/Component/Security/Guard/composer.json
index af3ce94a9b2d0..09b30d11ef3c9 100644
--- a/src/Symfony/Component/Security/Guard/composer.json
+++ b/src/Symfony/Component/Security/Guard/composer.json
@@ -18,7 +18,7 @@
"require": {
"php": "^7.1.3",
"symfony/security-core": "^3.4.22|^4.2.3|^5.0",
- "symfony/security-http": "^4.3"
+ "symfony/security-http": "^4.4.1"
},
"require-dev": {
"psr/log": "~1.0"
diff --git a/src/Symfony/Component/Security/Http/Firewall.php b/src/Symfony/Component/Security/Http/Firewall.php
index 08d4873c28af8..ee769496a6918 100644
--- a/src/Symfony/Component/Security/Http/Firewall.php
+++ b/src/Symfony/Component/Security/Http/Firewall.php
@@ -138,7 +138,7 @@ protected function handleRequest(GetResponseEvent $event, $listeners)
if (\is_callable($listener)) {
$listener($event);
} else {
- @trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, implement "__invoke()" instead.', \get_class($listener)), E_USER_DEPRECATED);
+ @trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, extend "%s" instead.', \get_class($listener), AbstractListener::class), E_USER_DEPRECATED);
$listener->handle($event);
}
diff --git a/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php
index fed7785a6153a..736c247e7d924 100644
--- a/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php
@@ -49,7 +49,7 @@
* @author Fabien Potencier
* @author Johannes M. Schmitt
*/
-abstract class AbstractAuthenticationListener implements ListenerInterface
+abstract class AbstractAuthenticationListener extends AbstractListener implements ListenerInterface
{
use LegacyListenerTrait;
@@ -105,20 +105,24 @@ public function setRememberMeServices(RememberMeServicesInterface $rememberMeSer
$this->rememberMeServices = $rememberMeServices;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function supports(Request $request): ?bool
+ {
+ return $this->requiresAuthentication($request);
+ }
+
/**
* Handles form based authentication.
*
* @throws \RuntimeException
* @throws SessionUnavailableException
*/
- public function __invoke(RequestEvent $event)
+ public function authenticate(RequestEvent $event)
{
$request = $event->getRequest();
- if (!$this->requiresAuthentication($request)) {
- return;
- }
-
if (!$request->hasSession()) {
throw new \RuntimeException('This authentication method requires a session.');
}
diff --git a/src/Symfony/Component/Security/Http/Firewall/AbstractListener.php b/src/Symfony/Component/Security/Http/Firewall/AbstractListener.php
new file mode 100644
index 0000000000000..ecbfa30233eb5
--- /dev/null
+++ b/src/Symfony/Component/Security/Http/Firewall/AbstractListener.php
@@ -0,0 +1,42 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Security\Http\Firewall;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Event\RequestEvent;
+
+/**
+ * A base class for listeners that can tell whether they should authenticate incoming requests.
+ *
+ * @author Nicolas Grekas
+ */
+abstract class AbstractListener
+{
+ final public function __invoke(RequestEvent $event)
+ {
+ if (false !== $this->supports($event->getRequest())) {
+ $this->authenticate($event);
+ }
+ }
+
+ /**
+ * Tells whether the authenticate() method should be called or not depending on the incoming request.
+ *
+ * Returning null means authenticate() can be called lazily when accessing the token storage.
+ */
+ abstract public function supports(Request $request): ?bool;
+
+ /**
+ * Does whatever is required to authenticate the request, typically calling $event->setResponse() internally.
+ */
+ abstract public function authenticate(RequestEvent $event);
+}
diff --git a/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php b/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php
index 500ae43e498bd..e14dd1a95a946 100644
--- a/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php
@@ -35,7 +35,7 @@
*
* @internal since Symfony 4.3
*/
-abstract class AbstractPreAuthenticatedListener implements ListenerInterface
+abstract class AbstractPreAuthenticatedListener extends AbstractListener implements ListenerInterface
{
use LegacyListenerTrait;
@@ -56,20 +56,31 @@ public function __construct(TokenStorageInterface $tokenStorage, AuthenticationM
}
/**
- * Handles pre-authentication.
+ * {@inheritdoc}
*/
- public function __invoke(RequestEvent $event)
+ public function supports(Request $request): ?bool
{
- $request = $event->getRequest();
-
try {
- list($user, $credentials) = $this->getPreAuthenticatedData($request);
+ $request->attributes->set('_pre_authenticated_data', $this->getPreAuthenticatedData($request));
} catch (BadCredentialsException $e) {
$this->clearToken($e);
- return;
+ return false;
}
+ return true;
+ }
+
+ /**
+ * Handles pre-authentication.
+ */
+ public function authenticate(RequestEvent $event)
+ {
+ $request = $event->getRequest();
+
+ [$user, $credentials] = $request->attributes->get('_pre_authenticated_data');
+ $request->attributes->remove('_pre_authenticated_data');
+
if (null !== $this->logger) {
$this->logger->debug('Checking current security token.', ['token' => (string) $this->tokenStorage->getToken()]);
}
diff --git a/src/Symfony/Component/Security/Http/Firewall/AccessListener.php b/src/Symfony/Component/Security/Http/Firewall/AccessListener.php
index 6164adde5db02..00673f60aba2d 100644
--- a/src/Symfony/Component/Security/Http/Firewall/AccessListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/AccessListener.php
@@ -11,10 +11,12 @@
namespace Symfony\Component\Security\Http\Firewall;
+use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
+use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
use Symfony\Component\Security\Http\AccessMapInterface;
@@ -27,7 +29,7 @@
*
* @final since Symfony 4.3
*/
-class AccessListener implements ListenerInterface
+class AccessListener extends AbstractListener implements ListenerInterface
{
use LegacyListenerTrait;
@@ -44,13 +46,24 @@ public function __construct(TokenStorageInterface $tokenStorage, AccessDecisionM
$this->authManager = $authManager;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function supports(Request $request): ?bool
+ {
+ [$attributes] = $this->map->getPatterns($request);
+ $request->attributes->set('_access_control_attributes', $attributes);
+
+ return $attributes && [AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] !== $attributes ? true : null;
+ }
+
/**
* Handles access authorization.
*
* @throws AccessDeniedException
* @throws AuthenticationCredentialsNotFoundException
*/
- public function __invoke(RequestEvent $event)
+ public function authenticate(RequestEvent $event)
{
if (!$event instanceof LazyResponseEvent && null === $token = $this->tokenStorage->getToken()) {
throw new AuthenticationCredentialsNotFoundException('A Token was not found in the TokenStorage.');
@@ -58,9 +71,10 @@ public function __invoke(RequestEvent $event)
$request = $event->getRequest();
- list($attributes) = $this->map->getPatterns($request);
+ $attributes = $request->attributes->get('_access_control_attributes');
+ $request->attributes->remove('_access_control_attributes');
- if (!$attributes) {
+ if (!$attributes || ([AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] === $attributes && $event instanceof LazyResponseEvent)) {
return;
}
diff --git a/src/Symfony/Component/Security/Http/Firewall/AnonymousAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/AnonymousAuthenticationListener.php
index b7a7381bfc885..0f1da391e6dff 100644
--- a/src/Symfony/Component/Security/Http/Firewall/AnonymousAuthenticationListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/AnonymousAuthenticationListener.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\Security\Http\Firewall;
use Psr\Log\LoggerInterface;
+use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
@@ -26,7 +27,7 @@
*
* @final since Symfony 4.3
*/
-class AnonymousAuthenticationListener implements ListenerInterface
+class AnonymousAuthenticationListener extends AbstractListener implements ListenerInterface
{
use LegacyListenerTrait;
@@ -43,10 +44,18 @@ public function __construct(TokenStorageInterface $tokenStorage, string $secret,
$this->logger = $logger;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function supports(Request $request): ?bool
+ {
+ return null; // always run authenticate() lazily with lazy firewalls
+ }
+
/**
* Handles anonymous authentication.
*/
- public function __invoke(RequestEvent $event)
+ public function authenticate(RequestEvent $event)
{
if (null !== $this->tokenStorage->getToken()) {
return;
diff --git a/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php
index 9d6d81715c294..dd18e87c5b307 100644
--- a/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php
@@ -29,7 +29,7 @@
*
* @final since Symfony 4.3
*/
-class BasicAuthenticationListener implements ListenerInterface
+class BasicAuthenticationListener extends AbstractListener implements ListenerInterface
{
use LegacyListenerTrait;
@@ -55,10 +55,18 @@ public function __construct(TokenStorageInterface $tokenStorage, AuthenticationM
$this->ignoreFailure = false;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function supports(Request $request): ?bool
+ {
+ return null !== $request->headers->get('PHP_AUTH_USER');
+ }
+
/**
* Handles basic authentication.
*/
- public function __invoke(RequestEvent $event)
+ public function authenticate(RequestEvent $event)
{
$request = $event->getRequest();
diff --git a/src/Symfony/Component/Security/Http/Firewall/ChannelListener.php b/src/Symfony/Component/Security/Http/Firewall/ChannelListener.php
index 671f279fdf9a2..1033aa47ed3b4 100644
--- a/src/Symfony/Component/Security/Http/Firewall/ChannelListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/ChannelListener.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\Security\Http\Firewall;
use Psr\Log\LoggerInterface;
+use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Http\AccessMapInterface;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
@@ -24,7 +25,7 @@
*
* @final since Symfony 4.3
*/
-class ChannelListener implements ListenerInterface
+class ChannelListener extends AbstractListener implements ListenerInterface
{
use LegacyListenerTrait;
@@ -42,10 +43,8 @@ public function __construct(AccessMapInterface $map, AuthenticationEntryPointInt
/**
* Handles channel management.
*/
- public function __invoke(RequestEvent $event)
+ public function supports(Request $request): ?bool
{
- $request = $event->getRequest();
-
list(, $channel) = $this->map->getPatterns($request);
if ('https' === $channel && !$request->isSecure()) {
@@ -59,11 +58,7 @@ public function __invoke(RequestEvent $event)
}
}
- $response = $this->authenticationEntryPoint->start($request);
-
- $event->setResponse($response);
-
- return;
+ return true;
}
if ('http' === $channel && $request->isSecure()) {
@@ -71,9 +66,18 @@ public function __invoke(RequestEvent $event)
$this->logger->info('Redirecting to HTTP.');
}
- $response = $this->authenticationEntryPoint->start($request);
-
- $event->setResponse($response);
+ return true;
}
+
+ return false;
+ }
+
+ public function authenticate(RequestEvent $event)
+ {
+ $request = $event->getRequest();
+
+ $response = $this->authenticationEntryPoint->start($request);
+
+ $event->setResponse($response);
}
}
diff --git a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php
index 4015262f01b87..e9e29ab46773f 100644
--- a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php
@@ -32,6 +32,7 @@
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Event\DeauthenticatedEvent;
+use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
/**
* ContextListener manages the SecurityContext persistence through a session.
@@ -41,7 +42,7 @@
*
* @final since Symfony 4.3
*/
-class ContextListener implements ListenerInterface
+class ContextListener extends AbstractListener implements ListenerInterface
{
use LegacyListenerTrait;
@@ -52,6 +53,7 @@ class ContextListener implements ListenerInterface
private $dispatcher;
private $registered;
private $trustResolver;
+ private $rememberMeServices;
private $sessionTrackerEnabler;
/**
@@ -84,10 +86,18 @@ public function setLogoutOnUserChange($logoutOnUserChange)
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1.', __METHOD__), E_USER_DEPRECATED);
}
+ /**
+ * {@inheritdoc}
+ */
+ public function supports(Request $request): ?bool
+ {
+ return null; // always run authenticate() lazily with lazy firewalls
+ }
+
/**
* Reads the Security Token from the session.
*/
- public function __invoke(RequestEvent $event)
+ public function authenticate(RequestEvent $event)
{
if (!$this->registered && null !== $this->dispatcher && $event->isMasterRequest()) {
$this->dispatcher->addListener(KernelEvents::RESPONSE, [$this, 'onKernelResponse']);
@@ -128,6 +138,10 @@ public function __invoke(RequestEvent $event)
if ($token instanceof TokenInterface) {
$token = $this->refreshUser($token);
+
+ if (!$token && $this->rememberMeServices) {
+ $this->rememberMeServices->loginFail($request);
+ }
} elseif (null !== $token) {
if (null !== $this->logger) {
$this->logger->warning('Expected a security token from the session, got something else.', ['key' => $this->sessionKey, 'received' => $token]);
@@ -307,4 +321,9 @@ public static function handleUnserializeCallback($class)
{
throw new \ErrorException('Class not found: '.$class, 0x37313bc);
}
+
+ public function setRememberMeServices(RememberMeServicesInterface $rememberMeServices)
+ {
+ $this->rememberMeServices = $rememberMeServices;
+ }
}
diff --git a/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php b/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php
index a53aeccf4a25a..e78f21826f362 100644
--- a/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php
@@ -30,7 +30,7 @@
*
* @final since Symfony 4.3
*/
-class LogoutListener implements ListenerInterface
+class LogoutListener extends AbstractListener implements ListenerInterface
{
use LegacyListenerTrait;
@@ -63,6 +63,14 @@ public function addHandler(LogoutHandlerInterface $handler)
$this->handlers[] = $handler;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function supports(Request $request): ?bool
+ {
+ return $this->requiresLogout($request);
+ }
+
/**
* Performs the logout if requested.
*
@@ -72,14 +80,10 @@ public function addHandler(LogoutHandlerInterface $handler)
* @throws LogoutException if the CSRF token is invalid
* @throws \RuntimeException if the LogoutSuccessHandlerInterface instance does not return a response
*/
- public function __invoke(RequestEvent $event)
+ public function authenticate(RequestEvent $event)
{
$request = $event->getRequest();
- if (!$this->requiresLogout($request)) {
- return;
- }
-
if (null !== $this->csrfTokenManager) {
$csrfToken = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']);
diff --git a/src/Symfony/Component/Security/Http/Firewall/RememberMeListener.php b/src/Symfony/Component/Security/Http/Firewall/RememberMeListener.php
index ebc03db862952..0cfac54b3412d 100644
--- a/src/Symfony/Component/Security/Http/Firewall/RememberMeListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/RememberMeListener.php
@@ -13,6 +13,7 @@
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy;
+use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
@@ -31,7 +32,7 @@
*
* @final since Symfony 4.3
*/
-class RememberMeListener implements ListenerInterface
+class RememberMeListener extends AbstractListener implements ListenerInterface
{
use LegacyListenerTrait;
@@ -54,10 +55,18 @@ public function __construct(TokenStorageInterface $tokenStorage, RememberMeServi
$this->sessionStrategy = null === $sessionStrategy ? new SessionAuthenticationStrategy(SessionAuthenticationStrategy::MIGRATE) : $sessionStrategy;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function supports(Request $request): ?bool
+ {
+ return null; // always run authenticate() lazily with lazy firewalls
+ }
+
/**
* Handles remember-me cookie based authentication.
*/
- public function __invoke(RequestEvent $event)
+ public function authenticate(RequestEvent $event)
{
if (null !== $this->tokenStorage->getToken()) {
return;
diff --git a/src/Symfony/Component/Security/Http/Firewall/SimplePreAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/SimplePreAuthenticationListener.php
index 2c444e823b6fe..0641d9e45a128 100644
--- a/src/Symfony/Component/Security/Http/Firewall/SimplePreAuthenticationListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/SimplePreAuthenticationListener.php
@@ -41,7 +41,7 @@
*
* @deprecated since Symfony 4.2, use Guard instead.
*/
-class SimplePreAuthenticationListener implements ListenerInterface
+class SimplePreAuthenticationListener extends AbstractListener implements ListenerInterface
{
use LegacyListenerTrait;
@@ -79,10 +79,28 @@ public function setSessionAuthenticationStrategy(SessionAuthenticationStrategyIn
$this->sessionStrategy = $sessionStrategy;
}
+ public function supports(Request $request): ?bool
+ {
+ if ((null !== $token = $this->tokenStorage->getToken()) && !$this->trustResolver->isAnonymous($token)) {
+ return false;
+ }
+
+ $token = $this->simpleAuthenticator->createToken($request, $this->providerKey);
+
+ // allow null to be returned to skip authentication
+ if (null === $token) {
+ return false;
+ }
+
+ $request->attributes->set('_simple_pre_authenticator_token', $token);
+
+ return true;
+ }
+
/**
* Handles basic authentication.
*/
- public function __invoke(RequestEvent $event)
+ public function authenticate(RequestEvent $event)
{
$request = $event->getRequest();
@@ -91,16 +109,14 @@ public function __invoke(RequestEvent $event)
}
if ((null !== $token = $this->tokenStorage->getToken()) && !$this->trustResolver->isAnonymous($token)) {
+ $request->attributes->remove('_simple_pre_authenticator_token');
+
return;
}
try {
- $token = $this->simpleAuthenticator->createToken($request, $this->providerKey);
-
- // allow null to be returned to skip authentication
- if (null === $token) {
- return;
- }
+ $token = $request->attributes->get('_simple_pre_authenticator_token');
+ $request->attributes->remove('_simple_pre_authenticator_token');
$token = $this->authenticationManager->authenticate($token);
diff --git a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php
index 4d546285f52a2..2263623d75d1b 100644
--- a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php
@@ -39,7 +39,7 @@
*
* @final since Symfony 4.3
*/
-class SwitchUserListener implements ListenerInterface
+class SwitchUserListener extends AbstractListener implements ListenerInterface
{
use LegacyListenerTrait;
@@ -75,14 +75,10 @@ public function __construct(TokenStorageInterface $tokenStorage, UserProviderInt
}
/**
- * Handles the switch to another user.
- *
- * @throws \LogicException if switching to a user failed
+ * {@inheritdoc}
*/
- public function __invoke(RequestEvent $event)
+ public function supports(Request $request): ?bool
{
- $request = $event->getRequest();
-
// usernames can be falsy
$username = $request->get($this->usernameParameter);
@@ -92,9 +88,26 @@ public function __invoke(RequestEvent $event)
// if it's still "empty", nothing to do.
if (null === $username || '' === $username) {
- return;
+ return false;
}
+ $request->attributes->set('_switch_user_username', $username);
+
+ return true;
+ }
+
+ /**
+ * Handles the switch to another user.
+ *
+ * @throws \LogicException if switching to a user failed
+ */
+ public function authenticate(RequestEvent $event)
+ {
+ $request = $event->getRequest();
+
+ $username = $request->attributes->get('_switch_user_username');
+ $request->attributes->remove('_switch_user_username');
+
if (null === $this->tokenStorage->getToken()) {
throw new AuthenticationCredentialsNotFoundException('Could not find original Token object.');
}
@@ -148,7 +161,6 @@ private function attemptSwitchUser(Request $request, string $username): ?TokenIn
try {
$this->provider->loadUserByUsername($nonExistentUsername);
- throw new \LogicException('AuthenticationException expected');
} catch (AuthenticationException $e) {
}
} catch (AuthenticationException $e) {
diff --git a/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php
index 851e160bebbef..50eb405c6120d 100644
--- a/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php
@@ -44,7 +44,7 @@
*
* @final since Symfony 4.3
*/
-class UsernamePasswordJsonAuthenticationListener implements ListenerInterface
+class UsernamePasswordJsonAuthenticationListener extends AbstractListener implements ListenerInterface
{
use LegacyListenerTrait;
@@ -74,22 +74,27 @@ public function __construct(TokenStorageInterface $tokenStorage, AuthenticationM
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}
- /**
- * {@inheritdoc}
- */
- public function __invoke(RequestEvent $event)
+ public function supports(Request $request): ?bool
{
- $request = $event->getRequest();
if (false === strpos($request->getRequestFormat(), 'json')
&& false === strpos($request->getContentType(), 'json')
) {
- return;
+ return false;
}
if (isset($this->options['check_path']) && !$this->httpUtils->checkRequestPath($request, $this->options['check_path'])) {
- return;
+ return false;
}
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function authenticate(RequestEvent $event)
+ {
+ $request = $event->getRequest();
$data = json_decode($request->getContent());
try {
diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php
index 1dff48dfda84f..168e25643705b 100644
--- a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php
@@ -14,6 +14,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
@@ -26,7 +27,7 @@ class AccessListenerTest extends TestCase
public function testHandleWhenTheAccessDecisionManagerDecidesToRefuseAccess()
{
$this->expectException('Symfony\Component\Security\Core\Exception\AccessDeniedException');
- $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->disableOriginalConstructor()->disableOriginalClone()->getMock();
+ $request = new Request();
$accessMap = $this->getMockBuilder('Symfony\Component\Security\Http\AccessMapInterface')->getMock();
$accessMap
@@ -65,19 +66,12 @@ public function testHandleWhenTheAccessDecisionManagerDecidesToRefuseAccess()
$this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface')->getMock()
);
- $event = $this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock();
- $event
- ->expects($this->any())
- ->method('getRequest')
- ->willReturn($request)
- ;
-
- $listener($event);
+ $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST));
}
public function testHandleWhenTheTokenIsNotAuthenticated()
{
- $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->disableOriginalConstructor()->disableOriginalClone()->getMock();
+ $request = new Request();
$accessMap = $this->getMockBuilder('Symfony\Component\Security\Http\AccessMapInterface')->getMock();
$accessMap
@@ -136,19 +130,12 @@ public function testHandleWhenTheTokenIsNotAuthenticated()
$authManager
);
- $event = $this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock();
- $event
- ->expects($this->any())
- ->method('getRequest')
- ->willReturn($request)
- ;
-
- $listener($event);
+ $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST));
}
public function testHandleWhenThereIsNoAccessMapEntryMatchingTheRequest()
{
- $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->disableOriginalConstructor()->disableOriginalClone()->getMock();
+ $request = new Request();
$accessMap = $this->getMockBuilder('Symfony\Component\Security\Http\AccessMapInterface')->getMock();
$accessMap
@@ -178,19 +165,12 @@ public function testHandleWhenThereIsNoAccessMapEntryMatchingTheRequest()
$this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface')->getMock()
);
- $event = $this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock();
- $event
- ->expects($this->any())
- ->method('getRequest')
- ->willReturn($request)
- ;
-
- $listener($event);
+ $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST));
}
public function testHandleWhenAccessMapReturnsEmptyAttributes()
{
- $request = $this->getMockBuilder(Request::class)->disableOriginalConstructor()->disableOriginalClone()->getMock();
+ $request = new Request();
$accessMap = $this->getMockBuilder(AccessMapInterface::class)->getMock();
$accessMap
@@ -213,12 +193,7 @@ public function testHandleWhenAccessMapReturnsEmptyAttributes()
$this->getMockBuilder(AuthenticationManagerInterface::class)->getMock()
);
- $event = $this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock();
- $event
- ->expects($this->any())
- ->method('getRequest')
- ->willReturn($request)
- ;
+ $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST);
$listener(new LazyResponseEvent($event));
}
@@ -233,7 +208,7 @@ public function testHandleWhenTheSecurityTokenStorageHasNoToken()
->willReturn(null)
;
- $request = $this->getMockBuilder(Request::class)->disableOriginalConstructor()->disableOriginalClone()->getMock();
+ $request = new Request();
$accessMap = $this->getMockBuilder(AccessMapInterface::class)->getMock();
$accessMap
@@ -250,13 +225,6 @@ public function testHandleWhenTheSecurityTokenStorageHasNoToken()
$this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface')->getMock()
);
- $event = $this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock();
- $event
- ->expects($this->any())
- ->method('getRequest')
- ->willReturn($request)
- ;
-
- $listener($event);
+ $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST));
}
}
diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/AnonymousAuthenticationListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/AnonymousAuthenticationListenerTest.php
index 47f09199c43e5..e6f9f42217efb 100644
--- a/src/Symfony/Component/Security/Http/Tests/Firewall/AnonymousAuthenticationListenerTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/Firewall/AnonymousAuthenticationListenerTest.php
@@ -12,7 +12,9 @@
namespace Symfony\Component\Security\Http\Tests\Firewall;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
use Symfony\Component\Security\Http\Firewall\AnonymousAuthenticationListener;
@@ -38,7 +40,7 @@ public function testHandleWithTokenStorageHavingAToken()
;
$listener = new AnonymousAuthenticationListener($tokenStorage, 'TheSecret', null, $authenticationManager);
- $listener($this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock());
+ $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST));
}
public function testHandleWithTokenStorageHavingNoToken()
@@ -69,7 +71,7 @@ public function testHandleWithTokenStorageHavingNoToken()
;
$listener = new AnonymousAuthenticationListener($tokenStorage, 'TheSecret', null, $authenticationManager);
- $listener($this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock());
+ $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST));
}
public function testHandledEventIsLogged()
@@ -84,6 +86,6 @@ public function testHandledEventIsLogged()
$authenticationManager = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface')->getMock();
$listener = new AnonymousAuthenticationListener($tokenStorage, 'TheSecret', $logger, $authenticationManager);
- $listener($this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock());
+ $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST));
}
}
diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php
index 4afdd1fe08978..9353d48ba652f 100644
--- a/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php
@@ -36,6 +36,7 @@
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Event\DeauthenticatedEvent;
use Symfony\Component\Security\Http\Firewall\ContextListener;
+use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
use Symfony\Contracts\Service\ServiceLocatorTrait;
class ContextListenerTest extends TestCase
@@ -221,7 +222,7 @@ public function testOnKernelResponseListenerRemovesItself()
->willReturn(true);
$request->expects($this->any())
->method('getSession')
- ->will($this->returnValue($session));
+ ->willReturn($session);
$event = new ResponseEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST, new Response());
@@ -262,10 +263,23 @@ public function testIfTokenIsNotDeauthenticated()
$tokenStorage = new TokenStorage();
$badRefreshedUser = new User('foobar', 'baz');
$goodRefreshedUser = new User('foobar', 'bar');
- $tokenStorage = $this->handleEventWithPreviousSession([new SupportingUserProvider($badRefreshedUser), new SupportingUserProvider($goodRefreshedUser)], $goodRefreshedUser, true);
+ $tokenStorage = $this->handleEventWithPreviousSession([new SupportingUserProvider($badRefreshedUser), new SupportingUserProvider($goodRefreshedUser)], $goodRefreshedUser);
$this->assertSame($goodRefreshedUser, $tokenStorage->getToken()->getUser());
}
+ public function testRememberMeGetsCanceledIfTokenIsDeauthenticated()
+ {
+ $tokenStorage = new TokenStorage();
+ $refreshedUser = new User('foobar', 'baz');
+
+ $rememberMeServices = $this->createMock(RememberMeServicesInterface::class);
+ $rememberMeServices->expects($this->once())->method('loginFail');
+
+ $tokenStorage = $this->handleEventWithPreviousSession([new NotSupportingUserProvider(), new SupportingUserProvider($refreshedUser)], null, $rememberMeServices);
+
+ $this->assertNull($tokenStorage->getToken());
+ }
+
public function testTryAllUserProvidersUntilASupportingUserProviderIsFound()
{
$refreshedUser = new User('foobar', 'baz');
@@ -351,7 +365,7 @@ protected function runSessionOnKernelResponse($newToken, $original = null)
$request->cookies->set('MOCKSESSID', true);
$sessionId = $session->getId();
- $usageIndex = \method_exists(Request::class, 'getPreferredFormat') ? $session->getUsageIndex() : null;
+ $usageIndex = method_exists(Request::class, 'getPreferredFormat') ? $session->getUsageIndex() : null;
$event = new ResponseEvent(
$this->getMockBuilder(HttpKernelInterface::class)->getMock(),
@@ -374,7 +388,7 @@ protected function runSessionOnKernelResponse($newToken, $original = null)
return $session;
}
- private function handleEventWithPreviousSession($userProviders, UserInterface $user = null)
+ private function handleEventWithPreviousSession($userProviders, UserInterface $user = null, RememberMeServicesInterface $rememberMeServices = null)
{
$user = $user ?: new User('foo', 'bar');
$session = new Session(new MockArraySessionStorage());
@@ -388,7 +402,7 @@ private function handleEventWithPreviousSession($userProviders, UserInterface $u
$usageIndex = null;
$sessionTrackerEnabler = null;
- if (\method_exists(Request::class, 'getPreferredFormat')) {
+ if (method_exists(Request::class, 'getPreferredFormat')) {
$usageIndex = $session->getUsageIndex();
$tokenStorage = new UsageTrackingTokenStorage($tokenStorage, new class([
'session' => function () use ($session) { return $session; }
@@ -399,6 +413,10 @@ private function handleEventWithPreviousSession($userProviders, UserInterface $u
}
$listener = new ContextListener($tokenStorage, $userProviders, 'context_key', null, null, null, $sessionTrackerEnabler);
+
+ if ($rememberMeServices) {
+ $listener->setRememberMeServices($rememberMeServices);
+ }
$listener(new RequestEvent($this->getMockBuilder(HttpKernelInterface::class)->getMock(), $request, HttpKernelInterface::MASTER_REQUEST));
if (null !== $usageIndex) {
diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/RememberMeListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/RememberMeListenerTest.php
index ceb557b139d0a..d321ed68921bd 100644
--- a/src/Symfony/Component/Security/Http/Tests/Firewall/RememberMeListenerTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/Firewall/RememberMeListenerTest.php
@@ -15,6 +15,7 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Firewall\RememberMeListener;
use Symfony\Component\Security\Http\SecurityEvents;
@@ -27,7 +28,7 @@ public function testOnCoreSecurityDoesNotTryToPopulateNonEmptyTokenStorage()
list($listener, $tokenStorage) = $this->getListener();
$tokenStorage
- ->expects($this->once())
+ ->expects($this->any())
->method('getToken')
->willReturn($this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock())
;
@@ -45,7 +46,7 @@ public function testOnCoreSecurityDoesNothingWhenNoCookieIsSet()
list($listener, $tokenStorage, $service) = $this->getListener();
$tokenStorage
- ->expects($this->once())
+ ->expects($this->any())
->method('getToken')
->willReturn(null)
;
@@ -57,11 +58,6 @@ public function testOnCoreSecurityDoesNothingWhenNoCookieIsSet()
;
$event = $this->getGetResponseEvent();
- $event
- ->expects($this->once())
- ->method('getRequest')
- ->willReturn(new Request())
- ;
$this->assertNull($listener($event));
}
@@ -73,7 +69,7 @@ public function testOnCoreSecurityIgnoresAuthenticationExceptionThrownByAuthenti
$exception = new AuthenticationException('Authentication failed.');
$tokenStorage
- ->expects($this->once())
+ ->expects($this->any())
->method('getToken')
->willReturn(null)
;
@@ -96,12 +92,7 @@ public function testOnCoreSecurityIgnoresAuthenticationExceptionThrownByAuthenti
->willThrowException($exception)
;
- $event = $this->getGetResponseEvent();
- $event
- ->expects($this->once())
- ->method('getRequest')
- ->willReturn($request)
- ;
+ $event = $this->getGetResponseEvent($request);
$listener($event);
}
@@ -113,7 +104,7 @@ public function testOnCoreSecurityIgnoresAuthenticationOptionallyRethrowsExcepti
list($listener, $tokenStorage, $service, $manager) = $this->getListener(false, false);
$tokenStorage
- ->expects($this->once())
+ ->expects($this->any())
->method('getToken')
->willReturn(null)
;
@@ -137,11 +128,6 @@ public function testOnCoreSecurityIgnoresAuthenticationOptionallyRethrowsExcepti
;
$event = $this->getGetResponseEvent();
- $event
- ->expects($this->once())
- ->method('getRequest')
- ->willReturn(new Request())
- ;
$listener($event);
}
@@ -151,7 +137,7 @@ public function testOnCoreSecurityAuthenticationExceptionDuringAutoLoginTriggers
list($listener, $tokenStorage, $service, $manager) = $this->getListener();
$tokenStorage
- ->expects($this->once())
+ ->expects($this->any())
->method('getToken')
->willReturn(null)
;
@@ -174,11 +160,6 @@ public function testOnCoreSecurityAuthenticationExceptionDuringAutoLoginTriggers
;
$event = $this->getGetResponseEvent();
- $event
- ->expects($this->once())
- ->method('getRequest')
- ->willReturn(new Request())
- ;
$listener($event);
}
@@ -188,7 +169,7 @@ public function testOnCoreSecurity()
list($listener, $tokenStorage, $service, $manager) = $this->getListener();
$tokenStorage
- ->expects($this->once())
+ ->expects($this->any())
->method('getToken')
->willReturn(null)
;
@@ -213,11 +194,6 @@ public function testOnCoreSecurity()
;
$event = $this->getGetResponseEvent();
- $event
- ->expects($this->once())
- ->method('getRequest')
- ->willReturn(new Request())
- ;
$listener($event);
}
@@ -227,7 +203,7 @@ public function testSessionStrategy()
list($listener, $tokenStorage, $service, $manager, , , $sessionStrategy) = $this->getListener(false, true, true);
$tokenStorage
- ->expects($this->once())
+ ->expects($this->any())
->method('getToken')
->willReturn(null)
;
@@ -258,25 +234,10 @@ public function testSessionStrategy()
->willReturn(true)
;
- $request = $this->getMockBuilder('\Symfony\Component\HttpFoundation\Request')->getMock();
- $request
- ->expects($this->once())
- ->method('hasSession')
- ->willReturn(true)
- ;
-
- $request
- ->expects($this->once())
- ->method('getSession')
- ->willReturn($session)
- ;
+ $request = new Request();
+ $request->setSession($session);
- $event = $this->getGetResponseEvent();
- $event
- ->expects($this->once())
- ->method('getRequest')
- ->willReturn($request)
- ;
+ $event = $this->getGetResponseEvent($request);
$sessionStrategy
->expects($this->once())
@@ -292,7 +253,7 @@ public function testSessionIsMigratedByDefault()
list($listener, $tokenStorage, $service, $manager) = $this->getListener(false, true, false);
$tokenStorage
- ->expects($this->once())
+ ->expects($this->any())
->method('getToken')
->willReturn(null)
;
@@ -327,25 +288,10 @@ public function testSessionIsMigratedByDefault()
->method('migrate')
;
- $request = $this->getMockBuilder('\Symfony\Component\HttpFoundation\Request')->getMock();
- $request
- ->expects($this->any())
- ->method('hasSession')
- ->willReturn(true)
- ;
+ $request = new Request();
+ $request->setSession($session);
- $request
- ->expects($this->any())
- ->method('getSession')
- ->willReturn($session)
- ;
-
- $event = $this->getGetResponseEvent();
- $event
- ->expects($this->once())
- ->method('getRequest')
- ->willReturn($request)
- ;
+ $event = $this->getGetResponseEvent($request);
$listener($event);
}
@@ -355,7 +301,7 @@ public function testOnCoreSecurityInteractiveLoginEventIsDispatchedIfDispatcherI
list($listener, $tokenStorage, $service, $manager, , $dispatcher) = $this->getListener(true);
$tokenStorage
- ->expects($this->once())
+ ->expects($this->any())
->method('getToken')
->willReturn(null)
;
@@ -380,12 +326,6 @@ public function testOnCoreSecurityInteractiveLoginEventIsDispatchedIfDispatcherI
;
$event = $this->getGetResponseEvent();
- $request = new Request();
- $event
- ->expects($this->once())
- ->method('getRequest')
- ->willReturn($request)
- ;
$dispatcher
->expects($this->once())
@@ -399,9 +339,20 @@ public function testOnCoreSecurityInteractiveLoginEventIsDispatchedIfDispatcherI
$listener($event);
}
- protected function getGetResponseEvent()
+ protected function getGetResponseEvent(Request $request = null): RequestEvent
{
- return $this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock();
+ $request = $request ?? new Request();
+
+ $event = $this->getMockBuilder(RequestEvent::class)
+ ->setConstructorArgs([$this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST])
+ ->getMock();
+ $event
+ ->expects($this->any())
+ ->method('getRequest')
+ ->willReturn($request)
+ ;
+
+ return $event;
}
protected function getResponseEvent(): ResponseEvent
diff --git a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php
index 59fe793a3fe67..56d93d4408253 100644
--- a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php
+++ b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php
@@ -44,6 +44,7 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
self::HEADERS_KEY => [],
self::KEY_SEPARATOR_KEY => '.',
self::NO_HEADERS_KEY => false,
+ self::AS_COLLECTION_KEY => false,
self::OUTPUT_UTF8_BOM_KEY => false,
];
@@ -106,7 +107,7 @@ public function encode($data, $format, array $context = [])
$headers = array_merge(array_values($headers), array_diff($this->extractHeaders($data), $headers));
- if (!($context[self::NO_HEADERS_KEY] ?? false)) {
+ if (!($context[self::NO_HEADERS_KEY] ?? $this->defaultContext[self::NO_HEADERS_KEY])) {
fputcsv($handle, $headers, $delimiter, $enclosure, $escapeChar);
}
@@ -164,7 +165,7 @@ public function decode($data, $format, array $context = [])
if (null === $headers) {
$nbHeaders = $nbCols;
- if ($context[self::NO_HEADERS_KEY] ?? false) {
+ if ($context[self::NO_HEADERS_KEY] ?? $this->defaultContext[self::NO_HEADERS_KEY]) {
for ($i = 0; $i < $nbCols; ++$i) {
$headers[] = [$i];
}
@@ -204,7 +205,7 @@ public function decode($data, $format, array $context = [])
}
fclose($handle);
- if ($context[self::AS_COLLECTION_KEY] ?? false) {
+ if ($context[self::AS_COLLECTION_KEY] ?? $this->defaultContext[self::AS_COLLECTION_KEY]) {
return $result;
}
diff --git a/src/Symfony/Component/Serializer/Mapping/Factory/ClassResolverTrait.php b/src/Symfony/Component/Serializer/Mapping/Factory/ClassResolverTrait.php
index 15b535b0c73e4..b32a04b5a92a0 100644
--- a/src/Symfony/Component/Serializer/Mapping/Factory/ClassResolverTrait.php
+++ b/src/Symfony/Component/Serializer/Mapping/Factory/ClassResolverTrait.php
@@ -27,7 +27,7 @@ trait ClassResolverTrait
*
* @param object|string $value
*
- * @throws InvalidArgumentException If the class does not exists
+ * @throws InvalidArgumentException If the class does not exist
*/
private function getClass($value): string
{
diff --git a/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php b/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php
index edde9949176ef..bbf70eaac3734 100644
--- a/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php
+++ b/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php
@@ -123,7 +123,7 @@ private function getCacheValueForAttributesMetadata(string $class, array $contex
if (!$groups && ($context[AbstractNormalizer::GROUPS] ?? [])) {
continue;
}
- if ($groups && !array_intersect($groups, $context[AbstractNormalizer::GROUPS] ?? [])) {
+ if ($groups && !array_intersect($groups, (array) ($context[AbstractNormalizer::GROUPS] ?? []))) {
continue;
}
diff --git a/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php
index 0569c2923bcda..1b88957f76d74 100644
--- a/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php
@@ -41,14 +41,15 @@ public function __construct(bool $debug = false, array $defaultContext = [])
public function normalize($exception, $format = null, array $context = [])
{
$context += $this->defaultContext;
+ $debug = $this->debug && $context['debug'] ?? true;
$data = [
'type' => $context['type'],
'title' => $context['title'],
'status' => $context['status'] ?? $exception->getStatusCode(),
- 'detail' => $this->debug ? $exception->getMessage() : $exception->getStatusText(),
+ 'detail' => $debug ? $exception->getMessage() : $exception->getStatusText(),
];
- if ($this->debug) {
+ if ($debug) {
$data['class'] = $exception->getClass();
$data['trace'] = $exception->getTrace();
}
diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php
index 9518b1f86848d..33a16ee4402e2 100644
--- a/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php
@@ -203,6 +203,24 @@ public function testEncodeCustomSettingsPassedInContext()
]));
}
+ public function testEncodeCustomSettingsPassedInConstructor()
+ {
+ $encoder = new CsvEncoder([
+ CsvEncoder::DELIMITER_KEY => ';',
+ CsvEncoder::ENCLOSURE_KEY => "'",
+ CsvEncoder::ESCAPE_CHAR_KEY => '|',
+ CsvEncoder::KEY_SEPARATOR_KEY => '-',
+ ]);
+ $value = ['a' => 'he\'llo', 'c' => ['d' => 'foo']];
+
+ $this->assertSame(<<<'CSV'
+a;c-d
+'he''llo';foo
+
+CSV
+ , $encoder->encode($value, 'csv'));
+ }
+
public function testEncodeEmptyArray()
{
$this->assertEquals("\n\n", $this->encoder->encode([], 'csv'));
@@ -374,6 +392,15 @@ public function testEncodeWithoutHeader()
, $this->encoder->encode([['a', 'b'], ['c', 'd']], 'csv', [
CsvEncoder::NO_HEADERS_KEY => true,
]));
+ $encoder = new CsvEncoder([CsvEncoder::NO_HEADERS_KEY => true]);
+ $this->assertSame(<<<'CSV'
+a,b
+c,d
+
+CSV
+ , $encoder->encode([['a', 'b'], ['c', 'd']], 'csv', [
+ CsvEncoder::NO_HEADERS_KEY => true,
+ ]));
}
public function testEncodeArrayObject()
@@ -562,6 +589,23 @@ public function testDecodeCustomSettingsPassedInContext()
]));
}
+ public function testDecodeCustomSettingsPassedInConstructor()
+ {
+ $encoder = new CsvEncoder([
+ CsvEncoder::DELIMITER_KEY => ';',
+ CsvEncoder::ENCLOSURE_KEY => "'",
+ CsvEncoder::ESCAPE_CHAR_KEY => '|',
+ CsvEncoder::KEY_SEPARATOR_KEY => '-',
+ CsvEncoder::AS_COLLECTION_KEY => true, // Can be removed in 5.0
+ ]);
+ $expected = [['a' => 'hell\'o', 'bar' => ['baz' => 'b']]];
+ $this->assertEquals($expected, $encoder->decode(<<<'CSV'
+a;bar-baz
+'hell''o';b;c
+CSV
+ , 'csv'));
+ }
+
public function testDecodeMalformedCollection()
{
$expected = [
@@ -591,6 +635,15 @@ public function testDecodeWithoutHeader()
a,b
c,d
+CSV
+ , 'csv', [
+ CsvEncoder::NO_HEADERS_KEY => true,
+ ]));
+ $encoder = new CsvEncoder([CsvEncoder::NO_HEADERS_KEY => true]);
+ $this->assertEquals([['a', 'b'], ['c', 'd']], $encoder->decode(<<<'CSV'
+a,b
+c,d
+
CSV
, 'csv', [
CsvEncoder::NO_HEADERS_KEY => true,
diff --git a/src/Symfony/Component/Serializer/Tests/NameConverter/MetadataAwareNameConverterTest.php b/src/Symfony/Component/Serializer/Tests/NameConverter/MetadataAwareNameConverterTest.php
index d95c3ca91d2ee..540e59ed58aff 100644
--- a/src/Symfony/Component/Serializer/Tests/NameConverter/MetadataAwareNameConverterTest.php
+++ b/src/Symfony/Component/Serializer/Tests/NameConverter/MetadataAwareNameConverterTest.php
@@ -146,6 +146,8 @@ public function attributeAndContextProvider()
return [
['buz', 'buz', ['groups' => ['a']]],
['buzForExport', 'buz', ['groups' => ['b']]],
+ ['buz', 'buz', ['groups' => 'a']],
+ ['buzForExport', 'buz', ['groups' => 'b']],
['buz', 'buz', ['groups' => ['c']]],
['buz', 'buz', []],
];
diff --git a/src/Symfony/Component/Validator/Constraint.php b/src/Symfony/Component/Validator/Constraint.php
index 569ccfa93d4be..671e97e9c3811 100644
--- a/src/Symfony/Component/Validator/Constraint.php
+++ b/src/Symfony/Component/Validator/Constraint.php
@@ -288,7 +288,7 @@ public function getTargets()
*
* @internal
*/
- public function __sleep(): array
+ public function __sleep()
{
// Initialize "groups" option if it is not set
$this->groups;
diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf
index 5a391a2e6626e..21e2392c7d96c 100644
--- a/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf
+++ b/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf
@@ -362,6 +362,10 @@
This password has been leaked in a data breach, it must not be used. Please use another password.
このパスワードは漏洩している為使用できません。
+
+ This value should be between {{ min }} and {{ max }}.
+ {{ min }}以上{{ max }}以下でなければなりません。
+