Merge branch 'QA_5_0'
[phpmyadmin.git] / libraries / classes / ErrorHandler.php
blobd4eb4b76b4b1c22a65dd4ad55a525e48fd5c8966
1 <?php
2 /**
3 * Holds class PhpMyAdmin\ErrorHandler
4 */
5 declare(strict_types=1);
7 namespace PhpMyAdmin;
9 use function array_splice;
10 use function count;
11 use function defined;
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;
23 use const E_ERROR;
24 use const E_NOTICE;
25 use const E_PARSE;
26 use const E_RECOVERABLE_ERROR;
27 use const E_STRICT;
28 use const E_USER_ERROR;
29 use const E_USER_NOTICE;
30 use const E_USER_WARNING;
31 use const E_WARNING;
33 /**
34 * handling errors
36 class ErrorHandler
38 /**
39 * holds errors to be displayed or reported later ...
41 * @var Error[]
43 protected $errors = [];
45 /**
46 * Hide location of errors
48 protected $hide_location = false;
50 /**
51 * Initial error reporting state
53 protected $error_reporting = 0;
55 public function __construct()
57 /**
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();
71 /**
72 * Destructor
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) {
84 /**
85 * We don't want to store all errors here as it would
86 * explode user session.
88 if (count($_SESSION['errors']) >= 10) {
89 $error = new Error(
91 __('Too many error messages, some are not displayed.'),
92 __FILE__,
93 __LINE__
95 $_SESSION['errors'][$error->getHash()] = $error;
96 break;
97 } elseif (($error instanceof Error)
98 && ! $error->isDisplayed()
99 ) {
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
120 * @return Error[]
122 public function getErrors(bool $check = true): array
124 if ($check) {
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
134 * @return Error[]
136 public function getCurrentErrors(): array
138 return $this->errors;
142 * Pops recent errors from the storage
144 * @param int $count Old error count
146 * @return Error[]
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(
167 int $errno,
168 string $errstr,
169 string $errfile,
170 int $errline
171 ): void {
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
181 return;
183 } else {
184 if (($errno & (E_USER_WARNING | E_USER_ERROR | E_USER_NOTICE | E_USER_DEPRECATED)) == 0) {
185 return;
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,
197 * E_COMPILE_WARNING,
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(
210 string $errstr,
211 int $errno,
212 string $errfile,
213 int $errline,
214 bool $escape = true
215 ): void {
216 if ($escape) {
217 $errstr = htmlspecialchars($errstr);
219 // create error object
220 $error = new Error(
221 $errno,
222 $errstr,
223 $errfile,
224 $errline
226 $error->setHideLocation($this->hide_location);
228 // do not repeat errors
229 $this->errors[$error->getHash()] = $error;
231 switch ($error->getNumber()) {
232 case E_STRICT:
233 case E_DEPRECATED:
234 case E_NOTICE:
235 case E_WARNING:
236 case E_CORE_WARNING:
237 case E_COMPILE_WARNING:
238 case E_RECOVERABLE_ERROR:
239 /* Avoid rendering BB code in PHP errors */
240 $error->setBBCode(false);
241 break;
242 case E_USER_NOTICE:
243 case E_USER_WARNING:
244 case E_USER_ERROR:
245 case E_USER_DEPRECATED:
246 // just collect the error
247 // display is called from outside
248 break;
249 case E_ERROR:
250 case E_PARSE:
251 case E_CORE_ERROR:
252 case E_COMPILE_ERROR:
253 default:
254 // FATAL error, display it and exit
255 $this->dispFatalError($error);
256 exit;
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);
283 $error->display();
284 $this->dispPageEnd();
285 exit;
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
301 $retval = '';
302 foreach ($this->getErrors() as $error) {
303 if ($error->isUserError() && ! $error->isDisplayed()) {
304 $retval .= $error->getDisplay();
307 return $retval;
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>';
319 if ($error) {
320 echo $error->getTitle();
321 } else {
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
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();
348 } else {
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"';
363 $retval .= '>';
364 $retval .= Url::getHiddenFields([
365 'exception_type' => 'php',
366 'send_error_report' => '1',
367 'server' => $GLOBALS['server'],
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>';
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">';
384 $retval .= '<input type="submit" value="'
385 . __('Ignore All')
386 . '" id="pma_ignore_all_errors_bottom" class="btn btn-secondary floatright">';
387 $retval .= '</form>';
389 return $retval;
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
430 $count = 0;
431 if ($this->countErrors()) {
432 foreach ($this->getErrors() as $error) {
433 if ($error->isUserError()) {
434 $count++;
439 return $count;
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.
518 return;
520 // Delete all the prev_errors in session & store new prev_errors in session
521 $this->savePreviousErrors();
522 $response = Response::getInstance();
523 $jsCode = '';
524 if ($GLOBALS['cfg']['SendErrorReports'] == 'always') {
525 if ($response->isAjax()) {
526 // set flag for automatic report submission.
527 $response->addJSON('sendErrorAlways', '1');
528 } else {
529 // send the error reports asynchronously & without asking user
530 $jsCode .= '$("#pma_report_errors_form").submit();'
531 . 'Functions.ajaxShowMessage(
532 Messages.phpErrorsBeingSubmitted, false
533 );';
534 // js code to appropriate focusing,
535 $jsCode .= '$("html, body").animate({
536 scrollTop:$(document).height()
537 }, "slow");';
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()
546 });'
547 . '$("#pma_ignore_all_errors_popup").on("click",
548 function() {
549 Functions.ignorePhpErrors(false)
550 });'
551 . '$("#pma_ignore_errors_bottom").on("click", function(e) {
552 e.preventDefault();
553 Functions.ignorePhpErrors()
554 });'
555 . '$("#pma_ignore_all_errors_bottom").on("click",
556 function(e) {
557 e.preventDefault();
558 Functions.ignorePhpErrors(false)
559 });'
560 . '$("html, body").animate({
561 scrollTop:$(document).height()
562 }, "slow");';
565 // The errors are already sent from the response.
566 // Just focus on errors division upon load event.
567 $response->getFooter()->getScripts()->addCode($jsCode);