GithubApiComponent   A
last analyzed

Complexity

Total Complexity 16

Size/Duplication

Total Lines 243
Duplicated Lines 0 %

Test Coverage

Coverage 92.94%

Importance

Changes 7
Bugs 0 Features 1
Metric Value
eloc 77
dl 0
loc 243
ccs 79
cts 85
cp 0.9294
rs 10
c 7
b 0
f 1
wmc 16

9 Methods

Rating   Name   Duplication   Size   Complexity  
A getRedirectUrl() 0 16 1
A getIssue() 0 8 1
A sendRequest() 0 51 5
A createIssue() 0 8 1
A apiRequest() 0 14 2
A createComment() 0 8 1
A canCommitTo() 0 14 3
A getAccessToken() 0 10 1
A getUserInfo() 0 3 1
1
<?php
2
3
/**
4
 * Github api component handling comunication with github.
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\Component;
20
21
use Cake\Controller\Component;
22
use Cake\Core\Configure;
23
use Cake\Log\Log;
24
use Cake\Routing\Router;
25
26
use function array_merge;
27
use function curl_close;
28
use function curl_error;
29
use function curl_init;
30
use function curl_setopt;
31
use function http_build_query;
32
use function json_decode;
33
use function json_encode;
34
use function strtoupper;
35
36
use const CURL_HTTP_VERSION_1_1;
37
use const CURLINFO_HTTP_CODE;
38
use const CURLOPT_CUSTOMREQUEST;
39
use const CURLOPT_FOLLOWLOCATION;
40
use const CURLOPT_HTTP_VERSION;
41
use const CURLOPT_HTTPHEADER;
42
use const CURLOPT_POSTFIELDS;
43
use const CURLOPT_RETURNTRANSFER;
44
use const CURLOPT_USERAGENT;
45
46
/**
47
 * Github api component handling comunication with github.
48
 */
49
class GithubApiComponent extends Component
50
{
51
    /**
52
     * perform an api request given a path, the data to send, the method and whether
53
     * or not to return a status.
54
     *
55
     * @param string       $path         the api path to preform the request to
56
     * @param array|string $data         the data to send in the request. This works with both GET
57
     *                            and Post requests
58
     * @param string       $method       the method type of the request
59
     * @param bool         $returnStatus whether to return the status code with the
60
     *                                   request
61
     * @param string       $access_token the github access token
62
     *
63
     * @return array the returned response decoded and optionally the status code,
64
     *               see GithubApiComponent::sendRequest()
65
     *
66
     * @see GithubApiComponent::sendRequest()
67
     */
68 42
    public function apiRequest(
69
        string $path = '',
70
        $data = [],
71
        string $method = 'GET',
72
        bool $returnStatus = false,
73
        string $access_token = ''
74
    ): array {
75 42
        $path = 'https://api.github.com/' . $path;
76 42
        if (strtoupper($method) === 'GET') {
77 28
            $path .= '?' . http_build_query($data);
78 28
            $data = [];
79
        }
80
81 42
        return $this->sendRequest($path, $data, $method, $returnStatus, $access_token);
82
    }
83
84
    /**
85
     * retrieve an access token using a code that has been authorized by a user.
86
     *
87
     * @param string $code the code returned by github to the callback url
88
     *
89
     * @return string|null the access token
90
     */
91 7
    public function getAccessToken(?string $code): ?string
92
    {
93 7
        $url = 'https://github.com/login/oauth/access_token';
94 7
        $data = array_merge(
95 7
            Configure::read('GithubConfig', []),
96 7
            ['code' => $code]
97
        );
98 7
        $decodedResponse = $this->sendRequest($url, http_build_query($data), 'POST');
99
100 7
        return $decodedResponse['access_token'] ?? null;
101
    }
102
103
    /**
104
     * retrieve the github info stored on a user by his access token.
105
     *
106
     * @param string $accessToken the access token belonging to the user being
107
     *                            requested
108
     *
109
     * @return array the github info returned by github as an associative array
110
     */
111 7
    public function getUserInfo(string $accessToken): array
112
    {
113 7
        return $this->apiRequest('user', [], 'GET', true, $accessToken);
114
    }
115
116
    /**
117
     * perform an http request using curl given a url, the post data to send, the
118
     * request method and whether or not to return a status.
119
     *
120
     * @param string       $url          the url to preform the request to
121
     * @param array|string $data         the post data to send in the request. This only works with POST requests. GET requests need the data appended in the url.
122
     *                            with POST requests. GET requests need the data appended
123
     *                            in the url.
124
     * @param string       $method       the method type of the request
125
     * @param bool         $returnCode   whether to return the status code with the
126
     *                                   request
127
     * @param string       $access_token the github access token
128
     *
129
     * @return array the returned response decoded and optionally the status code,
130
     *               eg: array($decodedResponse, $statusCode) or just $decodedResponse
131
     */
132 42
    public function sendRequest(
133
        string $url,
134
        $data,
135
        string $method,
136
        bool $returnCode = false,
137
        string $access_token = ''
138
    ): array {
139 42
        Log::debug('Request-url: ' . $url);
140 42
        $curlHandle = curl_init($url);
141 42
        if ($curlHandle === false) {
142
            Log::error('Curl init error for: ' . $url);
143
144
            return ['', 0];
145
        }
146
147 42
        curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, $method);
148 42
        $header = ['Accept: application/json'];
149 42
        if ($access_token !== '') {
150 42
            $header[] = 'Authorization: token ' . $access_token;
151
        }
152
153 42
        curl_setopt($curlHandle, CURLOPT_HTTPHEADER, $header);
154 42
        curl_setopt($curlHandle, CURLOPT_USERAGENT, 'phpMyAdmin - Error Reporting Server');
155 42
        curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $data);
156 42
        curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
157 42
        curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, 1);// Issues moved to another repo have redirects
158 42
        curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1);
159 42
        $response = curl_exec($curlHandle);// phpcs:ignore SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly.ReferenceViaFallbackGlobalName
160 42
        if ($response === false) {
161
            Log::error('Curl error: "' . curl_error($curlHandle) . '" for: ' . $url);
162
            curl_close($curlHandle);
163
164
            return ['', 0];
165
        }
166
167 42
        $decodedResponse = json_decode($response, true);
168 42
        if ($returnCode) {
169 42
            $status = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);// phpcs:ignore SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly.ReferenceViaFallbackGlobalName
170
            // phpcs ignored patterns for mock testing reasons
171 42
            curl_close($curlHandle);
172 42
            Log::debug('Response-code: ' . $status . ' for: ' . $url);
173
174
            return [
175 42
                $decodedResponse,
176 42
                $status,
177
            ];
178
        }
179
180 7
        curl_close($curlHandle);
181
182 7
        return $decodedResponse;
183
    }
184
185
    /**
186
     * generate the url to redirect the user to for authorization given the
187
     * requested scope.
188
     *
189
     * @param string $scope the api scope for the user to authorize
190
     *
191
     * @return string the generated url to redirect the user to
192
     */
193 7
    public function getRedirectUrl(?string $scope = null): string
194
    {
195 7
        $url = 'https://github.com/login/oauth/authorize';
196 2
        $data = [
197 7
            'client_id' => Configure::read('GithubConfig', ['client_id' => ''])['client_id'],
198 7
            'redirect_uri' => Router::url(
199
                [
200 7
                    'controller' => 'developers',
201
                    'action' => 'callback',
202
                ],
203 7
                true
204
            ),
205 7
            'scope' => $scope,
206
        ];
207
208 7
        return $url . '?' . http_build_query($data);
209
    }
210
211
    /**
212
     * Check if a user can commit to a rep.
213
     *
214
     * @param string $username     the username to check
215
     * @param string $repoPath     the repo path of the repo to check for
216
     * @param string $access_token the github access token
217
     *
218
     * @return bool true if the user is a collaborator and false if they are not
219
     */
220 7
    public function canCommitTo(string $username, string $repoPath, string $access_token): bool
221
    {
222 7
        [, $status] = $this->apiRequest(
223 7
            'repos/' . $repoPath . '/collaborators/' . $username,
224 7
            [],
225 7
            'GET',
226 7
            true,
227 1
            $access_token
228
        );
229 7
        if ($status !== 204 && $status !== 404) {
230
            Log::error('Collaborators call ended in a status code: ' . $status);
231
        }
232
233 7
        return $status === 204;
234
    }
235
236
    /**
237
     * make api request for github issue creation.
238
     *
239
     * @param string $repoPath     The repo slug
240
     * @param array  $data         issue details
241
     * @param string $access_token the github access token
242
     * @return array
243
     */
244 7
    public function createIssue(string $repoPath, array $data, string $access_token): array
245
    {
246 7
        return $this->apiRequest(
247 7
            'repos/' . $repoPath . '/issues',
248 7
            json_encode($data),
249 7
            'POST',
250 7
            true,
251 1
            $access_token
252
        );
253
    }
254
255
    /**
256
     * make api request for github comment creation.
257
     *
258
     * @param string $repoPath     The repo slug
259
     * @param array  $data         The data
260
     * @param int    $issueNumber  The issue number
261
     * @param string $access_token The github access token
262
     * @return array
263
     */
264 14
    public function createComment(string $repoPath, array $data, int $issueNumber, string $access_token): array
265
    {
266 14
        return $this->apiRequest(
267 14
            'repos/' . $repoPath . '/issues/' . $issueNumber . '/comments',
268 14
            json_encode($data),
269 14
            'POST',
270 14
            true,
271 2
            $access_token
272
        );
273
    }
274
275
    /**
276
     * Make API request for getting Github issue's status
277
     *
278
     * @param string $repoPath     The repo slug
279
     * @param array  $data         The data
280
     * @param int    $issueNumber  The issue number
281
     * @param string $access_token The github access token
282
     * @return array
283
     */
284 21
    public function getIssue(string $repoPath, array $data, int $issueNumber, string $access_token): array
285
    {
286 21
        return $this->apiRequest(
287 21
            'repos/' . $repoPath . '/issues/' . $issueNumber,
288 3
            $data,
289 21
            'GET',
290 21
            true,
291 3
            $access_token
292
        );
293
    }
294
}
295