Passed
Push — master ( 366f40...c67819 )
by Christian
13:04 queued 10s
created

Framework/Routing/StorefrontSubscriber.php (2 issues)

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
It seems like $this->requestStack->getMasterRequest() can also be of type null; however, parameter $request of Shopware\Storefront\Cont...rrorController::error() does only seem to accept Symfony\Component\HttpFoundation\Request, 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 ignore-type  annotation

247
                /** @scrutinizer ignore-type */ $this->requestStack->getMasterRequest(),
Loading history...
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 ignore-type  annotation

248
                /** @scrutinizer ignore-type */ $event->getRequest()->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT)
Loading history...
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