Completed
Push — master ( 89830f...07e3a8 )
by William
20:06
created

GithubController::getTotalIncidentCount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 16
nc 1
nop 1
dl 0
loc 24
ccs 11
cts 11
cp 1
crap 1
rs 9.7333
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 20
    public $helpers = [
42
        'Html',
43 20
        'Form',
44 20
    ];
45 20
    /** @var string */
46 20
    public $components = ['GithubApi'];
47
48
    public function beforeFilter(Event $event): void
49
    {
50
        parent::beforeFilter($event);
51
        $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
        $this->GithubApi->githubRepo = Configure::read('GithubRepoPath');
53
    }
54
55
    /**
56 4
     * create Github Issue.
57
     *
58 4
     * @param int $reportId The report number
59
     *
60
     * @throws NotFoundException
61
     * @return void Nothing
62 4
     */
63 4
    public function create_issue(int $reportId): void
64
    {
65 4
        if (! isset($reportId) || ! $reportId) {
66 4
            throw new NotFoundException(__('Invalid report'));
67
        }
68
69 4
        $reportsTable = TableRegistry::getTableLocator()->get('Reports');
70 4
        $report = $reportsTable->findById($reportId)->all()->first();
71 4
72 4
        if (! $report) {
73
            throw new NotFoundException(__('Invalid report'));
74 4
        }
75
76
        $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
            $this->set('error_name', $reportArray['error_name']);
79 4
            $this->set('error_message', $reportArray['error_message']);
80 4
81
            return;
82 4
        }
83 4
84 4
        $this->autoRender = false;
85 4
        $data = [
86
            '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 4
        ];
89 4
        $incidents_query = TableRegistry::getTableLocator()->get('Incidents')->findByReportId($reportId)->all();
90
        $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 2
94 4
        $data['body']
95
            = $this->getReportDescriptionText($reportId, $reportArray);
96
        $data['labels'][] = 'automated-error-report';
97 4
98
        [$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 4
            $data,
101
            $this->request->getSession()->read('access_token')
102 4
        );
103 4
104
        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 4
109 4
            $this->redirect(['controller' => 'reports', 'action' => 'view',
110
                $reportId,
111
            ]);
112 4
        } else {
113
            $flash_class = 'alert alert-error';
114
            $this->Flash->default(
115
                $this->getErrors($issueDetails, $status),
116
                ['params' => ['class' => $flash_class]]
117
            );
118
        }
119
    }
120 4
121
    /**
122 4
     * Links error report to existing issue on Github.
123
     *
124
     * @param int $reportId The report Id
125
     * @return void Nothing
126 4
     */
127 4
    public function link_issue(int $reportId): void
128
    {
129 4
        if (! isset($reportId) || ! $reportId) {
130 4
            throw new NotFoundException(__('Invalid reportId'));
131
        }
132
133 4
        $reportsTable = TableRegistry::getTableLocator()->get('Reports');
134 4
        $report = $reportsTable->findById($reportId)->all()->first();
135 4
136
        if (! $report) {
137 4
            throw new NotFoundException(__('Invalid report'));
138
        }
139 4
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
            throw new NotFoundException(__('Invalid Ticket ID!!'));
143 4
        }
144 4
        $reportArray = $report->toArray();
145 2
146
        $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 4
150 2
        $commentText = $this->getReportDescriptionText(
151 4
            $reportId,
152
            $reportArray
153 4
        );
154
        [$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
            ['body' => $commentText],
157 4
            $ticket_id,
158 4
            $this->request->getSession()->read('access_token')
159 4
        );
160 2
        if ($this->handleGithubResponse($status, 2, $reportId, $ticket_id)) {
161 4
            // Update report status
162
            $report->status = 'forwarded';
163 4
164
            [$issueDetails, $status] = $this->GithubApi->getIssue(
165 4
                Configure::read('GithubRepoPath'),
166 4
                [],
167
                $ticket_id,
168
                $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 4
                );
175 4
            }
176
177
            $reportsTable->save($report);
178
        } else {
179 4
            $flash_class = 'alert alert-error';
180 4
            $this->Flash->default(
181
                $this->getErrors($commentDetails, $status),
182 4
                ['params' => ['class' => $flash_class]]
183
            );
184
        }
185
186
        $this->redirect(['controller' => 'reports', 'action' => 'view',
187
            $reportId,
188
        ]);
189
    }
190 4
191
    /**
192 4
     * Un-links error report to associated issue on Github.
193
     *
194
     * @param int $reportId The report Id
195
     * @return void Nothing
196 4
     */
197 4
    public function unlink_issue(int $reportId): void
198
    {
199 4
        if (! isset($reportId) || ! $reportId) {
200 4
            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
        $reportArray = $report->toArray();
211
        $ticket_id = $reportArray['sourceforge_bug_id'];
212 4
213 4
        if (! $ticket_id) {
214 4
            throw new NotFoundException(__('Invalid Ticket ID!!'));
215 4
        }
216 4
217 4
        // "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 2
            . ')'
223 4
            . "\n\n*This comment is posted automatically by phpMyAdmin's "
224
            . '[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
            Configure::read('GithubRepoPath'),
228 4
            ['body' => $commentText],
229 4
            $ticket_id,
230
            $this->request->getSession()->read('access_token')
231 4
        );
232 4
233 4
        if ($this->handleGithubResponse($status, 3, $reportId)) {
234 4
            // Update report status
235
            $report->status = 'new';
236
            $reportsTable->save($report);
237
        } else {
238 4
            $flash_class = 'alert alert-error';
239 4
            $this->Flash->default(
240
                $this->getErrors($commentDetails, $status),
241 4
                ['params' => ['class' => $flash_class]]
242
            );
243
        }
244
245
        $this->redirect(['controller' => 'reports', 'action' => 'view',
246
            $reportId,
247
        ]);
248
    }
249
250
    /**
251 12
     * Returns pretty error message string.
252
     *
253
     * @param object|array $response the response returned by Github api
254 12
     * @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 12
    {
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
            . print_r($response, true)
266
            . '</pre>';
267
268
        return $errorString;
269
    }
270
271
    /**
272 8
     * Returns the text to be added while creating an issue
273
     *
274 8
     * @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 8
    {
280
        $incident_count = $this->getTotalIncidentCount($reportId);
281
282 8
        // "formatted" text of the comment.
283 8
        $formattedText
284 8
            = array_key_exists('description', $report) ? $report['description'] . "\n\n"
285 8
                : '';
286 8
        $formattedText .= "\nParam | Value "
287 8
            . "\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
                . $reportId
295 8
                . ']('
296
                . Router::url('/reports/view/' . $reportId, true)
297
                . ')'
298
            . "\n\n*This comment is posted automatically by phpMyAdmin's "
299
            . '[error-reporting-server](https://reports.phpmyadmin.net).*';
300
301
        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 20
     *                       4 for get_issue
313
     * @param int $report_id report id
314 20
     * @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 12
        }
323 16
324
        $updateReport = true;
325
326 12
        if ($type === 4 && $response === 200) {
327 4
            // issue details fetched successfully
328 4
            return true;
329 8
        }
330 4
331 4
        if ($response === 201) {
332 4
            // success
333 4
            switch ($type) {
334 4
                case 1:
335 4
                    $msg = 'Github issue has been created for this report.';
336
                    break;
337
                case 2:
338
                    $msg = 'Github issue has been linked with this report.';
339
                    break;
340
                case 3:
341
                    $msg = 'Github issue has been unlinked with this report.';
342 12
                    $ticket_id = null;
343 12
                    break;
344 12
345 12
                default:
346
                    $msg = 'Something went wrong!';
347
                    break;
348 12
            }
349 12
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
                TableRegistry::getTableLocator()->get('Reports')->save($report);
354
            }
355
356 12
            if ($msg !== '') {
357 16
                $flash_class = 'alert alert-success';
358 4
                $this->Flash->default(
359 4
                    $msg,
360
                    ['params' => ['class' => $flash_class]]
361
                );
362 4
            }
363 4
364
            return true;
365
        }
366 4
367 12
        if ($response === 403) {
368 12
            $flash_class = 'alert alert-error';
369
            $this->Flash->default(
370 4
                'Unauthorised access to Github. github'
371 4
                    . ' credentials may be out of date. Please check and try again'
372
                    . ' later.',
373 4
                ['params' => ['class' => $flash_class]]
374 4
            );
375
376
            return false;
377 4
        }
378
379
        if ($response === 404
380
            && $type === 2
381 8
        ) {
382 8
            $flash_class = 'alert alert-error';
383 8
            $this->Flash->default(
384 8
                'Bug Issue not found on Github.'
385
                    . ' Are you sure the issue number is correct? Please check and try again!',
386
                ['params' => ['class' => $flash_class]]
387 8
            );
388
389
            return false;
390
        }
391
392
        // unknown response code
393
        $flash_class = 'alert alert-error';
394
        $this->Flash->default(
395
            'Unhandled response code received: ' . $response,
396
            ['params' => ['class' => $flash_class]]
397
        );
398 8
399
        return false;
400 8
    }
401 8
402
    /**
403
     * Get Incident counts for a report and
404 8
     * all its related reports
405
     *
406 8
     * @param int $reportId The report Id
407
     *
408
     * @return int Total Incident count for a report
409
     */
410 4
    protected function getTotalIncidentCount(int $reportId): int
411 4
    {
412
        $incidents_query = TableRegistry::getTableLocator()->get('Incidents')->findByReportId($reportId)->all();
413
        $incident_count = $incidents_query->count();
414 8
415 8
        $params_count = [
416 4
            'fields' => ['inci_count' => 'inci_count'],
417
            'conditions' => [
418 8
                'related_to = ' . $reportId,
419 8
            ],
420 8
        ];
421 8
        $subquery_params_count = [
422
            'fields' => ['report_id' => 'report_id'],
423 8
        ];
424
        $subquery_count = TableRegistry::getTableLocator()->get('Incidents')->find(
425
            'all',
426
            $subquery_params_count
427
        );
428
        $inci_count_related = TableRegistry::getTableLocator()->get('Reports')->find('all', $params_count)->innerJoin(
429
            ['incidents' => $subquery_count],
430
            ['incidents.report_id = Reports.related_to']
431
        )->count();
432
433 16
        return $incident_count + $inci_count_related;
434
    }
435
436 16
    /**
437 12
     * Get corresponding report status from Github issue state
438 16
     *
439 12
     * @param string $issueState Linked Github issue's state
440 12
     *
441
     * @return string Corresponding status to which the linked report should be updated to
442
     */
443 16
    protected function getReportStatusFromIssueState(string $issueState): string
444 16
    {
445
        // default
446
        $reportStatus = '';
447 16
        switch ($issueState) {
448
            case 'closed':
449
                $reportStatus = 'resolved';
450
                break;
451
452
            default:
453
                $reportStatus = 'forwarded';
454
                break;
455
        }
456
457
        return $reportStatus;
458 8
    }
459
460 8
    /**
461 4
     * Synchronize Report Statuses from Github issues
462 4
     *
463 4
     * To be used as a cron job (using webroot/cron_dispatcher.php).
464 4
     *
465
     * Can not (& should not) be directly accessed via web.
466
     *
467 4
     * @return void Nothing
468 4
     */
469
    public function sync_issue_status(): void
470
    {
471 8
        if (! Configure::read('CronDispatcher')) {
472 8
            $flash_class = 'alert alert-error';
473
            $this->Flash->default(
474
                'Unauthorised action! This action is not available on Web interface',
475 8
                ['params' => ['class' => $flash_class]]
476 8
            );
477
478 4
            $this->redirect('/');
479 4
480
            return;
481
        }
482
483
        $this->autoRender = false;
484
        $reportsTable = TableRegistry::getTableLocator()->get('Reports');
485
486
        // Fetch all linked reports
487 8
        $reports = $reportsTable->find(
488 8
            'all',
489
            [
490
                'conditions' => [
491 8
                    'sourceforge_bug_id IS NOT NULL',
492 8
                    'NOT' => ['status' => 'resolved'],
493 8
                ],
494 8
            ]
495 8
        );
496
497
        foreach ($reports as $report) {
498 8
            $report = $report->toArray();
499 4
500
            // fetch the new issue status
501 4
            [$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 4
                Configure::read('GithubRepoPath'),
503 4
                [],
504 4
                $report['sourceforge_bug_id'],
505 4
                Configure::read('GithubAccessToken')
506
            );
507 4
508
            if (! $this->handleGithubResponse($status, 4, $report['id'], $report['sourceforge_bug_id'])) {
509
                Log::error(
510
                    'FAILED: Fetching status of Issue #'
511 8
                        . $report['sourceforge_bug_id']
512 8
                        . ' associated with Report#'
513 8
                        . $report['id']
514
                        . '. Status returned: ' . $status,
515
                    ['scope' => 'cron_jobs']
516 8
                );
517
                continue;
518 8
            }
519
520 8
            // if Github issue state has changed, update the status of report
521 8
            if ($report['status'] === $issueDetails['state']) {
522 8
                continue;
523
            }
524
525
            $rep = $reportsTable->get($report['id']);
526 8
            $rep->status = $this->getReportStatusFromIssueState($issueDetails['state']);
527
528
            // Save the report
529
            $reportsTable->save($rep);
530
531
            Log::debug(
532
                'SUCCESS: Updated status of Report #'
533
                . $report['id'] . ' from state of its linked Github issue #'
534
                . $report['sourceforge_bug_id'] . ' to ' . $rep->status,
535
                ['scope' => 'cron_jobs']
536
            );
537
        }
538
    }
539
}
540