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