- Implemented SecurityTest to validate token generation, CSRF protection, input sanitization, and rate limiting. - Created UserModelTest to ensure correct database operations for user management, including creation, updating, banning, and fetching active users. - Developed ValidationTest to verify input validation and sanitization for user IDs, nicknames, messages, and API requests. - Introduced Security and Validation utility classes with methods for secure token generation, input sanitization, and comprehensive validation rules.
364 lines
12 KiB
PHP
364 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* Global Error Handler
|
|
* Provides centralized error handling and logging for the application
|
|
*/
|
|
|
|
class ErrorHandler
|
|
{
|
|
private static $app = null;
|
|
private static $logFile = null;
|
|
|
|
/**
|
|
* Initialize error handling
|
|
*/
|
|
public static function initialize()
|
|
{
|
|
// Set up basic error reporting
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', 0);
|
|
ini_set('log_errors', 1);
|
|
|
|
// Set custom error handlers
|
|
set_error_handler([__CLASS__, 'errorHandler']);
|
|
set_exception_handler([__CLASS__, 'exceptionHandler']);
|
|
register_shutdown_function([__CLASS__, 'shutdownHandler']);
|
|
|
|
// Get log file path
|
|
self::$logFile = Config::get('log.file', __DIR__ . '/../logs/app.log');
|
|
|
|
// Ensure log directory exists
|
|
$logDir = dirname(self::$logFile);
|
|
if (!is_dir($logDir) && !mkdir($logDir, 0755, true)) {
|
|
error_log("Cannot create log directory: {$logDir}");
|
|
}
|
|
|
|
if (Config::isDebug()) {
|
|
error_log("Error handler initialized. Log file: " . self::$logFile);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Custom error handler
|
|
*/
|
|
public static function errorHandler($errno, $errstr, $errfile, $errline, $errcontext = null)
|
|
{
|
|
// Convert error level to readable format
|
|
$errorLevels = [
|
|
E_ERROR => 'Error',
|
|
E_WARNING => 'Warning',
|
|
E_PARSE => 'Parse Error',
|
|
E_NOTICE => 'Notice',
|
|
E_CORE_ERROR => 'Core Error',
|
|
E_CORE_WARNING => 'Core Warning',
|
|
E_COMPILE_ERROR => 'Compile Error',
|
|
E_COMPILE_WARNING => 'Compile Warning',
|
|
E_USER_ERROR => 'User Error',
|
|
E_USER_WARNING => 'User Warning',
|
|
E_USER_NOTICE => 'User Notice',
|
|
E_STRICT => 'Strict Notice',
|
|
E_RECOVERABLE_ERROR => 'Recoverable Error',
|
|
E_DEPRECATED => 'Deprecated',
|
|
E_USER_DEPRECATED => 'User Deprecated'
|
|
];
|
|
|
|
$errorType = $errorLevels[$errno] ?? 'Unknown Error';
|
|
|
|
$error = [
|
|
'type' => 'php_error',
|
|
'level' => $errno,
|
|
'error_type' => $errorType,
|
|
'message' => $errstr,
|
|
'file' => $errfile,
|
|
'line' => $errline,
|
|
'context' => isset($errcontext['this']) ? get_class($errcontext['this']) : 'global',
|
|
'timestamp' => date('Y-m-d H:i:s'),
|
|
'request_uri' => $_SERVER['REQUEST_URI'] ?? '',
|
|
'remote_ip' => Security::getClientIP(),
|
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
|
'session_id' => session_id(),
|
|
'backtrace' => self::getBacktrace()
|
|
];
|
|
|
|
// Log the error
|
|
self::logError($error);
|
|
|
|
// In development, show errors
|
|
if (Config::isEnvironment('development')) {
|
|
// Return error info for debugging without exposing sensitive data
|
|
return [
|
|
'error' => true,
|
|
'type' => $errorType,
|
|
'message' => $errstr,
|
|
'file' => basename($errfile),
|
|
'line' => $errline
|
|
];
|
|
}
|
|
|
|
// In production, don't show errors
|
|
if (Config::isEnvironment('production')) {
|
|
return false; // Let PHP handle it
|
|
}
|
|
|
|
// For PHP's error handling, still need to return false for some errors
|
|
return ($errno & (E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR)) ? false : true;
|
|
}
|
|
|
|
/**
|
|
* Uncaught exception handler
|
|
*/
|
|
public static function exceptionHandler($exception)
|
|
{
|
|
$error = [
|
|
'type' => 'uncaught_exception',
|
|
'class' => get_class($exception),
|
|
'message' => $exception->getMessage(),
|
|
'code' => $exception->getCode(),
|
|
'file' => $exception->getFile(),
|
|
'line' => $exception->getLine(),
|
|
'timestamp' => date('Y-m-d H:i:s'),
|
|
'request_uri' => $_SERVER['REQUEST_URI'] ?? '',
|
|
'remote_ip' => Security::getClientIP(),
|
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
|
'session_id' => session_id(),
|
|
'backtrace' => self::getBacktrace(),
|
|
'previous' => $exception->getPrevious() ? $exception->getPrevious()->getMessage() : null
|
|
];
|
|
|
|
// Log the exception
|
|
self::logError($error);
|
|
|
|
// Handle display based on environment
|
|
if (Config::isEnvironment('development')) {
|
|
// Show detailed error page
|
|
self::renderErrorPage($error, 500);
|
|
exit;
|
|
} else {
|
|
// Show generic error page in production
|
|
self::renderErrorPage([
|
|
'message' => 'An unexpected error occurred. Please try again later.'
|
|
], 500);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shutdown handler for fatal errors
|
|
*/
|
|
public static function shutdownHandler()
|
|
{
|
|
$error = error_get_last();
|
|
|
|
if ($error !== null) {
|
|
$fatalErrors = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR];
|
|
|
|
if (in_array($error['type'], $fatalErrors)) {
|
|
$error['type'] = 'fatal_error';
|
|
$error['timestamp'] = date('Y-m-d H:i:s');
|
|
$error['request_uri'] = $_SERVER['REQUEST_URI'] ?? '';
|
|
$error['remote_ip'] = Security::getClientIP();
|
|
$error['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
|
$error['session_id'] = session_id();
|
|
$error['backtrace'] = self::getBacktrace();
|
|
|
|
self::logError($error);
|
|
|
|
if (Config::isEnvironment('development')) {
|
|
echo "<h1>Fatal Error</h1>";
|
|
echo "<pre>" . htmlspecialchars(print_r($error, true)) . "</pre>";
|
|
} else {
|
|
self::renderErrorPage([
|
|
'message' => 'A critical error occurred. Our team has been notified.'
|
|
], 500);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clean up tasks (optional)
|
|
self::performCleanup();
|
|
}
|
|
|
|
/**
|
|
* Log error to file and/or external service
|
|
*/
|
|
private static function logError($error)
|
|
{
|
|
$logMessage = sprintf(
|
|
"[%s] %s: %s in %s:%d\nContext: %s\nIP: %s\nURI: %s\nBacktrace:\n%s\n---\n",
|
|
$error['timestamp'],
|
|
$error['type'] ?? 'unknown',
|
|
$error['message'] ?? 'Unknown error',
|
|
$error['file'] ?? 'unknown',
|
|
$error['line'] ?? 0,
|
|
$error['context'] ?? 'unknown',
|
|
$error['remote_ip'] ?? 'unknown',
|
|
$error['request_uri'] ?? 'unknown',
|
|
implode("\n", $error['backtrace'] ?? [])
|
|
);
|
|
|
|
// Write to log file
|
|
if (self::$logFile) {
|
|
$result = file_put_contents(self::$logFile, $logMessage, FILE_APPEND | LOCK_EX);
|
|
if ($result === false) {
|
|
error_log("Failed to write to error log: " . self::$logFile);
|
|
}
|
|
}
|
|
|
|
// Also log via PHP's error_log if our file fails
|
|
if (!self::$logFile || !file_exists(self::$logFile)) {
|
|
error_log("ErrorHandler: " . json_encode($error));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get backtrace safely
|
|
*/
|
|
private static function getBacktrace()
|
|
{
|
|
try {
|
|
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
|
$formatted = [];
|
|
|
|
foreach ($backtrace as $i => $trace) {
|
|
$formatted[] = sprintf(
|
|
"#%d %s(%s): %s(%s)",
|
|
$i,
|
|
$trace['file'] ?? 'unknown',
|
|
$trace['line'] ?? 'unknown',
|
|
isset($trace['class']) ? $trace['class'] . '::' . $trace['function'] : $trace['function'] ?? 'unknown',
|
|
isset($trace['args']) ? json_encode(count($trace['args'])) . ' args' : 'unknown args'
|
|
);
|
|
}
|
|
|
|
return $formatted;
|
|
} catch (Exception $e) {
|
|
return ["Failed to generate backtrace: " . $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render error page
|
|
*/
|
|
private static function renderErrorPage($error, $httpCode = 500)
|
|
{
|
|
http_response_code($httpCode);
|
|
|
|
if (!headers_sent()) {
|
|
header('Content-Type: text/html; charset=UTF-8');
|
|
header('Cache-Control: no-cache, no-store, must-revalidate');
|
|
}
|
|
|
|
$title = $httpCode === 404 ? 'Page Not Found' : 'Server Error';
|
|
$message = $error['message'] ?? 'An unexpected error occurred';
|
|
|
|
echo "<!DOCTYPE html>
|
|
<html lang='en'>
|
|
<head>
|
|
<meta charset='UTF-8'>
|
|
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
|
<title>{$title}</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; background: #f9f9f9; }
|
|
.error-container { max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
|
h1 { color: #e74c3c; margin-bottom: 20px; }
|
|
p { color: #666; line-height: 1.6; }
|
|
.error-details { text-align: left; margin-top: 30px; padding: 15px; background: #f8f8f8; border-radius: 5px; font-family: monospace; font-size: 12px; }
|
|
pre { white-space: pre-wrap; word-wrap: break-word; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class='error-container'>
|
|
<h1>{$title}</h1>
|
|
<p>{$message}</p>";
|
|
|
|
if (Config::isEnvironment('development') && isset($error['file'])) {
|
|
echo "<div class='error-details'>
|
|
<strong>File:</strong> {$error['file']}<br>
|
|
<strong>Line:</strong> {$error['line']}<br>
|
|
<strong>Type:</strong> " . ($error['error_type'] ?? $error['type'] ?? 'unknown') . "
|
|
</div>";
|
|
}
|
|
|
|
echo "</div>
|
|
</body>
|
|
</html>";
|
|
}
|
|
|
|
/**
|
|
* Perform cleanup tasks on shutdown
|
|
*/
|
|
private static function performCleanup()
|
|
{
|
|
// Clean up expired sessions or temporary files if needed
|
|
// This could be expanded based on application needs
|
|
|
|
// Example: Clean up old temp files
|
|
$tempDir = sys_get_temp_dir();
|
|
$pattern = $tempDir . '/dodgers_*';
|
|
|
|
foreach (glob($pattern) as $file) {
|
|
// Remove files older than 1 hour
|
|
if (filemtime($file) < time() - 3600) {
|
|
@unlink($file);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle API errors with JSON response
|
|
*/
|
|
public static function apiError($message, $code = 500, $details = null)
|
|
{
|
|
$error = [
|
|
'success' => false,
|
|
'error' => $message,
|
|
'code' => $code,
|
|
'timestamp' => time()
|
|
];
|
|
|
|
if (Config::isEnvironment('development') && $details) {
|
|
$error['details'] = $details;
|
|
}
|
|
|
|
http_response_code($code);
|
|
header('Content-Type: application/json');
|
|
echo json_encode($error);
|
|
}
|
|
|
|
/**
|
|
* Log security events
|
|
*/
|
|
public static function logSecurityEvent($event, $details = [])
|
|
{
|
|
$logEntry = [
|
|
'type' => 'security_event',
|
|
'event' => $event,
|
|
'details' => $details,
|
|
'timestamp' => date('Y-m-d H:i:s'),
|
|
'remote_ip' => Security::getClientIP(),
|
|
'request_uri' => $_SERVER['REQUEST_URI'] ?? '',
|
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
|
'session_id' => session_id()
|
|
];
|
|
|
|
self::logError($logEntry);
|
|
}
|
|
|
|
/**
|
|
* Log performance metrics
|
|
*/
|
|
public static function logPerformance($operation, $duration, $details = [])
|
|
{
|
|
$logEntry = [
|
|
'type' => 'performance',
|
|
'operation' => $operation,
|
|
'duration' => $duration,
|
|
'details' => $details,
|
|
'timestamp' => date('Y-m-d H:i:s'),
|
|
'remote_ip' => Security::getClientIP(),
|
|
'session_id' => session_id()
|
|
];
|
|
|
|
self::logError($logEntry);
|
|
}
|
|
}
|