+ */ +class LazyFirewallContext extends FirewallContext +{ + private $accessListener; + private $tokenStorage; + private $map; + + public function __construct(iterable $listeners, ?ExceptionListener $exceptionListener, ?LogoutListener $logoutListener, ?FirewallConfig $config, AccessListener $accessListener, TokenStorage $tokenStorage, AccessMapInterface $map) + { + parent::__construct($listeners, $exceptionListener, $logoutListener, $config); + + $this->accessListener = $accessListener; + $this->tokenStorage = $tokenStorage; + $this->map = $map; + } + + public function getListeners(): iterable + { + return [$this]; + } + + public function __invoke(RequestEvent $event) + { + $this->tokenStorage->setInitializer(function () use ($event) { + $event = new LazyResponseEvent($event); + foreach (parent::getListeners() as $listener) { + if (\is_callable($listener)) { + $listener($event); + } else { + @trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, implement "__invoke()" instead.', \get_class($listener)), E_USER_DEPRECATED); + $listener->handle($event); + } + } + }); + + try { + [$attributes] = $this->map->getPatterns($event->getRequest()); + + if ($attributes && [AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] !== $attributes) { + ($this->accessListener)($event); + } + } catch (LazyResponseException $e) { + $event->setResponse($e->getResponse()); + } + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php index 269827e2df..cf0e1150af 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php @@ -59,6 +59,6 @@ class LocalizedController implements ContainerAwareInterface public function homepageAction() { - return new Response('
Homepage'); + return (new Response('Homepage'))->setPublic(); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php index 06260c1bed..0303f1b4ee 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php @@ -129,6 +129,16 @@ class SecurityRoutingIntegrationTest extends AbstractWebTestCase $client->request('GET', '/unprotected_resource'); } + public function testPublicHomepage() + { + $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'config.yml']); + $client->request('GET', '/en/'); + + $this->assertEquals(200, $client->getResponse()->getStatusCode(), (string) $client->getResponse()); + $this->assertTrue($client->getResponse()->headers->getCacheControlDirective('public')); + $this->assertSame(0, self::$container->get('session')->getUsageIndex()); + } + private function assertAllowed($client, $path) { $client->request('GET', $path); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml index 4e2ac1e11b..ad8beee94c 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml @@ -27,7 +27,7 @@ security: check_path: /login_check default_target_path: /profile logout: ~ - anonymous: ~ + anonymous: lazy # This firewall is here just to check its the logout functionality second_area: @@ -38,6 +38,7 @@ security: path: /second/logout access_control: + - { path: ^/en/$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/unprotected_resource$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/secure-but-not-covered-by-access-control$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/secured-by-one-ip$, ip: 10.10.10.10, roles: IS_AUTHENTICATED_ANONYMOUSLY } diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 4f4fdd271e..75234ee110 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -536,6 +536,10 @@ class Application implements ResetInterface { $namespaces = []; foreach ($this->all() as $command) { + if ($command->isHidden()) { + continue; + } + $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName())); foreach ($command->getAliases() as $alias) { @@ -629,6 +633,11 @@ class Application implements ResetInterface $message = sprintf('Command "%s" is not defined.', $name); if ($alternatives = $this->findAlternatives($name, $allCommands)) { + // remove hidden commands + $alternatives = array_filter($alternatives, function ($name) { + return !$this->get($name)->isHidden(); + }); + if (1 == \count($alternatives)) { $message .= "\n\nDid you mean this?\n "; } else { @@ -637,7 +646,7 @@ class Application implements ResetInterface $message .= implode("\n ", $alternatives); } - throw new CommandNotFoundException($message, $alternatives); + throw new CommandNotFoundException($message, array_values($alternatives)); } // filter out aliases for commands which are already on the list @@ -658,20 +667,36 @@ class Application implements ResetInterface foreach ($abbrevs as $abbrev) { $maxLen = max(Helper::strlen($abbrev), $maxLen); } - $abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen) { + $abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) { if (!$commandList[$cmd] instanceof Command) { - return $cmd; + $commandList[$cmd] = $this->commandLoader->get($cmd); } + + if ($commandList[$cmd]->isHidden()) { + unset($commands[array_search($cmd, $commands)]); + + return false; + } + $abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription(); return Helper::strlen($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev; }, array_values($commands)); - $suggestions = $this->getAbbreviationSuggestions($abbrevs); - throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $name, $suggestions), array_values($commands)); + if (\count($commands) > 1) { + $suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs)); + + throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $name, $suggestions), array_values($commands)); + } } - return $this->get(reset($commands)); + $command = $this->get(reset($commands)); + + if ($command->isHidden()) { + @trigger_error(sprintf('Command "%s" is hidden, finding it using an abbreviation is deprecated since Symfony 4.4, use its full name instead.', $command->getName()), E_USER_DEPRECATED); + } + + return $command; } /** diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 073d434c4f..27342307d9 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -16,6 +16,7 @@ CHANGELOG 4.4.0 ----- + * deprecated finding hidden commands using an abbreviation, use the full name instead * added `Question::setTrimmable` default to true to allow the answer to be trimmed * added method `preventRedrawFasterThan()` and `forceRedrawSlowerThan()` on `ProgressBar` * `Application` implements `ResetInterface` diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 19b3c34b8c..e750975d2d 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -75,6 +75,8 @@ class ApplicationTest extends TestCase require_once self::$fixturesPath.'/FooWithoutAliasCommand.php'; require_once self::$fixturesPath.'/TestAmbiguousCommandRegistering.php'; require_once self::$fixturesPath.'/TestAmbiguousCommandRegistering2.php'; + require_once self::$fixturesPath.'/FooHiddenCommand.php'; + require_once self::$fixturesPath.'/BarHiddenCommand.php'; } protected function normalizeLineBreaks($text) @@ -440,6 +442,16 @@ class ApplicationTest extends TestCase ]; } + public function testFindWithAmbiguousAbbreviationsFindsCommandIfAlternativesAreHidden() + { + $application = new Application(); + + $application->add(new \FooCommand()); + $application->add(new \FooHiddenCommand()); + + $this->assertInstanceOf('FooCommand', $application->find('foo:')); + } + public function testFindCommandEqualNamespace() { $application = new Application(); @@ -664,6 +676,7 @@ class ApplicationTest extends TestCase $application->add(new \Foo1Command()); $application->add(new \Foo2Command()); $application->add(new \Foo3Command()); + $application->add(new \FooHiddenCommand()); $expectedAlternatives = [ 'afoobar', @@ -706,6 +719,49 @@ class ApplicationTest extends TestCase $application->find('foo::bar'); } + public function testFindHiddenWithExactName() + { + $application = new Application(); + $application->add(new \FooHiddenCommand()); + + $this->assertInstanceOf('FooHiddenCommand', $application->find('foo:hidden')); + $this->assertInstanceOf('FooHiddenCommand', $application->find('afoohidden')); + } + + /** + * @group legacy + * @expectedDeprecation Command "%s:hidden" is hidden, finding it using an abbreviation is deprecated since Symfony 4.4, use its full name instead. + * @dataProvider provideAbbreviationsForHiddenCommands + */ + public function testFindHiddenWithAbbreviatedName($name) + { + $application = new Application(); + + $application->add(new \FooHiddenCommand()); + $application->add(new \BarHiddenCommand()); + + $application->find($name); + } + + public function provideAbbreviationsForHiddenCommands() + { + return [ + ['foo:hidde'], + ['afoohidd'], + ['bar:hidde'], + ]; + } + + public function testFindAmbiguousCommandsIfAllAlternativesAreHidden() + { + $application = new Application(); + + $application->add(new \FooCommand()); + $application->add(new \FooHiddenCommand()); + + $this->assertInstanceOf('FooCommand', $application->find('foo:')); + } + public function testSetCatchExceptions() { $application = new Application(); diff --git a/src/Symfony/Component/Console/Tests/Fixtures/BarHiddenCommand.php b/src/Symfony/Component/Console/Tests/Fixtures/BarHiddenCommand.php new file mode 100644 index 0000000000..d500f8731f --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/BarHiddenCommand.php @@ -0,0 +1,21 @@ +setName('bar:hidden') + ->setAliases(['abarhidden']) + ->setHidden(true) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + } +} diff --git a/src/Symfony/Component/Console/Tests/Fixtures/FooHiddenCommand.php b/src/Symfony/Component/Console/Tests/Fixtures/FooHiddenCommand.php new file mode 100644 index 0000000000..75fbf7804d --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/FooHiddenCommand.php @@ -0,0 +1,21 @@ +setName('foo:hidden') + ->setAliases(['afoohidden']) + ->setHidden(true) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php index 4045975fe1..fa25ba7608 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php @@ -110,6 +110,10 @@ abstract class AbstractRecursivePass implements CompilerPassInterface */ protected function getConstructor(Definition $definition, bool $required) { + if ($definition->isSynthetic()) { + return null; + } + if (\is_string($factory = $definition->getFactory())) { if (!\function_exists($factory)) { throw new RuntimeException(sprintf('Invalid service "%s": function "%s" does not exist.', $this->currentId, $factory)); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index d3ee821831..2110f02171 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -568,12 +568,10 @@ class AutowirePassTest extends TestCase ); } - /** - * @exceptedExceptionMessage Invalid service "Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy": method "setLogger()" does not exist. - */ public function testWithNonExistingSetterAndAutowiring() { $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Invalid service "Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass": method "setLogger()" does not exist.'); $container = new ContainerBuilder(); $definition = $container->register(CaseSensitiveClass::class, CaseSensitiveClass::class)->setAutowired(true); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php index 1f27f6cdcc..8ea5101e6e 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php @@ -15,9 +15,11 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\BoundArgument; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\Compiler\AutowireRequiredMethodsPass; +use Symfony\Component\DependencyInjection\Compiler\DefinitionErrorExceptionPass; use Symfony\Component\DependencyInjection\Compiler\ResolveBindingsPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy; @@ -131,4 +133,25 @@ class ResolveBindingsPassTest extends TestCase $pass = new ResolveBindingsPass(); $pass->process($container); } + + public function testSyntheticServiceWithBind() + { + $container = new ContainerBuilder(); + $argument = new BoundArgument('bar'); + + $container->register('foo', 'stdClass') + ->addArgument(new Reference('synthetic.service')); + + $container->register('synthetic.service') + ->setSynthetic(true) + ->setBindings(['$apiKey' => $argument]); + + $container->register(NamedArgumentsDummy::class, NamedArgumentsDummy::class) + ->setBindings(['$apiKey' => $argument]); + + (new ResolveBindingsPass())->process($container); + (new DefinitionErrorExceptionPass())->process($container); + + $this->assertSame([1 => 'bar'], $container->getDefinition(NamedArgumentsDummy::class)->getArguments()); + } } diff --git a/src/Symfony/Component/HttpFoundation/Cookie.php b/src/Symfony/Component/HttpFoundation/Cookie.php index 6ada5889f6..fc711ee663 100644 --- a/src/Symfony/Component/HttpFoundation/Cookie.php +++ b/src/Symfony/Component/HttpFoundation/Cookie.php @@ -18,6 +18,10 @@ namespace Symfony\Component\HttpFoundation; */ class Cookie { + const SAMESITE_NONE = 'none'; + const SAMESITE_LAX = 'lax'; + const SAMESITE_STRICT = 'strict'; + protected $name; protected $value; protected $domain; @@ -25,13 +29,14 @@ class Cookie protected $path; protected $secure; protected $httpOnly; + private $raw; private $sameSite; private $secureDefault = false; - const SAMESITE_NONE = 'none'; - const SAMESITE_LAX = 'lax'; - const SAMESITE_STRICT = 'strict'; + private static $reservedCharsList = "=,; \t\r\n\v\f"; + private static $reservedCharsFrom = ['=', ',', ';', ' ', "\t", "\r", "\n", "\v", "\f"]; + private static $reservedCharsTo = ['%3D', '%2C', '%3B', '%20', '%09', '%0D', '%0A', '%0B', '%0C']; /** * Creates cookie from raw header string. @@ -86,7 +91,7 @@ class Cookie public function __construct(string $name, string $value = null, $expire = 0, ?string $path = '/', string $domain = null, bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = 'lax') { // from PHP source code - if (preg_match("/[=,; \t\r\n\013\014]/", $name)) { + if ($raw && false !== strpbrk($name, self::$reservedCharsList)) { throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name)); } @@ -134,7 +139,13 @@ class Cookie */ public function __toString() { - $str = ($this->isRaw() ? $this->getName() : urlencode($this->getName())).'='; + if ($this->isRaw()) { + $str = $this->getName(); + } else { + $str = str_replace(self::$reservedCharsFrom, self::$reservedCharsTo, $this->getName()); + } + + $str .= '='; if ('' === (string) $this->getValue()) { $str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0'; diff --git a/src/Symfony/Component/HttpFoundation/Response.php b/src/Symfony/Component/HttpFoundation/Response.php index 0a594f6c12..9122b13c58 100644 --- a/src/Symfony/Component/HttpFoundation/Response.php +++ b/src/Symfony/Component/HttpFoundation/Response.php @@ -335,7 +335,7 @@ class Response // cookies foreach ($this->headers->getCookies() as $cookie) { - header('Set-Cookie: '.$cookie->getName().strstr($cookie, '='), false, $this->statusCode); + header('Set-Cookie: '.$cookie, false, $this->statusCode); } // status diff --git a/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php b/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php index 61a278e656..55287e082d 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php @@ -24,10 +24,9 @@ use Symfony\Component\HttpFoundation\Cookie; */ class CookieTest extends TestCase { - public function invalidNames() + public function namesWithSpecialCharacters() { return [ - [''], [',MyName'], [';MyName'], [' MyName'], @@ -40,12 +39,26 @@ class CookieTest extends TestCase } /** - * @dataProvider invalidNames + * @dataProvider namesWithSpecialCharacters */ - public function testInstantiationThrowsExceptionIfCookieNameContainsInvalidCharacters($name) + public function testInstantiationThrowsExceptionIfRawCookieNameContainsSpecialCharacters($name) { $this->expectException('InvalidArgumentException'); - Cookie::create($name); + Cookie::create($name, null, 0, null, null, null, false, true); + } + + /** + * @dataProvider namesWithSpecialCharacters + */ + public function testInstantiationSucceedNonRawCookieNameContainsSpecialCharacters($name) + { + $this->assertInstanceOf(Cookie::class, Cookie::create($name)); + } + + public function testInstantiationThrowsExceptionIfCookieNameIsEmpty() + { + $this->expectException('InvalidArgumentException'); + Cookie::create(''); } public function testInvalidExpiration() diff --git a/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/cookie_urlencode.expected b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/cookie_urlencode.expected index 14e44a398a..17a9efc669 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/cookie_urlencode.expected +++ b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/cookie_urlencode.expected @@ -4,7 +4,8 @@ Array [0] => Content-Type: text/plain; charset=utf-8 [1] => Cache-Control: no-cache, private [2] => Date: Sat, 12 Nov 1955 20:04:00 GMT - [3] => Set-Cookie: ?*():@&+$/%#[]=%3F%2A%28%29%3A%40%26%2B%24%2F%25%23%5B%5D; path=/ + [3] => Set-Cookie: %3D%2C%3B%20%09%0D%0A%0B%0C=%3D%2C%3B%20%09%0D%0A%0B%0C; path=/ [4] => Set-Cookie: ?*():@&+$/%#[]=%3F%2A%28%29%3A%40%26%2B%24%2F%25%23%5B%5D; path=/ + [5] => Set-Cookie: ?*():@&+$/%#[]=%3F%2A%28%29%3A%40%26%2B%24%2F%25%23%5B%5D; path=/ ) shutdown diff --git a/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/cookie_urlencode.php b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/cookie_urlencode.php index c0363b829d..9ffb0dfec8 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/cookie_urlencode.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/cookie_urlencode.php @@ -4,9 +4,12 @@ use Symfony\Component\HttpFoundation\Cookie; $r = require __DIR__.'/common.inc'; -$str = '?*():@&+$/%#[]'; +$str1 = "=,; \t\r\n\v\f"; +$r->headers->setCookie(new Cookie($str1, $str1, 0, '', null, false, false, false, null)); -$r->headers->setCookie(new Cookie($str, $str, 0, '', null, false, false, false, null)); +$str2 = '?*():@&+$/%#[]'; + +$r->headers->setCookie(new Cookie($str2, $str2, 0, '', null, false, false, false, null)); $r->sendHeaders(); -setcookie($str, $str, 0, '/'); +setcookie($str2, $str2, 0, '/'); diff --git a/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/invalid_cookie_name.php b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/invalid_cookie_name.php index 0afaaa8a57..3acf86039d 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/invalid_cookie_name.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/invalid_cookie_name.php @@ -5,7 +5,7 @@ use Symfony\Component\HttpFoundation\Cookie; $r = require __DIR__.'/common.inc'; try { - $r->headers->setCookie(Cookie::create('Hello + world', 'hodor')); + $r->headers->setCookie(new Cookie('Hello + world', 'hodor', 0, null, null, null, false, true)); } catch (\InvalidArgumentException $e) { echo $e->getMessage(); } diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/Storage/TokenStorage.php b/src/Symfony/Component/Security/Core/Authentication/Token/Storage/TokenStorage.php index f1f5391ba7..5c1a9f239c 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/Storage/TokenStorage.php +++ b/src/Symfony/Component/Security/Core/Authentication/Token/Storage/TokenStorage.php @@ -25,12 +25,18 @@ use Symfony\Contracts\Service\ResetInterface; class TokenStorage implements TokenStorageInterface, ResetInterface { private $token; + private $initializer; /** * {@inheritdoc} */ public function getToken() { + if ($initializer = $this->initializer) { + $this->initializer = null; + $initializer(); + } + return $this->token; } @@ -39,9 +45,15 @@ class TokenStorage implements TokenStorageInterface, ResetInterface */ public function setToken(TokenInterface $token = null) { + $this->initializer = null; $this->token = $token; } + public function setInitializer(?callable $initializer): void + { + $this->initializer = $initializer; + } + public function reset() { $this->setToken(null); diff --git a/src/Symfony/Component/Security/Core/Exception/LazyResponseException.php b/src/Symfony/Component/Security/Core/Exception/LazyResponseException.php new file mode 100644 index 0000000000..8edc248a04 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Exception/LazyResponseException.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +use Symfony\Component\HttpFoundation\Response; + +/** + * A signaling exception that wraps a lazily computed response. + * + * @author Nicolas Grekas+ */ +class LazyResponseException extends \Exception implements ExceptionInterface +{ + private $response; + + public function __construct(Response $response) + { + $this->response = $response; + } + + public function getResponse(): Response + { + return $this->response; + } +} diff --git a/src/Symfony/Component/Security/Http/Event/LazyResponseEvent.php b/src/Symfony/Component/Security/Http/Event/LazyResponseEvent.php new file mode 100644 index 0000000000..aa473bc0aa --- /dev/null +++ b/src/Symfony/Component/Security/Http/Event/LazyResponseEvent.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Event; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Security\Core\Exception\LazyResponseException; + +/** + * Wraps a lazily computed response in a signaling exception. + * + * @author Nicolas Grekas
+ */ +final class LazyResponseEvent extends RequestEvent +{ + private $event; + + public function __construct(parent $event) + { + $this->event = $event; + } + + /** + * {@inheritdoc} + */ + public function setResponse(Response $response) + { + $this->stopPropagation(); + $this->event->stopPropagation(); + + throw new LazyResponseException($response); + } + + /** + * {@inheritdoc} + */ + public function getKernel(): HttpKernelInterface + { + return $this->event->getKernel(); + } + + /** + * {@inheritdoc} + */ + public function getRequest(): Request + { + return $this->event->getRequest(); + } + + /** + * {@inheritdoc} + */ + public function getRequestType(): int + { + return $this->event->getRequestType(); + } + + /** + * {@inheritdoc} + */ + public function isMasterRequest(): bool + { + return $this->event->isMasterRequest(); + } +} diff --git a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php index 6d45922bc3..9162f56f65 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php @@ -26,6 +26,7 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Exception\AccountStatusException; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException; +use Symfony\Component\Security\Core\Exception\LazyResponseException; use Symfony\Component\Security\Core\Exception\LogoutException; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface; @@ -103,6 +104,12 @@ class ExceptionListener return; } + if ($exception instanceof LazyResponseException) { + $event->setResponse($exception->getResponse()); + + return; + } + if ($exception instanceof LogoutException) { $this->handleLogoutException($exception);