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

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