1 | <?php |
||
2 | |||
3 | namespace dokuwiki\plugin\issuelinks\services; |
||
4 | |||
5 | use dokuwiki\Form\Form; |
||
6 | use dokuwiki\plugin\issuelinks\classes\ExternalServerException; |
||
7 | use dokuwiki\plugin\issuelinks\classes\HTTPRequestException; |
||
8 | use dokuwiki\plugin\issuelinks\classes\Issue; |
||
9 | use dokuwiki\plugin\issuelinks\classes\Repository; |
||
10 | use dokuwiki\plugin\issuelinks\classes\RequestResult; |
||
11 | |||
12 | class GitHub extends AbstractService |
||
13 | { |
||
14 | |||
15 | |||
16 | const SYNTAX = 'gh'; |
||
17 | const DISPLAY_NAME = 'GitHub'; |
||
18 | const ID = 'github'; |
||
19 | const WEBHOOK_UA = 'GitHub-Hookshot/'; |
||
20 | protected $configError = ''; |
||
21 | protected $user = []; |
||
22 | protected $total = null; |
||
23 | protected $orgs; |
||
24 | /** @var \DokuHTTPClient */ |
||
25 | protected $dokuHTTPClient; |
||
26 | protected $githubUrl = 'https://api.github.com'; |
||
27 | private $scopes = ['admin:repo_hook', 'read:org', 'public_repo']; |
||
28 | |||
29 | 1 | protected function __construct() |
|
30 | { |
||
31 | 1 | $this->dokuHTTPClient = new \DokuHTTPClient(); |
|
32 | 1 | } |
|
33 | |||
34 | public static function getProjectIssueSeparator($isMergeRequest) |
||
35 | { |
||
36 | return '#'; |
||
37 | } |
||
38 | |||
39 | public static function isOurWebhook() |
||
40 | { |
||
41 | global $INPUT; |
||
42 | $userAgent = $INPUT->server->str('HTTP_USER_AGENT'); |
||
43 | return strpos($userAgent, self::WEBHOOK_UA) === 0; |
||
44 | } |
||
45 | |||
46 | public static function isIssueValid(Issue $issue) |
||
47 | { |
||
48 | $summary = $issue->getSummary(); |
||
49 | $valid = !blank($summary); |
||
50 | $status = $issue->getStatus(); |
||
51 | $valid &= !blank($status); |
||
52 | $type = $issue->getType(); |
||
53 | $valid &= !blank($type); |
||
54 | return $valid; |
||
55 | } |
||
56 | |||
57 | public function getIssueURL($projectId, $issueId, $isMergeRequest) |
||
58 | { |
||
59 | return 'https://github.com' . '/' . $projectId . '/issues/' . $issueId; |
||
60 | } |
||
61 | |||
62 | 1 | public function parseIssueSyntax($issueSyntax) |
|
63 | { |
||
64 | 1 | list($projectKey, $issueId) = explode('#', $issueSyntax); |
|
65 | |||
66 | // try to load as pull request |
||
67 | 1 | $issue = Issue::getInstance('github', $projectKey, $issueId, true); |
|
68 | 1 | $isPullRequest = $issue->getFromDB(); |
|
69 | |||
70 | 1 | if ($isPullRequest) { |
|
71 | return $issue; |
||
72 | } |
||
73 | |||
74 | // not a pull request, retrieve it as normal issue |
||
75 | 1 | $issue = Issue::getInstance('github', $projectKey, $issueId, false); |
|
76 | 1 | $issue->getFromDB(); |
|
77 | |||
78 | 1 | return $issue; |
|
79 | } |
||
80 | |||
81 | /** |
||
82 | * @return bool |
||
83 | */ |
||
84 | public function isConfigured() |
||
85 | { |
||
86 | /** @var \helper_plugin_issuelinks_db $db */ |
||
87 | $db = plugin_load('helper', 'issuelinks_db'); |
||
88 | $authToken = $db->getKeyValue('github_token'); |
||
89 | |||
90 | if (empty($authToken)) { |
||
91 | $this->configError = 'Authentication token is missing!'; |
||
92 | return false; |
||
93 | } |
||
94 | |||
95 | try { |
||
96 | $user = $this->makeGitHubGETRequest('/user'); |
||
97 | // $status = $this->connector->getLastStatus(); |
||
98 | } catch (\Exception $e) { |
||
99 | $this->configError = 'The GitHub authentication failed with message: ' . hsc($e->getMessage()); |
||
100 | return false; |
||
101 | } |
||
102 | $this->user = $user; |
||
103 | |||
104 | $headers = $this->dokuHTTPClient->resp_headers; |
||
105 | $missing_scopes = array_diff($this->scopes, explode(', ', $headers['x-oauth-scopes'])); |
||
106 | if (count($missing_scopes) !== 0) { |
||
107 | $this->configError = 'Scopes "' . implode(', ', $missing_scopes) . '" are missing!'; |
||
108 | return false; |
||
109 | } |
||
110 | return true; |
||
111 | } |
||
112 | |||
113 | /** |
||
114 | * |
||
115 | * todo: ensure correct headers are set: https://developer.github.com/v3/#current-version |
||
116 | * |
||
117 | * @param string $endpoint the endpoint as defined in the GitHub documentation. With leading and trailing slashes |
||
118 | * @param int|null $max do not make more requests after this number of items have been retrieved |
||
119 | * |
||
120 | * @return array The decoded response-text |
||
121 | * @throws HTTPRequestException |
||
122 | */ |
||
123 | protected function makeGitHubGETRequest($endpoint, $max = null) |
||
124 | { |
||
125 | $results = []; |
||
126 | $waittime = 0; |
||
127 | /** @var \helper_plugin_issuelinks_util $utils */ |
||
128 | $utils = plugin_load('helper', 'issuelinks_util'); |
||
129 | do { |
||
130 | usleep($waittime); |
||
131 | try { |
||
132 | $data = $this->makeGitHubRequest($endpoint, [], 'GET', []); |
||
133 | } catch (ExternalServerException $e) { |
||
134 | if ($waittime >= 500) { |
||
135 | msg('Error repeats. Aborting Requests.', -1); |
||
136 | dbglog('Error repeats. Aborting Requests.', -1); |
||
137 | break; |
||
138 | } |
||
139 | $waittime += 100; |
||
140 | msg("Server Error occured. Waiting $waittime ms between requests and repeating request.", -1); |
||
141 | dbglog("Server Error occured. Waiting $waittime ms between requests and repeating request.", -1); |
||
142 | |||
143 | continue; |
||
144 | } |
||
145 | |||
146 | |||
147 | if ($this->dokuHTTPClient->resp_headers['x-ratelimit-remaining'] < 500) { |
||
148 | msg(sprintf( |
||
149 | $utils->getLang('error:system too many requests'), |
||
150 | dformat($this->dokuHTTPClient->resp_headers['x-ratelimit-reset']) |
||
151 | ), -1); |
||
152 | break; |
||
153 | } |
||
154 | |||
155 | $results = array_merge($results, $data); |
||
156 | |||
157 | if (empty($this->dokuHTTPClient->resp_headers['link'])) { |
||
158 | break; |
||
159 | } |
||
160 | $links = $utils->parseHTTPLinkHeaders($this->dokuHTTPClient->resp_headers['link']); |
||
161 | if (empty($links['next'])) { |
||
162 | break; |
||
163 | } |
||
164 | $endpoint = substr($links['next'], strlen($this->githubUrl)); |
||
165 | } while (empty($max) || count($results) < $max); |
||
166 | return $results; |
||
167 | } |
||
168 | |||
169 | /** |
||
170 | * @param string $endpoint |
||
171 | * @param array $data |
||
172 | * @param string $method |
||
173 | * @param array $headers |
||
174 | * |
||
175 | * @return mixed |
||
176 | * |
||
177 | * @throws HTTPRequestException|ExternalServerException |
||
178 | */ |
||
179 | protected function makeGitHubRequest($endpoint, $data, $method, $headers = []) |
||
180 | { |
||
181 | /** @var \helper_plugin_issuelinks_db $db */ |
||
182 | $db = plugin_load('helper', 'issuelinks_db'); |
||
183 | $authToken = $db->getKeyValue('github_token'); |
||
184 | $defaultHeaders = [ |
||
185 | 'Authorization' => "token $authToken", |
||
186 | 'Content-Type' => 'application/json', |
||
187 | ]; |
||
188 | |||
189 | $requestHeaders = array_merge($defaultHeaders, $headers); |
||
190 | |||
191 | // todo ensure correct slashes everywhere |
||
192 | $url = $this->githubUrl . $endpoint; |
||
193 | |||
194 | return $this->makeHTTPRequest($this->dokuHTTPClient, $url, $requestHeaders, $data, $method); |
||
195 | } |
||
196 | |||
197 | public function hydrateConfigForm(Form $configForm) |
||
198 | { |
||
199 | $scopes = implode(', ', $this->scopes); |
||
200 | $link = '<a href="https://github.com/settings/tokens">https://github.com/settings/tokens/</a>'; |
||
201 | $message = '<p>'; |
||
202 | $message .= $this->configError; |
||
203 | $message .= " Please go to $link and generate a new token for this plugin with the scopes $scopes."; |
||
204 | $message .= '</p>'; |
||
205 | $configForm->addHTML($message); |
||
206 | $configForm->addTextInput('githubToken', 'GitHub AccessToken')->useInput(false); |
||
207 | } |
||
208 | |||
209 | public function handleAuthorization() |
||
210 | { |
||
211 | global $INPUT; |
||
212 | |||
213 | $token = $INPUT->str('githubToken'); |
||
214 | |||
215 | /** @var \helper_plugin_issuelinks_db $db */ |
||
216 | $db = plugin_load('helper', 'issuelinks_db'); |
||
217 | $db->saveKeyValuePair('github_token', $token); |
||
218 | } |
||
219 | |||
220 | /** |
||
221 | * @inheritdoc |
||
222 | */ |
||
223 | public function getListOfAllReposAndHooks($organisation) |
||
224 | { |
||
225 | $endpoint = "/orgs/$organisation/repos"; |
||
226 | try { |
||
227 | $repos = $this->makeGitHubGETRequest($endpoint); |
||
228 | } catch (HTTPRequestException $e) { |
||
229 | msg($e->getMessage() . ' ' . $e->getCode(), -1); |
||
230 | return []; |
||
231 | } |
||
232 | $projects = []; |
||
233 | |||
234 | foreach ($repos as $repoData) { |
||
235 | $repo = new Repository(); |
||
236 | $repo->full_name = $repoData['full_name']; |
||
237 | $repo->displayName = $repoData['name']; |
||
238 | |||
239 | $endpoint = "/repos/$repoData[full_name]/hooks"; |
||
240 | try { |
||
241 | $repoHooks = $this->makeGitHubGETRequest($endpoint); |
||
242 | } catch (HTTPRequestException $e) { |
||
243 | $repoHooks = []; |
||
244 | $repo->error = 403; |
||
245 | } |
||
246 | $repoHooks = array_filter($repoHooks, [$this, 'isOurIssueHook']); |
||
247 | $ourIsseHook = reset($repoHooks); |
||
248 | if (!empty($ourIsseHook)) { |
||
249 | $repo->hookID = $ourIsseHook['id']; |
||
250 | } |
||
251 | $projects[] = $repo; |
||
252 | } |
||
253 | |||
254 | return $projects; |
||
255 | } |
||
256 | |||
257 | public function deleteWebhook($project, $hookid) |
||
258 | { |
||
259 | try { |
||
260 | $data = $this->makeGitHubRequest("/repos/$project/hooks/$hookid", [], 'DELETE'); |
||
261 | $status = $this->dokuHTTPClient->status; |
||
262 | |||
263 | /** @var \helper_plugin_issuelinks_db $db */ |
||
264 | $db = plugin_load('helper', 'issuelinks_db'); |
||
265 | $db->deleteWebhook('github', $project, $hookid); |
||
266 | } catch (HTTPRequestException $e) { |
||
267 | $data = $e->getMessage(); |
||
268 | $status = $e->getCode(); |
||
269 | } |
||
270 | |||
271 | return ['data' => $data, 'status' => $status]; |
||
272 | } |
||
273 | |||
274 | public function createWebhook($project) |
||
275 | { |
||
276 | $secret = md5(openssl_random_pseudo_bytes(32)); |
||
277 | $config = [ |
||
278 | "url" => self::WEBHOOK_URL, |
||
279 | "content_type" => 'json', |
||
280 | "insecure_ssl" => 0, |
||
281 | "secret" => $secret, |
||
282 | ]; |
||
283 | $data = [ |
||
284 | "name" => "web", |
||
285 | "config" => $config, |
||
286 | "active" => true, |
||
287 | 'events' => ['issues', 'issue_comment', 'pull_request'], |
||
288 | ]; |
||
289 | try { |
||
290 | $data = $this->makeGitHubRequest("/repos/$project/hooks", $data, 'POST'); |
||
291 | $status = $this->dokuHTTPClient->status; |
||
292 | $id = $data['id']; |
||
293 | /** @var \helper_plugin_issuelinks_db $db */ |
||
294 | $db = plugin_load('helper', 'issuelinks_db'); |
||
295 | $db->saveWebhook('github', $project, $id, $secret); |
||
296 | } catch (HTTPRequestException $e) { |
||
297 | $data = $e->getMessage(); |
||
298 | $status = $e->getCode(); |
||
299 | } |
||
300 | |||
301 | return ['data' => $data, 'status' => $status]; |
||
302 | } |
||
303 | |||
304 | public function validateWebhook($webhookBody) |
||
305 | { |
||
306 | $data = json_decode($webhookBody, true); |
||
307 | if (!$this->isSignatureValid($webhookBody, $data['repository']['full_name'])) { |
||
308 | return new RequestResult(403, 'Signature invalid or missing!'); |
||
309 | } |
||
310 | return true; |
||
311 | } |
||
312 | |||
313 | /** |
||
314 | * Check if the signature in the header provided by github is valid by using a stored secret |
||
315 | * |
||
316 | * |
||
317 | * Known issue: |
||
318 | * * We have to cycle through the webhooks/secrets stored for a given repo because the hookid is not in the |
||
319 | * request |
||
320 | * |
||
321 | * @param string $body The unaltered payload of the request |
||
322 | * @param string $repo_id the repo id (in the format of "organisation/repo-name") |
||
323 | * |
||
324 | * @return bool wether the provided signature checks out against a stored one |
||
325 | */ |
||
326 | protected function isSignatureValid($body, $repo_id) |
||
327 | { |
||
328 | global $INPUT; |
||
329 | if (!$INPUT->server->has('HTTP_X_HUB_SIGNATURE')) { |
||
330 | return false; |
||
331 | } |
||
332 | list($algo, $signature_github) = explode('=', $INPUT->server->str('HTTP_X_HUB_SIGNATURE')); |
||
333 | /** @var \helper_plugin_issuelinks_db $db */ |
||
334 | $db = plugin_load('helper', 'issuelinks_db'); |
||
335 | $secrets = $db->getWebhookSecrets('github', $repo_id); |
||
336 | foreach ($secrets as $secret) { |
||
337 | $signature_local = hash_hmac($algo, $body, $secret['secret']); |
||
338 | if (hash_equals($signature_local, $signature_github)) { |
||
339 | return true; |
||
340 | } |
||
341 | } |
||
342 | return false; |
||
343 | } |
||
344 | |||
345 | public function handleWebhook($webhookBody) |
||
346 | { |
||
347 | global $INPUT; |
||
348 | $data = json_decode($webhookBody, true); |
||
349 | $event = $INPUT->server->str('HTTP_X_GITHUB_EVENT'); |
||
350 | |||
351 | if ($event === 'ping') { |
||
352 | return new RequestResult(202, 'Webhook ping successful. Pings are not processed.'); |
||
353 | } |
||
354 | |||
355 | if (!$this->saveIssue($data)) { |
||
356 | return new RequestResult(500, 'There was an error saving the issue.'); |
||
357 | } |
||
358 | |||
359 | |||
360 | return new RequestResult(200, 'OK'); |
||
361 | } |
||
362 | |||
363 | /** |
||
364 | * Handle the webhook event, triggered by an updated or created issue |
||
365 | * |
||
366 | * @param array $data |
||
367 | * |
||
368 | * @return bool whether saving was successful |
||
369 | * |
||
370 | * @throws \InvalidArgumentException |
||
371 | * @throws HTTPRequestException |
||
372 | */ |
||
373 | protected function saveIssue($data) |
||
374 | { |
||
375 | |||
376 | $issue = Issue::getInstance( |
||
377 | 'github', |
||
378 | $data['repository']['full_name'], |
||
379 | $data['issue']['number'], |
||
380 | false, |
||
381 | true |
||
382 | ); |
||
383 | |||
384 | $this->setIssueData($issue, $data['issue']); |
||
385 | |||
386 | return $issue->saveToDB(); |
||
387 | } |
||
388 | |||
389 | /** |
||
390 | * @param Issue $issue |
||
391 | * @param array $info |
||
392 | */ |
||
393 | protected function setIssueData(Issue $issue, $info) |
||
394 | { |
||
395 | $issue->setSummary($info['title']); |
||
396 | $issue->setDescription($info['body']); |
||
397 | $labels = []; |
||
398 | foreach ($info['labels'] as $label) { |
||
399 | $labels[] = $label['name']; |
||
400 | $issue->setLabelData($label['name'], '#' . $label['color']); |
||
401 | } |
||
402 | $issue->setType($this->getTypeFromLabels($labels)); |
||
403 | $issue->setStatus(isset($info['merged']) ? 'merged' : $info['state']); |
||
404 | $issue->setUpdated($info['updated_at']); |
||
405 | if (!empty($info['milestone'])) { |
||
406 | $issue->setVersions([$info['milestone']['title']]); |
||
407 | } |
||
408 | $issue->setLabels($labels); |
||
409 | if ($info['assignee']) { |
||
410 | $issue->setAssignee($info['assignee']['login'], $info['assignee']['avatar_url']); |
||
411 | } |
||
412 | } |
||
413 | |||
414 | protected function getTypeFromLabels(array $labels) |
||
415 | { |
||
416 | $bugTypeLabels = ['bug']; |
||
417 | $improvementTypeLabels = ['enhancement']; |
||
418 | $storyTypeLabels = ['feature']; |
||
419 | |||
420 | if (count(array_intersect($labels, $bugTypeLabels))) { |
||
421 | return 'bug'; |
||
422 | } |
||
423 | |||
424 | if (count(array_intersect($labels, $improvementTypeLabels))) { |
||
425 | return 'improvement'; |
||
426 | } |
||
427 | |||
428 | if (count(array_intersect($labels, $storyTypeLabels))) { |
||
429 | return 'story'; |
||
430 | } |
||
431 | |||
432 | return 'unknown'; |
||
433 | } |
||
434 | |||
435 | public function getListOfAllUserOrganisations() |
||
436 | { |
||
437 | if ($this->orgs === null) { |
||
438 | $endpoint = '/user/orgs'; |
||
439 | try { |
||
440 | $this->orgs = $this->makeGitHubGETRequest($endpoint); |
||
441 | } catch (\Throwable $e) { |
||
442 | $this->orgs = []; |
||
443 | msg(hsc($e->getMessage()), -1); |
||
444 | } |
||
445 | } |
||
446 | // fixme: add 'user repos'! |
||
447 | return array_map(function ($org) { |
||
448 | return $org['login']; |
||
449 | }, $this->orgs); |
||
450 | } |
||
451 | |||
452 | public function getUserString() |
||
453 | { |
||
454 | return $this->user['login']; |
||
455 | } |
||
456 | |||
457 | public function retrieveIssue(Issue $issue) |
||
458 | { |
||
459 | $repo = $issue->getProject(); |
||
460 | $issueNumber = $issue->getKey(); |
||
461 | $endpoint = '/repos/' . $repo . '/issues/' . $issueNumber; |
||
462 | $result = $this->makeGitHubGETRequest($endpoint); |
||
463 | $this->setIssueData($issue, $result); |
||
464 | if (isset($result['pull_request'])) { |
||
465 | $issue->isMergeRequest(true); |
||
466 | $endpoint = '/repos/' . $repo . '/pulls/' . $issueNumber; |
||
467 | $result = $this->makeGitHubGETRequest($endpoint); |
||
468 | $issue->setStatus($result['merged'] ? 'merged' : $result['state']); |
||
469 | $mergeRequestText = $issue->getSummary() . ' ' . $issue->getDescription(); |
||
470 | $issues = $this->parseMergeRequestDescription($repo, $mergeRequestText); |
||
471 | /** @var \helper_plugin_issuelinks_db $db */ |
||
472 | $db = plugin_load('helper', 'issuelinks_db'); |
||
473 | $db->saveIssueIssues($issue, $issues); |
||
474 | } |
||
475 | } |
||
476 | |||
477 | /** |
||
478 | * Parse a string for issue-ids |
||
479 | * |
||
480 | * Currently only parses issues for the same repo and jira issues |
||
481 | * |
||
482 | * @param string $currentProject |
||
483 | * @param string $description |
||
484 | * |
||
485 | * @return array |
||
486 | */ |
||
487 | protected function parseMergeRequestDescription($currentProject, $description) |
||
488 | { |
||
489 | $issues = []; |
||
490 | |||
491 | $issueOwnRepoPattern = '/(?:\W|^)#([1-9]\d*)\b/'; |
||
492 | preg_match_all($issueOwnRepoPattern, $description, $githubMatches); |
||
493 | foreach ($githubMatches[1] as $issueId) { |
||
494 | $issues[] = [ |
||
495 | 'service' => 'github', |
||
496 | 'project' => $currentProject, |
||
497 | 'issueId' => $issueId, |
||
498 | ]; |
||
499 | } |
||
500 | |||
501 | // FIXME: this should be done by JIRA service class |
||
502 | $jiraMatches = []; |
||
503 | $jiraPattern = '/[A-Z0-9]+-[1-9]\d*/'; |
||
504 | preg_match_all($jiraPattern, $description, $jiraMatches); |
||
505 | foreach ($jiraMatches[0] as $match) { |
||
506 | list($project, $issueId) = explode('-', $match); |
||
507 | $issues[] = [ |
||
508 | 'service' => 'jira', |
||
509 | 'project' => $project, |
||
510 | 'issueId' => $issueId, |
||
511 | ]; |
||
512 | } |
||
513 | |||
514 | return $issues; |
||
515 | } |
||
516 | |||
517 | /** |
||
518 | * |
||
519 | * @see https://developer.github.com/v3/issues/#list-issues-for-a-repository |
||
520 | * |
||
521 | * @param string $projectKey The short-key of the project to be imported |
||
522 | * @param int $startat The offset from the last Element from which to start importing |
||
523 | * |
||
524 | * @return array The issues, suitable to be saved into the db |
||
525 | * @throws HTTPRequestException |
||
526 | * |
||
527 | * // FIXME: set Header application/vnd.github.symmetra-preview+json ? |
||
528 | */ |
||
529 | public function retrieveAllIssues($projectKey, &$startat = 0) |
||
530 | { |
||
531 | $perPage = 30; |
||
532 | $page = ceil(($startat + 1) / $perPage); |
||
533 | // FIXME: implent `since` parameter? |
||
534 | $endpoint = "/repos/$projectKey/issues?state=all&page=$page"; |
||
535 | $issues = $this->makeGitHubGETRequest($endpoint); |
||
536 | |||
537 | if (!is_array($issues)) { |
||
538 | return []; |
||
539 | } |
||
540 | if (empty($this->total)) { |
||
541 | $this->total = $this->estimateTotal($perPage, count($issues)); |
||
542 | } |
||
543 | $retrievedIssues = []; |
||
544 | foreach ($issues as $issueData) { |
||
545 | try { |
||
546 | $issue = Issue::getInstance( |
||
547 | 'github', |
||
548 | $projectKey, |
||
549 | $issueData['number'], |
||
550 | !empty($issueData['pull_request']) |
||
551 | ); |
||
552 | } catch (\InvalidArgumentException $e) { |
||
553 | continue; |
||
554 | } |
||
555 | $this->setIssueData($issue, $issueData); |
||
556 | $issue->saveToDB(); |
||
557 | $retrievedIssues[] = $issue; |
||
558 | } |
||
559 | $startat += $perPage; |
||
560 | return $retrievedIssues; |
||
561 | } |
||
562 | |||
563 | /** |
||
564 | * Estimate the total amount of results |
||
565 | * |
||
566 | * @param int $perPage amount of results per page |
||
567 | * @param int $default what is returned if the total can not be calculated otherwise |
||
568 | * |
||
569 | * @return |
||
570 | */ |
||
571 | protected function estimateTotal($perPage, $default) |
||
572 | { |
||
573 | $headers = $this->dokuHTTPClient->resp_headers; |
||
574 | |||
575 | if (empty($headers['link'])) { |
||
576 | return $default; |
||
577 | } |
||
578 | |||
579 | /** @var \helper_plugin_issuelinks_util $util */ |
||
580 | $util = plugin_load('helper', 'issuelinks_util'); |
||
581 | $links = $util->parseHTTPLinkHeaders($headers['link']); |
||
582 | preg_match('/page=(\d+)$/', $links['last'], $matches); |
||
583 | if (!empty($matches[1])) { |
||
584 | return $matches[1] * $perPage; |
||
585 | } |
||
586 | return $default; |
||
587 | } |
||
588 | |||
589 | /** |
||
590 | * @return mixed |
||
591 | */ |
||
592 | public function getTotalIssuesBeingImported() |
||
593 | { |
||
594 | return $this->total; |
||
595 | } |
||
596 | |||
597 | /** |
||
598 | * See if this is a hook for issue events, that has been set by us |
||
599 | * |
||
600 | * @param array $hook the hook data coming from github |
||
601 | * |
||
602 | * @return bool |
||
603 | */ |
||
604 | protected function isOurIssueHook($hook) |
||
605 | { |
||
606 | if ($hook['config']['url'] !== self::WEBHOOK_URL) { |
||
607 | return false; |
||
608 | } |
||
609 | |||
610 | if ($hook['config']['content_type'] !== 'json') { |
||
611 | return false; |
||
612 | } |
||
613 | |||
614 | if ($hook['config']['insecure_ssl'] !== '0') { |
||
615 | return false; |
||
616 | } |
||
617 | |||
618 | if (!$hook['active']) { |
||
619 | return false; |
||
620 | } |
||
621 | |||
622 | $missingEvents = array_diff($hook['events'], ['issues', 'issue_comment', 'pull_request']); |
||
623 | if (count($hook['events']) !== 3 || $missingEvents) { |
||
0 ignored issues
–
show
|
|||
624 | return false; |
||
625 | } |
||
626 | |||
627 | return true; |
||
628 | } |
||
629 | } |
||
630 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.