Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
@@ -14,7 +14,8 @@ final readonly class CliErrorHandler
|
||||
{
|
||||
public function __construct(
|
||||
private ?ConsoleOutput $output = null
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
@@ -25,7 +26,7 @@ final readonly class CliErrorHandler
|
||||
|
||||
public function handleError(int $severity, string $message, string $file, int $line): bool
|
||||
{
|
||||
if (!(error_reporting() & $severity)) {
|
||||
if (! (error_reporting() & $severity)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
@@ -18,4 +19,9 @@ final class DummyTemplateRenderer implements TemplateRenderer
|
||||
// seine eigene Fallback-Methode hat
|
||||
return '';
|
||||
}
|
||||
|
||||
public function renderPartial(RenderContext $context): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
use App\Framework\Http\RequestId;
|
||||
@@ -13,8 +15,7 @@ final readonly class ErrorContext
|
||||
public ErrorLevel $level,
|
||||
public RequestId $requestId,
|
||||
public array $additionalData = []
|
||||
)
|
||||
{
|
||||
) {
|
||||
$this->stackTrace = new StackTrace($exception);
|
||||
}
|
||||
|
||||
@@ -33,6 +34,7 @@ final readonly class ErrorContext
|
||||
{
|
||||
$class = get_class($this->exception);
|
||||
$parts = explode('\\', $class);
|
||||
|
||||
return end($parts);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
use App\Framework\Config\EnvKey;
|
||||
use App\Framework\Config\Environment;
|
||||
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\ErrorHandlerContext;
|
||||
use App\Framework\Exception\ExceptionContext;
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
use App\Framework\Exception\RequestContext;
|
||||
use App\Framework\Exception\SecurityException;
|
||||
use App\Framework\Exception\SecurityLogLevel;
|
||||
use App\Framework\Http\HttpResponse;
|
||||
use App\Framework\Exception\SystemContext;
|
||||
use App\Framework\Http\MiddlewareContext;
|
||||
use App\Framework\Http\RequestIdGenerator;
|
||||
use App\Framework\Http\Response;
|
||||
use App\Framework\Http\ResponseEmitter;
|
||||
use App\Framework\Http\Responses\RedirectResponse;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Performance\MemoryMonitor;
|
||||
use App\Framework\Validation\Exceptions\ValidationException;
|
||||
use App\Framework\Validation\ValidationFormHandler;
|
||||
use App\Framework\View\TemplateRenderer;
|
||||
use Throwable;
|
||||
|
||||
final readonly class ErrorHandler
|
||||
{
|
||||
private ErrorLogger $logger;
|
||||
|
||||
private SecurityEventHandler $securityHandler;
|
||||
|
||||
private bool $isDebugMode;
|
||||
|
||||
public function __construct(
|
||||
@@ -29,8 +44,8 @@ final readonly class ErrorHandler
|
||||
?Logger $logger = null,
|
||||
?bool $isDebugMode = null,
|
||||
?SecurityEventHandler $securityHandler = null
|
||||
){
|
||||
$this->isDebugMode = $isDebugMode ?? (bool)($_ENV['APP_DEBUG'] ?? false);
|
||||
) {
|
||||
$this->isDebugMode = $isDebugMode ?? $this->getDebugModeFromEnvironment();
|
||||
$this->logger = new ErrorLogger($logger);
|
||||
$this->securityHandler = $securityHandler ?? SecurityEventHandler::createDefault($logger);
|
||||
}
|
||||
@@ -38,16 +53,23 @@ final readonly class ErrorHandler
|
||||
public function register(): void
|
||||
{
|
||||
set_exception_handler($this->handleException(...));
|
||||
set_error_handler($this->handleError(...));;
|
||||
register_shutdown_function($this->handleShutdown(...));;
|
||||
set_error_handler($this->handleError(...));
|
||||
|
||||
register_shutdown_function($this->handleShutdown(...));
|
||||
|
||||
}
|
||||
|
||||
public function createHttpResponse(\Throwable $e, ?MiddlewareContext $context = null): HttpResponse
|
||||
public function createHttpResponse(\Throwable $e, ?MiddlewareContext $context = null): Response
|
||||
{
|
||||
$errorContext = $this->createErrorContext($e, $context);
|
||||
// Handle ValidationException with form-specific logic
|
||||
if ($e instanceof ValidationException) {
|
||||
return $this->handleValidationException($e, $context);
|
||||
}
|
||||
|
||||
// Standard Error Logging
|
||||
$this->logger->logError($errorContext);
|
||||
$errorHandlerContext = $this->createErrorHandlerContext($e, $context);
|
||||
|
||||
// Standard Error Logging mit ErrorHandlerContext
|
||||
$this->logger->logErrorHandlerContext($errorHandlerContext);
|
||||
|
||||
// Security Event Logging delegieren
|
||||
$this->securityHandler->handleIfSecurityException($e, $context);
|
||||
@@ -56,19 +78,35 @@ final readonly class ErrorHandler
|
||||
|
||||
$isApiRequest = $responseFactory->isApiRequest();
|
||||
|
||||
return $responseFactory->createResponse(
|
||||
$errorContext,
|
||||
return $responseFactory->createResponseFromHandlerContext(
|
||||
$errorHandlerContext,
|
||||
$this->isDebugMode,
|
||||
$isApiRequest
|
||||
);
|
||||
}
|
||||
|
||||
private function handleValidationException(ValidationException $exception, ?MiddlewareContext $context): Response
|
||||
{
|
||||
// Try to get ValidationFormHandler from container
|
||||
if ($this->container->has(ValidationFormHandler::class)) {
|
||||
$formHandler = $this->container->get(ValidationFormHandler::class);
|
||||
|
||||
return $formHandler->handle($exception, $context);
|
||||
}
|
||||
|
||||
error_log("ErrorHandler: ValidationFormHandler NOT found, using fallback");
|
||||
// Fallback: create basic redirect response
|
||||
$refererUrl = $context?->request?->server?->getRefererUri() ?? '/';
|
||||
|
||||
return new RedirectResponse($refererUrl);
|
||||
}
|
||||
|
||||
public function handleException(Throwable $e): never
|
||||
{
|
||||
$context = $this->createErrorContext($e);
|
||||
$errorHandlerContext = $this->createErrorHandlerContext($e);
|
||||
|
||||
// Logge den Fehler
|
||||
$this->logger->logError($context);
|
||||
// Logge den Fehler mit ErrorHandlerContext
|
||||
$this->logger->logErrorHandlerContext($errorHandlerContext);
|
||||
|
||||
// Security Event Logging delegieren
|
||||
$this->securityHandler->handleIfSecurityException($e);
|
||||
@@ -79,8 +117,8 @@ final readonly class ErrorHandler
|
||||
// Bestimme ob es ein API-Request ist
|
||||
$isApiRequest = $responseFactory->isApiRequest();
|
||||
|
||||
// Erstelle eine Response basierend auf dem Kontext
|
||||
$response = $responseFactory->createResponse($context, $this->isDebugMode, $isApiRequest);
|
||||
// Erstelle eine Response basierend auf dem ErrorHandlerContext
|
||||
$response = $responseFactory->createResponseFromHandlerContext($errorHandlerContext, $this->isDebugMode, $isApiRequest);
|
||||
|
||||
// Sende die Fehlermeldung
|
||||
$this->emitter->emit($response);
|
||||
@@ -113,24 +151,112 @@ final readonly class ErrorHandler
|
||||
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
|
||||
private function createErrorHandlerContext(Throwable $exception, ?MiddlewareContext $context = null): ErrorHandlerContext
|
||||
{
|
||||
$level = $this->determineErrorLevel($exception);
|
||||
// ExceptionContext aus der Exception extrahieren (falls FrameworkException)
|
||||
$exceptionContext = ExceptionContext::empty();
|
||||
if ($exception instanceof FrameworkException) {
|
||||
$exceptionContext = $exception->getContext();
|
||||
|
||||
$requestId = $context->requestId ?? $this->requestIdGenerator->generate();
|
||||
// Ensure framework exceptions also have the original exception for stack traces
|
||||
if (! isset($exceptionContext->data['original_exception'])) {
|
||||
$exceptionContext = $exceptionContext->withData([
|
||||
'original_exception' => $exception,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// For non-framework exceptions, add the actual exception message and the original exception
|
||||
$exceptionContext = $exceptionContext->withData([
|
||||
'exception_message' => $exception->getMessage(),
|
||||
'exception_file' => $exception->getFile(),
|
||||
'exception_line' => $exception->getLine(),
|
||||
'original_exception' => $exception, // Preserve original exception for stack trace
|
||||
]);
|
||||
}
|
||||
|
||||
return new ErrorContext(
|
||||
exception: $exception,
|
||||
level: $level,
|
||||
requestId: $requestId,
|
||||
additionalData: [
|
||||
'timestamp' => date('c'),
|
||||
'memory_usage' => memory_get_peak_usage(true),
|
||||
]
|
||||
// RequestContext aus MiddlewareContext oder Globals erstellen
|
||||
$requestContext = $this->createRequestContext($context);
|
||||
|
||||
// SystemContext mit aktuellen System-Metriken erstellen
|
||||
$memoryMonitor = $this->container->has(MemoryMonitor::class)
|
||||
? $this->container->get(MemoryMonitor::class)
|
||||
: null;
|
||||
$systemContext = SystemContext::current($memoryMonitor);
|
||||
|
||||
// Metadata für verschiedene Exception-Typen
|
||||
$metadata = $this->createExceptionMetadata($exception);
|
||||
|
||||
return ErrorHandlerContext::create(
|
||||
$exceptionContext,
|
||||
$requestContext,
|
||||
$systemContext,
|
||||
$metadata
|
||||
);
|
||||
}
|
||||
|
||||
private function determineErrorLevel(Throwable $exception):ErrorLevel
|
||||
private function createRequestContext(?MiddlewareContext $context): RequestContext
|
||||
{
|
||||
if ($context?->request) {
|
||||
$request = $context->request;
|
||||
|
||||
return RequestContext::create(
|
||||
clientIp: (string) $request->server->getClientIp(),
|
||||
userAgent: $request->server->getUserAgent(),
|
||||
requestMethod: $request->method->value,
|
||||
requestUri: (string) $request->getUri(),
|
||||
hostIp: $request->server->get('SERVER_ADDR', 'unknown'),
|
||||
hostname: $request->server->getHttpHost() ?: 'localhost',
|
||||
protocol: (string) $request->server->getProtocol()->value,
|
||||
port: (string) $request->server->getServerPort(),
|
||||
requestId: $request->id
|
||||
);
|
||||
}
|
||||
|
||||
return RequestContext::fromGlobals();
|
||||
}
|
||||
|
||||
private function createExceptionMetadata(Throwable $exception): array
|
||||
{
|
||||
$metadata = [
|
||||
'exception_class' => get_class($exception),
|
||||
'error_level' => $this->determineErrorLevel($exception)->name,
|
||||
];
|
||||
|
||||
// HTTP-Status-Code basierend auf Exception-Typ
|
||||
$metadata['http_status'] = match (true) {
|
||||
$exception instanceof \App\Framework\Exception\Authentication\InvalidCredentialsException,
|
||||
$exception instanceof \App\Framework\Exception\Authentication\TokenExpiredException,
|
||||
$exception instanceof \App\Framework\Exception\Authentication\SessionTimeoutException => 401,
|
||||
|
||||
$exception instanceof \App\Framework\Exception\Authentication\AccountLockedException => 423,
|
||||
$exception instanceof \App\Framework\Exception\Authentication\InsufficientPrivilegesException => 403,
|
||||
|
||||
$exception instanceof \App\Framework\Exception\Http\RouteNotFoundException => 404,
|
||||
$exception instanceof \App\Framework\Exception\Http\MalformedJsonException,
|
||||
$exception instanceof \App\Framework\Exception\Http\InvalidContentTypeException,
|
||||
$exception instanceof \App\Framework\Exception\Security\SqlInjectionAttemptException,
|
||||
$exception instanceof \App\Framework\Exception\Security\XssAttemptException,
|
||||
$exception instanceof \App\Framework\Exception\Security\PathTraversalAttemptException => 400,
|
||||
|
||||
$exception instanceof \App\Framework\Exception\Http\OversizedRequestException => 413,
|
||||
$exception instanceof \App\Framework\Exception\Http\RateLimitExceededException => 429,
|
||||
|
||||
default => 500,
|
||||
};
|
||||
|
||||
// Zusätzliche Header für spezielle Exceptions
|
||||
if ($exception instanceof \App\Framework\Exception\Http\RateLimitExceededException) {
|
||||
$metadata['additional_headers'] = $exception->getRateLimitHeaders();
|
||||
}
|
||||
|
||||
if ($exception instanceof \App\Framework\Exception\Http\InvalidContentTypeException) {
|
||||
$metadata['additional_headers'] = $exception->getResponseHeaders();
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
private function determineErrorLevel(Throwable $exception): ErrorLevel
|
||||
{
|
||||
return match(true) {
|
||||
$exception instanceof \Error => ErrorLevel::CRITICAL,
|
||||
@@ -156,7 +282,6 @@ final readonly class ErrorHandler
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Bestimmt den Fehler-Level basierend auf dem ErrorException-Severity-Level
|
||||
*/
|
||||
@@ -176,6 +301,20 @@ final readonly class ErrorHandler
|
||||
* Erstellt eine ErrorResponseFactory mit Renderern
|
||||
* Versucht den TemplateRenderer aus dem Container zu laden, falls vorhanden
|
||||
*/
|
||||
private function getDebugModeFromEnvironment(): bool
|
||||
{
|
||||
try {
|
||||
if ($this->container->has(Environment::class)) {
|
||||
$environment = $this->container->get(Environment::class);
|
||||
return $environment->getBool(EnvKey::APP_DEBUG, false);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
// Sicherer Fallback für Production
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function createResponseFactory(): ErrorResponseFactory
|
||||
{
|
||||
$templateRenderer = null;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
enum ErrorLevel
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
use App\Framework\Exception\ErrorHandlerContext;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
|
||||
@@ -13,10 +15,11 @@ final readonly class ErrorLogger
|
||||
{
|
||||
public function __construct(
|
||||
private ?Logger $logger = null
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Loggt einen Fehler basierend auf dem ErrorContext
|
||||
* Loggt einen Fehler basierend auf dem ErrorContext (Legacy)
|
||||
*/
|
||||
public function logError(ErrorContext $context): void
|
||||
{
|
||||
@@ -34,7 +37,7 @@ final readonly class ErrorLogger
|
||||
'exception' => $exception,
|
||||
'trace' => $exception->getTraceAsString(),
|
||||
'requestId' => $context->requestId,
|
||||
'additionalData' => $context->additionalData
|
||||
'additionalData' => $context->additionalData,
|
||||
];
|
||||
|
||||
if ($this->logger !== null) {
|
||||
@@ -46,9 +49,134 @@ final readonly class ErrorLogger
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loggt einen Fehler basierend auf dem ErrorHandlerContext
|
||||
*/
|
||||
public function logErrorHandlerContext(ErrorHandlerContext $context): void
|
||||
{
|
||||
// Structured Logging mit allen Context-Informationen
|
||||
$logData = $context->forLogging();
|
||||
|
||||
// Security Event Logging falls Security-Event
|
||||
if ($context->exception->metadata['security_event'] ?? false) {
|
||||
$this->logSecurityEvent($context);
|
||||
}
|
||||
|
||||
// Standard Error Logging
|
||||
$message = sprintf(
|
||||
'[%s] %s: %s',
|
||||
$context->request->requestId?->toString() ?? 'NO-REQUEST-ID',
|
||||
$context->exception->component ?? 'Application',
|
||||
$this->extractExceptionMessage($context)
|
||||
);
|
||||
|
||||
// Debug: Log the actual exception class
|
||||
if (isset($context->metadata['exception_class'])) {
|
||||
error_log("DEBUG: Exception class: " . $context->metadata['exception_class']);
|
||||
}
|
||||
|
||||
if ($this->logger !== null) {
|
||||
$logLevel = $this->determineLogLevel($context);
|
||||
$this->logger->log($logLevel, $message, $logData);
|
||||
} else {
|
||||
// Fallback auf error_log
|
||||
$this->logHandlerContextToErrorLog($context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback-Methode für Logging ohne Framework Logger
|
||||
* Loggt Security Events im OWASP-Format
|
||||
*/
|
||||
private function logSecurityEvent(ErrorHandlerContext $context): void
|
||||
{
|
||||
$securityLog = $context->toSecurityEventFormat($_ENV['APP_NAME'] ?? 'app');
|
||||
|
||||
if ($this->logger !== null) {
|
||||
$this->logger->log(LogLevel::WARNING, 'Security Event', $securityLog);
|
||||
} else {
|
||||
error_log('SECURITY_EVENT: ' . json_encode($securityLog, JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert Exception-Message aus ErrorHandlerContext
|
||||
*/
|
||||
private function extractExceptionMessage(ErrorHandlerContext $context): string
|
||||
{
|
||||
// Versuche Exception aus context.exception.data zu extrahieren
|
||||
$exceptionData = $context->exception->data;
|
||||
|
||||
if (isset($exceptionData['exception_message'])) {
|
||||
return $exceptionData['exception_message'];
|
||||
}
|
||||
|
||||
// Debug: Log what data we have
|
||||
error_log("DEBUG: Exception data: " . json_encode($exceptionData));
|
||||
|
||||
// Fallback: Operation und Component verwenden
|
||||
$operation = $context->exception->operation ?? 'unknown_operation';
|
||||
$component = $context->exception->component ?? 'unknown_component';
|
||||
|
||||
return "Error in {$component} during {$operation}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestimmt Log-Level aus ErrorHandlerContext
|
||||
*/
|
||||
private function determineLogLevel(ErrorHandlerContext $context): LogLevel
|
||||
{
|
||||
// Security Events haben höhere Priorität
|
||||
if ($context->exception->metadata['security_event'] ?? false) {
|
||||
$securityLevel = $context->exception->metadata['security_level'] ?? 'ERROR';
|
||||
|
||||
return match ($securityLevel) {
|
||||
'DEBUG' => LogLevel::DEBUG,
|
||||
'INFO' => LogLevel::INFO,
|
||||
'WARN' => LogLevel::WARNING,
|
||||
'ERROR' => LogLevel::ERROR,
|
||||
'FATAL' => LogLevel::CRITICAL,
|
||||
default => LogLevel::ERROR,
|
||||
};
|
||||
}
|
||||
|
||||
// Standard Log-Level aus Metadata
|
||||
$errorLevel = $context->exception->metadata['error_level'] ?? 'ERROR';
|
||||
|
||||
return match ($errorLevel) {
|
||||
'CRITICAL' => LogLevel::CRITICAL,
|
||||
'ERROR' => LogLevel::ERROR,
|
||||
'WARNING' => LogLevel::WARNING,
|
||||
'NOTICE' => LogLevel::NOTICE,
|
||||
'INFO' => LogLevel::INFO,
|
||||
'DEBUG' => LogLevel::DEBUG,
|
||||
default => LogLevel::ERROR,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback-Methode für ErrorHandlerContext ohne Framework Logger
|
||||
*/
|
||||
private function logHandlerContextToErrorLog(ErrorHandlerContext $context): void
|
||||
{
|
||||
$message = sprintf(
|
||||
'[%s] [%s] %s: %s',
|
||||
date('Y-m-d H:i:s'),
|
||||
$context->exception->metadata['error_level'] ?? 'ERROR',
|
||||
$context->exception->component ?? 'Application',
|
||||
$this->extractExceptionMessage($context)
|
||||
);
|
||||
|
||||
error_log($message);
|
||||
|
||||
// Security Events auch separat loggen
|
||||
if ($context->exception->metadata['security_event'] ?? false) {
|
||||
$securityLog = $context->toSecurityEventJson($_ENV['APP_NAME'] ?? 'app');
|
||||
error_log('SECURITY_EVENT: ' . $securityLog);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback-Methode für Logging ohne Framework Logger (Legacy)
|
||||
*/
|
||||
private function logToErrorLog(ErrorContext $context): void
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
@@ -4,8 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
use App\Framework\Exception\SecurityException;
|
||||
use App\Framework\Exception\ErrorHandlerContext;
|
||||
use App\Framework\Exception\SecurityException;
|
||||
|
||||
/**
|
||||
* Manager für Security-Alerts - Integration zu externen Alert-Systemen
|
||||
@@ -14,7 +14,8 @@ final readonly class SecurityAlertManager
|
||||
{
|
||||
public function __construct(
|
||||
private array $alertChannels = []
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet Alert über alle konfigurierten Kanäle
|
||||
@@ -57,10 +58,10 @@ final readonly class SecurityAlertManager
|
||||
'user_agent' => $context->request->userAgent,
|
||||
'request_uri' => $context->request->requestUri,
|
||||
'timestamp' => date('c'),
|
||||
'request_id' => $context->request->requestId
|
||||
'request_id' => $context->request->requestId,
|
||||
],
|
||||
'event_data' => $securityEvent->toArray(),
|
||||
'context' => $context->forLogging()
|
||||
'context' => $context->forLogging(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
use App\Framework\Exception\SecurityException;
|
||||
use App\Framework\Exception\ErrorHandlerContext;
|
||||
use App\Framework\Exception\SecurityException;
|
||||
use App\Framework\Http\MiddlewareContext;
|
||||
use App\Framework\Logging\Logger;
|
||||
use Throwable;
|
||||
@@ -18,7 +18,8 @@ final readonly class SecurityEventHandler
|
||||
public function __construct(
|
||||
private SecurityEventLogger $securityLogger,
|
||||
private ?SecurityAlertManager $alertManager = null
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Behandelt Security-Exception und führt entsprechendes Logging durch
|
||||
@@ -29,7 +30,7 @@ final readonly class SecurityEventHandler
|
||||
): void {
|
||||
try {
|
||||
// Erstelle ErrorHandlerContext für OWASP-Format
|
||||
$errorHandlerContext = ErrorHandlerContext::fromException($exception, $context);
|
||||
$errorHandlerContext = $this->createErrorHandlerContext($exception, $context);
|
||||
|
||||
// Führe Security-Logging durch
|
||||
$this->securityLogger->logSecurityEvent($exception, $errorHandlerContext);
|
||||
@@ -52,11 +53,12 @@ final readonly class SecurityEventHandler
|
||||
Throwable $exception,
|
||||
?MiddlewareContext $context = null
|
||||
): bool {
|
||||
if (!$exception instanceof SecurityException) {
|
||||
if (! $exception instanceof SecurityException) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->handleSecurityException($exception, $context);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -99,7 +101,7 @@ final readonly class SecurityEventHandler
|
||||
'description' => $originalException->getSecurityEvent()->getDescription(),
|
||||
'level' => 'FATAL',
|
||||
'audit_failure' => true,
|
||||
'original_error' => $loggingError->getMessage()
|
||||
'original_error' => $loggingError->getMessage(),
|
||||
];
|
||||
|
||||
error_log('SECURITY_EVENT_FALLBACK: ' . json_encode($minimalLog));
|
||||
@@ -110,6 +112,26 @@ final readonly class SecurityEventHandler
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt ErrorHandlerContext aus Exception und MiddlewareContext
|
||||
*/
|
||||
private function createErrorHandlerContext(
|
||||
SecurityException $exception,
|
||||
?MiddlewareContext $context = null
|
||||
): ErrorHandlerContext {
|
||||
// Extrahiere Metadata aus dem MiddlewareContext
|
||||
$metadata = [];
|
||||
if ($context) {
|
||||
$metadata = [
|
||||
'request_id' => $context->request->id->toString(),
|
||||
'middleware_context' => true,
|
||||
];
|
||||
}
|
||||
|
||||
// Verwende die bestehende fromException Methode mit korrekten Metadata
|
||||
return ErrorHandlerContext::fromException($exception, $metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory-Methode mit Standard-Konfiguration
|
||||
*/
|
||||
|
||||
@@ -4,8 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
use App\Framework\Exception\SecurityException;
|
||||
use App\Framework\Exception\ErrorHandlerContext;
|
||||
use App\Framework\Exception\SecurityException;
|
||||
use App\Framework\Exception\SecurityLogLevel;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Logging\LogLevel;
|
||||
@@ -18,7 +18,8 @@ final readonly class SecurityEventLogger
|
||||
public function __construct(
|
||||
private ?Logger $logger = null,
|
||||
private string $applicationId = 'app'
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Loggt Security-Event im OWASP-Format
|
||||
@@ -45,7 +46,7 @@ final readonly class SecurityEventLogger
|
||||
'requires_alert' => $securityEvent->requiresAlert(),
|
||||
'owasp_format' => $owaspLog,
|
||||
'event_data' => $securityEvent->toArray(),
|
||||
'context_data' => $context->forLogging()
|
||||
'context_data' => $context->forLogging(),
|
||||
]
|
||||
);
|
||||
} else {
|
||||
@@ -100,7 +101,7 @@ final readonly class SecurityEventLogger
|
||||
'region' => $_ENV['AWS_REGION'] ?? 'unknown',
|
||||
'geo' => $_ENV['GEO_LOCATION'] ?? 'unknown',
|
||||
'category' => $securityEvent->getCategory(),
|
||||
'requires_alert' => $securityEvent->requiresAlert()
|
||||
'requires_alert' => $securityEvent->requiresAlert(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -122,7 +123,7 @@ final readonly class SecurityEventLogger
|
||||
'request_method' => $context->request->requestMethod,
|
||||
'timestamp' => date('c'),
|
||||
'request_id' => $context->request->requestId,
|
||||
'event_data' => $exception->getSecurityEvent()->toArray()
|
||||
'event_data' => $exception->getSecurityEvent()->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
use ArrayAccess;
|
||||
use ArrayIterator;
|
||||
use Countable;
|
||||
use IteratorAggregate;
|
||||
use ArrayIterator;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
@@ -15,7 +17,7 @@ use Traversable;
|
||||
final class StackTrace implements ArrayAccess, IteratorAggregate, Countable
|
||||
{
|
||||
/** @var TraceItem[] */
|
||||
private(set) array $items = [];
|
||||
public private(set) array $items = [];
|
||||
|
||||
/**
|
||||
* Erstellt ein neues StackTrace-Objekt aus einer Exception
|
||||
@@ -147,6 +149,7 @@ final class StackTrace implements ArrayAccess, IteratorAggregate, Countable
|
||||
foreach ($this->items as $index => $item) {
|
||||
$result .= "#{$index} {$item}\n";
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
/**
|
||||
@@ -15,7 +17,8 @@ final readonly class TraceItem
|
||||
public ?string $type,
|
||||
public array $args,
|
||||
public bool $isExceptionOrigin
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Dateipfad relativ zum Projekt-Root zurück
|
||||
@@ -33,11 +36,12 @@ final readonly class TraceItem
|
||||
*/
|
||||
public function getShortClass(): ?string
|
||||
{
|
||||
if (!$this->class) {
|
||||
if (! $this->class) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parts = explode('\\', $this->class);
|
||||
|
||||
return end($parts);
|
||||
}
|
||||
|
||||
@@ -46,11 +50,12 @@ final readonly class TraceItem
|
||||
*/
|
||||
public function getNamespace(): ?string
|
||||
{
|
||||
if (!$this->class) {
|
||||
if (! $this->class) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$lastSlash = strrpos($this->class, '\\');
|
||||
|
||||
return $lastSlash ? substr($this->class, 0, $lastSlash) : null;
|
||||
}
|
||||
|
||||
@@ -126,6 +131,7 @@ final readonly class TraceItem
|
||||
private function removeNamespace(string $class): string
|
||||
{
|
||||
$lastSlash = strrpos($class, '\\');
|
||||
|
||||
return $lastSlash ? substr($class, $lastSlash + 1) : $class;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling\View;
|
||||
|
||||
use App\Framework\ErrorHandling\ErrorContext;
|
||||
use App\Framework\ErrorHandling\ExceptionConverter;
|
||||
use App\Framework\Exception\ErrorHandlerContext;
|
||||
|
||||
/**
|
||||
* Renderer für API-Fehler im JSON-Format
|
||||
@@ -12,7 +14,72 @@ use App\Framework\ErrorHandling\ExceptionConverter;
|
||||
final readonly class ApiErrorRenderer implements ErrorViewRendererInterface
|
||||
{
|
||||
/**
|
||||
* Rendert einen API-Fehler als JSON
|
||||
* Rendert einen API-Fehler als JSON basierend auf ErrorHandlerContext
|
||||
*/
|
||||
public function renderFromHandlerContext(ErrorHandlerContext $context, bool $isDebug = false): string
|
||||
{
|
||||
$responseData = [
|
||||
'error' => true,
|
||||
'code' => $context->metadata['http_status'] ?? 500,
|
||||
'message' => $this->getUserMessage($context, $isDebug),
|
||||
'requestId' => $context->request->requestId,
|
||||
'timestamp' => date('c'),
|
||||
];
|
||||
|
||||
// Exception-spezifische Daten hinzufügen
|
||||
if ($errorCode = $this->getErrorCode($context)) {
|
||||
$responseData['errorCode'] = $errorCode;
|
||||
}
|
||||
|
||||
// Security Event Informationen (nur für interne Zwecke)
|
||||
if ($context->exception->metadata['security_event'] ?? false) {
|
||||
$responseData['type'] = 'security_event';
|
||||
|
||||
if (! $isDebug) {
|
||||
// In Production keine Details von Security Events preisgeben
|
||||
$responseData['message'] = 'Invalid request. Please check your input and try again.';
|
||||
}
|
||||
}
|
||||
|
||||
// Rate Limit spezifische Informationen
|
||||
if (isset($context->metadata['additional_headers']['X-RateLimit-Limit'])) {
|
||||
$responseData['rateLimit'] = [
|
||||
'limit' => (int) $context->metadata['additional_headers']['X-RateLimit-Limit'],
|
||||
'remaining' => (int) $context->metadata['additional_headers']['X-RateLimit-Remaining'],
|
||||
'reset' => (int) $context->metadata['additional_headers']['X-RateLimit-Reset'],
|
||||
'retryAfter' => (int) ($context->metadata['additional_headers']['Retry-After'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
// Debug-Informationen
|
||||
if ($isDebug) {
|
||||
$responseData['debug'] = [
|
||||
'operation' => $context->exception->operation,
|
||||
'component' => $context->exception->component,
|
||||
'exception_class' => $context->metadata['exception_class'],
|
||||
'client_ip' => $context->request->clientIp,
|
||||
'request_method' => $context->request->requestMethod,
|
||||
'request_uri' => $context->request->requestUri,
|
||||
'user_agent' => $context->request->userAgent,
|
||||
'memory_usage' => $context->system->memoryUsage ?? 0,
|
||||
'execution_time' => $context->system->executionTime ?? 0,
|
||||
];
|
||||
|
||||
// Exception-spezifische Debug-Daten
|
||||
if (! empty($context->exception->data)) {
|
||||
$responseData['debug']['exception_data'] = $context->exception->data;
|
||||
}
|
||||
|
||||
if (! empty($context->exception->debug)) {
|
||||
$responseData['debug']['exception_debug'] = $context->exception->debug;
|
||||
}
|
||||
}
|
||||
|
||||
return json_encode($responseData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert einen API-Fehler als JSON (Legacy)
|
||||
*/
|
||||
public function render(ErrorContext $context, bool $isDebug = false): string
|
||||
{
|
||||
@@ -36,4 +103,52 @@ final readonly class ApiErrorRenderer implements ErrorViewRendererInterface
|
||||
|
||||
return json_encode($responseData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert benutzerfreundliche Nachricht aus ErrorHandlerContext
|
||||
*/
|
||||
private function getUserMessage(ErrorHandlerContext $context, bool $isDebug): string
|
||||
{
|
||||
// Bei Debug-Modus die technische Nachricht verwenden
|
||||
if ($isDebug && isset($context->exception->data['exception_message'])) {
|
||||
return $context->exception->data['exception_message'];
|
||||
}
|
||||
|
||||
// Exception-spezifische Nachrichten verwenden falls verfügbar
|
||||
if (isset($context->exception->data['user_message'])) {
|
||||
return $context->exception->data['user_message'];
|
||||
}
|
||||
|
||||
// Fallback auf generische Nachrichten basierend auf HTTP-Status
|
||||
return match ($context->metadata['http_status'] ?? 500) {
|
||||
400 => 'Bad Request: Invalid input provided.',
|
||||
401 => 'Unauthorized: Authentication required.',
|
||||
403 => 'Forbidden: Insufficient permissions.',
|
||||
404 => 'Not Found: The requested resource was not found.',
|
||||
413 => 'Payload Too Large: Request size exceeds limit.',
|
||||
415 => 'Unsupported Media Type: Invalid content type.',
|
||||
423 => 'Locked: Account or resource is temporarily locked.',
|
||||
429 => 'Too Many Requests: Rate limit exceeded.',
|
||||
500 => 'Internal Server Error: An unexpected error occurred.',
|
||||
default => 'An error occurred while processing your request.',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert Error-Code aus ErrorHandlerContext
|
||||
*/
|
||||
private function getErrorCode(ErrorHandlerContext $context): ?string
|
||||
{
|
||||
// Aus Exception-Daten extrahieren
|
||||
if (isset($context->exception->data['error_code'])) {
|
||||
return $context->exception->data['error_code'];
|
||||
}
|
||||
|
||||
// Aus Metadata extrahieren
|
||||
if (isset($context->exception->metadata['error_code'])) {
|
||||
return $context->exception->metadata['error_code'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling\View;
|
||||
|
||||
use App\Framework\ErrorHandling\ErrorContext;
|
||||
use App\Framework\ErrorHandling\ExceptionConverter;
|
||||
use App\Framework\Exception\ErrorHandlerContext;
|
||||
use App\Framework\Http\Headers;
|
||||
use App\Framework\Http\HttpResponse;
|
||||
use App\Framework\Http\Status;
|
||||
@@ -17,10 +19,59 @@ final readonly class ErrorResponseFactory
|
||||
public function __construct(
|
||||
private ErrorViewRendererInterface $htmlRenderer,
|
||||
private ErrorViewRendererInterface $apiRenderer
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine HTTP-Response basierend auf dem ErrorContext
|
||||
* Erstellt eine HTTP-Response basierend auf dem ErrorHandlerContext
|
||||
*/
|
||||
public function createResponseFromHandlerContext(ErrorHandlerContext $context, bool $isDebug = false, bool $isApiRequest = false): HttpResponse
|
||||
{
|
||||
// HTTP-Status aus Metadata extrahieren
|
||||
$status = $context->metadata['http_status'] ?? 500;
|
||||
|
||||
// Headers aufbauen
|
||||
$headers = new Headers();
|
||||
|
||||
// Content-Type basierend auf Request-Typ
|
||||
if ($isApiRequest) {
|
||||
$headers = $headers->with('Content-Type', 'application/json; charset=utf-8');
|
||||
$body = $this->apiRenderer->renderFromHandlerContext($context, $isDebug);
|
||||
} else {
|
||||
$headers = $headers->with('Content-Type', 'text/html; charset=utf-8');
|
||||
$body = $this->htmlRenderer->renderFromHandlerContext($context, $isDebug);
|
||||
}
|
||||
|
||||
// Exception-spezifische Header hinzufügen
|
||||
if (isset($context->metadata['additional_headers'])) {
|
||||
foreach ($context->metadata['additional_headers'] as $name => $value) {
|
||||
$headers = $headers->with($name, $value);
|
||||
}
|
||||
}
|
||||
|
||||
// Standard Security Header
|
||||
$headers = $headers
|
||||
->with('Cache-Control', 'no-store, no-cache, must-revalidate')
|
||||
->with('X-Content-Type-Options', 'nosniff')
|
||||
->with('X-Frame-Options', 'DENY');
|
||||
|
||||
// Security Event Header für Security-Exceptions
|
||||
if ($context->exception->metadata['security_event'] ?? false) {
|
||||
$headers = $headers->with('X-Security-Event', 'true');
|
||||
|
||||
// Request-ID für Security-Tracking
|
||||
$headers = $headers->with('X-Request-ID', $context->request->requestId?->toString() ?? 'unknown');
|
||||
}
|
||||
|
||||
return new HttpResponse(
|
||||
status: Status::from($status),
|
||||
headers: $headers,
|
||||
body: $body
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine HTTP-Response basierend auf dem ErrorContext (Legacy)
|
||||
*/
|
||||
public function createResponse(ErrorContext $context, bool $isDebug = false, bool $isApiRequest = false): HttpResponse
|
||||
{
|
||||
@@ -39,7 +90,7 @@ final readonly class ErrorResponseFactory
|
||||
$headers = $headers->with('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||
|
||||
return new HttpResponse(
|
||||
status: $status,
|
||||
status: Status::from($status),
|
||||
headers: $headers,
|
||||
body: $body
|
||||
);
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling\View;
|
||||
|
||||
use App\Framework\ErrorHandling\ErrorContext;
|
||||
use App\Framework\ErrorHandling\StackTrace;
|
||||
use App\Framework\ErrorHandling\TraceItem;
|
||||
use App\Framework\Exception\ErrorHandlerContext;
|
||||
use App\Framework\Meta\MetaData;
|
||||
use App\Framework\Meta\OpenGraphTypeWebsite;
|
||||
use App\Framework\SyntaxHighlighter\FileHighlighter;
|
||||
use App\Framework\View\RawHtml;
|
||||
use App\Framework\View\RenderContext;
|
||||
use App\Framework\View\TemplateRenderer;
|
||||
use Throwable;
|
||||
@@ -20,10 +25,149 @@ final readonly class ErrorTemplateRenderer implements ErrorViewRendererInterface
|
||||
private TemplateRenderer $renderer,
|
||||
private string $debugTemplate = 'debug',
|
||||
private string $productionTemplate = 'production'
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert eine Fehlerseite basierend auf dem ErrorContext
|
||||
* Rendert eine Fehlerseite basierend auf dem ErrorHandlerContext
|
||||
*/
|
||||
public function renderFromHandlerContext(ErrorHandlerContext $context, bool $isDebug = false): string
|
||||
{
|
||||
$template = $isDebug ? $this->debugTemplate : $this->productionTemplate;
|
||||
|
||||
try {
|
||||
// Sichere Datenaufbereitung für ErrorHandlerContext
|
||||
$safeData = [
|
||||
'errorClass' => $context->metadata['exception_class'] ?? 'Unknown',
|
||||
'errorMessage' => $this->extractErrorMessage($context),
|
||||
'error' => [
|
||||
'message' => $this->extractErrorMessage($context),
|
||||
'operation' => $context->exception->operation ?? 'unknown',
|
||||
'component' => $context->exception->component ?? 'Application',
|
||||
'class' => $context->metadata['exception_class'] ?? 'Unknown',
|
||||
],
|
||||
'requestId' => $context->request->requestId ?? 'NO-REQUEST-ID',
|
||||
'timestamp' => date('c'),
|
||||
'level' => $context->metadata['error_level'] ?? 'ERROR',
|
||||
'memory' => $context->system->memoryUsage ?? 0,
|
||||
'httpStatus' => $context->metadata['http_status'] ?? 500,
|
||||
'clientIp' => $context->request->clientIp,
|
||||
'requestUri' => $context->request->requestUri,
|
||||
'userAgent' => (string) $context->request->userAgent,
|
||||
];
|
||||
|
||||
// Security Event spezifische Daten
|
||||
if ($context->exception->metadata['security_event'] ?? false) {
|
||||
$safeData['securityEvent'] = [
|
||||
'type' => $context->exception->metadata['attack_type'] ?? 'unknown',
|
||||
'category' => $context->exception->metadata['category'] ?? 'security',
|
||||
'requiresAlert' => $context->exception->metadata['requires_alert'] ?? false,
|
||||
];
|
||||
|
||||
// In Production keine Security Details anzeigen
|
||||
if (! $isDebug) {
|
||||
$safeData['errorMessage'] = 'Security violation detected. Access denied.';
|
||||
$safeData['error']['message'] = 'Security violation detected. Access denied.';
|
||||
}
|
||||
}
|
||||
|
||||
// Debug-spezifische Daten
|
||||
if ($isDebug) {
|
||||
$safeData['debug'] = [
|
||||
'exception_data' => $context->exception->data,
|
||||
'exception_debug' => $context->exception->debug,
|
||||
'system_data' => $context->system->data ?? [],
|
||||
'execution_time' => $context->system->executionTime ?? 0,
|
||||
];
|
||||
|
||||
// Dependency Injection spezifische Informationen
|
||||
if (isset($context->exception->data['dependencyChain'])) {
|
||||
$chainDisplay = implode(' → ', $context->exception->data['dependencyChain']) . ' → ' . ($context->exception->data['class'] ?? 'Unknown');
|
||||
$targetClass = $context->exception->data['class'] ?? 'Unknown';
|
||||
|
||||
$dependencyHtml = '<div class="dependency-info" style="margin-top: 20px; padding: 15px; background: #fff3cd; border-left: 4px solid #ffc107; border-radius: 4px;">' .
|
||||
'<h3 style="margin: 0 0 10px 0; color: #856404;">🔄 Zyklische Abhängigkeit</h3>' .
|
||||
'<p style="margin: 0 0 10px 0; font-family: monospace; font-size: 0.9rem; color: #856404;">' .
|
||||
'<strong>Abhängigkeitskette:</strong><br>' .
|
||||
htmlspecialchars($chainDisplay, ENT_QUOTES, 'UTF-8') .
|
||||
'</p>' .
|
||||
'<p style="margin: 0; font-size: 0.85rem; color: #856404;">' .
|
||||
'Die Klasse <code>' . htmlspecialchars($targetClass, ENT_QUOTES, 'UTF-8') . '</code> kann nicht instanziiert werden, ' .
|
||||
'da sie Teil einer zyklischen Abhängigkeit ist.' .
|
||||
'</p>' .
|
||||
'</div>';
|
||||
|
||||
$safeData['dependencyInfo'] = RawHtml::from($dependencyHtml);
|
||||
}
|
||||
|
||||
// Code-Highlighter für Debug-Template
|
||||
$file = $context->exception->data['exception_file'] ?? null;
|
||||
$line = $context->exception->data['exception_line'] ?? null;
|
||||
|
||||
if ($file && $line && is_numeric($line)) {
|
||||
$errorLine = (int)$line;
|
||||
$codeHtml = new FileHighlighter()($file, max(1, $errorLine - 10), 20, $errorLine);
|
||||
$safeData['code'] = RawHtml::from($codeHtml);
|
||||
} else {
|
||||
$safeData['code'] = RawHtml::from('<div style="padding: 20px; background: #f8f9fa; border-radius: 4px; color: #6c757d;">Code-Vorschau nicht verfügbar</div>');
|
||||
}
|
||||
|
||||
// Stack Trace für Debug-Template
|
||||
// Prüfe zuerst, ob eine ursprüngliche Exception verfügbar ist
|
||||
if (isset($context->exception->data['original_exception']) && $context->exception->data['original_exception'] instanceof \Throwable) {
|
||||
// Verwende die ursprüngliche Exception für den Stack Trace
|
||||
$originalException = $context->exception->data['original_exception'];
|
||||
$stackTrace = new StackTrace($originalException);
|
||||
$safeData['trace'] = $stackTrace->getItems();
|
||||
} elseif (isset($context->exception->data['previous_exception']) && $context->exception->data['previous_exception'] instanceof \Throwable) {
|
||||
// Alternative: Prüfe auf previous_exception
|
||||
$stackTrace = new StackTrace($context->exception->data['previous_exception']);
|
||||
$safeData['trace'] = $stackTrace->getItems();
|
||||
} else {
|
||||
// Fallback: Erstelle minimalen Trace aus verfügbaren Daten
|
||||
$safeData['trace'] = [];
|
||||
|
||||
// Suche nach verfügbaren Trace-Informationen
|
||||
$file = $context->exception->data['exception_file'] ?? null;
|
||||
$line = $context->exception->data['exception_line'] ?? null;
|
||||
|
||||
if ($file && $line) {
|
||||
$safeData['trace'] = [
|
||||
new TraceItem(
|
||||
file: $file,
|
||||
line: (int)$line,
|
||||
function: null,
|
||||
class: null,
|
||||
type: null,
|
||||
args: [],
|
||||
isExceptionOrigin: true
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$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->createFallbackErrorPageFromHandlerContext($context, $isDebug, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert eine Fehlerseite basierend auf dem ErrorContext (Legacy)
|
||||
*/
|
||||
public function render(ErrorContext $context, bool $isDebug = false): string
|
||||
{
|
||||
@@ -31,8 +175,10 @@ final readonly class ErrorTemplateRenderer implements ErrorViewRendererInterface
|
||||
|
||||
try {
|
||||
// Sichere Datenaufbereitung ohne Closures oder komplexe Objekte
|
||||
$errorLine = $context->exception->getLine();
|
||||
$codeHtml = new FileHighlighter()($context->exception->getFile(), $errorLine - 10, 10, $errorLine);
|
||||
$safeData = [
|
||||
'code' => new FileHighlighter()($context->exception->getFile(), $context->exception->getLine() -10, 10),
|
||||
'code' => RawHtml::from($codeHtml),
|
||||
'errorClass' => $context->exception::class,
|
||||
'errorMessage' => $context->exception->getMessage(),
|
||||
'error' => [
|
||||
@@ -45,18 +191,19 @@ final readonly class ErrorTemplateRenderer implements ErrorViewRendererInterface
|
||||
'requestId' => $context->requestId->toString(),
|
||||
'timestamp' => $context->additionalData['timestamp'] ?? date('c'),
|
||||
'level' => $context->level->name,
|
||||
'memory' => $context->additionalData['memory_usage'] ?? 0
|
||||
'memory' => $context->additionalData['memory_usage'] ?? 0,
|
||||
'trace' => (new StackTrace($context->exception))->getItems(),
|
||||
];
|
||||
|
||||
$renderContext = new RenderContext(
|
||||
template: $template,
|
||||
metaData: new MetaData('', '', new OpenGraphTypeWebsite),
|
||||
metaData: new MetaData('', '', new OpenGraphTypeWebsite()),
|
||||
data: $safeData
|
||||
);
|
||||
|
||||
$renderedContent = $this->renderer->render($renderContext);
|
||||
|
||||
if($renderedContent === ""){
|
||||
if ($renderedContent === "") {
|
||||
throw new \Exception("Template Renderer returned empty string");
|
||||
}
|
||||
|
||||
@@ -115,7 +262,7 @@ final readonly class ErrorTemplateRenderer implements ErrorViewRendererInterface
|
||||
</body>
|
||||
</html>',
|
||||
htmlspecialchars($exception->getMessage()),
|
||||
htmlspecialchars($context->requestId->toString()),
|
||||
htmlspecialchars($context->requestId?->toString() ?? 'unknown'),
|
||||
htmlspecialchars($context->additionalData['timestamp'] ?? date('c')),
|
||||
htmlspecialchars($context->level->name),
|
||||
htmlspecialchars($exception->getFile()),
|
||||
@@ -147,7 +294,144 @@ final readonly class ErrorTemplateRenderer implements ErrorViewRendererInterface
|
||||
</div>
|
||||
</body>
|
||||
</html>',
|
||||
htmlspecialchars($context->requestId->toString())
|
||||
htmlspecialchars($context->requestId?->toString() ?? 'unknown')
|
||||
);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert Fehlermeldung aus ErrorHandlerContext
|
||||
*/
|
||||
private function extractErrorMessage(ErrorHandlerContext $context): string
|
||||
{
|
||||
// Aus Exception-Daten extrahieren
|
||||
if (isset($context->exception->data['exception_message'])) {
|
||||
return $context->exception->data['exception_message'];
|
||||
}
|
||||
|
||||
// Aus Exception-Daten (message)
|
||||
if (isset($context->exception->data['message'])) {
|
||||
return $context->exception->data['message'];
|
||||
}
|
||||
|
||||
// Aus Exception-Daten (user_message)
|
||||
if (isset($context->exception->data['user_message'])) {
|
||||
return $context->exception->data['user_message'];
|
||||
}
|
||||
|
||||
// Fallback: Operation und Component verwenden
|
||||
$operation = $context->exception->operation ?? 'unknown_operation';
|
||||
$component = $context->exception->component ?? 'Application';
|
||||
|
||||
return "Error in {$component} during {$operation}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine einfache Fehlerseite als Fallback für ErrorHandlerContext
|
||||
*/
|
||||
private function createFallbackErrorPageFromHandlerContext(ErrorHandlerContext $context, bool $isDebug, ?Throwable $renderException = null): string
|
||||
{
|
||||
$errorMessage = $this->extractErrorMessage($context);
|
||||
$isSecurityEvent = $context->exception->metadata['security_event'] ?? false;
|
||||
|
||||
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; }
|
||||
.security-warning { background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 4px; margin: 15px 0; }
|
||||
.debug-section { margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<h1 class="error-title">%s: %s</h1>
|
||||
|
||||
%s
|
||||
|
||||
<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<br>
|
||||
<strong>HTTP-Status:</strong> %d<br>
|
||||
<strong>Component:</strong> %s<br>
|
||||
<strong>Operation:</strong> %s
|
||||
</div>
|
||||
|
||||
<div class="error-info">
|
||||
<p><strong>Client-IP:</strong> %s</p>
|
||||
<p><strong>Request-URI:</strong> %s</p>
|
||||
<p><strong>User-Agent:</strong> %s</p>
|
||||
</div>
|
||||
|
||||
<div class="debug-section">
|
||||
<h3>Exception Data:</h3>
|
||||
<pre>%s</pre>
|
||||
</div>
|
||||
|
||||
%s
|
||||
</div>
|
||||
</body>
|
||||
</html>',
|
||||
htmlspecialchars($context->exception->component ?? 'Application'),
|
||||
htmlspecialchars($errorMessage),
|
||||
$isSecurityEvent ? '<div class="security-warning"><strong>⚠️ Security Event:</strong> This error indicates a potential security issue.</div>' : '',
|
||||
htmlspecialchars($context->request->requestId?->toString() ?? 'unknown'),
|
||||
htmlspecialchars(date('c')),
|
||||
htmlspecialchars($context->metadata['error_level'] ?? 'ERROR'),
|
||||
$context->metadata['http_status'] ?? 500,
|
||||
htmlspecialchars($context->exception->component ?? 'Application'),
|
||||
htmlspecialchars($context->exception->operation ?? 'unknown'),
|
||||
htmlspecialchars($context->request->clientIp ?? 'unknown'),
|
||||
htmlspecialchars($context->request->requestUri ?? '/'),
|
||||
htmlspecialchars(substr((string)$context->request->userAgent ?? '', 0, 100)),
|
||||
htmlspecialchars(json_encode($context->exception->data, JSON_PRETTY_PRINT)),
|
||||
$renderException ? '<div class="render-error"><h3>Fehler beim Rendern der Fehlerseite:</h3><p>' .
|
||||
htmlspecialchars($renderException->getMessage()) . '</p></div>' : ''
|
||||
);
|
||||
} else {
|
||||
// Produktions-Fallback mit minimalen Informationen
|
||||
$userMessage = $isSecurityEvent ? 'Access denied.' : 'Es ist ein Fehler aufgetreten.';
|
||||
|
||||
$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; }
|
||||
.status-code { font-size: 4em; font-weight: bold; color: #dc3545; margin: 20px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<div class="status-code">%d</div>
|
||||
<h1 class="error-title">%s</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>',
|
||||
$context->metadata['http_status'] ?? 500,
|
||||
htmlspecialchars($userMessage),
|
||||
htmlspecialchars($context->request->requestId?->toString() ?? 'unknown')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling\View;
|
||||
|
||||
use App\Framework\ErrorHandling\ErrorContext;
|
||||
use App\Framework\Exception\ErrorHandlerContext;
|
||||
|
||||
/**
|
||||
* Interface für alle Renderer von Fehlerseiten
|
||||
@@ -11,7 +13,12 @@ use App\Framework\ErrorHandling\ErrorContext;
|
||||
interface ErrorViewRendererInterface
|
||||
{
|
||||
/**
|
||||
* Rendert eine Fehlerseite basierend auf dem ErrorContext
|
||||
* Rendert eine Fehlerseite basierend auf dem ErrorHandlerContext
|
||||
*/
|
||||
public function renderFromHandlerContext(ErrorHandlerContext $context, bool $isDebug = false): string;
|
||||
|
||||
/**
|
||||
* Rendert eine Fehlerseite basierend auf dem ErrorContext (Legacy)
|
||||
*/
|
||||
public function render(ErrorContext $context, bool $isDebug = false): string;
|
||||
}
|
||||
|
||||
@@ -175,13 +175,13 @@
|
||||
<span>Request ID: {{ requestId }}</span>
|
||||
<span>Zeit: {{ timestamp }}</span>
|
||||
<span>Level: {{ level }}</span>
|
||||
<span>Speicherverbrauch: {{ format_filesize($memory) }}</span>
|
||||
<span>Speicherverbrauch: {{ memory }}</span>
|
||||
</div>
|
||||
|
||||
{{ dependencyInfo }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php echo "Hallo" ?>
|
||||
|
||||
{{ code }}
|
||||
|
||||
<div class="trace-container">
|
||||
@@ -191,17 +191,15 @@
|
||||
|
||||
<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-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>
|
||||
|
||||
Reference in New Issue
Block a user