<?php declare(strict_types=1); 
 
namespace Shopware\Storefront\Framework\Routing\NotFound; 
 
use Shopware\Core\Framework\Adapter\Cache\AbstractCacheTracer; 
use Shopware\Core\Framework\Adapter\Cache\CacheInvalidator; 
use Shopware\Core\Framework\DataAbstractionLayer\Cache\EntityCacheKeyGenerator; 
use Shopware\Core\Framework\Feature; 
use Shopware\Core\Framework\Uuid\Uuid; 
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\Core\System\SystemConfig\Event\SystemConfigChangedEvent; 
use Shopware\Storefront\Controller\ErrorController; 
use Shopware\Storefront\Framework\Routing\StorefrontResponse; 
use Symfony\Component\EventDispatcher\EventSubscriberInterface; 
use Symfony\Component\HttpFoundation\Request; 
use Symfony\Component\HttpFoundation\RequestStack; 
use Symfony\Component\HttpFoundation\Response; 
use Symfony\Component\HttpKernel\Event\ExceptionEvent; 
use Symfony\Component\HttpKernel\Exception\HttpException; 
use Symfony\Component\HttpKernel\KernelEvents; 
use Symfony\Contracts\Cache\CacheInterface; 
use Symfony\Contracts\Cache\ItemInterface; 
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; 
 
class NotFoundSubscriber implements EventSubscriberInterface 
{ 
    private const ALL_TAG = 'error-page'; 
    private const SYSTEM_CONFIG_KEY = 'core.basicInformation.http404Page'; 
 
    private ErrorController $controller; 
 
    private RequestStack $requestStack; 
 
    private SalesChannelContextServiceInterface $contextService; 
 
    private bool $kernelDebug; 
 
    private CacheInterface $cache; 
 
    /** 
     * @var AbstractCacheTracer<Response> 
     */ 
    private AbstractCacheTracer $cacheTracer; 
 
    private EntityCacheKeyGenerator $generator; 
 
    private CacheInvalidator $cacheInvalidator; 
 
    private EventDispatcherInterface $eventDispatcher; 
 
    /** 
     * @internal 
     * 
     * @param AbstractCacheTracer<Response> $cacheTracer 
     */ 
    public function __construct( 
        ErrorController $controller, 
        RequestStack $requestStack, 
        SalesChannelContextServiceInterface $contextService, 
        bool $kernelDebug, 
        CacheInterface $cache, 
        AbstractCacheTracer $cacheTracer, 
        EntityCacheKeyGenerator $generator, 
        CacheInvalidator $cacheInvalidator, 
        EventDispatcherInterface $eventDispatcher 
    ) { 
        $this->controller = $controller; 
        $this->requestStack = $requestStack; 
        $this->contextService = $contextService; 
        $this->kernelDebug = $kernelDebug; 
        $this->cache = $cache; 
        $this->cacheTracer = $cacheTracer; 
        $this->generator = $generator; 
        $this->cacheInvalidator = $cacheInvalidator; 
        $this->eventDispatcher = $eventDispatcher; 
    } 
 
    public static function getSubscribedEvents(): array 
    { 
        if (Feature::isActive('v6.5.0.0')) { 
            return [ 
                KernelEvents::EXCEPTION => [ 
                    ['onError', -100], 
                ], 
                SystemConfigChangedEvent::class => 'onSystemConfigChanged', 
            ]; 
        } 
 
        return [ 
            SystemConfigChangedEvent::class => 'onSystemConfigChanged', 
        ]; 
    } 
 
    public function onError(ExceptionEvent $event): void 
    { 
        $request = $event->getRequest(); 
        if ($this->kernelDebug || $request->attributes->has(SalesChannelRequest::ATTRIBUTE_STORE_API_PROXY)) { 
            return; 
        } 
 
        $event->stopPropagation(); 
 
        $salesChannelId = $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID, ''); 
        $domainId = $request->attributes->get(SalesChannelRequest::ATTRIBUTE_DOMAIN_ID, ''); 
        $languageId = $request->attributes->get(PlatformRequest::HEADER_LANGUAGE_ID, ''); 
 
        if (!$request->attributes->has(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT)) { 
            // When no sales-channel context is resolved, we need to resolve it now. 
            $this->setSalesChannelContext($request); 
        } 
 
        $is404StatusCode = $event->getThrowable() instanceof HttpException && $event->getThrowable()->getStatusCode() === Response::HTTP_NOT_FOUND; 
 
        // If the exception is not a 404 status code, we don't need to cache it. 
        if (!$is404StatusCode) { 
            $event->setResponse($this->controller->error( 
                $event->getThrowable(), 
                $request, 
                $event->getRequest()->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT) 
            )); 
 
            return; 
        } 
 
        /** @var SalesChannelContext $context */ 
        $context = $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT); 
 
        $name = self::buildName($salesChannelId, $domainId, $languageId); 
        $key = $this->generateKey($salesChannelId, $domainId, $languageId, $request, $context); 
 
        $response = $this->cache->get($key, function (ItemInterface $item) use ($event, $name, $context) { 
            /** @var StorefrontResponse $response */ 
            $response = $this->cacheTracer->trace($name, function () use ($event) { 
                /** @var Request $request */ 
                $request = $this->requestStack->getMainRequest(); 
 
                return $this->controller->error( 
                    $event->getThrowable(), 
                    $request, 
                    $event->getRequest()->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT) 
                ); 
            }); 
 
            $item->tag($this->generateTags($name, $event->getRequest(), $context)); 
 
            $response->setData(null); 
            $response->setContext(null); 
 
            return $response; 
        }); 
 
        $event->setResponse($response); 
    } 
 
    public function onSystemConfigChanged(SystemConfigChangedEvent $event): void 
    { 
        if ($event->getKey() !== self::SYSTEM_CONFIG_KEY) { 
            return; 
        } 
 
        $this->cacheInvalidator->invalidate([self::ALL_TAG]); 
    } 
 
    public static function buildName(string $salesChannelId, string $domainId, string $languageId): string 
    { 
        return 'error-page-' . $salesChannelId . $domainId . $languageId; 
    } 
 
    private function generateKey(string $salesChannelId, string $domainId, string $languageId, Request $request, SalesChannelContext $context): string 
    { 
        $key = self::buildName($salesChannelId, $domainId, $languageId) . md5($this->generator->getSalesChannelContextHash($context)); 
 
        $event = new NotFoundPageCacheKeyEvent($key, $request, $context); 
 
        $this->eventDispatcher->dispatch($event); 
 
        return $event->getKey(); 
    } 
 
    private function generateTags(string $name, Request $request, SalesChannelContext $context): array 
    { 
        $tags = array_merge( 
            $this->cacheTracer->get($name), 
            [$name, self::ALL_TAG] 
        ); 
 
        $event = new NotFoundPageTagsEvent($tags, $request, $context); 
 
        $this->eventDispatcher->dispatch($event); 
 
        return array_unique(array_filter($event->getTags())); 
    } 
 
    private function setSalesChannelContext(Request $request): void 
    { 
        $salesChannelId = (string) $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID); 
 
        $context = $this->contextService->get( 
            new SalesChannelContextServiceParameters( 
                $salesChannelId, 
                Uuid::randomHex(), 
                $request->headers->get(PlatformRequest::HEADER_LANGUAGE_ID), 
                $request->attributes->get(SalesChannelRequest::ATTRIBUTE_DOMAIN_CURRENCY_ID), 
                $request->attributes->get(SalesChannelRequest::ATTRIBUTE_DOMAIN_ID) 
            ) 
        ); 
 
        $request->attributes->set(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT, $context); 
    } 
}