Passed
Push — master ( 8072b8...acfa49 )
by William
04:14
created

GithubApiComponent::createComment()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

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