ErrorHandler.php 17 KB

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