Completed
Push — master ( 89830f...07e3a8 )
by William
20:06
created

EventsController   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 202
Duplicated Lines 0 %

Test Coverage

Coverage 96.84%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 90
dl 0
loc 202
ccs 92
cts 95
cp 0.9684
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 3 1
B index() 0 64 8
A initialize() 0 6 1
A getHash() 0 8 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\Event;
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
33 4
/**
34
 * Events controller Github webhook events
35 4
 */
36 4
class EventsController extends AppController
37
{
38 4
    public function initialize(): void
39 4
    {
40
        parent::initialize();
41 4
        $this->loadComponent('Csrf');
42
43 4
        $this->Reports = TableRegistry::getTableLocator()->get('Reports');
0 ignored issues
show
Bug Best Practice introduced by
The property Reports does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
44 4
    }
45
46 4
    public function beforeFilter(Event $event): void
47
    {
48
        $this->eventManager()->off($this->Csrf);
0 ignored issues
show
Deprecated Code introduced by
The function Cake\Controller\Controller::eventManager() has been deprecated: 3.5.0 Use getEventManager()/setEventManager() instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

48
        /** @scrutinizer ignore-deprecated */ $this->eventManager()->off($this->Csrf);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
49 4
    }
50
51
    public function index(): ?Response
52 4
    {
53 4
        // Only allow POST requests
54
        $this->request->allowMethod(['post']);
55 4
56
        // Validate request
57
        $statusCode = $this->validateRequest($this->request);
58
        if ($statusCode !== 201) {
59 4
            Log::error(
60 4
                'Could not validate the request. Sending a '
61
                    . $statusCode . ' response.'
62 4
            );
63 4
64
            // Send a response
65
            $this->auto_render = false;
0 ignored issues
show
Bug Best Practice introduced by
The property auto_render does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
66
            $this->response->statusCode($statusCode);
0 ignored issues
show
Deprecated Code introduced by
The function Cake\Http\Response::statusCode() has been deprecated: 3.4.0 Use `getStatusCode()` and `withStatus()` instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

66
            /** @scrutinizer ignore-deprecated */ $this->response->statusCode($statusCode);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
67
68
            return $this->response;
69
        }
70
71 4
        if ($statusCode === 200) {
72 4
           // Send a success response to ping event
73 4
            $this->auto_render = false;
74
            $this->response->statusCode($statusCode);
0 ignored issues
show
Deprecated Code introduced by
The function Cake\Http\Response::statusCode() has been deprecated: 3.4.0 Use `getStatusCode()` and `withStatus()` instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

74
            /** @scrutinizer ignore-deprecated */ $this->response->statusCode($statusCode);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
75 4
76 4
            return $this->response;
77 4
        }
78
79 4
        $issuesData = $this->request->input('json_decode', true);
80
        $eventAction = $issuesData['action'];
81 4
        $issueNumber = $issuesData['issue'] ? $issuesData['issue']['number'] : '';
82 4
83 4
        if ($eventAction === 'closed'
84 4
            || $eventAction === 'opened'
85 4
            || $eventAction === 'reopened'
86
        ) {
87
            $status = $this->getAppropriateStatus($eventAction);
88 4
            $reportsUpdated = $this->Reports->setLinkedReportStatus($issueNumber, $status);
89 4
            if ($reportsUpdated > 0) {
90 4
                Log::debug(
91
                    $reportsUpdated . ' linked reports to issue number '
92 4
                        . $issueNumber . ' were updated according to received action '
93
                        . $eventAction
94
                );
95 4
            } else {
96 4
                Log::info(
97 4
                    'No linked report found for issue number \'' . $issueNumber
98
                    . '\'. Ignoring the event.'
99 4
                );
100
                $statusCode = 204;
101
            }
102
        } else {
103 4
            Log::info(
104 4
                'received a webhook event for action \'' . $eventAction
105
                . '\' on issue number ' . $issueNumber . '. Ignoring the event.'
106 4
            );
107
            $statusCode = 204;
108
        }
109
110
        // Send a response
111
        $this->auto_render = false;
112
        $this->response->statusCode($statusCode);
0 ignored issues
show
Deprecated Code introduced by
The function Cake\Http\Response::statusCode() has been deprecated: 3.4.0 Use `getStatusCode()` and `withStatus()` instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

112
        /** @scrutinizer ignore-deprecated */ $this->response->statusCode($statusCode);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
113
114
        return $this->response;
115
    }
116
117 4
    /**
118
     * Validate HTTP Request received
119
     *
120 4
     * @param ServerRequest $request Request object
121
     *
122 4
     * @return int status code based on if this is a valid request
123 4
     */
124
    protected function validateRequest(ServerRequest $request): int
125 4
    {
126 4
        // Default $statusCode
127 4
        $statusCode = 201;
128 4
129 4
        $userAgent = $request->getHeaderLine('User-Agent');
130 4
        $eventType = $request->getHeaderLine('X-GitHub-Event');
131 4
132 4
        $receivedHashHeader = $request->getHeaderLine('X-Hub-Signature');
133
        $algo = '';
134
        $receivedHash = '';
135
        if ($receivedHashHeader !== null) {
136 4
            $parts = explode('=', $receivedHashHeader);
137
            if (count($parts) > 1) {
138 4
                $algo = $parts[0];
139
                $receivedHash = $parts[1];
140
            }
141
        }
142 4
143 4
        $expectedHash = $this->getHash(file_get_contents('php://input'), $algo);
144 4
145
        if ($userAgent !== null && strpos($userAgent, 'GitHub-Hookshot') !== 0) {
146 4
            // Check if the User-agent is Github
147
            // Otherwise, Send a '403: Forbidden'
148 4
149 4
            Log::error(
150
                'Invalid User agent: ' . $userAgent
151
                . '. Ignoring the event.'
152
            );
153 4
154 4
            return 403;
155
        }
156 4
157
        if ($eventType !== null && $eventType === 'ping') {
158 4
            // Check if the request is based on 'issues' event
159 4
            // Otherwise, Send a '400: Bad Request'
160
161
            Log::info(
162
                'Ping event type received.'
163 4
            );
164 4
165 4
            return 200;
166
        }
167 4
168
        if ($eventType !== null && $eventType !== 'issues') {
169 4
            // Check if the request is based on 'issues' event
170 4
            // Otherwise, Send a '400: Bad Request'
171
172
            Log::error(
173
                'Unexpected event type: ' . $eventType
174 4
                . '. Ignoring the event.'
175 4
            );
176 4
177 4
            return 400;
178
        }
179 4
180
        if ($receivedHash !== $expectedHash) {
181 4
            // Check if hash matches
182
            // Otherwise, Send a '401: Unauthorized'
183
184 4
            Log::error(
185
                'received hash ' . $receivedHash . ' does not match '
186
                . ' expected hash ' . $expectedHash
187
                . '. Ignoring the event.'
188
            );
189
190
            return 401;
191
        }
192
193
        return $statusCode;
194
    }
195 4
196
    /**
197 4
     * Get the hash of raw POST payload
198 4
     *
199
     * @param string $payload Raw POST body string
200 4
     * @param string $algo    Algorithm used to calculate the hash
201
     *
202 4
     * @return string Hmac Digest-based hash of payload
203
     */
204
    protected function getHash(string $payload, string $algo): string
205
    {
206
        if ($algo === '') {
207
            return '';
208
        }
209
        $key = Configure::read('GithubWebhookSecret');
210
211
        return hash_hmac($algo, $payload, $key);
212 4
    }
213
214 4
    /**
215
     * Get appropriate new status based on action received in github event
216 3
     *
217 4
     * @param string $action Action received in Github webhook event
218 4
     *
219
     * @return string Appropriate new status for the related reports
220 4
     */
221 4
    protected function getAppropriateStatus(string $action): string
222
    {
223 4
        $status = 'forwarded';
224 4
225 4
        switch ($action) {
226
            case 'opened':
227
                break;
228 4
229
            case 'reopened':
230
                break;
231
232
            case 'closed':
233
                $status = 'resolved';
234
                break;
235
        }
236
237
        return $status;
238
    }
239
}
240