jhit
3/13/2018 - 10:18 AM

Event for filtering Cache Invalidation

See https://sourcegraph.com/github.com/shopware/shopware@5.3/-/blob/engine/Shopware/Plugins/Default/Core/HttpCache/Bootstrap.php#L667:14

You can register a plugin to the Shopware_Plugins_HttpCache_ShouldNotInvalidateCache event and let decide your plugin if you want to invalidate (return false) or if you do not want to invalidate (return true) your cached element.

<?php
/**
 * Shopware 5
 * Copyright (c) shopware AG
 *
 * According to our dual licensing model, this program can be used either
 * under the terms of the GNU Affero General Public License, version 3,
 * or under a proprietary license.
 *
 * The texts of the GNU Affero General Public License with an additional
 * permission and of our proprietary license can be found at and
 * in the LICENSE file you have received along with this program.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * "Shopware" is a registered trademark of shopware AG.
 * The licensing of the program under the AGPLv3 does not imply a
 * trademark license. Therefore any rights, title and interest in
 * our trademarks remain entirely with us.
 */

use Doctrine\Common\EventArgs;
use Enlight_Controller_Request_Request as Request;
use Enlight_Controller_Response_ResponseHttp as Response;
use Shopware\Components\HttpCache\Store;
use Shopware\Components\Model\ModelManager;
use ShopwarePlugins\HttpCache\CacheControl;
use ShopwarePlugins\HttpCache\CacheIdCollector;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;

/**
 * @category  Shopware
 *
 * @copyright Copyright (c) shopware AG (http://www.shopware.de)
 */
class Shopware_Plugins_Core_HttpCache_Bootstrap extends Shopware_Components_Plugin_Bootstrap
{
    /**
     * @var \Enlight_Controller_Action
     */
    private $action;

    /**
     * @var Request
     */
    private $request;

    /**
     * @var Response
     */
    private $response;

    /**
     * @var array
     */
    private $cacheInvalidationBuffer = [];

    /**
     * @return string
     */
    public function getLabel()
    {
        return 'Frontendcache (HttpCache)';
    }

    /**
     * @return string
     */
    public function getVersion()
    {
        return '1.1.0';
    }

    /**
     * @return array
     */
    public function getInfo()
    {
        return [
            'version' => $this->getVersion(),
            'label' => $this->getLabel(),
        ];
    }

    /**
     * Install the plugin.
     *
     * @return bool
     */
    public function install()
    {
        $this->subscribeEvent(
            'Enlight_Controller_Action_PreDispatch',
            'onPreDispatch'
        );

        $this->subscribeEvent(
            'Shopware_Plugins_HttpCache_InvalidateCacheId',
            'onInvalidateCacheId'
        );

        $this->subscribeEvent(
            'Shopware_Plugins_HttpCache_ClearCache',
            'onClearCache'
        );

        $this->createCronJob(
            'HTTP Cache löschen',
            'ClearHttpCache',
            86400,
            true
        );

        $this->subscribeEvent(
            'Shopware_CronJob_ClearHttpCache',
            'onClearHttpCache'
        );

        $this->subscribeEvent('Enlight_Bootstrap_InitResource_http_cache.cache_control', 'initCacheControl');
        $this->subscribeEvent('Enlight_Bootstrap_InitResource_http_cache.cache_id_collector', 'initCacheIdCollector');

        $this->subscribeEvent('Shopware\Models\Article\Price::postUpdate', 'onPostPersist');
        $this->subscribeEvent('Shopware\Models\Article\Price::postPersist', 'onPostPersist');

        $this->subscribeEvent('Shopware\Models\Article\Article::postUpdate', 'onPostPersist');
        $this->subscribeEvent('Shopware\Models\Article\Article::postPersist', 'onPostPersist');

        $this->subscribeEvent('Shopware\Models\Article\Detail::postUpdate', 'onPostPersist');
        $this->subscribeEvent('Shopware\Models\Article\Detail::postPersist', 'onPostPersist');

        $this->subscribeEvent('Shopware\Models\Category\Category::postPersist', 'onPostPersist');
        $this->subscribeEvent('Shopware\Models\Category\Category::postUpdate', 'onPostPersist');

        $this->subscribeEvent('Shopware\Models\Banner\Banner::postPersist', 'onPostPersist');
        $this->subscribeEvent('Shopware\Models\Banner\Banner::postUpdate', 'onPostPersist');

        $this->subscribeEvent('Shopware\Models\Blog\Blog::postPersist', 'onPostPersist');
        $this->subscribeEvent('Shopware\Models\Blog\Blog::postUpdate', 'onPostPersist');

        $this->subscribeEvent('Shopware\Models\Emotion\Emotion::postPersist', 'onPostPersist');
        $this->subscribeEvent('Shopware\Models\Emotion\Emotion::postUpdate', 'onPostPersist');

        $this->subscribeEvent('Shopware\Models\Site\Site::postPersist', 'onPostPersist');
        $this->subscribeEvent('Shopware\Models\Site\Site::postUpdate', 'onPostPersist');

        $this->installForm();

        return true;
    }

    public function initCacheControl(Enlight_Event_EventArgs $args)
    {
        return new CacheControl(
            $this->get('session'),
            $this->Config(),
            $this->get('events')
        );
    }

    public function initCacheIdCollector()
    {
        return new CacheIdCollector();
    }

    public function afterInit()
    {
        $this->get('loader')->registerNamespace('ShopwarePlugins\\HttpCache', __DIR__);
        parent::afterInit();
    }

    /**
     * @return array
     */
    public function getCapabilities()
    {
        return [
            'install' => false,
            'enable' => false,
            'update' => true,
        ];
    }

    /**
     * Enable plugin method
     * Although plugin is flagged as capability: enable => false,
     * it can still be enabled/disabled programmatically
     *
     * @return bool
     */
    public function enable()
    {
        return true;
    }

    /**
     * Disable plugin method
     * Although plugin is flagged as capability: enable => false,
     * it can still be enabled/disabled programmatically
     *
     * @return bool
     */
    public function disable()
    {
        return true;
    }

    /**
     * Install config-form
     */
    public function installForm()
    {
        $form = $this->Form();

        /** @var $parent \Shopware\Models\Config\Form */
        $parent = $this->Forms()->findOneBy(['name' => 'Core']);

        $form->setParent($parent);
        $form->setElement('textarea', 'cacheControllers', [
            'label' => 'Cache-Controller / Zeiten',
            'value' => "frontend/listing 3600\r\n" .
            "frontend/index 3600\r\n" .
            "frontend/detail 3600\r\n" .
            "frontend/campaign 14400\r\n" .
            "widgets/listing 14400\r\n" .
            "frontend/custom 14400\r\n" .
            "frontend/sitemap 14400\r\n" .
            "frontend/blog 14400\r\n" .
            "widgets/index 3600\r\n" .
            "widgets/checkout 3600\r\n" .
            "widgets/compare 3600\r\n" .
            "widgets/emotion 14400\r\n" .
            "widgets/recommendation 14400\r\n" .
            "widgets/lastArticles 3600\n" .
            "widgets/campaign 3600\n",
        ]);

        $form->setElement('textarea', 'noCacheControllers', [
            'label' => 'NoCache-Controller / Tags',
            'value' => "frontend/listing price\n" .
            "frontend/index price\n" .
            "frontend/detail price\n" .
            "widgets/lastArticles detail\n" .
            "widgets/checkout checkout\n" .
            "widgets/compare compare\n" .
            "widgets/emotion price\n",
        ]);

        $form->setElement('boolean', 'proxyPrune', [
            'label' => 'Proxy-Prune aktivieren',
            'description' => 'Das automatische Leeren des Caches aktivieren.',
            'value' => true,
        ]);

        $form->setElement('text', 'proxy', [
            'label' => 'Alternative Proxy-Url',
            'description' => 'Link zum Http-Proxy mit „http://“ am Anfang.',
            'value' => null,
        ]);

        $form->setElement('boolean', 'admin', [
            'label' => 'Admin-View',
            'description' => 'Cache bei Artikel-Vorschau und Schnellbestellung deaktivieren',
            'value' => false,
        ]);
    }

    /**
     * Returns the configured proxy-url.
     *
     * Fallbacks to autodetection if proxy-url is not configured and $request is given.
     * Returns null if $request is not given or autodetection fails.
     *
     * @param Request $request
     *
     * @return string|null
     */
    public function getProxyUrl(Request $request = null)
    {
        $proxyUrl = trim($this->Config()->get('proxy'));
        if (!empty($proxyUrl)) {
            return $proxyUrl;
        }

        // if proxy url is not set fall back to host detection
        if ($request !== null && $request->getHttpHost()) {
            return $request->getScheme() . '://'
                   . $request->getHttpHost()
                   . $request->getBaseUrl() . '/';
        }

        /** @var ModelManager $em */
        $em = $this->get('models');
        $repository = $em->getRepository('Shopware\Models\Shop\Shop');

        /** @var Shopware\Models\Shop\Shop $shop */
        $shop = $repository->findOneBy(['default' => true]);

        if (!$shop->getHost()) {
            return null;
        }

        $url = sprintf(
            '%s://%s%s/',
            'http',
            $shop->getHost(),
            $shop->getBasePath()
        );

        return $url;
    }

    /**
     * Do http caching jobs
     *
     * @param Enlight_Controller_ActionEventArgs $args
     */
    public function onPreDispatch(\Enlight_Controller_ActionEventArgs $args)
    {
        $this->action = $args->getSubject();
        $this->request = $args->getRequest();
        $this->response = $args->getResponse();

        $this->Application()->Events()->registerListener(
            new Enlight_Event_Handler_Default(
                'Enlight_Controller_Action_PostDispatch',
                [$this, 'onPostDispatch'],
                // must be positioned before ViewRender Plugin
                // so the ESI renderer can be registered
                // before the template is rendered
                399
            )
        );
    }

    /**
     * On post dispatch we try to find affected articleIds displayed during this request
     *
     * @param \Enlight_Controller_ActionEventArgs $args
     */
    public function onPostDispatch(\Enlight_Controller_ActionEventArgs $args)
    {
        $view = $args->getSubject()->View();
        if (!$this->request->isDispatched()
            || $this->response->isException()
            || !$view->hasTemplate()
        ) {
            return;
        }

        if (!$this->hasSurrogateEsiCapability($this->request)) {
            return;
        }

        if ($this->request->getModuleName() != 'frontend' && $this->request->getModuleName() != 'widgets') {
            return;
        }

        // Do not cache if shop(template) is not esi-enabled
        if (!Shopware()->Shop()->get('esi')) {
            return;
        }

        // Enable esi tag output
        $this->registerEsiRenderer();

        $this->addSurrogateControl($this->response);

        $this->addContextCookie($this->request, $this->response);

        $this->setNoCacheCookie();

        $this->setCacheHeaders();
    }

    /**
     * Callback for event Shopware_CronJob_ClearHttpCache
     *
     * Clears the file-based http-cache storage directory
     *
     * @param Shopware_Components_Cron_CronJob $job
     *
     * @return string
     */
    public function onClearHttpCache(\Shopware_Components_Cron_CronJob $job)
    {
        if ($this->clearCache()) {
            return "Cleared HTTP-Cache\n";
        }
    }

    /**
     * Callback for Shopware_Plugins_HttpCache_ClearCache-Event
     *
     * This events should be used to clear the http-cache without having
     * to check if the http-cache-plugin is installed and enabled.
     *
     * <code>
     * Shopware()->Events()->notify('Shopware_Plugins_HttpCache_ClearCache');
     * </code>
     *
     * @param \Enlight_Event_EventArgs $args
     */
    public function onClearCache(\Enlight_Event_EventArgs $args)
    {
        $result = $this->clearCache();

        $args->setReturn($result);
    }

    /**
     * Callback for Shopware_Plugins_HttpCache_InvalidateCacheId-Event
     *
     * This events should be used to invalidate cacheIds without having
     * to check if the http-cache-plugin is installed and enabled.
     *
     * <code>
     * Shopware()->Events()->notify(
     *     'Shopware_Plugins_HttpCache_InvalidateCacheId',
     *     array('cacheId' => 'a123')
     * );
     * </code>
     *
     * @param \Enlight_Event_EventArgs $args
     */
    public function onInvalidateCacheId(\Enlight_Event_EventArgs $args)
    {
        $cacheId = $args->get('cacheId');
        if (!$cacheId) {
            $args->setReturn(false);

            return;
        }

        $result = $this->invalidateCacheId($cacheId);
        $args->setReturn($result);
    }

    /**
     * Sets the shopware cache headers
     */
    public function setCacheHeaders()
    {
        /** @var CacheControl $cacheControl */
        $cacheControl = $this->get('http_cache.cache_control');

        $shopId = $this->get('shop')->getId();
        if (!$cacheControl->isCacheableRoute($this->request)) {
            return false;
        }

        if ($cacheControl->useNoCacheControl($this->request, $this->response, $shopId)) {
            $this->response->setHeader('Cache-Control', 'private, no-cache');

            return false;
        }

        $cacheTime = (int) $cacheControl->getCacheTime($this->request);

        $this->request->setParam('__cache', $cacheTime);
        $this->response->setHeader('Cache-Control', 'public, max-age=' . $cacheTime . ', s-maxage=' . $cacheTime);

        $noCacheTags = $cacheControl->getNoCacheTagsForRequest($this->request, $shopId);
        if (!empty($noCacheTags)) {
            $this->response->setHeader('x-shopware-allow-nocache', implode(', ', $noCacheTags));
        }

        $cacheCollector = $this->get('http_cache.cache_id_collector');

        $context = $this->get('shopware_storefront.context_service')->getShopContext();

        $this->setCacheIdHeader(
            $cacheCollector->getCacheIdsFromController($this->action, $context)
        );

        return true;
    }

    /**
     * This methods sets the nocache-cookie if actions in the shop are triggerd
     */
    public function setNoCacheCookie()
    {
        /** @var CacheControl $cacheControl */
        $cacheControl = $this->get('http_cache.cache_control');

        $context = $this->get('shopware_storefront.context_service')->getShopContext();

        $additions = $cacheControl->getTagsForNoCacheCookie($this->request, $context);
        $additions = array_keys(array_flip($additions));
        foreach ($additions as $tag) {
            $this->setNoCacheTag($tag);
        }

        $removals = $cacheControl->getRemovableCacheTags($this->request, $context);
        $removals = array_keys(array_flip($removals));
        foreach ($removals as $tag) {
            $this->setNoCacheTag($tag, true);
        }
    }

    /**
     * Set or remove given $noCacheTag from cookie
     *
     * @param string $newTag
     * @param bool   $remove
     */
    public function setNoCacheTag($newTag, $remove = false)
    {
        if ($existingTags = $this->getResponseCookie($this->response)) {
            $existingTags = explode(', ', $existingTags);
        } elseif ($this->request->getCookie('nocache')) {
            $existingTags = $this->request->getCookie('nocache');
            $existingTags = explode(', ', $existingTags);
        } else {
            $existingTags = [];
        }

        $shopId = Shopware()->Shop()->getId();

        if (!empty($newTag) && $newTag !== 'slt') {
            $newTag .= '-' . $shopId;
        }

        if (empty($newTag)) {
            $newCacheTags = [];
        } elseif ($remove) {
            // remove $noCacheTag from $newCacheTags
            $newCacheTags = array_diff($existingTags, [$newTag]);
        } elseif (!$remove && !in_array($newTag, $existingTags)) {
            // add $noCacheTag to $newCacheTags
            $newCacheTags = $existingTags;
            $newCacheTags[] = $newTag;
        }

        if (isset($newCacheTags)) {
            $this->response->setCookie(
                'nocache',
                implode(', ', $newCacheTags),
                0,
                $this->request->getBasePath() . '/'
            );
        }
    }

    /**
     * Register the action plugin override
     */
    public function registerEsiRenderer()
    {
        $engine = $this->action->View()->Engine();

        $engine->unregisterPlugin(
            Smarty::PLUGIN_FUNCTION,
            'action'
        );
        $engine->registerPlugin(
            Smarty::PLUGIN_FUNCTION,
            'action',
            [$this, 'renderEsiTag']
        );

        if (strpos($engine->getCompileId(), '_esi') === false) {
            $engine->setCompileId($engine->getCompileId() . '_esi');
        }
    }

    /**
     * @param array $params
     *
     * @return string
     */
    public function renderEsiTag($params)
    {
        $request = $this->action->Request();

        // Alias from "name" to "action" to be compatible with non-http-cache implementation
        // @see engine/Library/Enlight/Template/Plugins/function.action.php
        if (isset($params['name'])) {
            $params['action'] = $params['name'];
            unset($params['name']);
        }

        if (isset($params['params'])) {
            $params = array_merge((array) $params['params'], $params);
            unset($params['params']);
        }

        if (!isset($params['module'])) {
            $params['module'] = $request->getModuleName();
            if (!isset($params['controller'])) {
                $params['controller'] = $request->getControllerName();
            }
        }

        $targetName = strtolower($params['module'] . '/' . $params['controller']);

        /** @var CacheControl $cacheControl */
        $cacheControl = $this->get('http_cache.cache_control');

        if ($cacheControl->useNoCacheParameterForEsi($request, $targetName)) {
            $params['nocache'] = 1;
        }

        $url = sprintf('%s/?%s', $request->getBaseUrl(), http_build_query($params, null, '&'));

        return '<esi:include src="' . $url . '" />';
    }

    /**
     * Helper function to flag the request with cacheIds
     * to invalidate the caching.
     *
     * @param array $cacheIds
     */
    public function setCacheIdHeader($cacheIds = [])
    {
        $cacheIds = $this->Application()->Events()->filter(
            'Shopware_Plugins_HttpCache_GetCacheIds',
            $cacheIds,
            ['subject' => $this, 'action' => $this->action]
        );

        if (empty($cacheIds)) {
            return;
        }

        $cacheIds = ';' . implode(';', $cacheIds) . ';';
        $this->response->setHeader('x-shopware-cache-id', $cacheIds);
    }

    /**
     * Execute cache invalidation after Doctrine flush
     *
     * @param EventArgs $eventArgs
     */
    public function postFlush(EventArgs $eventArgs)
    {
        $cacheIds = array_keys($this->cacheInvalidationBuffer);
        foreach ($cacheIds as $cacheId) {
            $this->invalidateCacheId($cacheId);
        }
        $this->cacheInvalidationBuffer = [];
    }

    /**
     * Cache invalidation based on model events
     *
     * @param Enlight_Event_EventArgs $eventArgs
     */
    public function onPostPersist(Enlight_Event_EventArgs $eventArgs)
    {
        if (!$this->Config()->get('proxyPrune')) {
            return;
        }

        $entity = $eventArgs->get('entity');
        if ($entity instanceof \Doctrine\ORM\Proxy\Proxy) {
            $entityName = get_parent_class($entity);
        } else {
            $entityName = get_class($entity);
        }
        
        if (Shopware()->Events()->notifyUntil(
            'Shopware_Plugins_HttpCache_ShouldNotInvalidateCache',
            [
                'entity' => $entity,
                'entityName' => $entityName
            ]
        )) {
            return;
        }

        $cacheIds = [];

        switch ($entityName) {
            case Shopware\Models\Article\Price::class:
                $cacheIds[] = 'a' . $entity->getArticle()->getId();
                break;
            case Shopware\Models\Article\Article::class:
                $cacheIds[] = 'a' . $entity->getId();
                break;
            case Shopware\Models\Article\Detail::class:
                $cacheIds[] = 'a' . $entity->getArticleId();
                break;
            case Shopware\Models\Category\Category::class:
                $cacheIds[] = 'c' . $entity->getId();
                break;
            case Shopware\Models\Banner\Banner::class:
                $cacheIds[] = 'c' . $entity->getCategoryId();
                break;
            case Shopware\Models\Blog\Blog::class:
                $cacheIds[] = 'c' . $entity->getCategoryId();
                break;
            case Shopware\Models\Emotion\Emotion::class:
                $cacheIds[] = 'e' . $entity->getId();
                break;
            case Shopware\Models\Site\Site::class:
                $cacheIds[] = 's' . $entity->getId();
                break;
        }

        foreach ($cacheIds as $cacheId) {
            $this->cacheInvalidationBuffer[$cacheId] = true;
        }

        /** @var \Doctrine\ORM\EntityManager $entityManager */
        $entityManager = Shopware()->Container()->get('models');
        $entityManager->getEventManager()->addEventListener(['postFlush'], $this);
    }

    /**
     * Helper function to enable the http cache for a single shopware controller.
     *
     * @param int   $cacheTime
     * @param array $cacheIds
     */
    public function enableControllerCache($cacheTime = 3600, $cacheIds = [])
    {
        $this->response->setHeader('Cache-Control', 'public, max-age=' . $cacheTime . ', s-maxage=' . $cacheTime, true);
        $this->setCacheIdHeader($cacheIds);
    }

    /**
     * Helper function to disable the http cache for a single shopware controller
     */
    public function disableControllerCache()
    {
        $this->response->setHeader('Cache-Control', 'private', true);
    }

    private function getResponseCookie(Response $response)
    {
        $cookies = $response->getCookies();
        foreach ($cookies as $cookie) {
            if ($cookie['name'] === 'nocache') {
                return $cookie['value'];
            }
        }

        return null;
    }

    /**
     * Clears the cache
     *
     * @return bool
     */
    private function clearCache()
    {
        return $this->invalidate();
    }

    /**
     * Invalidates a given $cacheId
     *
     * This sends a http-ban-request to the proxyUrl containing
     * the $cacheId in the x-shopware-invalidates http-header
     *
     * @param string $cacheId
     *
     * @return bool
     */
    private function invalidateCacheId($cacheId)
    {
        if (!$this->Config()->get('proxyPrune')) {
            return false;
        }

        return $this->invalidate($cacheId);
    }

    /**
     * Will send BAN requests to all configured reverse proxies. If cacheId is provided,
     * the corresponding headers will be set.
     *
     * @param string $cacheId If set, only pages including these cacheIds will be invalidated
     *
     * @return bool True will be returned, if *all* operations succeeded
     */
    private function invalidate($cacheId = null)
    {
        $proxyUrl = trim($this->Config()->get('proxy'));
        if (!empty($proxyUrl)) {
            return $this->invalidateWithBANRequest($proxyUrl, $cacheId);
        }

        if ($this->get('service_container')->has('httpCache')) {
            return $this->invalidateWithStore($cacheId);
        }

        // if no explicit proxy was configured + no host is configured
        $proxyUrl = $this->getProxyUrl($this->request);
        if ($proxyUrl !== null) {
            return $this->invalidateWithBANRequest($proxyUrl, $cacheId);
        }

        return false;
    }

    /**
     * @param string $urls    Comma separated URLs
     * @param string $cacheId
     *
     * @return bool
     */
    private function invalidateWithBANRequest($urls, $cacheId)
    {
        // expand + trim proxies (comma separated)
        $urls = array_map(
            'trim',
            explode(',', $urls)
        );

        $success = true;
        foreach ($urls as $url) {
            try {
                $client = new Zend_Http_Client($url, [
                    'useragent' => 'Shopware/' . Shopware()->Config()->get('version'),
                    'timeout' => 3,
                ]);

                if ($cacheId) {
                    $client->setHeaders('x-shopware-invalidates', $cacheId);
                }

                $response = $client->request('BAN');
                if ($response->getStatus() < 200 || $response->getStatus() >= 300) {
                    $this->get('corelogger')->error(
                        'Reverse proxy returned invalid status code',
                        ['response' => $response->getRawBody(), 'code' => $response->getStatus()]
                    );
                }
            } catch (\Exception $e) {
                $this->get('corelogger')->error($e->getMessage(), ['exception' => $e]);
                $success = false;
            }
        }

        return $success;
    }

    /**
     * @param string $cacheId
     *
     * @return bool
     */
    private function invalidateWithStore($cacheId = null)
    {
        /** @var HttpCache $httpCache */
        $httpCache = $this->get('httpCache');

        /** @var Store $store */
        $store = $httpCache->getStore();

        if (!$cacheId) {
            return $store->purgeAll();
        }

        return $store->purgeByHeader('x-shopware-cache-id', $cacheId);
    }

    /**
     * Adds HTTP headers to specify that the Response needs to be parsed for ESI.
     *
     * This method only adds an ESI HTTP header if the Response has some ESI tags.
     *
     * @param Response $response A Response instance
     */
    private function addSurrogateControl(Response $response)
    {
        $response->setHeader('Surrogate-Control', 'content="ESI/1.0"');
    }

    /**
     * Checks that at least one surrogate has ESI/1.0 capability.
     *
     * @param Request $request A Request instance
     *
     * @return bool true if one surrogate has ESI/1.0 capability, false otherwise
     */
    private function hasSurrogateEsiCapability(Request $request)
    {
        if (null === $value = $request->getHeader('Surrogate-Capability')) {
            return false;
        }

        return false !== strpos($value, 'ESI/1.0');
    }

    /**
     * Add context cookie
     *
     * @param Request  $request
     * @param Response $response
     */
    private function addContextCookie(Request $request, Response $response)
    {
        /** @var CacheControl $cacheControl */
        $cacheControl = $this->get('http_cache.cache_control');

        $context = $this->get('shopware_storefront.context_service')->getShopContext();

        $cacheControl->setContextCacheKey($request, $context, $response);
    }
}