mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
Add autosave draft system for partage form with HTMX-based session persistence
- New fragment endpoint POST/GET /partage/fragments/draft.php: saves all form fields to PHP session, excludes file/csrf/slug fields GET returns JSON for JS hydration on page load rotates both global CSRF and share CSRF tokens in sync - form.php accepts optional $formExtraAttrs and $showAutosaveStatus: allows injecting HTMX attributes and 'Brouillon enregistré' indicator - renderShareLinkForm adds hx-post with change/input debounce trigger, loads autosave-handler.js, hydrate fields from draft on page load - Draft cleared on successful form submission in handleShareLinkSubmission - autosave-handler.js now also updates share_link_token hidden input when rotating CSRF token (partage form uses both csrf_token and share_link_token) - Added .autosave-status CSS to form.css (was admin.css-only) - Updated fragment routing to accept GET requests (needed for draft hydration)
This commit is contained in:
98
app/public/partage/fragments/draft.php
Normal file
98
app/public/partage/fragments/draft.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/**
|
||||
* Partagé autosave draft endpoint.
|
||||
*
|
||||
* POST — receive all form fields and persist them to the session draft store.
|
||||
* GET — return all stored draft fields as JSON for page-load hydration.
|
||||
*
|
||||
* The draft is scoped to the share link slug, kept in $_SESSION, and
|
||||
* cleared on successful form submission.
|
||||
*
|
||||
* Excluded field patterns (not persisted as drafts):
|
||||
* - csrf_token, share_link_token, share_password*
|
||||
* - FilePond metadata (filepond_mode, queue_file, filepond_*)
|
||||
* - Files-related fields (couverture, note_intention, files, annexes, etc.)
|
||||
* - Empty values
|
||||
*/
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
App::boot();
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
// ── CSRF check ──────────────────────────────────────────────────────────
|
||||
if ($method === 'POST') {
|
||||
if (
|
||||
!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|
||||
|| !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])
|
||||
) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Token de sécurité invalide.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Slug validation ─────────────────────────────────────────────────────
|
||||
$slug = $_GET['slug'] ?? ($_POST['slug'] ?? '');
|
||||
if (!preg_match('#^\d{8}-[A-Z0-9+/]{8}$#', $slug)) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Slug invalide.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Draft storage key
|
||||
$draftKey = 'partage_draft_' . $slug;
|
||||
|
||||
// ── POST: save all form fields ──────────────────────────────────────────
|
||||
if ($method === 'POST') {
|
||||
// Fields that should never be persisted as drafts
|
||||
$excludePrefixes = [
|
||||
'csrf_token', 'share_link_token', 'share_password',
|
||||
'filepond_mode', 'queue_file', 'filepond_',
|
||||
];
|
||||
$excludeExact = ['slug', 'couverture', 'note_intention', 'files', 'annexes',
|
||||
'peertube_video', 'peertube_audio', 'cover_remove',
|
||||
'go', 'MAX_FILE_SIZE'];
|
||||
|
||||
$draft = [];
|
||||
foreach ($_POST as $key => $value) {
|
||||
// Skip excluded fields
|
||||
if (in_array($key, $excludeExact, true)) continue;
|
||||
$skip = false;
|
||||
foreach ($excludePrefixes as $prefix) {
|
||||
if (str_starts_with($key, $prefix)) { $skip = true; break; }
|
||||
}
|
||||
if ($skip) continue;
|
||||
|
||||
// Skip empty values (but keep '0' as valid)
|
||||
if ($value === '' || $value === null || (is_array($value) && count($value) === 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$draft[$key] = $value;
|
||||
}
|
||||
|
||||
$_SESSION[$draftKey] = $draft;
|
||||
|
||||
// Rotate CSRF after mutation — keep share CSRF in sync
|
||||
$newToken = bin2hex(random_bytes(32));
|
||||
$_SESSION['csrf_token'] = $newToken;
|
||||
$_SESSION['share_csrf_' . $slug] = $newToken;
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'csrf_token' => $newToken,
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── GET: return draft fields for hydration ──────────────────────────────
|
||||
header('Content-Type: application/json');
|
||||
$draft = $_SESSION[$draftKey] ?? [];
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'draft' => $draft,
|
||||
]);
|
||||
exit;
|
||||
Reference in New Issue
Block a user