/ tag builders (CI4 html helper). // site/assets is served with a one-year immutable Cache-Control, so local files // carry their mtime as ?v= — an edited style.css/main.js gets a new URL instead // of staying stuck in returning visitors' caches. function _asset_versioned(string $path): string { $file = dirname(__DIR__) . '/' . ltrim($path, '/'); if (strpos($path, '?') !== false || preg_match('~^https?://~', $path) || !is_file($file)) return base_url($path); return base_url($path) . '?v=' . filemtime($file); } function script_tag(string $src): string { return '' . "\n"; } function link_tag(string $href, string $rel = 'stylesheet'): string { return '' . "\n"; } function site_url(string $path = ''): string { return base_url($path); } function esc($value, string $context = 'html'): string { return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); } // Render a partial like CI4's view(): extract $data + the shared page vars // (info / navbar / current_uri_2 via $GLOBALS['_view_shared']) and return the // buffered HTML. Lets partials read $info without threading it through callers. function view(string $name, array $data = []): string { $shared = $GLOBALS['_view_shared'] ?? []; extract($shared); extract($data); ob_start(); include SITE_INCLUDES . '/' . $name . '.php'; return ob_get_clean(); } // Minimal session() shim — the templates only read getFlashdata(). // Per-request singleton: the constructor's flash sweep therefore runs once per // request, which is what makes flashdata actually "flash". function session(): _FitSession { static $s = null; if ($s === null) $s = new _FitSession(); return $s; } class _FitSession { // CI4-style flashdata lifecycle. A value set with setFlashdata() in request N // (the POST handler, just before its redirect) must be readable in request // N+1 (the redirect target) and then disappear — otherwise the banner sticks // around forever, surviving reloads, because nothing ever clears it. // // We track an age marker per key: setFlashdata() marks it 'new'; on the next // request the sweep ages 'new' → 'old' (still readable), and on the request // after that it deletes anything 'old'. The sweep runs once, here, before any // read or write this request (setFlashdata() is itself the first session() // call in a POST handler, so the constructor always precedes it). public function __construct() { foreach ($_SESSION['_flash_age'] ?? [] as $k => $age) { if ($age === 'old') { unset($_SESSION['_flash'][$k], $_SESSION['_flash_age'][$k]); } else { $_SESSION['_flash_age'][$k] = 'old'; } } // Sweep orphans left by the previous (never-expiring) shim: any flash // value with no age marker is stale and would otherwise stick forever. foreach (array_keys($_SESSION['_flash'] ?? []) as $k) { if (!isset($_SESSION['_flash_age'][$k])) unset($_SESSION['_flash'][$k]); } if (empty($_SESSION['_flash'])) unset($_SESSION['_flash']); if (empty($_SESSION['_flash_age'])) unset($_SESSION['_flash_age']); } public function getFlashdata(string $key) { return $_SESSION['_flash'][$key] ?? null; } public function setFlashdata(string $key, $val): void { $_SESSION['_flash'][$key] = $val; $_SESSION['_flash_age'][$key] = 'new'; } public function get(string $key) { return $_SESSION[$key] ?? null; } public function set(string $key, $val): void { $_SESSION[$key] = $val; } }