ErrorHandler.php 16 KB

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