ErrorHandler.php 17 KB

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