mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 08:09:18 +02:00
Replace text labels (h1, bold, italic) with rendered HTML in the Rendu column: headings, strong, em, del, code, links, blockquote, lists, hr, sup, small
479 lines
18 KiB
Markdown
479 lines
18 KiB
Markdown
# 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 v2 Migration Plan
|
|
|
|
### The Core Pattern
|
|
|
|
HTMX v2 replaces the entire `autosave.js` fetch/debounce/CSRF-update loop. The key pieces:
|
|
|
|
- `hx-trigger` handles debouncing
|
|
- `hx-on::after-request` handles CSRF token rotation
|
|
- `hx-swap="none"` since you only need the JSON response side-effect
|
|
- A response header (`HX-Trigger`) can drive the status indicator
|
|
|
|
### 1. Native Input Forms (Contacts & Sidebar Links) — Full Replacement
|
|
|
|
These are straightforward since all inputs fire native DOM events.
|
|
|
|
```html
|
|
<form
|
|
hx-post="/admin/actions/apropos.php"
|
|
hx-trigger="change delay:1500ms, input delay:1500ms"
|
|
hx-swap="none"
|
|
hx-on::after-request="handleAutosaveResponse(event)"
|
|
>
|
|
<input type="hidden" name="csrf_token" value="...">
|
|
<input type="hidden" name="apropos_key" value="contacts">
|
|
|
|
<!-- your inputs -->
|
|
|
|
<span data-autosave-status></span>
|
|
</form>
|
|
```
|
|
|
|
One subtlety: HTMX v2 fires the trigger on the **element with `hx-trigger`**, which
|
|
here is the `<form>` — so `change` and `input` events bubbling up from child inputs
|
|
will correctly trigger it.
|
|
|
|
### 2. OverType Forms (Static Pages & Form Help) — Partial Replacement
|
|
|
|
OverType updates the hidden input programmatically without firing DOM events, so you
|
|
need to dispatch a custom event from its `onChange` hook:
|
|
|
|
```js
|
|
// In your OverType init
|
|
onChange: function(value) {
|
|
hiddenInput.value = value;
|
|
// Dispatch a custom event that HTMX can listen for
|
|
hiddenInput.dispatchEvent(
|
|
new CustomEvent('overtype:change', { bubbles: true })
|
|
);
|
|
}
|
|
```
|
|
|
|
Then on the form:
|
|
|
|
```html
|
|
<form
|
|
hx-post="/admin/actions/page.php"
|
|
hx-trigger="overtype:change delay:1500ms"
|
|
hx-swap="none"
|
|
hx-on::after-request="handleAutosaveResponse(event)"
|
|
>
|
|
<input type="hidden" name="csrf_token" value="...">
|
|
<input type="hidden" name="slug" value="charte">
|
|
<input type="hidden" id="content" name="content" value="">
|
|
|
|
<span data-autosave-status></span>
|
|
</form>
|
|
```
|
|
|
|
### 3. CSRF Rotation + Status Indicator
|
|
|
|
Replace `autosave.js`'s `.json().then()` pattern with a single shared handler. The
|
|
silent-parse-error risk disappears because you're explicitly handling the response:
|
|
|
|
```js
|
|
function handleAutosaveResponse(event) {
|
|
const status = event.target.closest('form')
|
|
.querySelector('[data-autosave-status]');
|
|
|
|
if (!event.detail.successful) {
|
|
if (status) status.textContent = 'Erreur !';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const data = JSON.parse(event.detail.xhr.responseText);
|
|
|
|
// Rotate CSRF token in both the form and the meta tag
|
|
if (data.csrf_token) {
|
|
event.target.closest('form')
|
|
.querySelector('input[name="csrf_token"]').value = data.csrf_token;
|
|
const meta = document.querySelector('meta[name="csrf-token"]');
|
|
if (meta) meta.content = data.csrf_token;
|
|
}
|
|
|
|
if (status) status.textContent = data.success ? 'Enregistré ✓' : 'Erreur !';
|
|
|
|
} catch {
|
|
// JSON parse failed (e.g. PHP warning in output) — surface it rather than silently swallowing
|
|
if (status) status.textContent = 'Erreur !';
|
|
console.warn('Autosave: could not parse response', event.detail.xhr.responseText);
|
|
}
|
|
}
|
|
```
|
|
|
|
This is a direct improvement over the current `autosave.js` `.catch(() => {})`
|
|
silent swallow — you now see the PHP warning in the console instead of just
|
|
getting a mystery 403 on the next save.
|
|
|
|
### 4. "Loading" State During Save
|
|
|
|
If you want a saving indicator (the current `"Enregistrement…"` state), use
|
|
`htmx:beforeRequest` on the form — or use HTMX's built-in `htmx-request` class
|
|
which is added to the element automatically while a request is in flight:
|
|
|
|
```css
|
|
/* Target the class HTMX adds */
|
|
form.htmx-request [data-autosave-status]::after {
|
|
content: 'Enregistrement…';
|
|
}
|
|
```
|
|
|
|
Or explicitly with an event listener:
|
|
|
|
```js
|
|
document.body.addEventListener('htmx:beforeRequest', e => {
|
|
const status = e.target.querySelector('[data-autosave-status]');
|
|
if (status) status.textContent = 'Enregistrement…';
|
|
});
|
|
```
|
|
|
|
### 5. The Add/Remove Group JS Still Works Unchanged
|
|
|
|
The reindex logic (updating `name` attributes after add/remove) is inline
|
|
`<script>` independent of `autosave.js`. This continues to work — HTMX reads
|
|
`FormData` at request time, so newly added/reindexed inputs are automatically
|
|
included in the next triggered save.
|
|
|
|
### Migration Checklist
|
|
|
|
| Form | Change required |
|
|
|---|---|
|
|
| `contenus-edit.php` (pages) | Add `hx-*` attrs, add `overtype:change` dispatch in OverType `onChange` |
|
|
| `contenus-edit.php` (form_help) | Same as above |
|
|
| `apropos-groups-form.php` (contacts) | Add `hx-*` attrs only |
|
|
| `contenus-edit.php` (sidebar_links) | Add `hx-*` attrs only |
|
|
| `autosave.js` | **Delete** once all four forms are migrated |
|
|
| Backend handlers | **No changes needed** — they already return `{success, csrf_token}` |
|
|
|
|
The backend is untouched throughout, which is the cleanest part of this migration.
|
|
|
|
## 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.
|