Passed
Push — main ( 6d3c72...2f3aa0 )
by Daniel
04:34
created

AddMercureTokenListener   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 108
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 2
Bugs 2 Features 0
Metric Value
eloc 43
dl 0
loc 108
ccs 0
cts 45
cp 0
rs 10
c 2
b 2
f 0
wmc 17

5 Methods

Rating   Name   Duplication   Size   Complexity  
A onKernelResponse() 0 22 4
A buildAbsoluteUriTemplate() 0 11 4
A getSubscribeIrisForResource() 0 23 4
A getMercureResourceOperation() 0 20 4
A __construct() 0 10 1
1
<?php
2
3
/*
4
 * This file is part of the Silverback API Components Bundle Project
5
 *
6
 * (c) Daniel West <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Silverback\ApiComponentsBundle\EventListener\Mercure;
15
16
use ApiPlatform\Exception\OperationNotFoundException;
17
use ApiPlatform\Metadata\HttpOperation;
18
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
19
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
20
use ApiPlatform\Util\CorsTrait;
21
use Silverback\ApiComponentsBundle\Annotation\Publishable;
22
use Silverback\ApiComponentsBundle\Helper\Publishable\PublishableStatusChecker;
23
use Symfony\Component\HttpFoundation\Cookie;
24
use Symfony\Component\HttpKernel\Event\ResponseEvent;
25
use Symfony\Component\Mercure\Authorization;
26
use Symfony\Component\Routing\RequestContext;
27
28
29
class AddMercureTokenListener
30
{
31
    use CorsTrait;
0 ignored issues
show
Bug introduced by
The trait ApiPlatform\Util\CorsTrait requires the property $headers which is not provided by Silverback\ApiComponents...AddMercureTokenListener.
Loading history...
32
33
    public function __construct(
34
        private readonly ResourceNameCollectionFactoryInterface     $resourceNameCollectionFactory,
35
        private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
36
        private readonly PublishableStatusChecker                   $publishableStatusChecker,
37
        private readonly RequestContext                             $requestContext,
38
        private readonly Authorization                              $mercureAuthorization,
39
        private readonly string                                     $cookieSameSite = Cookie::SAMESITE_STRICT,
40
        private readonly ?string                                    $hubName = null
41
    )
42
    {
43
    }
44
45
    /**
46
     * Sends the Mercure header on each response.
47
     * Probably lock this on the "/me" route.
48
     */
49
    public function onKernelResponse(ResponseEvent $event): void
50
    {
51
        $request = $event->getRequest();
52
        // Prevent issues with NelmioCorsBundle
53
        if ($this->isPreflightRequest($request)) {
54
            return;
55
        }
56
57
        $subscribeIris = [];
58
        $response = $event->getResponse();
59
        foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
60
            if ($resourceIris = $this->getSubscribeIrisForResource($resourceClass)) {
61
                $subscribeIris[] = $resourceIris;
62
            }
63
        }
64
        $subscribeIris = array_merge([], ...$subscribeIris);
65
66
        // Todo: await merge of https://github.com/symfony/mercure/pull/93 to remove ability to publish any updates and set to  null
67
        // May also be able to await a mercure bundle update to set the cookie samesite in mercure configs
68
        $cookie = $this->mercureAuthorization->createCookie($request, $request, $subscribeIris, [], $this->hubName);
69
        $cookie->withSameSite($this->cookieSameSite);
70
        $response->headers->setCookie($cookie);
71
    }
72
73
    private function getSubscribeIrisForResource(string $resourceClass): ?array
74
    {
75
        $operation = $this->getMercureResourceOperation($resourceClass);
76
        if (!$operation) {
77
            return null;
78
        }
79
80
        $refl = new \ReflectionClass($operation->getClass());
81
        $isPublishable = \count($refl->getAttributes(Publishable::class));
82
83
        $uriTemplate = $this->buildAbsoluteUriTemplate() . $operation->getRoutePrefix() . $operation->getUriTemplate();
84
        $subscribeIris = [$uriTemplate];
85
86
        if (!$isPublishable) {
87
            return $subscribeIris;
88
        }
89
90
        // Note that `?draft=1` is also hard coded into the PublishableIriConverter, probably make this configurable somewhere
91
        if ($this->publishableStatusChecker->isGranted($operation->getClass())) {
92
            $subscribeIris[] = $uriTemplate . '?draft=1';
93
        }
94
95
        return $subscribeIris;
96
    }
97
98
    private function getMercureResourceOperation(string $resourceClass): ?HttpOperation
99
    {
100
        $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
101
102
        try {
103
            $operation = $resourceMetadataCollection->getOperation(forceCollection: false, httpOperation: true);
104
        } catch (OperationNotFoundException $e) {
105
            return null;
106
        }
107
108
        if (!$operation instanceof HttpOperation) {
109
            return null;
110
        }
111
112
        $mercure = $operation->getMercure();
113
114
        if (!$mercure) {
115
            return null;
116
        }
117
        return $operation;
118
    }
119
120
    /**
121
     * Mercure subscribe iris should be absolute
122
     * this code can also be found in Symfony's URL Generator
123
     * but as we work without a symfony route here (and we would not want to do this as its not spec-compliant)
124
     * we do it by hand.
125
     */
126
    private function buildAbsoluteUriTemplate(): string
127
    {
128
        $scheme = $this->requestContext->getScheme();
129
        $host = $this->requestContext->getHost();
130
        $port = $this->requestContext->isSecure() ? $this->requestContext->getHttpsPort() : $this->requestContext->getHttpPort();
131
132
        if (80 !== $port || 443 !== $port) {
133
            return sprintf('%s://%s:%d', $scheme, $host, $port);
134
        }
135
136
        return sprintf('%s://%s', $scheme, $host);
137
    }
138
}
139