diff --git a/i18n/default_de.json b/i18n/default_de.json index ef886fd..f300dca 100644 --- a/i18n/default_de.json +++ b/i18n/default_de.json @@ -218,6 +218,44 @@ "Failed to send access emails": "Zugangsdaten konnten nicht gesendet werden", "Read": "Gelesen", "Home": "Home", + "Exception": "Ausnahme", + "An error occurred": "Ein Fehler ist aufgetreten", + "in %s on line %s": "in %s in Zeile %s", + "Copy request ID": "Request-ID kopieren", + "Error details": "Fehlerdetails", + "Request": "Anfrage", + "Queries": "Abfragen", + "An unexpected error occurred. If the problem persists, please contact support with the reference below.": "Ein unerwarteter Fehler ist aufgetreten. Wenn das Problem weiterhin besteht, kontaktiere bitte den Support mit der untenstehenden Referenz.", + "Stack Trace": "Stack-Trace", + "Caused by": "Verursacht durch", + "URL": "URL", + "User Agent": "User-Agent", + "Headers": "Header", + "GET Parameters": "GET-Parameter", + "POST Parameters": "POST-Parameter", + "Session (keys + types)": "Session (Schlüssel + Typen)", + "Context": "Kontext", + "PHP": "PHP", + "SAPI": "SAPI", + "Memory (current)": "Speicher (aktuell)", + "Memory (peak)": "Speicher (maximal)", + "Application": "Anwendung", + "Active Modules": "Aktive Module", + "Loaded Extensions (%d)": "Geladene Erweiterungen (%d)", + "Query tracking is not available (Debugger disabled or not initialized).": "Query-Tracking ist nicht verfügbar (Debugger deaktiviert oder nicht initialisiert).", + "No queries were executed before the error occurred.": "Vor dem Fehler wurden keine Abfragen ausgeführt.", + "Queries (%d)": "Abfragen (%d)", + "Bad Request": "Ungültige Anfrage", + "Unauthorized": "Nicht autorisiert", + "Forbidden": "Verboten", + "Not Found": "Nicht gefunden", + "Method Not Allowed": "Methode nicht erlaubt", + "Request Timeout": "Zeitüberschreitung der Anfrage", + "Unprocessable Entity": "Nicht verarbeitbare Anfrage", + "Too Many Requests": "Zu viele Anfragen", + "Internal Server Error": "Interner Serverfehler", + "Bad Gateway": "Fehlerhaftes Gateway", + "Service Unavailable": "Dienst nicht verfügbar", "Users": "Benutzer", "All users": "Alle Benutzer", "Avatar": "Avatar", diff --git a/i18n/default_en.json b/i18n/default_en.json index 2f6b342..0725ca4 100644 --- a/i18n/default_en.json +++ b/i18n/default_en.json @@ -218,6 +218,44 @@ "Failed to send access emails": "Failed to send access emails", "Read": "Read", "Home": "Home", + "Exception": "Exception", + "An error occurred": "An error occurred", + "in %s on line %s": "in %s on line %s", + "Copy request ID": "Copy request ID", + "Error details": "Error details", + "Request": "Request", + "Queries": "Queries", + "An unexpected error occurred. If the problem persists, please contact support with the reference below.": "An unexpected error occurred. If the problem persists, please contact support with the reference below.", + "Stack Trace": "Stack Trace", + "Caused by": "Caused by", + "URL": "URL", + "User Agent": "User Agent", + "Headers": "Headers", + "GET Parameters": "GET Parameters", + "POST Parameters": "POST Parameters", + "Session (keys + types)": "Session (keys + types)", + "Context": "Context", + "PHP": "PHP", + "SAPI": "SAPI", + "Memory (current)": "Memory (current)", + "Memory (peak)": "Memory (peak)", + "Application": "Application", + "Active Modules": "Active Modules", + "Loaded Extensions (%d)": "Loaded Extensions (%d)", + "Query tracking is not available (Debugger disabled or not initialized).": "Query tracking is not available (Debugger disabled or not initialized).", + "No queries were executed before the error occurred.": "No queries were executed before the error occurred.", + "Queries (%d)": "Queries (%d)", + "Bad Request": "Bad Request", + "Unauthorized": "Unauthorized", + "Forbidden": "Forbidden", + "Not Found": "Not Found", + "Method Not Allowed": "Method Not Allowed", + "Request Timeout": "Request Timeout", + "Unprocessable Entity": "Unprocessable Entity", + "Too Many Requests": "Too Many Requests", + "Internal Server Error": "Internal Server Error", + "Bad Gateway": "Bad Gateway", + "Service Unavailable": "Service Unavailable", "Users": "Users", "All users": "All users", "Avatar": "Avatar", diff --git a/lib/Http/ErrorHandler.php b/lib/Http/ErrorHandler.php index dc804d1..c146b73 100644 --- a/lib/Http/ErrorHandler.php +++ b/lib/Http/ErrorHandler.php @@ -40,8 +40,10 @@ final class ErrorHandler self::$reservedMemory = null; // Discard any partial output from the failed request. - while (ob_get_level() > 0) { - ob_end_clean(); + if (!self::isPhpUnitRuntime()) { + while (ob_get_level() > 0) { + ob_end_clean(); + } } // Set HTTP 500 if no error status has been set yet. @@ -187,4 +189,9 @@ final class ErrorHandler echo "\n" . $exception->getMessage() . "\n" . $exception->getTraceAsString() . "\n"; } } + + private static function isPhpUnitRuntime(): bool + { + return defined('PHPUNIT_COMPOSER_INSTALL') || defined('__PHPUNIT_PHAR__'); + } } diff --git a/lib/Http/ErrorLogger.php b/lib/Http/ErrorLogger.php index 5c6d10f..7fc210f 100644 --- a/lib/Http/ErrorLogger.php +++ b/lib/Http/ErrorLogger.php @@ -7,6 +7,7 @@ final class ErrorLogger private const LOG_DIR = 'storage/logs/errors'; private const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB private const MAX_ROTATED_FILES = 5; + private const REDACTION_MARKER = '[REDACTED]'; public static function log(\Throwable $exception, array $collectedData): void { @@ -77,6 +78,7 @@ final class ErrorLogger $exceptionData = $collectedData['exception'] ?? []; $requestData = $collectedData['request'] ?? []; $envData = $collectedData['environment'] ?? []; + $exceptionMessage = (string) ($exceptionData['message'] ?? $exception->getMessage()); $traceSummary = ''; $frames = $exceptionData['trace'] ?? []; @@ -93,7 +95,8 @@ final class ErrorLogger 'channel' => $requestData['channel'] ?? 'web', 'exception' => [ 'class' => $exceptionData['class'] ?? get_class($exception), - 'message' => $exceptionData['message'] ?? $exception->getMessage(), + 'message' => self::REDACTION_MARKER, + 'message_hash' => self::hashMessage($exceptionMessage), 'code' => $exceptionData['code'] ?? $exception->getCode(), 'file' => self::shortenPath($exceptionData['file'] ?? $exception->getFile()), 'line' => $exceptionData['line'] ?? $exception->getLine(), @@ -101,7 +104,7 @@ final class ErrorLogger 'trace_summary' => trim($traceSummary), 'request' => [ 'method' => $requestData['method'] ?? null, - 'url' => $requestData['url'] ?? null, + 'path' => self::normalizeRequestPath($requestData['url'] ?? null), 'ip_hash' => self::hashIp($requestData['ip'] ?? null), 'user_id' => $requestData['user_id'] ?? null, 'tenant_id' => $requestData['tenant_id'] ?? null, @@ -120,6 +123,40 @@ final class ErrorLogger return 'sha256:' . hash('sha256', $ip); } + private static function hashMessage(string $message): ?string + { + $message = trim($message); + if ($message === '') { + return null; + } + + return 'sha256:' . hash('sha256', $message); + } + + private static function normalizeRequestPath(mixed $rawUrl): ?string + { + if (!is_string($rawUrl) || trim($rawUrl) === '') { + return null; + } + + $path = parse_url($rawUrl, PHP_URL_PATH); + if (!is_string($path) || trim($path) === '') { + return '/'; + } + + $path = preg_replace('/[[:cntrl:]]+/', '', $path) ?? ''; + $path = trim($path); + if ($path === '') { + return '/'; + } + + if (!str_starts_with($path, '/')) { + $path = '/' . $path; + } + + return substr($path, 0, 500); + } + private static function shortenPath(string $path): string { $root = getcwd(); diff --git a/lib/Http/ErrorRenderer.php b/lib/Http/ErrorRenderer.php index 5bffae8..88faddf 100644 --- a/lib/Http/ErrorRenderer.php +++ b/lib/Http/ErrorRenderer.php @@ -2,6 +2,8 @@ namespace MintyPHP\Http; +use MintyPHP\I18n; + final class ErrorRenderer { /** @@ -24,23 +26,37 @@ final class ErrorRenderer $css = self::loadAsset(__DIR__ . '/../../web/css/pages/app-error-debug.css'); $js = self::loadAsset(__DIR__ . '/../../web/js/components/app-error-debug.js'); - $excClass = self::esc($exc['class'] ?? 'Exception'); - $excMessage = self::esc($exc['message'] ?? 'An error occurred'); - $excFile = self::esc(self::shortenPath($exc['file'] ?? '')); - $excLine = self::esc((string) ($exc['line'] ?? '?')); + $excClass = self::esc($exc['class'] ?? self::tr('Exception')); + $excMessage = self::esc((string) ($exc['message'] ?? self::tr('An error occurred'))); + $excLocation = self::esc(self::tr( + 'in %s on line %s', + self::shortenPath((string) ($exc['file'] ?? '')), + (string) ($exc['line'] ?? '?') + )); $exceptionPanel = self::renderExceptionPanel($exc); $requestPanel = self::renderRequestPanel($req); $environmentPanel = self::renderEnvironmentPanel($env); $queryPanel = self::renderQueryPanel($qry); + $lang = self::esc(self::htmlLang()); + $statusTextEsc = self::esc($statusText); + $requestIdLabel = self::esc(self::tr('Request ID')); + $copyRequestIdTitle = self::esc(self::tr('Copy request ID')); + $tabsLabel = self::esc(self::tr('Error details')); + + $tabException = self::esc(self::tr('Exception')); + $tabRequest = self::esc(self::tr('Request')); + $tabEnvironment = self::esc(self::tr('Environment')); + $tabQueries = self::esc(self::tr('Queries')); + return << - +
-