GithubController::link_issue()   B
last analyzed

Complexity

Conditions 7
Paths 9

Size

Total Lines 65
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 7.0007

Importance

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