f3 = Base::instance(); } protected function render(string $view, array $data = [], int $ttl = 0): void { $this->ensureCsrf(); $user = $this->user(); $flash = array_key_exists('flash', $data) ? $data['flash'] : $this->pullFlash(); $this->f3->expire($user ? 0 : $ttl); $this->f3->mset($data + [ 'view' => $view, 'flash' => is_array($flash) ? $flash : [], 'currentUser' => $user, 'adminMode' => false, 'metaDescription' => null, 'CSRF_TOKEN' => (string) $this->f3->get('SESSION.csrf_token'), ]); echo Template::instance()->render('layout.html'); } protected function user(): ?array { if (!$this->f3->exists('ctx.user_loaded')) { $id = (int) ($this->f3->get('SESSION.user_id') ?: 0); $this->f3->set('currentUser', $id > 0 ? (new User())->findPublic($id) : null); $this->f3->set('ctx.user_loaded', true); } return $this->f3->get('currentUser'); } protected function requireAuth(): void { if ($this->user()) { return; } $this->flash('error', 'Connecte-toi pour continuer.'); $this->f3->reroute('@login'); } protected function checkCsrf(): void { $sent = trim((string) ($this->f3->get('POST.csrf_token') ?: '')); $expected = trim((string) ($this->f3->get('SESSION.csrf_token') ?: '')); if ($sent !== '' && $expected !== '' && hash_equals($expected, $sent)) { return; } $this->f3->error(400); } protected function flash(string $type, string $message): void { $this->f3->push('SESSION.flash', ['type' => $type, 'message' => $message]); } protected function rotateCsrf(): void { $this->f3->clear('SESSION.csrf_token'); $this->ensureCsrf(); } private function ensureCsrf(): void { if ($this->f3->exists('SESSION.csrf_token')) { return; } $seed = trim((string) ($this->f3->get('CSRF') ?: '')); $this->f3->set('SESSION.csrf_token', $seed !== '' ? $seed : bin2hex(random_bytes(16))); } private function pullFlash(): array { return $this->f3->pull('SESSION.flash') ?: []; } }