MercureAuthorization::buildAbsoluteUriTemplate()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
eloc 6
c 2
b 0
f 0
nc 4
nop 0
dl 0
loc 10
ccs 0
cts 7
cp 0
crap 20
rs 10
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\Mercure;
15
16
use ApiPlatform\Exception\OperationNotFoundException as LegacyOperationNotFoundException;
17
use ApiPlatform\Metadata\Exception\OperationNotFoundException;
18
use ApiPlatform\Metadata\HttpOperation;
19
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
20
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
21
use Silverback\ApiComponentsBundle\Annotation\Publishable;
22
use Silverback\ApiComponentsBundle\Helper\Publishable\PublishableStatusChecker;
23
use Symfony\Component\HttpFoundation\Cookie;
24
use Symfony\Component\HttpFoundation\RequestStack;
25
use Symfony\Component\Mercure\Authorization;
26
use Symfony\Component\Routing\RequestContext;
27
28
class MercureAuthorization
29
{
30
    public function __construct(
31
        private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory,
32
        private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
33
        private readonly PublishableStatusChecker $publishableStatusChecker,
34
        private readonly RequestContext $requestContext,
35
        private readonly Authorization $mercureAuthorization,
36
        private readonly RequestStack $requestStack,
37
        private readonly string $cookieSameSite = Cookie::SAMESITE_STRICT,
38
        private readonly ?string $hubName = null
39
    ) {
40
    }
41
42
    public function getAuthorizationCookie(): Cookie
43
    {
44
        $subscribeTopics = $this->getSubscribeTopics();
45
        $cookie = $this->mercureAuthorization->createCookie($this->requestStack->getCurrentRequest(), $subscribeTopics, null, [], $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

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