ErrorHandler.php 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. <?php
  2. /**
  3. * This file is part of the ForkBB <https://github.com/forkbb>.
  4. *
  5. * @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
  6. * @license The MIT License (MIT)
  7. */
  8. declare(strict_types=1);
  9. namespace ForkBB\Core;
  10. use Throwable;
  11. class ErrorHandler
  12. {
  13. /**
  14. * Уровень буфера вывода на котором работает обработчик
  15. * @var int
  16. */
  17. protected $obLevel;
  18. /**
  19. * Описание ошибки
  20. * @var array
  21. */
  22. protected $error;
  23. /**
  24. * Флаг отправки сообщения в лог
  25. * @var bool
  26. */
  27. protected $logged = false;
  28. /**
  29. * Скрываемая часть пути до файла
  30. * @var string
  31. */
  32. protected $hidePath;
  33. /**
  34. * Список ошибок
  35. * @var array
  36. */
  37. protected $type = [
  38. 0 => 'OTHER_ERROR',
  39. \E_ERROR => 'E_ERROR',
  40. \E_WARNING => 'E_WARNING',
  41. \E_PARSE => 'E_PARSE',
  42. \E_NOTICE => 'E_NOTICE',
  43. \E_CORE_ERROR => 'E_CORE_ERROR',
  44. \E_CORE_WARNING => 'E_CORE_WARNING',
  45. \E_COMPILE_ERROR => 'E_COMPILE_ERROR',
  46. \E_COMPILE_WARNING => 'E_COMPILE_WARNING',
  47. \E_USER_ERROR => 'E_USER_ERROR',
  48. \E_USER_WARNING => 'E_USER_WARNING',
  49. \E_USER_NOTICE => 'E_USER_NOTICE',
  50. \E_STRICT => 'E_STRICT',
  51. \E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR',
  52. \E_DEPRECATED => 'E_DEPRECATED',
  53. \E_USER_DEPRECATED => 'E_USER_DEPRECATED',
  54. ];
  55. public function __construct()
  56. {
  57. $this->hidePath = \realpath(__DIR__ . '/../../');
  58. \set_error_handler([$this, 'errorHandler']);
  59. \set_exception_handler([$this, 'exceptionHandler']);
  60. \register_shutdown_function([$this, 'shutdownHandler']);
  61. \ob_start();
  62. $this->obLevel = \ob_get_level();
  63. }
  64. public function __destruct()
  65. {
  66. \restore_error_handler();
  67. \restore_exception_handler();
  68. //????
  69. }
  70. /**
  71. * Обрабатыет перехватываемые ошибки
  72. */
  73. public function errorHandler(int $type, string $message, string $file, string $line): bool
  74. {
  75. $error = [
  76. 'type' => $type,
  77. 'message' => $message,
  78. 'file' => $file,
  79. 'line' => $line,
  80. 'trace' => \debug_backtrace(0),
  81. ];
  82. $this->log($error);
  83. if ($type & \error_reporting()) {
  84. $this->error = $error;
  85. exit(1);
  86. }
  87. $this->logged = false;
  88. return true;
  89. }
  90. /**
  91. * Обрабатывает не перехваченные исключения
  92. */
  93. public function exceptionHandler(Throwable $e): void
  94. {
  95. $this->error = [
  96. 'type' => 0, //????
  97. 'message' => $e->getMessage(),
  98. 'file' => $e->getFile(),
  99. 'line' => $e->getLine(),
  100. 'trace' => $e->getTrace(),
  101. ];
  102. }
  103. /**
  104. * Окончательно обрабатывает ошибки (в том числе фатальные) и исключения
  105. */
  106. public function shutdownHandler(): void
  107. {
  108. if (isset($this->error['type'])) {
  109. $show = true;
  110. } else {
  111. $show = false;
  112. $this->error = \error_get_last();
  113. if (isset($this->error['type'])) {
  114. switch ($this->error['type']) {
  115. case \E_ERROR:
  116. case \E_PARSE:
  117. case \E_CORE_ERROR:
  118. case \E_CORE_WARNING:
  119. case \E_COMPILE_ERROR:
  120. case \E_COMPILE_WARNING:
  121. $show = true;
  122. break;
  123. }
  124. }
  125. }
  126. if (
  127. isset($this->error['type'])
  128. && ! $this->logged
  129. ) {
  130. $this->log($this->error);
  131. }
  132. while (\ob_get_level() > $this->obLevel) {
  133. \ob_end_clean();
  134. }
  135. if (\ob_get_level() === $this->obLevel) {
  136. if ($show) {
  137. \ob_end_clean();
  138. $this->show($this->error);
  139. } else {
  140. \ob_end_flush();
  141. }
  142. }
  143. }
  144. /**
  145. * Отправляет сообщение в лог
  146. */
  147. protected function log(array $error): void
  148. {
  149. $this->logged = true;
  150. $message = \preg_replace('%[\x00-\x1F]%', ' ', $this->message($error));
  151. \error_log($message);
  152. }
  153. /**
  154. * Выводит сообщение об ошибке
  155. *
  156. * @param array $error
  157. */
  158. protected function show(array $error): void
  159. {
  160. \header('HTTP/1.1 500 Internal Server Error');
  161. \header('Content-Type: text/html; charset=utf-8');
  162. echo <<<'EOT'
  163. <!DOCTYPE html>
  164. <html lang="en" dir="ltr">
  165. <head>
  166. <meta charset="utf-8">
  167. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  168. <title>500 Internal Server Error</title>
  169. </head>
  170. <body>
  171. EOT;
  172. if (1 == \ini_get('display_errors')) {
  173. echo '<p>' . $this->e($this->message($error)) . '</p>';
  174. if (
  175. isset($error['trace'])
  176. && \is_array($error['trace'])
  177. ) {
  178. echo '<div><p>Trace:</p><ol>';
  179. foreach ($error['trace'] as $cur) {
  180. if (
  181. isset($cur['file'], $cur['line'], $error['file'], $error['line'])
  182. && $error['line'] === $cur['line']
  183. && $error['file'] === $cur['file']
  184. ) {
  185. continue;
  186. }
  187. $line = $cur['file'] ?? '-';
  188. $line .= '(' . ($cur['line'] ?? '-') . '): ';
  189. if (isset($cur['class'])) {
  190. $line .= $cur['class'] . $cur['type'];
  191. }
  192. $line .= ($cur['function'] ?? 'unknown') . '(';
  193. if (
  194. ! empty($cur['args'])
  195. && \is_array($cur['args'])
  196. ) {
  197. $comma = '';
  198. foreach($cur['args'] as $arg) {
  199. $type = \gettype($arg);
  200. switch ($type) {
  201. case 'boolean':
  202. $type = $arg ? 'true' : 'false';
  203. break;
  204. case 'array':
  205. $type .= '(' . \count($arg) . ')';
  206. break;
  207. case 'resource':
  208. $type = \get_resource_type($arg);
  209. break;
  210. case 'object':
  211. $type .= '{' . \get_class($arg) . '}';
  212. break;
  213. }
  214. $line .= $comma . $type;
  215. $comma = ', ';
  216. }
  217. }
  218. $line .= ')';
  219. $line = $this->e(\str_replace($this->hidePath, '...', $line));
  220. echo "<li>{$line}</li>";
  221. }
  222. echo '</ol></div>';
  223. }
  224. } else {
  225. echo '<p>Oops</p>';
  226. }
  227. echo <<<'EOT'
  228. </body>
  229. </html>
  230. EOT;
  231. }
  232. /**
  233. * Формирует сообщение
  234. */
  235. protected function message(array $error): string
  236. {
  237. $type = $this->type[$error['type']] ?? $this->type[0];
  238. $file = \str_replace($this->hidePath, '...', $error['file']);
  239. return "PHP {$type}: \"{$error['message']}\" in {$file}:[{$error['line']}]";
  240. }
  241. /**
  242. * Экранирует спецсимволов HTML-сущностями
  243. */
  244. protected function e(string $arg): string
  245. {
  246. return \htmlspecialchars($arg, \ENT_HTML5 | \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
  247. }
  248. }