Passed
Push — master ( 4bc08b...df48b1 )
by Maurício
08:59
created

ErrorHandler::__destruct()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 25
rs 9.2222
c 0
b 0
f 0
cc 6
nc 8
nop 0
1
<?php
2
/* vim: set expandtab sw=4 ts=4 sts=4: */
3
/**
4
 * Holds class PhpMyAdmin\ErrorHandler
5
 *
6
 * @package PhpMyAdmin
7
 */
8
declare(strict_types=1);
9
10
namespace PhpMyAdmin;
11
12
use PhpMyAdmin\Error;
13
use PhpMyAdmin\Response;
14
use PhpMyAdmin\Url;
15
16
/**
17
 * handling errors
18
 *
19
 * @package PhpMyAdmin
20
 */
21
class ErrorHandler
22
{
23
    /**
24
     * holds errors to be displayed or reported later ...
25
     *
26
     * @var Error[]
27
     */
28
    protected $errors = [];
29
30
    /**
31
     * Hide location of errors
32
     */
33
    protected $hide_location = false;
34
35
    /**
36
     * Initial error reporting state
37
     */
38
    protected $error_reporting = 0;
39
40
    /**
41
     * Constructor - set PHP error handler
42
     *
43
     */
44
    public function __construct()
45
    {
46
        /**
47
         * Do not set ourselves as error handler in case of testsuite.
48
         *
49
         * This behavior is not tested there and breaks other tests as they
50
         * rely on PHPUnit doing it's own error handling which we break here.
51
         */
52
        if (! defined('TESTSUITE')) {
53
            set_error_handler([$this, 'handleError']);
54
        }
55
        $this->error_reporting = error_reporting();
56
    }
57
58
    /**
59
     * Destructor
60
     *
61
     * stores errors in session
62
     *
63
     */
64
    public function __destruct()
65
    {
66
        if (! isset($_SESSION['errors'])) {
67
            $_SESSION['errors'] = [];
68
        }
69
70
        // remember only not displayed errors
71
        foreach ($this->errors as $key => $error) {
72
            /**
73
             * We don't want to store all errors here as it would
74
             * explode user session.
75
             */
76
            if (count($_SESSION['errors']) >= 10) {
77
                $error = new Error(
78
                    0,
79
                    __('Too many error messages, some are not displayed.'),
80
                    __FILE__,
81
                    __LINE__
82
                );
83
                $_SESSION['errors'][$error->getHash()] = $error;
84
                break;
85
            } elseif (($error instanceof Error)
86
                && ! $error->isDisplayed()
87
            ) {
88
                $_SESSION['errors'][$key] = $error;
89
            }
90
        }
91
    }
92
93
    /**
94
     * Toggles location hiding
95
     *
96
     * @param boolean $hide Whether to hide
97
     *
98
     * @return void
99
     */
100
    public function setHideLocation(bool $hide): void
101
    {
102
        $this->hide_location = $hide;
103
    }
104
105
    /**
106
     * returns array with all errors
107
     *
108
     * @param bool $check Whether to check for session errors
109
     *
110
     * @return Error[]
111
     */
112
    public function getErrors(bool $check = true): array
113
    {
114
        if ($check) {
115
            $this->checkSavedErrors();
116
        }
117
        return $this->errors;
118
    }
119
120
    /**
121
     * returns the errors occurred in the current run only.
122
     * Does not include the errors saved in the SESSION
123
     *
124
     * @return Error[]
125
     */
126
    public function getCurrentErrors(): array
127
    {
128
        return $this->errors;
129
    }
130
131
    /**
132
     * Pops recent errors from the storage
133
     *
134
     * @param int $count Old error count
135
     *
136
     * @return Error[]
137
     */
138
    public function sliceErrors(int $count): array
139
    {
140
        $errors = $this->getErrors(false);
141
        $this->errors = array_splice($errors, 0, $count);
142
        return array_splice($errors, $count);
143
    }
144
145
    /**
146
     * Error handler - called when errors are triggered/occurred
147
     *
148
     * This calls the addError() function, escaping the error string
149
     * Ignores the errors wherever Error Control Operator (@) is used.
150
     *
151
     * @param integer $errno   error number
152
     * @param string  $errstr  error string
153
     * @param string  $errfile error file
154
     * @param integer $errline error line
155
     *
156
     * @return void
157
     */
158
    public function handleError(
159
        int $errno,
160
        string $errstr,
161
        string $errfile,
162
        int $errline
163
    ): void {
164
        /**
165
         * Check if Error Control Operator (@) was used, but still show
166
         * user errors even in this case.
167
         */
168
        if (error_reporting() == 0 &&
169
            $this->error_reporting != 0 &&
170
            ($errno & (E_USER_WARNING | E_USER_ERROR | E_USER_NOTICE)) == 0
171
        ) {
172
            return;
173
        }
174
        $this->addError($errstr, $errno, $errfile, $errline, true);
175
    }
176
177
    /**
178
     * Add an error; can also be called directly (with or without escaping)
179
     *
180
     * The following error types cannot be handled with a user defined function:
181
     * E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR,
182
     * E_COMPILE_WARNING,
183
     * and most of E_STRICT raised in the file where set_error_handler() is called.
184
     *
185
     * Do not use the context parameter as we want to avoid storing the
186
     * complete $GLOBALS inside $_SESSION['errors']
187
     *
188
     * @param string  $errstr  error string
189
     * @param integer $errno   error number
190
     * @param string  $errfile error file
191
     * @param integer $errline error line
192
     * @param boolean $escape  whether to escape the error string
193
     *
194
     * @return void
195
     */
196
    public function addError(
197
        string $errstr,
198
        int $errno,
199
        string $errfile,
200
        int $errline,
201
        bool $escape = true
202
    ): void {
203
        if ($escape) {
204
            $errstr = htmlspecialchars($errstr);
205
        }
206
        // create error object
207
        $error = new Error(
208
            $errno,
209
            $errstr,
210
            $errfile,
211
            $errline
212
        );
213
        $error->setHideLocation($this->hide_location);
214
215
        // do not repeat errors
216
        $this->errors[$error->getHash()] = $error;
217
218
        switch ($error->getNumber()) {
219
            case E_STRICT:
220
            case E_DEPRECATED:
221
            case E_NOTICE:
222
            case E_WARNING:
223
            case E_CORE_WARNING:
224
            case E_COMPILE_WARNING:
225
            case E_RECOVERABLE_ERROR:
226
                /* Avoid rendering BB code in PHP errors */
227
                $error->setBBCode(false);
228
                break;
229
            case E_USER_NOTICE:
230
            case E_USER_WARNING:
231
            case E_USER_ERROR:
232
                // just collect the error
233
                // display is called from outside
234
                break;
235
            case E_ERROR:
236
            case E_PARSE:
237
            case E_CORE_ERROR:
238
            case E_COMPILE_ERROR:
239
            default:
240
                // FATAL error, display it and exit
241
                $this->dispFatalError($error);
242
                exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
243
        }
244
    }
245
246
    /**
247
     * trigger a custom error
248
     *
249
     * @param string  $errorInfo   error message
250
     * @param integer $errorNumber error number
251
     *
252
     * @return void
253
     */
254
    public function triggerError(string $errorInfo, ?int $errorNumber = null): void
255
    {
256
        // we could also extract file and line from backtrace
257
        // and call handleError() directly
258
        trigger_error($errorInfo, $errorNumber);
259
    }
260
261
    /**
262
     * display fatal error and exit
263
     *
264
     * @param Error $error the error
265
     *
266
     * @return void
267
     */
268
    protected function dispFatalError(Error $error): void
269
    {
270
        if (! headers_sent()) {
271
            $this->dispPageStart($error);
272
        }
273
        $error->display();
274
        $this->dispPageEnd();
275
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
276
    }
277
278
    /**
279
     * Displays user errors not displayed
280
     *
281
     * @return void
282
     */
283
    public function dispUserErrors(): void
284
    {
285
        echo $this->getDispUserErrors();
286
    }
287
288
    /**
289
     * Renders user errors not displayed
290
     *
291
     * @return string
292
     */
293
    public function getDispUserErrors(): string
294
    {
295
        $retval = '';
296
        foreach ($this->getErrors() as $error) {
297
            if ($error->isUserError() && ! $error->isDisplayed()) {
298
                $retval .= $error->getDisplay();
299
            }
300
        }
301
        return $retval;
302
    }
303
304
    /**
305
     * display HTML header
306
     *
307
     * @param Error $error the error
308
     *
309
     * @return void
310
     */
311
    protected function dispPageStart(?Error $error = null): void
312
    {
313
        Response::getInstance()->disable();
314
        echo '<html><head><title>';
315
        if ($error) {
316
            echo $error->getTitle();
317
        } else {
318
            echo 'phpMyAdmin error reporting page';
319
        }
320
        echo '</title></head>';
321
    }
322
323
    /**
324
     * display HTML footer
325
     *
326
     * @return void
327
     */
328
    protected function dispPageEnd(): void
329
    {
330
        echo '</body></html>';
331
    }
332
333
    /**
334
     * renders errors not displayed
335
     *
336
     * @return string
337
     */
338
    public function getDispErrors(): string
339
    {
340
        $retval = '';
341
        // display errors if SendErrorReports is set to 'ask'.
342
        if ($GLOBALS['cfg']['SendErrorReports'] != 'never') {
343
            foreach ($this->getErrors() as $error) {
344
                if (! $error->isDisplayed()) {
345
                    $retval .= $error->getDisplay();
346
                }
347
            }
348
        } else {
349
            $retval .= $this->getDispUserErrors();
350
        }
351
        // if preference is not 'never' and
352
        // there are 'actual' errors to be reported
353
        if ($GLOBALS['cfg']['SendErrorReports'] != 'never'
354
            &&  $this->countErrors() !=  $this->countUserErrors()
355
        ) {
356
            // add report button.
357
            $retval .= '<form method="post" action="error_report.php"'
358
                    . ' id="pma_report_errors_form"';
359
            if ($GLOBALS['cfg']['SendErrorReports'] == 'always') {
360
                // in case of 'always', generate 'invisible' form.
361
                $retval .= ' class="hide"';
362
            }
363
            $retval .=  '>';
364
            $retval .= Url::getHiddenFields([
365
                'exception_type' => 'php',
366
                'send_error_report' => '1',
367
                'server' => $GLOBALS['server'],
368
            ]);
369
            $retval .= '<input type="submit" value="'
370
                    . __('Report')
371
                    . '" id="pma_report_errors" class="btn btn-primary floatright">'
372
                    . '<input type="checkbox" name="always_send"'
373
                    . ' id="always_send_checkbox" value="true">'
374
                    . '<label for="always_send_checkbox">'
375
                    . __('Automatically send report next time')
376
                    . '</label>';
377
378
            if ($GLOBALS['cfg']['SendErrorReports'] == 'ask') {
379
                // add ignore buttons
380
                $retval .= '<input type="submit" value="'
381
                        . __('Ignore')
382
                        . '" id="pma_ignore_errors_bottom" class="btn btn-secondary floatright">';
383
            }
384
            $retval .= '<input type="submit" value="'
385
                    . __('Ignore All')
386
                    . '" id="pma_ignore_all_errors_bottom" class="btn btn-secondary floatright">';
387
            $retval .= '</form>';
388
        }
389
        return $retval;
390
    }
391
392
    /**
393
     * displays errors not displayed
394
     *
395
     * @return void
396
     */
397
    public function dispErrors(): void
398
    {
399
        echo $this->getDispErrors();
400
    }
401
402
    /**
403
     * look in session for saved errors
404
     *
405
     * @return void
406
     */
407
    protected function checkSavedErrors(): void
408
    {
409
        if (isset($_SESSION['errors'])) {
410
            // restore saved errors
411
            foreach ($_SESSION['errors'] as $hash => $error) {
412
                if ($error instanceof Error && ! isset($this->errors[$hash])) {
413
                    $this->errors[$hash] = $error;
414
                }
415
            }
416
417
            // delete stored errors
418
            $_SESSION['errors'] = [];
419
            unset($_SESSION['errors']);
420
        }
421
    }
422
423
    /**
424
     * return count of errors
425
     *
426
     * @param bool $check Whether to check for session errors
427
     *
428
     * @return integer number of errors occurred
429
     */
430
    public function countErrors(bool $check = true): int
431
    {
432
        return count($this->getErrors($check));
433
    }
434
435
    /**
436
     * return count of user errors
437
     *
438
     * @return integer number of user errors occurred
439
     */
440
    public function countUserErrors(): int
441
    {
442
        $count = 0;
443
        if ($this->countErrors()) {
444
            foreach ($this->getErrors() as $error) {
445
                if ($error->isUserError()) {
446
                    $count++;
447
                }
448
            }
449
        }
450
451
        return $count;
452
    }
453
454
    /**
455
     * whether use errors occurred or not
456
     *
457
     * @return boolean
458
     */
459
    public function hasUserErrors(): bool
460
    {
461
        return (bool) $this->countUserErrors();
462
    }
463
464
    /**
465
     * whether errors occurred or not
466
     *
467
     * @return boolean
468
     */
469
    public function hasErrors(): bool
470
    {
471
        return (bool) $this->countErrors();
472
    }
473
474
    /**
475
     * number of errors to be displayed
476
     *
477
     * @return integer number of errors to be displayed
478
     */
479
    public function countDisplayErrors(): int
480
    {
481
        if ($GLOBALS['cfg']['SendErrorReports'] != 'never') {
482
            return $this->countErrors();
483
        }
484
485
        return $this->countUserErrors();
486
    }
487
488
    /**
489
     * whether there are errors to display or not
490
     *
491
     * @return boolean
492
     */
493
    public function hasDisplayErrors(): bool
494
    {
495
        return (bool) $this->countDisplayErrors();
496
    }
497
498
    /**
499
     * Deletes previously stored errors in SESSION.
500
     * Saves current errors in session as previous errors.
501
     * Required to save current errors in case  'ask'
502
     *
503
     * @return void
504
     */
505
    public function savePreviousErrors(): void
506
    {
507
        unset($_SESSION['prev_errors']);
508
        $_SESSION['prev_errors'] = $GLOBALS['error_handler']->getCurrentErrors();
509
    }
510
511
    /**
512
     * Function to check if there are any errors to be prompted.
513
     * Needed because user warnings raised are
514
     *      also collected by global error handler.
515
     * This distinguishes between the actual errors
516
     *      and user errors raised to warn user.
517
     *
518
     * @return boolean true if there are errors to be "prompted", false otherwise
519
     */
520
    public function hasErrorsForPrompt(): bool
521
    {
522
        return (
523
            $GLOBALS['cfg']['SendErrorReports'] != 'never'
524
            && $this->countErrors() !=  $this->countUserErrors()
525
        );
526
    }
527
528
    /**
529
     * Function to report all the collected php errors.
530
     * Must be called at the end of each script
531
     *      by the $GLOBALS['error_handler'] only.
532
     *
533
     * @return void
534
     */
535
    public function reportErrors(): void
536
    {
537
        // if there're no actual errors,
538
        if (! $this->hasErrors()
539
            || $this->countErrors() ==  $this->countUserErrors()
540
        ) {
541
            // then simply return.
542
            return;
543
        }
544
        // Delete all the prev_errors in session & store new prev_errors in session
545
        $this->savePreviousErrors();
546
        $response = Response::getInstance();
547
        $jsCode = '';
548
        if ($GLOBALS['cfg']['SendErrorReports'] == 'always') {
549
            if ($response->isAjax()) {
550
                // set flag for automatic report submission.
551
                $response->addJSON('sendErrorAlways', '1');
552
            } else {
553
                // send the error reports asynchronously & without asking user
554
                $jsCode .= '$("#pma_report_errors_form").submit();'
555
                        . 'Functions.ajaxShowMessage(
556
                            Messages.phpErrorsBeingSubmitted, false
557
                        );';
558
                // js code to appropriate focusing,
559
                $jsCode .= '$("html, body").animate({
560
                                scrollTop:$(document).height()
561
                            }, "slow");';
562
            }
563
        } elseif ($GLOBALS['cfg']['SendErrorReports'] == 'ask') {
564
            //ask user whether to submit errors or not.
565
            if (! $response->isAjax()) {
566
                // js code to show appropriate msgs, event binding & focusing.
567
                $jsCode = 'Functions.ajaxShowMessage(Messages.phpErrorsFound);'
568
                        . '$("#pma_ignore_errors_popup").on("click", function() {
569
                            Functions.ignorePhpErrors()
570
                        });'
571
                        . '$("#pma_ignore_all_errors_popup").on("click",
572
                            function() {
573
                                Functions.ignorePhpErrors(false)
574
                            });'
575
                        . '$("#pma_ignore_errors_bottom").on("click", function(e) {
576
                            e.preventDefault();
577
                            Functions.ignorePhpErrors()
578
                        });'
579
                        . '$("#pma_ignore_all_errors_bottom").on("click",
580
                            function(e) {
581
                                e.preventDefault();
582
                                Functions.ignorePhpErrors(false)
583
                            });'
584
                        . '$("html, body").animate({
585
                            scrollTop:$(document).height()
586
                        }, "slow");';
587
            }
588
        }
589
        // The errors are already sent from the response.
590
        // Just focus on errors division upon load event.
591
        $response->getFooter()->getScripts()->addCode($jsCode);
592
    }
593
}
594