[HttpKernel] Make session-related services extra-lazy
This commit is contained in:
parent
6e6ac9eaee
commit
5f535581b9
@ -63,13 +63,14 @@
|
||||
<tag name="container.service_locator" />
|
||||
<argument type="collection">
|
||||
<argument key="session" type="service" id="session" on-invalid="ignore" />
|
||||
<argument key="initialized_session" type="service" id="session" on-invalid="ignore_uninitialized" />
|
||||
</argument>
|
||||
</service>
|
||||
</argument>
|
||||
</service>
|
||||
|
||||
<service id="session.save_listener" class="Symfony\Component\HttpKernel\EventListener\SaveSessionListener">
|
||||
<tag name="kernel.event_subscriber" />
|
||||
<deprecated>The "%service_id%" service is deprecated since Symfony 4.1 and will be removed in 5.0. Use the "session_listener" service instead.</deprecated>
|
||||
</service>
|
||||
|
||||
<!-- for BC -->
|
||||
|
@ -720,7 +720,12 @@ class Request
|
||||
*/
|
||||
public function getSession()
|
||||
{
|
||||
return $this->session;
|
||||
$session = $this->session;
|
||||
if (!$session instanceof SessionInterface && null !== $session) {
|
||||
$this->setSession($session = $session());
|
||||
}
|
||||
|
||||
return $session;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -732,7 +737,7 @@ class Request
|
||||
public function hasPreviousSession()
|
||||
{
|
||||
// the check for $this->session avoids malicious users trying to fake a session cookie with proper name
|
||||
return $this->hasSession() && $this->cookies->has($this->session->getName());
|
||||
return $this->hasSession() && $this->cookies->has($this->getSession()->getName());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -759,6 +764,14 @@ class Request
|
||||
$this->session = $session;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setSessionFactory(callable $factory)
|
||||
{
|
||||
$this->session = $factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the client IP addresses.
|
||||
*
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\Component\HttpKernel\EventListener;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
|
||||
@ -22,9 +23,17 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
* Sets the session in the request.
|
||||
*
|
||||
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||
* @author Tobias Schultze <http://tobion.de>
|
||||
*/
|
||||
abstract class AbstractSessionListener implements EventSubscriberInterface
|
||||
{
|
||||
protected $container;
|
||||
|
||||
public function __construct(ContainerInterface $container = null)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public function onKernelRequest(GetResponseEvent $event)
|
||||
{
|
||||
if (!$event->isMasterRequest()) {
|
||||
@ -32,12 +41,13 @@ abstract class AbstractSessionListener implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
$request = $event->getRequest();
|
||||
$session = $this->getSession();
|
||||
if (null === $session || $request->hasSession()) {
|
||||
return;
|
||||
if ($request->hasSession()) {
|
||||
// no-op
|
||||
} elseif (method_exists($request, 'setSessionFactory')) {
|
||||
$request->setSessionFactory(function () { return $this->getSession(); });
|
||||
} elseif ($session = $this->getSession()) {
|
||||
$request->setSession($session);
|
||||
}
|
||||
|
||||
$request->setSession($session);
|
||||
}
|
||||
|
||||
public function onKernelResponse(FilterResponseEvent $event)
|
||||
@ -46,7 +56,7 @@ abstract class AbstractSessionListener implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$session = $event->getRequest()->getSession()) {
|
||||
if (!$session = $this->container && $this->container->has('initialized_session') ? $this->container->get('initialized_session') : $event->getRequest()->getSession()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -56,13 +66,42 @@ abstract class AbstractSessionListener implements EventSubscriberInterface
|
||||
->setMaxAge(0)
|
||||
->headers->addCacheControlDirective('must-revalidate');
|
||||
}
|
||||
|
||||
if ($session->isStarted()) {
|
||||
/*
|
||||
* Saves the session, in case it is still open, before sending the response/headers.
|
||||
*
|
||||
* This ensures several things in case the developer did not save the session explicitly:
|
||||
*
|
||||
* * If a session save handler without locking is used, it ensures the data is available
|
||||
* on the next request, e.g. after a redirect. PHPs auto-save at script end via
|
||||
* session_register_shutdown is executed after fastcgi_finish_request. So in this case
|
||||
* the data could be missing the next request because it might not be saved the moment
|
||||
* the new request is processed.
|
||||
* * A locking save handler (e.g. the native 'files') circumvents concurrency problems like
|
||||
* the one above. But by saving the session before long-running things in the terminate event,
|
||||
* we ensure the session is not blocked longer than needed.
|
||||
* * When regenerating the session ID no locking is involved in PHPs session design. See
|
||||
* https://bugs.php.net/bug.php?id=61470 for a discussion. So in this case, the session must
|
||||
* be saved anyway before sending the headers with the new session ID. Otherwise session
|
||||
* data could get lost again for concurrent requests with the new ID. One result could be
|
||||
* that you get logged out after just logging in.
|
||||
*
|
||||
* This listener should be executed as one of the last listeners, so that previous listeners
|
||||
* can still operate on the open session. This prevents the overhead of restarting it.
|
||||
* Listeners after closing the session can still work with the session as usual because
|
||||
* Symfonys session implementation starts the session on demand. So writing to it after
|
||||
* it is saved will just restart it.
|
||||
*/
|
||||
$session->save();
|
||||
}
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return array(
|
||||
KernelEvents::REQUEST => array('onKernelRequest', 128),
|
||||
// low priority to come after regular response listeners, same as SaveSessionListener
|
||||
// low priority to come after regular response listeners, but higher than StreamedResponseListener
|
||||
KernelEvents::RESPONSE => array('onKernelResponse', -1000),
|
||||
);
|
||||
}
|
||||
|
@ -11,36 +11,16 @@
|
||||
|
||||
namespace Symfony\Component\HttpKernel\EventListener;
|
||||
|
||||
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1 and will be removed in 5.0. Use AbstractSessionListener instead.', SaveSessionListener::class), E_USER_DEPRECATED);
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
/**
|
||||
* Saves the session, in case it is still open, before sending the response/headers.
|
||||
*
|
||||
* This ensures several things in case the developer did not save the session explicitly:
|
||||
*
|
||||
* * If a session save handler without locking is used, it ensures the data is available
|
||||
* on the next request, e.g. after a redirect. PHPs auto-save at script end via
|
||||
* session_register_shutdown is executed after fastcgi_finish_request. So in this case
|
||||
* the data could be missing the next request because it might not be saved the moment
|
||||
* the new request is processed.
|
||||
* * A locking save handler (e.g. the native 'files') circumvents concurrency problems like
|
||||
* the one above. But by saving the session before long-running things in the terminate event,
|
||||
* we ensure the session is not blocked longer than needed.
|
||||
* * When regenerating the session ID no locking is involved in PHPs session design. See
|
||||
* https://bugs.php.net/bug.php?id=61470 for a discussion. So in this case, the session must
|
||||
* be saved anyway before sending the headers with the new session ID. Otherwise session
|
||||
* data could get lost again for concurrent requests with the new ID. One result could be
|
||||
* that you get logged out after just logging in.
|
||||
*
|
||||
* This listener should be executed as one of the last listeners, so that previous listeners
|
||||
* can still operate on the open session. This prevents the overhead of restarting it.
|
||||
* Listeners after closing the session can still work with the session as usual because
|
||||
* Symfonys session implementation starts the session on demand. So writing to it after
|
||||
* it is saved will just restart it.
|
||||
*
|
||||
* @author Tobias Schultze <http://tobion.de>
|
||||
*
|
||||
* @deprecated since Symfony 4.1, to be removed in 5.0. Use AbstractSessionListener instead.
|
||||
*/
|
||||
class SaveSessionListener implements EventSubscriberInterface
|
||||
{
|
||||
|
@ -22,8 +22,6 @@ use Psr\Container\ContainerInterface;
|
||||
*/
|
||||
class SessionListener extends AbstractSessionListener
|
||||
{
|
||||
private $container;
|
||||
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
|
@ -124,9 +124,12 @@ class InlineFragmentRenderer extends RoutableFragmentRenderer
|
||||
$subRequest->headers->set('Surrogate-Capability', $request->headers->get('Surrogate-Capability'));
|
||||
}
|
||||
|
||||
if ($session = $request->getSession()) {
|
||||
$subRequest->setSession($session);
|
||||
static $setSession;
|
||||
|
||||
if (null === $setSession) {
|
||||
$setSession = \Closure::bind(function ($subRequest, $request) { $subRequest->session = $request->session; }, null, Request::class);
|
||||
}
|
||||
$setSession($subRequest, $request);
|
||||
|
||||
return $subRequest;
|
||||
}
|
||||
|
@ -19,6 +19,9 @@ use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
|
||||
use Symfony\Component\HttpKernel\EventListener\SaveSessionListener;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
/**
|
||||
* @group legacy
|
||||
*/
|
||||
class SaveSessionListenerTest extends TestCase
|
||||
{
|
||||
public function testOnlyTriggeredOnMasterRequest()
|
||||
|
@ -13,6 +13,7 @@ namespace Symfony\Component\HttpKernel\Tests\EventListener;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\DependencyInjection\Container;
|
||||
use Symfony\Component\DependencyInjection\ServiceLocator;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
@ -58,22 +59,33 @@ class SessionListenerTest extends TestCase
|
||||
public function testResponseIsPrivate()
|
||||
{
|
||||
$session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock();
|
||||
$session->expects($this->once())->method('isStarted')->willReturn(false);
|
||||
$session->expects($this->exactly(2))->method('isStarted')->willReturn(false);
|
||||
$session->expects($this->once())->method('hasBeenStarted')->willReturn(true);
|
||||
|
||||
$container = new Container();
|
||||
$container->set('session', $session);
|
||||
$container->set('initialized_session', $session);
|
||||
|
||||
$listener = new SessionListener($container);
|
||||
$kernel = $this->getMockBuilder(HttpKernelInterface::class)->disableOriginalConstructor()->getMock();
|
||||
|
||||
$request = new Request();
|
||||
$response = new Response();
|
||||
$listener->onKernelRequest(new GetResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST));
|
||||
$listener->onKernelResponse(new FilterResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response));
|
||||
$listener->onKernelResponse(new FilterResponseEvent($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, $response));
|
||||
|
||||
$this->assertTrue($response->headers->hasCacheControlDirective('private'));
|
||||
$this->assertTrue($response->headers->hasCacheControlDirective('must-revalidate'));
|
||||
$this->assertSame('0', $response->headers->getCacheControlDirective('max-age'));
|
||||
}
|
||||
|
||||
public function testUninitilizedSession()
|
||||
{
|
||||
$event = $this->getMockBuilder(FilterResponseEvent::class)->disableOriginalConstructor()->getMock();
|
||||
$event->expects($this->once())->method('isMasterRequest')->willReturn(true);
|
||||
|
||||
$container = new ServiceLocator(array(
|
||||
'initialized_session' => function () {},
|
||||
));
|
||||
|
||||
$listener = new SessionListener($container);
|
||||
$listener->onKernelResponse($event);
|
||||
}
|
||||
}
|
||||
|
@ -77,9 +77,13 @@ class HttpUtils
|
||||
public function createRequest(Request $request, $path)
|
||||
{
|
||||
$newRequest = Request::create($this->generateUri($request, $path), 'get', array(), $request->cookies->all(), array(), $request->server->all());
|
||||
if ($request->hasSession()) {
|
||||
$newRequest->setSession($request->getSession());
|
||||
|
||||
static $setSession;
|
||||
|
||||
if (null === $setSession) {
|
||||
$setSession = \Closure::bind(function ($newRequest, $request) { $newRequest->session = $request->session; }, null, Request::class);
|
||||
}
|
||||
$setSession($newRequest, $request);
|
||||
|
||||
if ($request->attributes->has(Security::AUTHENTICATION_ERROR)) {
|
||||
$newRequest->attributes->set(Security::AUTHENTICATION_ERROR, $request->attributes->get(Security::AUTHENTICATION_ERROR));
|
||||
|
Reference in New Issue
Block a user