Passed
Push — main ( c81305...16cc9f )
by Daniel
15:49 queued 17s
created

MercureAuthorization::getSubscribeTopics()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 5
c 0
b 0
f 0
nc 3
nop 0
dl 0
loc 9
ccs 0
cts 6
cp 0
crap 12
rs 10
1
<?php
2
3
namespace Silverback\ApiComponentsBundle\Mercure;
4
5
use ApiPlatform\Exception\OperationNotFoundException;
6
use ApiPlatform\Metadata\HttpOperation;
7
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
8
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
9
use Silverback\ApiComponentsBundle\Annotation\Publishable;
10
use Silverback\ApiComponentsBundle\Helper\Publishable\PublishableStatusChecker;
11
use Symfony\Component\HttpFoundation\Cookie;
12
use Symfony\Component\HttpFoundation\RequestStack;
13
use Symfony\Component\Mercure\Authorization;
14
use Symfony\Component\Routing\RequestContext;
15
16
class MercureAuthorization
17
{
18
    public function __construct(
19
        private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory,
20
        private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
21
        private readonly PublishableStatusChecker $publishableStatusChecker,
22
        private readonly RequestContext $requestContext,
23
        private readonly Authorization $mercureAuthorization,
24
        private readonly RequestStack $requestStack,
25
        private readonly string $cookieSameSite = Cookie::SAMESITE_STRICT,
26
        private readonly ?string $hubName = null
27
    ) {
28
    }
29
30
    public function createAuthorizationCookie(): Cookie
31
    {
32
        $subscribeTopics = $this->getSubscribeTopics();
33
        // Todo: await merge of https://github.com/symfony/mercure/pull/93 to remove ability to publish any updates and set to  null
34
        // May also be able to await a mercure bundle update to set the cookie samesite in mercure configs
35
        $cookie = $this->mercureAuthorization->createCookie($this->requestStack->getCurrentRequest(), $subscribeTopics, [], [], $this->hubName);
0 ignored issues
show
Bug introduced by
It seems like $this->requestStack->getCurrentRequest() can also be of type null; however, parameter $request of Symfony\Component\Mercur...ization::createCookie() 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

35
        $cookie = $this->mercureAuthorization->createCookie(/** @scrutinizer ignore-type */ $this->requestStack->getCurrentRequest(), $subscribeTopics, [], [], $this->hubName);
Loading history...
36
        return $cookie
37
            ->withSameSite($this->cookieSameSite)
38
            ->withExpires(time() + (10 * 365 * 24 * 60 * 60));
39
    }
40
41
    public function getSubscribeTopics(): array
42
    {
43
        $subscribeIris = [];
44
        foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
45
            if ($resourceIris = $this->getSubscribeIrisForResource($resourceClass)) {
46
                $subscribeIris[] = $resourceIris;
47
            }
48
        }
49
        return array_merge([], ...$subscribeIris);
50
    }
51
52
    private function getSubscribeIrisForResource(string $resourceClass): ?array
53
    {
54
        $operation = $this->getMercureResourceOperation($resourceClass);
55
        if (!$operation) {
56
            return null;
57
        }
58
59
        $refl = new \ReflectionClass($operation->getClass());
60
        $isPublishable = \count($refl->getAttributes(Publishable::class));
61
62
        $uriTemplate = $this->buildAbsoluteUriTemplate() . $operation->getRoutePrefix() . $operation->getUriTemplate();
63
        $subscribeIris = [$uriTemplate];
64
65
        if (!$isPublishable) {
66
            return $subscribeIris;
67
        }
68
69
        // Note that `?draft=1` is also hard coded into the PublishableIriConverter, probably make this configurable somewhere
70
        if ($this->publishableStatusChecker->isGranted($operation->getClass())) {
71
            $subscribeIris[] = $uriTemplate . '?draft=1';
72
        }
73
74
        return $subscribeIris;
75
    }
76
77
    private function getMercureResourceOperation(string $resourceClass): ?HttpOperation
78
    {
79
        $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
80
81
        try {
82
            $operation = $resourceMetadataCollection->getOperation(forceCollection: false, httpOperation: true);
83
        } catch (OperationNotFoundException $e) {
84
            return null;
85
        }
86
87
        if (!$operation instanceof HttpOperation) {
88
            return null;
89
        }
90
91
        $mercure = $operation->getMercure();
92
93
        if (!$mercure) {
94
            return null;
95
        }
96
97
        return $operation;
98
    }
99
100
    /**
101
     * Mercure subscribe iris should be absolute
102
     * this code can also be found in Symfony's URL Generator
103
     * but as we work without a symfony route here (and we would not want to do this as its not spec-compliant)
104
     * we do it by hand.
105
     */
106
    private function buildAbsoluteUriTemplate(): string
107
    {
108
        $scheme = $this->requestContext->getScheme();
109
        $host = $this->requestContext->getHost();
110
        $port = $this->requestContext->isSecure() ? $this->requestContext->getHttpsPort() : $this->requestContext->getHttpPort();
111
112
        if (80 !== $port || 443 !== $port) {
113
            return sprintf('%s://%s:%d', $scheme, $host, $port);
114
        }
115
116
        return sprintf('%s://%s', $scheme, $host);
117
    }
118
}
119