vendor/contao/core-bundle/src/EventListener/CsrfTokenCookieSubscriber.php line 50

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4.  * This file is part of Contao.
  5.  *
  6.  * (c) Leo Feyer
  7.  *
  8.  * @license LGPL-3.0-or-later
  9.  */
  10. namespace Contao\CoreBundle\EventListener;
  11. use Contao\CoreBundle\Csrf\MemoryTokenStorage;
  12. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  13. use Symfony\Component\HttpFoundation\Cookie;
  14. use Symfony\Component\HttpFoundation\ParameterBag;
  15. use Symfony\Component\HttpFoundation\Request;
  16. use Symfony\Component\HttpFoundation\Response;
  17. use Symfony\Component\HttpFoundation\ResponseHeaderBag;
  18. use Symfony\Component\HttpKernel\Event\RequestEvent;
  19. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  20. use Symfony\Component\HttpKernel\KernelEvents;
  21. /**
  22.  * @internal
  23.  */
  24. class CsrfTokenCookieSubscriber implements EventSubscriberInterface
  25. {
  26.     /**
  27.      * @var MemoryTokenStorage
  28.      */
  29.     private $tokenStorage;
  30.     /**
  31.      * @var string
  32.      */
  33.     private $cookiePrefix;
  34.     public function __construct(MemoryTokenStorage $tokenStoragestring $cookiePrefix 'csrf_')
  35.     {
  36.         $this->tokenStorage $tokenStorage;
  37.         $this->cookiePrefix $cookiePrefix;
  38.     }
  39.     /**
  40.      * Reads the cookies from the request and injects them into the storage.
  41.      */
  42.     public function onKernelRequest(RequestEvent $event): void
  43.     {
  44.         if (!$event->isMasterRequest()) {
  45.             return;
  46.         }
  47.         $this->tokenStorage->initialize($this->getTokensFromCookies($event->getRequest()->cookies));
  48.     }
  49.     /**
  50.      * Adds the token cookies to the response.
  51.      */
  52.     public function onKernelResponse(ResponseEvent $event): void
  53.     {
  54.         if (!$event->isMasterRequest()) {
  55.             return;
  56.         }
  57.         $request $event->getRequest();
  58.         $response $event->getResponse();
  59.         if ($this->requiresCsrf($request$response)) {
  60.             $this->setCookies($request$response);
  61.         } elseif ($response->isSuccessful()) {
  62.             // Only delete the CSRF token cookie if the response is successful (#2252)
  63.             $this->removeCookies($request$response);
  64.             $this->replaceTokenOccurrences($response);
  65.         }
  66.     }
  67.     public static function getSubscribedEvents(): array
  68.     {
  69.         return [
  70.             // The priority must be higher than the one of the Symfony route listener (defaults to 32)
  71.             KernelEvents::REQUEST => ['onKernelRequest'36],
  72.             // The priority must be higher than the one of the make-response-private listener (defaults to -896)
  73.             KernelEvents::RESPONSE => ['onKernelResponse', -832],
  74.         ];
  75.     }
  76.     private function requiresCsrf(Request $requestResponse $response): bool
  77.     {
  78.         foreach ($request->cookies as $key => $value) {
  79.             if (!$this->isCsrfCookie($key$value)) {
  80.                 return true;
  81.             }
  82.         }
  83.         if (\count($response->headers->getCookies(ResponseHeaderBag::COOKIES_ARRAY))) {
  84.             return true;
  85.         }
  86.         if ($request->getUserInfo()) {
  87.             return true;
  88.         }
  89.         if ($request->hasSession() && $request->getSession()->isStarted()) {
  90.             return true;
  91.         }
  92.         return false;
  93.     }
  94.     private function setCookies(Request $requestResponse $response): void
  95.     {
  96.         $isSecure $request->isSecure();
  97.         $basePath $request->getBasePath() ?: '/';
  98.         foreach ($this->tokenStorage->getUsedTokens() as $key => $value) {
  99.             $cookieKey $this->cookiePrefix.$key;
  100.             // The cookie already exists
  101.             if ($request->cookies->has($cookieKey) && $value === $request->cookies->get($cookieKey)) {
  102.                 continue;
  103.             }
  104.             $expires null === $value 0;
  105.             $response->headers->setCookie(
  106.                 new Cookie($cookieKey$value$expires$basePathnull$isSecuretruefalseCookie::SAMESITE_LAX)
  107.             );
  108.         }
  109.     }
  110.     private function replaceTokenOccurrences(Response $response): void
  111.     {
  112.         // Return if the response is not an HTML document
  113.         if (false === stripos((string) $response->headers->get('Content-Type'), 'text/html')) {
  114.             return;
  115.         }
  116.         $content $response->getContent();
  117.         $tokens $this->tokenStorage->getUsedTokens();
  118.         if (!\is_string($content) || empty($tokens)) {
  119.             return;
  120.         }
  121.         $content str_replace($tokens''$content$replacedCount);
  122.         if ($replacedCount <= 0) {
  123.             return;
  124.         }
  125.         $response->setContent($content);
  126.         // Remove the Content-Length header now that we have changed the
  127.         // content length (see #2416). Do not add the header or adjust an
  128.         // existing one (see symfony/symfony#1846).
  129.         $response->headers->remove('Content-Length');
  130.     }
  131.     private function removeCookies(Request $requestResponse $response): void
  132.     {
  133.         $isSecure $request->isSecure();
  134.         $basePath $request->getBasePath() ?: '/';
  135.         foreach ($request->cookies as $key => $value) {
  136.             if ($this->isCsrfCookie($key$value)) {
  137.                 $response->headers->clearCookie($key$basePathnull$isSecure);
  138.             }
  139.         }
  140.     }
  141.     /**
  142.      * @return array<string,string>
  143.      */
  144.     private function getTokensFromCookies(ParameterBag $cookies): array
  145.     {
  146.         $tokens = [];
  147.         foreach ($cookies as $key => $value) {
  148.             if ($this->isCsrfCookie($key$value)) {
  149.                 $tokens[substr($key, \strlen($this->cookiePrefix))] = $value;
  150.             }
  151.         }
  152.         return $tokens;
  153.     }
  154.     private function isCsrfCookie($key$value): bool
  155.     {
  156.         if (!\is_string($key)) {
  157.             return false;
  158.         }
  159.         return === strpos($key$this->cookiePrefix) && preg_match('/^[a-z0-9_-]+$/i'$value);
  160.     }
  161. }