f3 = Base::instance(); } protected function render(string $view, array $data = [], int $cacheTtl = 0): void { // Les pages publiques restent cacheables avec le TTL demandé. // Si un utilisateur est connecté, le layout dépend de la session // (navigation admin, déconnexion + CSRF) : on force expire(0) // pour ne pas servir ce rendu à d'autres visiteurs. $currentUser = $this->currentUser(); $this->f3->expire($currentUser !== null ? 0 : $cacheTtl); $flash = array_key_exists('flash', $data) && is_array($data['flash']) ? $data['flash'] : $this->pullFlash(); // On s'appuie sur Session(..., 'CSRF') pour la génération F3 du // jeton, mais on le persiste en session pour qu'il reste valide // entre la requête GET qui rend le formulaire et le POST suivant. $this->ensureCsrfToken(); $this->f3->mset($data + [ 'view' => $view, 'flash' => $flash, 'metaDescription' => null, 'adminMode' => false, 'currentUser' => $currentUser, 'CSRF_TOKEN' => (string) $this->f3->get('SESSION.csrf_token'), ]); echo Template::instance()->render('layout.html'); } // Résout l'utilisateur courant une seule fois par requête et le // stocke dans le hive — accessible partout, y compris les templates. protected function currentUser(): ?array { if (!$this->f3->exists('ctx.current_user_loaded')) { $userId = (int) ($this->f3->get('SESSION.user_id') ?? 0); $user = $userId > 0 ? (new User())->findById($userId) : null; $this->f3->set('currentUser', $user); $this->f3->set('ctx.current_user_loaded', true); } return $this->f3->get('currentUser'); } protected function requireAuth(): void { if ($this->currentUser() !== null) { return; } $this->flash('error', 'Connecte-toi pour continuer.'); $this->f3->reroute('@login'); } protected function verifyCsrf(): void { $submitted = trim((string) ($this->f3->get('POST.csrf_token') ?? '')); $expected = trim((string) ($this->f3->get('SESSION.csrf_token') ?? '')); if ($submitted !== '' && $expected !== '' && hash_equals($expected, $submitted)) { return; } $this->f3->error(400, 'Jeton CSRF invalide.'); } // Empile un message flash — permet plusieurs messages par requête. protected function flash(string $type, string $message): void { $this->f3->push('SESSION.flash', ['type' => $type, 'message' => $message]); } protected function resetCsrfToken(): void { $this->f3->clear('SESSION.csrf_token'); $this->ensureCsrfToken(); } private function ensureCsrfToken(): void { $token = trim((string) ($this->f3->get('SESSION.csrf_token') ?? '')); if ($token !== '') { return; } $seed = trim((string) ($this->f3->get('CSRF') ?? '')); if ($seed === '') { $seed = bin2hex(random_bytes(32)); } $this->f3->set('SESSION.csrf_token', $seed); } private function pullFlash(): array { return $this->f3->pull('SESSION.flash') ?: []; } }