Less home code more F3
This commit is contained in:
@@ -2,10 +2,188 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
abstract class AdminController extends BaseController
|
||||
class AdminController extends Controller
|
||||
{
|
||||
private const MEDIA_PICKER_LIMIT = 60;
|
||||
private const UPLOAD_MAX_BYTES = 10 * 1024 * 1024;
|
||||
|
||||
public function beforeRoute(): void
|
||||
{
|
||||
$this->requireAuth();
|
||||
}
|
||||
|
||||
public function index(): void
|
||||
{
|
||||
$page = max(1, (int) ($this->f3->get('GET.page') ?: 1));
|
||||
$result = (new Post())->page($page, 12);
|
||||
|
||||
$this->render('admin/dashboard.html', [
|
||||
'pageTitle' => 'Tableau de bord',
|
||||
'posts' => $result['items'],
|
||||
'pagination' => $result['pagination'],
|
||||
'paginationAlias' => 'dashboard',
|
||||
'adminMode' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): void
|
||||
{
|
||||
$this->postForm('Nouvel article', $this->f3->alias('post_store'), Post::blank());
|
||||
}
|
||||
|
||||
public function store(): void
|
||||
{
|
||||
$this->checkCsrf();
|
||||
$input = $this->postInput();
|
||||
|
||||
try {
|
||||
(new Post())->savePost($input);
|
||||
$this->flash('success', 'Article créé.');
|
||||
$this->f3->reroute('@dashboard');
|
||||
} catch (RuntimeException $e) {
|
||||
$this->postForm('Nouvel article', $this->f3->alias('post_store'), $input, $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function edit(): void
|
||||
{
|
||||
$post = (new Post())->findForForm((int) $this->f3->get('PARAMS.id'));
|
||||
if (!$post) {
|
||||
$this->f3->error(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->postForm('Modifier l\'article', $this->f3->alias('post_update', ['id' => $post['id']]), $post);
|
||||
}
|
||||
|
||||
public function update(): void
|
||||
{
|
||||
$this->checkCsrf();
|
||||
$id = (int) $this->f3->get('PARAMS.id');
|
||||
$input = $this->postInput() + ['id' => $id];
|
||||
|
||||
try {
|
||||
(new Post())->savePost($input, $id);
|
||||
$this->flash('success', 'Article mis à jour.');
|
||||
$this->f3->reroute('@dashboard');
|
||||
} catch (RuntimeException $e) {
|
||||
$this->postForm('Modifier l\'article', $this->f3->alias('post_update', ['id' => $id]), $input, $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(): void
|
||||
{
|
||||
$this->checkCsrf();
|
||||
|
||||
try {
|
||||
(new Post())->deleteById((int) $this->f3->get('PARAMS.id'));
|
||||
$this->flash('success', 'Article supprimé.');
|
||||
} catch (RuntimeException $e) {
|
||||
$this->flash('error', $e->getMessage());
|
||||
}
|
||||
|
||||
$this->f3->reroute('@dashboard');
|
||||
}
|
||||
|
||||
public function media(): void
|
||||
{
|
||||
$page = max(1, (int) ($this->f3->get('GET.page') ?: 1));
|
||||
$result = (new Media())->page($page, 24);
|
||||
|
||||
$this->render('admin/media.html', [
|
||||
'pageTitle' => 'Médiathèque',
|
||||
'items' => $result['items'],
|
||||
'pagination' => $result['pagination'],
|
||||
'paginationAlias' => 'media_index',
|
||||
]);
|
||||
}
|
||||
|
||||
public function mediaUpload(): void
|
||||
{
|
||||
$this->checkCsrf();
|
||||
|
||||
try {
|
||||
$original = (string) ($this->f3->get('FILES.image.name') ?: '');
|
||||
$received = Web::instance()->receive(
|
||||
fn(array $file): bool => (int) ($file['size'] ?? 0) > 0 && (int) ($file['size'] ?? 0) <= self::UPLOAD_MAX_BYTES,
|
||||
overwrite: false
|
||||
);
|
||||
$path = array_key_first(array_filter($received));
|
||||
if (!$path) {
|
||||
throw new RuntimeException('Choisis une image valide à envoyer.');
|
||||
}
|
||||
|
||||
(new Media())->upload($path, $original);
|
||||
$this->flash('success', 'Image ajoutée.');
|
||||
} catch (RuntimeException $e) {
|
||||
$this->flash('error', $e->getMessage());
|
||||
}
|
||||
|
||||
$this->f3->reroute('@media_index');
|
||||
}
|
||||
|
||||
public function mediaAlt(): void
|
||||
{
|
||||
$this->checkCsrf();
|
||||
|
||||
try {
|
||||
(new Media())->updateAlt((int) $this->f3->get('PARAMS.id'), $this->f3->clean((string) ($this->f3->get('POST.alt') ?: '')));
|
||||
$this->flash('success', 'Texte alternatif mis à jour.');
|
||||
} catch (RuntimeException $e) {
|
||||
$this->flash('error', $e->getMessage());
|
||||
}
|
||||
|
||||
$this->f3->reroute('@media_index');
|
||||
}
|
||||
|
||||
public function mediaDelete(): void
|
||||
{
|
||||
$this->checkCsrf();
|
||||
|
||||
try {
|
||||
$media = new Media();
|
||||
$item = $media->findById((int) $this->f3->get('PARAMS.id'));
|
||||
if (!$item) {
|
||||
throw new RuntimeException('Image introuvable.');
|
||||
}
|
||||
if ((new Post())->usesMedia($item['file_name'])) {
|
||||
throw new RuntimeException('Cette image est encore utilisée par un article.');
|
||||
}
|
||||
$media->deleteById($item['id']);
|
||||
$this->flash('success', 'Image supprimée.');
|
||||
} catch (RuntimeException $e) {
|
||||
$this->flash('error', $e->getMessage());
|
||||
}
|
||||
|
||||
$this->f3->reroute('@media_index');
|
||||
}
|
||||
|
||||
private function postForm(string $title, string $action, array $post, ?string $error = null): void
|
||||
{
|
||||
$media = new Media();
|
||||
$items = $media->recent(self::MEDIA_PICKER_LIMIT);
|
||||
$count = $media->count();
|
||||
|
||||
$this->render('admin/post_form.html', [
|
||||
'pageTitle' => $title,
|
||||
'formAction' => $action,
|
||||
'post' => $post,
|
||||
'mediaItems' => $items,
|
||||
'mediaCount' => $count,
|
||||
'mediaPickerLimit' => self::MEDIA_PICKER_LIMIT,
|
||||
'mediaPickerTruncated' => $count > count($items),
|
||||
'titleMax' => Post::TITLE_MAX_LENGTH,
|
||||
'excerptMax' => Post::EXCERPT_MAX_LENGTH,
|
||||
'flash' => $error ? [['type' => 'error', 'message' => $error]] : [],
|
||||
]);
|
||||
}
|
||||
|
||||
private function postInput(): array
|
||||
{
|
||||
return [
|
||||
'title' => $this->f3->clean((string) ($this->f3->get('POST.title') ?: '')),
|
||||
'excerpt' => $this->f3->clean((string) ($this->f3->get('POST.excerpt') ?: '')),
|
||||
'body_markdown' => trim((string) ($this->f3->get('POST.body_markdown') ?: '')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class AuthController extends BaseController
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function show(): void
|
||||
{
|
||||
if ($this->currentUser() !== null) {
|
||||
if ($this->user()) {
|
||||
$this->f3->reroute('@dashboard');
|
||||
return;
|
||||
}
|
||||
@@ -16,36 +16,35 @@ class AuthController extends BaseController
|
||||
|
||||
public function login(): void
|
||||
{
|
||||
$this->verifyCsrf();
|
||||
$this->checkCsrf();
|
||||
|
||||
$username = $this->f3->clean((string) ($this->f3->get('POST.username') ?? ''));
|
||||
$password = (string) ($this->f3->get('POST.password') ?? '');
|
||||
|
||||
// User étend DB\SQL\Mapper — inutile de recréer un Mapper générique.
|
||||
// Le 3e argument du constructeur Auth est le callback de comparaison.
|
||||
$user = new User();
|
||||
$auth = new \Auth($user, ['id' => 'username', 'pw' => 'password_hash'], 'password_verify');
|
||||
$auth = new Auth($user, ['id' => 'username', 'pw' => 'password_hash'], 'password_verify');
|
||||
$ok = $auth->login(
|
||||
$this->f3->clean((string) ($this->f3->get('POST.username') ?: '')),
|
||||
(string) ($this->f3->get('POST.password') ?: '')
|
||||
);
|
||||
|
||||
if (!$auth->login($username, $password)) {
|
||||
usleep(1_500_000); // 1,5 s — ralentit le brute-force
|
||||
if (!$ok) {
|
||||
usleep(1000000);
|
||||
$this->flash('error', 'Identifiants invalides.');
|
||||
$this->f3->reroute('@login');
|
||||
return;
|
||||
}
|
||||
|
||||
session_regenerate_id(true); // Prévient la fixation de session.
|
||||
$this->resetCsrfToken(); // Le nouveau contexte repart avec un jeton dédié.
|
||||
$this->f3->set('SESSION.user_id', $user->id);
|
||||
session_regenerate_id(true);
|
||||
$this->f3->set('SESSION.user_id', (int) $user->id);
|
||||
$this->rotateCsrf();
|
||||
$this->flash('success', 'Connexion réussie.');
|
||||
$this->f3->reroute('@dashboard');
|
||||
}
|
||||
|
||||
public function logout(): void
|
||||
{
|
||||
$this->verifyCsrf();
|
||||
$this->checkCsrf();
|
||||
$this->f3->clear('SESSION.user_id');
|
||||
$this->resetCsrfToken();
|
||||
session_regenerate_id(true); // Invalide l'ancien ID de session.
|
||||
session_regenerate_id(true);
|
||||
$this->rotateCsrf();
|
||||
$this->flash('success', 'Déconnexion effectuée.');
|
||||
$this->f3->reroute('@login');
|
||||
}
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
abstract class BaseController
|
||||
{
|
||||
protected Base $f3;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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') ?: [];
|
||||
}
|
||||
}
|
||||
91
app/Controllers/Controller.php
Normal file
91
app/Controllers/Controller.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
protected Base $f3;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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') ?: [];
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class MediaController extends AdminController
|
||||
{
|
||||
private const UPLOAD_MAX_BYTES = 10 * 1024 * 1024; // 10 Mo
|
||||
private const PER_PAGE = 24;
|
||||
|
||||
public function index(): void
|
||||
{
|
||||
$page = max(1, (int) ($this->f3->get('GET.page') ?? 1));
|
||||
$result = (new Media())->paginateLibrary($page, self::PER_PAGE);
|
||||
|
||||
$this->render('admin/media.html', [
|
||||
'pageTitle' => 'Médiathèque',
|
||||
'items' => $result['items'],
|
||||
'pagination' => $result,
|
||||
'paginationAlias' => 'media_index',
|
||||
]);
|
||||
}
|
||||
|
||||
public function upload(): void
|
||||
{
|
||||
$this->verifyCsrf();
|
||||
|
||||
try {
|
||||
// Lire le nom d'origine avant que Web::receive() déplace le fichier.
|
||||
$originalName = (string) ($this->f3->get('FILES.image.name') ?? '');
|
||||
|
||||
$received = Web::instance()->receive(
|
||||
// F3 gère le transport et le renommage ; la validation métier
|
||||
// (format réel, dimensions, réencodage) reste centralisée dans Media.
|
||||
fn(array $file): bool => (int) ($file['size'] ?? 0) > 0
|
||||
&& (int) ($file['size'] ?? 0) <= self::UPLOAD_MAX_BYTES,
|
||||
overwrite: false,
|
||||
slug: true
|
||||
);
|
||||
|
||||
// Le formulaire n'envoie qu'un seul fichier : on garde le premier
|
||||
// chemin accepté retourné par Web::receive().
|
||||
$accepted = array_keys(array_filter($received));
|
||||
$destPath = $accepted[0] ?? null;
|
||||
|
||||
if ($destPath === null) {
|
||||
throw new RuntimeException('Choisis une image valide à envoyer (JPG, PNG, WebP ≤ ' . (int) (self::UPLOAD_MAX_BYTES / 1024 / 1024) . ' Mo).');
|
||||
}
|
||||
|
||||
(new Media())->upload($destPath, $originalName);
|
||||
|
||||
$this->flash('success', 'Image ajoutée.');
|
||||
} catch (RuntimeException $e) {
|
||||
$this->flash('error', $e->getMessage());
|
||||
}
|
||||
|
||||
$this->f3->reroute('@media_index');
|
||||
}
|
||||
|
||||
public function updateAlt(): void
|
||||
{
|
||||
$this->verifyCsrf();
|
||||
|
||||
try {
|
||||
$alt = $this->f3->clean((string) ($this->f3->get('POST.alt') ?? ''));
|
||||
(new Media())->updateAlt((int) $this->f3->get('PARAMS.id'), $alt);
|
||||
$this->flash('success', 'Texte alternatif mis à jour.');
|
||||
} catch (RuntimeException $e) {
|
||||
$this->flash('error', $e->getMessage());
|
||||
}
|
||||
|
||||
$this->f3->reroute('@media_index');
|
||||
}
|
||||
|
||||
public function delete(): void
|
||||
{
|
||||
$this->verifyCsrf();
|
||||
|
||||
try {
|
||||
$id = (int) $this->f3->get('PARAMS.id');
|
||||
$media = new Media();
|
||||
$item = $media->findById($id);
|
||||
|
||||
if ($item === null) {
|
||||
throw new RuntimeException('Image introuvable.');
|
||||
}
|
||||
|
||||
if ((new Post())->isMediaUsed($item['id'], $item['file_name'])) {
|
||||
throw new RuntimeException('Cette image est encore utilisée par un article.');
|
||||
}
|
||||
|
||||
$media->delete($id);
|
||||
$this->flash('success', 'Image supprimée.');
|
||||
} catch (RuntimeException $e) {
|
||||
$this->flash('error', $e->getMessage());
|
||||
}
|
||||
|
||||
$this->f3->reroute('@media_index');
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class PostController extends AdminController
|
||||
{
|
||||
private const PER_PAGE = 12;
|
||||
private const MEDIA_PICKER_LIMIT = 60;
|
||||
|
||||
public function index(): void
|
||||
{
|
||||
$page = max(1, (int) ($this->f3->get('GET.page') ?? 1));
|
||||
$media = new Media();
|
||||
$result = (new Post())->paginateList($page, self::PER_PAGE, $media);
|
||||
|
||||
$this->render('admin/dashboard.html', [
|
||||
'pageTitle' => 'Tableau de bord',
|
||||
'posts' => $result['posts'],
|
||||
'pagination' => $result,
|
||||
'paginationAlias' => 'dashboard',
|
||||
'adminMode' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): void
|
||||
{
|
||||
$this->renderForm('Nouvel article', $this->f3->alias('post_store'), Post::emptyForm());
|
||||
}
|
||||
|
||||
public function store(): void
|
||||
{
|
||||
$this->verifyCsrf();
|
||||
|
||||
$media = new Media();
|
||||
$input = $this->postInput();
|
||||
|
||||
try {
|
||||
(new Post())->create($input, $media);
|
||||
$this->flash('success', 'Article créé.');
|
||||
$this->f3->reroute('@dashboard');
|
||||
} catch (RuntimeException $e) {
|
||||
$this->renderForm('Nouvel article', $this->f3->alias('post_store'), $input, $e->getMessage(), $media);
|
||||
}
|
||||
}
|
||||
|
||||
public function edit(): void
|
||||
{
|
||||
$post = (new Post())->findForEdit((int) $this->f3->get('PARAMS.id'));
|
||||
if ($post === null) {
|
||||
$this->f3->error(404, 'Article introuvable.');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->renderForm('Modifier l\'article', $this->f3->alias('post_update', ['id' => $post['id']]), $post);
|
||||
}
|
||||
|
||||
public function update(): void
|
||||
{
|
||||
$this->verifyCsrf();
|
||||
|
||||
$media = new Media();
|
||||
$id = (int) $this->f3->get('PARAMS.id');
|
||||
$input = $this->postInput() + ['id' => $id];
|
||||
|
||||
try {
|
||||
$updated = (new Post())->updatePost($id, $input, $media);
|
||||
if (!$updated) {
|
||||
$this->f3->error(404, 'Article introuvable.');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->flash('success', 'Article mis à jour.');
|
||||
$this->f3->reroute('@dashboard');
|
||||
} catch (RuntimeException $e) {
|
||||
$this->renderForm('Modifier l\'article', $this->f3->alias('post_update', ['id' => $id]), $input, $e->getMessage(), $media);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(): void
|
||||
{
|
||||
$this->verifyCsrf();
|
||||
|
||||
try {
|
||||
(new Post())->delete((int) $this->f3->get('PARAMS.id'));
|
||||
$this->flash('success', 'Article supprimé.');
|
||||
} catch (RuntimeException $e) {
|
||||
$this->flash('error', $e->getMessage());
|
||||
}
|
||||
|
||||
$this->f3->reroute('@dashboard');
|
||||
}
|
||||
|
||||
private function renderForm(string $pageTitle, string $formAction, array $post, ?string $error = null, ?Media $media = null): void
|
||||
{
|
||||
$media ??= new Media();
|
||||
|
||||
$coverPreview = null;
|
||||
if (!empty($post['cover_media_id'])) {
|
||||
$coverPreview = $media->findById((int) $post['cover_media_id']);
|
||||
}
|
||||
|
||||
$mediaItems = $media->latest(self::MEDIA_PICKER_LIMIT);
|
||||
$mediaCount = $media->count();
|
||||
$flash = $error !== null ? [['type' => 'error', 'message' => $error]] : [];
|
||||
|
||||
$this->render('admin/post_form.html', [
|
||||
'pageTitle' => $pageTitle,
|
||||
'formAction' => $formAction,
|
||||
'post' => $post,
|
||||
'coverPreview' => $coverPreview,
|
||||
'mediaItems' => $mediaItems,
|
||||
'mediaCount' => $mediaCount,
|
||||
'mediaPickerLimit' => self::MEDIA_PICKER_LIMIT,
|
||||
'mediaPickerTruncated' => $mediaCount > count($mediaItems),
|
||||
'titleMax' => Post::TITLE_MAX_LENGTH,
|
||||
'excerptMax' => Post::EXCERPT_MAX_LENGTH,
|
||||
'flash' => $flash,
|
||||
]);
|
||||
}
|
||||
|
||||
private function postInput(): array
|
||||
{
|
||||
return [
|
||||
'title' => $this->f3->clean((string) ($this->f3->get('POST.title') ?? '')),
|
||||
'excerpt' => $this->f3->clean((string) ($this->f3->get('POST.excerpt') ?? '')),
|
||||
'cover_media_id' => (string) ($this->f3->get('POST.cover_media_id') ?? ''),
|
||||
'body_markdown' => trim((string) ($this->f3->get('POST.body_markdown') ?? '')),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,26 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class SiteController extends BaseController
|
||||
class SiteController extends Controller
|
||||
{
|
||||
public function home(): void
|
||||
{
|
||||
$page = max(1, (int) ($this->f3->get('GET.page') ?? 1));
|
||||
$media = new Media();
|
||||
$result = (new Post())->paginateList($page, 12, $media);
|
||||
$page = max(1, (int) ($this->f3->get('GET.page') ?: 1));
|
||||
$result = (new Post())->page($page, 12);
|
||||
|
||||
$this->render('site/home.html', [
|
||||
'pageTitle' => 'Accueil',
|
||||
'posts' => $result['posts'],
|
||||
'pagination' => $result,
|
||||
'pageTitle' => 'Articles',
|
||||
'posts' => $result['items'],
|
||||
'pagination' => $result['pagination'],
|
||||
'paginationAlias' => 'home',
|
||||
], 300);
|
||||
}
|
||||
|
||||
public function show(): void
|
||||
{
|
||||
$media = new Media();
|
||||
$post = (new Post())->findBySlug((string) $this->f3->get('PARAMS.slug'), $media);
|
||||
if ($post === null) {
|
||||
$this->f3->error(404, 'Article introuvable.');
|
||||
$post = (new Post())->findBySlug((string) $this->f3->get('PARAMS.slug'));
|
||||
if (!$post) {
|
||||
$this->f3->error(404);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,6 +29,6 @@ class SiteController extends BaseController
|
||||
'pageTitle' => $post['title'],
|
||||
'metaDescription' => $post['excerpt'],
|
||||
'post' => $post,
|
||||
], 3600);
|
||||
], 300);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user