mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 08:09:18 +02:00
Add Charte static page (public + admin editing)
This commit is contained in:
34
TODO.md
34
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)
|
||||
|
||||
@@ -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'] ?? '';
|
||||
|
||||
|
||||
@@ -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"] ?? "";
|
||||
|
||||
@@ -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();
|
||||
|
||||
42
app/src/Controllers/CharteController.php
Normal file
42
app/src/Controllers/CharteController.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/ErrorHandler.php';
|
||||
require_once APP_ROOT . '/src/EmailObfuscator.php';
|
||||
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
|
||||
class CharteController
|
||||
{
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function handle(): array
|
||||
{
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$dbPage = $db->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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
* /tfe/<id> → 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/<slug> → 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;
|
||||
|
||||
@@ -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"}}
|
||||
|
||||
@@ -50,6 +50,10 @@ $_thesisId = $_GET['id'] ?? null;
|
||||
<a href="/licence"
|
||||
<?= ($_navCurrent === 'licence') ? 'aria-current="page"' : '' ?>>Licences</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/charte"
|
||||
<?= ($_navCurrent === 'charte') ? 'aria-current="page"' : '' ?>>Charte</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/apropos"
|
||||
<?= ($_navCurrent === 'apropos') ? 'aria-current="page"' : '' ?>>À Propos</a>
|
||||
@@ -68,6 +72,10 @@ $_thesisId = $_GET['id'] ?? null;
|
||||
<a href="/licence"
|
||||
<?= ($_navCurrent === 'licence') ? 'aria-current="page"' : '' ?>>Licences</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/charte"
|
||||
<?= ($_navCurrent === 'charte') ? 'aria-current="page"' : '' ?>>Charte</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/apropos"
|
||||
<?= ($_navCurrent === 'apropos') ? 'aria-current="page"' : '' ?>>À Propos</a>
|
||||
|
||||
9
app/templates/public/charte.php
Normal file
9
app/templates/public/charte.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<main class="apropos-main" id="main-content">
|
||||
<div class="prose apropos-single">
|
||||
<?php if (!empty(trim($content))): ?>
|
||||
<?= $html ?>
|
||||
<?php else: ?>
|
||||
<p>Contenu à venir.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</main>
|
||||
328
docs/autosave-system.md
Normal file
328
docs/autosave-system.md
Normal file
@@ -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 <form data-autosave> │
|
||||
│ 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 `<form>` → 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 `<script>`
|
||||
in the template). The reindex logic updates `name` attributes after add/remove.
|
||||
|
||||
### 3. About Sidebar Links
|
||||
|
||||
| Attribute | Value |
|
||||
|---|---|
|
||||
| **Template** | Inline in `app/templates/admin/contenus-edit.php` (the `about_page` branch) |
|
||||
| **Form action** | `/admin/actions/apropos.php` |
|
||||
| **Fields** | `csrf_token`, `apropos_key=sidebar_links`, `links[][label]`, `links[][url]` |
|
||||
|
||||
Has custom JS for add/remove/reindex (inline `<script>` in the template).
|
||||
|
||||
### 4. Form Help Blocks
|
||||
|
||||
| Attribute | Value |
|
||||
|---|---|
|
||||
| **Template** | `app/templates/admin/contenus-edit.php` (branch `form_help`) |
|
||||
| **Form action** | `/admin/actions/form-help.php` |
|
||||
| **Editor** | OverType (same as static pages) |
|
||||
| **Fields** | `csrf_token`, `form_help_key`, `content` |
|
||||
|
||||
Same OverType + autosave integration as static pages.
|
||||
|
||||
## Backend Handlers
|
||||
|
||||
### `page.php` — `/admin/actions/page.php`
|
||||
|
||||
```
|
||||
CSRF check → slug validation (about|licenses|charte) → savePage(slug, content)
|
||||
→ AdminLogger::logPageEdit() → regenerate CSRF → return JSON
|
||||
```
|
||||
|
||||
On AJAX: returns `{success: true, csrf_token: "<new_token>"}`.
|
||||
On non-AJAX (regular form POST): flashes message, redirects to `/admin/contenus.php`.
|
||||
|
||||
### `apropos.php` — `/admin/actions/apropos.php`
|
||||
|
||||
```
|
||||
CSRF check → key validation → dispatch by key type:
|
||||
URL keys (erg_site_url, source_code_url) → saveAproposContent(key, url)
|
||||
Link lists (sidebar_links) → validate URLs, save structured array
|
||||
Group-based (contacts) → validate groups/entries, save structured array
|
||||
→ AdminLogger::logAproposEdit() → regenerate CSRF → return JSON
|
||||
```
|
||||
|
||||
### `form-help.php` — `/admin/actions/form-help.php`
|
||||
|
||||
```
|
||||
CSRF check → key validation (FORM_HELP_KEYS) → setFormHelpBlock(key, content)
|
||||
→ AdminLogger::logFormStructureEdit() → regenerate CSRF → return JSON
|
||||
```
|
||||
|
||||
### CSRF Rotation Pattern (shared by all three)
|
||||
|
||||
```
|
||||
1. Verify: hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])
|
||||
2. Save to DB
|
||||
3. Rotate: $_SESSION['csrf_token'] = bin2hex(random_bytes(32))
|
||||
4. Return: {success: true, csrf_token: <NEW>}
|
||||
5. Client: autosave.js reads data.csrf_token, updates hidden input
|
||||
```
|
||||
|
||||
## Other Autosave-like UI (Non-autosave.js)
|
||||
|
||||
These use HTMX directly, NOT `autosave.js`:
|
||||
|
||||
| Location | Trigger | Target | Handler |
|
||||
|---|---|---|---|
|
||||
| Settings checkboxes (Accès, Types) | `change` via `hx-post` | `#acces-response` / `#types-response` | `/admin/actions/settings.php` |
|
||||
| TFE messages textareas | `change` via `hx-post` | inline toast | `/admin/actions/settings.php` |
|
||||
| File restrictions toggle | `change` via `hx-post` | inline toast | `/admin/actions/settings.php` |
|
||||
| Form help inline editor | Manual `Enregistrer` via `hx-post` | collapsed chip | `/admin/form-help-inline-fragment.php` |
|
||||
| Form help toggle (dot button) | Click via `hx-post` | collapsed chip | `/admin/form-help-inline-fragment.php` |
|
||||
| Language inline renames | Manual submit via `hx-post` | `#langues-table-wrap` | `/admin/actions/language.php` |
|
||||
|
||||
## autosave.js — Detailed Behavior
|
||||
|
||||
```js
|
||||
// Watches: form[data-autosave]
|
||||
// Debounce: 1500ms after last input/change event
|
||||
// Status display: sibling element matching [data-autosave-status]
|
||||
|
||||
const DEBOUNCE_MS = 1500;
|
||||
let timer = null;
|
||||
let dirty = false;
|
||||
|
||||
// schedule() — called on every input/change event
|
||||
// Sets dirty=true, clears previous timer, starts new 1500ms timer
|
||||
schedule() → clearTimeout(timer); timer = setTimeout(doSave, 1500);
|
||||
|
||||
// doSave() — fires after 1500ms of inactivity
|
||||
doSave() → if (!dirty) return; // guard: skip if not dirty
|
||||
dirty = false; // mark clean (re-set to true on error)
|
||||
setStatus('Enregistrement…');
|
||||
const fd = new FormData(form); // snapshots ALL current form values
|
||||
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: fd,
|
||||
headers: { 'Accept': 'application/json' }
|
||||
})
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status); // 403/500/etc → catch
|
||||
setStatus('Enregistré ✓');
|
||||
r.json().then(data => {
|
||||
if (data.csrf_token) {
|
||||
// Update both form input and meta tag
|
||||
form.querySelector('input[name="csrf_token"]').value = data.csrf_token;
|
||||
document.querySelector('meta[name="csrf-token"]').content = data.csrf_token;
|
||||
}
|
||||
}).catch(() => {}); // silently ignore JSON parse errors
|
||||
})
|
||||
.catch(() => {
|
||||
setStatus('Erreur !');
|
||||
dirty = true; // re-arm: will retry on next input
|
||||
});
|
||||
```
|
||||
|
||||
**Important**: The CSRF token update happens asynchronously inside `.json().then()`,
|
||||
after the status is already set to "Enregistré ✓". In practice this is fine
|
||||
because the 1.5s debounce provides ample time for the microtask to complete
|
||||
before the next save cycle.
|
||||
|
||||
If the JSON response is malformed (PHP warnings/notices in output), the
|
||||
`.catch(() => {})` silently discards the parse error, the CSRF token stays
|
||||
stale, and all subsequent saves fail with 403.
|
||||
|
||||
## Known Issues
|
||||
|
||||
### 403 Forbidden on Autosave
|
||||
|
||||
Observed on charte page (and possibly about/licenses). The response is:
|
||||
```
|
||||
HTTP/1.1 403 Forbidden
|
||||
{"error": "Erreur de sécurité : token invalide."}
|
||||
```
|
||||
|
||||
The 403 comes from the CSRF check in `page.php` (line ~18):
|
||||
```php
|
||||
if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|
||||
|| !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
|
||||
```
|
||||
|
||||
Possible causes:
|
||||
|
||||
1. **Stale CSRF after error**: First save succeeds and rotates the token, but
|
||||
`autosave.js` fails to update the form (e.g., JSON parse error from PHP
|
||||
warning in output). All subsequent saves fail with 403.
|
||||
|
||||
2. **Session not initialized**: If `$_SESSION['csrf_token']` is not set when
|
||||
`page.php` runs. In admin context, it should be set by:
|
||||
- `contenus.php` (list page): `if (empty($_SESSION['csrf_token'])) { ... }`
|
||||
- `contenus-edit.php` (edit page): `if (empty($_SESSION['csrf_token'])) { ... }`
|
||||
- `AdminAuth::requireLogin()` calls `session_start()`
|
||||
|
||||
3. **Session collision**: PHP's default file-based session locking serializes
|
||||
concurrent requests. Unlikely with 1.5s debounce.
|
||||
|
||||
4. **OverType → hidden input sync**: OverType's `onChange` updates the hidden
|
||||
input, but if the OverType script fails to load, the hidden input stays
|
||||
empty, and the form submission has no content (but CSRF token should still
|
||||
be present).
|
||||
|
||||
### Non-event-emitting value updates
|
||||
|
||||
`autosave.js` relies on `input`/`change` events bubbling. The OverType editor
|
||||
updates `hidden.value` programmatically. If someone adds custom JS that updates
|
||||
form values without firing events, autosave won't detect those changes.
|
||||
|
||||
## HTMX Migration Feasibility
|
||||
|
||||
The settings toggles already use HTMX successfully for similar patterns
|
||||
(checkbox → toggle → server → toast). Here's an assessment for each form:
|
||||
|
||||
### 1. Static Pages (page.php) — **Challenging**
|
||||
|
||||
The OverType editor is a heavy custom JS component (contenteditable-based
|
||||
Markdown WYSIWYG). HTMX could handle the *submission* side (replacing
|
||||
`fetch()` with `hx-post`), but the editor itself would remain custom JS.
|
||||
|
||||
An HTMX approach would:
|
||||
- Add `hx-post="/admin/actions/page.php"` to the form
|
||||
- Add `hx-trigger="every 3s"` or a custom event dispatched by OverType's `onChange`
|
||||
- Use `hx-swap="none"` or a status indicator swap
|
||||
- Need a custom event from OverType like `overtype:changed` to trigger saves
|
||||
at the right time (not polling-based like `every 3s`)
|
||||
|
||||
**Verdict**: Partial replacement possible. HTMX for submission, keep OverType
|
||||
for editing. Could eliminate ~40 lines of autosave.js logic for this form.
|
||||
|
||||
### 2. About Contacts (apropos.php) — **Easy**
|
||||
|
||||
Simple text inputs. The add/remove group/entry logic is separate JS. Autosave
|
||||
just watches `input` events.
|
||||
|
||||
HTMX approach:
|
||||
- `hx-post` with `hx-trigger="change delay:1500ms"` on each input
|
||||
- `hx-swap="none"` + custom status indicator via `hx-on::after-request`
|
||||
|
||||
**Verdict**: Straightforward replacement. Eliminates autosave.js dependency.
|
||||
|
||||
### 3. About Sidebar Links (apropos.php) — **Easy**
|
||||
|
||||
Same as contacts — simple text/URL inputs. The add/remove JS is independent
|
||||
of autosave.
|
||||
|
||||
**Verdict**: Same as contacts.
|
||||
|
||||
### 4. Form Help Blocks (form-help.php) — **Challenging**
|
||||
|
||||
Same OverType integration as static pages. Same considerations apply.
|
||||
|
||||
**Verdict**: Same as static pages.
|
||||
|
||||
### 5. The autosave.js Itself — **Can be Replaced**
|
||||
|
||||
If every form with `data-autosave` is converted to HTMX, `autosave.js` can be
|
||||
removed entirely. The script is ~60 lines.
|
||||
|
||||
### Summary Table
|
||||
|
||||
| Form | Editor | HTMX Feasibility | Effort |
|
||||
|---|---|---|---|
|
||||
| Static pages (about/licenses/charte) | OverType | Partial (keep OverType) | Medium |
|
||||
| Form help blocks | OverType | Partial (keep OverType) | Medium |
|
||||
| About contacts | Native inputs | Full | Low |
|
||||
| About sidebar links | Native inputs | Full | Low |
|
||||
|
||||
### Recommended HTMX Strategy
|
||||
|
||||
For static pages and form help blocks:
|
||||
|
||||
```html
|
||||
<form hx-post="/admin/actions/page.php"
|
||||
hx-trigger="overtype:changed delay:1500ms"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="
|
||||
var data = JSON.parse(event.detail.xhr.responseText);
|
||||
if (data.csrf_token) {
|
||||
document.querySelector('input[name=csrf_token]').value = data.csrf_token;
|
||||
}
|
||||
"
|
||||
data-autosave>
|
||||
```
|
||||
|
||||
And dispatch `overtype:changed` from OverType's `onChange`:
|
||||
```js
|
||||
onChange: function(value) {
|
||||
hidden.value = value;
|
||||
editorElement.dispatchEvent(new CustomEvent('overtype:changed', { bubbles: true }));
|
||||
}
|
||||
```
|
||||
|
||||
For contacts/sidebar links — use per-input `hx-trigger="change delay:1500ms"`
|
||||
or a form-level trigger on any input change.
|
||||
|
||||
The key advantage: HTMX handles the CSRF token rotation natively via
|
||||
`hx-on::after-request`, eliminating the async `.json().then()` pattern
|
||||
and the associated silent-parse-error risk.
|
||||
Reference in New Issue
Block a user