Completed
Push — master ( 7c757d...667317 )
by William
02:47
created

EventsController::initialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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