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

Open in your IDE?
  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\Feature;
  13. use Shopware\Core\Framework\Log\Package;
  14. use Shopware\Core\Framework\Routing\Annotation\RouteScope;
  15. use Shopware\Core\Framework\Routing\Event\SalesChannelContextResolvedEvent;
  16. use Shopware\Core\Framework\Routing\KernelListenerPriorities;
  17. use Shopware\Core\Framework\Util\Random;
  18. use Shopware\Core\PlatformRequest;
  19. use Shopware\Core\SalesChannelRequest;
  20. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  21. use Shopware\Core\System\SystemConfig\SystemConfigService;
  22. use Shopware\Storefront\Event\StorefrontRenderEvent;
  23. use Shopware\Storefront\Framework\Csrf\CsrfPlaceholderHandler;
  24. use Shopware\Storefront\Framework\Routing\NotFound\NotFoundSubscriber;
  25. use Shopware\Storefront\Theme\StorefrontPluginRegistryInterface;
  26. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  27. use Symfony\Component\HttpFoundation\RedirectResponse;
  28. use Symfony\Component\HttpFoundation\RequestStack;
  29. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  30. use Symfony\Component\HttpKernel\Event\ControllerEvent;
  31. use Symfony\Component\HttpKernel\Event\ExceptionEvent;
  32. use Symfony\Component\HttpKernel\Event\RequestEvent;
  33. use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
  34. use Symfony\Component\HttpKernel\KernelEvents;
  35. use Symfony\Component\Routing\RouterInterface;
  36. /**
  37. * @deprecated tag:v6.5.0 - reason:becomes-internal - EventSubscribers will become internal in v6.5.0
  38. */
  39. #[Package('storefront')]
  40. class StorefrontSubscriber implements EventSubscriberInterface
  41. {
  42. private RequestStack $requestStack;
  43. private RouterInterface $router;
  44. private CsrfPlaceholderHandler $csrfPlaceholderHandler;
  45. private MaintenanceModeResolver $maintenanceModeResolver;
  46. private HreflangLoaderInterface $hreflangLoader;
  47. private ShopIdProvider $shopIdProvider;
  48. private ActiveAppsLoader $activeAppsLoader;
  49. private SystemConfigService $systemConfigService;
  50. private StorefrontPluginRegistryInterface $themeRegistry;
  51. private NotFoundSubscriber $notFoundSubscriber;
  52. /**
  53. * @internal
  54. */
  55. public function __construct(
  56. RequestStack $requestStack,
  57. RouterInterface $router,
  58. CsrfPlaceholderHandler $csrfPlaceholderHandler,
  59. HreflangLoaderInterface $hreflangLoader,
  60. MaintenanceModeResolver $maintenanceModeResolver,
  61. ShopIdProvider $shopIdProvider,
  62. ActiveAppsLoader $activeAppsLoader,
  63. SystemConfigService $systemConfigService,
  64. StorefrontPluginRegistryInterface $themeRegistry,
  65. NotFoundSubscriber $notFoundSubscriber
  66. ) {
  67. $this->requestStack = $requestStack;
  68. $this->router = $router;
  69. $this->csrfPlaceholderHandler = $csrfPlaceholderHandler;
  70. $this->maintenanceModeResolver = $maintenanceModeResolver;
  71. $this->hreflangLoader = $hreflangLoader;
  72. $this->shopIdProvider = $shopIdProvider;
  73. $this->activeAppsLoader = $activeAppsLoader;
  74. $this->systemConfigService = $systemConfigService;
  75. $this->themeRegistry = $themeRegistry;
  76. $this->notFoundSubscriber = $notFoundSubscriber;
  77. }
  78. public static function getSubscribedEvents(): array
  79. {
  80. if (Feature::isActive('v6.5.0.0')) {
  81. return [
  82. KernelEvents::REQUEST => [
  83. ['startSession', 40],
  84. ['maintenanceResolver'],
  85. ],
  86. KernelEvents::EXCEPTION => [
  87. ['customerNotLoggedInHandler'],
  88. ['maintenanceResolver'],
  89. ],
  90. KernelEvents::CONTROLLER => [
  91. ['preventPageLoadingFromXmlHttpRequest', KernelListenerPriorities::KERNEL_CONTROLLER_EVENT_SCOPE_VALIDATE],
  92. ],
  93. CustomerLoginEvent::class => [
  94. 'updateSessionAfterLogin',
  95. ],
  96. CustomerLogoutEvent::class => [
  97. 'updateSessionAfterLogout',
  98. ],
  99. BeforeSendResponseEvent::class => [
  100. ['setCanonicalUrl'],
  101. ],
  102. StorefrontRenderEvent::class => [
  103. ['addHreflang'],
  104. ['addShopIdParameter'],
  105. ['addIconSetConfig'],
  106. ],
  107. SalesChannelContextResolvedEvent::class => [
  108. ['replaceContextToken'],
  109. ],
  110. ];
  111. }
  112. return [
  113. KernelEvents::REQUEST => [
  114. ['startSession', 40],
  115. ['maintenanceResolver'],
  116. ],
  117. KernelEvents::EXCEPTION => [
  118. ['showHtmlExceptionResponse', -100],
  119. ['customerNotLoggedInHandler'],
  120. ['maintenanceResolver'],
  121. ],
  122. KernelEvents::CONTROLLER => [
  123. ['preventPageLoadingFromXmlHttpRequest', KernelListenerPriorities::KERNEL_CONTROLLER_EVENT_SCOPE_VALIDATE],
  124. ],
  125. CustomerLoginEvent::class => [
  126. 'updateSessionAfterLogin',
  127. ],
  128. CustomerLogoutEvent::class => [
  129. 'updateSessionAfterLogout',
  130. ],
  131. BeforeSendResponseEvent::class => [
  132. ['replaceCsrfToken'],
  133. ['setCanonicalUrl'],
  134. ],
  135. StorefrontRenderEvent::class => [
  136. ['addHreflang'],
  137. ['addShopIdParameter'],
  138. ['addIconSetConfig'],
  139. ],
  140. SalesChannelContextResolvedEvent::class => [
  141. ['replaceContextToken'],
  142. ],
  143. ];
  144. }
  145. public function startSession(): void
  146. {
  147. $master = $this->requestStack->getMainRequest();
  148. if (!$master) {
  149. return;
  150. }
  151. if (!$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) {
  152. return;
  153. }
  154. if (!$master->hasSession()) {
  155. return;
  156. }
  157. $session = $master->getSession();
  158. if (!$session->isStarted()) {
  159. $session->setName('session-');
  160. $session->start();
  161. $session->set('sessionId', $session->getId());
  162. }
  163. $salesChannelId = $master->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID);
  164. if ($salesChannelId === null) {
  165. /** @var SalesChannelContext|null $salesChannelContext */
  166. $salesChannelContext = $master->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
  167. if ($salesChannelContext !== null) {
  168. $salesChannelId = $salesChannelContext->getSalesChannel()->getId();
  169. }
  170. }
  171. if ($this->shouldRenewToken($session, $salesChannelId)) {
  172. $token = Random::getAlphanumericString(32);
  173. $session->set(PlatformRequest::HEADER_CONTEXT_TOKEN, $token);
  174. $session->set(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID, $salesChannelId);
  175. }
  176. $master->headers->set(
  177. PlatformRequest::HEADER_CONTEXT_TOKEN,
  178. $session->get(PlatformRequest::HEADER_CONTEXT_TOKEN)
  179. );
  180. }
  181. public function updateSessionAfterLogin(CustomerLoginEvent $event): void
  182. {
  183. $token = $event->getContextToken();
  184. $this->updateSession($token);
  185. }
  186. public function updateSessionAfterLogout(): void
  187. {
  188. $newToken = Random::getAlphanumericString(32);
  189. $this->updateSession($newToken, true);
  190. }
  191. public function updateSession(string $token, bool $destroyOldSession = false): void
  192. {
  193. $master = $this->requestStack->getMainRequest();
  194. if (!$master) {
  195. return;
  196. }
  197. if (!$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) {
  198. return;
  199. }
  200. if (!$master->hasSession()) {
  201. return;
  202. }
  203. $session = $master->getSession();
  204. $session->migrate($destroyOldSession);
  205. $session->set('sessionId', $session->getId());
  206. $session->set(PlatformRequest::HEADER_CONTEXT_TOKEN, $token);
  207. $master->headers->set(PlatformRequest::HEADER_CONTEXT_TOKEN, $token);
  208. }
  209. /**
  210. * @deprecated tag:v6.5.0 - reason:remove-subscriber - Use `NotFoundSubscriber::onError` instead
  211. */
  212. public function showHtmlExceptionResponse(ExceptionEvent $event): void
  213. {
  214. $this->notFoundSubscriber->onError($event);
  215. }
  216. public function customerNotLoggedInHandler(ExceptionEvent $event): void
  217. {
  218. if (!$event->getRequest()->attributes->has(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) {
  219. return;
  220. }
  221. if (!$event->getThrowable() instanceof CustomerNotLoggedInException) {
  222. return;
  223. }
  224. $request = $event->getRequest();
  225. $parameters = [
  226. 'redirectTo' => $request->attributes->get('_route'),
  227. 'redirectParameters' => json_encode($request->attributes->get('_route_params')),
  228. ];
  229. $redirectResponse = new RedirectResponse($this->router->generate('frontend.account.login.page', $parameters));
  230. $event->setResponse($redirectResponse);
  231. }
  232. public function maintenanceResolver(RequestEvent $event): void
  233. {
  234. if ($this->maintenanceModeResolver->shouldRedirect($event->getRequest())) {
  235. $event->setResponse(
  236. new RedirectResponse(
  237. $this->router->generate('frontend.maintenance.page'),
  238. RedirectResponse::HTTP_TEMPORARY_REDIRECT
  239. )
  240. );
  241. }
  242. }
  243. public function preventPageLoadingFromXmlHttpRequest(ControllerEvent $event): void
  244. {
  245. if (!$event->getRequest()->isXmlHttpRequest()) {
  246. return;
  247. }
  248. /** @var RouteScope|list<string> $scope */
  249. $scope = $event->getRequest()->attributes->get(PlatformRequest::ATTRIBUTE_ROUTE_SCOPE, []);
  250. if ($scope instanceof RouteScope) {
  251. $scope = $scope->getScopes();
  252. }
  253. if (!\in_array(StorefrontRouteScope::ID, $scope, true)) {
  254. return;
  255. }
  256. $controller = $event->getController();
  257. // happens if Controller is a closure
  258. if (!\is_array($controller)) {
  259. return;
  260. }
  261. $isAllowed = $event->getRequest()->attributes->getBoolean('XmlHttpRequest', false);
  262. if ($isAllowed) {
  263. return;
  264. }
  265. throw new AccessDeniedHttpException('PageController can\'t be requested via XmlHttpRequest.');
  266. }
  267. // used to switch session token - when the context token expired
  268. public function replaceContextToken(SalesChannelContextResolvedEvent $event): void
  269. {
  270. $context = $event->getSalesChannelContext();
  271. // only update session if token expired and switched
  272. if ($event->getUsedToken() === $context->getToken()) {
  273. return;
  274. }
  275. $this->updateSession($context->getToken());
  276. }
  277. public function setCanonicalUrl(BeforeSendResponseEvent $event): void
  278. {
  279. if (!$event->getResponse()->isSuccessful()) {
  280. return;
  281. }
  282. if ($canonical = $event->getRequest()->attributes->get(SalesChannelRequest::ATTRIBUTE_CANONICAL_LINK)) {
  283. $canonical = sprintf('<%s>; rel="canonical"', $canonical);
  284. $event->getResponse()->headers->set('Link', $canonical);
  285. }
  286. }
  287. /**
  288. * @deprecated tag:v6.5.0 - replaceCsrfToken method will be removed as the csrf system will be removed in favor for the samesite approach
  289. */
  290. public function replaceCsrfToken(BeforeSendResponseEvent $event): void
  291. {
  292. if (Feature::isActive('v6.5.0.0')) {
  293. return;
  294. }
  295. Feature::triggerDeprecationOrThrow(
  296. 'v6.5.0.0',
  297. Feature::deprecatedMethodMessage(__CLASS__, __METHOD__, 'v6.5.0.0')
  298. );
  299. $event->setResponse(
  300. $this->csrfPlaceholderHandler->replaceCsrfToken($event->getResponse(), $event->getRequest())
  301. );
  302. }
  303. public function addHreflang(StorefrontRenderEvent $event): void
  304. {
  305. $request = $event->getRequest();
  306. $route = $request->attributes->get('_route');
  307. if ($route === null) {
  308. return;
  309. }
  310. $routeParams = $request->attributes->get('_route_params', []);
  311. $salesChannelContext = $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
  312. $parameter = new HreflangLoaderParameter($route, $routeParams, $salesChannelContext);
  313. $event->setParameter('hrefLang', $this->hreflangLoader->load($parameter));
  314. }
  315. public function addShopIdParameter(StorefrontRenderEvent $event): void
  316. {
  317. if (!$this->activeAppsLoader->getActiveApps()) {
  318. return;
  319. }
  320. try {
  321. $shopId = $this->shopIdProvider->getShopId();
  322. } catch (AppUrlChangeDetectedException $e) {
  323. return;
  324. }
  325. $event->setParameter('appShopId', $shopId);
  326. }
  327. public function addIconSetConfig(StorefrontRenderEvent $event): void
  328. {
  329. $request = $event->getRequest();
  330. // get name if theme is not inherited
  331. $theme = $request->attributes->get(SalesChannelRequest::ATTRIBUTE_THEME_NAME);
  332. if (!$theme) {
  333. // get theme name from base theme because for inherited themes the name is always null
  334. $theme = $request->attributes->get(SalesChannelRequest::ATTRIBUTE_THEME_BASE_NAME);
  335. }
  336. if (!$theme) {
  337. return;
  338. }
  339. $themeConfig = $this->themeRegistry->getConfigurations()->getByTechnicalName($theme);
  340. if (!$themeConfig) {
  341. return;
  342. }
  343. $iconConfig = [];
  344. foreach ($themeConfig->getIconSets() as $pack => $path) {
  345. $iconConfig[$pack] = [
  346. 'path' => $path,
  347. 'namespace' => $theme,
  348. ];
  349. }
  350. $event->setParameter('themeIconConfig', $iconConfig);
  351. }
  352. private function shouldRenewToken(SessionInterface $session, ?string $salesChannelId = null): bool
  353. {
  354. if (!$session->has(PlatformRequest::HEADER_CONTEXT_TOKEN) || $salesChannelId === null) {
  355. return true;
  356. }
  357. if ($this->systemConfigService->get('core.systemWideLoginRegistration.isCustomerBoundToSalesChannel')) {
  358. return $session->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID) !== $salesChannelId;
  359. }
  360. return false;
  361. }
  362. }