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

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