Doc
This commit is contained in:
@@ -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) ───────────────────
|
||||
|
||||
72
README.md
72
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.
|
||||
|
||||
@@ -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') ?? '');
|
||||
|
||||
Reference in New Issue
Block a user