* @copyright 2025-2026 Vateron Media * @link https://github.com/Vateron-Media/XC_VM * @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html */ final class Logger { /** @var bool Whether errors should be displayed on screen */ private static bool $showErrors = false; /** @var string Full path to the log file */ private static string $logFile; /** * Initializes error, exception, and fatal error handlers. * * @param bool $showErrors true to display errors, false to hide UI output * @param string $logFile Full path to the file where logs will be written */ public static function init(bool $showErrors, string $logFile): void { self::$showErrors = $showErrors; self::$logFile = $logFile; // Set custom handlers set_error_handler([self::class, 'handleError']); set_exception_handler([self::class, 'handleException']); register_shutdown_function([self::class, 'handleFatal']); // Always collect all errors for logging; UI visibility is controlled separately. error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED); if ($showErrors) { ini_set('display_errors', '1'); ini_set('display_startup_errors', '1'); } else { ini_set('display_errors', '0'); ini_set('display_startup_errors', '0'); } } /* ================= ERRORS ================= */ /** * Handler for regular PHP errors (trigger_error, warnings, notices, etc.). * * @param int $errno Error level (E_WARNING, E_NOTICE, etc.) * @param string $message Error message * @param string $file File where the error occurred * @param int $line Line number in the file * * @return bool true if the error was handled (prevents default PHP handler) */ public static function handleError( int $errno, string $message, string $file, int $line ): bool { // Ignore suppressed errors (using @ operator) if (!(error_reporting() & $errno)) { return false; } self::log( self::mapErrorLevel($errno), $message, self::buildTrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)), $file, $line ); return true; } /* ================= EXCEPTIONS ================= */ /** * Handler for uncaught exceptions. * * @param Throwable $e The uncaught exception */ public static function handleException(Throwable $e): void { self::log( 'EXCEPTION', $e->getMessage(), self::buildExceptionTrace($e), $e->getFile(), $e->getLine() ); } /* ================= FATAL ================= */ /** * Handler for fatal errors (E_ERROR, E_PARSE, etc.) that terminate script execution. * Called via register_shutdown_function. */ public static function handleFatal(): void { $error = error_get_last(); // Process only true fatal errors if ($error && in_array($error['type'], [ E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR ], true)) { self::log( 'FATAL', $error['message'], '', // Stack trace is not available for fatal errors $error['file'], $error['line'] ); } } /* ================= CORE LOG ================= */ /** * Core logging method. Writes the event to the log file and optionally outputs it on screen. * * @param string $type Event type (ERROR, WARNING, NOTICE, EXCEPTION, FATAL, etc.) * @param string $message Message text * @param string $trace Stack trace (optional) * @param string $file File where the event occurred * @param int $line Line number */ public static function log( string $type, string $message, string $trace = '', string $file = '', int $line = 0 ): void { $data = [ 'type' => $type, 'log_message' => $message, 'file' => $file, 'line' => $line, 'log_extra' => $trace, // Process ID 'time' => time(), // Unix timestamp 'env' => php_sapi_name() // SAPI name (cli, fpm-fcgi, etc.) ]; // Ensure log directory exists $logDir = dirname(self::$logFile); if (!is_dir($logDir)) { @mkdir($logDir, 0775, true); } // Write to file as base64-encoded JSON (easy to parse and prevents line corruption) file_put_contents( self::$logFile, base64_encode(json_encode($data, JSON_UNESCAPED_UNICODE)) . "\n", FILE_APPEND | LOCK_EX ); // Set log file permissions if running as root (common in containers) if (function_exists('posix_geteuid') && posix_geteuid() === 0) { @chown(self::$logFile, 'xc_vm'); @chgrp(self::$logFile, 'xc_vm'); @chmod(self::$logFile, 0664); } // If UI output is enabled, display a readable message on screen if (self::$showErrors) { self::output($data); } } /* ================= TRACE ================= */ /** * Builds a stack trace for an exception chain (including previous exceptions). * * @param Throwable $e The exception * * @return string Formatted trace string */ private static function buildExceptionTrace(Throwable $e): string { $out = []; do { // Main exception info $out[] = sprintf( "%s: %s in %s:%d", get_class($e), $e->getMessage(), $e->getFile(), $e->getLine() ); // Call stack for the current exception foreach ($e->getTrace() as $i => $t) { $out[] = sprintf( "#%d %s(%s): %s()", $i, $t['file'] ?? '[internal]', $t['line'] ?? '?', $t['function'] ); } $e = $e->getPrevious(); if ($e) { $out[] = "---- CAUSED BY ----"; } } while ($e); return implode("\n", $out); } /** * Builds a stack trace from the array returned by debug_backtrace(). * * @param array $trace Backtrace array * * @return string Formatted trace string */ private static function buildTrace(array $trace): string { $out = []; foreach ($trace as $i => $t) { $out[] = sprintf( "#%d %s(%s): %s()", $i, $t['file'] ?? '[internal]', $t['line'] ?? '?', $t['function'] ); } return implode("\n", $out); } /* ================= HELPERS ================= */ /** * Maps a PHP error constant to a human-readable string. * * @param int $errno Error level constant * * @return string Error type (ERROR, WARNING, NOTICE, INFO) */ private static function mapErrorLevel(int $errno): string { return match ($errno) { E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR => 'ERROR', E_WARNING, E_USER_WARNING => 'WARNING', E_NOTICE, E_USER_NOTICE => 'NOTICE', default => 'INFO', }; } /** * Outputs error/exception information to the screen in a readable format. * Format depends on the environment (CLI or Web). * * @param array $d Event data (from the log method) */ private static function output(array $d): void { if (php_sapi_name() === 'cli') { // Color scheme for terminal $color = match ($d['type']) { 'FATAL', 'ERROR' => "\033[41m\033[97m", // red background, white text 'WARNING' => "\033[43m\033[30m", // yellow background, black text 'NOTICE' => "\033[44m\033[97m", // blue background, white text default => "\033[45m\033[97m", // magenta background, white text }; echo "\n{$color} {$d['type']} \033[0m "; echo date('Y-m-d H:i:s', $d['time']) . "\n"; echo "{$d['log_message']}\n"; echo "{$d['file']}:{$d['line']}\n"; if (!empty($d['log_extra'])) { echo str_repeat('-', 60) . "\n"; echo $d['log_extra'] . "\n"; } } else { // Web environment output echo "
"; echo "{$d['type']} "; echo "" . date('Y-m-d H:i:s', $d['time']) . "
"; echo htmlspecialchars($d['log_message'], ENT_SUBSTITUTE) . "
"; echo "{$d['file']}:{$d['line']}"; if (!empty($d['log_extra'])) { echo "
";
                echo htmlspecialchars($d['log_extra'], ENT_SUBSTITUTE);
                echo "
"; } echo "
"; } } }