PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC] ); } catch (PDOException $e) { http_response_code(500); exit('資料庫連線失敗,請先啟動 XAMPP MySQL 並匯入 setup.sql。'); } function e(?string $value): string { return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8'); } function loggedIn(): bool { return isset($_SESSION['admin_id']); } function requireLogin(): void { if (!loggedIn()) { header('Location: login.php'); exit; } } function csrf(): string { if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(24)); return $_SESSION['csrf']; } function checkCsrf(): void { if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) exit('表單驗證失敗'); } function youtubeId(string $url): string { if (preg_match('~(?:youtu\.be/|youtube\.com/(?:watch\?v=|embed/|shorts/))([\w-]{11})~', $url, $m)) return $m[1]; return preg_match('/^[\w-]{11}$/', $url) ? $url : ''; }