vendor/pimcore/pimcore/bundles/CoreBundle/EventListener/Frontend/FullPageCacheListener.php line 328

Open in your IDE?
  1. <?php
  2. /**
  3.  * Pimcore
  4.  *
  5.  * This source file is available under two different licenses:
  6.  * - GNU General Public License version 3 (GPLv3)
  7.  * - Pimcore Commercial License (PCL)
  8.  * Full copyright and license information is available in
  9.  * LICENSE.md which is distributed with this source code.
  10.  *
  11.  *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  12.  *  @license    http://www.pimcore.org/license     GPLv3 and PCL
  13.  */
  14. namespace Pimcore\Bundle\CoreBundle\EventListener\Frontend;
  15. use Pimcore\Bundle\CoreBundle\EventListener\Traits\PimcoreContextAwareTrait;
  16. use Pimcore\Bundle\CoreBundle\EventListener\Traits\StaticPageContextAwareTrait;
  17. use Pimcore\Cache;
  18. use Pimcore\Cache\FullPage\SessionStatus;
  19. use Pimcore\Config;
  20. use Pimcore\Event\Cache\FullPage\CacheResponseEvent;
  21. use Pimcore\Event\Cache\FullPage\PrepareResponseEvent;
  22. use Pimcore\Event\FullPageCacheEvents;
  23. use Pimcore\Http\Request\Resolver\PimcoreContextResolver;
  24. use Pimcore\Logger;
  25. use Pimcore\Targeting\VisitorInfoStorageInterface;
  26. use Pimcore\Tool;
  27. use Symfony\Component\HttpFoundation\BinaryFileResponse;
  28. use Symfony\Component\HttpFoundation\Response;
  29. use Symfony\Component\HttpFoundation\StreamedResponse;
  30. use Symfony\Component\HttpKernel\Event\KernelEvent;
  31. use Symfony\Component\HttpKernel\Event\RequestEvent;
  32. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  33. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  34. class FullPageCacheListener
  35. {
  36.     use PimcoreContextAwareTrait;
  37.     use StaticPageContextAwareTrait;
  38.     /**
  39.      * @var bool
  40.      */
  41.     protected $enabled true;
  42.     /**
  43.      * @var bool
  44.      */
  45.     protected $stopResponsePropagation false;
  46.     /**
  47.      * @var null|int
  48.      */
  49.     protected $lifetime null;
  50.     /**
  51.      * @var bool
  52.      */
  53.     protected $addExpireHeader true;
  54.     /**
  55.      * @var string|null
  56.      */
  57.     protected $disableReason;
  58.     /**
  59.      * @var string
  60.      */
  61.     protected $defaultCacheKey;
  62.     public function __construct(
  63.         private VisitorInfoStorageInterface $visitorInfoStorage,
  64.         private SessionStatus $sessionStatus,
  65.         private EventDispatcherInterface $eventDispatcher,
  66.         protected Config $config
  67.     ) {
  68.     }
  69.     /**
  70.      * @param string|null $reason
  71.      *
  72.      * @return bool
  73.      */
  74.     public function disable($reason null)
  75.     {
  76.         if ($reason) {
  77.             $this->disableReason $reason;
  78.         }
  79.         $this->enabled false;
  80.         return true;
  81.     }
  82.     /**
  83.      * @return bool
  84.      */
  85.     public function enable()
  86.     {
  87.         $this->enabled true;
  88.         return true;
  89.     }
  90.     /**
  91.      * @return bool
  92.      */
  93.     public function isEnabled()
  94.     {
  95.         return $this->enabled;
  96.     }
  97.     /**
  98.      * @param int|null $lifetime
  99.      *
  100.      * @return $this
  101.      */
  102.     public function setLifetime($lifetime)
  103.     {
  104.         $this->lifetime $lifetime;
  105.         return $this;
  106.     }
  107.     /**
  108.      * @return int|null
  109.      */
  110.     public function getLifetime()
  111.     {
  112.         return $this->lifetime;
  113.     }
  114.     public function disableExpireHeader()
  115.     {
  116.         $this->addExpireHeader false;
  117.     }
  118.     public function enableExpireHeader()
  119.     {
  120.         $this->addExpireHeader true;
  121.     }
  122.     /**
  123.      * @param RequestEvent $event
  124.      */
  125.     public function onKernelRequest(RequestEvent $event)
  126.     {
  127.         if (!$this->isEnabled()) {
  128.             return;
  129.         }
  130.         $request $event->getRequest();
  131.         if (!$event->isMainRequest()) {
  132.             return;
  133.         }
  134.         if (!$this->matchesPimcoreContext($requestPimcoreContextResolver::CONTEXT_DEFAULT)) {
  135.             return;
  136.         }
  137.         if (!\Pimcore\Tool::useFrontendOutputFilters()) {
  138.             return;
  139.         }
  140.         $requestUri $request->getRequestUri();
  141.         $excludePatterns = [];
  142.         // disable the output-cache if the client sends an authorization header
  143.         if ($request->headers->has('authorization')) {
  144.             $this->disable('authorization header in use');
  145.             return;
  146.         }
  147.         // only enable GET method
  148.         if (!$request->isMethodCacheable()) {
  149.             $this->disable();
  150.             return;
  151.         }
  152.         // disable the output-cache if browser wants the most recent version
  153.         // unfortunately only Chrome + Firefox if not using SSL
  154.         if (!$request->isSecure()) {
  155.             if (isset($_SERVER['HTTP_CACHE_CONTROL']) && $_SERVER['HTTP_CACHE_CONTROL'] === 'no-cache') {
  156.                 $this->disable('HTTP Header Cache-Control: no-cache was sent');
  157.                 return;
  158.             }
  159.             if (isset($_SERVER['HTTP_PRAGMA']) && $_SERVER['HTTP_PRAGMA'] === 'no-cache') {
  160.                 $this->disable('HTTP Header Pragma: no-cache was sent');
  161.                 return;
  162.             }
  163.         }
  164.         try {
  165.             if ($conf $this->config['full_page_cache']) {
  166.                 if (empty($conf['enabled'])) {
  167.                     $this->disable();
  168.                     return;
  169.                 }
  170.                 if (\Pimcore::inDebugMode()) {
  171.                     $this->disable('Debug flag DISABLE_FULL_PAGE_CACHE is enabled');
  172.                     return;
  173.                 }
  174.                 if (!empty($conf['lifetime'])) {
  175.                     $this->setLifetime((int) $conf['lifetime']);
  176.                 }
  177.                 if (!empty($conf['exclude_patterns'])) {
  178.                     $confExcludePatterns explode(','$conf['exclude_patterns']);
  179.                     $excludePatterns $confExcludePatterns;
  180.                 }
  181.                 if (!empty($conf['exclude_cookie'])) {
  182.                     $cookies explode(',', (string)$conf['exclude_cookie']);
  183.                     foreach ($cookies as $cookie) {
  184.                         if (!empty($cookie) && isset($_COOKIE[trim($cookie)])) {
  185.                             $this->disable('exclude cookie in system-settings matches');
  186.                             return;
  187.                         }
  188.                     }
  189.                 }
  190.                 if ($this->sessionStatus->isDisabledBySession($request)) {
  191.                     $this->disable('Session in use');
  192.                     return;
  193.                 }
  194.                 // output-cache is always disabled when logged in at the admin ui
  195.                 if (null !== $pimcoreUser Tool\Authentication::authenticateSession($request)) {
  196.                     $this->disable('backend user is logged in');
  197.                     return;
  198.                 }
  199.             } else {
  200.                 $this->disable();
  201.                 return;
  202.             }
  203.         } catch (\Exception $e) {
  204.             Logger::error($e);
  205.             $this->disable('ERROR: Exception (see log files in /var/log)');
  206.             return;
  207.         }
  208.         foreach ($excludePatterns as $pattern) {
  209.             if (@preg_match($pattern$requestUri)) {
  210.                 $this->disable('exclude path pattern in system-settings matches');
  211.                 return;
  212.             }
  213.         }
  214.         $deviceDetector Tool\DeviceDetector::getInstance();
  215.         $device $deviceDetector->getDevice();
  216.         $deviceDetector->setWasUsed(false);
  217.         $appendKey '';
  218.         // this is for example for the image-data-uri plugin
  219.         if (isset($_REQUEST['pimcore_cache_tag_suffix'])) {
  220.             $tags $_REQUEST['pimcore_cache_tag_suffix'];
  221.             if (is_array($tags)) {
  222.                 $appendKey '_' implode('_'$tags);
  223.             }
  224.         }
  225.         if ($request->isXmlHttpRequest()) {
  226.             $appendKey .= 'xhr';
  227.         }
  228.         $appendKey .= $request->getMethod();
  229.         $this->defaultCacheKey 'output_' md5(\Pimcore\Tool::getHostname() . $requestUri $appendKey);
  230.         $cacheKeys = [
  231.             $this->defaultCacheKey '_' $device,
  232.             $this->defaultCacheKey,
  233.         ];
  234.         $cacheKey null;
  235.         $cacheItem null;
  236.         foreach ($cacheKeys as $cacheKey) {
  237.             $cacheItem Cache::load($cacheKey);
  238.             if ($cacheItem) {
  239.                 break;
  240.             }
  241.         }
  242.         if ($cacheItem) {
  243.             /** @var Response $response */
  244.             $response $cacheItem;
  245.             $response->headers->set('X-Pimcore-Output-Cache-Tag'$cacheKeytrue);
  246.             $cacheItemDate strtotime($response->headers->get('X-Pimcore-Cache-Date'));
  247.             $response->headers->set('Age', (time() - $cacheItemDate));
  248.             $event->setResponse($response);
  249.             $this->stopResponsePropagation true;
  250.         }
  251.     }
  252.     /**
  253.      * @param KernelEvent $event
  254.      */
  255.     public function stopPropagationCheck(KernelEvent $event)
  256.     {
  257.         if ($this->stopResponsePropagation) {
  258.             $event->stopPropagation();
  259.         }
  260.     }
  261.     /**
  262.      * @param ResponseEvent $event
  263.      */
  264.     public function onKernelResponse(ResponseEvent $event)
  265.     {
  266.         if (!$event->isMainRequest()) {
  267.             return;
  268.         }
  269.         $request $event->getRequest();
  270.         if (!\Pimcore\Tool::isFrontend() || \Pimcore\Tool::isFrontendRequestByAdmin($request)) {
  271.             return;
  272.         }
  273.         if (!$this->matchesPimcoreContext($requestPimcoreContextResolver::CONTEXT_DEFAULT)) {
  274.             return;
  275.         }
  276.         if ($this->matchesStaticPageContext($request)) {
  277.             $this->disable('Response can\'t be cached for static pages');
  278.         }
  279.         $response $event->getResponse();
  280.         if (!$this->responseCanBeCached($response)) {
  281.             $this->disable('Response can\'t be cached');
  282.         }
  283.         // check if targeting matched anything and disable cache
  284.         if ($this->disabledByTargeting()) {
  285.             $this->disable('Targeting matched rules/target groups');
  286.             return;
  287.         }
  288.         if ($this->enabled && $this->sessionStatus->isDisabledBySession($request)) {
  289.             $this->disable('Session in use');
  290.         }
  291.         if ($this->disableReason) {
  292.             $response->headers->set('X-Pimcore-Output-Cache-Disable-Reason'$this->disableReasontrue);
  293.         }
  294.         if ($this->enabled && $response->getStatusCode() == 200 && $this->defaultCacheKey) {
  295.             try {
  296.                 if ($this->lifetime && $this->addExpireHeader) {
  297.                     // add cache control for proxies and http-caches like varnish, ...
  298.                     $response->headers->set('Cache-Control''public, max-age=' $this->lifetimetrue);
  299.                     // add expire header
  300.                     $date = new \DateTime('now');
  301.                     $date->add(new \DateInterval('PT' $this->lifetime 'S'));
  302.                     $response->headers->set('Expires'$date->format(\DateTime::RFC1123), true);
  303.                 }
  304.                 $now = new \DateTime('now');
  305.                 $response->headers->set('X-Pimcore-Cache-Date'$now->format(\DateTime::ISO8601));
  306.                 $cacheKey $this->defaultCacheKey;
  307.                 $deviceDetector Tool\DeviceDetector::getInstance();
  308.                 if ($deviceDetector->wasUsed()) {
  309.                     $cacheKey .= '_' $deviceDetector->getDevice();
  310.                 }
  311.                 $event = new PrepareResponseEvent($request$response);
  312.                 $this->eventDispatcher->dispatch($eventFullPageCacheEvents::PREPARE_RESPONSE);
  313.                 $cacheItem $event->getResponse();
  314.                 $tags = ['output'];
  315.                 if ($this->lifetime) {
  316.                     $tags = ['output_lifetime'];
  317.                 }
  318.                 Cache::save($cacheItem$cacheKey$tags$this->lifetime1000true);
  319.             } catch (\Exception $e) {
  320.                 Logger::error($e);
  321.                 return;
  322.             }
  323.         } else {
  324.             // output-cache was disabled, add "output" as cleared tag to ensure that no other "output" tagged elements
  325.             // like the inc and snippet cache get into the cache
  326.             Cache::addIgnoredTagOnSave('output_inline');
  327.         }
  328.     }
  329.     private function responseCanBeCached(Response $response): bool
  330.     {
  331.         $cache true;
  332.         // do not cache common responses
  333.         if ($response instanceof BinaryFileResponse) {
  334.             $cache false;
  335.         }
  336.         if ($response instanceof StreamedResponse) {
  337.             $cache false;
  338.         }
  339.         // fire an event to allow full customozations
  340.         $event = new CacheResponseEvent($response$cache);
  341.         $this->eventDispatcher->dispatch($eventFullPageCacheEvents::CACHE_RESPONSE);
  342.         return $event->getCache();
  343.     }
  344.     private function disabledByTargeting(): bool
  345.     {
  346.         if (!$this->visitorInfoStorage->hasVisitorInfo()) {
  347.             return false;
  348.         }
  349.         $visitorInfo $this->visitorInfoStorage->getVisitorInfo();
  350.         if (!empty($visitorInfo->getMatchingTargetingRules())) {
  351.             return true;
  352.         }
  353.         if (!empty($visitorInfo->getTargetGroupAssignments())) {
  354.             return true;
  355.         }
  356.         return false;
  357.     }
  358. }