= 2 && (($value[0] === '"' && $value[-1] === '"') || ($value[0] === "'" && $value[-1] === "'"))) { $value = substr($value, 1, -1); } putenv("{$name}={$value}"); } } load_dotenv(dirname(__DIR__) . '/.env'); function env_required(string $name): string { $value = getenv($name); if ($value === false || trim($value) === '') { throw new RuntimeException("Missing required environment variable: {$name}"); } return $value; } function db(): PDO { static $pdo = null; if ($pdo instanceof PDO) return $pdo; $dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', env_required('DB_HOST'), getenv('DB_PORT') ?: '3306', env_required('DB_DATABASE')); $pdo = new PDO($dsn, env_required('DB_USERNAME'), env_required('DB_PASSWORD'), [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]); return $pdo; } function csrf_token(): string { if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(32)); return $_SESSION['csrf']; } function scalar_input(mixed $value, string $default = ''): string { return is_scalar($value) ? (string)$value : $default; } function verify_csrf(): void { $provided = scalar_input($_POST['_csrf'] ?? null); if (!hash_equals((string)($_SESSION['csrf'] ?? ''), $provided)) { http_response_code(419); exit('Invalid CSRF token'); } } function ensure_initial_administrator(): void { static $checked = false; if ($checked) return; $checked = true; if ((int)db()->query('SELECT COUNT(*) FROM users')->fetchColumn() !== 0) return; $email = strtolower(trim(env_required('ADMIN_EMAIL'))); $password = env_required('ADMIN_PASSWORD'); if (strlen($password) < 12) throw new RuntimeException('ADMIN_PASSWORD must be at least 12 characters'); $roleId = (int)db()->query("SELECT id FROM roles WHERE name = 'Administrator'")->fetchColumn(); if ($roleId < 1) throw new RuntimeException('Administrator role is missing from the database'); $stmt = db()->prepare('INSERT INTO users (role_id, email, name, password_hash) VALUES (:role, :email, :name, :hash)'); $stmt->execute(['role' => $roleId, 'email' => $email, 'name' => 'System Administrator', 'hash' => password_hash($password, PASSWORD_DEFAULT)]); } function current_user(): ?array { static $user = false; if ($user !== false) return $user; $id = $_SESSION['user_id'] ?? null; if (!$id) return $user = null; $stmt = db()->prepare('SELECT u.*, r.name AS role_name FROM users u JOIN roles r ON r.id = u.role_id WHERE u.id = :id AND u.is_active = 1'); $stmt->execute(['id' => $id]); return $user = ($stmt->fetch() ?: null); } function require_login(): array { $user = current_user(); if (!$user) { header('Location: /?route=login'); exit; } return $user; } function can(string $permission): bool { $user = current_user(); if (!$user) return false; static $permissions = null; if ($permissions === null) { $stmt = db()->prepare('SELECT p.name FROM permissions p JOIN role_permissions rp ON rp.permission_id = p.id WHERE rp.role_id = :role'); $stmt->execute(['role' => $user['role_id']]); $permissions = array_column($stmt->fetchAll(), 'name'); } return in_array($permission, $permissions, true); } function require_permission(string $permission): void { if (!can($permission)) { http_response_code(403); exit('Forbidden'); } } function audit(string $action, string $entityType, ?int $entityId = null, array $metadata = []): void { $user = current_user(); $stmt = db()->prepare('INSERT INTO audit_events (user_id, action, entity_type, entity_id, metadata, ip_address) VALUES (:user_id, :action, :entity_type, :entity_id, :metadata, :ip)'); $stmt->execute(['user_id' => $user['id'] ?? null, 'action' => $action, 'entity_type' => $entityType, 'entity_id' => $entityId, 'metadata' => $metadata ? json_encode($metadata, JSON_THROW_ON_ERROR) : null, 'ip' => $_SERVER['REMOTE_ADDR'] ?? null]); } function e(string $value): string { return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }