f3 = Base::instance(); $this->db = $this->f3->get('DB'); } protected function render(string $view, array $data = [], int $cacheTtl = 0): void { $user = $this->currentUser(); // 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. $this->f3->expire($user !== null ? 0 : $cacheTtl); $flash = array_key_exists('flash', $data) && is_array($data['flash']) ? $data['flash'] : $this->pullFlash(); $this->f3->mset($data + [ 'view' => $view, 'currentUser' => $user, 'flash' => $flash, 'metaDescription' => null, ]); // Recopier @CSRF en session pour que verifyCsrf() puisse // vérifier le jeton soumis au POST suivant. $this->f3->copy('CSRF', 'SESSION.csrf'); echo Template::instance()->render('layout.html'); } protected function currentUser(): ?array { if ($this->resolvedUser === false) { $userId = (int) ($this->f3->get('SESSION.user_id') ?? 0); $this->resolvedUser = $userId > 0 ? (new User($this->db))->findById($userId) : null; } return $this->resolvedUser; } 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 = (string) ($this->f3->get('POST.csrf_token') ?? ''); $expected = (string) ($this->f3->get('SESSION.csrf') ?? ''); // hash_equals : comparaison en temps constant contre les attaques temporelles. if ($submitted !== '' && $expected !== '' && hash_equals($expected, $submitted)) { return; } $this->f3->error(400, 'Jeton CSRF invalide.'); } protected function flash(string $type, string $message): void { $this->f3->set('SESSION.flash', ['type' => $type, 'message' => $message]); } private function pullFlash(): ?array { $flash = $this->f3->get('SESSION.flash'); $this->f3->clear('SESSION.flash'); return is_array($flash) ? $flash : null; } }