Completed
Push — master ( cc9938...9febb8 )
by Deven
05:58
created

GithubController::unlink_issue()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 49
Code Lines 32

Duplication

Lines 5
Ratio 10.2 %

Code Coverage

Tests 30
CRAP Score 6.0012

Importance

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