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

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