iptv-stream-web/includes/ErrorHandler.php
Vincent 41cd7a4fd8 Add comprehensive unit tests for Security, UserModel, and Validation utilities
- 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.
2025-09-30 21:22:28 -04:00

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);
}
}