[HttpKernel] Make session-related services extra-lazy

This commit is contained in:
Nicolas Grekas 2018-01-06 08:55:13 +01:00
parent 6e6ac9eaee
commit 5f535581b9
9 changed files with 98 additions and 45 deletions

View File

@ -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 -->

View File

@ -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.
*

View File

@ -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),
);
}

View File

@ -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
{

View File

@ -22,8 +22,6 @@ use Psr\Container\ContainerInterface;
*/
class SessionListener extends AbstractSessionListener
{
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;

View File

@ -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;
}

View File

@ -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()

View File

@ -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);
}
}

View File

@ -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));