316 lines
9.6 KiB
PHP
316 lines
9.6 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace App\Services;
|
||
|
|
|
||
|
|
use App\Models\ErrorLog;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* ErrorHandler Service
|
||
|
|
*
|
||
|
|
* Centralized error handling system:
|
||
|
|
* - Captures all errors and exceptions
|
||
|
|
* - Logs to files and database
|
||
|
|
* - Generates unique error IDs
|
||
|
|
* - Displays appropriate error pages
|
||
|
|
* - Sanitizes sensitive data
|
||
|
|
*/
|
||
|
|
class ErrorHandler
|
||
|
|
{
|
||
|
|
private Logger $logger;
|
||
|
|
private ?ErrorLog $errorLogModel = null;
|
||
|
|
private bool $isDevelopment;
|
||
|
|
|
||
|
|
public function __construct()
|
||
|
|
{
|
||
|
|
$this->logger = new Logger('errors');
|
||
|
|
// Default to development if APP_ENV not set (show debug info for config errors)
|
||
|
|
$this->isDevelopment = ($_ENV['APP_ENV'] ?? 'development') === 'development';
|
||
|
|
|
||
|
|
// Initialize ErrorLog model if database is available
|
||
|
|
try {
|
||
|
|
$this->errorLogModel = new ErrorLog();
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
// Database not available, will only use file logging
|
||
|
|
// Don't use error_log as it might fail too
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle an exception
|
||
|
|
*/
|
||
|
|
public function handleException(\Throwable $exception): void
|
||
|
|
{
|
||
|
|
$errorData = $this->captureError($exception);
|
||
|
|
|
||
|
|
// Log to file
|
||
|
|
$this->logToFile($errorData);
|
||
|
|
|
||
|
|
// Log to database if available
|
||
|
|
$dbErrorId = $this->logToDatabase($errorData);
|
||
|
|
|
||
|
|
// Display error page
|
||
|
|
$this->displayError($errorData, $dbErrorId);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle PHP errors (convert to exception)
|
||
|
|
*/
|
||
|
|
public function handleError(int $errno, string $errstr, string $errfile, int $errline): bool
|
||
|
|
{
|
||
|
|
// Don't handle suppressed errors (@)
|
||
|
|
if (!(error_reporting() & $errno)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ignore certain non-critical errors during error handling itself
|
||
|
|
if (error_reporting() === 0) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Convert to ErrorException and handle it
|
||
|
|
$exception = new \ErrorException($errstr, 0, $errno, $errfile, $errline);
|
||
|
|
$this->handleException($exception);
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle fatal errors on shutdown
|
||
|
|
*/
|
||
|
|
public function handleShutdown(): void
|
||
|
|
{
|
||
|
|
$error = error_get_last();
|
||
|
|
|
||
|
|
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
|
||
|
|
$exception = new \ErrorException(
|
||
|
|
$error['message'],
|
||
|
|
0,
|
||
|
|
$error['type'],
|
||
|
|
$error['file'],
|
||
|
|
$error['line']
|
||
|
|
);
|
||
|
|
|
||
|
|
$this->handleException($exception);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Capture complete error context
|
||
|
|
*/
|
||
|
|
private function captureError(\Throwable $exception): array
|
||
|
|
{
|
||
|
|
// Generate unique error ID
|
||
|
|
$errorId = $this->generateErrorId();
|
||
|
|
|
||
|
|
// Sanitize request data (remove passwords, tokens, etc.)
|
||
|
|
$requestData = $this->sanitizeArray(array_merge($_GET, $_POST));
|
||
|
|
|
||
|
|
// Sanitize session data
|
||
|
|
$sessionData = $this->sanitizeArray($_SESSION ?? []);
|
||
|
|
|
||
|
|
return [
|
||
|
|
'error_id' => $errorId,
|
||
|
|
'error_type' => get_class($exception),
|
||
|
|
'error_message' => $exception->getMessage(),
|
||
|
|
'error_file' => $exception->getFile(),
|
||
|
|
'error_line' => $exception->getLine(),
|
||
|
|
'stack_trace' => $exception->getTraceAsString(),
|
||
|
|
'request_method' => $_SERVER['REQUEST_METHOD'] ?? 'CLI',
|
||
|
|
'request_uri' => $_SERVER['REQUEST_URI'] ?? 'N/A',
|
||
|
|
'request_data' => json_encode($requestData),
|
||
|
|
'user_id' => $_SESSION['user_id'] ?? null,
|
||
|
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown',
|
||
|
|
'ip_address' => $this->getIpAddress(),
|
||
|
|
'session_data' => json_encode($sessionData),
|
||
|
|
'php_version' => PHP_VERSION,
|
||
|
|
'memory_usage' => memory_get_usage(true),
|
||
|
|
'occurred_at' => date('Y-m-d H:i:s')
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate unique error ID for reference
|
||
|
|
*/
|
||
|
|
private function generateErrorId(): string
|
||
|
|
{
|
||
|
|
return strtoupper(substr(md5(uniqid('error_', true)), 0, 12));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get client IP address
|
||
|
|
*/
|
||
|
|
private function getIpAddress(): string
|
||
|
|
{
|
||
|
|
$headers = [
|
||
|
|
'HTTP_CF_CONNECTING_IP', // Cloudflare
|
||
|
|
'HTTP_X_FORWARDED_FOR', // Proxy
|
||
|
|
'HTTP_X_REAL_IP', // Nginx
|
||
|
|
'REMOTE_ADDR' // Direct
|
||
|
|
];
|
||
|
|
|
||
|
|
foreach ($headers as $header) {
|
||
|
|
if (!empty($_SERVER[$header])) {
|
||
|
|
$ip = $_SERVER[$header];
|
||
|
|
// Handle multiple IPs (X-Forwarded-For)
|
||
|
|
if (strpos($ip, ',') !== false) {
|
||
|
|
$ip = trim(explode(',', $ip)[0]);
|
||
|
|
}
|
||
|
|
return $ip;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return 'Unknown';
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sanitize array to remove sensitive data
|
||
|
|
*/
|
||
|
|
private function sanitizeArray(array $data): array
|
||
|
|
{
|
||
|
|
$sensitive = ['password', 'password_confirm', 'current_password', 'new_password',
|
||
|
|
'token', 'csrf_token', 'api_key', 'secret', 'bot_token',
|
||
|
|
'mail_password', 'webhook_url', 'captcha_secret_key'];
|
||
|
|
|
||
|
|
$sanitized = [];
|
||
|
|
foreach ($data as $key => $value) {
|
||
|
|
$lowerKey = strtolower($key);
|
||
|
|
$isSensitive = false;
|
||
|
|
|
||
|
|
foreach ($sensitive as $pattern) {
|
||
|
|
if (strpos($lowerKey, $pattern) !== false) {
|
||
|
|
$isSensitive = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($isSensitive) {
|
||
|
|
$sanitized[$key] = '***REDACTED***';
|
||
|
|
} elseif (is_array($value)) {
|
||
|
|
$sanitized[$key] = $this->sanitizeArray($value);
|
||
|
|
} else {
|
||
|
|
$sanitized[$key] = $value;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $sanitized;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Log error to file
|
||
|
|
*/
|
||
|
|
private function logToFile(array $errorData): void
|
||
|
|
{
|
||
|
|
$this->logger->separator('ERROR CAPTURED');
|
||
|
|
$this->logger->critical('Error occurred', [
|
||
|
|
'error_id' => $errorData['error_id'],
|
||
|
|
'type' => $errorData['error_type'],
|
||
|
|
'message' => $errorData['error_message'],
|
||
|
|
'file' => $errorData['error_file'],
|
||
|
|
'line' => $errorData['error_line'],
|
||
|
|
'uri' => $errorData['request_uri'],
|
||
|
|
'user_id' => $errorData['user_id'],
|
||
|
|
'ip' => $errorData['ip_address']
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Log stack trace separately for readability
|
||
|
|
$this->logger->error('Stack Trace', ['trace' => $errorData['stack_trace']]);
|
||
|
|
$this->logger->separator('END ERROR');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Log error to database
|
||
|
|
*/
|
||
|
|
private function logToDatabase(array $errorData): ?int
|
||
|
|
{
|
||
|
|
if ($this->errorLogModel === null) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
return $this->errorLogModel->logError($errorData);
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
// Database logging failed, continue with file logging only
|
||
|
|
error_log("Failed to log error to database: " . $e->getMessage());
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Display appropriate error page
|
||
|
|
*/
|
||
|
|
private function displayError(array $errorData, ?int $dbErrorId): void
|
||
|
|
{
|
||
|
|
// Set HTTP status code
|
||
|
|
http_response_code(500);
|
||
|
|
|
||
|
|
// Clean any output buffers
|
||
|
|
while (ob_get_level() > 0) {
|
||
|
|
ob_end_clean();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Extract variables for view (avoid using extract() which might fail)
|
||
|
|
$error_id = $errorData['error_id'];
|
||
|
|
$error_type = $errorData['error_type'];
|
||
|
|
$error_message = $errorData['error_message'];
|
||
|
|
$error_file = $errorData['error_file'];
|
||
|
|
$error_line = $errorData['error_line'];
|
||
|
|
$stack_trace = $errorData['stack_trace'];
|
||
|
|
$request_method = $errorData['request_method'];
|
||
|
|
$request_uri = $errorData['request_uri'];
|
||
|
|
$user_agent = $errorData['user_agent'];
|
||
|
|
$ip_address = $errorData['ip_address'];
|
||
|
|
$php_version = $errorData['php_version'];
|
||
|
|
$memory_usage = $errorData['memory_usage'];
|
||
|
|
$occurred_at = $errorData['occurred_at'];
|
||
|
|
$user_info = $this->getUserInfo($errorData['user_id']);
|
||
|
|
$request_data = json_decode($errorData['request_data'], true);
|
||
|
|
$session_data = json_decode($errorData['session_data'], true);
|
||
|
|
|
||
|
|
// Display debug page in development, clean 500 in production
|
||
|
|
if ($this->isDevelopment) {
|
||
|
|
require __DIR__ . '/../Views/errors/debug.php';
|
||
|
|
} else {
|
||
|
|
require __DIR__ . '/../Views/errors/500.php';
|
||
|
|
}
|
||
|
|
|
||
|
|
exit;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get user info for error context
|
||
|
|
*/
|
||
|
|
private function getUserInfo(?int $userId): ?array
|
||
|
|
{
|
||
|
|
if ($userId === null) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return [
|
||
|
|
'id' => $userId,
|
||
|
|
'username' => $_SESSION['username'] ?? 'Unknown',
|
||
|
|
'role' => $_SESSION['role'] ?? 'guest',
|
||
|
|
'email' => $_SESSION['email'] ?? 'Unknown'
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Static helper to register global handlers
|
||
|
|
*/
|
||
|
|
public static function register(): self
|
||
|
|
{
|
||
|
|
$handler = new self();
|
||
|
|
|
||
|
|
// Set exception handler
|
||
|
|
set_exception_handler([$handler, 'handleException']);
|
||
|
|
|
||
|
|
// Set error handler (converts errors to exceptions)
|
||
|
|
set_error_handler([$handler, 'handleError']);
|
||
|
|
|
||
|
|
// Set shutdown handler (catch fatal errors)
|
||
|
|
register_shutdown_function([$handler, 'handleShutdown']);
|
||
|
|
|
||
|
|
return $handler;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|