Completed
Push — master ( 07e3a8...9561cd )
by William
16:58 queued 14:23
created

GithubController::getErrors()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 2
dl 0
loc 11
ccs 5
cts 5
cp 1
crap 1
rs 10
c 0
b 0
f 0
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\Event\Event;
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
use function __;
29
use function array_key_exists;
30
use function explode;
31
use function in_array;
32
use function intval;
33
use function print_r;
34
35
/**
36
 * Github controller handling github issue submission and creation.
37
 */
38
class GithubController extends AppController
39
{
40
    /** @var string */
41
    public $helpers = [
42
        'Html',
43
        'Form',
44
    ];
45
    /** @var string */
46
    public $components = ['GithubApi'];
47
48 20
    public function beforeFilter(Event $event): void
49
    {
50 20
        parent::beforeFilter($event);
51 20
        $this->GithubApi->githubConfig = Configure::read('GithubConfig');
0 ignored issues
show
Bug Best Practice introduced by
The property GithubApi does not exist on App\Controller\GithubController. Since you implemented __get, consider adding a @property annotation.
Loading history...
52 20
        $this->GithubApi->githubRepo = Configure::read('GithubRepoPath');
53 20
    }
54
55
    /**
56
     * create Github Issue.
57
     *
58
     * @param int $reportId The report number
59
     *
60
     * @throws NotFoundException
61
     * @return void Nothing
62
     */
63 4
    public function create_issue(int $reportId): void
64
    {
65 4
        if (! isset($reportId) || ! $reportId) {
66
            throw new NotFoundException(__('Invalid report'));
67
        }
68
69 4
        $reportsTable = TableRegistry::getTableLocator()->get('Reports');
70 4
        $report = $reportsTable->findById($reportId)->all()->first();
71
72 4
        if (! $report) {
73 4
            throw new NotFoundException(__('Invalid report'));
74
        }
75
76 4
        $reportArray = $report->toArray();
77 4
        if (empty($this->request->data)) {
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Http\ServerRequest::$data has been deprecated: 3.4.0 This public property will be removed in 4.0.0. Use getData() instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

77
        if (empty(/** @scrutinizer ignore-deprecated */ $this->request->data)) {

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
78 4
            $this->set('error_name', $reportArray['error_name']);
79 4
            $this->set('error_message', $reportArray['error_message']);
80
81 4
            return;
82
        }
83
84 4
        $this->autoRender = false;
85
        $data = [
86 4
            'title' => $this->request->data['summary'],
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Http\ServerRequest::$data has been deprecated: 3.4.0 This public property will be removed in 4.0.0. Use getData() instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

86
            'title' => /** @scrutinizer ignore-deprecated */ $this->request->data['summary'],

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
87 4
            'labels' => $this->request->data['labels'] ? explode(',', $this->request->data['labels']) : [],
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Http\ServerRequest::$data has been deprecated: 3.4.0 This public property will be removed in 4.0.0. Use getData() instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

87
            'labels' => $this->request->data['labels'] ? explode(',', /** @scrutinizer ignore-deprecated */ $this->request->data['labels']) : [],

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
88
        ];
89 4
        $incidents_query = TableRegistry::getTableLocator()->get('Incidents')->findByReportId($reportId)->all();
90 4
        $incident = $incidents_query->first();
91 4
        $reportArray['exception_type'] = $incident['exception_type'] ? 'php' : 'js';
92 4
        $reportArray['description'] = $this->request->data['description'];
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Http\ServerRequest::$data has been deprecated: 3.4.0 This public property will be removed in 4.0.0. Use getData() instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

92
        $reportArray['description'] = /** @scrutinizer ignore-deprecated */ $this->request->data['description'];

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
93
94 4
        $data['body']
95 4
            = $this->getReportDescriptionText($reportId, $reportArray);
96 4
        $data['labels'][] = 'automated-error-report';
97
98 4
        [$issueDetails, $status] = $this->GithubApi->createIssue(
0 ignored issues
show
Bug Best Practice introduced by
The property GithubApi does not exist on App\Controller\GithubController. Since you implemented __get, consider adding a @property annotation.
Loading history...
99 4
            Configure::read('GithubRepoPath'),
100 2
            $data,
101 4
            $this->request->getSession()->read('access_token')
102
        );
103
104 4
        if ($this->handleGithubResponse($status, 1, $reportId, $issueDetails['number'])) {
105
            // Update report status
106 4
            $report->status = $this->getReportStatusFromIssueState($issueDetails['state']);
107 4
            $reportsTable->save($report);
108
109 4
            $this->redirect(['controller' => 'reports', 'action' => 'view',
110 4
                $reportId,
111
            ]);
112
        } else {
113 4
            $flash_class = 'alert alert-error';
114 4
            $this->Flash->default(
115 4
                $this->getErrors($issueDetails, $status),
116 4
                ['params' => ['class' => $flash_class]]
117
            );
118
        }
119 4
    }
120
121
    /**
122
     * Links error report to existing issue on Github.
123
     *
124
     * @param int $reportId The report Id
125
     * @return void Nothing
126
     */
127 4
    public function link_issue(int $reportId): void
128
    {
129 4
        if (! isset($reportId) || ! $reportId) {
130
            throw new NotFoundException(__('Invalid reportId'));
131
        }
132
133 4
        $reportsTable = TableRegistry::getTableLocator()->get('Reports');
134 4
        $report = $reportsTable->findById($reportId)->all()->first();
135
136 4
        if (! $report) {
137 4
            throw new NotFoundException(__('Invalid report'));
138
        }
139
140 4
        $ticket_id = intval($this->request->query['ticket_id']);
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Http\ServerRequest::$query has been deprecated: 3.4.0 This public property will be removed in 4.0.0. Use getQuery() or getQueryParams() instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

140
        $ticket_id = intval(/** @scrutinizer ignore-deprecated */ $this->request->query['ticket_id']);

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
141 4
        if (! $ticket_id) {
142 4
            throw new NotFoundException(__('Invalid Ticket ID!!'));
143
        }
144 4
        $reportArray = $report->toArray();
145
146 4
        $incidents_query = TableRegistry::getTableLocator()->get('Incidents')->findByReportId($reportId)->all();
147 4
        $incident = $incidents_query->first();
148 4
        $reportArray['exception_type'] = $incident['exception_type'] ? 'php' : 'js';
149
150 4
        $commentText = $this->getReportDescriptionText(
151 4
            $reportId,
152 2
            $reportArray
153
        );
154 4
        [$commentDetails, $status] = $this->GithubApi->createComment(
0 ignored issues
show
Bug Best Practice introduced by
The property GithubApi does not exist on App\Controller\GithubController. Since you implemented __get, consider adding a @property annotation.
Loading history...
155 4
            Configure::read('GithubRepoPath'),
156 4
            ['body' => $commentText],
157 2
            $ticket_id,
158 4
            $this->request->getSession()->read('access_token')
159
        );
160 4
        if ($this->handleGithubResponse($status, 2, $reportId, $ticket_id)) {
161
            // Update report status
162 4
            $report->status = 'forwarded';
163
164 4
            [$issueDetails, $status] = $this->GithubApi->getIssue(
165 4
                Configure::read('GithubRepoPath'),
166 4
                [],
167 2
                $ticket_id,
168 4
                $this->request->getSession()->read('access_token')
169
            );
170 4
            if ($this->handleGithubResponse($status, 4, $reportId, $ticket_id)) {
171
                // If linked Github issue state is available, use it to update Report's status
172 4
                $report->status = $this->getReportStatusFromIssueState(
173 4
                    $issueDetails['state']
174
                );
175
            }
176
177 4
            $reportsTable->save($report);
178
        } else {
179 4
            $flash_class = 'alert alert-error';
180 4
            $this->Flash->default(
181 4
                $this->getErrors($commentDetails, $status),
182 4
                ['params' => ['class' => $flash_class]]
183
            );
184
        }
185
186 4
        $this->redirect(['controller' => 'reports', 'action' => 'view',
187 4
            $reportId,
188
        ]);
189 4
    }
190
191
    /**
192
     * Un-links error report to associated issue on Github.
193
     *
194
     * @param int $reportId The report Id
195
     * @return void Nothing
196
     */
197 4
    public function unlink_issue(int $reportId): void
198
    {
199 4
        if (! isset($reportId) || ! $reportId) {
200
            throw new NotFoundException(__('Invalid reportId'));
201
        }
202
203 4
        $reportsTable = TableRegistry::getTableLocator()->get('Reports');
204 4
        $report = $reportsTable->findById($reportId)->all()->first();
205
206 4
        if (! $report) {
207 4
            throw new NotFoundException(__('Invalid report'));
208
        }
209
210 4
        $reportArray = $report->toArray();
211 4
        $ticket_id = $reportArray['sourceforge_bug_id'];
212
213 4
        if (! $ticket_id) {
214 4
            throw new NotFoundException(__('Invalid Ticket ID!!'));
215
        }
216
217
        // "formatted" text of the comment.
218
        $commentText = 'This Issue is no longer associated with [Report#'
219 4
            . $reportId
220 4
            . ']('
221 4
            . Router::url('/reports/view/' . $reportId, true)
222 4
            . ')'
223 4
            . "\n\n*This comment is posted automatically by phpMyAdmin's "
224 4
            . '[error-reporting-server](https://reports.phpmyadmin.net).*';
225
226 4
        [$commentDetails, $status] = $this->GithubApi->createComment(
0 ignored issues
show
Bug Best Practice introduced by
The property GithubApi does not exist on App\Controller\GithubController. Since you implemented __get, consider adding a @property annotation.
Loading history...
227 4
            Configure::read('GithubRepoPath'),
228 4
            ['body' => $commentText],
229 2
            $ticket_id,
230 4
            $this->request->getSession()->read('access_token')
231
        );
232
233 4
        if ($this->handleGithubResponse($status, 3, $reportId)) {
234
            // Update report status
235 4
            $report->status = 'new';
236 4
            $reportsTable->save($report);
237
        } else {
238 4
            $flash_class = 'alert alert-error';
239 4
            $this->Flash->default(
240 4
                $this->getErrors($commentDetails, $status),
241 4
                ['params' => ['class' => $flash_class]]
242
            );
243
        }
244
245 4
        $this->redirect(['controller' => 'reports', 'action' => 'view',
246 4
            $reportId,
247
        ]);
248 4
    }
249
250
    /**
251
     * Returns pretty error message string.
252
     *
253
     * @param object|array $response the response returned by Github api
254
     * @param int          $status   status returned by Github API
255
     *
256
     * @return string error string
257
     */
258 12
    protected function getErrors($response, int $status): string
259
    {
260
        $errorString = 'There were some problems with the issue submission.'
261 12
            . ' Returned status is (' . $status . ')';
262
        $errorString .= '<br/> Here is the dump for the errors field provided by'
263
            . ' github: <br/>'
264
            . '<pre>'
265 12
            . print_r($response, true)
266 12
            . '</pre>';
267
268 12
        return $errorString;
269
    }
270
271
    /**
272
     * Returns the text to be added while creating an issue
273
     *
274
     * @param int   $reportId Report Id
275
     * @param array $report   Report associative array
276
     * @return string the text
277
     */
278 8
    protected function getReportDescriptionText(int $reportId, array $report): string
279
    {
280 8
        $incident_count = $this->getTotalIncidentCount($reportId);
281
282
        // "formatted" text of the comment.
283
        $formattedText
284 8
            = array_key_exists('description', $report) ? $report['description'] . "\n\n"
285 8
                : '';
286
        $formattedText .= "\nParam | Value "
287
            . "\n -----------|--------------------"
288 8
            . "\n Error Type | " . $report['error_name']
289 8
            . "\n Error Message |" . $report['error_message']
290 8
            . "\n Exception Type |" . $report['exception_type']
291 8
            . "\n phpMyAdmin version |" . $report['pma_version']
292 8
            . "\n Incident count | " . $incident_count
293 8
            . "\n Link | [Report#"
294 8
                . $reportId
295 8
                . ']('
296 8
                . Router::url('/reports/view/' . $reportId, true)
297 8
                . ')'
298 8
            . "\n\n*This comment is posted automatically by phpMyAdmin's "
299 8
            . '[error-reporting-server](https://reports.phpmyadmin.net).*';
300
301 8
        return $formattedText;
302
    }
303
304
    /**
305
     * Github Response Handler.
306
     *
307
     * @param int $response  the status returned by Github API
308
     * @param int $type      type of response. 1 for create_issue, 2 for link_issue, 3 for unlink_issue,
309
     *                       1 for create_issue,
310
     *                       2 for link_issue,
311
     *                       3 for unlink_issue,
312
     *                       4 for get_issue
313
     * @param int $report_id report id
314
     * @param int $ticket_id ticket id, required for link ticket only
315
     *
316
     * @return bool value. True on success. False on any type of failure.
317
     */
318 20
    protected function handleGithubResponse(int $response, int $type, int $report_id, int $ticket_id = 1): bool
319
    {
320 20
        if (! in_array($type, [1, 2, 3, 4])) {
321
            throw new InvalidArgumentException('Invalid Argument ' . $type . '.');
322
        }
323
324 20
        $updateReport = true;
325
326 20
        if ($type === 4 && $response === 200) {
327
            // issue details fetched successfully
328 12
            return true;
329
        }
330
331 16
        if ($response === 201) {
332
            // success
333
            switch ($type) {
334 12
                case 1:
335 4
                    $msg = 'Github issue has been created for this report.';
336 4
                    break;
337 8
                case 2:
338 4
                    $msg = 'Github issue has been linked with this report.';
339 4
                    break;
340 4
                case 3:
341 4
                    $msg = 'Github issue has been unlinked with this report.';
342 4
                    $ticket_id = null;
343 4
                    break;
344
345
                default:
346
                    $msg = 'Something went wrong!';
347
                    break;
348
            }
349
350 12
            if ($updateReport) {
0 ignored issues
show
introduced by
The condition $updateReport is always true.
Loading history...
351 12
                $report = TableRegistry::getTableLocator()->get('Reports')->get($report_id);
352 12
                $report->sourceforge_bug_id = $ticket_id;
0 ignored issues
show
Bug introduced by
Accessing sourceforge_bug_id on the interface Cake\Datasource\EntityInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
353 12
                TableRegistry::getTableLocator()->get('Reports')->save($report);
354
            }
355
356 12
            if ($msg !== '') {
357 12
                $flash_class = 'alert alert-success';
358 12
                $this->Flash->default(
359 12
                    $msg,
360 12
                    ['params' => ['class' => $flash_class]]
361
                );
362
            }
363
364 12
            return true;
365
        }
366
367 16
        if ($response === 403) {
368 4
            $flash_class = 'alert alert-error';
369 4
            $this->Flash->default(
370
                'Unauthorised access to Github. github'
371
                    . ' credentials may be out of date. Please check and try again'
372 4
                    . ' later.',
373 4
                ['params' => ['class' => $flash_class]]
374
            );
375
376 4
            return false;
377
        }
378
379 12
        if ($response === 404
380 12
            && $type === 2
381
        ) {
382 4
            $flash_class = 'alert alert-error';
383 4
            $this->Flash->default(
384
                'Bug Issue not found on Github.'
385 4
                    . ' Are you sure the issue number is correct? Please check and try again!',
386 4
                ['params' => ['class' => $flash_class]]
387
            );
388
389 4
            return false;
390
        }
391
392
        // unknown response code
393 8
        $flash_class = 'alert alert-error';
394 8
        $this->Flash->default(
395 8
            'Unhandled response code received: ' . $response,
396 8
            ['params' => ['class' => $flash_class]]
397
        );
398
399 8
        return false;
400
    }
401
402
    /**
403
     * Get Incident counts for a report and
404
     * all its related reports
405
     *
406
     * @param int $reportId The report Id
407
     *
408
     * @return int Total Incident count for a report
409
     */
410 8
    protected function getTotalIncidentCount(int $reportId): int
411
    {
412 8
        $incidents_query = TableRegistry::getTableLocator()->get('Incidents')->findByReportId($reportId)->all();
413 8
        $incident_count = $incidents_query->count();
414
415
        $params_count = [
416 8
            'fields' => ['inci_count' => 'inci_count'],
417
            'conditions' => [
418 8
                'related_to = ' . $reportId,
419
            ],
420
        ];
421
        $subquery_params_count = [
422 8
            'fields' => ['report_id' => 'report_id'],
423
        ];
424 8
        $subquery_count = TableRegistry::getTableLocator()->get('Incidents')->find(
425 8
            'all',
426 4
            $subquery_params_count
427
        );
428 8
        $inci_count_related = TableRegistry::getTableLocator()->get('Reports')->find('all', $params_count)->innerJoin(
429 8
            ['incidents' => $subquery_count],
430 8
            ['incidents.report_id = Reports.related_to']
431 8
        )->count();
432
433 8
        return $incident_count + $inci_count_related;
434
    }
435
436
    /**
437
     * Get corresponding report status from Github issue state
438
     *
439
     * @param string $issueState Linked Github issue's state
440
     *
441
     * @return string Corresponding status to which the linked report should be updated to
442
     */
443 16
    protected function getReportStatusFromIssueState(string $issueState): string
444
    {
445
        // default
446 16
        $reportStatus = '';
447 12
        switch ($issueState) {
448 16
            case 'closed':
449 12
                $reportStatus = 'resolved';
450 12
                break;
451
452
            default:
453 16
                $reportStatus = 'forwarded';
454 16
                break;
455
        }
456
457 16
        return $reportStatus;
458
    }
459
460
    /**
461
     * Synchronize Report Statuses from Github issues
462
     *
463
     * To be used as a cron job (using webroot/cron_dispatcher.php).
464
     *
465
     * Can not (& should not) be directly accessed via web.
466
     *
467
     * @return void Nothing
468
     */
469 8
    public function sync_issue_status(): void
470
    {
471 8
        if (! Configure::read('CronDispatcher')) {
472 4
            $flash_class = 'alert alert-error';
473 4
            $this->Flash->default(
474 4
                'Unauthorised action! This action is not available on Web interface',
475 4
                ['params' => ['class' => $flash_class]]
476
            );
477
478 4
            $this->redirect('/');
479
480 4
            return;
481
        }
482
483 8
        $this->autoRender = false;
484 8
        $reportsTable = TableRegistry::getTableLocator()->get('Reports');
485
486
        // Fetch all linked reports
487 8
        $reports = $reportsTable->find(
488 8
            'all',
489
            [
490 4
                'conditions' => [
491 4
                    'sourceforge_bug_id IS NOT NULL',
492
                    'NOT' => ['status' => 'resolved'],
493
                ],
494
            ]
495
        );
496
497 8
        foreach ($reports as $report) {
498 8
            $report = $report->toArray();
499
500
            // fetch the new issue status
501 8
            [$issueDetails, $status] = $this->GithubApi->getIssue(
0 ignored issues
show
Bug Best Practice introduced by
The property GithubApi does not exist on App\Controller\GithubController. Since you implemented __get, consider adding a @property annotation.
Loading history...
502 8
                Configure::read('GithubRepoPath'),
503 8
                [],
504 8
                $report['sourceforge_bug_id'],
505 8
                Configure::read('GithubAccessToken')
506
            );
507
508 8
            if (! $this->handleGithubResponse($status, 4, $report['id'], $report['sourceforge_bug_id'])) {
509 4
                Log::error(
510
                    'FAILED: Fetching status of Issue #'
511 4
                        . $report['sourceforge_bug_id']
512 4
                        . ' associated with Report#'
513 4
                        . $report['id']
514 4
                        . '. Status returned: ' . $status,
515 4
                    ['scope' => 'cron_jobs']
516
                );
517 4
                continue;
518
            }
519
520
            // if Github issue state has changed, update the status of report
521 8
            if ($report['status'] === $issueDetails['state']) {
522
                continue;
523
            }
524
525 8
            $rep = $reportsTable->get($report['id']);
526 8
            $rep->status = $this->getReportStatusFromIssueState($issueDetails['state']);
527
528
            // Save the report
529 8
            $reportsTable->save($rep);
530
531 8
            Log::debug(
532
                'SUCCESS: Updated status of Report #'
533 8
                . $report['id'] . ' from state of its linked Github issue #'
534 8
                . $report['sourceforge_bug_id'] . ' to ' . $rep->status,
535 8
                ['scope' => 'cron_jobs']
536
            );
537
        }
538 8
    }
539
}
540