Passed
Push — master ( 7a2782...032b3c )
by Christian
11:03
created

StorefrontSubscriber::getSubscribedEvents()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 28
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 20
nc 1
nop 0
dl 0
loc 28
rs 9.6
c 0
b 0
f 0
ccs 0
cts 3
cp 0
crap 2
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\Storefront\Controller\ErrorController;
22
use Shopware\Storefront\Event\StorefrontRenderEvent;
23
use Shopware\Storefront\Framework\Csrf\CsrfPlaceholderHandler;
24
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
25
use Symfony\Component\HttpFoundation\RedirectResponse;
26
use Symfony\Component\HttpFoundation\RequestStack;
27
use Symfony\Component\HttpKernel\Event\ControllerEvent;
28
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
29
use Symfony\Component\HttpKernel\Event\RequestEvent;
30
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
31
use Symfony\Component\HttpKernel\KernelEvents;
32
use Symfony\Component\Routing\RouterInterface;
33
34 2
class StorefrontSubscriber implements EventSubscriberInterface
35
{
36 2
    /**
37 2
     * @var RequestStack
38 2
     */
39 2
    private $requestStack;
40
41 2
    /**
42
     * @var RouterInterface
43
     */
44 2
    private $router;
45
46
    /**
47
     * @var ErrorController
48
     */
49
    private $errorController;
50
51
    /**
52
     * @var SalesChannelContextServiceInterface
53
     */
54 201
    private $contextService;
55
56 201
    /**
57 201
     * @var bool
58
     */
59
    private $kernelDebug;
60 201
61 201
    /**
62
     * @var CsrfPlaceholderHandler
63
     */
64
    private $csrfPlaceholderHandler;
65
66
    /**
67
     * @var MaintenanceModeResolver
68
     */
69
    private $maintenanceModeResolver;
70
71
    /**
72
     * @var HreflangLoaderInterface
73
     */
74
    private $hreflangLoader;
75
76
    /**
77
     * @var ShopIdProvider|null
78
     */
79
    private $shopIdProvider;
80
81
    /**
82
     * @var ActiveAppsLoader|null
83
     */
84
    private $activeAppsLoader;
85
86
    public function __construct(
87
        RequestStack $requestStack,
88
        RouterInterface $router,
89
        ErrorController $errorController,
90
        SalesChannelContextServiceInterface $contextService,
91
        CsrfPlaceholderHandler $csrfPlaceholderHandler,
92
        HreflangLoaderInterface $hreflangLoader,
93
        bool $kernelDebug,
94
        MaintenanceModeResolver $maintenanceModeResolver,
95
        ?ShopIdProvider $shopIdProvider,
96
        ?ActiveAppsLoader $activeAppsLoader
97 57
    ) {
98
        $this->requestStack = $requestStack;
99 57
        $this->router = $router;
100 57
        $this->errorController = $errorController;
101
        $this->contextService = $contextService;
102
        $this->kernelDebug = $kernelDebug;
103
        $this->csrfPlaceholderHandler = $csrfPlaceholderHandler;
104
        $this->maintenanceModeResolver = $maintenanceModeResolver;
105
        $this->hreflangLoader = $hreflangLoader;
106
        $this->shopIdProvider = $shopIdProvider;
107
        $this->activeAppsLoader = $activeAppsLoader;
108
    }
109
110
    public static function getSubscribedEvents(): array
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
            ],
139
        ];
140
    }
141
142
    public function startSession(): void
143
    {
144
        $master = $this->requestStack->getMasterRequest();
145
146
        if (!$master) {
147
            return;
148
        }
149
        if (!$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) {
150
            return;
151
        }
152
153
        if (!$master->hasSession()) {
154
            return;
155
        }
156
157
        $session = $master->getSession();
158
        $applicationId = $master->attributes->get(PlatformRequest::ATTRIBUTE_OAUTH_CLIENT_ID);
159
160
        if (!$session->isStarted()) {
161
            $session->setName('session-' . $applicationId);
162
            $session->start();
163
            $session->set('sessionId', $session->getId());
164
        }
165
166
        if (!$session->has(PlatformRequest::HEADER_CONTEXT_TOKEN)) {
167
            $token = Random::getAlphanumericString(32);
168
            $session->set(PlatformRequest::HEADER_CONTEXT_TOKEN, $token);
169
        }
170
171
        $master->headers->set(
172
            PlatformRequest::HEADER_CONTEXT_TOKEN,
173
            $session->get(PlatformRequest::HEADER_CONTEXT_TOKEN)
174
        );
175
    }
176
177
    public function updateSessionAfterLogin(CustomerLoginEvent $event): void
178
    {
179
        $token = $event->getContextToken();
180
181
        $this->updateSession($token);
182
    }
183
184
    public function updateSessionAfterLogout(CustomerLogoutEvent $event): void
185
    {
186
        if (!Feature::isActive('FEATURE_NEXT_10058')) {
187
            return;
188
        }
189
190
        $newToken = $event->getSalesChannelContext()->getToken();
191
192
        $this->updateSession($newToken);
193
    }
194
195
    public function updateSession(string $token): void
196
    {
197
        $master = $this->requestStack->getMasterRequest();
198
        if (!$master) {
199
            return;
200
        }
201
        if (!$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) {
202
            return;
203
        }
204
205
        if (!$master->hasSession()) {
206
            return;
207
        }
208
209
        $session = $master->getSession();
210
        $session->migrate();
211
        $session->set('sessionId', $session->getId());
212
213
        $session->set(PlatformRequest::HEADER_CONTEXT_TOKEN, $token);
214
        $master->headers->set(PlatformRequest::HEADER_CONTEXT_TOKEN, $token);
215
    }
216
217
    public function showHtmlExceptionResponse(ExceptionEvent $event): void
218
    {
219
        if ($this->kernelDebug) {
220
            return;
221
        }
222
223
        if (!$event->getRequest()->attributes->has(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT)) {
224
            //When no saleschannel context is resolved, we need to resolve it now.
225
            $this->setSalesChannelContext($event);
226
        }
227
228
        if ($event->getRequest()->attributes->has(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT)) {
229
            $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

229
            /** @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...
230
            $response = $this->errorController->error(
231
                $event->getThrowable(),
232
                $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

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

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

362
        $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...
363
            $salesChannelId,
364
            $contextToken,
365
            $event->getRequest()->headers->get(PlatformRequest::HEADER_LANGUAGE_ID),
366
            $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

366
        /** @scrutinizer ignore-call */ 
367
        $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...
367
        );
368
        $event->getRequest()->attributes->set(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT, $context);
369
    }
370
}
371