Completed
Pull Request — master (#164)
by Deven
09:43
created

GithubController::_getTotalIncidentCount()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 0
cts 24
cp 0
rs 8.8571
c 0
b 0
f 0
cc 1
eloc 17
nc 1
nop 1
crap 2
1
<?php
2
3
/* vim: set expandtab sw=4 ts=4 sts=4: */
4
/**
5
 * Sourceforge controller handling source forge ticket submission and creation.
6
 *
7
 * phpMyAdmin Error reporting server
8
 * Copyright (c) phpMyAdmin project (https://www.phpmyadmin.net/)
9
 *
10
 * Licensed under The MIT License
11
 * For full copyright and license information, please see the LICENSE.txt
12
 * Redistributions of files must retain the above copyright notice.
13
 *
14
 * @copyright Copyright (c) phpMyAdmin project (https://www.phpmyadmin.net/)
15
 * @license   https://opensource.org/licenses/mit-license.php MIT License
16
 *
17
 * @see      https://www.phpmyadmin.net/
18
 */
19
20
namespace App\Controller;
21
22
use Cake\Core\Configure;
23
use Cake\Event\Event;
24
use Cake\Log\Log;
25
use Cake\Network\Exception\NotFoundException;
26
use Cake\ORM\TableRegistry;
27
use Cake\Routing\Router;
28
29
/**
30
 * Github controller handling github issue submission and creation.
31
 */
32
class GithubController extends AppController
33
{
34
    public $helpers = array('Html', 'Form');
35
36
    public $components = array('GithubApi');
37
38
    public function beforeFilter(Event $event)
39
    {
40
        parent::beforeFilter($event);
41
        $this->GithubApi->githubConfig = Configure::read('GithubConfig');
42
        $this->GithubApi->githubRepo = Configure::read('GithubRepoPath');
43
    }
44
45
    /**
46
     * create Github Issue.
47
     *
48
     * @param int $reportId
49
     *
50
     * @throws \NotFoundException
51
     * @throws NotFoundException
52
     */
53
    public function create_issue($reportId)
54
    {
55
        if (!$reportId) {
56
            throw new \NotFoundException(__('Invalid report'));
57
        }
58
59
        $reportsTable = TableRegistry::get('Reports');
60
        $report = $reportsTable->findById($reportId)->all()->first();
61
        $reportArray = $report->toArray();
62
63
        if (!$report) {
64
            throw new NotFoundException(__('Invalid report'));
65
        }
66
67
        if (empty($this->request->data)) {
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Http\ServerRequest::$data has been deprecated with message: 3.4.0 This public property will be removed in 4.0.0. Use getData() instead.

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...
68
            $this->set('pma_version', $reportArray['pma_version']);
69
            $this->set('error_name', $reportArray['error_name']);
70
            $this->set('error_message', $reportArray['error_message']);
71
72
            return;
73
        }
74
        $data = array(
75
            'title' => $this->request->data['summary'],
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Http\ServerRequest::$data has been deprecated with message: 3.4.0 This public property will be removed in 4.0.0. Use getData() instead.

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...
76
            'labels' => $this->request->data['labels'] ? explode(',', $this->request->data['labels']) : array(),
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Http\ServerRequest::$data has been deprecated with message: 3.4.0 This public property will be removed in 4.0.0. Use getData() instead.

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...
77
        );
78
        $incidents_query = TableRegistry::get('Incidents')->findByReportId($reportId)->all();
79
        $incident = $incidents_query->first();
80
        $reportArray['exception_type'] = $incident['exception_type'] ? 'php' : 'js';
81
        $reportArray['description'] = $this->request->data['description'];
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Http\ServerRequest::$data has been deprecated with message: 3.4.0 This public property will be removed in 4.0.0. Use getData() instead.

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...
82
83
        $data['body']
84
            = $this->_getReportDescriptionText($reportId, $reportArray);
85
        $data['labels'][] = 'automated-error-report';
86
87
        list($issueDetails, $status) = $this->GithubApi->createIssue(
88
            Configure::read('GithubRepoPath'),
89
            $data,
90
            $this->request->session()->read('access_token')
91
        );
92
93
        if ($this->_handleGithubResponse($status, 1, $reportId, $issueDetails['number'])) {
94
            // Update report status
95
            $report->status = $this->_getReportStatusFromIssueState($issueDetails['state']);
96
            $reportsTable->save($report);
97
98
            $this->redirect(array('controller' => 'reports', 'action' => 'view',
99
                $reportId, ));
100 View Code Duplication
        } else {
101
            $flash_class = 'alert alert-error';
102
            $this->Flash->default(_getErrors($issueDetails, $status),
103
                array('params' => array('class' => $flash_class)));
104
        }
105
    }
106
107
    /**
108
     * Links error report to existing issue on Github.
109
     *
110
     * @param mixed $reportId
111
     */
112
    public function link_issue($reportId)
113
    {
114
        if (!$reportId) {
115
            throw new NotFoundException(__('Invalid reportId'));
116
        }
117
118
        $reportsTable = TableRegistry::get('Reports');
119
        $report = $reportsTable->findById($reportId)->all()->first();
120
        $reportArray = $report->toArray();
121
        if (!$report) {
122
            throw new NotFoundException(__('Invalid Report'));
123
        }
124
125
        $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 with message: 3.4.0 This public property will be removed in 4.0.0. Use getQuery() or getQueryParams() instead.

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...
126
        if (!$ticket_id) {
127
            throw new NotFoundException(__('Invalid Ticket ID!!'));
128
        }
129
130
        $incidents_query = TableRegistry::get('Incidents')->findByReportId($reportId)->all();
131
        $incident = $incidents_query->first();
132
        $reportArray['exception_type'] = $incident['exception_type'] ? 'php' : 'js';
133
134
        $commentText = $this->_getReportDescriptionText(
135
            $reportId, $reportArray
136
        );
137
        list($commentDetails, $status) = $this->GithubApi->createComment(
138
            Configure::read('GithubRepoPath'),
139
            array('body' => $commentText),
140
            $ticket_id,
141
            $this->request->session()->read('access_token')
142
        );
143
        if ($this->_handleGithubResponse($status, 2, $reportId, $ticket_id)) {
144
            // Update report status
145
            $report->status = 'forwarded';
146
147
            list($issueDetails, $status) = $this->GithubApi->getIssue(
148
                Configure::read('GithubRepoPath'),
149
                array(),
150
                $ticket_id,
151
                $this->request->session()->read('access_token')
152
            );
153
            if ($this->_handleGithubResponse($status, 4, $reportId, $ticket_id)) {
154
                // If linked Github issue state is available, use it to update Report's status
155
                $report->status = $this->_getReportStatusFromIssueState(
156
                    $issueDetails['state']
157
                );
158
            }
159
160
            $reportsTable->save($report);
161 View Code Duplication
        } else {
162
            $flash_class = 'alert alert-error';
163
            $this->Flash->default(_getErrors($commentDetails, $status),
164
                    array('params' => array('class' => $flash_class)));
165
        }
166
167
        $this->redirect(array('controller' => 'reports', 'action' => 'view',
168
                        $reportId, ));
169
    }
170
171
    /**
172
     * Un-links error report to associated issue on Github.
173
     *
174
     * @param mixed $reportId
175
     */
176
    public function unlink_issue($reportId)
177
    {
178
        if (!$reportId) {
179
            throw new NotFoundException(__('Invalid reportId'));
180
        }
181
182
        $reportsTable = TableRegistry::get('Reports');
183
        $report = $reportsTable->findById($reportId)->all()->first();
184
        $reportArray = $report->toArray();
185
186
        if (!$report) {
187
            throw new NotFoundException(__('Invalid Report'));
188
        }
189
190
        $ticket_id = $reportArray['sourceforge_bug_id'];
191
        if (!$ticket_id) {
192
            throw new NotFoundException(__('Invalid Ticket ID!!'));
193
        }
194
195
        // "formatted" text of the comment.
196
        $commentText = 'This Issue is no longer associated with [Report#'
197
            . $reportId
198
            . ']('
199
            . Router::url('/reports/view/' . $reportId, true)
200
            . ')'
201
            . "\n\n*This comment is posted automatically by phpMyAdmin's "
202
            . '[error-reporting-server](https://reports.phpmyadmin.net).*';
203
204
        list($commentDetails, $status) = $this->GithubApi->createComment(
205
            Configure::read('GithubRepoPath'),
206
            array('body' => $commentText),
207
            $ticket_id,
208
            $this->request->session()->read('access_token')
209
        );
210
211
        if ($this->_handleGithubResponse($status, 3, $reportId)) {
212
            // Update report status
213
            $report->status = 'new';
214
            $reportsTable->save($report);
215 View Code Duplication
        } else {
216
            $flash_class = 'alert alert-error';
217
            $this->Flash->default(_getErrors($commentDetails, $status),
218
                    array('params' => array('class' => $flash_class)));
219
        }
220
221
        $this->redirect(array('controller' => 'reports', 'action' => 'view',
222
                        $reportId, ));
223
    }
224
225
    /**
226
     * Returns pretty error message string.
227
     *
228
     * @param object $response the response returned by Github api
229
     * @param int    $status   status returned by Github API
230
     *
231
     * @return error string
232
     */
233
    protected function _getErrors($response, $status)
234
    {
235
        $errorString = 'There were some problems with the issue submission.'
236
            . ' Returned status is (' . $status . ')';
237
        $errorString .= '<br/> Here is the dump for the errors field provided by'
238
            . ' github: <br/>'
239
            . '<pre>'
240
            . print_r($response, true)
241
            . '</pre>';
242
243
        return $errorString;
244
    }
245
246
    /**
247
     * Returns the text to be added while creating an issue
248
     *
249
     * @param integer $reportId       Report Id
250
     * @param array   $report         Report associative array
251
     *
252
     * @return string
253
     */
254
    protected function _getReportDescriptionText($reportId, $report)
255
    {
256
        $incident_count = $this->_getTotalIncidentCount($reportId);
257
258
        // "formatted" text of the comment.
259
        $formattedText
260
            = array_key_exists('description', $report) ? $report['description'] . "\n\n"
261
                : '';
262
        $formattedText .= "\nParam | Value "
263
            . "\n -----------|--------------------"
264
            . "\n Error Type | " . $report['error_name']
265
            . "\n Error Message |" . $report['error_message']
266
            . "\n Exception Type |" . $report['exception_type']
267
            . "\n phpMyAdmin version |" . $report['pma_version']
268
            . "\n Incident count | " . $incident_count
269
            . "\n Link | [Report#"
270
                . $reportId
271
                . ']('
272
                . Router::url('/reports/view/' . $reportId, true)
273
                . ')'
274
            . "\n\n*This comment is posted automatically by phpMyAdmin's "
275
            . '[error-reporting-server](https://reports.phpmyadmin.net).*';
276
277
        return $formattedText;
278
    }
279
280
    /**
281
     * Github Response Handler.
282
     *
283
     * @param int $response  the status returned by Github API
284
     * @param int $type      type of response. 1 for create_issue, 2 for link_issue, 3 for unlink_issue,
285
     *                       1 for create_issue,
286
     *                       2 for link_issue,
287
     *                       3 for unlink_issue,
288
     *                       4 for get_issue
289
     * @param int $report_id report id
290
     * @param int $ticket_id ticket id, required for link ticket only
291
     *
292
     * @return bool value. True on success. False on any type of failure.
293
     */
294
    protected function _handleGithubResponse($response, $type, $report_id, $ticket_id = 1)
295
    {
296
        if (!in_array($type, array(1, 2, 3, 4))) {
297
            throw new InvalidArgumentException('Invalid Argument "$type".');
298
        }
299
300
        $updateReport = true;
301
302
        if ($type == 4 && $response == 200) {
303
            // issue details fetched successfully
304
            return true;
305
        } elseif ($response == 201) {
306
            // success
307
            switch ($type) {
308
                case 1:
309
                    $msg = 'Github issue has been created for this report.';
310
                    break;
311
                case 2:
312
                    $msg = 'Github issue has been linked with this report.';
313
                    break;
314
                case 3:
315
                    $msg = 'Github issue has been unlinked with this report.';
316
                    $ticket_id = null;
317
                    break;
318
319
                default:
320
                    $msg = 'Something went wrong!';
321
                    break;
322
            }
323
324
            if ($updateReport) {
325
                $report = TableRegistry::get('Reports')->get($report_id);
326
                $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?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
327
                TableRegistry::get('Reports')->save($report);
328
            }
329
330 View Code Duplication
            if ($msg !== '') {
331
                $flash_class = 'alert alert-success';
332
                $this->Flash->default($msg,
333
                    array('params' => array('class' => $flash_class)));
334
            }
335
336
            return true;
337
        } elseif ($response === 403) {
338
            $flash_class = 'alert alert-error';
339
            $this->Flash->default(
340
                    'Unauthorised access to Github. github'
341
                    . ' credentials may be out of date. Please check and try again'
342
                    . ' later.',
343
                    array('params' => array('class' => $flash_class)));
344
345
            return false;
346
        } elseif ($response === 404
347
            && $type == 2
348
        ) {
349
            $flash_class = 'alert alert-error';
350
            $this->Flash->default(
351
                    'Bug Issue not found on Github.'
352
                    . ' Are you sure the issue number is correct? Please check and try again!',
353
                     array('params' => array('class' => $flash_class)));
354
355
            return false;
356
        }
357
358
        // unknown response code
359
        $flash_class = 'alert alert-error';
360
        $this->Flash->default('Unhandled response code recieved: ' . $response,
361
                    array('params' => array('class' => $flash_class)));
362
363
        return false;
364
    }
365
366
    /**
367
     * Get Incident counts for a report and
368
     * all its related reports
369
     *
370
     * @param $reportId Report ID
371
     *
372
     * @return $total_incident_count Total Incident count for a report
0 ignored issues
show
Documentation introduced by
The doc-type $total_incident_count could not be parsed: Unknown type name "$total_incident_count" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
373
     */
374
    protected function _getTotalIncidentCount($reportId)
375
    {
376
        $incidents_query = TableRegistry::get('Incidents')->findByReportId($reportId)->all();
377
        $incident_count = $incidents_query->count();
378
379
        $params_count = array(
380
            'fields' => array('inci_count' => 'inci_count'),
381
            'conditions' => array(
382
                    'related_to = ' . $reportId,
383
            ),
384
        );
385
        $subquery_params_count = array(
386
            'fields' => array(
387
                'report_id' => 'report_id',
388
            ),
389
        );
390
        $subquery_count = TableRegistry::get('Incidents')->find(
391
            'all', $subquery_params_count
392
        );
393
        $inci_count_related = TableRegistry::get('Reports')->find('all', $params_count)->innerJoin(
394
                array('incidents' => $subquery_count),
395
                array('incidents.report_id = Reports.related_to')
396
            )->count();
397
398
        return $incident_count + $inci_count_related;
399
    }
400
401
    /**
402
     * Get corresponding report status from Github issue state
403
     *
404
     * @param $issueState Linked Github issue's state
405
     *
406
     * @return Corresponding status to which the linked report should be updated to
407
     */
408
    protected function _getReportStatusFromIssueState($issueState)
409
    {
410
        // default
411
        $reportStatus = '';
0 ignored issues
show
Unused Code introduced by
$reportStatus is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
412
        switch ($issueState) {
413
            case 'closed':
414
                $reportStatus = 'resolved';
415
                break;
416
417
            default:
418
                $reportStatus = 'forwarded';
419
                break;
420
        }
421
422
        return $reportStatus;
423
    }
424
425
    /**
426
     * Synchronize Report Statuses from Github issues
427
     *
428
     * To be used as a cron job (using webroot/cron_dispatcher.php).
429
     *
430
     * Can not (& should not) be directly accessed via web.
431
     */
432
    public function sync_issue_status()
433
    {
434
435
        if (!defined('CRON_DISPATCHER')) {
436
            $flash_class = 'alert alert-error';
437
            $this->Flash->default(
438
                'Unauthorised action! This action is not available on Web interface',
439
                array('params' => array('class' => $flash_class))
440
            );
441
442
            $this->redirect('/');
443
        }
444
445
        $this->autoRender = false;
446
        $reportsTable = TableRegistry::get('Reports');
447
448
        // Fetch all linked reports
449
        $reports = $reportsTable->find(
450
            'all',
451
            array(
452
                'conditions' => array(
453
                    'sourceforge_bug_id IS NOT NULL'
454
                )
455
            )
456
        );
457
458
        foreach ($reports as $report) {
459
            $report = $report->toArray();
460
461
            // fetch the new issue status
462
            list($issueDetails, $status) = $this->GithubApi->getIssue(
463
                Configure::read('GithubRepoPath'),
464
                array(),
465
                $report['sourceforge_bug_id'],
466
                Configure::read('GithubAccessToken')
467
            );
468
469
            if (!$this->_handleGithubResponse($status, 4, $report['id'], $report['sourceforge_bug_id'])) {
470
                Log::error(
471
                    'FAILED: Fetching status of Issue #'
472
                        . ($report['sourceforge_bug_id'])
473
                        . ' associated with Report#'
474
                        . ($report['id'])
475
                        . '. Status returned: ' . $status,
476
                    ['scope' => 'cron_jobs']
477
                );
478
                continue;
479
            }
480
481
            // if Github issue state has changed, update the status of report
482
            if ($report['status'] !== $issueDetails['state']) {
483
                $rep = $reportsTable->get($report['id']);
484
                $rep->status = $this->_getReportStatusFromIssueState($issueDetails['state']);
0 ignored issues
show
Bug introduced by
Accessing status on the interface Cake\Datasource\EntityInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
485
486
                // Save the report
487
                $reportsTable->save($rep);
488
489
                Log::debug(
490
                    'SUCCESS: Updated status of Report #'
491
                    . $report['id'] . ' from state of its linked Github issue #'
492
                    . $report['sourceforge_bug_id'] . ' to ' . $rep->status,
0 ignored issues
show
Bug introduced by
Accessing status on the interface Cake\Datasource\EntityInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
493
                    ['scope' => 'cron_jobs']
494
                );
495
            }
496
        }
497
    }
498
}
499