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 d010bc71ee7de..0302981f0f9e2 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, string $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 41115698d0b8b..d2e81db377313 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php
@@ -374,6 +374,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()
@@ -766,12 +769,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 617f4ba00cf96..38fb908897fee 100644
--- a/src/Symfony/Component/Dotenv/Dotenv.php
+++ b/src/Symfony/Component/Dotenv/Dotenv.php
@@ -461,7 +461,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 ed54d2a689919..489c7fa6d1933 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 585df7408e7f5..7ab4c00bd3f33 100644
--- a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php
+++ b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php
@@ -399,7 +399,6 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
if (
'Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV7' === $class
|| 'Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV6' === $class
- || 'Test\Symfony\Component\Debug\Tests' === $refl->getNamespaceName()
) {
return [];
}
@@ -724,7 +723,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/ErrorHandler/Tests/Exception/FlattenExceptionTest.php b/src/Symfony/Component/ErrorHandler/Tests/Exception/FlattenExceptionTest.php
index 5db49b88fc739..cceb144b123e6 100644
--- a/src/Symfony/Component/ErrorHandler/Tests/Exception/FlattenExceptionTest.php
+++ b/src/Symfony/Component/ErrorHandler/Tests/Exception/FlattenExceptionTest.php
@@ -12,7 +12,6 @@
namespace Symfony\Component\ErrorHandler\Tests\Exception;
use PHPUnit\Framework\TestCase;
-use Symfony\Component\Debug\Exception\FatalThrowableError;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php
index bb1484ab67798..d4299da4d93f3 100644
--- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php
+++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php
@@ -783,7 +783,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/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php
index b4bac3858cab7..1b184b6ab4ccf 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 948b2d8eae89c..fb9228b10610d 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[]
@@ -36,7 +37,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);
@@ -51,6 +52,13 @@ public function collect(Request $request, Response $response, \Throwable $except
}
}
+ public function lateCollect()
+ {
+ foreach ($this->clients as $client) {
+ $client->reset();
+ }
+ }
+
public function getClients(): array
{
return $this->data['clients'] ?? [];
@@ -66,17 +74,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}
*/
@@ -85,7 +82,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 dd5898dffe519..f282e562c0508 100644
--- a/src/Symfony/Component/HttpClient/composer.json
+++ b/src/Symfony/Component/HttpClient/composer.json
@@ -24,7 +24,8 @@
"php": "^7.2.5",
"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,12 +34,7 @@
"psr/http-client": "^1.0",
"symfony/dependency-injection": "^4.4|^5.0",
"symfony/http-kernel": "^4.4|^5.0",
- "symfony/process": "^4.4|^5.0",
- "symfony/service-contracts": "^1.0|^2",
- "symfony/var-dumper": "^4.4|^5.0"
- },
- "conflict": {
- "symfony/http-kernel": "<4.4"
+ "symfony/process": "^4.4|^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 ad839cefe4f6d..d37cbd29681c4 100644
--- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php
+++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php
@@ -340,7 +340,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.
*
* @return $this
diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md
index bab37d1bee583..0af7d000af455 100644
--- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md
+++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md
@@ -17,6 +17,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 bb99c475b5798..37302128ad674 100644
--- a/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php
+++ b/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php
@@ -91,7 +91,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 9475c47992e02..698af72ce2b2e 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, bool $catch = fal
}
// 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 226802fd562c4..b26b487072cbe 100644
--- a/src/Symfony/Component/HttpKernel/Kernel.php
+++ b/src/Symfony/Component/HttpKernel/Kernel.php
@@ -68,11 +68,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
private static $freshCache = [];
- const VERSION = '5.0.0';
- const VERSION_ID = 50000;
+ const VERSION = '5.0.1';
+ const VERSION_ID = 50001;
const MAJOR_VERSION = 5;
const MINOR_VERSION = 0;
- const RELEASE_VERSION = 0;
+ const RELEASE_VERSION = 1;
const EXTRA_VERSION = '';
const END_OF_MAINTENANCE = '07/2020';
@@ -433,8 +433,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);
@@ -458,7 +459,7 @@ protected function initializeContainer()
$cache = new class($cachePath, $this->debug) extends ConfigCache {
public $lock;
- public function write($content, array $metadata = null)
+ public function write(string $content, array $metadata = null)
{
rewind($this->lock);
ftruncate($this->lock, 0);
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 e185c6f862e53..e9f100a8afa8f 100644
--- a/src/Symfony/Component/HttpKernel/Tests/KernelTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/KernelTest.php
@@ -29,7 +29,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 24f66ac7e1a9c..32c2e853af604 100644
--- a/src/Symfony/Component/Ldap/Ldap.php
+++ b/src/Symfony/Component/Ldap/Ldap.php
@@ -75,11 +75,7 @@ public function escape(string $subject, string $ignore = '', int $flags = 0): st
public static function create(string $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/Notifier/Bridge/Slack/Tests/SlackTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Slack/Tests/SlackTransportFactoryTest.php
new file mode 100644
index 0000000000000..afd77200ba8e8
--- /dev/null
+++ b/src/Symfony/Component/Notifier/Bridge/Slack/Tests/SlackTransportFactoryTest.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Notifier\Bridge\Slack\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Notifier\Bridge\Slack\SlackTransportFactory;
+use Symfony\Component\Notifier\Exception\IncompleteDsnException;
+use Symfony\Component\Notifier\Exception\UnsupportedSchemeException;
+use Symfony\Component\Notifier\Transport\Dsn;
+
+final class SlackTransportFactoryTest extends TestCase
+{
+ public function testCreateWithDsn(): void
+ {
+ $factory = new SlackTransportFactory();
+
+ $host = 'testHost';
+ $channel = 'testChannel';
+ $transport = $factory->create(Dsn::fromString(sprintf('slack://testUser@%s/?channel=%s', $host, $channel)));
+
+ $this->assertSame(sprintf('slack://%s?channel=%s', $host, $channel), (string) $transport);
+ }
+
+ public function testCreateWithNoTokenThrowsMalformed(): void
+ {
+ $factory = new SlackTransportFactory();
+
+ $this->expectException(IncompleteDsnException::class);
+ $factory->create(Dsn::fromString(sprintf('slack://%s/?channel=%s', 'testHost', 'testChannel')));
+ }
+
+ public function testSupportsSlackScheme(): void
+ {
+ $factory = new SlackTransportFactory();
+
+ $this->assertTrue($factory->supports(Dsn::fromString('slack://host/?channel=testChannel')));
+ $this->assertFalse($factory->supports(Dsn::fromString('somethingElse://host/?channel=testChannel')));
+ }
+
+ public function testNonSlackSchemeThrows(): void
+ {
+ $factory = new SlackTransportFactory();
+
+ $this->expectException(UnsupportedSchemeException::class);
+
+ $factory->create(Dsn::fromString('somethingElse://user:pwd@host/?channel=testChannel'));
+ }
+}
diff --git a/src/Symfony/Component/Notifier/Bridge/Slack/Tests/SlackTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Slack/Tests/SlackTransportTest.php
new file mode 100644
index 0000000000000..cbfaadd6060c1
--- /dev/null
+++ b/src/Symfony/Component/Notifier/Bridge/Slack/Tests/SlackTransportTest.php
@@ -0,0 +1,212 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Notifier\Bridge\Slack\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpClient\MockHttpClient;
+use Symfony\Component\Notifier\Bridge\Slack\SlackOptions;
+use Symfony\Component\Notifier\Bridge\Slack\SlackTransport;
+use Symfony\Component\Notifier\Exception\LogicException;
+use Symfony\Component\Notifier\Exception\TransportException;
+use Symfony\Component\Notifier\Message\ChatMessage;
+use Symfony\Component\Notifier\Message\MessageInterface;
+use Symfony\Component\Notifier\Message\MessageOptionsInterface;
+use Symfony\Component\Notifier\Notification\Notification;
+use Symfony\Component\Notifier\Recipient\Recipient;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+
+final class SlackTransportTest extends TestCase
+{
+ public function testToStringContainsProperties(): void
+ {
+ $host = 'testHost';
+ $channel = 'testChannel';
+
+ $transport = new SlackTransport('testToken', $channel, $this->createMock(HttpClientInterface::class));
+ $transport->setHost('testHost');
+
+ $this->assertSame(sprintf('slack://%s?channel=%s', $host, $channel), (string) $transport);
+ }
+
+ public function testSupportsChatMessage(): void
+ {
+ $transport = new SlackTransport('testToken', 'testChannel', $this->createMock(HttpClientInterface::class));
+
+ $this->assertTrue($transport->supports(new ChatMessage('testChatMessage')));
+ $this->assertFalse($transport->supports($this->createMock(MessageInterface::class)));
+ }
+
+ public function testSendNonChatMessageThrows(): void
+ {
+ $this->expectException(LogicException::class);
+
+ $transport = new SlackTransport('testToken', 'testChannel', $this->createMock(HttpClientInterface::class));
+
+ $transport->send($this->createMock(MessageInterface::class));
+ }
+
+ public function testSendWithEmptyArrayResponseThrows(): void
+ {
+ $this->expectException(TransportException::class);
+
+ $response = $this->createMock(ResponseInterface::class);
+ $response->expects($this->exactly(2))
+ ->method('getStatusCode')
+ ->willReturn(500);
+ $response->expects($this->once())
+ ->method('getContent')
+ ->willReturn('[]');
+
+ $client = new MockHttpClient(static function () use ($response): ResponseInterface {
+ return $response;
+ });
+
+ $transport = new SlackTransport('testToken', 'testChannel', $client);
+
+ $transport->send(new ChatMessage('testMessage'));
+ }
+
+ public function testSendWithErrorResponseThrows(): void
+ {
+ $this->expectException(TransportException::class);
+ $this->expectExceptionMessageRegExp('/testErrorCode/');
+
+ $response = $this->createMock(ResponseInterface::class);
+ $response->expects($this->exactly(2))
+ ->method('getStatusCode')
+ ->willReturn(400);
+
+ $response->expects($this->once())
+ ->method('getContent')
+ ->willReturn(json_encode(['error' => 'testErrorCode']));
+
+ $client = new MockHttpClient(static function () use ($response): ResponseInterface {
+ return $response;
+ });
+
+ $transport = new SlackTransport('testToken', 'testChannel', $client);
+
+ $transport->send(new ChatMessage('testMessage'));
+ }
+
+ public function testSendWithOptions(): void
+ {
+ $token = 'testToken';
+ $channel = 'testChannel';
+ $message = 'testMessage';
+
+ $response = $this->createMock(ResponseInterface::class);
+
+ $response->expects($this->exactly(2))
+ ->method('getStatusCode')
+ ->willReturn(200);
+
+ $response->expects($this->once())
+ ->method('getContent')
+ ->willReturn(json_encode(['ok' => true]));
+
+ $expectedBody = sprintf('token=%s&channel=%s&text=%s', $token, $channel, $message);
+
+ $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($response, $expectedBody): ResponseInterface {
+ $this->assertSame($expectedBody, $options['body']);
+
+ return $response;
+ });
+
+ $transport = new SlackTransport($token, $channel, $client);
+
+ $transport->send(new ChatMessage('testMessage'));
+ }
+
+ public function testSendWithNotification(): void
+ {
+ $token = 'testToken';
+ $channel = 'testChannel';
+ $message = 'testMessage';
+
+ $response = $this->createMock(ResponseInterface::class);
+
+ $response->expects($this->exactly(2))
+ ->method('getStatusCode')
+ ->willReturn(200);
+
+ $response->expects($this->once())
+ ->method('getContent')
+ ->willReturn(json_encode(['ok' => true]));
+
+ $notification = new Notification($message);
+ $chatMessage = ChatMessage::fromNotification($notification, new Recipient('test-email@example.com'));
+ $options = SlackOptions::fromNotification($notification);
+
+ $expectedBody = http_build_query([
+ 'blocks' => $options->toArray()['blocks'],
+ 'token' => $token,
+ 'channel' => $channel,
+ 'text' => $message,
+ ]);
+
+ $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($response, $expectedBody): ResponseInterface {
+ $this->assertSame($expectedBody, $options['body']);
+
+ return $response;
+ });
+
+ $transport = new SlackTransport($token, $channel, $client);
+
+ $transport->send($chatMessage);
+ }
+
+ public function testSendWithInvalidOptions(): void
+ {
+ $this->expectException(LogicException::class);
+
+ $client = new MockHttpClient(function (string $method, string $url, array $options = []): ResponseInterface {
+ return $this->createMock(ResponseInterface::class);
+ });
+
+ $transport = new SlackTransport('testToken', 'testChannel', $client);
+
+ $transport->send(new ChatMessage('testMessage', $this->createMock(MessageOptionsInterface::class)));
+ }
+
+ public function testSendWith200ResponseButNotOk(): void
+ {
+ $token = 'testToken';
+ $channel = 'testChannel';
+ $message = 'testMessage';
+
+ $this->expectException(TransportException::class);
+
+ $response = $this->createMock(ResponseInterface::class);
+
+ $response->expects($this->exactly(2))
+ ->method('getStatusCode')
+ ->willReturn(200);
+
+ $response->expects($this->once())
+ ->method('getContent')
+ ->willReturn(json_encode(['ok' => false, 'error' => 'testErrorCode']));
+
+ $expectedBody = sprintf('token=%s&channel=%s&text=%s', $token, $channel, $message);
+
+ $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($response, $expectedBody): ResponseInterface {
+ $this->assertSame($expectedBody, $options['body']);
+
+ return $response;
+ });
+
+ $transport = new SlackTransport($token, $channel, $client);
+
+ $transport->send(new ChatMessage('testMessage'));
+ }
+}
diff --git a/src/Symfony/Component/Notifier/Bridge/Slack/composer.json b/src/Symfony/Component/Notifier/Bridge/Slack/composer.json
index feb1e5f4edbb9..9b9d08930a9be 100644
--- a/src/Symfony/Component/Notifier/Bridge/Slack/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Slack/composer.json
@@ -20,6 +20,9 @@
"symfony/http-client": "^4.3|^5.0",
"symfony/notifier": "~5.0.0"
},
+ "require-dev": {
+ "symfony/event-dispatcher": "^4.3|^5.0"
+ },
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Slack\\": "" },
"exclude-from-classmap": [
diff --git a/src/Symfony/Component/Notifier/Bridge/Telegram/Tests/TelegramTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Telegram/Tests/TelegramTransportFactoryTest.php
new file mode 100644
index 0000000000000..c74d38f2f6882
--- /dev/null
+++ b/src/Symfony/Component/Notifier/Bridge/Telegram/Tests/TelegramTransportFactoryTest.php
@@ -0,0 +1,65 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Notifier\Bridge\Telegram\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Notifier\Bridge\Telegram\TelegramTransportFactory;
+use Symfony\Component\Notifier\Exception\IncompleteDsnException;
+use Symfony\Component\Notifier\Exception\UnsupportedSchemeException;
+use Symfony\Component\Notifier\Transport\Dsn;
+
+final class TelegramTransportFactoryTest extends TestCase
+{
+ public function testCreateWithDsn(): void
+ {
+ $factory = new TelegramTransportFactory();
+
+ $host = 'testHost';
+ $channel = 'testChannel';
+
+ $transport = $factory->create(Dsn::fromString(sprintf('telegram://%s@%s/?channel=%s', 'testUser:testPassword', $host, $channel)));
+
+ $this->assertSame(sprintf('telegram://%s?channel=%s', $host, $channel), (string) $transport);
+ }
+
+ public function testCreateWithNoPasswordThrowsMalformed(): void
+ {
+ $factory = new TelegramTransportFactory();
+
+ $this->expectException(IncompleteDsnException::class);
+ $factory->create(Dsn::fromString(sprintf('telegram://%s@%s/?channel=%s', 'simpleToken', 'testHost', 'testChannel')));
+ }
+
+ public function testCreateWithNoTokenThrowsMalformed(): void
+ {
+ $factory = new TelegramTransportFactory();
+
+ $this->expectException(IncompleteDsnException::class);
+ $factory->create(Dsn::fromString(sprintf('telegram://%s/?channel=%s', 'testHost', 'testChannel')));
+ }
+
+ public function testSupportsTelegramScheme(): void
+ {
+ $factory = new TelegramTransportFactory();
+
+ $this->assertTrue($factory->supports(Dsn::fromString('telegram://host/?channel=testChannel')));
+ $this->assertFalse($factory->supports(Dsn::fromString('somethingElse://host/?channel=testChannel')));
+ }
+
+ public function testNonTelegramSchemeThrows(): void
+ {
+ $factory = new TelegramTransportFactory();
+
+ $this->expectException(UnsupportedSchemeException::class);
+ $factory->create(Dsn::fromString('somethingElse://user:pwd@host/?channel=testChannel'));
+ }
+}
diff --git a/src/Symfony/Component/Notifier/Bridge/Telegram/Tests/TelegramTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Telegram/Tests/TelegramTransportTest.php
new file mode 100644
index 0000000000000..352b390ffd00b
--- /dev/null
+++ b/src/Symfony/Component/Notifier/Bridge/Telegram/Tests/TelegramTransportTest.php
@@ -0,0 +1,138 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Notifier\Bridge\Telegram\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpClient\MockHttpClient;
+use Symfony\Component\Notifier\Bridge\Telegram\TelegramTransport;
+use Symfony\Component\Notifier\Exception\LogicException;
+use Symfony\Component\Notifier\Exception\TransportException;
+use Symfony\Component\Notifier\Message\ChatMessage;
+use Symfony\Component\Notifier\Message\MessageInterface;
+use Symfony\Component\Notifier\Message\MessageOptionsInterface;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+
+final class TelegramTransportTest extends TestCase
+{
+ public function testToStringContainsProperties(): void
+ {
+ $channel = 'testChannel';
+
+ $transport = new TelegramTransport('testToken', $channel, $this->createMock(HttpClientInterface::class));
+ $transport->setHost('testHost');
+
+ $this->assertSame(sprintf('telegram://%s?channel=%s', 'testHost', $channel), (string) $transport);
+ }
+
+ public function testSupportsChatMessage(): void
+ {
+ $transport = new TelegramTransport('testToken', 'testChannel', $this->createMock(HttpClientInterface::class));
+
+ $this->assertTrue($transport->supports(new ChatMessage('testChatMessage')));
+ $this->assertFalse($transport->supports($this->createMock(MessageInterface::class)));
+ }
+
+ public function testSendNonChatMessageThrows(): void
+ {
+ $this->expectException(LogicException::class);
+ $transport = new TelegramTransport('testToken', 'testChannel', $this->createMock(HttpClientInterface::class));
+
+ $transport->send($this->createMock(MessageInterface::class));
+ }
+
+ public function testSendWithErrorResponseThrows(): void
+ {
+ $this->expectException(TransportException::class);
+ $this->expectExceptionMessageRegExp('/testDescription.+testErrorCode/');
+
+ $response = $this->createMock(ResponseInterface::class);
+ $response->expects($this->exactly(2))
+ ->method('getStatusCode')
+ ->willReturn(400);
+ $response->expects($this->once())
+ ->method('getContent')
+ ->willReturn(json_encode(['description' => 'testDescription', 'error_code' => 'testErrorCode']));
+
+ $client = new MockHttpClient(static function () use ($response): ResponseInterface {
+ return $response;
+ });
+
+ $transport = new TelegramTransport('testToken', 'testChannel', $client);
+
+ $transport->send(new ChatMessage('testMessage'));
+ }
+
+ public function testSendWithOptions(): void
+ {
+ $channel = 'testChannel';
+
+ $response = $this->createMock(ResponseInterface::class);
+ $response->expects($this->exactly(2))
+ ->method('getStatusCode')
+ ->willReturn(200);
+ $response->expects($this->once())
+ ->method('getContent')
+ ->willReturn('');
+
+ $expectedBody = [
+ 'chat_id' => $channel,
+ 'text' => 'testMessage',
+ 'parse_mode' => 'Markdown',
+ ];
+
+ $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($response, $expectedBody): ResponseInterface {
+ $this->assertEquals($expectedBody, json_decode($options['body'], true));
+
+ return $response;
+ });
+
+ $transport = new TelegramTransport('testToken', $channel, $client);
+
+ $transport->send(new ChatMessage('testMessage'));
+ }
+
+ public function testSendWithChannelOverride(): void
+ {
+ $channelOverride = 'channelOverride';
+
+ $response = $this->createMock(ResponseInterface::class);
+ $response->expects($this->exactly(2))
+ ->method('getStatusCode')
+ ->willReturn(200);
+ $response->expects($this->once())
+ ->method('getContent')
+ ->willReturn('');
+
+ $expectedBody = [
+ 'chat_id' => $channelOverride,
+ 'text' => 'testMessage',
+ 'parse_mode' => 'Markdown',
+ ];
+
+ $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($response, $expectedBody): ResponseInterface {
+ $this->assertEquals($expectedBody, json_decode($options['body'], true));
+
+ return $response;
+ });
+
+ $transport = new TelegramTransport('testToken', 'defaultChannel', $client);
+
+ $messageOptions = $this->createMock(MessageOptionsInterface::class);
+ $messageOptions
+ ->expects($this->once())
+ ->method('getRecipientId')
+ ->willReturn($channelOverride);
+
+ $transport->send(new ChatMessage('testMessage', $messageOptions));
+ }
+}
diff --git a/src/Symfony/Component/Notifier/Bridge/Telegram/composer.json b/src/Symfony/Component/Notifier/Bridge/Telegram/composer.json
index a562f74dce549..003215dd49e9a 100644
--- a/src/Symfony/Component/Notifier/Bridge/Telegram/composer.json
+++ b/src/Symfony/Component/Notifier/Bridge/Telegram/composer.json
@@ -20,6 +20,9 @@
"symfony/http-client": "^4.3|^5.0",
"symfony/notifier": "~5.0.0"
},
+ "require-dev": {
+ "symfony/event-dispatcher": "^4.3|^5.0"
+ },
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Telegram\\": "" },
"exclude-from-classmap": [
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 3af07c7cf79f1..47eace5b0d817 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 aefb8295115ab..1a375d95ff013 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, string $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/Matcher/UrlMatcher.php b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php
index 81f8b04dbd44f..4c76231e3de81 100644
--- a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php
+++ b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php
@@ -93,9 +93,7 @@ public function match(string $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 6aa88ffed9b73..cb655f4485057 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/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md
index d5c28dc67ab88..7d971ee50440a 100644
--- a/src/Symfony/Component/Security/CHANGELOG.md
+++ b/src/Symfony/Component/Security/CHANGELOG.md
@@ -15,7 +15,7 @@ CHANGELOG
**After**
```php
- if ($this->authorizationChecker->isGranted(new Expression("has_role('ROLE_USER') or has_role('ROLE_ADMIN')"))) {}
+ if ($this->authorizationChecker->isGranted(new Expression("is_granted('ROLE_USER') or is_granted('ROLE_ADMIN')"))) {}
// or:
if ($this->authorizationChecker->isGranted('ROLE_USER')
|| $this->authorizationChecker->isGranted('ROLE_ADMIN')
@@ -62,6 +62,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 3f62de4ef8e6a..b9c984f21d334 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\RememberMe\RememberMeServicesInterface;
/**
@@ -31,7 +32,7 @@
*
* @final
*/
-class GuardAuthenticationListener
+class GuardAuthenticationListener extends AbstractListener
{
private $guardHandler;
private $authenticationManager;
@@ -58,9 +59,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];
@@ -72,7 +73,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;
@@ -93,19 +126,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 63f0330f07625..a70886b02148c 100644
--- a/src/Symfony/Component/Security/Guard/composer.json
+++ b/src/Symfony/Component/Security/Guard/composer.json
@@ -18,7 +18,7 @@
"require": {
"php": "^7.2.5",
"symfony/security-core": "^5.0",
- "symfony/security-http": "^4.4|^5.0"
+ "symfony/security-http": "^4.4.1|^5.0.1"
},
"require-dev": {
"psr/log": "~1.0"
diff --git a/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php
index b9fdb382b97c9..4fa09b30fc2df 100644
--- a/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php
@@ -48,7 +48,7 @@
* @author Fabien Potencier
* @author Johannes M. Schmitt
*/
-abstract class AbstractAuthenticationListener
+abstract class AbstractAuthenticationListener extends AbstractListener
{
protected $options;
protected $logger;
@@ -102,20 +102,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 b969ae3bdb1d0..d5c5d9325d1c4 100644
--- a/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php
@@ -34,7 +34,7 @@
*
* @internal
*/
-abstract class AbstractPreAuthenticatedListener
+abstract class AbstractPreAuthenticatedListener extends AbstractListener
{
protected $logger;
private $tokenStorage;
@@ -53,20 +53,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 2dbeee1033151..3d88103d39abc 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
*/
-class AccessListener
+class AccessListener extends AbstractListener
{
private $tokenStorage;
private $accessDecisionManager;
@@ -42,13 +44,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.');
@@ -56,9 +69,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 f9f0ff0946400..c54e6ba67d8b4 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
*/
-class AnonymousAuthenticationListener
+class AnonymousAuthenticationListener extends AbstractListener
{
private $tokenStorage;
private $secret;
@@ -41,10 +42,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 77c4f9dac96a9..0692e055d0539 100644
--- a/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php
@@ -29,7 +29,7 @@
*
* @final
*/
-class BasicAuthenticationListener
+class BasicAuthenticationListener extends AbstractListener
{
private $tokenStorage;
private $authenticationManager;
@@ -53,10 +53,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 94515061de88c..243faecbc083b 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
*/
-class ChannelListener
+class ChannelListener extends AbstractListener
{
private $map;
private $authenticationEntryPoint;
@@ -40,10 +41,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()) {
@@ -57,11 +56,7 @@ public function __invoke(RequestEvent $event)
}
}
- $response = $this->authenticationEntryPoint->start($request);
-
- $event->setResponse($response);
-
- return;
+ return true;
}
if ('http' === $channel && $request->isSecure()) {
@@ -69,9 +64,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 b9f5809723d73..02de94628a396 100644
--- a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php
@@ -13,6 +13,7 @@
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy;
+use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
@@ -29,6 +30,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;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
@@ -39,7 +41,7 @@
*
* @final
*/
-class ContextListener
+class ContextListener extends AbstractListener
{
private $tokenStorage;
private $sessionKey;
@@ -48,6 +50,7 @@ class ContextListener
private $dispatcher;
private $registered;
private $trustResolver;
+ private $rememberMeServices;
private $sessionTrackerEnabler;
/**
@@ -68,10 +71,18 @@ public function __construct(TokenStorageInterface $tokenStorage, iterable $userP
$this->sessionTrackerEnabler = $sessionTrackerEnabler;
}
+ /**
+ * {@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']);
@@ -112,6 +123,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]);
@@ -282,4 +297,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 46f757e1ee340..1194cea95f1e2 100644
--- a/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php
@@ -30,7 +30,7 @@
*
* @final
*/
-class LogoutListener
+class LogoutListener extends AbstractListener
{
private $tokenStorage;
private $options;
@@ -61,6 +61,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.
*
@@ -70,14 +78,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 2d0626d05c501..c288fcb94b942 100644
--- a/src/Symfony/Component/Security/Http/Firewall/RememberMeListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/RememberMeListener.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\Storage\TokenStorageInterface;
@@ -30,7 +31,7 @@
*
* @final
*/
-class RememberMeListener
+class RememberMeListener extends AbstractListener
{
private $tokenStorage;
private $rememberMeServices;
@@ -51,10 +52,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/SwitchUserListener.php b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php
index eadd23f91e4af..f6d4c8a587c0a 100644
--- a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php
@@ -37,7 +37,7 @@
*
* @final
*/
-class SwitchUserListener
+class SwitchUserListener extends AbstractListener
{
const EXIT_VALUE = '_exit';
@@ -71,14 +71,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);
@@ -88,9 +84,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.');
}
@@ -144,7 +157,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 5c5a35e435093..ace15f03d522a 100644
--- a/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php
@@ -43,7 +43,7 @@
*
* @final
*/
-class UsernamePasswordJsonAuthenticationListener
+class UsernamePasswordJsonAuthenticationListener extends AbstractListener
{
private $tokenStorage;
private $authenticationManager;
@@ -71,19 +71,27 @@ public function __construct(TokenStorageInterface $tokenStorage, AuthenticationM
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}
- 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 3432bd7f97a6a..a6f118f6490a2 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');
@@ -372,7 +386,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());
@@ -392,6 +406,10 @@ private function handleEventWithPreviousSession($userProviders, UserInterface $u
$sessionTrackerEnabler = [$tokenStorage, 'enableUsageTracking'];
$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));
$this->assertSame($usageIndex, $session->getUsageIndex());
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 133f3d562bc00..183e02fce1e7a 100644
--- a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php
+++ b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php
@@ -37,7 +37,6 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
private $formulasStartCharacters = ['=', '-', '+', '@'];
private $defaultContext = [
- self::AS_COLLECTION_KEY => true,
self::DELIMITER_KEY => ',',
self::ENCLOSURE_KEY => '"',
self::ESCAPE_CHAR_KEY => '',
@@ -45,6 +44,7 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
self::HEADERS_KEY => [],
self::KEY_SEPARATOR_KEY => '.',
self::NO_HEADERS_KEY => false,
+ self::AS_COLLECTION_KEY => true,
self::OUTPUT_UTF8_BOM_KEY => false,
];
@@ -92,7 +92,7 @@ public function encode($data, string $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);
}
@@ -150,7 +150,7 @@ public function decode(string $data, string $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];
}
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 86626490f0ccc..80840a042f2fc 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 17d599d74e215..e2ec20b881f2a 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, string $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 29fd0f625f515..1b77704ef98ae 100644
--- a/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php
@@ -189,6 +189,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'));
@@ -346,6 +364,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()
@@ -500,6 +527,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 = [
@@ -529,6 +573,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 7ea71ffecacd1..9b0ad4d79c55b 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 224c7edd412e5..b349d8675d847 100644
--- a/src/Symfony/Component/Validator/Constraint.php
+++ b/src/Symfony/Component/Validator/Constraint.php
@@ -287,7 +287,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 }}以下でなければなりません。
+