ErrorHandler.php 17 KB

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