0, 'path' => '/', 'secure' => $isSecure, 'httponly' => true, 'samesite' => 'Lax', ]); session_start(); } self::ensureCsrf(); return Database::getInstance(); } /** * Gate for admin pages: require auth + CSRF token + load Database. */ public static function adminGuard(): Database { require_once APP_ROOT . '/src/AdminAuth.php'; AdminAuth::requireLogin(); return self::boot(); } // ── CSRF lifecycle ──────────────────────────────────────────────────────── /** * Ensure a CSRF token exists in the session. * Called automatically by boot() / adminGuard(). */ private static function ensureCsrf(): void { if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } } /** * Validate the CSRF token on a POST request. * Halts with 403 if the token is missing or invalid. */ public static function verifyCsrf(): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['csrf_token'], $_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) { http_response_code(403); exit('CSRF token invalide.'); } } /** * Regenerate the CSRF token after a successful mutation. */ public static function rotateCsrf(): void { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } // ── Flash messages ──────────────────────────────────────────────────────── /** * Store a flash message in the session. * * @param 'success'|'error'|'warning' $type */ public static function flash(string $type, string $message): void { $_SESSION["_flash_{$type}"] = $message; } /** * Store the name of the field that should receive autofocus after a * validation failure (WCAG 3.3.1). */ public static function flashAutofocus(string $fieldName): void { $_SESSION['_flash_autofocus'] = $fieldName; } /** * Consume and return the autofocus field name, then clear it. * Returns null when no autofocus hint is present. */ public static function consumeAutofocus(): ?string { $field = $_SESSION['_flash_autofocus'] ?? null; unset($_SESSION['_flash_autofocus']); return $field; } /** * Consume and return flash messages, then clear them from the session. * * @return array{error: ?string, success: ?string, warning: ?string} */ public static function consumeFlash(): array { $error = $_SESSION['_flash_error'] ?? null; $success = $_SESSION['_flash_success'] ?? null; $warning = $_SESSION['_flash_warning'] ?? null; unset($_SESSION['_flash_error'], $_SESSION['_flash_success'], $_SESSION['_flash_warning']); // Note: autofocus is consumed separately via consumeAutofocus(). return ['error' => $error, 'success' => $success, 'warning' => $warning]; } // ── Redirect ────────────────────────────────────────────────────────────── /** * Flash a message and redirect. Terminates the script. */ public static function redirect(string $url, ?string $success = null, ?string $error = null): never { if ($success !== null) { self::flash('success', $success); } if ($error !== null) { self::flash('error', $error); } header('Location: ' . $url); exit; } // ── Asset versioning ───────────────────────────────────────────────────── /** * Return an asset URL with a filemtime-based cache-busting query string. * Input is a root-relative URL path (e.g. /assets/css/main.css). */ public static function assetV(string $urlPath): string { $file = APP_ROOT . '/public' . $urlPath; $v = file_exists($file) ? filemtime($file) : 0; return $urlPath . ($v ? '?v=' . $v : ''); } // ── Template rendering ──────────────────────────────────────────────────── /** * Render a full page: head → header → content template → footer. * * Expects $vars to contain the same keys the templates already rely on * ($pageTitle, $bodyClass, $isAdmin, $extraCss, $ogTags, etc.). * The footer variant (public vs admin) is chosen automatically based * on $isAdmin. * * @param string $template Path relative to APP_ROOT/templates/ * @param array $vars Variables to expose inside the templates */ public static function render(string $template, array $vars = []): void { // Make all vars available in the template scope. extract($vars); include APP_ROOT . '/templates/head.php'; include APP_ROOT . '/templates/header.php'; include APP_ROOT . '/templates/' . $template; if (!empty($isAdmin)) { include APP_ROOT . '/templates/admin/footer.php'; } else { include APP_ROOT . '/templates/footer.php'; } } }