Passed
Push — master ( 8072b8...acfa49 )
by William
04:14
created

IncidentsTable::createIncidentFromBugReport()   C

Complexity

Conditions 13
Paths 13

Size

Total Lines 80
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 43
CRAP Score 13.1912

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 13
eloc 53
c 3
b 0
f 0
nc 13
nop 1
dl 0
loc 80
ccs 43
cts 48
cp 0.8958
crap 13.1912
rs 6.6166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * An incident a representing a single incident of a submited bug.
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\Model\Table;
20
21
use Cake\Log\Log;
22
use Cake\Model\Model;
23
use Cake\ORM\Entity;
24
use Cake\ORM\Table;
25
use Cake\ORM\TableRegistry;
26
use function array_merge;
27
use function array_push;
28
use function array_slice;
29
use function count;
30
use function date;
31
use function hash_final;
32
use function hash_init;
33
use function hash_update;
34
use function in_array;
35
use function json_encode;
36
use function mb_strlen;
37
use function preg_match;
38
use function strtotime;
39
use function time;
40
41
/**
42
 * An incident a representing a single incident of a submited bug.
43
 */
44
class IncidentsTable extends Table
45
{
46
    /**
47
     * @var array
48
     *
49
     * @see http://book.cakephp.org/2.0/en/models/behaviors.html#using-behaviors
50
     * @see Model::$actsAs
51
     */
52
    public $actsAs = ['Summarizable'];
53
54
    /**
55
     * @var array
56
     *
57
     * @see http://book.cakephp.org/2.0/en/models/model-attributes.html#validate
58
     * @see http://book.cakephp.org/2.0/en/models/data-validation.html
59
     * @see Model::$validate
60
     */
61
    public $validate = [
62
        'pma_version' => [
63
            'rule' => 'notEmpty',
64
            'required' => true,
65
        ],
66
        'php_version' => [
67
            'rule' => 'notEmpty',
68
            'required' => true,
69
        ],
70
        'full_report' => [
71
            'rule' => 'notEmpty',
72
            'required' => true,
73
        ],
74
        'stacktrace' => [
75
            'rule' => 'notEmpty',
76
            'required' => true,
77
        ],
78
        'browser' => [
79
            'rule' => 'notEmpty',
80
            'required' => true,
81
        ],
82
        'stackhash' => [
83
            'rule' => 'notEmpty',
84
            'required' => true,
85
        ],
86
        'user_os' => [
87
            'rule' => 'notEmpty',
88
            'required' => true,
89
        ],
90
        'locale' => [
91
            'rule' => 'notEmpty',
92
            'required' => true,
93
        ],
94
        'script_name' => [
95
            'rule' => 'notEmpty',
96
            'required' => true,
97
        ],
98
        'server_software' => [
99
            'rule' => 'notEmpty',
100
            'required' => true,
101
        ],
102
        'configuration_storage' => [
103
            'rule' => 'notEmpty',
104
            'required' => true,
105
        ],
106
    ];
107
108
    /**
109
     * @var array
110
     *
111
     * @see http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#belongsto
112
     * @see Model::$belongsTo
113
     */
114
115
    /**
116
     * The fields which are summarized in the report page with charts and are also
117
     * used in the overall stats and charts for the website.
118
     *
119
     * @var array
120
     */
121
    public $summarizableFields = [
122
        'browser',
123
        'pma_version',
124
        'php_version',
125
        'locale',
126
        'server_software',
127
        'user_os',
128
        'script_name',
129
        'configuration_storage',
130
    ];
131
132
    /** @var array[] */
133
    public $filterTimes = [];
134
135 224
    public function __construct(array $data)
136
    {
137 224
        parent::__construct($data);
138
139 224
        $this->filterTimes = [
140 32
            'all_time' => [
141 192
                'label' => 'All Time',
142
                'limit' => null,
143
                'group' => "DATE_FORMAT(Incidents.created, '%m %Y')",
144
            ],
145
            'day' => [
146 224
                'label' => 'Last Day',
147 224
                'limit' => date('Y-m-d', strtotime('-1 day')),
148 224
                'group' => "DATE_FORMAT(Incidents.created, '%a %b %d %Y %H')",
149
            ],
150
            'week' => [
151 224
                'label' => 'Last Week',
152 224
                'limit' => date('Y-m-d', strtotime('-1 week')),
153 224
                'group' => "DATE_FORMAT(Incidents.created, '%a %b %d %Y')",
154
            ],
155
            'month' => [
156 224
                'label' => 'Last Month',
157 224
                'limit' => date('Y-m-d', strtotime('-1 month')),
158 224
                'group' => "DATE_FORMAT(Incidents.created, '%a %b %d %Y')",
159
            ],
160
            'year' => [
161 224
                'label' => 'Last Year',
162 224
                'limit' => date('Y-m-d', strtotime('-1 year')),
163 224
                'group' => "DATE_FORMAT(Incidents.created, '%b %u %Y')",
164
            ],
165
        ];
166 224
    }
167
168
    /**
169
     * creates an incident/report record given a raw bug report object.
170
     *
171
     * This gets a decoded bug report from the submitted json body. This has not
172
     * yet been santized. It either adds it as an incident to another report or
173
     * creates a new report if nothing matches.
174
     *
175
     * @param array|null $bugReport the bug report being submitted
176
     *
177
     * @return array of:
178
     *          1. array of inserted incident ids. If the report/incident was not
179
     *               correctly saved, false is put in it place.
180
     *          2. array of newly created report ids. If no new report was created,
181
     *               an empty array is returned
182
     */
183 14
    public function createIncidentFromBugReport(?array $bugReport): array
184
    {
185 14
        if ($bugReport === null) {
186
            return [
187 7
                'incidents' => [false],
188
                'reports' => [],
189
            ];
190
        }
191 14
        $incident_ids = [];    // array to hold ids of all the inserted incidents
192 14
        $new_report_ids = []; // array to hold ids of all newly created reports
193
194
        // Avoid storing too many errors from single report
195 14
        if (isset($bugReport['errors']) && count($bugReport['errors']) > 40) {
196 7
            $bugReport['errors'] = array_slice($bugReport['errors'], 0, 40);
197
        }
198 14
        if (isset($bugReport['exception']['stack']) && count($bugReport['exception']['stack']) > 40) {
199 7
            $bugReport['exception']['stack'] = array_slice($bugReport['exception']['stack'], 0, 40);
200
        }
201
202
        // Also sanitizes the bug report
203 14
        $schematizedIncidents = $this->getSchematizedIncidents($bugReport);
204 14
        $incidentsTable = TableRegistry::getTableLocator()->get('Incidents');
205 14
        $reportsTable = TableRegistry::getTableLocator()->get('Reports');
206 14
        foreach ($schematizedIncidents as $index => $si) {
207
            // find closest report. If not found, create a new report.
208 14
            $closestReport = $this->getClosestReport($bugReport, $index);
209 14
            if ($closestReport) {
210 14
                $si['report_id'] = $closestReport['id'];
211 14
                $si = $incidentsTable->newEntity($si);
212 14
                $si->created = date('Y-m-d H:i:s', time());
213 14
                $si->modified = date('Y-m-d H:i:s', time());
214
215 14
                $this->logLongIncidentSubmissions($si, $incident_ids);
216 14
                if (in_array(false, $incident_ids)) {
217 14
                    break;
218
                }
219
            } else {
220
                // no close report. Create a new report.
221 14
                $report = $this->getReportDetails($bugReport, $index);
222
223 14
                $this->logLongIncidentSubmissions($si, $incident_ids);
224 14
                if (in_array(false, $incident_ids)) {
225
                    break;
226
                }
227
228 14
                $report = $reportsTable->newEntity($report);
229 14
                $report->created = date('Y-m-d H:i:s', time());
230 14
                $report->modified = date('Y-m-d H:i:s', time());
231 14
                $reportsTable->save($report);
232
233 14
                $si['report_id'] = $report->id;
234 14
                $new_report_ids[] = $report->id;
235 14
                $si = $incidentsTable->newEntity($si);
236 14
                $si->created = date('Y-m-d H:i:s', time());
237 14
                $si->modified = date('Y-m-d H:i:s', time());
238
            }
239
240 14
            $isSaved = $incidentsTable->save($si);
241 14
            if ($isSaved) {
242 14
                array_push($incident_ids, $si->id);
243 14
                if (! $closestReport) {
244
                    // add notifications entry
245 14
                    $tmpIncident = $incidentsTable->findById($si->id)->all()->first();
246 14
                    if (! TableRegistry::getTableLocator()->get('Notifications')->addNotifications((int) $tmpIncident['report_id'])) {
247
                        Log::write(
248
                            'error',
249
                            'ERRORED: Notification::addNotifications() failed on Report#'
250
                                . $tmpIncident['report_id'],
251 14
                            'alert'
252
                        );
253
                    }
254
                }
255
            } else {
256
                array_push($incident_ids, false);
257
            }
258
        }
259
260
        return [
261 14
            'incidents' => $incident_ids,
262 14
            'reports' => $new_report_ids,
263
        ];
264
    }
265
266
    /**
267
     * retrieves the closest report to a given bug report.
268
     *
269
     * it checks for another report with the same line number, filename and
270
     * pma_version
271
     *
272
     * @param array $bugReport the bug report being checked
273
     *                         Integer $index: for php exception type
274
     * @param int   $index     The report index
275
     *
276
     * @return Entity|null the first similar report or null
277
     */
278 21
    protected function getClosestReport(array $bugReport, int $index = 0): ?Entity
279
    {
280 21
        if (isset($bugReport['exception_type'])
281 21
            && $bugReport['exception_type'] === 'php'
282
        ) {
283 14
            $location = $bugReport['errors'][$index]['file'];
284 14
            $linenumber = $bugReport['errors'][$index]['lineNum'];
285
        } else {
286 6
            [$location, $linenumber] =
287 21
                    $this->getIdentifyingLocation($bugReport['exception']['stack'] ?? []);
288
        }
289
290 21
        return TableRegistry::getTableLocator()->get('Reports')->findByLocationAndLinenumberAndPmaVersion(
291 21
            $location,
292 3
            $linenumber,
293 21
            $this->getStrippedPmaVersion($bugReport['pma_version'])
294 21
        )->all()->first();
295
    }
296
297
    /**
298
     * creates the report data from an incident that has no related report.
299
     *
300
     * @param array $bugReport the bug report the report record is being created for
301
     *                         Integer $index: for php exception type
302
     * @param int   $index     The report index
303
     *
304
     * @return array an array with the report fields can be used with Report->save
305
     */
306 21
    protected function getReportDetails(array $bugReport, int $index = 0): array
307
    {
308 21
        if (isset($bugReport['exception_type'])
309 21
            && $bugReport['exception_type'] === 'php'
310
        ) {
311 21
            $location = $bugReport['errors'][$index]['file'];
312 21
            $linenumber = $bugReport['errors'][$index]['lineNum'];
313 6
            $reportDetails = [
314 21
                'error_message' => $bugReport['errors'][$index]['msg'],
315 21
                'error_name' => $bugReport['errors'][$index]['type'],
316
            ];
317 21
            $exception_type = 1;
318
        } else {
319 21
            $exception = $bugReport['exception'] ?? [];
320 6
            [$location, $linenumber] =
321 21
                $this->getIdentifyingLocation($exception['stack'] ?? []);
322
323 6
            $reportDetails = [
324 21
                'error_message' => $exception['message'] ?? '',
325 21
                'error_name' => $exception['name'] ?? '',
326
            ];
327 21
            $exception_type = 0;
328
        }
329
330 21
        $reportDetails = array_merge(
331 21
            $reportDetails,
332
            [
333 21
                'status' => 'new',
334 21
                'location' => $location,
335 21
                'linenumber' => $linenumber ?? 0,
336 21
                'pma_version' => $this->getStrippedPmaVersion($bugReport['pma_version']),
337 21
                'exception_type' => $exception_type,
338
            ]
339
        );
340
341 21
        return $reportDetails;
342
    }
343
344
    /**
345
     * creates the incident data from the submitted bug report.
346
     *
347
     * @param array $bugReport the bug report the report record is being created for
348
     *
349
     * @return array an array of schematized incident.
350
     *               Can be used with Incident->save
351
     */
352 21
    protected function getSchematizedIncidents(array $bugReport): array
353
    {
354
        //$bugReport = Sanitize::clean($bugReport, array('escape' => false));
355 21
        $schematizedReports = [];
356 6
        $schematizedCommonReport = [
357 21
            'pma_version' => $this->getStrippedPmaVersion($bugReport['pma_version']),
358 21
            'php_version' => $this->getSimpleVersion($bugReport['php_version'] ?? '', 2),
359 21
            'browser' => ($bugReport['browser_name'] ?? '') . ' '
360 21
                    . $this->getSimpleVersion($bugReport['browser_version'] ?? '', 1),
361 21
            'user_os' => $bugReport['user_os'] ?? '',
362 21
            'locale' => $bugReport['locale'] ?? '',
363 21
            'configuration_storage' => $bugReport['configuration_storage'] ?? '',
364 21
            'server_software' => $this->getServer($bugReport['server_software'] ?? ''),
365 21
            'full_report' => json_encode($bugReport),
366
        ];
367
368 21
        if (isset($bugReport['exception_type'])
369 21
            && $bugReport['exception_type'] === 'php'
370
        ) {
371
            // for each "errors"
372 21
            foreach ($bugReport['errors'] as $error) {
373 21
                $tmpReport = array_merge(
374 21
                    $schematizedCommonReport,
375
                    [
376 21
                        'error_name' => $error['type'],
377 21
                        'error_message' => $error['msg'],
378 21
                        'script_name' => $error['file'],
379 21
                        'stacktrace' => json_encode($error['stackTrace'] ?? []),
380 21
                        'stackhash' => $error['stackhash'],
381 21
                        'exception_type' => 1,         // 'php'
382
                    ]
383
                );
384 21
                array_push($schematizedReports, $tmpReport);
385
            }
386
        } else {
387 21
            $exception = $bugReport['exception'] ?? [
388 15
                'name' => '',
389
                'message' => '',
390
                'stack' => [],
391
            ];
392 21
            $tmpReport = array_merge(
393 21
                $schematizedCommonReport,
394
                [
395 21
                    'error_name' => $exception['name'],
396 21
                    'error_message' => $exception['message'],
397 21
                    'script_name' => $bugReport['script_name'],
398 21
                    'stacktrace' => json_encode($exception['stack'] ?? []),
399 21
                    'stackhash' => $this->getStackHash($exception['stack'] ?? []),
400 21
                    'exception_type' => 0,     //'js'
401
                ]
402
            );
403
404 21
            if (isset($bugReport['steps'])) {
405 21
                $tmpReport['steps'] = $bugReport['steps'];
406
            }
407 21
            array_push($schematizedReports, $tmpReport);
408
        }
409
410 21
        return $schematizedReports;
411
    }
412
413
    /**
414
     * Gets the identifiying location info from a stacktrace.
415
     *
416
     * This is used to skip stacktrace levels that are within the error reporting js
417
     * files that sometimes appear in the stacktrace but are not related to the bug
418
     * report
419
     *
420
     * returns two things in an array:
421
     * - the first element is the filename/scriptname of the error
422
     * - the second element is the linenumber of the error
423
     *
424
     * @param array $stacktrace the stacktrace being examined
425
     *
426
     * @return array an array with the filename/scriptname and linenumber of the
427
     *               error
428
     */
429 35
    protected function getIdentifyingLocation(array $stacktrace): array
430
    {
431 10
        $fallback = [
432 25
            'UNKNOWN',
433
            0,
434
        ];
435 35
        foreach ($stacktrace as $level) {
436 35
            if (isset($level['filename'])) {
437
                // ignore unrelated files that sometimes appear in the error report
438 35
                if ($level['filename'] === 'tracekit/tracekit.js') {
439 7
                    continue;
440
                }
441
442 35
                if ($level['filename'] === 'error_report.js') {
443
                    // in case the error really is in the error_report.js file save it for
444
                    // later
445 7
                    if ($fallback[0] === 'UNKNOWN') {
446 2
                        $fallback = [
447 7
                            $level['filename'],
448 7
                            $level['line'],
449
                        ];
450
                    }
451 7
                    continue;
452
                }
453
454
                return [
455 35
                    $level['filename'],
456 35
                    $level['line'],
457
                ];
458
            }
459
460 7
            if (isset($level['scriptname'])) {
461
                return [
462 7
                    $level['scriptname'],
463 7
                    $level['line'],
464
                ];
465
            }
466 7
            continue;
467
        }
468
469 7
        return $fallback;
470
    }
471
472
    /**
473
     * Gets a part of a version string according to the specified version Length.
474
     *
475
     * @param string $versionString the version string
476
     * @param string $versionLength the number of version components to return. eg
477
     *                              1 for major version only and 2 for major and
478
     *                              minor version
479
     *
480
     * @return string the major and minor version part
481
     */
482 28
    protected function getSimpleVersion(string $versionString, string $versionLength): string
483
    {
484 28
        $versionLength = (int) $versionLength;
485 28
        if ($versionLength < 1) {
486 7
            $versionLength = 1;
487
        }
488
        /* modify the re to accept a variable number of version components. I
489
         * atleast take one component and optionally get more components if need be.
490
         * previous code makes sure that the $versionLength variable is a positive
491
         * int
492
         */
493 28
        $result = preg_match(
494 28
            '/^(\d+\.){' . ($versionLength - 1) . '}\d+/',
495 28
            $versionString,
496 28
            $matches
497
        );
498 28
        if ($result) {
499 28
            return $matches[0];
500
        }
501
502
        return $versionString;
503
    }
504
505
    /**
506
     * Returns the version string stripped of
507
     * 'deb', 'ubuntu' and other suffixes
508
     *
509
     * @param string $versionString phpMyAdmin version
510
     *
511
     * @return string stripped phpMyAdmin version
512
     */
513 84
    public function getStrippedPmaVersion(string $versionString): string
514
    {
515 84
        $allowedRegexp = '/^(\d+)(\.\d+){0,3}(\-.*)?/';
516 84
        $matches = [];
517
518
        // Check if $versionString matches the regexp
519
        // and store the matched strings
520 84
        if (preg_match($allowedRegexp, $versionString, $matches)) {
521 84
            return $matches[0];
522
        }
523
524
        // If $versionString does not match the regexp at all,
525
        // leave it as it is
526
        return $versionString;
527
    }
528
529
    /**
530
     * Gets the server name and version from the server signature.
531
     *
532
     * @param string $signature the server signature
533
     *
534
     * @return string the server name and version or UNKNOWN
535
     */
536 28
    protected function getServer(string $signature): string
537
    {
538 28
        if (preg_match(
539
            '/(apache\/\d+\.\d+)|(nginx\/\d+\.\d+)|(iis\/\d+\.\d+)'
540 28
                . '|(lighttpd\/\d+\.\d+)/i',
541 28
            $signature,
542 28
            $matches
543
        )) {
544 28
            return $matches[0];
545
        }
546
547 7
        return 'UNKNOWN';
548
    }
549
550
    /**
551
     * returns the hash pertaining to a stacktrace.
552
     *
553
     * @param array $stacktrace the stacktrace in question
554
     *
555
     * @return string the hash string of the stacktrace
556
     */
557 28
    public function getStackHash(array $stacktrace): string
558
    {
559 28
        $handle = hash_init('md5');
560 28
        foreach ($stacktrace as $level) {
561 8
            $elements = [
562 20
                'filename',
563
                'scriptname',
564
                'line',
565
                'func',
566
                'column',
567
            ];
568 28
            foreach ($elements as $element) {
569 28
                if (! isset($level[$element])) {
570 28
                    continue;
571
                }
572 28
                hash_update($handle, $level[$element]);
573
            }
574
        }
575
576 28
        return hash_final($handle);
577
    }
578
579
    /**
580
     * Checks the length of stacktrace and full_report
581
     * and logs if it is greater than what it can hold
582
     *
583
     * @param object $si           submitted incident
584
     * @param array  $incident_ids incident IDs
585
     * @return void Nothing
586
     */
587 14
    private function logLongIncidentSubmissions($si, array &$incident_ids): void// phpcs:ignore SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
588
    {
589 14
        $stacktraceLength = mb_strlen($si['stacktrace']);
590 14
        $fullReportLength = mb_strlen($si['full_report']);
591 14
        $errorMessageLength = mb_strlen($si['error_message']);
592
593 14
        if ($stacktraceLength <= 65535
594 14
            && $fullReportLength <= 65535
595 14
            && $errorMessageLength <= 200 // length of field in 'incidents' table
596
        ) {
597 14
            return;
598
        }
599
600
        // If length of report is longer than
601
        // what can fit in the table field,
602
        // we log it and don't save it in the database
603 7
        Log::error(
604
            'Too long data submitted in the incident. The length of stacktrace: '
605 7
            . $stacktraceLength . ', the length of bug report: '
606 7
            . $fullReportLength . ', the length of error message: '
607 7
            . $errorMessageLength . '. The full incident reported was as follows: '
608 7
            . json_encode($si)
609
        );
610
611
        // add a 'false' to the return array
612 7
        array_push($incident_ids, false);
613 7
    }
614
}
615