1 | <?php declare(strict_types=1); |
||||
2 | |||||
3 | namespace Shopware\Storefront\Framework\Routing; |
||||
4 | |||||
5 | use Shopware\Core\Checkout\Cart\Exception\CustomerNotLoggedInException; |
||||
6 | use Shopware\Core\Checkout\Customer\Event\CustomerLoginEvent; |
||||
7 | use Shopware\Core\Checkout\Customer\Event\CustomerLogoutEvent; |
||||
8 | use Shopware\Core\Content\Seo\HreflangLoaderInterface; |
||||
9 | use Shopware\Core\Content\Seo\HreflangLoaderParameter; |
||||
10 | use Shopware\Core\Framework\App\ActiveAppsLoader; |
||||
11 | use Shopware\Core\Framework\App\Exception\AppUrlChangeDetectedException; |
||||
12 | use Shopware\Core\Framework\App\ShopId\ShopIdProvider; |
||||
13 | use Shopware\Core\Framework\Event\BeforeSendResponseEvent; |
||||
14 | use Shopware\Core\Framework\Routing\Annotation\RouteScope; |
||||
15 | use Shopware\Core\Framework\Routing\KernelListenerPriorities; |
||||
16 | use Shopware\Core\Framework\Util\Random; |
||||
17 | use Shopware\Core\PlatformRequest; |
||||
18 | use Shopware\Core\SalesChannelRequest; |
||||
19 | use Shopware\Core\System\SalesChannel\Context\SalesChannelContextServiceInterface; |
||||
20 | use Shopware\Core\System\SalesChannel\SalesChannelContext; |
||||
21 | use Shopware\Core\System\SystemConfig\SystemConfigService; |
||||
22 | use Shopware\Storefront\Controller\ErrorController; |
||||
23 | use Shopware\Storefront\Event\StorefrontRenderEvent; |
||||
24 | use Shopware\Storefront\Framework\Csrf\CsrfPlaceholderHandler; |
||||
25 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; |
||||
26 | use Symfony\Component\HttpFoundation\RedirectResponse; |
||||
27 | use Symfony\Component\HttpFoundation\RequestStack; |
||||
28 | use Symfony\Component\HttpFoundation\Session\SessionInterface; |
||||
29 | use Symfony\Component\HttpKernel\Event\ControllerEvent; |
||||
30 | use Symfony\Component\HttpKernel\Event\ExceptionEvent; |
||||
31 | use Symfony\Component\HttpKernel\Event\RequestEvent; |
||||
32 | use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; |
||||
33 | use Symfony\Component\HttpKernel\KernelEvents; |
||||
34 | 2 | use Symfony\Component\Routing\RouterInterface; |
|||
35 | |||||
36 | 2 | class StorefrontSubscriber implements EventSubscriberInterface |
|||
37 | 2 | { |
|||
38 | 2 | /** |
|||
39 | 2 | * @var RequestStack |
|||
40 | */ |
||||
41 | 2 | private $requestStack; |
|||
42 | |||||
43 | /** |
||||
44 | 2 | * @var RouterInterface |
|||
45 | */ |
||||
46 | private $router; |
||||
47 | |||||
48 | /** |
||||
49 | * @var ErrorController |
||||
50 | */ |
||||
51 | private $errorController; |
||||
52 | |||||
53 | /** |
||||
54 | 201 | * @var SalesChannelContextServiceInterface |
|||
55 | */ |
||||
56 | 201 | private $contextService; |
|||
57 | 201 | ||||
58 | /** |
||||
59 | * @var bool |
||||
60 | 201 | */ |
|||
61 | 201 | private $kernelDebug; |
|||
62 | |||||
63 | /** |
||||
64 | * @var CsrfPlaceholderHandler |
||||
65 | */ |
||||
66 | private $csrfPlaceholderHandler; |
||||
67 | |||||
68 | /** |
||||
69 | * @var MaintenanceModeResolver |
||||
70 | */ |
||||
71 | private $maintenanceModeResolver; |
||||
72 | |||||
73 | /** |
||||
74 | * @var HreflangLoaderInterface |
||||
75 | */ |
||||
76 | private $hreflangLoader; |
||||
77 | |||||
78 | /** |
||||
79 | * @var ShopIdProvider |
||||
80 | */ |
||||
81 | private $shopIdProvider; |
||||
82 | |||||
83 | /** |
||||
84 | * @var ActiveAppsLoader |
||||
85 | */ |
||||
86 | private $activeAppsLoader; |
||||
87 | |||||
88 | /** |
||||
89 | * @var SystemConfigService |
||||
90 | */ |
||||
91 | private $systemConfigService; |
||||
92 | |||||
93 | public function __construct( |
||||
94 | RequestStack $requestStack, |
||||
95 | RouterInterface $router, |
||||
96 | ErrorController $errorController, |
||||
97 | 57 | SalesChannelContextServiceInterface $contextService, |
|||
98 | CsrfPlaceholderHandler $csrfPlaceholderHandler, |
||||
99 | 57 | HreflangLoaderInterface $hreflangLoader, |
|||
100 | 57 | bool $kernelDebug, |
|||
101 | MaintenanceModeResolver $maintenanceModeResolver, |
||||
102 | ShopIdProvider $shopIdProvider, |
||||
103 | ActiveAppsLoader $activeAppsLoader, |
||||
104 | SystemConfigService $systemConfigService |
||||
105 | ) { |
||||
106 | $this->requestStack = $requestStack; |
||||
107 | $this->router = $router; |
||||
108 | $this->errorController = $errorController; |
||||
109 | $this->contextService = $contextService; |
||||
110 | $this->kernelDebug = $kernelDebug; |
||||
111 | $this->csrfPlaceholderHandler = $csrfPlaceholderHandler; |
||||
112 | $this->maintenanceModeResolver = $maintenanceModeResolver; |
||||
113 | $this->hreflangLoader = $hreflangLoader; |
||||
114 | $this->shopIdProvider = $shopIdProvider; |
||||
115 | $this->activeAppsLoader = $activeAppsLoader; |
||||
116 | $this->systemConfigService = $systemConfigService; |
||||
117 | } |
||||
118 | |||||
119 | public static function getSubscribedEvents(): array |
||||
120 | { |
||||
121 | return [ |
||||
122 | KernelEvents::REQUEST => [ |
||||
123 | ['startSession', 40], |
||||
124 | ['maintenanceResolver'], |
||||
125 | ], |
||||
126 | KernelEvents::EXCEPTION => [ |
||||
127 | ['showHtmlExceptionResponse', -100], |
||||
128 | ['customerNotLoggedInHandler'], |
||||
129 | ['maintenanceResolver'], |
||||
130 | ], |
||||
131 | KernelEvents::CONTROLLER => [ |
||||
132 | ['preventPageLoadingFromXmlHttpRequest', KernelListenerPriorities::KERNEL_CONTROLLER_EVENT_SCOPE_VALIDATE], |
||||
133 | ], |
||||
134 | CustomerLoginEvent::class => [ |
||||
135 | 'updateSessionAfterLogin', |
||||
136 | ], |
||||
137 | CustomerLogoutEvent::class => [ |
||||
138 | 'updateSessionAfterLogout', |
||||
139 | ], |
||||
140 | BeforeSendResponseEvent::class => [ |
||||
141 | ['replaceCsrfToken'], |
||||
142 | ['setCanonicalUrl'], |
||||
143 | ], |
||||
144 | StorefrontRenderEvent::class => [ |
||||
145 | ['addHreflang'], |
||||
146 | ['addShopIdParameter'], |
||||
147 | ], |
||||
148 | ]; |
||||
149 | } |
||||
150 | |||||
151 | public function startSession(): void |
||||
152 | { |
||||
153 | $master = $this->requestStack->getMasterRequest(); |
||||
154 | |||||
155 | if (!$master) { |
||||
156 | return; |
||||
157 | } |
||||
158 | if (!$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) { |
||||
159 | return; |
||||
160 | } |
||||
161 | |||||
162 | if (!$master->hasSession()) { |
||||
163 | return; |
||||
164 | } |
||||
165 | |||||
166 | $session = $master->getSession(); |
||||
167 | $applicationId = $master->attributes->get(PlatformRequest::ATTRIBUTE_OAUTH_CLIENT_ID); |
||||
168 | |||||
169 | if (!$session->isStarted()) { |
||||
170 | $session->setName('session-' . $applicationId); |
||||
171 | $session->start(); |
||||
172 | $session->set('sessionId', $session->getId()); |
||||
173 | } |
||||
174 | |||||
175 | $salesChannelId = $master->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID); |
||||
176 | if ($salesChannelId === null) { |
||||
177 | /** @var SalesChannelContext|null $salesChannelContext */ |
||||
178 | $salesChannelContext = $master->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT); |
||||
179 | if ($salesChannelContext !== null) { |
||||
180 | $salesChannelId = $salesChannelContext->getSalesChannel()->getId(); |
||||
181 | } |
||||
182 | } |
||||
183 | |||||
184 | if ($this->shouldRenewToken($session, $salesChannelId)) { |
||||
185 | $token = Random::getAlphanumericString(32); |
||||
186 | $session->set(PlatformRequest::HEADER_CONTEXT_TOKEN, $token); |
||||
187 | $session->set(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID, $salesChannelId); |
||||
188 | } |
||||
189 | |||||
190 | $master->headers->set( |
||||
191 | PlatformRequest::HEADER_CONTEXT_TOKEN, |
||||
192 | $session->get(PlatformRequest::HEADER_CONTEXT_TOKEN) |
||||
193 | ); |
||||
194 | } |
||||
195 | |||||
196 | public function updateSessionAfterLogin(CustomerLoginEvent $event): void |
||||
197 | { |
||||
198 | $token = $event->getContextToken(); |
||||
199 | |||||
200 | $this->updateSession($token); |
||||
201 | } |
||||
202 | |||||
203 | public function updateSessionAfterLogout(CustomerLogoutEvent $event): void |
||||
204 | { |
||||
205 | $newToken = $event->getSalesChannelContext()->getToken(); |
||||
206 | |||||
207 | $this->updateSession($newToken); |
||||
208 | } |
||||
209 | |||||
210 | public function updateSession(string $token): void |
||||
211 | { |
||||
212 | $master = $this->requestStack->getMasterRequest(); |
||||
213 | if (!$master) { |
||||
214 | return; |
||||
215 | } |
||||
216 | if (!$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) { |
||||
217 | return; |
||||
218 | } |
||||
219 | |||||
220 | if (!$master->hasSession()) { |
||||
221 | return; |
||||
222 | } |
||||
223 | |||||
224 | $session = $master->getSession(); |
||||
225 | $session->migrate(); |
||||
226 | $session->set('sessionId', $session->getId()); |
||||
227 | |||||
228 | $session->set(PlatformRequest::HEADER_CONTEXT_TOKEN, $token); |
||||
229 | $master->headers->set(PlatformRequest::HEADER_CONTEXT_TOKEN, $token); |
||||
230 | } |
||||
231 | |||||
232 | public function showHtmlExceptionResponse(ExceptionEvent $event): void |
||||
233 | { |
||||
234 | if ($this->kernelDebug) { |
||||
235 | return; |
||||
236 | } |
||||
237 | |||||
238 | if (!$event->getRequest()->attributes->has(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT)) { |
||||
239 | //When no saleschannel context is resolved, we need to resolve it now. |
||||
240 | $this->setSalesChannelContext($event); |
||||
241 | } |
||||
242 | |||||
243 | if ($event->getRequest()->attributes->has(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT)) { |
||||
244 | $event->stopPropagation(); |
||||
245 | $response = $this->errorController->error( |
||||
246 | $event->getThrowable(), |
||||
247 | $this->requestStack->getMasterRequest(), |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
248 | $event->getRequest()->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT) |
||||
0 ignored issues
–
show
It seems like
$event->getRequest()->ge...CHANNEL_CONTEXT_OBJECT) can also be of type null ; however, parameter $context of Shopware\Storefront\Cont...rrorController::error() does only seem to accept Shopware\Core\System\Sal...nel\SalesChannelContext , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
249 | ); |
||||
250 | $event->setResponse($response); |
||||
251 | } |
||||
252 | } |
||||
253 | |||||
254 | public function customerNotLoggedInHandler(ExceptionEvent $event): void |
||||
255 | { |
||||
256 | if (!$event->getRequest()->attributes->has(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) { |
||||
257 | return; |
||||
258 | } |
||||
259 | |||||
260 | if (!$event->getThrowable() instanceof CustomerNotLoggedInException) { |
||||
261 | return; |
||||
262 | } |
||||
263 | |||||
264 | $request = $event->getRequest(); |
||||
265 | |||||
266 | $parameters = [ |
||||
267 | 'redirectTo' => $request->attributes->get('_route'), |
||||
268 | 'redirectParameters' => json_encode($request->attributes->get('_route_params')), |
||||
269 | ]; |
||||
270 | |||||
271 | $redirectResponse = new RedirectResponse($this->router->generate('frontend.account.login.page', $parameters)); |
||||
272 | |||||
273 | $event->setResponse($redirectResponse); |
||||
274 | } |
||||
275 | |||||
276 | public function maintenanceResolver(RequestEvent $event): void |
||||
277 | { |
||||
278 | if ($this->maintenanceModeResolver->shouldRedirect($event->getRequest())) { |
||||
279 | $event->setResponse( |
||||
280 | new RedirectResponse( |
||||
281 | $this->router->generate('frontend.maintenance.page'), |
||||
282 | RedirectResponse::HTTP_TEMPORARY_REDIRECT |
||||
283 | ) |
||||
284 | ); |
||||
285 | } |
||||
286 | } |
||||
287 | |||||
288 | public function preventPageLoadingFromXmlHttpRequest(ControllerEvent $event): void |
||||
289 | { |
||||
290 | if (!$event->getRequest()->isXmlHttpRequest()) { |
||||
291 | return; |
||||
292 | } |
||||
293 | |||||
294 | /** @var RouteScope $scope */ |
||||
295 | $scope = $event->getRequest()->attributes->get(PlatformRequest::ATTRIBUTE_ROUTE_SCOPE, new RouteScope(['scopes' => []])); |
||||
296 | if (!$scope->hasScope(StorefrontRouteScope::ID)) { |
||||
297 | return; |
||||
298 | } |
||||
299 | |||||
300 | $controller = $event->getController(); |
||||
301 | |||||
302 | // happens if Controller is a closure |
||||
303 | if (!\is_array($controller)) { |
||||
304 | return; |
||||
305 | } |
||||
306 | |||||
307 | $isAllowed = $event->getRequest()->attributes->getBoolean('XmlHttpRequest', false); |
||||
308 | |||||
309 | if ($isAllowed) { |
||||
310 | return; |
||||
311 | } |
||||
312 | |||||
313 | throw new AccessDeniedHttpException('PageController can\'t be requested via XmlHttpRequest.'); |
||||
314 | } |
||||
315 | |||||
316 | public function setCanonicalUrl(BeforeSendResponseEvent $event): void |
||||
317 | { |
||||
318 | if (!$event->getResponse()->isSuccessful()) { |
||||
319 | return; |
||||
320 | } |
||||
321 | |||||
322 | if ($canonical = $event->getRequest()->attributes->get(SalesChannelRequest::ATTRIBUTE_CANONICAL_LINK)) { |
||||
323 | $canonical = sprintf('<%s>; rel="canonical"', $canonical); |
||||
324 | $event->getResponse()->headers->set('Link', $canonical); |
||||
325 | } |
||||
326 | } |
||||
327 | |||||
328 | public function replaceCsrfToken(BeforeSendResponseEvent $event): void |
||||
329 | { |
||||
330 | $event->setResponse( |
||||
331 | $this->csrfPlaceholderHandler->replaceCsrfToken($event->getResponse(), $event->getRequest()) |
||||
332 | ); |
||||
333 | } |
||||
334 | |||||
335 | public function addHreflang(StorefrontRenderEvent $event): void |
||||
336 | { |
||||
337 | $request = $event->getRequest(); |
||||
338 | $route = $request->attributes->get('_route'); |
||||
339 | |||||
340 | if ($route === null) { |
||||
341 | return; |
||||
342 | } |
||||
343 | |||||
344 | $routeParams = $request->attributes->get('_route_params', []); |
||||
345 | $salesChannelContext = $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT); |
||||
346 | $parameter = new HreflangLoaderParameter($route, $routeParams, $salesChannelContext); |
||||
347 | $event->setParameter('hrefLang', $this->hreflangLoader->load($parameter)); |
||||
348 | } |
||||
349 | |||||
350 | public function addShopIdParameter(StorefrontRenderEvent $event): void |
||||
351 | { |
||||
352 | if (!$this->activeAppsLoader->getActiveApps()) { |
||||
353 | return; |
||||
354 | } |
||||
355 | |||||
356 | try { |
||||
357 | $shopId = $this->shopIdProvider->getShopId(); |
||||
358 | } catch (AppUrlChangeDetectedException $e) { |
||||
359 | return; |
||||
360 | } |
||||
361 | |||||
362 | $event->setParameter('appShopId', $shopId); |
||||
363 | /* |
||||
364 | * @deprecated tag:v6.4.0 use `appShopId` instead |
||||
365 | */ |
||||
366 | $event->setParameter('swagShopId', $shopId); |
||||
367 | } |
||||
368 | |||||
369 | private function setSalesChannelContext(ExceptionEvent $event): void |
||||
370 | { |
||||
371 | $contextToken = $event->getRequest()->headers->get(PlatformRequest::HEADER_CONTEXT_TOKEN); |
||||
372 | $salesChannelId = $event->getRequest()->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID); |
||||
373 | |||||
374 | $context = $this->contextService->get( |
||||
375 | $salesChannelId, |
||||
376 | $contextToken, |
||||
377 | $event->getRequest()->headers->get(PlatformRequest::HEADER_LANGUAGE_ID), |
||||
378 | $event->getRequest()->attributes->get(SalesChannelRequest::ATTRIBUTE_DOMAIN_CURRENCY_ID) |
||||
379 | ); |
||||
380 | $event->getRequest()->attributes->set(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT, $context); |
||||
381 | } |
||||
382 | |||||
383 | private function shouldRenewToken(SessionInterface $session, ?string $salesChannelId = null): bool |
||||
384 | { |
||||
385 | if (!$session->has(PlatformRequest::HEADER_CONTEXT_TOKEN) || $salesChannelId === null) { |
||||
386 | return true; |
||||
387 | } |
||||
388 | |||||
389 | if ($this->systemConfigService->get('core.systemWideLoginRegistration.isCustomerBoundToSalesChannel')) { |
||||
390 | return $session->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID) !== $salesChannelId; |
||||
391 | } |
||||
392 | |||||
393 | return false; |
||||
394 | } |
||||
395 | } |
||||
396 |