Files
xamxam/app/public/admin/actions/draft.php
Pontoporeia 19bf9f101a Refactor apropos/charte/licence pages: shared layout, TOC anchors, and UI polish
Unify the three public pages (à propos, charte, licence) onto a single
grid layout (.page-content) with sticky TOC sidebar, replacing the old
separate  /  /  markup.

- Merge about.php, charte.php, licence.php templates into shared
  .page-content / .content-section structure
- Add CommonMark HeadingPermalinkExtension for stable heading anchors
- Use SlugNormalizer for TOC links so they match rendered heading IDs
- Standardize link styling across content blocks: bold black, accent on
  hover (consistent with global link style)
- Fix code block wrapping: use pre-wrap instead of pre, constrain grid
  columns with min-width:0, auto scrollbar
- Fix apropos page grid placement: force content-section into column 2
  so contacts and credits stay in the content area, not the sidebar

Also includes accumulated WIP changes:
- Header gradient: hardcoded purple-to-green (replaces CSS variables)
- Search placeholder font
- Duration field: replace minutes/sec/heures with h:m:s time inputs
- TFE file optional for formats 1,4,6 with client-side JS toggle
- Licence form: em-dash to hyphen, details/summary classes
- Pill search: block Enter key form submission when no results
- Draft autosave: remove CSRF rotation (broke concurrent FilePond uploads)
- Language pill: clear hints for excluded main languages
- Search results: gradient placeholder cards for items without covers
- TFE display: format durée values as XhYm instead of decimal
2026-06-19 19:40:05 +02:00

98 lines
3.4 KiB
PHP

<?php
/**
* Admin autosave draft endpoint.
*
* POST — receive all form fields and persist them to the session draft store.
*
* Drafts are scoped per mode:
* - add: keyed by a generated token (stored in form)
* - edit: keyed by thesis_id
*
* Excluded field patterns (not persisted as drafts):
* - csrf_token
* - FilePond metadata (filepond_mode, queue_file, filepond_*)
* - Files-related fields
* - Empty values
*/
require_once __DIR__ . '/../../../bootstrap.php';
require_once APP_ROOT . '/src/AdminAuth.php';
AdminAuth::requireLogin();
$method = $_SERVER['REQUEST_METHOD'];
// ── CSRF check ──────────────────────────────────────────────────────────
if ($method !== 'POST') {
http_response_code(405);
header('Content-Type: application/json');
echo json_encode(['error' => 'Méthode non autorisée.']);
exit;
}
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;
}
// ── Determine draft key ─────────────────────────────────────────────────
$draftToken = $_POST['draft_token'] ?? '';
$thesisId = (int)($_POST['thesis_id'] ?? 0);
if ($draftToken !== '' && preg_match('/^[a-f0-9]{16}$/', $draftToken)) {
$draftKey = 'admin_draft_' . $draftToken;
} elseif ($thesisId > 0) {
$draftKey = 'admin_draft_edit_' . $thesisId;
} else {
http_response_code(400);
header('Content-Type: application/json');
echo json_encode(['error' => 'Paramètres invalides.']);
exit;
}
// ── Save all form fields ────────────────────────────────────────────────
$excludePrefixes = [
'csrf_token', 'share_link_token',
'filepond_mode', 'queue_file', 'filepond_',
];
$excludeExact = ['draft_token', 'thesis_id', 'slug',
'couverture', 'note_intention', 'files', 'annexes',
'peertube_video', 'peertube_audio', 'cover_remove',
'go', 'MAX_FILE_SIZE'];
$draft = [];
foreach ($_POST as $key => $value) {
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;
if ($value === '' || $value === null || (is_array($value) && count($value) === 0)) {
continue;
}
$draft[$key] = $value;
}
$_SESSION[$draftKey] = $draft;
// NOTE: Do NOT rotate the CSRF token here.
// Rotating it breaks concurrent requests:
// 1. FilePond uploads in flight use the old token (from <meta name="csrf-token">)
// and fail when the server session already has the new token.
// 2. Overlapping autosave requests hit CSRF mismatch.
// 3. HTMX fragment requests (pill-search, language-autre) can't use the old token.
// The CSRF token already rotates on page load and form submit — that's sufficient.
// Autosave is a background persistence mechanism and does not need token rotation.
header('Content-Type: application/json');
echo json_encode([
'success' => true,
]);
exit;