Passed
Push — master ( 003da0...69fb1d )
by William
07:21
created

libraries/classes/ErrorHandler.php (1 issue)

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)) {
67
            if (! isset($_SESSION['errors'])) {
68
                $_SESSION['errors'] = [];
69
            }
70
71
            // remember only not displayed errors
72
            foreach ($this->errors as $key => $error) {
73
                /**
74
                 * We don't want to store all errors here as it would
75
                 * explode user session.
76
                 */
77
                if (count($_SESSION['errors']) >= 10) {
78
                    $error = new Error(
79
                        0,
80
                        __('Too many error messages, some are not displayed.'),
81
                        __FILE__,
82
                        __LINE__
83
                    );
84
                    $_SESSION['errors'][$error->getHash()] = $error;
85
                    break;
86
                } elseif (($error instanceof Error)
87
                    && ! $error->isDisplayed()
88
                ) {
89
                    $_SESSION['errors'][$key] = $error;
90
                }
91
            }
92
        }
93
    }
94
95
    /**
96
     * Toggles location hiding
97
     *
98
     * @param boolean $hide Whether to hide
99
     *
100
     * @return void
101
     */
102
    public function setHideLocation(bool $hide): void
103
    {
104
        $this->hide_location = $hide;
105
    }
106
107
    /**
108
     * returns array with all errors
109
     *
110
     * @param bool $check Whether to check for session errors
111
     *
112
     * @return Error[]
113
     */
114
    public function getErrors(bool $check = true): array
115
    {
116
        if ($check) {
117
            $this->checkSavedErrors();
118
        }
119
        return $this->errors;
120
    }
121
122
    /**
123
    * returns the errors occurred in the current run only.
124
    * Does not include the errors saved in the SESSION
125
    *
126
    * @return Error[]
127
    */
128
    public function getCurrentErrors(): array
129
    {
130
        return $this->errors;
131
    }
132
133
    /**
134
     * Pops recent errors from the storage
135
     *
136
     * @param int $count Old error count
137
     *
138
     * @return Error[]
139
     */
140
    public function sliceErrors(int $count): array
141
    {
142
        $errors = $this->getErrors(false);
143
        $this->errors = array_splice($errors, 0, $count);
144
        return array_splice($errors, $count);
145
    }
146
147
    /**
148
     * Error handler - called when errors are triggered/occurred
149
     *
150
     * This calls the addError() function, escaping the error string
151
     * Ignores the errors wherever Error Control Operator (@) is used.
152
     *
153
     * @param integer $errno   error number
154
     * @param string  $errstr  error string
155
     * @param string  $errfile error file
156
     * @param integer $errline error line
157
     *
158
     * @return void
159
     */
160
    public function handleError(
161
        int $errno,
162
        string $errstr,
163
        string $errfile,
164
        int $errline
165
    ): void {
166
        /**
167
         * Check if Error Control Operator (@) was used, but still show
168
         * user errors even in this case.
169
         */
170
        if (error_reporting() == 0 &&
171
            $this->error_reporting != 0 &&
172
            ($errno & (E_USER_WARNING | E_USER_ERROR | E_USER_NOTICE)) == 0
173
        ) {
174
            return;
175
        }
176
        $this->addError($errstr, $errno, $errfile, $errline, true);
177
    }
178
179
    /**
180
     * Add an error; can also be called directly (with or without escaping)
181
     *
182
     * The following error types cannot be handled with a user defined function:
183
     * E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR,
184
     * E_COMPILE_WARNING,
185
     * and most of E_STRICT raised in the file where set_error_handler() is called.
186
     *
187
     * Do not use the context parameter as we want to avoid storing the
188
     * complete $GLOBALS inside $_SESSION['errors']
189
     *
190
     * @param string  $errstr  error string
191
     * @param integer $errno   error number
192
     * @param string  $errfile error file
193
     * @param integer $errline error line
194
     * @param boolean $escape  whether to escape the error string
195
     *
196
     * @return void
197
     */
198
    public function addError(
199
        string $errstr,
200
        int $errno,
201
        string $errfile,
202
        int $errline,
203
        bool $escape = true
204
    ): void {
205
        if ($escape) {
206
            $errstr = htmlspecialchars($errstr);
207
        }
208
        // create error object
209
        $error = new Error(
210
            $errno,
211
            $errstr,
212
            $errfile,
213
            $errline
214
        );
215
        $error->setHideLocation($this->hide_location);
216
217
        // do not repeat errors
218
        $this->errors[$error->getHash()] = $error;
219
220
        switch ($error->getNumber()) {
221
            case E_STRICT:
222
            case E_DEPRECATED:
223
            case E_NOTICE:
224
            case E_WARNING:
225
            case E_CORE_WARNING:
226
            case E_COMPILE_WARNING:
227
            case E_RECOVERABLE_ERROR:
228
                /* Avoid rendering BB code in PHP errors */
229
                $error->setBBCode(false);
230
                break;
231
            case E_USER_NOTICE:
232
            case E_USER_WARNING:
233
            case E_USER_ERROR:
234
                // just collect the error
235
                // display is called from outside
236
                break;
237
            case E_ERROR:
238
            case E_PARSE:
239
            case E_CORE_ERROR:
240
            case E_COMPILE_ERROR:
241
            default:
242
                // FATAL error, display it and exit
243
                $this->dispFatalError($error);
244
                exit;
245
        }
246
    }
247
248
    /**
249
     * trigger a custom error
250
     *
251
     * @param string  $errorInfo   error message
252
     * @param integer $errorNumber error number
253
     *
254
     * @return void
255
     */
256
    public function triggerError(string $errorInfo, ?int $errorNumber = null): void
257
    {
258
        // we could also extract file and line from backtrace
259
        // and call handleError() directly
260
        trigger_error($errorInfo, $errorNumber);
261
    }
262
263
    /**
264
     * display fatal error and exit
265
     *
266
     * @param Error $error the error
267
     *
268
     * @return void
269
     */
270
    protected function dispFatalError(Error $error): void
271
    {
272
        if (! headers_sent()) {
273
            $this->dispPageStart($error);
274
        }
275
        $error->display();
276
        $this->dispPageEnd();
277
        exit;
0 ignored issues
show
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...
278
    }
279
280
    /**
281
     * Displays user errors not displayed
282
     *
283
     * @return void
284
     */
285
    public function dispUserErrors(): void
286
    {
287
        echo $this->getDispUserErrors();
288
    }
289
290
    /**
291
     * Renders user errors not displayed
292
     *
293
     * @return string
294
     */
295
    public function getDispUserErrors(): string
296
    {
297
        $retval = '';
298
        foreach ($this->getErrors() as $error) {
299
            if ($error->isUserError() && ! $error->isDisplayed()) {
300
                $retval .= $error->getDisplay();
301
            }
302
        }
303
        return $retval;
304
    }
305
306
    /**
307
     * display HTML header
308
     *
309
     * @param Error $error the error
310
     *
311
     * @return void
312
     */
313
    protected function dispPageStart(?Error $error = null): void
314
    {
315
        Response::getInstance()->disable();
316
        echo '<html><head><title>';
317
        if ($error) {
318
            echo $error->getTitle();
319
        } else {
320
            echo 'phpMyAdmin error reporting page';
321
        }
322
        echo '</title></head>';
323
    }
324
325
    /**
326
     * display HTML footer
327
     *
328
     * @return void
329
     */
330
    protected function dispPageEnd(): void
331
    {
332
        echo '</body></html>';
333
    }
334
335
    /**
336
     * renders errors not displayed
337
     *
338
     * @return string
339
     */
340
    public function getDispErrors(): string
341
    {
342
        $retval = '';
343
        // display errors if SendErrorReports is set to 'ask'.
344
        if ($GLOBALS['cfg']['SendErrorReports'] != 'never') {
345
            foreach ($this->getErrors() as $error) {
346
                if (! $error->isDisplayed()) {
347
                    $retval .= $error->getDisplay();
348
                }
349
            }
350
        } else {
351
            $retval .= $this->getDispUserErrors();
352
        }
353
        // if preference is not 'never' and
354
        // there are 'actual' errors to be reported
355
        if ($GLOBALS['cfg']['SendErrorReports'] != 'never'
356
            &&  $this->countErrors() !=  $this->countUserErrors()
357
        ) {
358
            // add report button.
359
            $retval .= '<form method="post" action="error_report.php"'
360
                    . ' id="pma_report_errors_form"';
361
            if ($GLOBALS['cfg']['SendErrorReports'] == 'always') {
362
                // in case of 'always', generate 'invisible' form.
363
                $retval .= ' class="hide"';
364
            }
365
            $retval .=  '>';
366
            $retval .= Url::getHiddenFields([
367
                'exception_type' => 'php',
368
                'send_error_report' => '1',
369
            ]);
370
            $retval .= '<input type="submit" value="'
371
                    . __('Report')
372
                    . '" id="pma_report_errors" class="floatright">'
373
                    . '<input type="checkbox" name="always_send"'
374
                    . ' id="always_send_checkbox" value="true"/>'
375
                    . '<label for="always_send_checkbox">'
376
                    . __('Automatically send report next time')
377
                    . '</label>';
378
379
            if ($GLOBALS['cfg']['SendErrorReports'] == 'ask') {
380
                // add ignore buttons
381
                $retval .= '<input type="submit" value="'
382
                        . __('Ignore')
383
                        . '" id="pma_ignore_errors_bottom" class="floatright">';
384
            }
385
            $retval .= '<input type="submit" value="'
386
                    . __('Ignore All')
387
                    . '" id="pma_ignore_all_errors_bottom" class="floatright">';
388
            $retval .= '</form>';
389
        }
390
        return $retval;
391
    }
392
393
    /**
394
     * displays errors not displayed
395
     *
396
     * @return void
397
     */
398
    public function dispErrors(): void
399
    {
400
        echo $this->getDispErrors();
401
    }
402
403
    /**
404
     * look in session for saved errors
405
     *
406
     * @return void
407
     */
408
    protected function checkSavedErrors(): void
409
    {
410
        if (isset($_SESSION['errors'])) {
411
            // restore saved errors
412
            foreach ($_SESSION['errors'] as $hash => $error) {
413
                if ($error instanceof Error && ! isset($this->errors[$hash])) {
414
                    $this->errors[$hash] = $error;
415
                }
416
            }
417
418
            // delete stored errors
419
            $_SESSION['errors'] = [];
420
            unset($_SESSION['errors']);
421
        }
422
    }
423
424
    /**
425
     * return count of errors
426
     *
427
     * @param bool $check Whether to check for session errors
428
     *
429
     * @return integer number of errors occurred
430
     */
431
    public function countErrors(bool $check = true): int
432
    {
433
        return count($this->getErrors($check));
434
    }
435
436
    /**
437
     * return count of user errors
438
     *
439
     * @return integer number of user errors occurred
440
     */
441
    public function countUserErrors(): int
442
    {
443
        $count = 0;
444
        if ($this->countErrors()) {
445
            foreach ($this->getErrors() as $error) {
446
                if ($error->isUserError()) {
447
                    $count++;
448
                }
449
            }
450
        }
451
452
        return $count;
453
    }
454
455
    /**
456
     * whether use errors occurred or not
457
     *
458
     * @return boolean
459
     */
460
    public function hasUserErrors(): bool
461
    {
462
        return (bool) $this->countUserErrors();
463
    }
464
465
    /**
466
     * whether errors occurred or not
467
     *
468
     * @return boolean
469
     */
470
    public function hasErrors(): bool
471
    {
472
        return (bool) $this->countErrors();
473
    }
474
475
    /**
476
     * number of errors to be displayed
477
     *
478
     * @return integer number of errors to be displayed
479
     */
480
    public function countDisplayErrors(): int
481
    {
482
        if ($GLOBALS['cfg']['SendErrorReports'] != 'never') {
483
            return $this->countErrors();
484
        }
485
486
        return $this->countUserErrors();
487
    }
488
489
    /**
490
     * whether there are errors to display or not
491
     *
492
     * @return boolean
493
     */
494
    public function hasDisplayErrors(): bool
495
    {
496
        return (bool) $this->countDisplayErrors();
497
    }
498
499
    /**
500
    * Deletes previously stored errors in SESSION.
501
    * Saves current errors in session as previous errors.
502
    * Required to save current errors in case  'ask'
503
    *
504
    * @return void
505
    */
506
    public function savePreviousErrors(): void
507
    {
508
        unset($_SESSION['prev_errors']);
509
        $_SESSION['prev_errors'] = $GLOBALS['error_handler']->getCurrentErrors();
510
    }
511
512
    /**
513
     * Function to check if there are any errors to be prompted.
514
     * Needed because user warnings raised are
515
     *      also collected by global error handler.
516
     * This distinguishes between the actual errors
517
     *      and user errors raised to warn user.
518
     *
519
     *@return boolean true if there are errors to be "prompted", false otherwise
520
     */
521
    public function hasErrorsForPrompt(): bool
522
    {
523
        return (
524
            $GLOBALS['cfg']['SendErrorReports'] != 'never'
525
            && $this->countErrors() !=  $this->countUserErrors()
526
        );
527
    }
528
529
    /**
530
     * Function to report all the collected php errors.
531
     * Must be called at the end of each script
532
     *      by the $GLOBALS['error_handler'] only.
533
     *
534
     * @return void
535
     */
536
    public function reportErrors(): void
537
    {
538
        // if there're no actual errors,
539
        if (!$this->hasErrors()
540
            || $this->countErrors() ==  $this->countUserErrors()
541
        ) {
542
            // then simply return.
543
            return;
544
        }
545
        // Delete all the prev_errors in session & store new prev_errors in session
546
        $this->savePreviousErrors();
547
        $response = Response::getInstance();
548
        $jsCode = '';
549
        if ($GLOBALS['cfg']['SendErrorReports'] == 'always') {
550
            if ($response->isAjax()) {
551
                // set flag for automatic report submission.
552
                $response->addJSON('_sendErrorAlways', '1');
553
            } else {
554
                // send the error reports asynchronously & without asking user
555
                $jsCode .= '$("#pma_report_errors_form").submit();'
556
                        . 'PMA_ajaxShowMessage(
557
                            PMA_messages["phpErrorsBeingSubmitted"], false
558
                        );';
559
                // js code to appropriate focusing,
560
                $jsCode .= '$("html, body").animate({
561
                                scrollTop:$(document).height()
562
                            }, "slow");';
563
            }
564
        } elseif ($GLOBALS['cfg']['SendErrorReports'] == 'ask') {
565
            //ask user whether to submit errors or not.
566
            if (!$response->isAjax()) {
567
                // js code to show appropriate msgs, event binding & focusing.
568
                $jsCode = 'PMA_ajaxShowMessage(PMA_messages["phpErrorsFound"]);'
569
                        . '$("#pma_ignore_errors_popup").bind("click", function() {
570
                            PMA_ignorePhpErrors()
571
                        });'
572
                        . '$("#pma_ignore_all_errors_popup").bind("click",
573
                            function() {
574
                                PMA_ignorePhpErrors(false)
575
                            });'
576
                        . '$("#pma_ignore_errors_bottom").bind("click", function(e) {
577
                            e.preventDefaulut();
578
                            PMA_ignorePhpErrors()
579
                        });'
580
                        . '$("#pma_ignore_all_errors_bottom").bind("click",
581
                            function(e) {
582
                                e.preventDefault();
583
                                PMA_ignorePhpErrors(false)
584
                            });'
585
                        . '$("html, body").animate({
586
                            scrollTop:$(document).height()
587
                        }, "slow");';
588
            }
589
        }
590
        // The errors are already sent from the response.
591
        // Just focus on errors division upon load event.
592
        $response->getFooter()->getScripts()->addCode($jsCode);
593
    }
594
}
595