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