custom/plugins/MaxiaTaxSwitch6/src/Storefront/Subscriber/StorefrontSubscriber.php line 266

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Maxia\MaxiaTaxSwitch6\Storefront\Subscriber;
  3. use Maxia\MaxiaTaxSwitch6\Storefront\Events\GenerateCacheHashEvent;
  4. use Maxia\MaxiaTaxSwitch6\Storefront\Service\TaxSwitchService;
  5. use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
  6. use Shopware\Core\Framework\Event\BeforeSendResponseEvent;
  7. use Shopware\Core\Framework\Struct\ArrayEntity;
  8. use Shopware\Core\Framework\Util\Random;
  9. use Shopware\Core\PlatformRequest;
  10. use Shopware\Core\SalesChannelRequest;
  11. use Shopware\Core\System\SalesChannel\Context\SalesChannelContextServiceInterface;
  12. use Shopware\Core\System\SalesChannel\Context\SalesChannelContextServiceParameters;
  13. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  14. use Shopware\Storefront\Event\StorefrontRenderEvent;
  15. use Shopware\Storefront\Framework\Cache\CacheResponseSubscriber;
  16. use Shopware\Storefront\Framework\Cache\Event\HttpCacheGenerateKeyEvent;
  17. use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
  18. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  19. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  20. use Symfony\Component\HttpFoundation\Cookie;
  21. use Symfony\Component\HttpFoundation\Request;
  22. use Symfony\Component\HttpFoundation\RequestStack;
  23. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  24. use Symfony\Component\HttpKernel\KernelEvents;
  25. /**
  26.  * @package Maxia\MaxiaAdvBlockPrices6\Subscriber
  27.  */
  28. class StorefrontSubscriber implements EventSubscriberInterface
  29. {
  30.     public static function getSubscribedEvents(): array
  31.     {
  32.         return [
  33.             StorefrontRenderEvent::class => 'addContextExtension',
  34.             KernelEvents::RESPONSE => [
  35.                 ['updateContextHash', -2005],
  36.                 ['updateCacheControl', -1000],
  37.                 ['updateCookie', -1000]
  38.             ],
  39.             BeforeSendResponseEvent::class => [
  40.                 ['removeDuplicateSession', -100],
  41.             ],
  42.             HttpCacheGenerateKeyEvent::class => 'updateHttpCacheKey'
  43.         ];
  44.     }
  45.     /** @var RequestStack */
  46.     private $requestStack;
  47.     /** @var SalesChannelContextServiceInterface */
  48.     private $contextService;
  49.     /** @var TagAwareAdapterInterface */
  50.     private $cache;
  51.     /** @var TaxSwitchService */
  52.     private $taxSwitchService;
  53.     /**
  54.      * @param RequestStack $requestStack
  55.      * @param CartService $cartService
  56.      * @param TaxSwitchService $taxSwitchService
  57.      */
  58.     public function __construct(
  59.         RequestStack $requestStack,
  60.         EventDispatcherInterface $eventDispatcher,
  61.         SalesChannelContextServiceInterface $contextService,
  62.         TagAwareAdapterInterface $cache,
  63.         TaxSwitchService $taxSwitchService
  64.     ) {
  65.         $this->requestStack $requestStack;
  66.         $this->eventDispatcher $eventDispatcher;
  67.         $this->contextService $contextService;
  68.         $this->cache $cache;
  69.         $this->taxSwitchService $taxSwitchService;
  70.     }
  71.     /**
  72.      * @param StorefrontRenderEvent $event
  73.      */
  74.     public function addContextExtension(StorefrontRenderEvent $event)
  75.     {
  76.         $context $event->getSalesChannelContext();
  77.         $entity = new ArrayEntity($this->taxSwitchService->getContext($context));
  78.         $context->addExtension('maxiaTaxSwitch'$entity);
  79.     }
  80.     /**
  81.      * Add isNet parameter to the HTTP cache key.
  82.      *
  83.      * @param HttpCacheGenerateKeyEvent $event
  84.      * @return void
  85.      */
  86.     public function updateHttpCacheKey(HttpCacheGenerateKeyEvent $event)
  87.     {
  88.         $request $event->getRequest();
  89.         $config $this->getCachedPluginConfig($request);
  90.         if (!$config || !$config['pluginEnabled']) {
  91.             return;
  92.         }
  93.         $isNet $config['preselection'] === 'net';
  94.         if ($request->query->has($config['urlParameterName']) && $config['urlParameterActive']) {
  95.             $isNet = (bool)$request->query->get($config['urlParameterName'], $isNet);
  96.         } else if ($request->cookies->has(TaxSwitchService::COOKIE_NAME)) {
  97.             $isNet = (bool)$request->cookies->get(TaxSwitchService::COOKIE_NAME$isNet);
  98.         }
  99.         if (!$request->cookies->has(CacheResponseSubscriber::CONTEXT_CACHE_COOKIE)) {
  100.             $value $isNet $config['netContextHash'] : $config['grossContextHash'];
  101.             $request->cookies->set(CacheResponseSubscriber::CONTEXT_CACHE_COOKIE$value);
  102.         }
  103.     }
  104.     /**
  105.      * Loads some basic plugin settings relevant for generating the HTTP cache key.
  106.      *
  107.      * @param Request $request
  108.      * @return array|null
  109.      */
  110.     protected function getCachedPluginConfig(Request $request): ?array
  111.     {
  112.         $salesChannelId $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID);
  113.         $language $request->headers->get(PlatformRequest::HEADER_LANGUAGE_ID);
  114.         $currencyId $request->attributes->get(SalesChannelRequest::ATTRIBUTE_DOMAIN_CURRENCY_ID);
  115.         if (!$salesChannelId) {
  116.             return null;
  117.         }
  118.         $cacheKey 'maxia-tax-switch-config-' implode('-', [
  119.             $salesChannelId$language$currencyId
  120.         ]);
  121.         $cacheItem $this->cache->getItem($cacheKey);
  122.         if (!$cacheItem->isHit()) {
  123.             $contextToken Random::getAlphanumericString(32);
  124.             $context $this->contextService
  125.                 ->get(new SalesChannelContextServiceParameters($salesChannelId$contextToken$language$currencyId));
  126.             $config $this->taxSwitchService->getConfig($context);
  127.             $result = [
  128.                 'pluginEnabled' => $config['pluginEnabled'],
  129.                 'preselection' => $config['preselection'],
  130.                 'urlParameterActive' => $config['urlParameterActive'],
  131.                 'urlParameterName' => $config['urlParameterName'],
  132.                 'cookieRequired' => $config['cookieRequired'],
  133.                 'netContextHash' => $this->buildCacheHash($contexttrue),
  134.                 'grossContextHash' => $this->buildCacheHash($contextfalse)
  135.             ];
  136.             $cacheItem->set(json_encode($result));
  137.             $this->cache->save($cacheItem);
  138.         } else {
  139.             $result json_decode($cacheItem->get(), true);
  140.         }
  141.         return $result;
  142.     }
  143.     /**
  144.      * Updates the sw-cache-hash cookie, taking the tax switch setting into account.
  145.      *
  146.      * @param ResponseEvent $event
  147.      */
  148.     public function updateContextHash(ResponseEvent $event): void
  149.     {
  150.         $master $this->getMainRequest();
  151.         $response $event->getResponse();
  152.         if (!$master ||
  153.             !$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST) ||
  154.             !$master->attributes->get(TaxSwitchService::TAX_SWITCH_ACTIVE_ATTR)) {
  155.             return;
  156.         }
  157.         /** @var SalesChannelContext $context */
  158.         $context $master->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
  159.         if (!$context) {
  160.             return;
  161.         }
  162.         $isNet $this->taxSwitchService->getDisplayNet($context);
  163.         if ($isNet === null) {
  164.             return;
  165.         }
  166.         $cookieName CacheResponseSubscriber::CONTEXT_CACHE_COOKIE;
  167.         $currentHash $master->cookies->get($cookieName);
  168.         $newHash $this->buildCacheHash($context$isNet);
  169.         // remove context cookie from headers if already set
  170.         $responseHash null;
  171.         $headers $response->headers->all();
  172.         if (isset($headers['set-cookie'])) {
  173.             foreach ($headers['set-cookie'] as $key => $value) {
  174.                 if (preg_match('/' preg_quote($cookieName'/') . '=(.+);/'$value$matches)) {
  175.                     unset($headers['set-cookie'][$key]);
  176.                     $response->headers->set('set-cookie'$headers['set-cookie']);
  177.                     $responseHash $matches[1];
  178.                     break;
  179.                 }
  180.             }
  181.         }
  182.         // update cookie if needed
  183.         if ($currentHash !== $newHash || ($responseHash && $responseHash !== $newHash)) {
  184.             $cookie Cookie::create($cookieName$newHash);
  185.             $cookie->setSecureDefault($master->isSecure());
  186.             $response->headers->setCookie($cookie);
  187.         }
  188.     }
  189.     /**
  190.      * Returns the value for the sw-cache-hash cookie.
  191.      * @todo Extend SW implementation when possible
  192.      *
  193.      * @param SalesChannelContext $context
  194.      * @param bool $isNet
  195.      * @return string
  196.      */
  197.     protected function buildCacheHash(SalesChannelContext $contextbool $isNet): string
  198.     {
  199.         $data = [
  200.             $context->getRuleIds(),
  201.             $context->getContext()->getVersionId(),
  202.             $context->getCurrency()->getId(),
  203.             $context->getCustomer() ? 'logged-in' 'not-logged-in',
  204.             $isNet
  205.         ];
  206.         $acrisCacheHashExtension $context->getExtension('acris_cache_hash');
  207.         if ($acrisCacheHashExtension) {
  208.             $acrisCacheHash md5(json_encode($acrisCacheHashExtension->all()));
  209.             $data[] = $acrisCacheHash;
  210.         }
  211.         $event = new GenerateCacheHashEvent($data$context);
  212.         $this->eventDispatcher->dispatch($event);
  213.         return md5(json_encode($event->getParameters()));
  214.     }
  215.     /**
  216.      * Updates the tax switch cookie, if the setting has been changed.
  217.      * Will be ignored, if the cookie was not set beforehand on the client side.
  218.      *
  219.      * @param ResponseEvent $event
  220.      */
  221.     public function updateCookie(ResponseEvent $event): void
  222.     {
  223.         $master $this->getMainRequest();
  224.         if (!$master || !$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) {
  225.             return;
  226.         }
  227.         if (!$master
  228.             || !$master->attributes->get(TaxSwitchService::TAX_SWITCH_ACTIVE_ATTR)
  229.             || !$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)
  230.             || !$master->attributes->get(TaxSwitchService::STATE_CHANGED_ATTR)
  231.             || $master->cookies->get(TaxSwitchService::COOKIE_NAME) === null)
  232.         {
  233.             return;
  234.         }
  235.         /** @var SalesChannelContext $context */
  236.         $context $master->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
  237.         if (!$context) {
  238.             return;
  239.         }
  240.         $isNet $this->taxSwitchService->getDisplayNet($context);
  241.         if ($isNet === null) {
  242.             return;
  243.         }
  244.         $cookie Cookie::create(TaxSwitchService::COOKIE_NAME, (string)((int)$isNet))
  245.             ->withHttpOnly(false)
  246.             ->withExpires((new \DateTime())->add(new \DateInterval('P30D')));
  247.         $cookie->setSecureDefault($master->isSecure());
  248.         $event->getResponse()->headers->setCookie($cookie);
  249.     }
  250.     /**
  251.      * Adds Cache-Control "no-store, must-revalidate" if plugin setting "disableClientCache" is active
  252.      *
  253.      * @param ResponseEvent $event
  254.      */
  255.     public function updateCacheControl(ResponseEvent $event): void
  256.     {
  257.         $master $this->getMainRequest();
  258.         $response $event->getResponse();
  259.         if (!$master ||
  260.             !$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST) ||
  261.             !$master->attributes->get(TaxSwitchService::TAX_SWITCH_ACTIVE_ATTR)) {
  262.             return;
  263.         }
  264.         /** @var SalesChannelContext $context */
  265.         $context $master->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
  266.         if ($context && $this->taxSwitchService->getConfig($context)['disableClientCache']) {
  267.             $cacheControl $response->headers->get('Cache-Control''');
  268.             $cacheControl explode(','$cacheControl);
  269.             $cacheControl[] = 'no-store';
  270.             $cacheControl[] = 'must-revalidate';
  271.             $response->headers->set('Cache-Control'implode(', 'array_unique($cacheControl)));
  272.         }
  273.     }
  274.     /**
  275.      * Remove the duplicate session cookie as this can result in the wrong tax setting being read
  276.      * https://issues.shopware.com/issues/NEXT-10238
  277.      * @todo Remove when possible
  278.      *
  279.      * @param BeforeSendResponseEvent $event
  280.      */
  281.     public function removeDuplicateSession(BeforeSendResponseEvent $event): void
  282.     {
  283.         if (!$event->getRequest()->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) {
  284.             return;
  285.         }
  286.         $hasSessionCookie false;
  287.         if (session_id() && headers_list()) {
  288.             $cookies array_filter(headers_list(), function ($item) {
  289.                 return strpos($item'session-='.session_id()) !== false;
  290.             });
  291.             $hasSessionCookie count($cookies) > 0;
  292.         }
  293.         $response $event->getResponse();
  294.         $headers $response->headers->all();
  295.         if (isset($headers['set-cookie'])) {
  296.             foreach ($headers['set-cookie'] as $key => $value) {
  297.                 if (str_starts_with($value'PHPSESSID=')) {
  298.                     unset($headers['set-cookie'][$key]);
  299.                 }
  300.                 if ($hasSessionCookie && str_starts_with($value'session-=')) {
  301.                     if (strpos($valuesession_id()) === false) {
  302.                         unset($headers['set-cookie'][$key]);
  303.                     }
  304.                 }
  305.             }
  306.             if ($headers['set-cookie']) {
  307.                 $response->headers->set('set-cookie'$headers['set-cookie']);
  308.                 $event->setResponse($response);
  309.             }
  310.         }
  311.     }
  312.     /**
  313.      * @return \Symfony\Component\HttpFoundation\Request|null
  314.      */
  315.     protected function getMainRequest()
  316.     {
  317.         return method_exists($this->requestStack'getMainRequest')
  318.             ? $this->requestStack->getMainRequest()
  319.             : $this->requestStack->getMasterRequest();
  320.     }
  321. }