+ */ +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 269827e2df5f2..cf0e1150aff9a 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 @@ public function profileAction() 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 06260c1bed042..0303f1b4eeff9 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php @@ -129,6 +129,16 @@ public function testInvalidIpsInAccessControl() $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 4e2ac1e11b9d6..ad8beee94c2e0 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/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 4bd0816938662..77bd4a0cfb76f 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -24,7 +24,7 @@ "symfony/security-core": "^4.4", "symfony/security-csrf": "^4.2|^5.0", "symfony/security-guard": "^4.2|^5.0", - "symfony/security-http": "^4.3" + "symfony/security-http": "^4.4" }, "require-dev": { "symfony/asset": "^3.4|^4.0|^5.0", 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 8a02802d9c98f..bf491797aa25d 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 @@ class TokenStorage implements TokenStorageInterface, ResetInterface { private $token; + private $initializer; /** * {@inheritdoc} */ public function getToken() { + if ($initializer = $this->initializer) { + $this->initializer = null; + $initializer(); + } + return $this->token; } @@ -43,9 +49,15 @@ public function setToken(TokenInterface $token = null) @trigger_error(sprintf('Not implementing the "%s::getRoleNames()" method in "%s" is deprecated since Symfony 4.3.', TokenInterface::class, \get_class($token)), E_USER_DEPRECATED); } + $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 0000000000000..8edc248a0415d --- /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 0000000000000..aa473bc0aa2da --- /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 c97a051024f6e..549543e3efb17 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\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 @@ public function onKernelException(GetResponseForExceptionEvent $event) return; } + if ($exception instanceof LazyResponseException) { + $event->setResponse($exception->getResponse()); + + return; + } + if ($exception instanceof LogoutException) { $this->handleLogoutException($exception);
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: