mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
329 lines
13 KiB
Markdown
329 lines
13 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 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.
|