chore: complete update

This commit is contained in:
2025-07-17 16:24:20 +02:00
parent 899227b0a4
commit 64a7051137
1300 changed files with 85570 additions and 2756 deletions

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleOutput;
/**
* CLI-spezifischer Error Handler für Worker und Console
*/
final readonly class CliErrorHandler
{
public function __construct(
private ?ConsoleOutput $output = null
) {}
public function register(): void
{
set_error_handler([$this, 'handleError']);
set_exception_handler([$this, 'handleException']);
register_shutdown_function([$this, 'handleShutdown']);
}
public function handleError(int $severity, string $message, string $file, int $line): bool
{
if (!(error_reporting() & $severity)) {
return false;
}
$errorType = $this->getErrorType($severity);
$color = $this->getErrorColor($severity);
$output = $this->output ?? new ConsoleOutput();
$output->writeLine("[$errorType] $message", $color);
$output->writeLine(" in $file:$line", ConsoleColor::GRAY);
if (in_array($severity, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR])) {
exit(1);
}
return true;
}
public function handleException(\Throwable $exception): void
{
$output = $this->output ?? new ConsoleOutput();
$output->writeLine("❌ Uncaught " . get_class($exception) . ": " . $exception->getMessage(), ConsoleColor::BRIGHT_RED);
$output->writeLine(" File: " . $exception->getFile() . ":" . $exception->getLine(), ConsoleColor::RED);
if ($exception->getPrevious()) {
$output->writeLine(" Caused by: " . $exception->getPrevious()->getMessage(), ConsoleColor::YELLOW);
}
$output->writeLine(" Stack trace:", ConsoleColor::GRAY);
foreach (explode("\n", $exception->getTraceAsString()) as $line) {
$output->writeLine(" " . $line, ConsoleColor::GRAY);
}
exit(1);
}
public function handleShutdown(): void
{
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
$output = $this->output ?? new ConsoleOutput();
$output->writeLine("💥 Fatal Error: " . $error['message'], ConsoleColor::BRIGHT_RED);
$output->writeLine(" File: " . $error['file'] . ":" . $error['line'], ConsoleColor::RED);
}
}
private function getErrorType(int $severity): string
{
return match ($severity) {
E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR => 'ERROR',
E_WARNING, E_CORE_WARNING, E_COMPILE_WARNING, E_USER_WARNING => 'WARNING',
E_NOTICE, E_USER_NOTICE => 'NOTICE',
E_STRICT => 'STRICT',
E_RECOVERABLE_ERROR => 'RECOVERABLE_ERROR',
E_DEPRECATED, E_USER_DEPRECATED => 'DEPRECATED',
default => 'UNKNOWN'
};
}
private function getErrorColor(int $severity): ConsoleColor
{
return match ($severity) {
E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR => ConsoleColor::BRIGHT_RED,
E_WARNING, E_CORE_WARNING, E_COMPILE_WARNING, E_USER_WARNING => ConsoleColor::YELLOW,
E_NOTICE, E_USER_NOTICE => ConsoleColor::CYAN,
E_DEPRECATED, E_USER_DEPRECATED => ConsoleColor::MAGENTA,
default => ConsoleColor::WHITE
};
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;
use App\Framework\View\RenderContext;
use App\Framework\View\TemplateRenderer;
/**
* Dummy-Implementierung des TemplateRenderer als Fallback
*/
final class DummyTemplateRenderer implements TemplateRenderer
{
public function render(RenderContext $context): string
{
// Dieser Renderer gibt immer einen leeren String zurück,
// da er nur als Fallback dient und der ErrorTemplateRenderer
// seine eigene Fallback-Methode hat
return '';
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Framework\ErrorHandling;
use App\Framework\Http\RequestId;
final readonly class ErrorContext
{
public StackTrace $stackTrace;
public function __construct(
public \Throwable $exception,
public ErrorLevel $level,
public RequestId $requestId,
public array $additionalData = []
)
{
$this->stackTrace = new StackTrace($exception);
}
/**
* Gibt eine HTML-formatierte Version der Fehlermeldung zurück
*/
public function getFormattedMessage(): string
{
return nl2br(htmlspecialchars($this->exception->getMessage(), ENT_QUOTES, 'UTF-8'));
}
/**
* Gibt die Fehlerklasse zurück (ohne Namespace)
*/
public function getErrorClass(): string
{
$class = get_class($this->exception);
$parts = explode('\\', $class);
return end($parts);
}
/**
* Gibt den vollständigen Klassennamen zurück
*/
public function getFullErrorClass(): string
{
return get_class($this->exception);
}
}

View File

@@ -2,62 +2,200 @@
namespace App\Framework\ErrorHandling;
use App\Framework\Http\Headers;
use App\Framework\DI\Container;
use App\Framework\ErrorHandling\View\ApiErrorRenderer;
use App\Framework\ErrorHandling\View\ErrorResponseFactory;
use App\Framework\ErrorHandling\View\ErrorTemplateRenderer;
use App\Framework\Exception\SecurityException;
use App\Framework\Exception\SecurityLogLevel;
use App\Framework\Http\HttpResponse;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\RequestIdGenerator;
use App\Framework\Http\ResponseEmitter;
use App\Framework\Http\Status;
use JetBrains\PhpStorm\NoReturn;
use App\Framework\Logging\Logger;
use App\Framework\View\TemplateRenderer;
use Throwable;
final class ErrorHandler
final readonly class ErrorHandler
{
public static function register(ResponseEmitter $emitter): void
{
set_exception_handler(function (Throwable $exception) use ($emitter) {
self::handleException($exception, $emitter);
});
private ErrorLogger $logger;
private SecurityEventHandler $securityHandler;
private bool $isDebugMode;
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use ($emitter) {
$exception = new \ErrorException($errstr, 0, $errno, $errfile, $errline);
self::handleException($exception, $emitter);
});
register_shutdown_function(function () use ($emitter) {
$error = error_get_last();
if ($error) {
$exception = new \ErrorException(
$error['message'],
0,
$error['type'],
$error['file'],
$error['line']
);
self::handleException($exception, $emitter);
}
});
public function __construct(
private ResponseEmitter $emitter,
private Container $container,
private RequestIdGenerator $requestIdGenerator,
?Logger $logger = null,
?bool $isDebugMode = null,
?SecurityEventHandler $securityHandler = null
){
$this->isDebugMode = $isDebugMode ?? (bool)($_ENV['APP_DEBUG'] ?? false);
$this->logger = new ErrorLogger($logger);
$this->securityHandler = $securityHandler ?? SecurityEventHandler::createDefault($logger);
}
#[NoReturn]
private static function handleException(Throwable $e, ResponseEmitter $emitter): void
public function register(): void
{
$isDebug = $_ENV['APP_DEBUG'] ?? false;
set_exception_handler($this->handleException(...));
set_error_handler($this->handleError(...));;
register_shutdown_function($this->handleShutdown(...));;
}
$status = Status::INTERNAL_SERVER_ERROR ?? 500;
$message = $isDebug
? sprintf("Fehler: %s in %s:%d\n\n%s", $e->getMessage(), $e->getFile(), $e->getLine(), $e->getTraceAsString())
: "Es ist ein interner Fehler aufgetreten.";
public function createHttpResponse(\Throwable $e, ?MiddlewareContext $context = null): HttpResponse
{
$errorContext = $this->createErrorContext($e, $context);
$headers = new Headers()->with('Content-Type', 'text/plain; charset=utf-8');
// Standard Error Logging
$this->logger->logError($errorContext);
$response = new HttpResponse(
status: $status,
headers: $headers,
body: $message
// Security Event Logging delegieren
$this->securityHandler->handleIfSecurityException($e, $context);
$responseFactory = $this->createResponseFactory();
$isApiRequest = $responseFactory->isApiRequest();
return $responseFactory->createResponse(
$errorContext,
$this->isDebugMode,
$isApiRequest
);
}
// Sende die Fehlermeldung (so früh wie möglich!)
$emitter->emit($response);
public function handleException(Throwable $e): never
{
$context = $this->createErrorContext($e);
// Logge den Fehler
$this->logger->logError($context);
// Security Event Logging delegieren
$this->securityHandler->handleIfSecurityException($e);
// Erstelle ResponseFactory mit dem TemplateRenderer
$responseFactory = $this->createResponseFactory();
// Bestimme ob es ein API-Request ist
$isApiRequest = $responseFactory->isApiRequest();
// Erstelle eine Response basierend auf dem Kontext
$response = $responseFactory->createResponse($context, $this->isDebugMode, $isApiRequest);
// Sende die Fehlermeldung
$this->emitter->emit($response);
exit(1);
}
public function handleError(int $errno, string $errstr, string $errfile, int $errline): void
{
$exception = new \ErrorException($errstr, 0, $errno, $errfile, $errline);
$this->handleException($exception);
}
public function handleShutdown(): void
{
$error = error_get_last();
if ($error && $this->isFatalError($error['type'])) {
$exception = new \ErrorException(
$error['message'],
0,
$error['type'],
$error['file'],
$error['line']
);
$this->handleException($exception);
}
}
private function isFatalError(int $errno): bool
{
return in_array($errno, [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR], true);
}
private function createErrorContext(Throwable $exception, ?MiddlewareContext $context = null):ErrorContext
{
$level = $this->determineErrorLevel($exception);
$requestId = $context->requestId ?? $this->requestIdGenerator->generate();
return new ErrorContext(
exception: $exception,
level: $level,
requestId: $requestId,
additionalData: [
'timestamp' => date('c'),
'memory_usage' => memory_get_peak_usage(true),
]
);
}
private function determineErrorLevel(Throwable $exception):ErrorLevel
{
return match(true) {
$exception instanceof \Error => ErrorLevel::CRITICAL,
$exception instanceof \ErrorException => $this->determineErrorExceptionLevel($exception),
$exception instanceof SecurityException => $this->determineSecurityErrorLevel($exception),
$exception instanceof \RuntimeException => ErrorLevel::ERROR,
$exception instanceof \LogicException => ErrorLevel::WARNING,
default => ErrorLevel::ERROR,
};
}
/**
* Bestimmt den Error-Level für SecurityExceptions basierend auf dem Security-Level
*/
private function determineSecurityErrorLevel(SecurityException $exception): ErrorLevel
{
return match($exception->getSecurityLevel()) {
SecurityLogLevel::DEBUG => ErrorLevel::DEBUG,
SecurityLogLevel::INFO => ErrorLevel::INFO,
SecurityLogLevel::WARN => ErrorLevel::WARNING,
SecurityLogLevel::ERROR => ErrorLevel::ERROR,
SecurityLogLevel::FATAL => ErrorLevel::CRITICAL,
};
}
/**
* Bestimmt den Fehler-Level basierend auf dem ErrorException-Severity-Level
*/
private function determineErrorExceptionLevel(\ErrorException $exception): ErrorLevel
{
return match($exception->getSeverity()) {
E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR => ErrorLevel::CRITICAL,
E_WARNING, E_CORE_WARNING, E_COMPILE_WARNING, E_USER_WARNING => ErrorLevel::WARNING,
E_NOTICE, E_USER_NOTICE => ErrorLevel::NOTICE,
E_DEPRECATED, E_USER_DEPRECATED => ErrorLevel::INFO,
E_STRICT => ErrorLevel::DEBUG,
default => ErrorLevel::ERROR,
};
}
/**
* Erstellt eine ErrorResponseFactory mit Renderern
* Versucht den TemplateRenderer aus dem Container zu laden, falls vorhanden
*/
private function createResponseFactory(): ErrorResponseFactory
{
$templateRenderer = null;
// Versuche TemplateRenderer aus Container zu laden
if ($this->container && $this->container->has(TemplateRenderer::class)) {
try {
$templateRenderer = $this->container->get(TemplateRenderer::class);
} catch (Throwable $e) {
// Falls ein Fehler beim Laden auftritt, verwende Fallback
$templateRenderer = null;
}
}
$htmlRenderer = $templateRenderer ?
new ErrorTemplateRenderer($templateRenderer) :
new ErrorTemplateRenderer(new DummyTemplateRenderer());
$apiRenderer = new ApiErrorRenderer();
return new ErrorResponseFactory($htmlRenderer, $apiRenderer);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Framework\ErrorHandling;
enum ErrorLevel
{
case CRITICAL;
case ERROR;
case WARNING;
case NOTICE;
case INFO;
case DEBUG;
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;
use App\Framework\Logging\Logger;
use App\Framework\Logging\LogLevel;
/**
* Logger für Fehler und Exceptions - nutzt Framework Logger
*/
final readonly class ErrorLogger
{
public function __construct(
private ?Logger $logger = null
) {}
/**
* Loggt einen Fehler basierend auf dem ErrorContext
*/
public function logError(ErrorContext $context): void
{
$exception = $context->exception;
$message = sprintf(
'[%s] %s: %s in %s:%d',
$context->requestId,
get_class($exception),
$exception->getMessage(),
$exception->getFile(),
$exception->getLine()
);
$contextData = [
'exception' => $exception,
'trace' => $exception->getTraceAsString(),
'requestId' => $context->requestId,
'additionalData' => $context->additionalData
];
if ($this->logger !== null) {
$logLevel = $this->mapErrorLevelToFrameworkLevel($context->level);
$this->logger->log($logLevel, $message, $contextData);
} else {
// Fallback auf error_log
$this->logToErrorLog($context);
}
}
/**
* Fallback-Methode für Logging ohne Framework Logger
*/
private function logToErrorLog(ErrorContext $context): void
{
$exception = $context->exception;
$message = sprintf(
'[%s] [%s] %s: %s in %s:%d',
date('Y-m-d H:i:s'),
$context->level->name,
get_class($exception),
$exception->getMessage(),
$exception->getFile(),
$exception->getLine()
);
error_log($message);
// Bei kritischen Fehlern auch den Stacktrace loggen
if ($context->level === ErrorLevel::CRITICAL) {
error_log("Stack trace: \n" . $exception->getTraceAsString());
}
}
/**
* Mappt interne ErrorLevel auf Framework LogLevel
*/
private function mapErrorLevelToFrameworkLevel(ErrorLevel $level): LogLevel
{
return match($level) {
ErrorLevel::CRITICAL => LogLevel::CRITICAL,
ErrorLevel::ERROR => LogLevel::ERROR,
ErrorLevel::WARNING => LogLevel::WARNING,
ErrorLevel::NOTICE => LogLevel::NOTICE,
ErrorLevel::INFO => LogLevel::INFO,
ErrorLevel::DEBUG => LogLevel::DEBUG,
};
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;
use App\Framework\Http\Status;
/**
* Hilfsklasse zur Konvertierung von Exceptions in HTTP-Status-Codes.
*/
final class ExceptionConverter
{
/**
* Bestimmt den HTTP-Status basierend auf dem Exception-Typ
*/
public static function getStatusFromException(\Throwable $e): Status
{
return match (true) {
// Routing-Fehler
$e instanceof \App\Framework\Router\Exception\RouteNotFound => Status::NOT_FOUND,
$e instanceof \App\Framework\Router\Exception\MethodNotAllowed => Status::METHOD_NOT_ALLOWED,
// Validierungs-Fehler
$e instanceof \App\Framework\Validation\Exceptions\ValidationException => Status::BAD_REQUEST,
// API-Fehler
$e instanceof \App\Framework\Api\ApiException => Status::BAD_GATEWAY,
// Datenbank-Fehler
$e instanceof \PDOException => Status::INTERNAL_SERVER_ERROR,
// Standard
default => Status::INTERNAL_SERVER_ERROR,
};
}
/**
* Erstellt den Response-Body basierend auf der Exception
*/
public static function getResponseBody(\Throwable $e, bool $debug = false, ?string $requestId = null): array
{
if ($debug) {
// Im Debug-Modus detaillierte Informationen zurückgeben
return [
'error' => true,
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => explode("\n", $e->getTraceAsString()),
'request_id' => $requestId,
];
}
// Im Produktionsmodus nur minimale Informationen
return [
'error' => true,
'message' => self::getUserFriendlyMessage($e),
'request_id' => $requestId,
];
}
/**
* Gibt eine benutzerfreundliche Fehlermeldung basierend auf dem Exception-Typ zurück
*/
public static function getUserFriendlyMessage(\Throwable $e): string
{
return match (true) {
$e instanceof \App\Framework\Router\Exception\RouteNotFound => 'Die angeforderte Seite wurde nicht gefunden.',
$e instanceof \App\Framework\Router\Exception\MethodNotAllowed => 'Diese Anfragemethode ist nicht erlaubt.',
$e instanceof \App\Framework\Validation\Exceptions\ValidationException => 'Die eingegebenen Daten sind ungültig.',
$e instanceof \App\Framework\Api\ApiException => 'Es ist ein Fehler bei der Kommunikation mit einem externen Dienst aufgetreten.',
default => 'Es ist ein interner Fehler aufgetreten.',
};
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;
use App\Framework\Exception\SecurityException;
use App\Framework\Exception\ErrorHandlerContext;
/**
* Manager für Security-Alerts - Integration zu externen Alert-Systemen
*/
final readonly class SecurityAlertManager
{
public function __construct(
private array $alertChannels = []
) {}
/**
* Sendet Alert über alle konfigurierten Kanäle
*/
public function sendAlert(
SecurityException $exception,
ErrorHandlerContext $context
): void {
$alertData = $this->createAlertPayload($exception, $context);
foreach ($this->alertChannels as $channel) {
try {
$channel->sendAlert($alertData);
} catch (\Throwable $e) {
// Alert-Fehler nicht propagieren, nur loggen
error_log("Alert channel failed: " . $e->getMessage());
}
}
}
/**
* Erstellt standardisierte Alert-Payload
*/
private function createAlertPayload(
SecurityException $exception,
ErrorHandlerContext $context
): array {
$securityEvent = $exception->getSecurityEvent();
return [
'type' => 'security_alert',
'severity' => $this->mapSeverity($exception->getSecurityLevel()),
'event_id' => $securityEvent->getEventIdentifier(),
'title' => 'Security Event: ' . $securityEvent->getCategory(),
'description' => $securityEvent->getDescription(),
'details' => [
'category' => $securityEvent->getCategory(),
'level' => $exception->getSecurityLevel()->value,
'client_ip' => $context->request->clientIp,
'user_agent' => $context->request->userAgent,
'request_uri' => $context->request->requestUri,
'timestamp' => date('c'),
'request_id' => $context->request->requestId
],
'event_data' => $securityEvent->toArray(),
'context' => $context->forLogging()
];
}
/**
* Mappt Security-Level auf Alert-Severity
*/
private function mapSeverity(\App\Framework\Exception\SecurityLogLevel $level): string
{
return match($level) {
\App\Framework\Exception\SecurityLogLevel::FATAL => 'critical',
\App\Framework\Exception\SecurityLogLevel::ERROR => 'high',
\App\Framework\Exception\SecurityLogLevel::WARN => 'medium',
\App\Framework\Exception\SecurityLogLevel::INFO => 'low',
\App\Framework\Exception\SecurityLogLevel::DEBUG => 'info',
};
}
/**
* Factory mit Standard-Alert-Kanälen
*/
public static function createWithChannels(array $channels): self
{
return new self($channels);
}
}

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;
use App\Framework\Exception\SecurityException;
use App\Framework\Exception\ErrorHandlerContext;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Logging\Logger;
use Throwable;
/**
* Spezialisierte Klasse für Security-Event-Handling und OWASP-konformes Logging
*/
final readonly class SecurityEventHandler
{
public function __construct(
private SecurityEventLogger $securityLogger,
private ?SecurityAlertManager $alertManager = null
) {}
/**
* Behandelt Security-Exception und führt entsprechendes Logging durch
*/
public function handleSecurityException(
SecurityException $exception,
?MiddlewareContext $context = null
): void {
try {
// Erstelle ErrorHandlerContext für OWASP-Format
$errorHandlerContext = ErrorHandlerContext::fromException($exception, $context);
// Führe Security-Logging durch
$this->securityLogger->logSecurityEvent($exception, $errorHandlerContext);
// Sende Alert falls erforderlich
if ($exception->requiresAlert()) {
$this->handleSecurityAlert($exception, $errorHandlerContext);
}
} catch (Throwable $loggingError) {
// Fallback-Logging: Niemals Security-Events verlieren
$this->handleLoggingFailure($exception, $loggingError);
}
}
/**
* Prüft ob Exception eine SecurityException ist und behandelt sie entsprechend
*/
public function handleIfSecurityException(
Throwable $exception,
?MiddlewareContext $context = null
): bool {
if (!$exception instanceof SecurityException) {
return false;
}
$this->handleSecurityException($exception, $context);
return true;
}
/**
* Behandelt Security-Alerts
*/
private function handleSecurityAlert(
SecurityException $exception,
ErrorHandlerContext $context
): void {
if ($this->alertManager) {
$this->alertManager->sendAlert($exception, $context);
} else {
// Fallback: Logge als kritisches Event
$this->securityLogger->logCriticalAlert($exception, $context);
}
}
/**
* Behandelt Fehler beim Security-Logging (Fallback)
*/
private function handleLoggingFailure(
SecurityException $originalException,
Throwable $loggingError
): void {
// Niemals Security-Events verlieren - verwende error_log als Fallback
$fallbackMessage = sprintf(
'SECURITY_AUDIT_FAILURE: Failed to log security event %s - %s',
$originalException->getSecurityEvent()->getEventIdentifier(),
$loggingError->getMessage()
);
error_log($fallbackMessage);
// Versuche minimales Security-Log
try {
$minimalLog = [
'datetime' => date('c'),
'event' => $originalException->getSecurityEvent()->getEventIdentifier(),
'description' => $originalException->getSecurityEvent()->getDescription(),
'level' => 'FATAL',
'audit_failure' => true,
'original_error' => $loggingError->getMessage()
];
error_log('SECURITY_EVENT_FALLBACK: ' . json_encode($minimalLog));
} catch (Throwable $criticalError) {
// Absoluter Fallback
error_log('CRITICAL_SECURITY_AUDIT_FAILURE: Unable to log security event');
}
}
/**
* Factory-Methode mit Standard-Konfiguration
*/
public static function createDefault(?Logger $logger = null): self
{
$securityLogger = new SecurityEventLogger($logger);
$alertManager = null; // Kann später konfiguriert werden
return new self($securityLogger, $alertManager);
}
}

View File

@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling;
use App\Framework\Exception\SecurityException;
use App\Framework\Exception\ErrorHandlerContext;
use App\Framework\Exception\SecurityLogLevel;
use App\Framework\Logging\Logger;
use App\Framework\Logging\LogLevel;
/**
* Spezialisierter Logger für Security-Events im OWASP-Format
*/
final readonly class SecurityEventLogger
{
public function __construct(
private ?Logger $logger = null,
private string $applicationId = 'app'
) {}
/**
* Loggt Security-Event im OWASP-Format
*/
public function logSecurityEvent(
SecurityException $exception,
ErrorHandlerContext $context
): void {
$securityEvent = $exception->getSecurityEvent();
// OWASP-konformes Log generieren
$owaspLog = $this->createOWASPLog($exception, $context);
if ($this->logger) {
// Strukturiertes Logging über Framework Logger
$frameworkLogLevel = $this->mapSecurityLevelToFrameworkLevel($securityEvent->getLogLevel());
$this->logger->log(
$frameworkLogLevel,
$securityEvent->getDescription(),
[
'security_event' => $securityEvent->getEventIdentifier(),
'security_category' => $securityEvent->getCategory(),
'requires_alert' => $securityEvent->requiresAlert(),
'owasp_format' => $owaspLog,
'event_data' => $securityEvent->toArray(),
'context_data' => $context->forLogging()
]
);
} else {
// Fallback auf error_log
$this->logToErrorLog($owaspLog);
}
}
/**
* Loggt kritische Security-Alerts
*/
public function logCriticalAlert(
SecurityException $exception,
ErrorHandlerContext $context
): void {
$alertData = $this->createAlertData($exception, $context);
if ($this->logger) {
$this->logger->log(
LogLevel::CRITICAL,
'SECURITY_ALERT: ' . $exception->getSecurityEvent()->getEventIdentifier(),
$alertData
);
} else {
error_log('SECURITY_ALERT: ' . json_encode($alertData));
}
}
/**
* Erstellt OWASP-konformes Log-Format
*/
private function createOWASPLog(
SecurityException $exception,
ErrorHandlerContext $context
): array {
$securityEvent = $exception->getSecurityEvent();
return [
'datetime' => date('c'),
'appid' => $this->applicationId,
'event' => $securityEvent->getEventIdentifier(),
'level' => $securityEvent->getLogLevel()->value,
'description' => $securityEvent->getDescription(),
'useragent' => $context->request->userAgent,
'source_ip' => $context->request->clientIp,
'host_ip' => $context->request->hostIp,
'hostname' => $context->request->hostname,
'protocol' => $context->request->protocol,
'port' => $context->request->port,
'request_uri' => $context->request->requestUri,
'request_method' => $context->request->requestMethod,
'region' => $_ENV['AWS_REGION'] ?? 'unknown',
'geo' => $_ENV['GEO_LOCATION'] ?? 'unknown',
'category' => $securityEvent->getCategory(),
'requires_alert' => $securityEvent->requiresAlert()
];
}
/**
* Erstellt Alert-Daten für kritische Events
*/
private function createAlertData(
SecurityException $exception,
ErrorHandlerContext $context
): array {
return [
'event' => $exception->getSecurityEvent()->getEventIdentifier(),
'description' => $exception->getSecurityEvent()->getDescription(),
'level' => $exception->getSecurityLevel()->value,
'category' => $exception->getSecurityEvent()->getCategory(),
'client_ip' => $context->request->clientIp,
'user_agent' => $context->request->userAgent,
'request_uri' => $context->request->requestUri,
'request_method' => $context->request->requestMethod,
'timestamp' => date('c'),
'request_id' => $context->request->requestId,
'event_data' => $exception->getSecurityEvent()->toArray()
];
}
/**
* Fallback-Logging über error_log
*/
private function logToErrorLog(array $owaspLog): void
{
$logMessage = 'SECURITY_EVENT: ' . json_encode($owaspLog, JSON_UNESCAPED_SLASHES);
error_log($logMessage);
}
/**
* Mappt SecurityLogLevel auf Framework LogLevel
*/
private function mapSecurityLevelToFrameworkLevel(
SecurityLogLevel $securityLevel
): LogLevel {
return match($securityLevel) {
SecurityLogLevel::DEBUG => LogLevel::DEBUG,
SecurityLogLevel::INFO => LogLevel::INFO,
SecurityLogLevel::WARN => LogLevel::WARNING,
SecurityLogLevel::ERROR => LogLevel::ERROR,
SecurityLogLevel::FATAL => LogLevel::CRITICAL,
};
}
/**
* Factory-Methode für Standard-Konfiguration
*/
public static function create(?Logger $logger = null): self
{
return new self(
$logger,
$_ENV['APP_NAME'] ?? $_ENV['APP_ID'] ?? 'app'
);
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace App\Framework\ErrorHandling;
use ArrayAccess;
use Countable;
use IteratorAggregate;
use ArrayIterator;
use Traversable;
/**
* Stacktrace-Klasse zum Aufbereiten und Darstellen von Exception-Traces
* Implementiert ArrayAccess, IteratorAggregate und Countable für einfachen Zugriff in Templates
*/
final class StackTrace implements ArrayAccess, IteratorAggregate, Countable
{
/** @var TraceItem[] */
private(set) array $items = [];
/**
* Erstellt ein neues StackTrace-Objekt aus einer Exception
*/
public function __construct(private readonly \Throwable $exception)
{
$this->processTrace($exception->getTrace());
}
/**
* Verarbeitet die rohen Trace-Daten in strukturierte TraceItems
*/
private function processTrace(array $trace): void
{
// Füge den Ursprung der Exception hinzu (file/line aus Exception selbst)
$this->items[] = new TraceItem(
file: $this->exception->getFile(),
line: $this->exception->getLine(),
function: null,
class: null,
type: null,
args: [],
isExceptionOrigin: true
);
// Verarbeite den Rest des Stacktraces
foreach ($trace as $index => $frame) {
$this->items[] = new TraceItem(
file: $frame['file'] ?? '(internal function)',
line: $frame['line'] ?? 0,
function: $frame['function'] ?? null,
class: $frame['class'] ?? null,
type: $frame['type'] ?? null,
args: $frame['args'] ?? [],
isExceptionOrigin: false
);
}
$this->items = array_reverse($this->items);
}
/**
* Gibt alle Trace-Items zurück
*
* @return TraceItem[]
*/
public function getItems(): array
{
return $this->items;
}
/**
* Gibt den Ursprung der Exception zurück (erstes Item)
*/
public function getOrigin(): ?TraceItem
{
return $this->items[0] ?? null;
}
/**
* Gibt die ersten N Elemente des Stacktraces zurück
*/
public function getFirst(int $count): array
{
return array_slice($this->items, 0, $count);
}
/**
* Gibt die letzten N Elemente des Stacktraces zurück
*/
public function getLast(int $count): array
{
return array_slice($this->items, -$count);
}
/**
* Filtert den Stacktrace nach bestimmten Kriterien
*/
public function filter(callable $callback): array
{
return array_filter($this->items, $callback);
}
/**
* Implementierung von ArrayAccess
*/
public function offsetExists(mixed $offset): bool
{
return isset($this->items[$offset]);
}
public function offsetGet(mixed $offset): mixed
{
return $this->items[$offset] ?? null;
}
public function offsetSet(mixed $offset, mixed $value): void
{
// Read-only
}
public function offsetUnset(mixed $offset): void
{
// Read-only
}
/**
* Implementierung von IteratorAggregate
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->items);
}
/**
* Implementierung von Countable
*/
public function count(): int
{
return count($this->items);
}
/**
* Formatiert den gesamten Stacktrace als String
*/
public function __toString(): string
{
$result = "";
foreach ($this->items as $index => $item) {
$result .= "#{$index} {$item}\n";
}
return $result;
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace App\Framework\ErrorHandling;
/**
* Repräsentiert einen einzelnen Eintrag im Stacktrace
*/
final readonly class TraceItem
{
public function __construct(
public string $file,
public int $line,
public ?string $function,
public ?string $class,
public ?string $type,
public array $args,
public bool $isExceptionOrigin
) {}
/**
* Gibt den Dateipfad relativ zum Projekt-Root zurück
*/
public function getRelativeFile(bool $namespace = true): string
{
$projectRoot = dirname(__DIR__, 3); // Annahme: 3 Verzeichnisse zurück zum Projekt-Root
$name = str_replace($projectRoot, '', $this->file);
return $namespace ? $name : $this->removeNamespace($name);
}
/**
* Gibt den Klassennamen ohne Namespace zurück
*/
public function getShortClass(): ?string
{
if (!$this->class) {
return null;
}
$parts = explode('\\', $this->class);
return end($parts);
}
/**
* Gibt den Namespace zurück
*/
public function getNamespace(): ?string
{
if (!$this->class) {
return null;
}
$lastSlash = strrpos($this->class, '\\');
return $lastSlash ? substr($this->class, 0, $lastSlash) : null;
}
/**
* Gibt zurück, ob es sich um einen Methodenaufruf handelt
*/
public function isMethod(): bool
{
return $this->class !== null && $this->function !== null;
}
/**
* Gibt zurück, ob es sich um einen Funktionsaufruf handelt
*/
public function isFunction(): bool
{
return $this->class === null && $this->function !== null;
}
/**
* Gibt den Aufruf-String zurück (z.B. "Namespace\Class->method()")
*/
public function getCallString(): string
{
if ($this->isExceptionOrigin) {
return "Exception origin: >>" . $this->file . '<< in Line: ' . $this->line;
}
if ($this->isMethod()) {
return $this->getShortClass() . $this->type . $this->function . '(' . $this->formatArgs() . ')';
}
if ($this->isFunction()) {
return $this->function . '(' . $this->formatArgs() . ')';
}
return '';
}
/**
* Formatiert die Argumente für die Anzeige
*/
private function formatArgs(): string
{
if (empty($this->args)) {
return '';
}
$formatted = [];
foreach ($this->args as $arg) {
$formatted[] = $this->formatArg($arg);
}
return implode(', ', $formatted);
}
/**
* Formatiert ein einzelnes Argument für die Anzeige
*/
private function formatArg(mixed $arg): string
{
return match(true) {
is_string($arg) => '"' . (strlen($arg) > 20 ? substr($arg, 0, 20) . '...' : $arg) . '"',
is_array($arg) => 'array(' . count($arg) . ')',
is_object($arg) => $this->removeNamespace(get_class($arg)),
is_bool($arg) => $arg ? 'true' : 'false',
is_null($arg) => 'null',
default => (string)$arg
};
}
private function removeNamespace(string $class): string
{
$lastSlash = strrpos($class, '\\');
return $lastSlash ? substr($class, $lastSlash + 1) : $class;
}
/**
* String-Repräsentation dieses TraceItems
*/
public function __toString(): string
{
$locationInfo = $this->file . ':' . $this->line;
if ($this->isExceptionOrigin) {
return "Exception origin: {$locationInfo}";
}
return $this->getCallString() . " at {$locationInfo}";
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling\View;
use App\Framework\ErrorHandling\ErrorContext;
use App\Framework\ErrorHandling\ExceptionConverter;
/**
* Renderer für API-Fehler im JSON-Format
*/
final readonly class ApiErrorRenderer implements ErrorViewRendererInterface
{
/**
* Rendert einen API-Fehler als JSON
*/
public function render(ErrorContext $context, bool $isDebug = false): string
{
$responseData = [
'error' => true,
'message' => $isDebug ? $context->exception->getMessage() : ExceptionConverter::getUserFriendlyMessage($context->exception),
'requestId' => $context->requestId,
'timestamp' => $context->additionalData['timestamp'] ?? date('c'),
];
// Im Debug-Modus zusätzliche Informationen hinzufügen
if ($isDebug) {
$responseData['details'] = [
'exception' => get_class($context->exception),
'file' => $context->exception->getFile(),
'line' => $context->exception->getLine(),
'trace' => explode("\n", $context->exception->getTraceAsString()),
'level' => $context->level->name,
];
}
return json_encode($responseData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling\View;
use App\Framework\ErrorHandling\ErrorContext;
use App\Framework\ErrorHandling\ExceptionConverter;
use App\Framework\Http\Headers;
use App\Framework\Http\HttpResponse;
use App\Framework\Http\Status;
/**
* Factory für HTTP-Fehlermeldungen
*/
final readonly class ErrorResponseFactory
{
public function __construct(
private ErrorViewRendererInterface $htmlRenderer,
private ErrorViewRendererInterface $apiRenderer
) {}
/**
* Erstellt eine HTTP-Response basierend auf dem ErrorContext
*/
public function createResponse(ErrorContext $context, bool $isDebug = false, bool $isApiRequest = false): HttpResponse
{
$status = ExceptionConverter::getStatusFromException($context->exception);
$headers = new Headers();
if ($isApiRequest) {
$headers = $headers->with('Content-Type', 'application/json; charset=utf-8');
$body = $this->apiRenderer->render($context, $isDebug);
} else {
$headers = $headers->with('Content-Type', 'text/html; charset=utf-8');
$body = $this->htmlRenderer->render($context, $isDebug);
}
// Cache-Control hinzufügen (Fehlerseiten sollten nicht gecacht werden)
$headers = $headers->with('Cache-Control', 'no-store, no-cache, must-revalidate');
return new HttpResponse(
status: $status,
headers: $headers,
body: $body
);
}
/**
* Bestimmt, ob es sich um eine API-Anfrage handelt
*/
public function isApiRequest(): bool
{
// Accept-Header prüfen
$acceptHeader = $_SERVER['HTTP_ACCEPT'] ?? '';
if (str_contains($acceptHeader, 'application/json')) {
return true;
}
// URL-Pfad prüfen (z.B. /api/...)
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
if (str_starts_with($requestUri, '/api/')) {
return true;
}
// Content-Type prüfen
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
if (str_contains($contentType, 'application/json')) {
return true;
}
// X-Requested-With Header prüfen (für AJAX)
$requestedWith = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? '';
if (strtolower($requestedWith) === 'xmlhttprequest') {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling\View;
use App\Framework\ErrorHandling\ErrorContext;
use App\Framework\Meta\MetaData;
use App\Framework\Meta\OpenGraphTypeWebsite;
use App\Framework\SyntaxHighlighter\FileHighlighter;
use App\Framework\View\RenderContext;
use App\Framework\View\TemplateRenderer;
use Throwable;
/**
* Renderer für Fehlerseiten unter Verwendung des View-Systems
*/
final readonly class ErrorTemplateRenderer implements ErrorViewRendererInterface
{
public function __construct(
private TemplateRenderer $renderer,
private string $debugTemplate = 'debug',
private string $productionTemplate = 'production'
) {}
/**
* Rendert eine Fehlerseite basierend auf dem ErrorContext
*/
public function render(ErrorContext $context, bool $isDebug = false): string
{
$template = $isDebug ? $this->debugTemplate : $this->productionTemplate;
try {
// Sichere Datenaufbereitung ohne Closures oder komplexe Objekte
$safeData = [
'code' => new FileHighlighter()($context->exception->getFile(), $context->exception->getLine() -10, 10),
'errorClass' => $context->exception::class,
'errorMessage' => $context->exception->getMessage(),
'error' => [
'message' => $context->exception->getMessage(),
'file' => $context->exception->getFile(),
'line' => $context->exception->getLine(),
'trace' => $context->exception->getTraceAsString(),
'class' => get_class($context->exception),
],
'requestId' => $context->requestId->toString(),
'timestamp' => $context->additionalData['timestamp'] ?? date('c'),
'level' => $context->level->name,
'memory' => $context->additionalData['memory_usage'] ?? 0
];
$renderContext = new RenderContext(
template: $template,
metaData: new MetaData('', '', new OpenGraphTypeWebsite),
data: $safeData
);
$renderedContent = $this->renderer->render($renderContext);
if($renderedContent === ""){
throw new \Exception("Template Renderer returned empty string");
}
return $renderedContent;
} catch (Throwable $e) {
// Fallback falls das Template-Rendering fehlschlägt
return $this->createFallbackErrorPage($context, $isDebug, $e);
}
}
/**
* Erstellt eine einfache Fehlerseite als Fallback
*/
private function createFallbackErrorPage(ErrorContext $context, bool $isDebug, ?Throwable $renderException = null): string
{
$exception = $context->exception;
if ($isDebug) {
$content = sprintf(
'<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Anwendungsfehler</title>
<style>
body { font-family: sans-serif; line-height: 1.6; color: #333; max-width: 1200px; margin: 0 auto; padding: 20px; }
.error-container { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px; padding: 20px; margin: 20px 0; }
.error-title { color: #dc3545; margin-top: 0; }
.error-meta { background: #e9ecef; padding: 10px; border-radius: 4px; font-size: 0.9em; margin: 10px 0; }
.error-trace { background: #f1f3f5; padding: 15px; border-radius: 4px; overflow-x: auto; font-family: monospace; font-size: 0.9em; }
.error-info { margin-bottom: 20px; }
.request-id { font-family: monospace; color: #6c757d; }
.render-error { margin-top: 30px; padding-top: 20px; border-top: 1px dashed #dee2e6; }
</style>
</head>
<body>
<div class="error-container">
<h1 class="error-title">Anwendungsfehler: %s</h1>
<div class="error-meta">
<strong>Request-ID:</strong> <span class="request-id">%s</span><br>
<strong>Zeit:</strong> %s<br>
<strong>Fehler-Level:</strong> %s
</div>
<div class="error-info">
<p><strong>Datei:</strong> %s</p>
<p><strong>Zeile:</strong> %d</p>
</div>
<h3>Stack Trace:</h3>
<div class="error-trace">%s</div>
%s
</div>
</body>
</html>',
htmlspecialchars($exception->getMessage()),
htmlspecialchars($context->requestId->toString()),
htmlspecialchars($context->additionalData['timestamp'] ?? date('c')),
htmlspecialchars($context->level->name),
htmlspecialchars($exception->getFile()),
$exception->getLine(),
nl2br(htmlspecialchars($exception->getTraceAsString())),
$renderException ? '<div class="render-error"><h3>Fehler beim Rendern der Fehlerseite:</h3><p>' .
htmlspecialchars($renderException->getMessage()) . '</p></div>' : ''
);
} else {
// Produktions-Fallback mit minimalen Informationen
$content = sprintf(
'<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Fehler</title>
<style>
body { font-family: sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 0 auto; padding: 40px 20px; text-align: center; }
.error-container { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px; padding: 40px 20px; }
.error-title { color: #dc3545; margin-top: 0; }
.request-id { font-family: monospace; color: #6c757d; font-size: 0.8em; margin-top: 30px; }
</style>
</head>
<body>
<div class="error-container">
<h1 class="error-title">Es ist ein Fehler aufgetreten</h1>
<p>Bitte versuchen Sie es später erneut oder kontaktieren Sie den Support.</p>
<p class="request-id">Request-ID: %s</p>
</div>
</body>
</html>',
htmlspecialchars($context->requestId->toString())
);
}
return $content;
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorHandling\View;
use App\Framework\ErrorHandling\ErrorContext;
/**
* Interface für alle Renderer von Fehlerseiten
*/
interface ErrorViewRendererInterface
{
/**
* Rendert eine Fehlerseite basierend auf dem ErrorContext
*/
public function render(ErrorContext $context, bool $isDebug = false): string;
}

View File

@@ -0,0 +1,209 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fehler: $errorClass</title>
<style>
:root {
--bg-color: #f8f9fa;
--text-color: #212529;
--border-color: #dee2e6;
--error-color: #dc3545;
--header-bg: #e9ecef;
--code-bg: #f5f5f5;
--link-color: #0d6efd;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #212529;
--text-color: #f8f9fa;
--border-color: #495057;
--error-color: #f8d7da;
--header-bg: #343a40;
--code-bg: #2b3035;
--link-color: #6ea8fe;
}
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: var(--text-color);
background: var(--bg-color);
margin: 0;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.error-container {
background: white;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-bottom: 20px;
}
.error-header {
background: var(--header-bg);
padding: 15px 20px;
border-bottom: 1px solid var(--border-color);
}
.error-title {
margin: 0;
color: var(--error-color);
font-size: 1.5rem;
}
.error-body {
padding: 20px;
}
.error-message {
font-size: 1.1rem;
margin-bottom: 20px;
padding: 15px;
background: #fff5f5;
border-left: 4px solid var(--error-color);
border-radius: 4px;
}
.error-meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
font-size: 0.9rem;
color: #6c757d;
margin-bottom: 20px;
}
.error-meta span {
background: var(--header-bg);
padding: 4px 8px;
border-radius: 4px;
}
.trace-container {
background: white;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.trace-header {
background: var(--header-bg);
padding: 5px 10px;
border-bottom: 1px solid var(--border-color);
}
.trace-title {
margin: 0;
font-size: 1.2rem;
}
.trace-body {
padding: 0;
}
.trace-item {
padding: 2px 4px;
border-bottom: 1px solid var(--border-color);
}
.trace-item:last-child {
border-bottom: none;
}
.trace-location {
font-family: monospace;
background: var(--code-bg);
padding: 4px 8px;
border-radius: 4px;
word-break: break-all;
}
.trace-call {
margin-top: 5px;
font-family: monospace;
color: #6c757d;
word-break: break-all;
}
.trace-origin {
background-color: #fff5f5;
}
code {
font-family: monospace;
background: var(--code-bg);
padding: 2px 4px;
border-radius: 3px;
}
@media (prefers-color-scheme: dark) {
.error-container, .trace-container {
background: #343a40;
}
.error-message {
background: #3a2a2a;
}
.trace-origin {
background-color: #3a2a2a;
}
}
</style>
</head>
<body>
<div class="container">
<div class="error-container">
<div class="error-header">
<h1 class="error-title">{{ errorClass }}</h1>
</div>
<div class="error-body">
<div class="error-message">{{ errorMessage }}</div>
<div class="error-meta">
<span>Request ID: {{ requestId }}</span>
<span>Zeit: {{ timestamp }}</span>
<span>Level: {{ level }}</span>
<span>Speicherverbrauch: {{ format_filesize($memory) }}</span>
</div>
</div>
</div>
<?php echo "Hallo" ?>
{{ code }}
<div class="trace-container">
<div class="trace-header">
<h2 class="trace-title">Stack Trace</h2>
</div>
<div class="trace-body" style="font-size: 0.8rem;">
<for var="item" in="trace">
<div class="trace-item {{ item->isExceptionOrigin() }} 'trace-origin' : ''">
<div class="trace-location">
{{ item.getRelativeFile() }} <i>Line {{ item.line }}</i>
<!--#$i $item->getRelativeFile():$item->line-->
</div>
<div class="trace-call" style="font-size: 1.25rem; background-color: #81a5ed; color: black; padding: 0.25em;">
{{ item.getCallString() }}
</div>
</div>
</for>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fehler</title>
<style>
:root {
--bg-color: #f8f9fa;
--text-color: #212529;
--border-color: #dee2e6;
--error-color: #dc3545;
--header-bg: #e9ecef;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #212529;
--text-color: #f8f9fa;
--border-color: #495057;
--error-color: #f8d7da;
--header-bg: #343a40;
}
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: var(--text-color);
background: var(--bg-color);
margin: 0;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.container {
max-width: 600px;
padding: 20px;
}
.error-container {
background: white;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.error-header {
background: var(--header-bg);
padding: 15px 20px;
border-bottom: 1px solid var(--border-color);
}
.error-title {
margin: 0;
color: var(--error-color);
font-size: 1.5rem;
}
.error-body {
padding: 30px 20px;
text-align: center;
}
.error-message {
font-size: 1.1rem;
margin-bottom: 20px;
}
.error-meta {
font-size: 0.9rem;
color: #6c757d;
margin-top: 20px;
}
@media (prefers-color-scheme: dark) {
.error-container {
background: #343a40;
}
}
</style>
</head>
<body>
<div class="container">
<div class="error-container">
<div class="error-header">
<h1 class="error-title">Ein Fehler ist aufgetreten</h1>
</div>
<div class="error-body">
<div class="error-message">
Es tut uns leid, aber es ist ein Fehler aufgetreten.
Unser Team wurde benachrichtigt und arbeitet an einer Lösung.
</div>
<div class="error-meta">
Request ID: $requestId
</div>
</div>
</div>
</div>
</body>
</html>