Passed
Push — master ( 6c011a...8a7749 )
by Romain
36s
created

Webhook::getBody()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Kerox\Messenger\Api;
6
7
use GuzzleHttp\ClientInterface;
8
use GuzzleHttp\Psr7\ServerRequest;
9
use Kerox\Messenger\Model\Callback\Entry;
10
use Kerox\Messenger\Request\WebhookRequest;
11
use Kerox\Messenger\Response\WebhookResponse;
12
use Psr\Http\Message\ServerRequestInterface;
13
14
class Webhook extends AbstractApi
15
{
16
    /**
17
     * @var string
18
     */
19
    protected $appSecret;
20
21
    /**
22
     * @var string
23
     */
24
    protected $verifyToken;
25
26
    /**
27
     * @var \Psr\Http\Message\ServerRequestInterface
28
     */
29
    protected $request;
30
31
    /**
32
     * @var string
33
     */
34
    protected $body;
35
36
    /**
37
     * @var array
38
     */
39
    protected $decodedBody;
40
41
    /**
42
     * @var \Kerox\Messenger\Model\Entry[]
43
     */
44
    protected $hydratedEntries;
45
46
    /**
47
     * Webhook constructor.
48
     *
49
     * @param string                                   $appSecret
50
     * @param string                                   $verifyToken
51
     * @param string                                   $pageToken
52
     * @param \GuzzleHttp\ClientInterface              $client
53
     * @param \Psr\Http\Message\ServerRequestInterface $request
54
     */
55 12
    public function __construct(
56
        string $appSecret,
57
        string $verifyToken,
58
        string $pageToken,
59
        ClientInterface $client,
60
        ?ServerRequestInterface $request = null
61
    ) {
62 12
        parent::__construct($pageToken, $client);
63
64 12
        $this->appSecret = $appSecret;
65 12
        $this->verifyToken = $verifyToken;
66 12
        $this->request = $request ?: ServerRequest::fromGlobals();
67 12
    }
68
69
    /**
70
     * @return bool
71
     */
72 3
    public function isValidToken(): bool
73
    {
74 3
        if ($this->request->getMethod() !== 'GET') {
75 1
            return false;
76
        }
77
78 2
        $params = $this->request->getQueryParams();
79 2
        if (!isset($params['hub_verify_token'])) {
80 1
            return false;
81
        }
82
83 1
        return $params['hub_mode'] === 'subscribe' && $params['hub_verify_token'] === $this->verifyToken;
84
    }
85
86
    /**
87
     * @return string|null
88
     */
89 1
    public function challenge(): ?string
90
    {
91 1
        $params = $this->request->getQueryParams();
92
93 1
        return $params['hub_challenge'] ?? null;
94
    }
95
96
    /**
97
     * @return \Kerox\Messenger\Response\WebhookResponse
98
     */
99 1
    public function subscribe(): WebhookResponse
100
    {
101 1
        $request = new WebhookRequest($this->pageToken);
102 1
        $response = $this->client->post('me/subscribed_apps', $request->build());
103
104 1
        return new WebhookResponse($response);
105
    }
106
107
    /**
108
     * @throws \Exception
109
     *
110
     * @return bool
111
     */
112 2
    public function isValidCallback(): bool
113
    {
114 2
        if (!$this->isValidHubSignature()) {
115 1
            return false;
116
        }
117
118 1
        $decodedBody = $this->getDecodedBody();
119
120 1
        $object = $decodedBody['object'] ?? null;
121 1
        $entry = $decodedBody['entry'] ?? null;
122
123 1
        return $object === 'page' && $entry !== null;
124
    }
125
126
    /**
127
     * @return string
128
     */
129 7
    public function getBody(): string
130
    {
131 7
        if ($this->body === null) {
132 7
            $this->body = (string) $this->request->getBody();
133
        }
134
135 7
        return $this->body;
136
    }
137
138
    /**
139
     * @throws \Exception
140
     *
141
     * @return array
142
     */
143 6
    public function getDecodedBody(): array
144
    {
145 6
        if ($this->decodedBody === null) {
146 6
            $decodedBody = json_decode($this->getBody(), true);
147 6
            if ($decodedBody === null || json_last_error() !== JSON_ERROR_NONE) {
148 1
                $decodedBody = [];
149
            }
150
151 6
            $this->decodedBody = $decodedBody;
152
        }
153
154 6
        return $this->decodedBody;
155
    }
156
157
    /**
158
     * @throws \Exception
159
     *
160
     * @return \Kerox\Messenger\Model\Callback\Entry[]
161
     */
162 1
    public function getCallbackEntries(): array
163
    {
164 1
        return $this->getHydratedEntries();
165
    }
166
167
    /**
168
     * @throws \Exception
169
     *
170
     * @return array
171
     */
172 2
    public function getCallbackEvents(): array
173
    {
174 2
        $events = [];
175 2
        foreach ($this->getHydratedEntries() as $hydratedEntry) {
176
            /** @var \Kerox\Messenger\Model\Callback\Entry $hydratedEntry */
177 2
            $events = array_merge($events, $hydratedEntry->getEvents());
178
        }
179
180 2
        return $events;
181
    }
182
183
    /**
184
     * @throws \Exception
185
     *
186
     * @return \Kerox\Messenger\Model\Callback\Entry[]
187
     */
188 3
    private function getHydratedEntries(): array
189
    {
190 3
        if ($this->hydratedEntries === null) {
191 3
            $decodedBody = $this->getDecodedBody();
192
193 3
            $hydrated = [];
194 3
            foreach ($decodedBody['entry'] as $entry) {
195 3
                $hydrated[] = Entry::create($entry);
196
            }
197
198 3
            $this->hydratedEntries = $hydrated;
0 ignored issues
show
Documentation Bug introduced by
$hydrated is of type array<mixed,mixed|Kerox\...r\Model\Callback\Entry>, but the property $hydratedEntries was declared to be of type Kerox\Messenger\Model\Entry[]. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
199
        }
200
201 3
        return $this->hydratedEntries;
202
    }
203
204
    /**
205
     * @return bool
206
     */
207 2
    private function isValidHubSignature(): bool
208
    {
209 2
        $headers = $this->request->getHeader('X-Hub-Signature');
210 2
        $content = $this->getBody();
211
212 2
        if (empty($headers)) {
213 1
            return false;
214
        }
215
216 1
        [$algorithm, $hash] = explode('=', $headers[0]);
217
218 1
        return hash_equals(hash_hmac($algorithm, $content, $this->appSecret), $hash);
219
    }
220
}
221