From 4a2b000fca3f0a7bc7c6549454b04c6da39d7b06 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Tue, 9 Jun 2026 19:43:09 +0200 Subject: [PATCH] Add Charte static page (public + admin editing) --- TODO.md | 34 +-- app/public/admin/actions/page.php | 2 +- app/public/admin/contenus-edit.php | 2 +- app/public/admin/contenus.php | 2 +- app/src/Controllers/CharteController.php | 42 +++ app/src/Dispatcher.php | 3 + app/storage/logs/admin-2026-06-09.log | 5 + app/templates/header.php | 8 + app/templates/public/charte.php | 9 + docs/autosave-system.md | 328 +++++++++++++++++++++++ 10 files changed, 405 insertions(+), 30 deletions(-) create mode 100644 app/src/Controllers/CharteController.php create mode 100644 app/templates/public/charte.php create mode 100644 docs/autosave-system.md diff --git a/TODO.md b/TODO.md index 74b42cc..150550e 100644 --- a/TODO.md +++ b/TODO.md @@ -1,29 +1,9 @@ # TODO -- [x] Fix #1: TFE publié se dépublie quand on modifie ses données (is_published missing from getThesisRawFields SELECT) -- [x] Fix #2: Renommer "Note contextuelle" → "Note contextuelle relative à soutenance" -- [x] Fix #3: Impossible de mettre une majuscule au nom d'étudiant·e — la recherche par nom en SQLite est case-sensitive (BINARY), contournait le UPDATE et tombait dans le fallback email sans updater le nom. Ajout COLLATE NOCASE + UPDATE dans le chemin email. -- [x] Fix #4: Décorréler contact interne et contact visible (ajouter colonne contact_visible sur theses) -- [x] Fix #4 (v2): Découplage complet dans ThesisCreateController — validateAndSanitise ne croise plus contact_interne ↔ mail/contact_visible, submit utilise contactInterne (et non mail) comme email de l'auteurice -- [x] Fix #5: "Contact public : non" partout, non modifiable, sans impact -- [x] Fix #6: Investiguer "libre → interne" impossible — aucune restriction trouvée dans le code admin -- [x] Hotfix: contact_visible manquant dans le SQL de updateThesis (l'edit matchait createThesis à la place) -- [x] Fix #7: Options de licence non persistées en edit — HTMX load trigger perdait les valeurs -- [x] Fix #3 (v2): findOrCreateAuthor avec cascade ID → nom → email, setThesisAuthors passe les IDs existants -- [x] Migration 038: corriger les identifiers theses qui ne matchent pas leur année -- [x] Filtres finalité + format dans la page de recherche (search.php) -- [x] Styliser boutons Filtrer/Réinitialiser : plus compacts, Réinitialiser en neutre -- [x] Commit + jj new - -- [x] Fix identifier-year mismatch: extend save() to regenerate identifier when prefix doesn't match year (not just on year change) -- [x] Fix migration runner run.php to support .php migrations alongside .sql -- [x] Fix runner: treat PHP subprocess idempotent errors (no such column / already exists) as skippable rather than fatal -- [x] Update 016 and 038 PHP migrations to accept $argv[1] DB path -- [x] Fix migration 016 to gracefully handle banner_path column already being dropped -- [x] Commit + jj new - -- [x] Formulaire étudiant : préciser "un seul contact" dans le label, mise à jour du hint pour le format le plus court (site sans https://www., insta/mastodon avec @), adaptation de l'affichage public pour supporter ces formats courts (liens automatiques pour @pseudo → Instagram, @pseudo@instance → Mastodon, domaine nu → https://) -- [x] Déplacer "Contact visible" du Backoffice vers "Informations du TFE" dans le formulaire admin edit, renommer "Identité" → "Informations du TFE" dans le récapitulatif admin -- [x] Rework contenus-edit: auto-save (debounce 1.5s) sur tous les formulaires (page, form-help, contacts, sidebar links), toolbar OverType sur tous les éditeurs markdown, sidebar links dynamiques (add/remove) remplaçant les 2 liens fixes erg_site_url/source_code_url par un seul key sidebar_links avec fallback de migration -- [x] Fix FilePond "Fichier trop volumineux Taille max: 1byte" — le plugin FileValidateSize surcharge le setter core de maxFileSize et parse la string "1GB" via toInt → 1 au lieu de toBytes → 1073741824. Passage de toutes les valeurs maxFileSize et perExtensionMaxSize en nombres bruts (bytes). Correction du nom d'option fileValidateSizeFilterItem → fileValidateSizeFilter. Adaptation de parseSize pour accepter les nombres. -- [x] Fix FilePond: fichiers perdus après reload — les uploads temporaires (tmp/filepond/) disparaissaient car data-existing-files ne contenait que les fichiers en DB. Ajout tracking session ($_SESSION['filepond_tmp']) dans handleProcess, injection des fichiers temporaires de la session dans data-existing-files via getSessionTempFiles(), loadTempFile() dans handleLoad pour streamer depuis tmp/, et routage remove → revert pour les hex IDs. +- [x] Create CharteController.php (public-facing controller) +- [x] Add /charte routes to Dispatcher.php +- [x] Create public/charte.php template +- [x] Add 'charte' to allowed slugs in contenus.php +- [x] Add 'charte' to allowed slugs in contenus-edit.php +- [x] Add 'charte' to allowed slugs in page.php action +- [x] Add Charte link to public header navigation (header.php) diff --git a/app/public/admin/actions/page.php b/app/public/admin/actions/page.php index 20595eb..ad4a6cb 100644 --- a/app/public/admin/actions/page.php +++ b/app/public/admin/actions/page.php @@ -23,7 +23,7 @@ if (!isset($_POST['csrf_token'], $_SESSION['csrf_token']) exit; } -$allowedSlugs = ['about', 'licenses']; +$allowedSlugs = ['about', 'licenses', 'charte']; $slug = $_POST['slug'] ?? ''; $content = $_POST['content'] ?? ''; diff --git a/app/public/admin/contenus-edit.php b/app/public/admin/contenus-edit.php index ea6497e..7f9467b 100644 --- a/app/public/admin/contenus-edit.php +++ b/app/public/admin/contenus-edit.php @@ -9,7 +9,7 @@ if (empty($_SESSION["csrf_token"])) { $_SESSION["csrf_token"] = bin2hex(random_bytes(32)); } -$allowedPageSlugs = ["about", "licenses"]; +$allowedPageSlugs = ["about", "licenses", "charte"]; $allowedApropos = ["contacts", "erg_site_url", "source_code_url"]; $pageSlug = $_GET["slug"] ?? ""; diff --git a/app/public/admin/contenus.php b/app/public/admin/contenus.php index c3756fb..6e5e4f0 100644 --- a/app/public/admin/contenus.php +++ b/app/public/admin/contenus.php @@ -10,7 +10,7 @@ if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } -$allowedPageSlugs = ['about', 'licenses']; +$allowedPageSlugs = ['about', 'licenses', 'charte']; try { $db = new Database(); diff --git a/app/src/Controllers/CharteController.php b/app/src/Controllers/CharteController.php new file mode 100644 index 0000000..8e480b8 --- /dev/null +++ b/app/src/Controllers/CharteController.php @@ -0,0 +1,42 @@ +getPage('charte'); + $content = $dbPage ? $dbPage['content'] : ''; + $pageTitle = $dbPage ? $dbPage['title'] : 'Charte'; + } catch (Exception $e) { + ErrorHandler::log('charte_page', $e); + $content = ''; + $pageTitle = 'Charte'; + } + + $converter = new CommonMarkConverter(['html_input' => 'strip']); + $html = EmailObfuscator::obfuscateHtml($converter->convert($content)->getContent()); + + return [ + 'content' => $content, + 'html' => $html, + 'pageTitle' => $pageTitle . ' – XAMXAM', + 'metaDescription' => "Charte d'utilisation de XAMXAM, le répertoire des TFE de l'erg.", + 'currentNav' => 'charte', + 'extraCss' => ['/assets/css/apropos.css'], + 'bodyClass' => 'apropos-body', + ]; + } +} diff --git a/app/src/Dispatcher.php b/app/src/Dispatcher.php index 80f420d..5348f99 100644 --- a/app/src/Dispatcher.php +++ b/app/src/Dispatcher.php @@ -13,6 +13,7 @@ * /tfe/ → TfeController → tfe view * /apropos → AboutController → about view * /licence → LicenceController → licence view + * /charte → CharteController → charte view * /media.php → MediaController (direct output) * /live-reload → LiveReloadController (direct output) * /partage/ → share-link flow @@ -34,6 +35,8 @@ class Dispatcher '/apropos.php' => ['controller' => 'AboutController', 'action' => 'handle', 'view' => 'public/about'], '/licence' => ['controller' => 'LicenceController', 'action' => 'handle', 'view' => 'public/licence'], '/licence.php' => ['controller' => 'LicenceController', 'action' => 'handle', 'view' => 'public/licence'], + '/charte' => ['controller' => 'CharteController', 'action' => 'handle', 'view' => 'public/charte'], + '/charte.php' => ['controller' => 'CharteController', 'action' => 'handle', 'view' => 'public/charte'], ]; private string $path; diff --git a/app/storage/logs/admin-2026-06-09.log b/app/storage/logs/admin-2026-06-09.log index 72ee02a..173d66b 100644 --- a/app/storage/logs/admin-2026-06-09.log +++ b/app/storage/logs/admin-2026-06-09.log @@ -16,3 +16,8 @@ {"timestamp":"2026-06-09T15:21:03+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"apropos","action":"edit","status":"success","context":{"key":"contacts"}} {"timestamp":"2026-06-09T15:23:37+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"apropos","action":"edit","status":"success","context":{"key":"sidebar_links"}} {"timestamp":"2026-06-09T15:23:39+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"apropos","action":"edit","status":"success","context":{"key":"sidebar_links"}} +{"timestamp":"2026-06-09T17:47:35+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"page","action":"edit","status":"success","context":{"slug":"charte"}} +{"timestamp":"2026-06-09T17:47:52+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"page","action":"edit","status":"success","context":{"slug":"charte"}} +{"timestamp":"2026-06-09T17:48:00+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"page","action":"edit","status":"success","context":{"slug":"charte"}} +{"timestamp":"2026-06-09T17:48:04+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"page","action":"edit","status":"success","context":{"slug":"charte"}} +{"timestamp":"2026-06-09T17:48:12+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"page","action":"edit","status":"success","context":{"slug":"charte"}} diff --git a/app/templates/header.php b/app/templates/header.php index b138625..789eb6b 100644 --- a/app/templates/header.php +++ b/app/templates/header.php @@ -50,6 +50,10 @@ $_thesisId = $_GET['id'] ?? null; >Licences +
  • + >Charte +
  • >À Propos @@ -68,6 +72,10 @@ $_thesisId = $_GET['id'] ?? null; >Licences
  • +
  • + >Charte +
  • >À Propos diff --git a/app/templates/public/charte.php b/app/templates/public/charte.php new file mode 100644 index 0000000..ba4d4da --- /dev/null +++ b/app/templates/public/charte.php @@ -0,0 +1,9 @@ +
    +
    + + + +

    Contenu à venir.

    + +
    +
    diff --git a/docs/autosave-system.md b/docs/autosave-system.md new file mode 100644 index 0000000..8c2afb7 --- /dev/null +++ b/docs/autosave-system.md @@ -0,0 +1,328 @@ +# Autosave System — Architecture & HTMX Migration Assessment + +## Overview + +The admin panel has a custom JavaScript autosave system (`autosave.js`) that +auto-submits forms after a 1.5s debounce. It is used on pages where content +is edited with the OverType Markdown editor (static pages, form help blocks) +and on structured-data forms (contacts, sidebar links). + +Three server-side action handlers implement the same CSRF-rotation + JSON +response contract that `autosave.js` consumes. + +## Architecture + +``` +┌──────────────────────────────────┐ +│ autosave.js │ +│ ───────── │ +│ Watches
    │ +│ Listens: input / change (bubble)│ +│ Debounce: 1500ms │ +│ POST via fetch() │ +│ Accept: application/json │ +│ On 2xx: shows "Enregistré ✓" │ +│ On err: shows "Erreur !", retry │ +│ Updates CSRF token from response│ +└──────────┬───────────────────────┘ + │ POST (JSON) + ▼ +┌──────────────────────────────────┐ +│ Backend handler (page.php, │ +│ apropos.php, form-help.php) │ +│ ────────────────────────────────│ +│ 1. Check CSRF token │ +│ 2. Validate payload │ +│ 3. Save to DB │ +│ 4. Regenerate CSRF token │ +│ 5. Return JSON: │ +│ {success:true, csrf_token:X} │ +└──────────────────────────────────┘ +``` + +## Forms Using `data-autosave` + +### 1. Static Pages (about, licenses, charte) + +| Attribute | Value | +|---|---| +| **Template** | `app/templates/admin/contenus-edit.php` (branches `about_page` and `page`) | +| **Form action** | `/admin/actions/page.php` | +| **Editor** | OverType (custom `contenteditable`-based Markdown WYSIWYG) | +| **Fields** | `csrf_token` (hidden), `slug` (hidden), `content` (hidden, synced by OverType `onChange`) | +| **How events reach autosave** | User types in OverType's `contenteditable` div → native `input` events bubble up to `` → autosave.js detects them | + +**Key interaction**: OverType's `onChange(value) { hiddenInput.value = value }` sets +the hidden `#content` input's value programmatically. This does NOT fire native DOM +events. The autosave trigger comes from `input` events on the contenteditable editor +div, not from the hidden input. + +### 2. About Contacts (apropos groups) + +| Attribute | Value | +|---|---| +| **Template** | `app/templates/admin/apropos-groups-form.php` | +| **Form action** | `/admin/actions/apropos.php` | +| **Editor** | Native text/email/url inputs | +| **Fields** | `csrf_token`, `apropos_key`, `groups[][role]`, `groups[][entries][][text/email/url]` | + +Has custom JS for adding/removing contact groups and entries (inline `