.svg). * Standard: dekorativ (aria-hidden). Mit $label wird es als beschriftetes Bild * (role="img") ausgegeben. Größe/Farbe steuert CSS über die Klasse .icon. * Quelle: Bootstrap Icons (MIT) — neue Icons via bin/icons-add.php hinzufügen. */ function icon(string $name, string $class = '', ?string $label = null): string { static $cache = []; if (!array_key_exists($name, $cache)) { $file = PUBLIC_PATH . '/assets/icons/' . basename($name) . '.svg'; $cache[$name] = is_file($file) ? trim((string) file_get_contents($file)) : ''; } if ($cache[$name] === '') { return ''; } $attrs = 'class="icon' . ($class !== '' ? ' ' . e($class) : '') . '"'; $attrs .= $label !== null && $label !== '' ? ' role="img" aria-label="' . e($label) . '"' : ' aria-hidden="true" focusable="false"'; return preg_replace('/.json) mit Request-weitem Cache. * Wirft bei kaputtem JSON — Datenfehler sollen laut scheitern, nicht leise. */ function json_load(string $name): array { static $cache = []; if (!array_key_exists($name, $cache)) { $file = DATA_PATH . '/' . $name . '.json'; if (!is_file($file)) { return []; } $cache[$name] = json_decode((string) file_get_contents($file), true, 512, JSON_THROW_ON_ERROR); } return $cache[$name]; } /** * Komponente rendern: component('hero', ['title' => …]). * Props werden als lokale Variablen extrahiert; Komponenten sind dumme Includes. */ function component(string $name, array $props = []): void { extract($props, EXTR_SKIP); require APP_PATH . '/components/' . $name . '.php'; } /** * Page-Datei ausführen: sie setzt $meta und emittiert ihren Body. * Rückgabe: [$meta, $html]. */ function render_page(string $file): array { $meta = []; ob_start(); require $file; return [$meta, (string) ob_get_clean()]; } /** * Signierten Zeitstempel für die Formular-Time-Trap erzeugen. */ function form_token(): string { $ts = (string) time(); return $ts . '.' . hash_hmac('sha256', $ts, (string) config('app_secret')); } /** * Time-Trap prüfen: Signatur gültig, älter als $min Sekunden, jünger als $max. */ function form_token_valid(string $token, int $min = 3, int $max = 7200): bool { $parts = explode('.', $token); if (count($parts) !== 2) { return false; } [$ts, $sig] = $parts; if (!hash_equals(hash_hmac('sha256', $ts, (string) config('app_secret')), $sig)) { return false; } $age = time() - (int) $ts; return $age >= $min && $age <= $max; }