Passed
Push — master ( 5c7ccb...e787c0 )
by Christian
22:18 queued 11:37
created

StorefrontSubscriber::addHreflang()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 1
dl 0
loc 13
ccs 0
cts 0
cp 0
crap 6
rs 10
c 0
b 0
f 0
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
        $salesChannelId = $master->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID);
167
168
        if (!$session->has(PlatformRequest::HEADER_CONTEXT_TOKEN) || $session->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID) !== $salesChannelId) {
169
            $token = Random::getAlphanumericString(32);
170
            $session->set(PlatformRequest::HEADER_CONTEXT_TOKEN, $token);
171
            $session->set(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID, $salesChannelId);
172
        }
173
174
        $master->headers->set(
175
            PlatformRequest::HEADER_CONTEXT_TOKEN,
176
            $session->get(PlatformRequest::HEADER_CONTEXT_TOKEN)
177
        );
178
    }
179
180
    public function updateSessionAfterLogin(CustomerLoginEvent $event): void
181
    {
182
        $token = $event->getContextToken();
183
184
        $this->updateSession($token);
185
    }
186
187
    public function updateSessionAfterLogout(CustomerLogoutEvent $event): void
188
    {
189
        if (!Feature::isActive('FEATURE_NEXT_10058')) {
190
            return;
191
        }
192
193
        $newToken = $event->getSalesChannelContext()->getToken();
194
195
        $this->updateSession($newToken);
196
    }
197
198
    public function updateSession(string $token): void
199
    {
200
        $master = $this->requestStack->getMasterRequest();
201
        if (!$master) {
202
            return;
203
        }
204
        if (!$master->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) {
205
            return;
206
        }
207
208
        if (!$master->hasSession()) {
209
            return;
210
        }
211
212
        $session = $master->getSession();
213
        $session->migrate();
214
        $session->set('sessionId', $session->getId());
215
216
        $session->set(PlatformRequest::HEADER_CONTEXT_TOKEN, $token);
217
        $master->headers->set(PlatformRequest::HEADER_CONTEXT_TOKEN, $token);
218
    }
219
220
    public function showHtmlExceptionResponse(ExceptionEvent $event): void
221
    {
222
        if ($this->kernelDebug) {
223
            return;
224
        }
225
226
        if (!$event->getRequest()->attributes->has(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT)) {
227
            //When no saleschannel context is resolved, we need to resolve it now.
228
            $this->setSalesChannelContext($event);
229
        }
230
231
        if ($event->getRequest()->attributes->has(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT)) {
232
            $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

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

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

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

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

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