Passed
Push — master ( 8d84d5...52d3d3 )
by Christian
11:34 queued 11s
created

StorefrontSubscriber   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 342
Duplicated Lines 0 %

Test Coverage

Coverage 33.33%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 153
c 2
b 0
f 0
dl 0
loc 342
rs 9.0399
ccs 14
cts 42
cp 0.3333
wmc 42

15 Methods

Rating   Name   Duplication   Size   Complexity  
A showHtmlExceptionResponse() 0 19 4
A replaceCsrfToken() 0 4 1
A preventPageLoadingFromXmlHttpRequest() 0 26 5
A updateSession() 0 20 4
A customerNotLoggedInHandler() 0 20 3
A updateSessionAfterLogout() 0 9 2
A __construct() 0 22 1
A updateSessionAfterLogin() 0 5 1
A maintenanceResolver() 0 7 2
A setCanonicalUrl() 0 9 3
A addHreflang() 0 13 2
B startSession() 0 42 9
A getSubscribedEvents() 0 28 1
A addShopIdParameter() 0 17 3
A setSalesChannelContext() 0 12 1

How to fix   Complexity   

Complex Class

Complex classes like StorefrontSubscriber often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use StorefrontSubscriber, and based on these observations, apply Extract Interface, too.

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\Feature;
15
use Shopware\Core\Framework\Routing\Annotation\RouteScope;
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\Context\SalesChannelContextServiceInterface;
21
use Shopware\Core\System\SalesChannel\SalesChannelContext;
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\HttpKernel\Event\ControllerEvent;
29
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
30
use Symfony\Component\HttpKernel\Event\RequestEvent;
31
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
32
use Symfony\Component\HttpKernel\KernelEvents;
33
use Symfony\Component\Routing\RouterInterface;
34 2
35
class StorefrontSubscriber implements EventSubscriberInterface
36 2
{
37 2
    /**
38 2
     * @var RequestStack
39 2
     */
40
    private $requestStack;
41 2
42
    /**
43
     * @var RouterInterface
44 2
     */
45
    private $router;
46
47
    /**
48
     * @var ErrorController
49
     */
50
    private $errorController;
51
52
    /**
53
     * @var SalesChannelContextServiceInterface
54 201
     */
55
    private $contextService;
56 201
57 201
    /**
58
     * @var bool
59
     */
60 201
    private $kernelDebug;
61 201
62
    /**
63
     * @var CsrfPlaceholderHandler
64
     */
65
    private $csrfPlaceholderHandler;
66
67
    /**
68
     * @var MaintenanceModeResolver
69
     */
70
    private $maintenanceModeResolver;
71
72
    /**
73
     * @var HreflangLoaderInterface
74
     */
75
    private $hreflangLoader;
76
77
    /**
78
     * @var ShopIdProvider
79
     */
80
    private $shopIdProvider;
81
82
    /**
83
     * @var ActiveAppsLoader
84
     */
85
    private $activeAppsLoader;
86
87
    public function __construct(
88
        RequestStack $requestStack,
89
        RouterInterface $router,
90
        ErrorController $errorController,
91
        SalesChannelContextServiceInterface $contextService,
92
        CsrfPlaceholderHandler $csrfPlaceholderHandler,
93
        HreflangLoaderInterface $hreflangLoader,
94
        bool $kernelDebug,
95
        MaintenanceModeResolver $maintenanceModeResolver,
96
        ShopIdProvider $shopIdProvider,
97 57
        ActiveAppsLoader $activeAppsLoader
98
    ) {
99 57
        $this->requestStack = $requestStack;
100 57
        $this->router = $router;
101
        $this->errorController = $errorController;
102
        $this->contextService = $contextService;
103
        $this->kernelDebug = $kernelDebug;
104
        $this->csrfPlaceholderHandler = $csrfPlaceholderHandler;
105
        $this->maintenanceModeResolver = $maintenanceModeResolver;
106
        $this->hreflangLoader = $hreflangLoader;
107
        $this->shopIdProvider = $shopIdProvider;
108
        $this->activeAppsLoader = $activeAppsLoader;
109
    }
110
111
    public static function getSubscribedEvents(): array
112
    {
113
        return [
114
            KernelEvents::REQUEST => [
115
                ['startSession', 40],
116
                ['maintenanceResolver'],
117
            ],
118
            KernelEvents::EXCEPTION => [
119
                ['showHtmlExceptionResponse', -100],
120
                ['customerNotLoggedInHandler'],
121
                ['maintenanceResolver'],
122
            ],
123
            KernelEvents::CONTROLLER => [
124
                ['preventPageLoadingFromXmlHttpRequest', KernelListenerPriorities::KERNEL_CONTROLLER_EVENT_SCOPE_VALIDATE],
125
            ],
126
            CustomerLoginEvent::class => [
127
                'updateSessionAfterLogin',
128
            ],
129
            CustomerLogoutEvent::class => [
130
                'updateSessionAfterLogout',
131
            ],
132
            BeforeSendResponseEvent::class => [
133
                ['replaceCsrfToken'],
134
                ['setCanonicalUrl'],
135
            ],
136
            StorefrontRenderEvent::class => [
137
                ['addHreflang'],
138
                ['addShopIdParameter'],
139
            ],
140
        ];
141
    }
142
143
    public function startSession(): void
144
    {
145
        $master = $this->requestStack->getMasterRequest();
146
147
        if (!$master) {
148
            return;
149
        }
150
        if (!$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) {
151
            return;
152
        }
153
154
        if (!$master->hasSession()) {
155
            return;
156
        }
157
158
        $session = $master->getSession();
159
        $applicationId = $master->attributes->get(PlatformRequest::ATTRIBUTE_OAUTH_CLIENT_ID);
160
161
        if (!$session->isStarted()) {
162
            $session->setName('session-' . $applicationId);
163
            $session->start();
164
            $session->set('sessionId', $session->getId());
165
        }
166
167
        $salesChannelId = $master->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID);
168
        if ($salesChannelId === null) {
169
            /** @var SalesChannelContext|null $salesChannelContext */
170
            $salesChannelContext = $master->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
171
            if ($salesChannelContext !== null) {
172
                $salesChannelId = $salesChannelContext->getSalesChannel()->getId();
173
            }
174
        }
175
176
        if (!$session->has(PlatformRequest::HEADER_CONTEXT_TOKEN) || $session->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID) !== $salesChannelId) {
177
            $token = Random::getAlphanumericString(32);
178
            $session->set(PlatformRequest::HEADER_CONTEXT_TOKEN, $token);
179
            $session->set(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID, $salesChannelId);
180
        }
181
182
        $master->headers->set(
183
            PlatformRequest::HEADER_CONTEXT_TOKEN,
184
            $session->get(PlatformRequest::HEADER_CONTEXT_TOKEN)
185
        );
186
    }
187
188
    public function updateSessionAfterLogin(CustomerLoginEvent $event): void
189
    {
190
        $token = $event->getContextToken();
191
192
        $this->updateSession($token);
193
    }
194
195
    public function updateSessionAfterLogout(CustomerLogoutEvent $event): void
196
    {
197
        if (!Feature::isActive('FEATURE_NEXT_10058')) {
198
            return;
199
        }
200
201
        $newToken = $event->getSalesChannelContext()->getToken();
202
203
        $this->updateSession($newToken);
204
    }
205
206
    public function updateSession(string $token): void
207
    {
208
        $master = $this->requestStack->getMasterRequest();
209
        if (!$master) {
210
            return;
211
        }
212
        if (!$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) {
213
            return;
214
        }
215
216
        if (!$master->hasSession()) {
217
            return;
218
        }
219
220
        $session = $master->getSession();
221
        $session->migrate();
222
        $session->set('sessionId', $session->getId());
223
224
        $session->set(PlatformRequest::HEADER_CONTEXT_TOKEN, $token);
225
        $master->headers->set(PlatformRequest::HEADER_CONTEXT_TOKEN, $token);
226
    }
227
228
    public function showHtmlExceptionResponse(ExceptionEvent $event): void
229
    {
230
        if ($this->kernelDebug) {
231
            return;
232
        }
233
234
        if (!$event->getRequest()->attributes->has(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT)) {
235
            //When no saleschannel context is resolved, we need to resolve it now.
236
            $this->setSalesChannelContext($event);
237
        }
238
239
        if ($event->getRequest()->attributes->has(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT)) {
240
            $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

240
            /** @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...
241
            $response = $this->errorController->error(
242
                $event->getThrowable(),
243
                $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

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

244
                /** @scrutinizer ignore-type */ $event->getRequest()->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT)
Loading history...
245
            );
246
            $event->setResponse($response);
247
        }
248
    }
249
250
    public function customerNotLoggedInHandler(ExceptionEvent $event): void
251
    {
252
        if (!$event->getRequest()->attributes->has(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) {
253
            return;
254
        }
255
256
        if (!$event->getThrowable() instanceof CustomerNotLoggedInException) {
257
            return;
258
        }
259
260
        $request = $event->getRequest();
261
262
        $parameters = [
263
            'redirectTo' => $request->attributes->get('_route'),
264
            'redirectParameters' => json_encode($request->attributes->get('_route_params')),
265
        ];
266
267
        $redirectResponse = new RedirectResponse($this->router->generate('frontend.account.login.page', $parameters));
268
269
        $event->setResponse($redirectResponse);
270
    }
271
272
    public function maintenanceResolver(RequestEvent $event): void
273
    {
274
        if ($this->maintenanceModeResolver->shouldRedirect($event->getRequest())) {
275
            $event->setResponse(
276
                new RedirectResponse(
277
                    $this->router->generate('frontend.maintenance.page'),
278
                    RedirectResponse::HTTP_TEMPORARY_REDIRECT
279
                )
280
            );
281
        }
282
    }
283
284
    public function preventPageLoadingFromXmlHttpRequest(ControllerEvent $event): void
285
    {
286
        if (!$event->getRequest()->isXmlHttpRequest()) {
287
            return;
288
        }
289
290
        /** @var RouteScope $scope */
291
        $scope = $event->getRequest()->attributes->get(PlatformRequest::ATTRIBUTE_ROUTE_SCOPE, new RouteScope(['scopes' => []]));
292
        if (!$scope->hasScope(StorefrontRouteScope::ID)) {
293
            return;
294
        }
295
296
        $controller = $event->getController();
297
298
        // happens if Controller is a closure
299
        if (!is_array($controller)) {
300
            return;
301
        }
302
303
        $isAllowed = $event->getRequest()->attributes->getBoolean('XmlHttpRequest', false);
304
305
        if ($isAllowed) {
306
            return;
307
        }
308
309
        throw new AccessDeniedHttpException('PageController can\'t be requested via XmlHttpRequest.');
310
    }
311
312
    public function setCanonicalUrl(BeforeSendResponseEvent $event): void
313
    {
314
        if (!$event->getResponse()->isSuccessful()) {
315
            return;
316
        }
317
318
        if ($canonical = $event->getRequest()->attributes->get(SalesChannelRequest::ATTRIBUTE_CANONICAL_LINK)) {
319
            $canonical = sprintf('<%s>; rel="canonical"', $canonical);
320
            $event->getResponse()->headers->set('Link', $canonical);
321
        }
322
    }
323
324
    public function replaceCsrfToken(BeforeSendResponseEvent $event): void
325
    {
326
        $event->setResponse(
327
            $this->csrfPlaceholderHandler->replaceCsrfToken($event->getResponse(), $event->getRequest())
328
        );
329
    }
330
331
    public function addHreflang(StorefrontRenderEvent $event): void
332
    {
333
        $request = $event->getRequest();
334
        $route = $request->attributes->get('_route');
335
336
        if ($route === null) {
337
            return;
338
        }
339
340
        $routeParams = $request->attributes->get('_route_params', []);
341
        $salesChannelContext = $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
342
        $parameter = new HreflangLoaderParameter($route, $routeParams, $salesChannelContext);
343
        $event->setParameter('hrefLang', $this->hreflangLoader->load($parameter));
344
    }
345
346
    public function addShopIdParameter(StorefrontRenderEvent $event): void
347
    {
348
        if (!$this->activeAppsLoader->getActiveApps()) {
349
            return;
350
        }
351
352
        try {
353
            $shopId = $this->shopIdProvider->getShopId();
354
        } catch (AppUrlChangeDetectedException $e) {
355
            return;
356
        }
357
358
        $event->setParameter('appShopId', $shopId);
359
        /*
360
         * @deprecated tag:v6.4.0 use `appShopId` instead
361
         */
362
        $event->setParameter('swagShopId', $shopId);
363
    }
364
365
    private function setSalesChannelContext(ExceptionEvent $event): void
366
    {
367
        $contextToken = $event->getRequest()->headers->get(PlatformRequest::HEADER_CONTEXT_TOKEN);
368
        $salesChannelId = $event->getRequest()->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID);
369
370
        $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

370
        $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...
371
            $salesChannelId,
372
            $contextToken,
373
            $event->getRequest()->headers->get(PlatformRequest::HEADER_LANGUAGE_ID),
374
            $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

374
        /** @scrutinizer ignore-call */ 
375
        $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...
375
        );
376
        $event->getRequest()->attributes->set(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT, $context);
377
    }
378
}
379