Passed
Push — master ( 6f5173...a1a76b )
by BENOIT
01:53
created

PublishController::withAttributes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 5
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 9
rs 10
ccs 3
cts 3
cp 1
crap 2
1
<?php
2
3
namespace BenTools\MercurePHP\Controller;
4
5
use BenTools\MercurePHP\Exception\Http\AccessDeniedHttpException;
6
use BenTools\MercurePHP\Exception\Http\BadRequestHttpException;
7
use BenTools\MercurePHP\Security\Authenticator;
8
use BenTools\MercurePHP\Security\TopicMatcher;
9
use BenTools\MercurePHP\Model\Message;
10
use Lcobucci\JWT\Token;
11
use Psr\Http\Message\RequestInterface;
12
use Psr\Http\Message\ServerRequestInterface;
13
use Psr\Log\LoggerInterface;
14
use Ramsey\Uuid\Uuid;
15
use React\Http\Message\Response;
16
use React\Promise\PromiseInterface;
17
18
use function React\Promise\resolve;
19
20
final class PublishController extends AbstractController
21 21
{
22
    private Authenticator $authenticator;
23 21
24 21
    public function __construct(LoggerInterface $logger)
25
    {
26 19
        $this->logger = $logger;
27
    }
28 19
29 17
    public function __invoke(ServerRequestInterface $request): PromiseInterface
30 17
    {
31 11
        $request = $this->withAttributes($request);
32 11
        $token = $request->getAttribute('token');
33 4
        $topicSelectors = $this->getAuthorizedTopicSelectors($token);
34
        $input = (array) $request->getParsedBody();
35 4
        $input = $this->normalizeInput($input);
36 1
        $canDispatchPrivateUpdates = ([] !== $topicSelectors);
37
38
        if ($input['private'] && !$canDispatchPrivateUpdates) {
39 3
            throw new AccessDeniedHttpException('You are not allowed to dispatch private updates.');
40 1
        }
41
42
        if (false === TopicMatcher::canUpdateTopic($input['topic'], $token, $input['private'])) {
43 2
            throw new AccessDeniedHttpException('You are not allowed to update this topic.');
44 2
        }
45 2
46 2
        $id = $input['id'] ?? (string) Uuid::uuid4();
47 2
        $message = new Message(
48 2
            $id,
49 2
            $input['data'],
50
            (bool) $input['private'],
51
            $input['type'],
52 2
            null !== $input['retry'] ? (int) $input['retry'] : null
53 2
        );
54 2
55
        $this->transport
56 2
            ->publish($input['topic'], $message)
57 2
            ->then(fn () => $this->storage->storeMessage($input['topic'], $message));
58 2
59 2
        $this->logger->debug(
60 2
            \sprintf(
61
                'Created message %s on topic %s',
62
                $message->getId(),
63
                $input['topic'],
64 2
            )
65 2
        );
66
67 2
        $headers = [
68
            'Content-Type' => 'text/plain',
69
            'Cache-Control' => 'no-cache',
70
        ];
71
72
        return resolve(new Response(201, $headers, $id));
73
    }
74 4
75
    public function matchRequest(RequestInterface $request): bool
76 4
    {
77 4
        return 'POST' === $request->getMethod()
78
            && '/.well-known/mercure' === $request->getUri()->getPath();
79
    }
80 11
81
    public function withConfig(array $config): self
82 11
    {
83 4
        /** @var self $clone */
84
        $clone = parent::withConfig($config);
85
86 7
        return $clone->withAuthenticator(Authenticator::createPublisherAuthenticator($config));
87 2
    }
88
89
    private function normalizeInput(array $input): array
90 5
    {
91 1
        if (!\is_scalar($input['topic'] ?? null)) {
92
            throw new BadRequestHttpException('Invalid topic parameter.');
93
        }
94 4
95 4
        if (!\is_scalar($input['data'] ?? '')) {
96 4
            throw new BadRequestHttpException('Invalid data parameter.');
97 4
        }
98
99 4
        if (isset($input['id']) && !Uuid::isValid($input['id'])) {
100
            throw new BadRequestHttpException('Invalid UUID.');
101
        }
102 19
103
        $input['data'] ??= null;
104
        $input['private'] ??= false;
105 19
        $input['type'] ??= null;
106 2
        $input['retry'] ??= null;
107 2
108
        return $input;
109
    }
110 17
111
    private function withAttributes(ServerRequestInterface $request): ServerRequestInterface
112
    {
113 17
        try {
114
            $token = $this->authenticator->authenticate($request);
115 17
        } catch (\RuntimeException $e) {
116 2
            throw new AccessDeniedHttpException($e->getMessage());
117
        }
118
119
        return $request->withAttribute('token', $token ?? null);
120 15
    }
121 1
122 1
    private function getAuthorizedTopicSelectors(?Token $token): array
123
    {
124
        if (null === $token) {
125 14
            throw new AccessDeniedHttpException('Invalid auth token.');
126
        }
127 14
128 3
        try {
129
            $claim = $token->getClaim('mercure');
130
        } catch (\OutOfBoundsException $e) {
131 11
            throw new AccessDeniedHttpException('Provided auth token doesn\'t contain the "mercure" claim.');
132
        }
133
134
        $topicSelectors = $claim->publish ?? null;
135
136
        if (null === $topicSelectors || !\is_array($topicSelectors)) {
137
            throw new AccessDeniedHttpException('Your are not authorized to publish on this hub.');
138
        }
139
140
        return $topicSelectors;
141
    }
142
143
    private function withAuthenticator(Authenticator $authenticator): self
144
    {
145
        $clone = clone $this;
146
        $clone->authenticator = $authenticator;
147
148
        return $clone;
149
    }
150
}
151