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

GithubController::sync_issue_status()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 67
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 5.0004

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 39
nc 5
nop 0
dl 0
loc 67
ccs 37
cts 38
cp 0.9737
crap 5.0004
rs 8.9848
c 1
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * Github controller handling issue creation, comments and sync.
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;
20
21
use Cake\Core\Configure;
22
use Cake\Http\Exception\NotFoundException;
23
use Cake\Log\Log;
24
use Cake\ORM\TableRegistry;
25
use Cake\Routing\Router;
26
use InvalidArgumentException;
27
use function __;
28
use function array_key_exists;
29
use function explode;
30
use function in_array;
31
use function intval;
32
use function print_r;
33
use App\Controller\Component\GithubApiComponent;
34
35
/**
36
 * Github controller handling github issue submission and creation.
37
 *
38
 * @property GithubApiComponent $GithubApi
39
 */
40
class GithubController extends AppController
41
{
42
    /**
43
     * Initialization hook method.
44
     *
45
     * Use this method to add common initialization code like loading components.
46
     *
47
     * @return void Nothing
48
     */
49 35
    public function initialize(): void
50
    {
51 35
        parent::initialize();
52 35
        $this->loadComponent('GithubApi');
53 35
        $this->viewBuilder()->setHelpers([
54 35
            'Html',
55
            'Form',
56
        ]);
57 35
    }
58
59
    /**
60
     * create Github Issue.
61
     *
62
     * @param int $reportId The report number
63
     *
64
     * @throws NotFoundException
65
     * @return void Nothing
66
     */
67 7
    public function create_issue($reportId): void
68
    {
69 7
        if (empty($reportId)) {
70
            throw new NotFoundException(__('Invalid report Id.'));
71
        }
72
73 7
        $reportId = (int) $reportId;
74
75 7
        $reportsTable = TableRegistry::getTableLocator()->get('Reports');
76 7
        $report = $reportsTable->findById($reportId)->all()->first();
77
78 7
        if (! $report) {
79 7
            throw new NotFoundException(__('The report does not exist.'));
80
        }
81
82 7
        $reportArray = $report->toArray();
83 7
        if (empty($this->request->getParsedBody())) {
84 7
            $this->set('error_name', $reportArray['error_name']);
85 7
            $this->set('error_message', $reportArray['error_message']);
86
87 7
            return;
88
        }
89
90 7
        $this->disableAutoRender();
91 2
        $data = [
92 7
            'title' => $this->request->getData('summary'),
93 7
            'labels' => $this->request->getData('labels') ? explode(',', $this->request->getData('labels')) : [],
94
        ];
95 7
        $incidents_query = TableRegistry::getTableLocator()->get('Incidents')->findByReportId($reportId)->all();
96 7
        $incident = $incidents_query->first();
97 7
        $reportArray['exception_type'] = $incident['exception_type'] ? 'php' : 'js';
98 7
        $reportArray['description'] = $this->request->getData('description');
99
100 7
        $data['body']
101 7
            = $this->getReportDescriptionText($reportId, $reportArray);
102 7
        $data['labels'][] = 'automated-error-report';
103
104 7
        [$issueDetails, $status] = $this->GithubApi->createIssue(
105 7
            Configure::read('GithubRepoPath'),
106 1
            $data,
107 7
            $this->request->getSession()->read('access_token')
108
        );
109
110 7
        if ($this->handleGithubResponse($status, 1, $reportId, $issueDetails['number'])) {
111
            // Update report status
112 7
            $report->status = $this->getReportStatusFromIssueState($issueDetails['state']);
113 7
            $reportsTable->save($report);
114
115 7
            $this->redirect([
116 7
                '_name' => 'reports:view',
117 7
                'id' => $reportId,
118
            ]);
119
        } else {
120 7
            $flash_class = 'alert alert-error';
121 7
            $this->Flash->set(
122 7
                $this->getErrors($issueDetails, $status),
123 7
                ['params' => ['class' => $flash_class]]
124
            );
125
        }
126 7
    }
127
128
    /**
129
     * Links error report to existing issue on Github.
130
     *
131
     * @param int $reportId The report Id
132
     * @throws NotFoundException
133
     * @return void Nothing
134
     */
135 7
    public function link_issue($reportId): void
136
    {
137 7
        if (empty($reportId)) {
138
            throw new NotFoundException(__('Invalid report Id.'));
139
        }
140
141 7
        $reportId = (int) $reportId;
142
143 7
        $reportsTable = TableRegistry::getTableLocator()->get('Reports');
144 7
        $report = $reportsTable->findById($reportId)->all()->first();
145
146 7
        if (! $report) {
147 7
            throw new NotFoundException(__('The report does not exist.'));
148
        }
149
150 7
        $ticket_id = intval($this->request->getQuery('ticket_id'));
151 7
        if (! $ticket_id) {
152 7
            throw new NotFoundException(__('Invalid Ticket ID!!'));
153
        }
154 7
        $reportArray = $report->toArray();
155
156 7
        $incidents_query = TableRegistry::getTableLocator()->get('Incidents')->findByReportId($reportId)->all();
157 7
        $incident = $incidents_query->first();
158 7
        $reportArray['exception_type'] = $incident['exception_type'] ? 'php' : 'js';
159
160 7
        $commentText = $this->getReportDescriptionText(
161 7
            $reportId,
162 1
            $reportArray
163
        );
164 7
        [$commentDetails, $status] = $this->GithubApi->createComment(
165 7
            Configure::read('GithubRepoPath'),
166 7
            ['body' => $commentText],
167 1
            $ticket_id,
168 7
            $this->request->getSession()->read('access_token')
169
        );
170 7
        if ($this->handleGithubResponse($status, 2, $reportId, $ticket_id)) {
171
            // Update report status
172 7
            $report->status = 'forwarded';
173
174 7
            [$issueDetails, $status] = $this->GithubApi->getIssue(
175 7
                Configure::read('GithubRepoPath'),
176 7
                [],
177 1
                $ticket_id,
178 7
                $this->request->getSession()->read('access_token')
179
            );
180 7
            if ($this->handleGithubResponse($status, 4, $reportId, $ticket_id)) {
181
                // If linked Github issue state is available, use it to update Report's status
182 7
                $report->status = $this->getReportStatusFromIssueState(
183 7
                    $issueDetails['state']
184
                );
185
            }
186
187 7
            $reportsTable->save($report);
188
        } else {
189 7
            $flash_class = 'alert alert-error';
190 7
            $this->Flash->set(
191 7
                $this->getErrors($commentDetails, $status),
192 7
                ['params' => ['class' => $flash_class]]
193
            );
194
        }
195
196 7
        $this->redirect([
197 7
            '_name' => 'reports:view',
198 7
            'id' => $reportId,
199
        ]);
200 7
    }
201
202
    /**
203
     * Un-links error report to associated issue on Github.
204
     *
205
     * @param int $reportId The report Id
206
     * @throws NotFoundException
207
     * @return void Nothing
208
     */
209 7
    public function unlink_issue($reportId): void
210
    {
211 7
        if (empty($reportId)) {
212
            throw new NotFoundException(__('Invalid report Id.'));
213
        }
214
215 7
        $reportId = (int) $reportId;
216
217 7
        $reportsTable = TableRegistry::getTableLocator()->get('Reports');
218 7
        $report = $reportsTable->findById($reportId)->all()->first();
219
220 7
        if (! $report) {
221 7
            throw new NotFoundException(__('The report does not exist.'));
222
        }
223
224 7
        $reportArray = $report->toArray();
225 7
        $ticket_id = $reportArray['sourceforge_bug_id'];
226
227 7
        if (! $ticket_id) {
228 7
            throw new NotFoundException(__('Invalid Ticket ID!!'));
229
        }
230
231
        // "formatted" text of the comment.
232 2
        $commentText = 'This Issue is no longer associated with [Report#'
233 7
            . $reportId
234 7
            . ']('
235 7
            . Router::url([
236 7
                '_name' => 'reports:view',
237 7
                'id' => $reportId,
238 7
            ], true) . ')'
239 7
            . "\n\n*This comment is posted automatically by phpMyAdmin's "
240 7
            . '[error-reporting-server](https://reports.phpmyadmin.net).*';
241
242 7
        [$commentDetails, $status] = $this->GithubApi->createComment(
243 7
            Configure::read('GithubRepoPath'),
244 7
            ['body' => $commentText],
245 1
            $ticket_id,
246 7
            $this->request->getSession()->read('access_token')
247
        );
248
249 7
        if ($this->handleGithubResponse($status, 3, $reportId)) {
250
            // Update report status
251 7
            $report->status = 'new';
252 7
            $reportsTable->save($report);
253
        } else {
254 7
            $flash_class = 'alert alert-error';
255 7
            $this->Flash->set(
256 7
                $this->getErrors($commentDetails, $status),
257 7
                ['params' => ['class' => $flash_class]]
258
            );
259
        }
260
261 7
        $this->redirect([
262 7
            '_name' => 'reports:view',
263 7
            'id' => $reportId,
264
        ]);
265 7
    }
266
267
    /**
268
     * Returns pretty error message string.
269
     *
270
     * @param object|array $response the response returned by Github api
271
     * @param int          $status   status returned by Github API
272
     *
273
     * @return string error string
274
     */
275 21
    protected function getErrors($response, int $status): string
276
    {
277 6
        $errorString = 'There were some problems with the issue submission.'
278 21
            . ' Returned status is (' . $status . ')';
279
        $errorString .= '<br/> Here is the dump for the errors field provided by'
280
            . ' github: <br/>'
281
            . '<pre>'
282 21
            . print_r($response, true)
283 21
            . '</pre>';
284
285 21
        return $errorString;
286
    }
287
288
    /**
289
     * Returns the text to be added while creating an issue
290
     *
291
     * @param int   $reportId Report Id
292
     * @param array $report   Report associative array
293
     * @return string the text
294
     */
295 14
    protected function getReportDescriptionText(int $reportId, array $report): string
296
    {
297 14
        $incident_count = $this->getTotalIncidentCount($reportId);
298
299
        // "formatted" text of the comment.
300 4
        $formattedText
301 14
            = array_key_exists('description', $report) ? $report['description'] . "\n\n"
302 12
                : '';
303
        $formattedText .= "\nParam | Value "
304
            . "\n -----------|--------------------"
305 14
            . "\n Error Type | " . $report['error_name']
306 14
            . "\n Error Message |" . $report['error_message']
307 14
            . "\n Exception Type |" . $report['exception_type']
308 14
            . "\n phpMyAdmin version |" . $report['pma_version']
309 14
            . "\n Incident count | " . $incident_count
310 14
            . "\n Link | [Report#"
311 14
                . $reportId
312 14
                . ']('
313 14
                . Router::url([
314 14
                    '_name' => 'reports:view',
315 14
                    'id' => $reportId,
316 14
                ], true)
317 14
                . ')'
318 14
            . "\n\n*This comment is posted automatically by phpMyAdmin's "
319 14
            . '[error-reporting-server](https://reports.phpmyadmin.net).*';
320
321 14
        return $formattedText;
322
    }
323
324
    /**
325
     * Github Response Handler.
326
     *
327
     * @param int $response  the status returned by Github API
328
     * @param int $type      type of response. 1 for create_issue, 2 for link_issue, 3 for unlink_issue,
329
     *                       1 for create_issue,
330
     *                       2 for link_issue,
331
     *                       3 for unlink_issue,
332
     *                       4 for get_issue
333
     * @param int $report_id report id
334
     * @param int $ticket_id ticket id, required for link ticket only
335
     *
336
     * @return bool value. True on success. False on any type of failure.
337
     */
338 35
    protected function handleGithubResponse(int $response, int $type, int $report_id, int $ticket_id = 1): bool
339
    {
340 35
        if (! in_array($type, [1, 2, 3, 4])) {
341
            throw new InvalidArgumentException('Invalid Argument ' . $type . '.');
342
        }
343
344 35
        $updateReport = true;
345
346 35
        if ($type === 4 && $response === 200) {
347
            // issue details fetched successfully
348 21
            return true;
349
        }
350
351 28
        if ($response === 201) {
352
            // success
353
            switch ($type) {
354 21
                case 1:
355 7
                    $msg = 'Github issue has been created for this report.';
356 7
                    break;
357 14
                case 2:
358 7
                    $msg = 'Github issue has been linked with this report.';
359 7
                    break;
360 7
                case 3:
361 7
                    $msg = 'Github issue has been unlinked with this report.';
362 7
                    $ticket_id = null;
363 7
                    break;
364
365
                default:
366
                    $msg = 'Something went wrong!';
367
                    break;
368
            }
369
370 21
            if ($updateReport) {
371 21
                $report = TableRegistry::getTableLocator()->get('Reports')->get($report_id);
372 21
                $report->sourceforge_bug_id = $ticket_id;
373 21
                TableRegistry::getTableLocator()->get('Reports')->save($report);
374
            }
375
376 21
            if ($msg !== '') {
377 21
                $flash_class = 'alert alert-success';
378 21
                $this->Flash->set(
379 21
                    $msg,
380 21
                    ['params' => ['class' => $flash_class]]
381
                );
382
            }
383
384 21
            return true;
385
        }
386
387 28
        if ($response === 403) {
388 7
            $flash_class = 'alert alert-error';
389 7
            $this->Flash->set(
390
                'Unauthorised access to Github. github'
391
                    . ' credentials may be out of date. Please check and try again'
392 7
                    . ' later.',
393 7
                ['params' => ['class' => $flash_class]]
394
            );
395
396 7
            return false;
397
        }
398
399 21
        if ($response === 404
400 21
            && $type === 2
401
        ) {
402 7
            $flash_class = 'alert alert-error';
403 7
            $this->Flash->set(
404
                'Bug Issue not found on Github.'
405 7
                    . ' Are you sure the issue number is correct? Please check and try again!',
406 7
                ['params' => ['class' => $flash_class]]
407
            );
408
409 7
            return false;
410
        }
411
412
        // unknown response code
413 14
        $flash_class = 'alert alert-error';
414 14
        $this->Flash->set(
415 14
            'Unhandled response code received: ' . $response,
416 14
            ['params' => ['class' => $flash_class]]
417
        );
418
419 14
        return false;
420
    }
421
422
    /**
423
     * Get Incident counts for a report and
424
     * all its related reports
425
     *
426
     * @param int $reportId The report Id
427
     *
428
     * @return int Total Incident count for a report
429
     */
430 14
    protected function getTotalIncidentCount(int $reportId): int
431
    {
432 14
        $incidents_query = TableRegistry::getTableLocator()->get('Incidents')->findByReportId($reportId)->all();
433 14
        $incident_count = $incidents_query->count();
434
435 4
        $params_count = [
436 14
            'fields' => ['inci_count' => 'inci_count'],
437
            'conditions' => [
438 14
                'related_to = ' . $reportId,
439
            ],
440
        ];
441 4
        $subquery_params_count = [
442 10
            'fields' => ['report_id' => 'report_id'],
443
        ];
444 14
        $subquery_count = TableRegistry::getTableLocator()->get('Incidents')->find(
445 14
            'all',
446 2
            $subquery_params_count
447
        );
448 14
        $inci_count_related = TableRegistry::getTableLocator()->get('Reports')->find('all', $params_count)->innerJoin(
449 14
            ['incidents' => $subquery_count],
450 14
            ['incidents.report_id = Reports.related_to']
451 14
        )->count();
452
453 14
        return $incident_count + $inci_count_related;
454
    }
455
456
    /**
457
     * Get corresponding report status from Github issue state
458
     *
459
     * @param string $issueState Linked Github issue's state
460
     *
461
     * @return string Corresponding status to which the linked report should be updated to
462
     */
463 28
    protected function getReportStatusFromIssueState(string $issueState): string
464
    {
465
        // default
466 28
        $reportStatus = '';
467 28
        switch ($issueState) {
468 28
            case 'closed':
469 21
                $reportStatus = 'resolved';
470 21
                break;
471
472
            default:
473 28
                $reportStatus = 'forwarded';
474 28
                break;
475
        }
476
477 28
        return $reportStatus;
478
    }
479
480
    /**
481
     * Synchronize Report Statuses from Github issues
482
     *
483
     * To be used as a cron job (using webroot/cron_dispatcher.php).
484
     *
485
     * Can not (& should not) be directly accessed via web.
486
     *
487
     * @return void Nothing
488
     */
489 14
    public function sync_issue_status(): void
490
    {
491 14
        if (! Configure::read('CronDispatcher')) {
492 7
            $flash_class = 'alert alert-error';
493 7
            $this->Flash->set(
494 7
                'Unauthorised action! This action is not available on Web interface',
495 7
                ['params' => ['class' => $flash_class]]
496
            );
497
498 7
            $this->redirect('/');
499
500 7
            return;
501
        }
502
503 14
        $this->disableAutoRender();
504 14
        $reportsTable = TableRegistry::getTableLocator()->get('Reports');
505
506
        // Fetch all linked reports
507 14
        $reports = $reportsTable->find(
508 14
            'all',
509
            [
510 2
                'conditions' => [
511 12
                    'sourceforge_bug_id IS NOT NULL',
512
                    'NOT' => ['status' => 'resolved'],
513
                ],
514
            ]
515
        );
516
517 14
        foreach ($reports as $report) {
518 14
            $report = $report->toArray();
519
520
            // fetch the new issue status
521 14
            [$issueDetails, $status] = $this->GithubApi->getIssue(
522 14
                Configure::read('GithubRepoPath'),
523 14
                [],
524 14
                $report['sourceforge_bug_id'],
525 14
                Configure::read('GithubAccessToken')
526
            );
527
528 14
            if (! $this->handleGithubResponse($status, 4, $report['id'], $report['sourceforge_bug_id'])) {
529 7
                Log::error(
530
                    'FAILED: Fetching status of Issue #'
531 7
                        . $report['sourceforge_bug_id']
532 7
                        . ' associated with Report#'
533 7
                        . $report['id']
534 7
                        . '. Status returned: ' . $status,
535 7
                    ['scope' => 'cron_jobs']
536
                );
537 7
                continue;
538
            }
539
540
            // if Github issue state has changed, update the status of report
541 14
            if ($report['status'] === $issueDetails['state']) {
542
                continue;
543
            }
544
545 14
            $rep = $reportsTable->get($report['id']);
546 14
            $rep->status = $this->getReportStatusFromIssueState($issueDetails['state']);
547
548
            // Save the report
549 14
            $reportsTable->save($rep);
550
551 14
            Log::debug(
552
                'SUCCESS: Updated status of Report #'
553 14
                . $report['id'] . ' from state of its linked Github issue #'
554 14
                . $report['sourceforge_bug_id'] . ' to ' . $rep->status,
555 14
                ['scope' => 'cron_jobs']
556
            );
557
        }
558 14
    }
559
}
560