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

  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. abstract class AbstractSessionListener implements EventSubscriberInterfaceResetInterface
  36. {
  37.     public const NO_AUTO_CACHE_CONTROL_HEADER 'Symfony-Session-NoAutoCacheControl';
  38.     /**
  39.      * @internal
  40.      */
  41.     protected ?ContainerInterface $container;
  42.     private bool $debug;
  43.     /**
  44.      * @var array<string, mixed>
  45.      */
  46.     private array $sessionOptions;
  47.     /**
  48.      * @internal
  49.      */
  50.     public function __construct(?ContainerInterface $container nullbool $debug false, array $sessionOptions = [])
  51.     {
  52.         $this->container $container;
  53.         $this->debug $debug;
  54.         $this->sessionOptions $sessionOptions;
  55.     }
  56.     /**
  57.      * @internal
  58.      */
  59.     public function onKernelRequest(RequestEvent $event): void
  60.     {
  61.         if (!$event->isMainRequest()) {
  62.             return;
  63.         }
  64.         $request $event->getRequest();
  65.         if (!$request->hasSession()) {
  66.             $request->setSessionFactory(function () use ($request) {
  67.                 // Prevent calling `$this->getSession()` twice in case the Request (and the below factory) is cloned
  68.                 static $sess;
  69.                 if (!$sess) {
  70.                     $sess $this->getSession();
  71.                     $request->setSession($sess);
  72.                     /*
  73.                      * For supporting sessions in php runtime with runners like roadrunner or swoole, the session
  74.                      * cookie needs to be read from the cookie bag and set on the session storage.
  75.                      *
  76.                      * Do not set it when a native php session is active.
  77.                      */
  78.                     if ($sess && !$sess->isStarted() && \PHP_SESSION_ACTIVE !== session_status()) {
  79.                         $sessionId $sess->getId() ?: $request->cookies->get($sess->getName(), '');
  80.                         $sess->setId($sessionId);
  81.                     }
  82.                 }
  83.                 return $sess;
  84.             });
  85.         }
  86.     }
  87.     /**
  88.      * @internal
  89.      */
  90.     public function onKernelResponse(ResponseEvent $event): void
  91.     {
  92.         if (!$event->isMainRequest() || (!$this->container->has('initialized_session') && !$event->getRequest()->hasSession())) {
  93.             return;
  94.         }
  95.         $response $event->getResponse();
  96.         $autoCacheControl = !$response->headers->has(self::NO_AUTO_CACHE_CONTROL_HEADER);
  97.         // Always remove the internal header if present
  98.         $response->headers->remove(self::NO_AUTO_CACHE_CONTROL_HEADER);
  99.         if (!$event->getRequest()->hasSession(true)) {
  100.             return;
  101.         }
  102.         $session $event->getRequest()->getSession();
  103.         if ($session->isStarted()) {
  104.             /*
  105.              * Saves the session, in case it is still open, before sending the response/headers.
  106.              *
  107.              * This ensures several things in case the developer did not save the session explicitly:
  108.              *
  109.              *  * If a session save handler without locking is used, it ensures the data is available
  110.              *    on the next request, e.g. after a redirect. PHPs auto-save at script end via
  111.              *    session_register_shutdown is executed after fastcgi_finish_request. So in this case
  112.              *    the data could be missing the next request because it might not be saved the moment
  113.              *    the new request is processed.
  114.              *  * A locking save handler (e.g. the native 'files') circumvents concurrency problems like
  115.              *    the one above. But by saving the session before long-running things in the terminate event,
  116.              *    we ensure the session is not blocked longer than needed.
  117.              *  * When regenerating the session ID no locking is involved in PHPs session design. See
  118.              *    https://bugs.php.net/61470 for a discussion. So in this case, the session must
  119.              *    be saved anyway before sending the headers with the new session ID. Otherwise session
  120.              *    data could get lost again for concurrent requests with the new ID. One result could be
  121.              *    that you get logged out after just logging in.
  122.              *
  123.              * This listener should be executed as one of the last listeners, so that previous listeners
  124.              * can still operate on the open session. This prevents the overhead of restarting it.
  125.              * Listeners after closing the session can still work with the session as usual because
  126.              * Symfonys session implementation starts the session on demand. So writing to it after
  127.              * it is saved will just restart it.
  128.              */
  129.             $session->save();
  130.             /*
  131.              * For supporting sessions in php runtime with runners like roadrunner or swoole the session
  132.              * cookie need to be written on the response object and should not be written by PHP itself.
  133.              */
  134.             $sessionName $session->getName();
  135.             $sessionId $session->getId();
  136.             $sessionOptions $this->getSessionOptions($this->sessionOptions);
  137.             $sessionCookiePath $sessionOptions['cookie_path'] ?? '/';
  138.             $sessionCookieDomain $sessionOptions['cookie_domain'] ?? null;
  139.             $sessionCookieSecure $sessionOptions['cookie_secure'] ?? false;
  140.             $sessionCookieHttpOnly $sessionOptions['cookie_httponly'] ?? true;
  141.             $sessionCookieSameSite $sessionOptions['cookie_samesite'] ?? Cookie::SAMESITE_LAX;
  142.             $sessionUseCookies $sessionOptions['use_cookies'] ?? true;
  143.             SessionUtils::popSessionCookie($sessionName$sessionId);
  144.             if ($sessionUseCookies) {
  145.                 $request $event->getRequest();
  146.                 $requestSessionCookieId $request->cookies->get($sessionName);
  147.                 $isSessionEmpty = ($session instanceof Session $session->isEmpty() : !$session->all()) && empty($_SESSION); // checking $_SESSION to keep compatibility with native sessions
  148.                 if ($requestSessionCookieId && $isSessionEmpty) {
  149.                     // PHP internally sets the session cookie value to "deleted" when setcookie() is called with empty string $value argument
  150.                     // which happens in \Symfony\Component\HttpFoundation\Session\Storage\Handler\AbstractSessionHandler::destroy
  151.                     // when the session gets invalidated (for example on logout) so we must handle this case here too
  152.                     // otherwise we would send two Set-Cookie headers back with the response
  153.                     SessionUtils::popSessionCookie($sessionName'deleted');
  154.                     $response->headers->clearCookie(
  155.                         $sessionName,
  156.                         $sessionCookiePath,
  157.                         $sessionCookieDomain,
  158.                         $sessionCookieSecure,
  159.                         $sessionCookieHttpOnly,
  160.                         $sessionCookieSameSite
  161.                     );
  162.                 } elseif ($sessionId !== $requestSessionCookieId && !$isSessionEmpty) {
  163.                     $expire 0;
  164.                     $lifetime $sessionOptions['cookie_lifetime'] ?? null;
  165.                     if ($lifetime) {
  166.                         $expire time() + $lifetime;
  167.                     }
  168.                     $response->headers->setCookie(
  169.                         Cookie::create(
  170.                             $sessionName,
  171.                             $sessionId,
  172.                             $expire,
  173.                             $sessionCookiePath,
  174.                             $sessionCookieDomain,
  175.                             $sessionCookieSecure,
  176.                             $sessionCookieHttpOnly,
  177.                             false,
  178.                             $sessionCookieSameSite
  179.                         )
  180.                     );
  181.                 }
  182.             }
  183.         }
  184.         if ($session instanceof Session === $session->getUsageIndex() : !$session->isStarted()) {
  185.             return;
  186.         }
  187.         if ($autoCacheControl) {
  188.             $maxAge $response->headers->hasCacheControlDirective('public') ? : (int) $response->getMaxAge();
  189.             $response
  190.                 ->setExpires(new \DateTimeImmutable('+'.$maxAge.' seconds'))
  191.                 ->setPrivate()
  192.                 ->setMaxAge($maxAge)
  193.                 ->headers->addCacheControlDirective('must-revalidate');
  194.         }
  195.         if (!$event->getRequest()->attributes->get('_stateless'false)) {
  196.             return;
  197.         }
  198.         if ($this->debug) {
  199.             throw new UnexpectedSessionUsageException('Session was used while the request was declared stateless.');
  200.         }
  201.         if ($this->container->has('logger')) {
  202.             $this->container->get('logger')->warning('Session was used while the request was declared stateless.');
  203.         }
  204.     }
  205.     /**
  206.      * @internal
  207.      */
  208.     public function onSessionUsage(): void
  209.     {
  210.         if (!$this->debug) {
  211.             return;
  212.         }
  213.         if ($this->container?->has('session_collector')) {
  214.             $this->container->get('session_collector')();
  215.         }
  216.         if (!$requestStack $this->container?->has('request_stack') ? $this->container->get('request_stack') : null) {
  217.             return;
  218.         }
  219.         $stateless false;
  220.         $clonedRequestStack = clone $requestStack;
  221.         while (null !== ($request $clonedRequestStack->pop()) && !$stateless) {
  222.             $stateless $request->attributes->get('_stateless');
  223.         }
  224.         if (!$stateless) {
  225.             return;
  226.         }
  227.         if (!$session $requestStack->getCurrentRequest()->getSession()) {
  228.             return;
  229.         }
  230.         if ($session->isStarted()) {
  231.             $session->save();
  232.         }
  233.         throw new UnexpectedSessionUsageException('Session was used while the request was declared stateless.');
  234.     }
  235.     /**
  236.      * @internal
  237.      */
  238.     public static function getSubscribedEvents(): array
  239.     {
  240.         return [
  241.             KernelEvents::REQUEST => ['onKernelRequest'128],
  242.             // low priority to come after regular response listeners
  243.             KernelEvents::RESPONSE => ['onKernelResponse', -1000],
  244.         ];
  245.     }
  246.     /**
  247.      * @internal
  248.      */
  249.     public function reset(): void
  250.     {
  251.         if (\PHP_SESSION_ACTIVE === session_status()) {
  252.             session_abort();
  253.         }
  254.         session_unset();
  255.         $_SESSION = [];
  256.         if (!headers_sent()) { // session id can only be reset when no headers were so we check for headers_sent first
  257.             session_id('');
  258.         }
  259.     }
  260.     /**
  261.      * Gets the session object.
  262.      *
  263.      * @internal
  264.      */
  265.     abstract protected function getSession(): ?SessionInterface;
  266.     private function getSessionOptions(array $sessionOptions): array
  267.     {
  268.         $mergedSessionOptions = [];
  269.         foreach (session_get_cookie_params() as $key => $value) {
  270.             $mergedSessionOptions['cookie_'.$key] = $value;
  271.         }
  272.         foreach ($sessionOptions as $key => $value) {
  273.             // do the same logic as in the NativeSessionStorage
  274.             if ('cookie_secure' === $key && 'auto' === $value) {
  275.                 continue;
  276.             }
  277.             $mergedSessionOptions[$key] = $value;
  278.         }
  279.         return $mergedSessionOptions;
  280.     }
  281. }