Passed
Push — master ( 913faa...617544 )
by Christian
57:46 queued 46:10
created

StorefrontSubscriber::updateSession()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 4
nop 1
dl 0
loc 20
rs 9.8666
c 0
b 0
f 0
ccs 0
cts 0
cp 0
crap 20
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();
0 ignored issues
show
Deprecated Code introduced by
The function Symfony\Component\EventD...vent::stopPropagation() has been deprecated: since Symfony 4.3, use "Symfony\Contracts\EventDispatcher\Event" instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

244
            /** @scrutinizer ignore-deprecated */ $event->stopPropagation();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
245
            $response = $this->errorController->error(
246
                $event->getThrowable(),
247
                $this->requestStack->getMasterRequest(),
0 ignored issues
show
Bug introduced by
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
Bug introduced by
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(
0 ignored issues
show
Deprecated Code introduced by
The function Shopware\Core\System\Sal...ServiceInterface::get() has been deprecated: tag:v6.4.0 - Parameter $currencyId will be mandatory in future implementation ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

374
        $context = /** @scrutinizer ignore-deprecated */ $this->contextService->get(

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
375
            $salesChannelId,
376
            $contextToken,
377
            $event->getRequest()->headers->get(PlatformRequest::HEADER_LANGUAGE_ID),
378
            $event->getRequest()->attributes->get(SalesChannelRequest::ATTRIBUTE_DOMAIN_CURRENCY_ID)
0 ignored issues
show
Unused Code introduced by
The call to Shopware\Core\System\Sal...ServiceInterface::get() has too many arguments starting with $event->getRequest()->at...UTE_DOMAIN_CURRENCY_ID). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

378
        /** @scrutinizer ignore-call */ 
379
        $context = $this->contextService->get(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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