vendor/symfony/http-kernel/EventListener/AbstractSessionListener.php line 60

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\HttpKernel\EventListener;
  11. use Psr\Container\ContainerInterface;
  12. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  13. use Symfony\Component\HttpFoundation\Cookie;
  14. use Symfony\Component\HttpFoundation\Session\Session;
  15. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  16. use Symfony\Component\HttpFoundation\Session\SessionUtils;
  17. use Symfony\Component\HttpKernel\Event\RequestEvent;
  18. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  19. use Symfony\Component\HttpKernel\Exception\UnexpectedSessionUsageException;
  20. use Symfony\Component\HttpKernel\KernelEvents;
  21. use Symfony\Contracts\Service\ResetInterface;
  22. /**
  23.  * Sets the session onto the request on the "kernel.request" event and saves
  24.  * it on the "kernel.response" event.
  25.  *
  26.  * In addition, if the session has been started it overrides the Cache-Control
  27.  * header in such a way that all caching is disabled in that case.
  28.  * If you have a scenario where caching responses with session information in
  29.  * them makes sense, you can disable this behaviour by setting the header
  30.  * AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER on the response.
  31.  *
  32.  * @author Johannes M. Schmitt <schmittjoh@gmail.com>
  33.  * @author Tobias Schultze <http://tobion.de>
  34.  *
  35.  * @internal
  36.  */
  37. abstract class AbstractSessionListener implements EventSubscriberInterfaceResetInterface
  38. {
  39.     public const NO_AUTO_CACHE_CONTROL_HEADER 'Symfony-Session-NoAutoCacheControl';
  40.     protected $container;
  41.     private bool $debug;
  42.     /**
  43.      * @var array<string, mixed>
  44.      */
  45.     private $sessionOptions;
  46.     public function __construct(ContainerInterface $container nullbool $debug false, array $sessionOptions = [])
  47.     {
  48.         $this->container $container;
  49.         $this->debug $debug;
  50.         $this->sessionOptions $sessionOptions;
  51.     }
  52.     public function onKernelRequest(RequestEvent $event)
  53.     {
  54.         if (!$event->isMainRequest()) {
  55.             return;
  56.         }
  57.         $request $event->getRequest();
  58.         if (!$request->hasSession()) {
  59.             // This variable prevents calling `$this->getSession()` twice in case the Request (and the below factory) is cloned
  60.             $sess null;
  61.             $request->setSessionFactory(function () use (&$sess$request) {
  62.                 if (!$sess) {
  63.                     $sess $this->getSession();
  64.                 }
  65.                 /*
  66.                  * For supporting sessions in php runtime with runners like roadrunner or swoole the session
  67.                  * cookie need read from the cookie bag and set on the session storage.
  68.                  */
  69.                 if ($sess && !$sess->isStarted()) {
  70.                     $sessionId $request->cookies->get($sess->getName(), '');
  71.                     $sess->setId($sessionId);
  72.                 }
  73.                 return $sess;
  74.             });
  75.         }
  76.     }
  77.     public function onKernelResponse(ResponseEvent $event)
  78.     {
  79.         if (!$event->isMainRequest()) {
  80.             return;
  81.         }
  82.         $response $event->getResponse();
  83.         $autoCacheControl = !$response->headers->has(self::NO_AUTO_CACHE_CONTROL_HEADER);
  84.         // Always remove the internal header if present
  85.         $response->headers->remove(self::NO_AUTO_CACHE_CONTROL_HEADER);
  86.         if (!$event->getRequest()->hasSession(true)) {
  87.             return;
  88.         }
  89.         $session $event->getRequest()->getSession();
  90.         if ($session->isStarted()) {
  91.             /*
  92.              * Saves the session, in case it is still open, before sending the response/headers.
  93.              *
  94.              * This ensures several things in case the developer did not save the session explicitly:
  95.              *
  96.              *  * If a session save handler without locking is used, it ensures the data is available
  97.              *    on the next request, e.g. after a redirect. PHPs auto-save at script end via
  98.              *    session_register_shutdown is executed after fastcgi_finish_request. So in this case
  99.              *    the data could be missing the next request because it might not be saved the moment
  100.              *    the new request is processed.
  101.              *  * A locking save handler (e.g. the native 'files') circumvents concurrency problems like
  102.              *    the one above. But by saving the session before long-running things in the terminate event,
  103.              *    we ensure the session is not blocked longer than needed.
  104.              *  * When regenerating the session ID no locking is involved in PHPs session design. See
  105.              *    https://bugs.php.net/61470 for a discussion. So in this case, the session must
  106.              *    be saved anyway before sending the headers with the new session ID. Otherwise session
  107.              *    data could get lost again for concurrent requests with the new ID. One result could be
  108.              *    that you get logged out after just logging in.
  109.              *
  110.              * This listener should be executed as one of the last listeners, so that previous listeners
  111.              * can still operate on the open session. This prevents the overhead of restarting it.
  112.              * Listeners after closing the session can still work with the session as usual because
  113.              * Symfonys session implementation starts the session on demand. So writing to it after
  114.              * it is saved will just restart it.
  115.              */
  116.             $session->save();
  117.             /*
  118.              * For supporting sessions in php runtime with runners like roadrunner or swoole the session
  119.              * cookie need to be written on the response object and should not be written by PHP itself.
  120.              */
  121.             $sessionName $session->getName();
  122.             $sessionId $session->getId();
  123.             $sessionCookiePath $this->sessionOptions['cookie_path'] ?? '/';
  124.             $sessionCookieDomain $this->sessionOptions['cookie_domain'] ?? null;
  125.             $sessionCookieSecure $this->sessionOptions['cookie_secure'] ?? false;
  126.             $sessionCookieHttpOnly $this->sessionOptions['cookie_httponly'] ?? true;
  127.             $sessionCookieSameSite $this->sessionOptions['cookie_samesite'] ?? Cookie::SAMESITE_LAX;
  128.             SessionUtils::popSessionCookie($sessionName$sessionId);
  129.             $request $event->getRequest();
  130.             $requestSessionCookieId $request->cookies->get($sessionName);
  131.             if ($requestSessionCookieId && ($session instanceof Session $session->isEmpty() : empty($session->all()))) {
  132.                 $response->headers->clearCookie(
  133.                     $sessionName,
  134.                     $sessionCookiePath,
  135.                     $sessionCookieDomain,
  136.                     $sessionCookieSecure,
  137.                     $sessionCookieHttpOnly,
  138.                     $sessionCookieSameSite
  139.                 );
  140.             } elseif ($sessionId !== $requestSessionCookieId) {
  141.                 $expire 0;
  142.                 $lifetime $this->sessionOptions['cookie_lifetime'] ?? null;
  143.                 if ($lifetime) {
  144.                     $expire time() + $lifetime;
  145.                 }
  146.                 $response->headers->setCookie(
  147.                     Cookie::create(
  148.                         $sessionName,
  149.                         $sessionId,
  150.                         $expire,
  151.                         $sessionCookiePath,
  152.                         $sessionCookieDomain,
  153.                         $sessionCookieSecure,
  154.                         $sessionCookieHttpOnly,
  155.                         false,
  156.                         $sessionCookieSameSite
  157.                     )
  158.                 );
  159.             }
  160.         }
  161.         if ($session instanceof Session === $session->getUsageIndex() : !$session->isStarted()) {
  162.             return;
  163.         }
  164.         if ($autoCacheControl) {
  165.             $response
  166.                 ->setExpires(new \DateTime())
  167.                 ->setPrivate()
  168.                 ->setMaxAge(0)
  169.                 ->headers->addCacheControlDirective('must-revalidate');
  170.         }
  171.         if (!$event->getRequest()->attributes->get('_stateless'false)) {
  172.             return;
  173.         }
  174.         if ($this->debug) {
  175.             throw new UnexpectedSessionUsageException('Session was used while the request was declared stateless.');
  176.         }
  177.         if ($this->container->has('logger')) {
  178.             $this->container->get('logger')->warning('Session was used while the request was declared stateless.');
  179.         }
  180.     }
  181.     public function onSessionUsage(): void
  182.     {
  183.         if (!$this->debug) {
  184.             return;
  185.         }
  186.         if ($this->container && $this->container->has('session_collector')) {
  187.             $this->container->get('session_collector')();
  188.         }
  189.         if (!$requestStack $this->container && $this->container->has('request_stack') ? $this->container->get('request_stack') : null) {
  190.             return;
  191.         }
  192.         $stateless false;
  193.         $clonedRequestStack = clone $requestStack;
  194.         while (null !== ($request $clonedRequestStack->pop()) && !$stateless) {
  195.             $stateless $request->attributes->get('_stateless');
  196.         }
  197.         if (!$stateless) {
  198.             return;
  199.         }
  200.         if (!$session $requestStack->getCurrentRequest()->getSession()) {
  201.             return;
  202.         }
  203.         if ($session->isStarted()) {
  204.             $session->save();
  205.         }
  206.         throw new UnexpectedSessionUsageException('Session was used while the request was declared stateless.');
  207.     }
  208.     public static function getSubscribedEvents(): array
  209.     {
  210.         return [
  211.             KernelEvents::REQUEST => ['onKernelRequest'128],
  212.             // low priority to come after regular response listeners, but higher than StreamedResponseListener
  213.             KernelEvents::RESPONSE => ['onKernelResponse', -1000],
  214.         ];
  215.     }
  216.     public function reset(): void
  217.     {
  218.         if (\PHP_SESSION_ACTIVE === session_status()) {
  219.             session_abort();
  220.         }
  221.         session_unset();
  222.         $_SESSION = [];
  223.         if (!headers_sent()) { // session id can only be reset when no headers were so we check for headers_sent first
  224.             session_id('');
  225.         }
  226.     }
  227.     /**
  228.      * Gets the session object.
  229.      */
  230.     abstract protected function getSession(): ?SessionInterface;
  231. }