. * * @copyright (c) Visman * @license The MIT License (MIT) */ declare(strict_types=1); namespace ForkBB\Core; use ForkBB\Core\Container; use Psr\Log\NullLogger; use Throwable; class ErrorHandler { protected Container $c; /** * Уровень буфера вывода на котором работает обработчик */ protected int $obLevel; /** * Описание ошибки */ protected ?array $error = null; /** * Скрываемая часть пути до файла */ protected string $hidePath; /** * Список ошибок */ protected array $type = [ 0 => ['OTHER_ERROR', 'error'], \E_ERROR => ['E_ERROR', 'error'], \E_WARNING => ['E_WARNING', 'warning'], \E_PARSE => ['E_PARSE', 'critical'], \E_NOTICE => ['E_NOTICE', 'notice'], \E_CORE_ERROR => ['E_CORE_ERROR', 'error'], \E_CORE_WARNING => ['E_CORE_WARNING', 'warning'], \E_COMPILE_ERROR => ['E_COMPILE_ERROR', 'error'], \E_COMPILE_WARNING => ['E_COMPILE_WARNING', 'warning'], \E_USER_ERROR => ['E_USER_ERROR', 'error'], \E_USER_WARNING => ['E_USER_WARNING', 'warning'], \E_USER_NOTICE => ['E_USER_NOTICE', 'notice'], \E_STRICT => ['E_STRICT', 'error'], \E_RECOVERABLE_ERROR => ['E_RECOVERABLE_ERROR', 'error'], \E_DEPRECATED => ['E_DEPRECATED', 'warning'], \E_USER_DEPRECATED => ['E_USER_DEPRECATED', 'warning'], ]; /** * Уровень ошибок только для логирования */ protected int $logOnly = 0; public function __construct() { $this->hidePath = \realpath(__DIR__ . '/../../'); \set_error_handler([$this, 'errorHandler'], \E_ALL); \set_exception_handler([$this, 'exceptionHandler']); \register_shutdown_function([$this, 'shutdownHandler']); \ob_start(); $this->obLevel = \ob_get_level(); } public function __destruct() { \restore_error_handler(); \restore_exception_handler(); //???? } public function setContainer(Container $c): void { $this->c = $c; $c->ErrorHandler = $this; } /** * Устанавливает уровень ошибок, которые логируются без остановки(?) скрипта */ public function logOnly(int $level): int { $result = $this->logOnly; $this->logOnly = $level; return $result; } /** * Обрабатыет перехватываемые ошибки */ public function errorHandler(int $type, string $message, string $file, int $line): bool { $logOnly = $this->logOnly & $type; if ( $type & \error_reporting() || $logOnly ) { $this->error = [ 'type' => $type, 'message' => $message, 'file' => $file, 'line' => $line, 'trace' => \debug_backtrace(0), ]; $this->log($this->error); if ($logOnly) { $this->error = []; \error_clear_last(); } else { exit(1); } } return true; } /** * Обрабатывает не перехваченные исключения */ public function exceptionHandler(Throwable $e): void { $this->error = [ 'type' => 0, 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'trace' => $e->getTrace(), 'exception' => $e, ]; $this->log($this->error); } /** * Окончательно обрабатывает ошибки (в том числе фатальные) и исключения */ public function shutdownHandler(): void { $show = false; if (isset($this->error['type'])) { $show = true; } elseif ( \is_array($this->error = \error_get_last()) && $this->error['type'] & \error_reporting() ) { $show = true; $this->log($this->error); } while (\ob_get_level() > $this->obLevel) { \ob_end_clean(); } if (\ob_get_level() === $this->obLevel) { if ($show) { \ob_end_clean(); $this->show($this->error); } else { \ob_end_flush(); } } } /** * Отправляет сообщение в лог */ protected function log(array $error): void { $useErrLog = true; try { if ( $this->c instanceof Container && ! $this->c->Log instanceof NullLogger ) { $context = []; $method = $this->type[$error['type']][1] ?? $this->type[0][1]; if (isset($error['exception'])) { $context['exception'] = $error['exception']; } $context['headers'] = false; $this->c->Log->{$method}($this->message($error), $context); $useErrLog = false; } } catch (Throwable $e) { \error_log($this->message([ 'type' => 0, 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'trace' => $e->getTrace(), 'exception' => $e, ], true)); } if ($useErrLog) { \error_log($this->message($error, true)); } } /** * Выводит сообщение об ошибке * * @param array $error */ protected function show(array $error): void { \header('HTTP/1.1 500 Internal Server Error'); \header('Content-Type: text/html; charset=utf-8'); echo <<<'EOT' 500 Internal Server Error EOT; if (\filter_var(\ini_get('display_errors'), \FILTER_VALIDATE_BOOL)) { echo '

' . $this->e($this->message($error)) . '

'; if ( isset($error['trace']) && \is_array($error['trace']) ) { echo '

Trace:

    '; foreach ($error['trace'] as $cur) { if ( isset($cur['file'], $cur['line'], $error['file'], $error['line']) && (int) $error['line'] === (int) $cur['line'] && $error['file'] === $cur['file'] ) { continue; } $line = $cur['file'] ?? '-'; $line .= '(' . ($cur['line'] ?? '-') . '): '; if (isset($cur['class'])) { $line .= $cur['class'] . $cur['type']; } $line .= ($cur['function'] ?? 'unknown') . '('; if ( ! empty($cur['args']) && \is_array($cur['args']) ) { $comma = ''; foreach ($cur['args'] as $arg) { $type = \gettype($arg); switch ($type) { case 'boolean': $type = $arg ? 'true' : 'false'; break; case 'array': $type .= '(' . \count($arg) . ')'; break; case 'resource': $type = \get_resource_type($arg); break; case 'object': $type .= '{' . \get_class($arg) . '}'; break; } $line .= $comma . $type; $comma = ', '; } } $line .= ')'; $line = $this->e(\str_replace($this->hidePath, '...', $line)); echo "
  1. {$line}
  2. "; } echo '
'; } } else { echo '

Server is tired :(

'; } echo <<<'EOT' EOT; } /** * Формирует сообщение */ protected function message(array $error, bool $useException = false): string { $type = $this->type[$error['type']][0] ?? $this->type[0][0]; if ( $useException && isset($error['exception']) && $error['exception'] instanceof Throwable ) { $result = "PHP {$type}: {$error['exception']}"; } else { $result = "PHP {$type}: {$error['message']} in {$error['file']}:[{$error['line']}]"; } return \preg_replace('%[\x00-\x1F]%', ' ', \str_replace($this->hidePath, '...', $result)); } /** * Экранирует спецсимволов HTML-сущностями */ protected function e(string $arg): string { return \htmlspecialchars($arg, \ENT_HTML5 | \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); } }