EventsController   A
last analyzed

Complexity

Total Complexity 26

Size/Duplication

Total Lines 202
Duplicated Lines 0 %

Test Coverage

Coverage 97.78%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 86
dl 0
loc 202
ccs 88
cts 90
cp 0.9778
rs 10
c 2
b 0
f 0
wmc 26

6 Methods

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