* * This was the first thing in the framework to start a native PHP session * — but only lazily, the moment token()/verify() is actually called. A page * that never touches Csrf never gets a session cookie. Lib\Session and * App\AdminAuth (session-based admin login) now touch the same native * session the same lazy way — safe in any order, since ensureSession() * no-ops when a session is already active. */ final class Csrf { public const FIELD_NAME = 'csrf_token'; private const SESSION_KEY = '_csrf_token'; public static function token(): string { self::ensureSession(); if (empty($_SESSION[self::SESSION_KEY])) { $_SESSION[self::SESSION_KEY] = bin2hex(random_bytes(32)); } return $_SESSION[self::SESSION_KEY]; } public static function verify(?string $submittedToken): bool { self::ensureSession(); $expected = $_SESSION[self::SESSION_KEY] ?? null; if ($submittedToken === null || $expected === null) { return false; } return hash_equals($expected, $submittedToken); } public static function fieldName(): string { return self::FIELD_NAME; } private static function ensureSession(): void { if (session_status() === PHP_SESSION_ACTIVE) { return; } // Must be called before session_start() — after is a silent no-op. session_set_cookie_params([ 'httponly' => true, 'samesite' => 'Lax', 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', ]); session_start(); } }