pimcore/lib/Pimcore/Bundle/CoreBundle/EventListener/Frontend/FullPageCacheListener.php line 303

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