From 38eea09710d680fc3ce4602a15065b9baa910cf6 Mon Sep 17 00:00:00 2001 From: julien Date: Fri, 27 Mar 2026 22:40:44 +0100 Subject: [PATCH] Doc --- Caddyfile.example | 4 +- README.md | 72 +++++++++++++++++++----------- app/Controllers/BaseController.php | 21 ++++----- 3 files changed, 60 insertions(+), 37 deletions(-) diff --git a/Caddyfile.example b/Caddyfile.example index 33ac4ec..a6fca2c 100644 --- a/Caddyfile.example +++ b/Caddyfile.example @@ -1,5 +1,5 @@ -# Exemple de configuration Caddy en reverse proxy vers le conteneur Docker. -# Copier ce fichier vers Caddyfile et adapter le domaine. +# Exemple de configuration Caddy en reverse proxy vers l'application. +# Copier ce fichier vers Caddyfile et adapter le domaine / la cible. blog.example.com { # ── En-têtes de sécurité (toutes les réponses) ─────────────────── diff --git a/README.md b/README.md index a68426d..48923c0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # F3 Simple Blog -Blog simple avec Fat-Free Framework. +Blog simple avec Fat-Free Framework, SQLite et une petite médiathèque d’images. ## Structure @@ -8,25 +8,27 @@ Blog simple avec Fat-Free Framework. project/ ├── config.local.ini # Surcharges locales (gitignored) ├── app/ -│ ├── config.ini # Routes et variables F3 -│ ├── bootstrap.php # Initialisation (DB, session, cache, erreurs) +│ ├── config.ini # Configuration F3 (globals + routes) +│ ├── bootstrap.php # Initialisation (config, DB, session, erreurs) │ ├── Controllers/ -│ ├── Helpers/ # Fonctions utilitaires (App.php, Error.php) +│ ├── Helpers/ # Fonctions utilitaires │ ├── Models/ # DB\SQL\Mapper (Post, Media, User) │ ├── Services/ # MarkdownService │ └── Views/ ├── db/ -│ └── app.sqlite +│ └── app.sqlite # Base SQLite persistante ├── logs/ -│ ├── app.log -│ └── php-error.log +│ └── php-error.log # Log PHP configuré au runtime ├── public/ -│ ├── assets/ # Sources CSS/JS (servis minifiés via /min/@file) +│ ├── assets/ # Sources CSS/JS servies via /min/@file │ └── uploads/ │ └── media/ # Images publiées (JPG conservé, PNG/WebP normalisés en PNG) +├── scripts/ +│ ├── install.php # Initialisation idempotente de la base +│ └── create-admin.php # Création d’un compte admin en CLI └── tmp/ - ├── cache/ # Cache F3 (pages publiques + assets minifiés) - └── uploads/ # Transit Web::receive() — nettoyé après chaque upload + ├── cache/ # Cache F3 + assets minifiés + └── uploads/ # Transit Web::receive(), nettoyé après chaque upload ``` ## Philosophie des dossiers runtime @@ -43,15 +45,18 @@ Autrement dit, `tmp/` peut être vidé sans perte métier. Les données à sauve ## Fonctionnalités F3 utilisées - **Routage nommé** — `config.ini [routes]`, filtre `alias` dans les templates, `reroute('@route')` dans les contrôleurs -- **Cache HTTP + serveur** — TTL déclarés directement dans `[routes]`, `Cache::reset('.url')` à la mutation +- **Cache HTTP / F3** — TTL appliqués dans les contrôleurs avec `expire()` + - accueil : `300 s` + - page article : `3600 s` + - assets minifiés : `86400 s` - **Assets minifiés** — `Web::minify()` via `AssetController` (`GET /min/@file`) - **Upload** — `Web::receive()` avec contrôle de taille, puis validation MIME/dimensions côté modèle - **Images** — normalisation des médias via GD (`JPG` conservé, `PNG/WebP` convertis en `PNG` pour préserver la transparence) - **Markdown** — `Markdown::instance()->convert()` + reconstruction DOM en liste blanche - **Slugs** — `Web::instance()->slug()` -- **Session** — `$f3->set('JAR', …)`, hooks `beforeRoute()` sur les contrôleurs protégés, token CSRF créé seulement sur les vues qui contiennent des formulaires -- **Logging** — `Log` de F3 avec fallback `file_put_contents` +- **Session / CSRF** — `$f3->set('JAR', …)`, hooks `beforeRoute()` sur les contrôleurs protégés, jeton exposé via `@CSRF` puis recopié en session au rendu pour vérification lors du POST suivant - **ORM** — `DB\SQL\Mapper` : `paginate()`, `copyfrom()`, `cast()`, `find()` +- **Erreurs** — gestion personnalisée en production via `ONERROR` + fallback HTML minimal sur erreur fatale ## Prérequis @@ -59,7 +64,7 @@ Autrement dit, `tmp/` peut être vidé sans perte métier. Les données à sauve - PHP 8.3+ - Composer -- Extensions PHP : `pdo_sqlite`, `dom`, `gd`, `mbstring` +- Extensions PHP : `pdo_sqlite`, `dom`, `gd`, `mbstring`, `intl` ### Déploiement Docker @@ -84,10 +89,12 @@ app.env=prod app.timezone=Europe/Paris ``` -Le fichier `config.local.ini` sert uniquement aux surcharges d'environnement. Les chemins runtime restent les mêmes partout : +Le fichier `config.local.ini` sert uniquement aux surcharges d’environnement. Les chemins runtime restent les mêmes partout : - `tmp/cache/` pour le cache F3 et les assets minifiés -- `tmp/uploads/` pour les fichiers temporaires d'upload +- `tmp/uploads/` pour les fichiers temporaires d’upload + +Les données persistantes restent hors de `tmp` : `db/`, `logs/`, `public/uploads/media/`. ## Développement local @@ -115,9 +122,9 @@ cp config.local.ini.example config.local.ini docker compose up -d --build ``` -Docker ne monte que les dossiers persistants (`db/`, `logs/`, `public/uploads/media/`) et laisse `tmp/` dans le conteneur pour qu'il reste réellement éphémère. +Docker ne monte que les dossiers persistants (`db/`, `logs/`, `public/uploads/media/`) et laisse `tmp/` dans le conteneur pour qu’il reste réellement éphémère. -Si `config.local.ini` n'existe pas, le conteneur démarre avec les valeurs par défaut de `app/config.ini`. +Le fichier `config.local.ini` est monté en lecture seule. Si le fichier hôte n’existe pas, Docker peut créer un répertoire à la place ; l’entrypoint le supprime et l’application retombe alors sur les valeurs par défaut de `app/config.ini`. Le service écoute sur `http://127.0.0.1:8888`. @@ -130,17 +137,26 @@ docker compose exec app php scripts/create-admin.php admin ## Cache public et navigation -Les pages publiques (`/` et `/posts/@slug`) restent cacheables parce que leur rendu n'accède ni à la session, ni au token CSRF. La navigation publique affiche donc un lien statique vers la connexion / l'administration, tandis que les vues d'administration restent session-aware. +Les pages publiques restent cacheables pour un visiteur anonyme : -## Médias et limites d'upload +- `/` est servie avec un TTL de `300 s` +- `/posts/@slug` est servie avec un TTL de `3600 s` +- `/min/app.css` et `/min/app.js` sont servis avec un TTL de `86400 s` -- Formats acceptés à l'entrée : `JPG`, `PNG`, `WebP` +Quand un utilisateur est connecté, le layout dépend de la session (navigation admin + formulaire de déconnexion avec CSRF). Le rendu est alors forcé en non-cacheable avec `expire(0)`. + +Le projet ne fait pas d’invalidation explicite du cache public lors des mutations d’articles : la fraîcheur dépend donc des TTL ci-dessus. + +## Médias et limites d’upload + +- Formats acceptés à l’entrée : `JPG`, `PNG`, `WebP` - Taille max du fichier reçu : `10 Mo` - Dimensions max : `8000 × 8000 px` - Limite de surface : `40 mégapixels` - Sortie publiée : `JPG` pour les sources JPEG, `PNG` pour les sources PNG/WebP +- Texte alternatif initial dérivé du nom de fichier d’origine -La médiathèque admin est paginée et le picker dans l'éditeur charge seulement les images les plus récentes pour éviter de charger toute la bibliothèque en mémoire à chaque formulaire. +La médiathèque admin est paginée et le picker dans l’éditeur charge seulement les `60` images les plus récentes pour éviter de charger toute la bibliothèque en mémoire à chaque formulaire. ## Reverse proxy Caddy @@ -164,11 +180,13 @@ blog.example.com { } ``` +Le fichier `Caddyfile.example` fournit en plus un jeu d’en-têtes de sécurité minimal. + ## Données à sauvegarder - `db/` — base SQLite - `public/uploads/media/` — images -- `logs/` — optionnel +- `logs/` — optionnel, utile pour diagnostic - `tmp/` — non persistant, recréable ## Mise à jour @@ -179,6 +197,10 @@ docker compose up -d --build ## Logs -- Applicatifs : `logs/app.log` - PHP : `logs/php-error.log` -- Apache (conteneur) : `docker compose logs -f app` +- Apache / conteneur : `docker compose logs -f app` + +## Notes + +- Les dates sont stockées en UTC (`gmdate`) puis formatées côté affichage avec le fuseau configuré. +- `scripts/install.php` peut être relancé sans danger : il crée les tables si elles n’existent pas. diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php index 1bb87f4..e1982df 100644 --- a/app/Controllers/BaseController.php +++ b/app/Controllers/BaseController.php @@ -20,9 +20,10 @@ abstract class BaseController { $user = $this->currentUser(); - // Les pages publiques émettent un Cache-Control avec le TTL demandé. - // Un utilisateur connecté voit un état de session (nav, CSRF) : - // on force expire(0) pour ne pas servir ce rendu à d'autres visiteurs. + // Les pages publiques peuvent rester cacheables avec le TTL demandé. + // Si un utilisateur est connecté, le layout dépend de la session + // (navigation d'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']) @@ -36,9 +37,9 @@ abstract class BaseController 'metaDescription' => null, ]); - // Persister le jeton CSRF courant en session : le formulaire - // affiche @CSRF (jeton de cette requête) ; à la soumission, - // verifyCsrf() le comparera à SESSION.csrf. + // Mémoriser en session la valeur exposée à @CSRF pour que les + // formulaires rendus pendant cette réponse puissent être vérifiés + // lors du POST suivant par verifyCsrf(). $this->f3->copy('CSRF', 'SESSION.csrf'); echo Template::instance()->render('layout.html'); } @@ -64,10 +65,10 @@ abstract class BaseController $this->f3->reroute('@login'); } - // Le jeton CSRF est fourni par la classe Session de F3 - // (clé de ruche « CSRF », copiée en SESSION.csrf à chaque requête). - // Le formulaire envoie le jeton affiché lors du GET précédent, - // qu'on compare au jeton sauvegardé en session. + // La classe Session de F3 expose la valeur courante via « CSRF ». + // Au rendu, on la recopie en SESSION.csrf ; le formulaire renvoie + // ensuite le jeton affiché lors du rendu précédent, qu'on compare à + // la valeur mémorisée en session. protected function verifyCsrf(): void { $submitted = (string) ($this->f3->get('POST.csrf_token') ?? '');