PageRenderTime 42ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/libraries/classes/ErrorHandler.php

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