vendor/shopware/storefront/Framework/Routing/StorefrontSubscriber.php line 278

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Storefront\Framework\Routing;
  3. use Shopware\Core\Checkout\Cart\Exception\CustomerNotLoggedInException;
  4. use Shopware\Core\Checkout\Customer\Event\CustomerLoginEvent;
  5. use Shopware\Core\Checkout\Customer\Event\CustomerLogoutEvent;
  6. use Shopware\Core\Content\Seo\HreflangLoaderInterface;
  7. use Shopware\Core\Content\Seo\HreflangLoaderParameter;
  8. use Shopware\Core\Framework\App\ActiveAppsLoader;
  9. use Shopware\Core\Framework\App\Exception\AppUrlChangeDetectedException;
  10. use Shopware\Core\Framework\App\ShopId\ShopIdProvider;
  11. use Shopware\Core\Framework\Event\BeforeSendResponseEvent;
  12. use Shopware\Core\Framework\Log\Package;
  13. use Shopware\Core\Framework\Routing\Event\SalesChannelContextResolvedEvent;
  14. use Shopware\Core\Framework\Routing\KernelListenerPriorities;
  15. use Shopware\Core\Framework\Util\Random;
  16. use Shopware\Core\PlatformRequest;
  17. use Shopware\Core\SalesChannelRequest;
  18. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  19. use Shopware\Core\System\SystemConfig\SystemConfigService;
  20. use Shopware\Storefront\Event\StorefrontRenderEvent;
  21. use Shopware\Storefront\Theme\StorefrontPluginRegistryInterface;
  22. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  23. use Symfony\Component\HttpFoundation\RedirectResponse;
  24. use Symfony\Component\HttpFoundation\RequestStack;
  25. use Symfony\Component\HttpFoundation\Response;
  26. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  27. use Symfony\Component\HttpKernel\Event\ControllerEvent;
  28. use Symfony\Component\HttpKernel\Event\ExceptionEvent;
  29. use Symfony\Component\HttpKernel\Event\RequestEvent;
  30. use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
  31. use Symfony\Component\HttpKernel\KernelEvents;
  32. use Symfony\Component\Routing\RouterInterface;
  33. /**
  34.  * @internal
  35.  */
  36. #[Package('storefront')]
  37. class StorefrontSubscriber implements EventSubscriberInterface
  38. {
  39.     /**
  40.      * @internal
  41.      */
  42.     public function __construct(
  43.         private readonly RequestStack $requestStack,
  44.         private readonly RouterInterface $router,
  45.         private readonly HreflangLoaderInterface $hreflangLoader,
  46.         private readonly MaintenanceModeResolver $maintenanceModeResolver,
  47.         private readonly ShopIdProvider $shopIdProvider,
  48.         private readonly ActiveAppsLoader $activeAppsLoader,
  49.         private readonly SystemConfigService $systemConfigService,
  50.         private readonly StorefrontPluginRegistryInterface $themeRegistry
  51.     ) {
  52.     }
  53.     public static function getSubscribedEvents(): array
  54.     {
  55.         return [
  56.             KernelEvents::REQUEST => [
  57.                 ['startSession'40],
  58.                 ['maintenanceResolver'],
  59.             ],
  60.             KernelEvents::EXCEPTION => [
  61.                 ['customerNotLoggedInHandler'],
  62.                 ['maintenanceResolver'],
  63.             ],
  64.             KernelEvents::CONTROLLER => [
  65.                 ['preventPageLoadingFromXmlHttpRequest'KernelListenerPriorities::KERNEL_CONTROLLER_EVENT_SCOPE_VALIDATE],
  66.             ],
  67.             CustomerLoginEvent::class => [
  68.                 'updateSessionAfterLogin',
  69.             ],
  70.             CustomerLogoutEvent::class => [
  71.                 'updateSessionAfterLogout',
  72.             ],
  73.             BeforeSendResponseEvent::class => [
  74.                 ['setCanonicalUrl'],
  75.             ],
  76.             StorefrontRenderEvent::class => [
  77.                 ['addHreflang'],
  78.                 ['addShopIdParameter'],
  79.                 ['addIconSetConfig'],
  80.             ],
  81.             SalesChannelContextResolvedEvent::class => [
  82.                 ['replaceContextToken'],
  83.             ],
  84.         ];
  85.     }
  86.     public function startSession(): void
  87.     {
  88.         $master $this->requestStack->getMainRequest();
  89.         if (!$master) {
  90.             return;
  91.         }
  92.         if (!$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) {
  93.             return;
  94.         }
  95.         if (!$master->hasSession()) {
  96.             return;
  97.         }
  98.         $session $master->getSession();
  99.         if (!$session->isStarted()) {
  100.             $session->setName('session-');
  101.             $session->start();
  102.             $session->set('sessionId'$session->getId());
  103.         }
  104.         $salesChannelId $master->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID);
  105.         if ($salesChannelId === null) {
  106.             /** @var SalesChannelContext|null $salesChannelContext */
  107.             $salesChannelContext $master->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
  108.             if ($salesChannelContext !== null) {
  109.                 $salesChannelId $salesChannelContext->getSalesChannel()->getId();
  110.             }
  111.         }
  112.         if ($this->shouldRenewToken($session$salesChannelId)) {
  113.             $token Random::getAlphanumericString(32);
  114.             $session->set(PlatformRequest::HEADER_CONTEXT_TOKEN$token);
  115.             $session->set(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID$salesChannelId);
  116.         }
  117.         $master->headers->set(
  118.             PlatformRequest::HEADER_CONTEXT_TOKEN,
  119.             $session->get(PlatformRequest::HEADER_CONTEXT_TOKEN)
  120.         );
  121.     }
  122.     public function updateSessionAfterLogin(CustomerLoginEvent $event): void
  123.     {
  124.         $token $event->getContextToken();
  125.         $this->updateSession($token);
  126.     }
  127.     public function updateSessionAfterLogout(): void
  128.     {
  129.         $newToken Random::getAlphanumericString(32);
  130.         $this->updateSession($newTokentrue);
  131.     }
  132.     public function updateSession(string $tokenbool $destroyOldSession false): void
  133.     {
  134.         $master $this->requestStack->getMainRequest();
  135.         if (!$master) {
  136.             return;
  137.         }
  138.         if (!$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) {
  139.             return;
  140.         }
  141.         if (!$master->hasSession()) {
  142.             return;
  143.         }
  144.         $session $master->getSession();
  145.         $session->migrate($destroyOldSession);
  146.         $session->set('sessionId'$session->getId());
  147.         $session->set(PlatformRequest::HEADER_CONTEXT_TOKEN$token);
  148.         $master->headers->set(PlatformRequest::HEADER_CONTEXT_TOKEN$token);
  149.     }
  150.     public function customerNotLoggedInHandler(ExceptionEvent $event): void
  151.     {
  152.         if (!$event->getRequest()->attributes->has(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) {
  153.             return;
  154.         }
  155.         if (!$event->getThrowable() instanceof CustomerNotLoggedInException) {
  156.             return;
  157.         }
  158.         $request $event->getRequest();
  159.         $parameters = [
  160.             'redirectTo' => $request->attributes->get('_route'),
  161.             'redirectParameters' => json_encode($request->attributes->get('_route_params'), \JSON_THROW_ON_ERROR),
  162.         ];
  163.         $redirectResponse = new RedirectResponse($this->router->generate('frontend.account.login.page'$parameters));
  164.         $event->setResponse($redirectResponse);
  165.     }
  166.     public function maintenanceResolver(RequestEvent $event): void
  167.     {
  168.         if ($this->maintenanceModeResolver->shouldRedirect($event->getRequest())) {
  169.             $event->setResponse(
  170.                 new RedirectResponse(
  171.                     $this->router->generate('frontend.maintenance.page'),
  172.                     RedirectResponse::HTTP_TEMPORARY_REDIRECT
  173.                 )
  174.             );
  175.         }
  176.     }
  177.     public function preventPageLoadingFromXmlHttpRequest(ControllerEvent $event): void
  178.     {
  179.         if (!$event->getRequest()->isXmlHttpRequest()) {
  180.             return;
  181.         }
  182.         /** @var list<string> $scope */
  183.         $scope $event->getRequest()->attributes->get(PlatformRequest::ATTRIBUTE_ROUTE_SCOPE, []);
  184.         if (!\in_array(StorefrontRouteScope::ID$scopetrue)) {
  185.             return;
  186.         }
  187.         /** @var callable(): Response $controller */
  188.         $controller $event->getController();
  189.         // happens if Controller is a closure
  190.         if (!\is_array($controller)) {
  191.             return;
  192.         }
  193.         $isAllowed $event->getRequest()->attributes->getBoolean('XmlHttpRequest');
  194.         if ($isAllowed) {
  195.             return;
  196.         }
  197.         throw new AccessDeniedHttpException('PageController can\'t be requested via XmlHttpRequest.');
  198.     }
  199.     // used to switch session token - when the context token expired
  200.     public function replaceContextToken(SalesChannelContextResolvedEvent $event): void
  201.     {
  202.         $context $event->getSalesChannelContext();
  203.         // only update session if token expired and switched
  204.         if ($event->getUsedToken() === $context->getToken()) {
  205.             return;
  206.         }
  207.         $this->updateSession($context->getToken());
  208.     }
  209.     public function setCanonicalUrl(BeforeSendResponseEvent $event): void
  210.     {
  211.         if (!$event->getResponse()->isSuccessful()) {
  212.             return;
  213.         }
  214.         if ($canonical $event->getRequest()->attributes->get(SalesChannelRequest::ATTRIBUTE_CANONICAL_LINK)) {
  215.             \assert(\is_string($canonical));
  216.             $canonical sprintf('<%s>; rel="canonical"'$canonical);
  217.             $event->getResponse()->headers->set('Link'$canonical);
  218.         }
  219.     }
  220.     public function addHreflang(StorefrontRenderEvent $event): void
  221.     {
  222.         $request $event->getRequest();
  223.         $route $request->attributes->get('_route');
  224.         if ($route === null) {
  225.             return;
  226.         }
  227.         $routeParams $request->attributes->get('_route_params', []);
  228.         $salesChannelContext $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
  229.         $parameter = new HreflangLoaderParameter($route$routeParams$salesChannelContext);
  230.         $event->setParameter('hrefLang'$this->hreflangLoader->load($parameter));
  231.     }
  232.     public function addShopIdParameter(StorefrontRenderEvent $event): void
  233.     {
  234.         if (!$this->activeAppsLoader->getActiveApps()) {
  235.             return;
  236.         }
  237.         try {
  238.             $shopId $this->shopIdProvider->getShopId();
  239.         } catch (AppUrlChangeDetectedException) {
  240.             return;
  241.         }
  242.         $event->setParameter('appShopId'$shopId);
  243.     }
  244.     public function addIconSetConfig(StorefrontRenderEvent $event): void
  245.     {
  246.         $request $event->getRequest();
  247.         // get name if theme is not inherited
  248.         $theme $request->attributes->get(SalesChannelRequest::ATTRIBUTE_THEME_NAME);
  249.         if (!$theme) {
  250.             // get theme name from base theme because for inherited themes the name is always null
  251.             $theme $request->attributes->get(SalesChannelRequest::ATTRIBUTE_THEME_BASE_NAME);
  252.         }
  253.         if (!$theme) {
  254.             return;
  255.         }
  256.         $themeConfig $this->themeRegistry->getConfigurations()->getByTechnicalName($theme);
  257.         if (!$themeConfig) {
  258.             return;
  259.         }
  260.         $iconConfig = [];
  261.         foreach ($themeConfig->getIconSets() as $pack => $path) {
  262.             $iconConfig[$pack] = [
  263.                 'path' => $path,
  264.                 'namespace' => $theme,
  265.             ];
  266.         }
  267.         $event->setParameter('themeIconConfig'$iconConfig);
  268.     }
  269.     private function shouldRenewToken(SessionInterface $session, ?string $salesChannelId null): bool
  270.     {
  271.         if (!$session->has(PlatformRequest::HEADER_CONTEXT_TOKEN) || $salesChannelId === null) {
  272.             return true;
  273.         }
  274.         if ($this->systemConfigService->get('core.systemWideLoginRegistration.isCustomerBoundToSalesChannel')) {
  275.             return $session->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID) !== $salesChannelId;
  276.         }
  277.         return false;
  278.     }
  279. }