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
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue