diff --git a/inc/error.php b/inc/error.php index 7ebf6296..fed211ac 100644 --- a/inc/error.php +++ b/inc/error.php @@ -2,12 +2,20 @@ namespace Vichan; class ErrorHandler { + private static array|false $error_recursion = false; + private bool $include_details; /** * @var callable */ private mixed $logger_fn; + + private static function isCli(): bool { + return defined('STDIN') || \PHP_SAPI === 'cli' || (\stristr(\PHP_SAPI, 'cgi') && \getenv('TERM')) + || (empty($_SERVER['REMOTE_ADDR']) && !isset($_SERVER['HTTP_USER_AGENT']) && \count($_SERVER['argv']) > 0); + } + private static function errnoToLogLevel(int $errno): int { switch ($errno) { default: @@ -16,9 +24,9 @@ class ErrorHandler { case \E_CORE_ERROR: case \E_COMPILE_ERROR: case \E_RECOVERABLE_ERROR: - return \LOG_EMERG; + return \LOG_EMERG; case \E_USER_ERROR: - return \LOG_ERR; + return \LOG_ERR; case \E_WARNING: case \E_CORE_WARNING: case \E_COMPILE_WARNING: @@ -37,11 +45,84 @@ class ErrorHandler { string $message, callable $logger, bool $display_details, - ?string $file, - ?int $line, - ?string $trace, + string $file, + int $line, + string $trace_str, + array $trace ): void { + global $board, $mod, $config, $db_error; + $newline = self::isCli() ? '\n' : '
'; + + if (self::$error_recursion !== false) { + list($o_message, $o_file, $o_line, $o_trace) = self::$error_recursion; + if ($display_details) { + echo("Error recursion detected with $message at $file:$line. Backtrace:$newline$trace_str{$newline}Original error: $o_message at $o_file:$o_line. Original backtrace:$newline$o_trace"); + } else { + echo("Error recursion detected with $message{$newline}Original error: $o_message"); + } + exit; + } + + self::$error_recursion = [ $message, $file, $line, $trace_str ]; + + // Message for the system, be it cli or error logging facility. + $sys_msg = "Error: $message at $file:$line.\nBacktrace: $trace_str\n"; + + if (self::isCli()) { + // Running from CLI. Always display the details. + echo($sys_msg); + exit; + } + + // Log the error if we aren't running from cli. + $logger($level, $sys_msg); + + // Recycled code. + $debug_stuff = []; + if ($config['debug']) { + if (isset($db_error)) { + $debug_stuff = \array_combine([ 'SQLSTATE', 'Error code', 'Error message' ], $db_error); + } + $debug_stuff['backtrace'] = $trace; + + // Remove the password from SQL errors. + $pw = $config['db']['password']; + $debug_callback = function($item) use (&$debug_callback, $pw) { + if (\is_array($item)) { + $item = \array_filter($item, $debug_callback); + } + return $item !== $pw || !$pw; + }; + $debug_stuff = \array_map($debug_callback, $debug_stuff); + } + + \header("{$_SERVER['SERVER_PROTOCOL']} 500 Internal Server Error"); + + // Error via json. + if (isset($_POST['json_response'])) { + \header('Content-Type: text/json; charset=utf-8'); + $data = [ 'error' => $message ]; + if (!empty($config) && $config['debug'] && !empty($debug_stuff)) { + $data['debug'] = $debug_stuff; + } + echo(\json_encode($data)); + exit; + } + + // Pretty print. + die(Element('page.html', [ + 'config' => $config, + 'title' => _('Error'), + 'subtitle' => _('An error has occured.'), + 'body' => Element('error.html', [ + 'config' => $config, + 'message' => $message, + 'mod' => $mod, + 'board' => isset($board) ? $board : false, + 'debug' => \str_replace("\n", ' ', utf8tohtml(print_r($debug_stuff, true))) + ]) + ])); } private function default_logger(int $level, string $message) { @@ -54,9 +135,13 @@ class ErrorHandler { } } + public function __construct() { + $this->include_details = self::isCli(); + $this->logger_fn = [ __CLASS__, 'default_logger' ]; + } + public function setIncludeDetails(bool $include_details) { $this->include_details = $include_details; - $this->logger_fn = [ __CLASS__, 'default_logger' ]; } public function setLogger(?callable $logger_fn) { @@ -65,7 +150,7 @@ class ErrorHandler { public function installGlobally() { $logger_fn = $this->logger_fn; - $include_details = $this->include_details; + $include_details = $this->include_details || self::isCli(); \set_error_handler(function($errno, $errstr, $errfile, $errline) use ($logger_fn, $include_details) { $errno = \error_reporting() & $errno; @@ -73,8 +158,10 @@ class ErrorHandler { $level = self::errnoToLogLevel($errno); // https://stackoverflow.com/a/35930682 - $trace = (new \Exception)->getTraceAsString(); - self::handle_error($level, $errstr, $logger_fn, $include_details, $errfile, $errline, $trace); + $e = new \Exception(); + $trace_str = $e->getTraceAsString(); + $trace = $e->getTrace(); + self::handle_error($level, $errstr, $logger_fn, $include_details, $errfile, $errline, $trace_str, $trace); } return false; }); @@ -83,9 +170,10 @@ class ErrorHandler { $message = $exception->getMessage(); $file = $exception->getFile(); $line = $exception->getLine(); - $trace = $exception->getTraceAsString(); + $trace_str = $exception->getTraceAsString(); + $trace = $exception->getTrace(); - self::handle_error(\LOG_EMERG, $message, $logger_fn, $include_details, $file, $line, $trace); + self::handle_error(\LOG_EMERG, $message, $logger_fn, $include_details, $file, $line, $trace_str, $trace); }); \register_shutdown_function(function() use ($logger_fn, $include_details) { @@ -96,7 +184,7 @@ class ErrorHandler { $file = $error['file']; $line = $error['line']; - self::handle_error($level, $message, $logger_fn, $include_details, $file, $line, null); + self::handle_error($level, $message, $logger_fn, $include_details, $file, $line, '', []); } }); } @@ -212,7 +300,7 @@ function error($message, $priority = true, $debug_stuff = false) { 'message' => $message, 'mod' => $mod, 'board' => isset($board) ? $board : false, - 'debug' => str_replace("\n", ' ', utf8tohtml(print_r($debug_stuff, true))) + 'debug' => \str_replace("\n", ' ', utf8tohtml(\print_r($debug_stuff, true))) )) ])); }