3 * Holds class PhpMyAdmin\ErrorHandler
5 declare(strict_types
=1);
9 use function array_splice
;
12 use function error_reporting
;
13 use function function_exists
;
14 use function headers_sent
;
15 use function htmlspecialchars
;
16 use function set_error_handler
;
17 use function trigger_error
;
18 use const E_COMPILE_ERROR
;
19 use const E_COMPILE_WARNING
;
20 use const E_CORE_ERROR
;
21 use const E_CORE_WARNING
;
22 use const E_DEPRECATED
;
26 use const E_RECOVERABLE_ERROR
;
28 use const E_USER_ERROR
;
29 use const E_USER_NOTICE
;
30 use const E_USER_WARNING
;
39 * holds errors to be displayed or reported later ...
43 protected $errors = [];
46 * Hide location of errors
48 protected $hide_location = false;
51 * Initial error reporting state
53 protected $error_reporting = 0;
55 public function __construct()
58 * Do not set ourselves as error handler in case of testsuite.
60 * This behavior is not tested there and breaks other tests as they
61 * rely on PHPUnit doing it's own error handling which we break here.
63 if (! defined('TESTSUITE')) {
64 set_error_handler([$this, 'handleError']);
66 if (function_exists('error_reporting')) {
67 $this->error_reporting
= error_reporting();
74 * stores errors in session
76 public function __destruct()
78 if (! isset($_SESSION['errors'])) {
79 $_SESSION['errors'] = [];
82 // remember only not displayed errors
83 foreach ($this->errors
as $key => $error) {
85 * We don't want to store all errors here as it would
86 * explode user session.
88 if (count($_SESSION['errors']) >= 10) {
91 __('Too many error messages, some are not displayed.'),
95 $_SESSION['errors'][$error->getHash()] = $error;
97 } elseif (($error instanceof Error
)
98 && ! $error->isDisplayed()
100 $_SESSION['errors'][$key] = $error;
106 * Toggles location hiding
108 * @param bool $hide Whether to hide
110 public function setHideLocation(bool $hide): void
112 $this->hide_location
= $hide;
116 * returns array with all errors
118 * @param bool $check Whether to check for session errors
122 public function getErrors(bool $check = true): array
125 $this->checkSavedErrors();
127 return $this->errors
;
131 * returns the errors occurred in the current run only.
132 * Does not include the errors saved in the SESSION
136 public function getCurrentErrors(): array
138 return $this->errors
;
142 * Pops recent errors from the storage
144 * @param int $count Old error count
148 public function sliceErrors(int $count): array
150 $errors = $this->getErrors(false);
151 $this->errors
= array_splice($errors, 0, $count);
152 return array_splice($errors, $count);
156 * Error handler - called when errors are triggered/occurred
158 * This calls the addError() function, escaping the error string
159 * Ignores the errors wherever Error Control Operator (@) is used.
161 * @param int $errno error number
162 * @param string $errstr error string
163 * @param string $errfile error file
164 * @param int $errline error line
166 public function handleError(
172 if (function_exists('error_reporting')) {
174 * Check if Error Control Operator (@) was used, but still show
175 * user errors even in this case.
177 if (error_reporting() == 0 &&
178 $this->error_reporting
!= 0 &&
179 ($errno & (E_USER_WARNING | E_USER_ERROR | E_USER_NOTICE | E_USER_DEPRECATED
)) == 0
184 if (($errno & (E_USER_WARNING | E_USER_ERROR | E_USER_NOTICE | E_USER_DEPRECATED
)) == 0) {
189 $this->addError($errstr, $errno, $errfile, $errline, true);
193 * Add an error; can also be called directly (with or without escaping)
195 * The following error types cannot be handled with a user defined function:
196 * E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR,
198 * and most of E_STRICT raised in the file where set_error_handler() is called.
200 * Do not use the context parameter as we want to avoid storing the
201 * complete $GLOBALS inside $_SESSION['errors']
203 * @param string $errstr error string
204 * @param int $errno error number
205 * @param string $errfile error file
206 * @param int $errline error line
207 * @param bool $escape whether to escape the error string
209 public function addError(
217 $errstr = htmlspecialchars($errstr);
219 // create error object
226 $error->setHideLocation($this->hide_location
);
228 // do not repeat errors
229 $this->errors
[$error->getHash()] = $error;
231 switch ($error->getNumber()) {
237 case E_COMPILE_WARNING
:
238 case E_RECOVERABLE_ERROR
:
239 /* Avoid rendering BB code in PHP errors */
240 $error->setBBCode(false);
245 case E_USER_DEPRECATED
:
246 // just collect the error
247 // display is called from outside
252 case E_COMPILE_ERROR
:
254 // FATAL error, display it and exit
255 $this->dispFatalError($error);
261 * trigger a custom error
263 * @param string $errorInfo error message
264 * @param int $errorNumber error number
266 public function triggerError(string $errorInfo, ?
int $errorNumber = null): void
268 // we could also extract file and line from backtrace
269 // and call handleError() directly
270 trigger_error($errorInfo, $errorNumber);
274 * display fatal error and exit
276 * @param Error $error the error
278 protected function dispFatalError(Error
$error): void
280 if (! headers_sent()) {
281 $this->dispPageStart($error);
284 $this->dispPageEnd();
289 * Displays user errors not displayed
291 public function dispUserErrors(): void
293 echo $this->getDispUserErrors();
297 * Renders user errors not displayed
299 public function getDispUserErrors(): string
302 foreach ($this->getErrors() as $error) {
303 if ($error->isUserError() && ! $error->isDisplayed()) {
304 $retval .= $error->getDisplay();
311 * display HTML header
313 * @param Error $error the error
315 protected function dispPageStart(?Error
$error = null): void
317 Response
::getInstance()->disable();
318 echo '<html><head><title>';
320 echo $error->getTitle();
322 echo 'phpMyAdmin error reporting page';
324 echo '</title></head>';
328 * display HTML footer
330 protected function dispPageEnd(): void
332 echo '</body></html>';
336 * renders errors not displayed
338 public function getDispErrors(): string
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();
349 $retval .= $this->getDispUserErrors();
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()
356 // add report button.
357 $retval .= '<form method="post" action="' . Url
::getFromRoute('/error-report')
358 . '" id="pma_report_errors_form"';
359 if ($GLOBALS['cfg']['SendErrorReports'] == 'always') {
360 // in case of 'always', generate 'invisible' form.
361 $retval .= ' class="hide"';
364 $retval .= Url
::getHiddenFields([
365 'exception_type' => 'php',
366 'send_error_report' => '1',
367 'server' => $GLOBALS['server'],
369 $retval .= '<input type="submit" value="'
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')
378 if ($GLOBALS['cfg']['SendErrorReports'] == 'ask') {
379 // add ignore buttons
380 $retval .= '<input type="submit" value="'
382 . '" id="pma_ignore_errors_bottom" class="btn btn-secondary floatright">';
384 $retval .= '<input type="submit" value="'
386 . '" id="pma_ignore_all_errors_bottom" class="btn btn-secondary floatright">';
387 $retval .= '</form>';
393 * look in session for saved errors
395 protected function checkSavedErrors(): void
397 if (isset($_SESSION['errors'])) {
398 // restore saved errors
399 foreach ($_SESSION['errors'] as $hash => $error) {
400 if ($error instanceof Error
&& ! isset($this->errors
[$hash])) {
401 $this->errors
[$hash] = $error;
405 // delete stored errors
406 $_SESSION['errors'] = [];
407 unset($_SESSION['errors']);
412 * return count of errors
414 * @param bool $check Whether to check for session errors
416 * @return int number of errors occurred
418 public function countErrors(bool $check = true): int
420 return count($this->getErrors($check));
424 * return count of user errors
426 * @return int number of user errors occurred
428 public function countUserErrors(): int
431 if ($this->countErrors()) {
432 foreach ($this->getErrors() as $error) {
433 if ($error->isUserError()) {
443 * whether use errors occurred or not
445 public function hasUserErrors(): bool
447 return (bool) $this->countUserErrors();
451 * whether errors occurred or not
453 public function hasErrors(): bool
455 return (bool) $this->countErrors();
459 * number of errors to be displayed
461 * @return int number of errors to be displayed
463 public function countDisplayErrors(): int
465 if ($GLOBALS['cfg']['SendErrorReports'] != 'never') {
466 return $this->countErrors();
469 return $this->countUserErrors();
473 * whether there are errors to display or not
475 public function hasDisplayErrors(): bool
477 return (bool) $this->countDisplayErrors();
481 * Deletes previously stored errors in SESSION.
482 * Saves current errors in session as previous errors.
483 * Required to save current errors in case 'ask'
485 public function savePreviousErrors(): void
487 unset($_SESSION['prev_errors']);
488 $_SESSION['prev_errors'] = $GLOBALS['error_handler']->getCurrentErrors();
492 * Function to check if there are any errors to be prompted.
493 * Needed because user warnings raised are
494 * also collected by global error handler.
495 * This distinguishes between the actual errors
496 * and user errors raised to warn user.
498 * @return bool true if there are errors to be "prompted", false otherwise
500 public function hasErrorsForPrompt(): bool
502 return $GLOBALS['cfg']['SendErrorReports'] != 'never'
503 && $this->countErrors() != $this->countUserErrors();
507 * Function to report all the collected php errors.
508 * Must be called at the end of each script
509 * by the $GLOBALS['error_handler'] only.
511 public function reportErrors(): void
513 // if there're no actual errors,
514 if (! $this->hasErrors()
515 ||
$this->countErrors() == $this->countUserErrors()
517 // then simply return.
520 // Delete all the prev_errors in session & store new prev_errors in session
521 $this->savePreviousErrors();
522 $response = Response
::getInstance();
524 if ($GLOBALS['cfg']['SendErrorReports'] == 'always') {
525 if ($response->isAjax()) {
526 // set flag for automatic report submission.
527 $response->addJSON('sendErrorAlways', '1');
529 // send the error reports asynchronously & without asking user
530 $jsCode .= '$("#pma_report_errors_form").submit();'
531 . 'Functions.ajaxShowMessage(
532 Messages.phpErrorsBeingSubmitted, false
534 // js code to appropriate focusing,
535 $jsCode .= '$("html, body").animate({
536 scrollTop:$(document).height()
539 } elseif ($GLOBALS['cfg']['SendErrorReports'] == 'ask') {
540 //ask user whether to submit errors or not.
541 if (! $response->isAjax()) {
542 // js code to show appropriate msgs, event binding & focusing.
543 $jsCode = 'Functions.ajaxShowMessage(Messages.phpErrorsFound);'
544 . '$("#pma_ignore_errors_popup").on("click", function() {
545 Functions.ignorePhpErrors()
547 . '$("#pma_ignore_all_errors_popup").on("click",
549 Functions.ignorePhpErrors(false)
551 . '$("#pma_ignore_errors_bottom").on("click", function(e) {
553 Functions.ignorePhpErrors()
555 . '$("#pma_ignore_all_errors_bottom").on("click",
558 Functions.ignorePhpErrors(false)
560 . '$("html, body").animate({
561 scrollTop:$(document).height()
565 // The errors are already sent from the response.
566 // Just focus on errors division upon load event.
567 $response->getFooter()->getScripts()->addCode($jsCode);