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

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.      * @return mixed
  144.      */
  145.     public function onKernelRequest(GetResponseEvent $event)
  146.     {
  147.         $request $event->getRequest();
  148.         if (!$event->isMasterRequest()) {
  149.             return;
  150.         }
  151.         if (!$this->matchesPimcoreContext($requestPimcoreContextResolver::CONTEXT_DEFAULT)) {
  152.             return;
  153.         }
  154.         if (!\Pimcore\Tool::useFrontendOutputFilters()) {
  155.             return false;
  156.         }
  157.         $requestUri $request->getRequestUri();
  158.         $excludePatterns = [];
  159.         // only enable GET method
  160.         if (!$request->isMethodCacheable()) {
  161.             return $this->disable();
  162.         }
  163.         // disable the output-cache if browser wants the most recent version
  164.         // unfortunately only Chrome + Firefox if not using SSL
  165.         if (!$request->isSecure()) {
  166.             if (isset($_SERVER['HTTP_CACHE_CONTROL']) && $_SERVER['HTTP_CACHE_CONTROL'] == 'no-cache') {
  167.                 return $this->disable('HTTP Header Cache-Control: no-cache was sent');
  168.             }
  169.             if (isset($_SERVER['HTTP_PRAGMA']) && $_SERVER['HTTP_PRAGMA'] == 'no-cache') {
  170.                 return $this->disable('HTTP Header Pragma: no-cache was sent');
  171.             }
  172.         }
  173.         try {
  174.             if ($conf $this->config['full_page_cache']) {
  175.                 if (!$conf['enabled']) {
  176.                     return $this->disable();
  177.                 }
  178.                 if (\Pimcore::inDebugMode()) {
  179.                     return $this->disable('Debug flag DISABLE_FULL_PAGE_CACHE is enabled');
  180.                 }
  181.                 if ($conf['lifetime']) {
  182.                     $this->setLifetime((int) $conf['lifetime']);
  183.                 }
  184.                 if ($conf['exclude_patterns']) {
  185.                     $confExcludePatterns explode(','$conf['exclude_patterns']);
  186.                     if (!empty($confExcludePatterns)) {
  187.                         $excludePatterns $confExcludePatterns;
  188.                     }
  189.                 }
  190.                 if ($conf['exclude_cookie']) {
  191.                     $cookies explode(','strval($conf['exclude_cookie']));
  192.                     foreach ($cookies as $cookie) {
  193.                         if (!empty($cookie) && isset($_COOKIE[trim($cookie)])) {
  194.                             return $this->disable('exclude cookie in system-settings matches');
  195.                         }
  196.                     }
  197.                 }
  198.                 // output-cache is always disabled when logged in at the admin ui
  199.                 if (null !== $pimcoreUser Tool\Authentication::authenticateSession($request)) {
  200.                     return $this->disable('backend user is logged in');
  201.                 }
  202.             } else {
  203.                 return $this->disable();
  204.             }
  205.         } catch (\Exception $e) {
  206.             Logger::error($e);
  207.             return $this->disable('ERROR: Exception (see log files in /var/logs)');
  208.         }
  209.         foreach ($excludePatterns as $pattern) {
  210.             if (@preg_match($pattern$requestUri)) {
  211.                 return $this->disable('exclude path pattern in system-settings matches');
  212.             }
  213.         }
  214.         // check if targeting matched anything and disable cache
  215.         if ($this->disabledByTargeting()) {
  216.             return $this->disable('Targeting matched rules/target groups');
  217.         }
  218.         $deviceDetector Tool\DeviceDetector::getInstance();
  219.         $device $deviceDetector->getDevice();
  220.         $deviceDetector->setWasUsed(false);
  221.         $appendKey '';
  222.         // this is for example for the image-data-uri plugin
  223.         if (isset($_REQUEST['pimcore_cache_tag_suffix'])) {
  224.             $tags $_REQUEST['pimcore_cache_tag_suffix'];
  225.             if (is_array($tags)) {
  226.                 $appendKey '_' implode('_'$tags);
  227.             }
  228.         }
  229.         if (Tool\Frontend::hasWebpSupport()) {
  230.             $appendKey .= 'webp';
  231.         }
  232.         if ($request->isXmlHttpRequest()) {
  233.             $appendKey .= 'xhr';
  234.         }
  235.         $appendKey .= $request->getMethod();
  236.         $this->defaultCacheKey 'output_' md5(\Pimcore\Tool::getHostname() . $requestUri $appendKey);
  237.         $cacheKeys = [
  238.             $this->defaultCacheKey '_' $device,
  239.             $this->defaultCacheKey,
  240.         ];
  241.         $cacheKey null;
  242.         $cacheItem null;
  243.         foreach ($cacheKeys as $cacheKey) {
  244.             $cacheItem Cache::load($cacheKey);
  245.             if ($cacheItem) {
  246.                 break;
  247.             }
  248.         }
  249.         if ($cacheItem) {
  250.             /** @var Response $response */
  251.             $response $cacheItem;
  252.             $response->headers->set('X-Pimcore-Output-Cache-Tag'$cacheKeytrue);
  253.             $cacheItemDate strtotime($response->headers->get('X-Pimcore-Cache-Date'));
  254.             $response->headers->set('Age', (time() - $cacheItemDate));
  255.             $event->setResponse($response);
  256.             $this->stopResponsePropagation true;
  257.         }
  258.     }
  259.     /**
  260.      * @param KernelEvent $event
  261.      */
  262.     public function stopPropagationCheck(KernelEvent $event)
  263.     {
  264.         if ($this->stopResponsePropagation) {
  265.             $event->stopPropagation();
  266.         }
  267.     }
  268.     /**
  269.      * @param FilterResponseEvent $event
  270.      */
  271.     public function onKernelResponse(FilterResponseEvent $event)
  272.     {
  273.         if (!$event->isMasterRequest()) {
  274.             return;
  275.         }
  276.         $request $event->getRequest();
  277.         if (!\Pimcore\Tool::isFrontend() || \Pimcore\Tool::isFrontendRequestByAdmin($request)) {
  278.             return;
  279.         }
  280.         if (!$this->matchesPimcoreContext($requestPimcoreContextResolver::CONTEXT_DEFAULT)) {
  281.             return;
  282.         }
  283.         $response $event->getResponse();
  284.         if (!$response) {
  285.             return;
  286.         }
  287.         if (!$this->responseCanBeCached($response)) {
  288.             $this->disable('Response can\'t be cached');
  289.         }
  290.         if ($this->enabled && $this->sessionStatus->isDisabledBySession($request)) {
  291.             $this->disable('Session in use');
  292.         }
  293.         if ($this->disableReason) {
  294.             $response->headers->set('X-Pimcore-Output-Cache-Disable-Reason'$this->disableReasontrue);
  295.         }
  296.         if ($this->enabled && $response->getStatusCode() == 200 && $this->defaultCacheKey) {
  297.             try {
  298.                 if ($this->lifetime && $this->addExpireHeader) {
  299.                     // add cache control for proxies and http-caches like varnish, ...
  300.                     $response->headers->set('Cache-Control''public, max-age=' $this->lifetimetrue);
  301.                     // add expire header
  302.                     $date = new \DateTime('now');
  303.                     $date->add(new \DateInterval('PT' $this->lifetime 'S'));
  304.                     $response->headers->set('Expires'$date->format(\DateTime::RFC1123), true);
  305.                 }
  306.                 $now = new \DateTime('now');
  307.                 $response->headers->set('X-Pimcore-Cache-Date'$now->format(\DateTime::ISO8601));
  308.                 $cacheKey $this->defaultCacheKey;
  309.                 $deviceDetector Tool\DeviceDetector::getInstance();
  310.                 if ($deviceDetector->wasUsed()) {
  311.                     $cacheKey .= '_' $deviceDetector->getDevice();
  312.                 }
  313.                 $event = new PrepareResponseEvent($request$response);
  314.                 $this->eventDispatcher->dispatch(FullPageCacheEvents::PREPARE_RESPONSE$event);
  315.                 $cacheItem $event->getResponse();
  316.                 $tags = ['output'];
  317.                 if ($this->lifetime) {
  318.                     $tags = ['output_lifetime'];
  319.                 }
  320.                 Cache::save($cacheItem$cacheKey$tags$this->lifetime1000true);
  321.             } catch (\Exception $e) {
  322.                 Logger::error($e);
  323.                 return;
  324.             }
  325.         } else {
  326.             // output-cache was disabled, add "output" as cleared tag to ensure that no other "output" tagged elements
  327.             // like the inc and snippet cache get into the cache
  328.             Cache::addIgnoredTagOnSave('output_inline');
  329.         }
  330.     }
  331.     private function responseCanBeCached(Response $response): bool
  332.     {
  333.         $cache true;
  334.         // do not cache common responses
  335.         if ($response instanceof BinaryFileResponse) {
  336.             $cache false;
  337.         }
  338.         if ($response instanceof StreamedResponse) {
  339.             $cache false;
  340.         }
  341.         // fire an event to allow full customozations
  342.         $event = new CacheResponseEvent($response$cache);
  343.         $this->eventDispatcher->dispatch(FullPageCacheEvents::CACHE_RESPONSE$event);
  344.         return $event->getCache();
  345.     }
  346.     private function disabledByTargeting(): bool
  347.     {
  348.         if (!$this->visitorInfoStorage->hasVisitorInfo()) {
  349.             return false;
  350.         }
  351.         $visitorInfo $this->visitorInfoStorage->getVisitorInfo();
  352.         if (!empty($visitorInfo->getMatchingTargetingRules())) {
  353.             return true;
  354.         }
  355.         if (!empty($visitorInfo->getTargetGroupAssignments())) {
  356.             return true;
  357.         }
  358.         return false;
  359.     }
  360. }