Passed
Push — main ( d99856...c81305 )
by Daniel
05:39
created

getMercureResourceOperation()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 11
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 21
ccs 0
cts 11
cp 0
crap 20
rs 9.9
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
        $subscribeIris = [];
33
        foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
34
            if ($resourceIris = $this->getSubscribeIrisForResource($resourceClass)) {
35
                $subscribeIris[] = $resourceIris;
36
            }
37
        }
38
        $subscribeIris = array_merge([], ...$subscribeIris);
39
40
        // Todo: await merge of https://github.com/symfony/mercure/pull/93 to remove ability to publish any updates and set to  null
41
        // May also be able to await a mercure bundle update to set the cookie samesite in mercure configs
42
        $cookie = $this->mercureAuthorization->createCookie($this->requestStack->getCurrentRequest(), $subscribeIris, [], [], $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

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