<?php declare(strict_types=1);
namespace Maxia\MaxiaTaxSwitch6\Storefront\Subscriber;
use Maxia\MaxiaTaxSwitch6\Storefront\Events\GenerateCacheHashEvent;
use Maxia\MaxiaTaxSwitch6\Storefront\Service\TaxSwitchService;
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
use Shopware\Core\Framework\Event\BeforeSendResponseEvent;
use Shopware\Core\Framework\Struct\ArrayEntity;
use Shopware\Core\Framework\Util\Random;
use Shopware\Core\PlatformRequest;
use Shopware\Core\SalesChannelRequest;
use Shopware\Core\System\SalesChannel\Context\SalesChannelContextServiceInterface;
use Shopware\Core\System\SalesChannel\Context\SalesChannelContextServiceParameters;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Event\StorefrontRenderEvent;
use Shopware\Storefront\Framework\Cache\CacheResponseSubscriber;
use Shopware\Storefront\Framework\Cache\Event\HttpCacheGenerateKeyEvent;
use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* @package Maxia\MaxiaAdvBlockPrices6\Subscriber
*/
class StorefrontSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
StorefrontRenderEvent::class => 'addContextExtension',
KernelEvents::RESPONSE => [
['updateContextHash', -2005],
['updateCacheControl', -1000],
['updateCookie', -1000]
],
BeforeSendResponseEvent::class => [
['removeDuplicateSession', -100],
],
HttpCacheGenerateKeyEvent::class => 'updateHttpCacheKey'
];
}
/** @var RequestStack */
private $requestStack;
/** @var SalesChannelContextServiceInterface */
private $contextService;
/** @var TagAwareAdapterInterface */
private $cache;
/** @var TaxSwitchService */
private $taxSwitchService;
/**
* @param RequestStack $requestStack
* @param CartService $cartService
* @param TaxSwitchService $taxSwitchService
*/
public function __construct(
RequestStack $requestStack,
EventDispatcherInterface $eventDispatcher,
SalesChannelContextServiceInterface $contextService,
TagAwareAdapterInterface $cache,
TaxSwitchService $taxSwitchService
) {
$this->requestStack = $requestStack;
$this->eventDispatcher = $eventDispatcher;
$this->contextService = $contextService;
$this->cache = $cache;
$this->taxSwitchService = $taxSwitchService;
}
/**
* @param StorefrontRenderEvent $event
*/
public function addContextExtension(StorefrontRenderEvent $event)
{
$context = $event->getSalesChannelContext();
$entity = new ArrayEntity($this->taxSwitchService->getContext($context));
$context->addExtension('maxiaTaxSwitch', $entity);
}
/**
* Add isNet parameter to the HTTP cache key.
*
* @param HttpCacheGenerateKeyEvent $event
* @return void
*/
public function updateHttpCacheKey(HttpCacheGenerateKeyEvent $event)
{
$request = $event->getRequest();
$config = $this->getCachedPluginConfig($request);
if (!$config || !$config['pluginEnabled']) {
return;
}
$isNet = $config['preselection'] === 'net';
if ($request->query->has($config['urlParameterName']) && $config['urlParameterActive']) {
$isNet = (bool)$request->query->get($config['urlParameterName'], $isNet);
} else if ($request->cookies->has(TaxSwitchService::COOKIE_NAME)) {
$isNet = (bool)$request->cookies->get(TaxSwitchService::COOKIE_NAME, $isNet);
}
if (!$request->cookies->has(CacheResponseSubscriber::CONTEXT_CACHE_COOKIE)) {
$value = $isNet ? $config['netContextHash'] : $config['grossContextHash'];
$request->cookies->set(CacheResponseSubscriber::CONTEXT_CACHE_COOKIE, $value);
}
}
/**
* Loads some basic plugin settings relevant for generating the HTTP cache key.
*
* @param Request $request
* @return array|null
*/
protected function getCachedPluginConfig(Request $request): ?array
{
$salesChannelId = $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID);
$language = $request->headers->get(PlatformRequest::HEADER_LANGUAGE_ID);
$currencyId = $request->attributes->get(SalesChannelRequest::ATTRIBUTE_DOMAIN_CURRENCY_ID);
if (!$salesChannelId) {
return null;
}
$cacheKey = 'maxia-tax-switch-config-' . implode('-', [
$salesChannelId, $language, $currencyId
]);
$cacheItem = $this->cache->getItem($cacheKey);
if (!$cacheItem->isHit()) {
$contextToken = Random::getAlphanumericString(32);
$context = $this->contextService
->get(new SalesChannelContextServiceParameters($salesChannelId, $contextToken, $language, $currencyId));
$config = $this->taxSwitchService->getConfig($context);
$result = [
'pluginEnabled' => $config['pluginEnabled'],
'preselection' => $config['preselection'],
'urlParameterActive' => $config['urlParameterActive'],
'urlParameterName' => $config['urlParameterName'],
'cookieRequired' => $config['cookieRequired'],
'netContextHash' => $this->buildCacheHash($context, true),
'grossContextHash' => $this->buildCacheHash($context, false)
];
$cacheItem->set(json_encode($result));
$this->cache->save($cacheItem);
} else {
$result = json_decode($cacheItem->get(), true);
}
return $result;
}
/**
* Updates the sw-cache-hash cookie, taking the tax switch setting into account.
*
* @param ResponseEvent $event
*/
public function updateContextHash(ResponseEvent $event): void
{
$master = $this->getMainRequest();
$response = $event->getResponse();
if (!$master ||
!$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST) ||
!$master->attributes->get(TaxSwitchService::TAX_SWITCH_ACTIVE_ATTR)) {
return;
}
/** @var SalesChannelContext $context */
$context = $master->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
if (!$context) {
return;
}
$isNet = $this->taxSwitchService->getDisplayNet($context);
if ($isNet === null) {
return;
}
$cookieName = CacheResponseSubscriber::CONTEXT_CACHE_COOKIE;
$currentHash = $master->cookies->get($cookieName);
$newHash = $this->buildCacheHash($context, $isNet);
// remove context cookie from headers if already set
$responseHash = null;
$headers = $response->headers->all();
if (isset($headers['set-cookie'])) {
foreach ($headers['set-cookie'] as $key => $value) {
if (preg_match('/' . preg_quote($cookieName, '/') . '=(.+);/', $value, $matches)) {
unset($headers['set-cookie'][$key]);
$response->headers->set('set-cookie', $headers['set-cookie']);
$responseHash = $matches[1];
break;
}
}
}
// update cookie if needed
if ($currentHash !== $newHash || ($responseHash && $responseHash !== $newHash)) {
$cookie = Cookie::create($cookieName, $newHash);
$cookie->setSecureDefault($master->isSecure());
$response->headers->setCookie($cookie);
}
}
/**
* Returns the value for the sw-cache-hash cookie.
* @todo Extend SW implementation when possible
*
* @param SalesChannelContext $context
* @param bool $isNet
* @return string
*/
protected function buildCacheHash(SalesChannelContext $context, bool $isNet): string
{
$data = [
$context->getRuleIds(),
$context->getContext()->getVersionId(),
$context->getCurrency()->getId(),
$context->getCustomer() ? 'logged-in' : 'not-logged-in',
$isNet
];
$acrisCacheHashExtension = $context->getExtension('acris_cache_hash');
if ($acrisCacheHashExtension) {
$acrisCacheHash = md5(json_encode($acrisCacheHashExtension->all()));
$data[] = $acrisCacheHash;
}
$event = new GenerateCacheHashEvent($data, $context);
$this->eventDispatcher->dispatch($event);
return md5(json_encode($event->getParameters()));
}
/**
* Updates the tax switch cookie, if the setting has been changed.
* Will be ignored, if the cookie was not set beforehand on the client side.
*
* @param ResponseEvent $event
*/
public function updateCookie(ResponseEvent $event): void
{
$master = $this->getMainRequest();
if (!$master || !$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) {
return;
}
if (!$master
|| !$master->attributes->get(TaxSwitchService::TAX_SWITCH_ACTIVE_ATTR)
|| !$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)
|| !$master->attributes->get(TaxSwitchService::STATE_CHANGED_ATTR)
|| $master->cookies->get(TaxSwitchService::COOKIE_NAME) === null)
{
return;
}
/** @var SalesChannelContext $context */
$context = $master->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
if (!$context) {
return;
}
$isNet = $this->taxSwitchService->getDisplayNet($context);
if ($isNet === null) {
return;
}
$cookie = Cookie::create(TaxSwitchService::COOKIE_NAME, (string)((int)$isNet))
->withHttpOnly(false)
->withExpires((new \DateTime())->add(new \DateInterval('P30D')));
$cookie->setSecureDefault($master->isSecure());
$event->getResponse()->headers->setCookie($cookie);
}
/**
* Adds Cache-Control "no-store, must-revalidate" if plugin setting "disableClientCache" is active
*
* @param ResponseEvent $event
*/
public function updateCacheControl(ResponseEvent $event): void
{
$master = $this->getMainRequest();
$response = $event->getResponse();
if (!$master ||
!$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST) ||
!$master->attributes->get(TaxSwitchService::TAX_SWITCH_ACTIVE_ATTR)) {
return;
}
/** @var SalesChannelContext $context */
$context = $master->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
if ($context && $this->taxSwitchService->getConfig($context)['disableClientCache']) {
$cacheControl = $response->headers->get('Cache-Control', '');
$cacheControl = explode(',', $cacheControl);
$cacheControl[] = 'no-store';
$cacheControl[] = 'must-revalidate';
$response->headers->set('Cache-Control', implode(', ', array_unique($cacheControl)));
}
}
/**
* Remove the duplicate session cookie as this can result in the wrong tax setting being read
* https://issues.shopware.com/issues/NEXT-10238
* @todo Remove when possible
*
* @param BeforeSendResponseEvent $event
*/
public function removeDuplicateSession(BeforeSendResponseEvent $event): void
{
if (!$event->getRequest()->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) {
return;
}
$hasSessionCookie = false;
if (session_id() && headers_list()) {
$cookies = array_filter(headers_list(), function ($item) {
return strpos($item, 'session-='.session_id()) !== false;
});
$hasSessionCookie = count($cookies) > 0;
}
$response = $event->getResponse();
$headers = $response->headers->all();
if (isset($headers['set-cookie'])) {
foreach ($headers['set-cookie'] as $key => $value) {
if (str_starts_with($value, 'PHPSESSID=')) {
unset($headers['set-cookie'][$key]);
}
if ($hasSessionCookie && str_starts_with($value, 'session-=')) {
if (strpos($value, session_id()) === false) {
unset($headers['set-cookie'][$key]);
}
}
}
if ($headers['set-cookie']) {
$response->headers->set('set-cookie', $headers['set-cookie']);
$event->setResponse($response);
}
}
}
/**
* @return \Symfony\Component\HttpFoundation\Request|null
*/
protected function getMainRequest()
{
return method_exists($this->requestStack, 'getMainRequest')
? $this->requestStack->getMainRequest()
: $this->requestStack->getMasterRequest();
}
}