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