<?php declare(strict_types=1);
namespace Maxia\MaxiaTaxSwitch6\Subscriber;
use Maxia\MaxiaTaxSwitch6\Events\TaxSwitchEvent;
use Maxia\MaxiaTaxSwitch6\Service\TaxSwitchService;
use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice;
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
use Shopware\Core\Framework\Event\BeforeSendResponseEvent;
use Shopware\Core\Framework\Struct\ArrayEntity;
use Shopware\Core\PlatformRequest;
use Shopware\Core\SalesChannelRequest;
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\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Cookie;
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',
TaxSwitchEvent::class => 'markCartModified',
KernelEvents::RESPONSE => [
['updateContextHash', -2005],
['updateCacheControl', -1000],
['updateCookie', -1000]
],
BeforeSendResponseEvent::class => [
['removeDuplicateSession', -100],
],
HttpCacheGenerateKeyEvent::class => [
'updateHttpCacheKey'
]
];
}
/**
* @var RequestStack
*/
private $requestStack;
/**
* @var CartService
*/
private $cartService;
/**
* @var TaxSwitchService
*/
private $taxSwitchService;
/**
* @param RequestStack $requestStack
* @param CartService $cartService
* @param TaxSwitchService $taxSwitchService
*/
public function __construct(
RequestStack $requestStack,
CartService $cartService,
TaxSwitchService $taxSwitchService
) {
$this->requestStack = $requestStack;
$this->cartService = $cartService;
$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);
}
/**
* @param TaxSwitchEvent $event
*/
public function markCartModified(TaxSwitchEvent $event)
{
$context = $event->getSalesChannelContext();
$taxState = $event->getDisplayNet() ? CartPrice::TAX_STATE_NET : CartPrice::TAX_STATE_GROSS;
$context->setTaxState($taxState);
$context->getCurrentCustomerGroup()->setDisplayGross(!$event->getDisplayNet());
$cart = $this->cartService->getCart($context->getToken(), $context, CartService::SALES_CHANNEL, false);
$price = $cart->getPrice();
$cart->setPrice(new CartPrice(
$price->getNetPrice(),
$price->getTotalPrice(),
$price->getPositionPrice(),
$price->getCalculatedTaxes(),
$price->getTaxRules(),
$taxState
));
$cart->markModified();
}
/**
* 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);
}
}
/**
* @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)));
}
}
/**
* 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);
}
/**
* Add tax state to http cache key
*
* @param HttpCacheGenerateKeyEvent $event
* @return void
*/
public function updateHttpCacheKey(HttpCacheGenerateKeyEvent $event)
{
$isNet = $event->getRequest()->cookies->get(TaxSwitchService::COOKIE_NAME);
if ($isNet !== null) {
$event->setHash(hash('sha256', $event->getHash() . '-' . ($isNet ? 1 : 0)));
}
}
/**
* Remove the duplicate session cookie, if it is being set.
* 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);
}
}
}
/**
* 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(),
$isNet
];
$acrisCacheHashExtension = $context->getExtension('acris_cache_hash');
if ($acrisCacheHashExtension) {
$acrisCacheHash = md5(json_encode($acrisCacheHashExtension->all()));
$data[] = $acrisCacheHash;
}
return md5(json_encode($data));
}
/**
* @return \Symfony\Component\HttpFoundation\Request|null
*/
protected function getMainRequest()
{
return method_exists($this->requestStack, 'getMainRequest')
? $this->requestStack->getMainRequest()
: $this->requestStack->getMasterRequest();
}
}