Passed
Push — master ( 083ff7...c84473 )
by William
03:02
created

EventsController::getAppropriateStatus()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 4
nop 1
dl 0
loc 17
ccs 11
cts 11
cp 1
crap 4
rs 9.9332
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Events controller Github webhook events
5
 *
6
 * phpMyAdmin Error reporting server
7
 * Copyright (c) phpMyAdmin project (https://www.phpmyadmin.net/)
8
 *
9
 * Licensed under The MIT License
10
 * For full copyright and license information, please see the LICENSE.txt
11
 * Redistributions of files must retain the above copyright notice.
12
 *
13
 * @copyright Copyright (c) phpMyAdmin project (https://www.phpmyadmin.net/)
14
 * @license   https://opensource.org/licenses/mit-license.php MIT License
15
 *
16
 * @see      https://www.phpmyadmin.net/
17
 */
18
19
namespace App\Controller;
20
21
use Cake\Core\Configure;
22
use Cake\Event\EventInterface;
23
use Cake\Http\Response;
24
use Cake\Http\ServerRequest;
25
use Cake\Log\Log;
26
use Cake\ORM\TableRegistry;
27
use function count;
28
use function explode;
29
use function file_get_contents;
30
use function hash_hmac;
31
use function strpos;
32
use function json_decode;
33
34
/**
35
 * Events controller Github webhook events
36
 */
37
class EventsController extends AppController
38
{
39 7
    public function initialize(): void
40
    {
41 7
        parent::initialize();
42
        //FIXME: it got deprecated and does not work anymore
43
        //$this->loadComponent('Csrf');
44
45 7
        $this->Reports = TableRegistry::getTableLocator()->get('Reports');
46 7
    }
47
48 7
    public function beforeFilter(EventInterface $event)
49
    {
50
        //$this->getEventManager()->off($this->Csrf);
51 7
    }
52
53 7
    public function index(): ?Response
54
    {
55
        // Only allow POST requests
56 7
        $this->request->allowMethod(['post']);
57
58
        // Validate request
59 7
        $statusCode = $this->validateRequest($this->request);
60 7
        if ($statusCode !== 201) {
61 7
            Log::error(
62
                'Could not validate the request. Sending a '
63 7
                    . $statusCode . ' response.'
64
            );
65
66
            // Send a response
67 7
            $this->disableAutoRender();
68
69 7
            return $this->response->withStatus($statusCode);
70
        }
71
72 7
        if ($statusCode === 200) {
73
           // Send a success response to ping event
74
            $this->disableAutoRender();
75
76
            return $this->response->withStatus($statusCode);
77
        }
78
79 7
        $issuesData = json_decode((string) $this->request->getBody(), true);
80 7
        $eventAction = $issuesData['action'];
81 7
        $issueNumber = $issuesData['issue'] ? $issuesData['issue']['number'] : '';
82
83 7
        if ($eventAction === 'closed'
84 7
            || $eventAction === 'opened'
85 7
            || $eventAction === 'reopened'
86
        ) {
87 7
            $status = $this->getAppropriateStatus($eventAction);
88 7
            $reportsUpdated = $this->Reports->setLinkedReportStatus($issueNumber, $status);
89 7
            if ($reportsUpdated > 0) {
90 7
                Log::debug(
91 7
                    $reportsUpdated . ' linked reports to issue number '
92 7
                        . $issueNumber . ' were updated according to received action '
93 7
                        . $eventAction
94
                );
95
            } else {
96 7
                Log::info(
97 7
                    'No linked report found for issue number \'' . $issueNumber
98 7
                    . '\'. Ignoring the event.'
99
                );
100 7
                $statusCode = 204;
101
            }
102
        } else {
103 7
            Log::info(
104 7
                'received a webhook event for action \'' . $eventAction
105 7
                . '\' on issue number ' . $issueNumber . '. Ignoring the event.'
106
            );
107 7
            $statusCode = 204;
108
        }
109
110
        // Send a response
111 7
        $this->disableAutoRender();
112
113 7
        return $this->response->withStatus($statusCode);
114
    }
115
116
    /**
117
     * Validate HTTP Request received
118
     *
119
     * @param ServerRequest $request Request object
120
     *
121
     * @return int status code based on if this is a valid request
122
     */
123 7
    protected function validateRequest(ServerRequest $request): int
124
    {
125
        // Default $statusCode
126 7
        $statusCode = 201;
127
128 7
        $userAgent = $request->getHeaderLine('User-Agent');
129 7
        $eventType = $request->getHeaderLine('X-GitHub-Event');
130
131 7
        $receivedHashHeader = $request->getHeaderLine('X-Hub-Signature');
132 7
        $algo = '';
133 7
        $receivedHash = '';
134 7
        if ($receivedHashHeader !== null) {
135 7
            $parts = explode('=', $receivedHashHeader);
136 7
            if (count($parts) > 1) {
137 7
                $algo = $parts[0];
138 7
                $receivedHash = $parts[1];
139
            }
140
        }
141
142 7
        $expectedHash = $this->getHash(file_get_contents('php://input'), $algo);
143
144 7
        if ($userAgent !== null && strpos($userAgent, 'GitHub-Hookshot') !== 0) {
145
            // Check if the User-agent is Github
146
            // Otherwise, Send a '403: Forbidden'
147
148 7
            Log::error(
149 7
                'Invalid User agent: ' . $userAgent
150 7
                . '. Ignoring the event.'
151
            );
152
153 7
            return 403;
154
        }
155
156 7
        if ($eventType !== null && $eventType === 'ping') {
157
            // Check if the request is based on 'issues' event
158
            // Otherwise, Send a '400: Bad Request'
159
160 7
            Log::info(
161 7
                'Ping event type received.'
162
            );
163
164 7
            return 200;
165
        }
166
167 7
        if ($eventType !== null && $eventType !== 'issues') {
168
            // Check if the request is based on 'issues' event
169
            // Otherwise, Send a '400: Bad Request'
170
171 7
            Log::error(
172 7
                'Unexpected event type: ' . $eventType
173 7
                . '. Ignoring the event.'
174
            );
175
176 7
            return 400;
177
        }
178
179 7
        if ($receivedHash !== $expectedHash) {
180
            // Check if hash matches
181
            // Otherwise, Send a '401: Unauthorized'
182
183 7
            Log::error(
184 7
                'received hash ' . $receivedHash . ' does not match '
185 7
                . ' expected hash ' . $expectedHash
186 7
                . '. Ignoring the event.'
187
            );
188
189 7
            return 401;
190
        }
191
192 7
        return $statusCode;
193
    }
194
195
    /**
196
     * Get the hash of raw POST payload
197
     *
198
     * @param string $payload Raw POST body string
199
     * @param string $algo    Algorithm used to calculate the hash
200
     *
201
     * @return string Hmac Digest-based hash of payload
202
     */
203 7
    protected function getHash(string $payload, string $algo): string
204
    {
205 7
        if ($algo === '') {
206 7
            return '';
207
        }
208 7
        $key = Configure::read('GithubWebhookSecret');
209
210 7
        return hash_hmac($algo, $payload, $key);
211
    }
212
213
    /**
214
     * Get appropriate new status based on action received in github event
215
     *
216
     * @param string $action Action received in Github webhook event
217
     *
218
     * @return string Appropriate new status for the related reports
219
     */
220 7
    protected function getAppropriateStatus(string $action): string
221
    {
222 7
        $status = 'forwarded';
223
224 7
        switch ($action) {
225 7
            case 'opened':
226 7
                break;
227
228 7
            case 'reopened':
229 7
                break;
230
231 7
            case 'closed':
232 7
                $status = 'resolved';
233 7
                break;
234
        }
235
236 7
        return $status;
237
    }
238
}
239