Passed
Push — master ( c84473...8b9427 )
by William
02:37
created

GithubApiComponent::getUserInfo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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