# F3 Simple Blog Blog simple avec Fat-Free Framework et SQLite. ## Structure ```text project/ ├── config.local.ini # Surcharges locales (gitignored) ├── app/ │ ├── config.ini # Configuration F3 (globals + routes) │ ├── bootstrap.php # Initialisation (config, DB, session, erreurs) │ ├── Controllers/ │ ├── Helpers/ # Fonctions utilitaires ciblées │ ├── Models/ # DB\SQL\Mapper (Post, Media, User) │ ├── Services/ # MarkdownService │ └── Views/ ├── db/ │ └── app.sqlite # Base SQLite persistante ├── logs/ │ └── php-error.log # Log PHP configuré au runtime ├── public/ │ ├── assets/ # Assets statiques servis directement │ └── uploads/ │ └── media/ # Images publiées (JPG conservé, PNG/WebP normalisés en PNG) ├── scripts/ │ ├── bootstrap.php # Autoload + bootstrap partagé par les scripts CLI │ ├── install.php # Initialisation idempotente de la base │ └── create-admin.php # Création d'un compte admin en CLI └── tmp/ # Runtime temporaire, recréable sans perte métier └── uploads/ # Transit Web::receive(), nettoyé après chaque upload ``` ## Fonctionnalités F3 utilisées - **Routage nommé** — `config.ini [routes]`, filtre `alias` dans les templates, `reroute('@route')` dans les contrôleurs. - **Cache HTTP** — TTL par contrôleur via `expire()`, forcé à `0` quand un utilisateur est connecté. - **Assets statiques** — servis directement depuis `public/assets/`, laissés au serveur HTTP / reverse proxy. - **Upload** — `Web::receive()` avec contrôle de taille, puis validation MIME/dimensions côté modèle. - **Images** — normalisation des médias via `Image`, lecture/écriture via `Base::read()` / `Base::write()`. - **Markdown** — `Markdown::instance()->convert()` suivi d'un assainissement DOM ciblé et de la résolution des images média. - **Slugs** — `Web::instance()->slug()`. - **Session / CSRF** — `$f3->set('JAR', …)`, `new Session(null, 'CSRF')`, hooks `beforeRoute()` sur les contrôleurs protégés, et petit pool de jetons côté session pour éviter les collisions multi-onglets. - **ORM** — `DB\SQL\Mapper` : `paginate()`, `copyfrom()`, `cast()`, `find()`. - **Erreurs** — gestion personnalisée en production via `ONERROR` + fallback HTML minimal sur erreur fatale. ## Prérequis ### Développement local - PHP 8.3+ - Composer - Extensions PHP : `pdo_sqlite`, `gd`, `mbstring`, `intl`, `dom` ### Déploiement Docker - Docker - Docker Compose ## Configuration Les paramètres par défaut sont dans `app/config.ini`. Pour surcharger localement ou en production : ```bash cp config.local.ini.example config.local.ini ``` Réglages minimums conseillés en production : ```ini [globals] app.env=prod app.timezone=Europe/Paris ``` ### Proxies de confiance L'application ne délègue pas la sécurité des cookies à Apache : elle détermine le schéma de la requête pour marquer la session en `Secure`. La valeur par défaut couvre les cas les plus courants : - `127.0.0.1,::1` — Caddy sur le même hôte ; - `172.16.0.0/12` — réseaux Docker IPv4 standard, y compris un Caddy conteneurisé sur le même réseau. Si ton proxy tourne sur un autre sous-réseau, surcharge `app.trusted_proxies` dans `config.local.ini`. ```ini [globals] app.trusted_proxies=127.0.0.1,::1,172.16.0.0/12 # ou, avec une liste F3 : app.trusted_proxies[]=10.0.0.42 ``` Seuls ces proxies sont autorisés à influencer `X-Forwarded-Proto` / `Forwarded`. ## Développement local ```bash composer install cp config.local.ini.example config.local.ini php scripts/install.php php -S 127.0.0.1:8080 -t public ``` Ouvre ensuite `http://127.0.0.1:8080`. Créer un compte admin : ```bash php scripts/create-admin.php admin # mot de passe : 10 caractères minimum ``` `scripts/install.php` peut être relancé sans danger : il crée les tables si elles n'existent pas. ## Déploiement avec Docker ```bash cp config.local.ini.example config.local.ini # édite config.local.ini (app.env=prod, app.timezone, trusted proxies si besoin) docker compose up -d --build ``` Le déploiement Docker est cohérent avec le projet : - l'image embarque les extensions réellement utilisées (`pdo_sqlite`, `gd`, `mbstring`, `intl`, `dom`) ; - `scripts/install.php` s'exécute au démarrage pour initialiser SQLite de façon idempotente ; - seuls les répertoires métier/persistants sont montés (`db/`, `logs/`, `public/uploads/media/`) ; - `tmp/` reste dans le conteneur et sert uniquement au runtime F3 (transit d'upload, fichiers temporaires). 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 sur les valeurs par défaut de `app/config.ini`. Le service écoute sur `http://127.0.0.1:8888`. Créer un compte admin : ```bash docker compose exec app php scripts/create-admin.php admin # mot de passe : 10 caractères minimum ``` ## Cache public Les pages publiques restent cacheables pour un visiteur anonyme : - `/` : TTL de 300 s ; - `/posts/@slug` : TTL de 3600 s. Quand un utilisateur est connecté, le rendu est forcé en non-cacheable avec `expire(0)` pour ne pas servir du contenu admin via un cache intermédiaire. Les assets statiques (`public/assets/`) sont servis directement ; leur éventuel cache HTTP relève du serveur web ou du reverse proxy. ## 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, 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 60 images les plus récentes. ## Reverse proxy Caddy ### Caddy sur le même hôte ```caddy blog.example.com { encode zstd gzip reverse_proxy 127.0.0.1:8888 } ``` ### Caddy dans Docker Si Caddy tourne aussi dans Docker, place-le sur le même réseau que `app` et cible directement le service : ```caddy blog.example.com { encode zstd gzip reverse_proxy app:80 } ``` Dans la plupart des cas, la valeur par défaut de `app.trusted_proxies` suffit aussi pour ce mode ; surcharge-la seulement si ton réseau Docker utilise un autre sous-réseau. Comme F3 lit aussi `X-Forwarded-For` pour déterminer `IP`, le conteneur applicatif ne doit pas être exposé directement à Internet : Caddy doit rester l'unique point d'entrée public et réécrire les en-têtes `X-Forwarded-*`. Le fichier `Caddyfile.example` fournit en plus un jeu d'en-têtes de sécurité minimal. ## Répartition des responsabilités avec Caddy Le projet n'essaie pas de dupliquer ce que le reverse proxy gère naturellement : - **Caddy** — TLS, compression, en-têtes de sécurité, terminaison HTTPS, reverse proxy. - **Application / PHP-F3** — sessions, CSRF, cookies, validation métier, rendu HTML et cache HTTP des pages dynamiques. ## Données à sauvegarder - `db/` — base SQLite. - `public/uploads/media/` — images. - `logs/` — optionnel, utile pour diagnostic. ## Mise à jour ```bash docker compose up -d --build ``` ## Logs - PHP : `logs/php-error.log`. - 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é. - Le pipeline Markdown n'autorise que les éléments utiles au rendu éditorial et remappe systématiquement les images vers la médiathèque locale.