diff --git a/inc/error.php b/inc/error.php index 4c8eb19e..fed211ac 100644 --- a/inc/error.php +++ b/inc/error.php @@ -1,9 +1,222 @@ 0); + } + + private static function errnoToLogLevel(int $errno): int { + switch ($errno) { + default: + case \E_ERROR: + case \E_PARSE: + case \E_CORE_ERROR: + case \E_COMPILE_ERROR: + case \E_RECOVERABLE_ERROR: + return \LOG_EMERG; + case \E_USER_ERROR: + return \LOG_ERR; + case \E_WARNING: + case \E_CORE_WARNING: + case \E_COMPILE_WARNING: + case \E_USER_WARNING: + return \LOG_WARNING; + case \E_NOTICE: + case \E_DEPRECATED: + case \E_USER_NOTICE: + case \E_USER_DEPRECATED: + return \LOG_NOTICE; + } + } + + public static function handle_error( + int $level, + string $message, + callable $logger, + bool $display_details, + 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) { + global $config; + + if (isset($config['syslog']) && $config['syslog']) { + _syslog($level, $message); + } else { + \error_log($message); + } + } + + 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; + } + + public function setLogger(?callable $logger_fn) { + $this->logger_fn = $logger_fn !== null ? $logger_fn : [ __CLASS__, 'default_logger' ]; + } + + public function installGlobally() { + $logger_fn = $this->logger_fn; + $include_details = $this->include_details || self::isCli(); + + \set_error_handler(function($errno, $errstr, $errfile, $errline) use ($logger_fn, $include_details) { + $errno = \error_reporting() & $errno; + if ($errno !== 0) { + $level = self::errnoToLogLevel($errno); + + // https://stackoverflow.com/a/35930682 + $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; + }); + + \set_exception_handler(function($exception) use ($logger_fn, $include_details) { + $message = $exception->getMessage(); + $file = $exception->getFile(); + $line = $exception->getLine(); + $trace_str = $exception->getTraceAsString(); + $trace = $exception->getTrace(); + + self::handle_error(\LOG_EMERG, $message, $logger_fn, $include_details, $file, $line, $trace_str, $trace); + }); + + \register_shutdown_function(function() use ($logger_fn, $include_details) { + $error = \error_get_last(); + if ($error !== null) { + $level = self::errnoToLogLevel($error['type']); + $message = $error['message']; + $file = $error['file']; + $line = $error['line']; + + self::handle_error($level, $message, $logger_fn, $include_details, $file, $line, '', []); + } + }); + } +} + +/** + * Install a fancy error handler with the given logger. + * + * @param ?callable $logger Called when there is to log something. Use null to use the minimal default implementation. + * The callable is invoke with two parameters, the first being the log level (integer), as + * listed by the syslog constants, the second is the log message (string). + * Consider that $config may not have been initialized yet. + */ +function install_error_handler(bool $basic, ?callable $logger_fn): void { + global $config; + + $logger_fn ??= function($level, $message) use ($config) { + if (isset($config['syslog']) && $config['syslog']) { + _syslog($level, $message); + } else { + \error_log($message); + } + }; + + if (!$basic) { + \set_error_handler(function($errno, $errstr, $errfile, $errline) use ($config) { + if (\error_reporting() & $errno) { + $config['debug'] = true; + \error("$errstr in $errfile at line $errline"); + } + return false; + }); } return false; } @@ -87,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))) )) ])); }