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.
This commit is contained in:
parent
5692874b10
commit
41cd7a4fd8
32 changed files with 5796 additions and 368 deletions
178
includes/Config.php
Normal file
178
includes/Config.php
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
<?php
|
||||
/**
|
||||
* Configuration Management Class
|
||||
* Loads and manages application configuration from .env files
|
||||
*/
|
||||
|
||||
class Config
|
||||
{
|
||||
private static $config = [];
|
||||
private static $loaded = false;
|
||||
|
||||
/**
|
||||
* Load configuration from environment and .env files
|
||||
*/
|
||||
public static function load()
|
||||
{
|
||||
if (self::$loaded) {
|
||||
return self::$config;
|
||||
}
|
||||
|
||||
// Load environment variables
|
||||
self::loadEnvironment();
|
||||
|
||||
// Load .env file if it exists
|
||||
self::loadDotEnv();
|
||||
|
||||
self::$loaded = true;
|
||||
return self::$config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a configuration value
|
||||
*/
|
||||
public static function get($key, $default = null)
|
||||
{
|
||||
$config = self::load();
|
||||
|
||||
// Support dot notation (e.g., 'database.host')
|
||||
$keys = explode('.', $key);
|
||||
$value = $config;
|
||||
|
||||
foreach ($keys as $k) {
|
||||
if (!isset($value[$k])) {
|
||||
return $default;
|
||||
}
|
||||
$value = $value[$k];
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a configuration value (runtime only)
|
||||
*/
|
||||
public static function set($key, $value)
|
||||
{
|
||||
$keys = explode('.', $key);
|
||||
$config = &self::$config;
|
||||
|
||||
foreach ($keys as $i => $k) {
|
||||
if ($i === count($keys) - 1) {
|
||||
$config[$k] = $value;
|
||||
} else {
|
||||
if (!isset($config[$k]) || !is_array($config[$k])) {
|
||||
$config[$k] = [];
|
||||
}
|
||||
$config = &$config[$k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load system environment variables
|
||||
*/
|
||||
private static function loadEnvironment()
|
||||
{
|
||||
// System environment variables get precedence
|
||||
foreach ($_ENV as $key => $value) {
|
||||
self::setFromEnv($key, $value);
|
||||
}
|
||||
|
||||
foreach ($_SERVER as $key => $value) {
|
||||
if (strpos($key, 'APP_') === 0 || strpos($key, 'DB_') === 0 ||
|
||||
strpos($key, 'STREAM_') === 0 || strpos($key, 'ADMIN_') === 0) {
|
||||
self::setFromEnv($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load .env file
|
||||
*/
|
||||
private static function loadDotEnv()
|
||||
{
|
||||
$envFile = __DIR__ . '/../.env';
|
||||
|
||||
if (!file_exists($envFile)) {
|
||||
error_log("Warning: .env file not found. Using default configuration.");
|
||||
return;
|
||||
}
|
||||
|
||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
|
||||
// Skip comments
|
||||
if (strpos($line, '#') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse key=value pairs
|
||||
if (strpos($line, '=') !== false) {
|
||||
list($key, $value) = explode('=', $line, 2);
|
||||
$key = trim($key);
|
||||
$value = trim($value);
|
||||
|
||||
// Remove quotes if present
|
||||
if ((strpos($value, '"') === 0 && strrpos($value, '"') === strlen($value) - 1) ||
|
||||
(strpos($value, "'") === 0 && strrpos($value, "'") === strlen($value) - 1)) {
|
||||
$value = substr($value, 1, -1);
|
||||
}
|
||||
|
||||
self::setFromEnv($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set configuration value from environment variable
|
||||
*/
|
||||
private static function setFromEnv($key, $value)
|
||||
{
|
||||
// Convert environment naming to configuration naming
|
||||
$configKey = strtolower(str_replace('_', '.', $key));
|
||||
|
||||
// Type coercion for boolean values
|
||||
if (in_array($value, ['true', 'false'])) {
|
||||
$value = $value === 'true';
|
||||
}
|
||||
|
||||
// Type coercion for numeric values
|
||||
if (is_numeric($value) && !is_string($value)) {
|
||||
$value = strpos($value, '.') !== false ? (float)$value : (int)$value;
|
||||
}
|
||||
|
||||
// Handle arrays (comma-separated)
|
||||
if (strpos($value, ',') !== false) {
|
||||
$value = array_map('trim', explode(',', $value));
|
||||
}
|
||||
|
||||
self::set($configKey, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all configuration as a flattened array
|
||||
*/
|
||||
public static function all()
|
||||
{
|
||||
return self::load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we are in a specific environment
|
||||
*/
|
||||
public static function isEnvironment($env)
|
||||
{
|
||||
return self::get('app.env') === $env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if debug mode is enabled
|
||||
*/
|
||||
public static function isDebug()
|
||||
{
|
||||
return self::get('app.debug', false);
|
||||
}
|
||||
}
|
||||
298
includes/Database.php
Normal file
298
includes/Database.php
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
<?php
|
||||
/**
|
||||
* Database Connection and Query Management Class
|
||||
* Handles SQLite database operations with prepared statements and error handling
|
||||
*/
|
||||
|
||||
class Database
|
||||
{
|
||||
private static $instance = null;
|
||||
private $pdo;
|
||||
private $dbPath;
|
||||
|
||||
/**
|
||||
* Get database instance (singleton pattern)
|
||||
*/
|
||||
public static function getInstance()
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor for singleton
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
$this->dbPath = Config::get('db.path', __DIR__ . '/../data/app.db');
|
||||
|
||||
// Ensure data directory exists
|
||||
$dataDir = dirname($this->dbPath);
|
||||
if (!is_dir($dataDir)) {
|
||||
mkdir($dataDir, 0755, true);
|
||||
}
|
||||
|
||||
$this->connect();
|
||||
$this->runMigrations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to SQLite database
|
||||
*/
|
||||
private function connect()
|
||||
{
|
||||
try {
|
||||
$this->pdo = new PDO("sqlite:{$this->dbPath}");
|
||||
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
$this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
|
||||
|
||||
// Enable WAL mode for better performance
|
||||
$this->pdo->exec('PRAGMA journal_mode = WAL');
|
||||
$this->pdo->exec('PRAGMA synchronous = NORMAL');
|
||||
$this->pdo->exec('PRAGMA cache_size = 10000');
|
||||
$this->pdo->exec('PRAGMA temp_store = MEMORY');
|
||||
|
||||
if (Config::isDebug()) {
|
||||
error_log("Database connected successfully: {$this->dbPath}");
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_log("Database connection failed: " . $e->getMessage());
|
||||
throw new Exception("Database connection error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run database migrations
|
||||
*/
|
||||
private function runMigrations()
|
||||
{
|
||||
$migrationDir = __DIR__ . '/../migrations';
|
||||
if (!is_dir($migrationDir)) {
|
||||
error_log("Migrations directory not found: {$migrationDir}");
|
||||
return;
|
||||
}
|
||||
|
||||
$migrationsRun = [];
|
||||
|
||||
// Check if migrations table exists
|
||||
try {
|
||||
$result = $this->pdo->query("SELECT name FROM sqlite_master WHERE type='table' AND name='migrations'");
|
||||
if ($result->fetch()) {
|
||||
// Get already run migrations
|
||||
$stmt = $this->pdo->query("SELECT migration_name FROM migrations");
|
||||
$migrationsRun = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
} else {
|
||||
// Create migrations table
|
||||
$this->pdo->exec("CREATE TABLE migrations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
migration_name VARCHAR(255) UNIQUE NOT NULL,
|
||||
run_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)");
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log("Error checking migrations table: " . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
// Get migration files
|
||||
$files = glob($migrationDir . '/*.sql');
|
||||
sort($files); // Run in order
|
||||
|
||||
foreach ($files as $file) {
|
||||
$migrationName = basename($file);
|
||||
|
||||
if (in_array($migrationName, $migrationsRun)) {
|
||||
continue; // Already run
|
||||
}
|
||||
|
||||
try {
|
||||
$sql = file_get_contents($file);
|
||||
if (empty($sql)) {
|
||||
error_log("Empty migration file: {$migrationName}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->pdo->exec($sql);
|
||||
|
||||
// Record migration as run
|
||||
$stmt = $this->pdo->prepare("INSERT INTO migrations (migration_name) VALUES (?)");
|
||||
$stmt->execute([$migrationName]);
|
||||
|
||||
error_log("Migration completed: {$migrationName}");
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_log("Migration failed {$migrationName}: " . $e->getMessage());
|
||||
// Continue with other migrations rather than stopping
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a prepared statement
|
||||
*/
|
||||
public function query($sql, $params = [])
|
||||
{
|
||||
try {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt;
|
||||
} catch (PDOException $e) {
|
||||
error_log("Database query error: " . $e->getMessage() . " | SQL: {$sql}");
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a query and return all results
|
||||
*/
|
||||
public function fetchAll($sql, $params = [])
|
||||
{
|
||||
return $this->query($sql, $params)->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a query and return single result
|
||||
*/
|
||||
public function fetch($sql, $params = [])
|
||||
{
|
||||
return $this->query($sql, $params)->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a query and return single column
|
||||
*/
|
||||
public function fetchColumn($sql, $params = [])
|
||||
{
|
||||
return $this->query($sql, $params)->fetchColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert and return last insert ID
|
||||
*/
|
||||
public function insert($sql, $params = [])
|
||||
{
|
||||
$this->query($sql, $params);
|
||||
return $this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update records and return affected row count
|
||||
*/
|
||||
public function update($sql, $params = [])
|
||||
{
|
||||
$stmt = $this->query($sql, $params);
|
||||
return $stmt->rowCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete records and return affected row count
|
||||
*/
|
||||
public function delete($sql, $params = [])
|
||||
{
|
||||
return $this->update($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin transaction
|
||||
*/
|
||||
public function beginTransaction()
|
||||
{
|
||||
$this->pdo->beginTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit transaction
|
||||
*/
|
||||
public function commit()
|
||||
{
|
||||
$this->pdo->commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback transaction
|
||||
*/
|
||||
public function rollback()
|
||||
{
|
||||
$this->pdo->rollBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PDO instance (for advanced operations)
|
||||
*/
|
||||
public function getPDO()
|
||||
{
|
||||
return $this->pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if table exists
|
||||
*/
|
||||
public function tableExists($tableName)
|
||||
{
|
||||
$result = $this->pdo->query("SELECT name FROM sqlite_master WHERE type='table' AND name='{$tableName}'");
|
||||
return $result->fetch() !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database statistics
|
||||
*/
|
||||
public function getStats()
|
||||
{
|
||||
$stats = [];
|
||||
|
||||
// Table counts
|
||||
$tables = ['users', 'chat_messages', 'active_viewers', 'banned_users'];
|
||||
foreach ($tables as $table) {
|
||||
if ($this->tableExists($table)) {
|
||||
$count = $this->fetchColumn("SELECT COUNT(*) FROM {$table}");
|
||||
$stats["{$table}_count"] = $count;
|
||||
}
|
||||
}
|
||||
|
||||
// Database size
|
||||
$stats['db_size'] = filesize($this->dbPath);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup database
|
||||
*/
|
||||
public function backup($backupPath = null)
|
||||
{
|
||||
if (!$backupPath) {
|
||||
$backupPath = $this->dbPath . '.backup.' . date('Y-m-d_H-i-s');
|
||||
}
|
||||
|
||||
if (copy($this->dbPath, $backupPath)) {
|
||||
// Backup WAL file if exists
|
||||
$walFile = $this->dbPath . '-wal';
|
||||
if (file_exists($walFile)) {
|
||||
copy($walFile, $backupPath . '-wal');
|
||||
}
|
||||
|
||||
return $backupPath;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize database
|
||||
*/
|
||||
public function optimize()
|
||||
{
|
||||
try {
|
||||
$this->pdo->exec('VACUUM');
|
||||
$this->pdo->exec('REINDEX');
|
||||
$this->pdo->exec('ANALYZE');
|
||||
error_log("Database optimization completed");
|
||||
} catch (PDOException $e) {
|
||||
error_log("Database optimization failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
364
includes/ErrorHandler.php
Normal file
364
includes/ErrorHandler.php
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
103
includes/autoloader.php
Normal file
103
includes/autoloader.php
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
/**
|
||||
* PSR-4 Autoloader
|
||||
* Automatically loads classes based on PSR-4 standards
|
||||
*/
|
||||
|
||||
spl_autoload_register(function ($className) {
|
||||
// PSR-4 mapping for the application
|
||||
$prefixes = [
|
||||
'App\\' => __DIR__ . '/../app/',
|
||||
'Models\\' => __DIR__ . '/../models/',
|
||||
'Controllers\\' => __DIR__ . '/../controllers/',
|
||||
'Utils\\' => __DIR__ . '/../utils/',
|
||||
'Services\\' => __DIR__ . '/../services/',
|
||||
'Middleware\\' => __DIR__ . '/../middleware/'
|
||||
];
|
||||
|
||||
// Check for exact class match first (for legacy classes)
|
||||
$legacyMappings = [
|
||||
'Config' => __DIR__ . '/Config.php',
|
||||
'Security' => __DIR__ . '/../utils/Security.php',
|
||||
'Validation' => __DIR__ . '/../utils/Validation.php',
|
||||
'Database' => __DIR__ . '/Database.php',
|
||||
'UserModel' => __DIR__ . '/../models/UserModel.php',
|
||||
'ChatMessageModel' => __DIR__ . '/../models/ChatMessageModel.php',
|
||||
'ActiveViewerModel' => __DIR__ . '/../models/ActiveViewerModel.php'
|
||||
];
|
||||
|
||||
// First check legacy mappings
|
||||
if (isset($legacyMappings[$className])) {
|
||||
$file = $legacyMappings[$className];
|
||||
if (file_exists($file)) {
|
||||
require_once $file;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check PSR-4 mappings
|
||||
foreach ($prefixes as $prefix => $baseDir) {
|
||||
$len = strlen($prefix);
|
||||
if (strncmp($prefix, $className, $len) !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$relativeClass = substr($className, $len);
|
||||
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
|
||||
|
||||
if (file_exists($file)) {
|
||||
require_once $file;
|
||||
if (Config::isDebug()) {
|
||||
error_log("Autoloaded: {$className} from {$file}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Class not found - this will throw an exception from spl_autoload_register
|
||||
if (Config::isDebug()) {
|
||||
error_log("Autoloader: Class {$className} not found in any mapping");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Optional: Load additional helper functions
|
||||
*/
|
||||
if (file_exists(__DIR__ . '/helpers.php')) {
|
||||
require_once __DIR__ . '/helpers.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional: Load composer autoloader if it exists (for future dependencies)
|
||||
*/
|
||||
$composerAutoloader = __DIR__ . '/../vendor/autoload.php';
|
||||
if (file_exists($composerAutoloader)) {
|
||||
require_once $composerAutoloader;
|
||||
}
|
||||
|
||||
// Verify critical classes are loaded
|
||||
$criticalClasses = [
|
||||
'Config',
|
||||
'Security',
|
||||
'Validation',
|
||||
'Database'
|
||||
];
|
||||
|
||||
foreach ($criticalClasses as $class) {
|
||||
if (!class_exists($class, false)) {
|
||||
// Try to load manually
|
||||
$legacyPaths = [
|
||||
'Config' => 'includes/Config.php',
|
||||
'Security' => 'utils/Security.php',
|
||||
'Validation' => 'utils/Validation.php',
|
||||
'Database' => 'includes/Database.php'
|
||||
];
|
||||
|
||||
if (isset($legacyPaths[$class])) {
|
||||
$path = __DIR__ . '/../' . $legacyPaths[$class];
|
||||
if (file_exists($path)) {
|
||||
require_once $path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue