isApiRequest(); if ($isApiRequest) { return $this->createApiResponse($exception, $contextProvider); } return $this->createHtmlResponse($exception, $contextProvider); } /** * Create JSON API error response */ private function createApiResponse( \Throwable $exception, ?ExceptionContextProvider $contextProvider ): HttpResponse { $statusCode = $this->getHttpStatusCode($exception); $stackTrace = StackTrace::fromThrowable($exception); // Get user-friendly message if translator is available $context = $contextProvider?->get($exception); $userMessage = $this->messageTranslator?->translate($exception, $context) ?? new \App\Framework\ExceptionHandling\Translation\UserFriendlyMessage( message: $this->isDebugMode ? $exception->getMessage() : 'An error occurred while processing your request.' ); $errorData = [ 'error' => [ 'message' => $userMessage->message, 'type' => $this->isDebugMode ? $this->getShortClassName(get_class($exception)) : 'ServerError', 'code' => $exception->getCode(), ] ]; if ($userMessage->title !== null) { $errorData['error']['title'] = $userMessage->title; } if ($userMessage->helpText !== null) { $errorData['error']['help'] = $userMessage->helpText; } // Add debug information if enabled if ($this->isDebugMode) { $errorData['error']['file'] = $exception->getFile(); $errorData['error']['line'] = $exception->getLine(); $errorData['error']['trace'] = $stackTrace->toArray(); $errorData['error']['trace_short'] = $stackTrace->formatShort(); // Add context from WeakMap if available if ($contextProvider !== null) { $context = $contextProvider->get($exception); if ($context !== null) { $errorData['context'] = [ 'operation' => $context->operation, 'component' => $context->component, 'request_id' => $context->requestId, 'occurred_at' => $context->occurredAt?->format('Y-m-d H:i:s'), ]; } } } $body = json_encode($errorData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); return new HttpResponse( status: Status::from($statusCode), body: $body, headers: new Headers([ 'Content-Type' => 'application/json', 'X-Content-Type-Options' => 'nosniff', ]) ); } /** * Create HTML error page response using Template System */ private function createHtmlResponse( \Throwable $exception, ?ExceptionContextProvider $contextProvider ): HttpResponse { $statusCode = $this->getHttpStatusCode($exception); // Try to render using template system $html = $this->renderWithTemplate($exception, $contextProvider, $statusCode); // Fallback to simple HTML if template rendering fails if ($html === null) { $html = $this->generateFallbackHtml($exception, $contextProvider, $statusCode); } return new HttpResponse( status: Status::from($statusCode), body: $html, headers: new Headers([ 'Content-Type' => 'text/html; charset=utf-8', 'X-Content-Type-Options' => 'nosniff', ]) ); } /** * Render error page using Template System * * @return string|null Rendered HTML or null if template not found or rendering failed */ private function renderWithTemplate( \Throwable $exception, ?ExceptionContextProvider $contextProvider, int $statusCode ): ?string { try { // Determine template name based on status code $templateName = $this->getTemplateName($statusCode); // Prepare template data $templateData = $this->prepareTemplateData($exception, $contextProvider, $statusCode); // Create RenderContext $renderContext = new RenderContext( template: $templateName, metaData: new MetaData($this->getErrorTitle($statusCode)), data: $templateData, processingMode: ProcessingMode::FULL ); // Render template return $this->engine->render($renderContext); } catch (\Throwable $e) { // Template not found or rendering failed - log error and return null for fallback error_log(sprintf( 'Failed to render error template "%s" for status %d: %s in %s:%d', $templateName ?? 'unknown', $statusCode, $e->getMessage(), $e->getFile(), $e->getLine() )); // Return null to trigger fallback HTML generation return null; } } /** * Get template name based on status code */ private function getTemplateName(int $statusCode): string { return match ($statusCode) { 403 => 'errors/403', 404 => 'errors/404', 500 => 'errors/500', default => 'errors/error', }; } /** * Prepare template data for rendering * * @return array */ private function prepareTemplateData( \Throwable $exception, ?ExceptionContextProvider $contextProvider, int $statusCode ): array { $data = [ 'statusCode' => $statusCode, 'title' => $this->getErrorTitle($statusCode), 'message' => $this->isDebugMode ? $exception->getMessage() : 'An error occurred while processing your request.', 'exceptionClass' => $this->getShortClassName(get_class($exception)), 'isDebugMode' => $this->isDebugMode, ]; // Add debug information if enabled if ($this->isDebugMode) { $stackTrace = StackTrace::fromThrowable($exception); $data['debug'] = [ 'file' => $exception->getFile(), 'line' => $exception->getLine(), 'trace' => $stackTrace->formatForHtml(), 'trace_short' => $stackTrace->formatShort(), 'trace_frames' => $stackTrace->toArray(), ]; // Add context from WeakMap if available if ($contextProvider !== null) { $context = $contextProvider->get($exception); if ($context !== null) { $data['context'] = [ 'operation' => $context->operation, 'component' => $context->component, 'request_id' => $context->requestId, 'occurred_at' => $context->occurredAt?->format('Y-m-d H:i:s'), ]; } } } return $data; } /** * Generate fallback HTML error page (when template not found) */ private function generateFallbackHtml( \Throwable $exception, ?ExceptionContextProvider $contextProvider, int $statusCode ): string { // HTML-encode all variables for security $title = htmlspecialchars($this->getErrorTitle($statusCode), ENT_QUOTES | ENT_HTML5, 'UTF-8'); $message = htmlspecialchars( $this->isDebugMode ? $exception->getMessage() : 'An error occurred while processing your request.', ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $debugInfo = ''; if ($this->isDebugMode) { $debugInfo = $this->generateDebugSection($exception, $contextProvider); } return << {$title}

{$title}

{$message}

{$debugInfo}
HTML; } /** * Generate debug information section */ private function generateDebugSection( \Throwable $exception, ?ExceptionContextProvider $contextProvider ): string { // HTML-encode all debug information for security $exceptionClass = htmlspecialchars($this->getShortClassName(get_class($exception)), ENT_QUOTES | ENT_HTML5, 'UTF-8'); $file = htmlspecialchars($exception->getFile(), ENT_QUOTES | ENT_HTML5, 'UTF-8'); $line = $exception->getLine(); $stackTrace = StackTrace::fromThrowable($exception); $trace = $stackTrace->formatForHtml(); // Already HTML-encoded in formatForHtml $traceShort = htmlspecialchars($stackTrace->formatShort(), ENT_QUOTES | ENT_HTML5, 'UTF-8'); $tracePlain = htmlspecialchars($stackTrace->formatForConsole(), ENT_QUOTES | ENT_HTML5, 'UTF-8'); $renderedFrames = $this->renderStackTraceList($stackTrace); $contextHtml = ''; if ($contextProvider !== null) { $context = $contextProvider->get($exception); if ($context !== null) { $operation = htmlspecialchars($context->operation ?? '', ENT_QUOTES | ENT_HTML5, 'UTF-8'); $component = htmlspecialchars($context->component ?? '', ENT_QUOTES | ENT_HTML5, 'UTF-8'); $requestId = htmlspecialchars($context->requestId ?? '', ENT_QUOTES | ENT_HTML5, 'UTF-8'); $occurredAt = $context->occurredAt?->format('Y-m-d H:i:s') ?? ''; $contextHtml = << Operation: {$operation}
Component: {$component}
Request ID: {$requestId}
Occurred At: {$occurredAt}
HTML; } } // Code-Ausschnitt für die Exception-Zeile $codeSnippet = $this->getCodeSnippet($exception->getFile(), $line); return <<

Debug Information

Exception: {$exceptionClass}
File: {$file}:{$line}
Short trace: {$traceShort}
{$contextHtml} {$codeSnippet}

Stack Trace

{$renderedFrames}
{$trace}
HTML; } /** * Determine if current request is API request */ private function isApiRequest(): bool { // Check for JSON Accept header $acceptHeader = $_SERVER['HTTP_ACCEPT'] ?? ''; if (str_contains($acceptHeader, 'application/json')) { return true; } // Check for API path prefix $requestUri = $_SERVER['REQUEST_URI'] ?? ''; if (str_starts_with($requestUri, '/api/')) { return true; } // Check for AJAX requests $requestedWith = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? ''; if (strtolower($requestedWith) === 'xmlhttprequest') { return true; } return false; } /** * Get HTTP status code from exception */ private function getHttpStatusCode(\Throwable $exception): int { // Map security exceptions to 403 Forbidden // Check for specific security exceptions first if ($exception instanceof \App\Framework\Exception\Security\CsrfValidationFailedException || $exception instanceof \App\Framework\Exception\Security\HoneypotTriggeredException || $exception instanceof \App\Framework\Exception\SecurityException) { return 403; } // Use exception code if it's a valid HTTP status code $code = $exception->getCode(); if ($code >= 400 && $code < 600) { return $code; } // Map common exceptions to status codes return match (true) { $exception instanceof \InvalidArgumentException => 400, $exception instanceof \RuntimeException => 500, $exception instanceof \LogicException => 500, default => 500, }; } /** * Get user-friendly error title from status code */ private function getErrorTitle(int $statusCode): string { return match ($statusCode) { 400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 429 => 'Too Many Requests', 500 => 'Internal Server Error', 502 => 'Bad Gateway', 503 => 'Service Unavailable', default => "Error {$statusCode}", }; } /** * Gibt Klassennamen ohne Namespace zurück */ private function getShortClassName(string $fullClassName): string { $parts = explode('\\', $fullClassName); return end($parts); } /** * Gibt Code-Ausschnitt für die Exception-Zeile zurück */ private function getCodeSnippet(string $file, int $line): string { if (!file_exists($file)) { return ''; } try { $fileHighlighter = new FileHighlighter(); $startLine = max(0, $line - 5); // 5 Zeilen vor der Exception $range = 11; // 5 vor + 1 Exception + 5 nach = 11 Zeilen // FileHighlighter gibt bereits HTML mit Syntax-Highlighting zurück $highlightedCode = $fileHighlighter($file, $startLine, $range, $line); return '

Code Context:

' . $highlightedCode; } catch (\Throwable $e) { // Bei Fehler beim Lesen der Datei, ignoriere Code-Ausschnitt return ''; } } private function renderStackTraceList(StackTrace $stackTrace): string { $frames = []; foreach ($stackTrace->getItems() as $index => $item) { $call = $item->getCall() !== '' ? $item->getCall() : '{main}'; $callEscaped = htmlspecialchars($call, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $locationEscaped = htmlspecialchars($item->getShortFile() . ':' . $item->line, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $isVendor = $item->isVendorFrame(); $expanded = !$isVendor && $index < 3; $codeSnippet = ''; if (!$isVendor && $index < 3) { $codeSnippet = $this->getCodeSnippet($item->file, $item->line); } $frames[] = sprintf( '
#%d %s %s%s
', $isVendor ? ' stack-frame--vendor' : '', $expanded ? ' open' : '', $index, $callEscaped, $locationEscaped, $codeSnippet !== '' ? '
' . $codeSnippet . '
' : '' ); } return implode("\n", $frames); } }