From 5f24dcae7eb26ea2572815efab9a4da3f9fa8b82 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Mon, 4 May 2026 17:04:09 +0200 Subject: [PATCH] fix: duplicate warning not shown in admin, double-encoded in partage, no focus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - toast-fragment.php: 204 early-exit now also checks flash['warning']; previously the warning was consumed by consumeFlash() then silently dropped - partage/index.php: store warning as plain text; htmlspecialchars() applied once at render time — previously htmlspecialchars() was called inside the stored string then again at output, producing ' entities etc. - partage/index.php: flash-warning div gets id + tabindex=-1; inline JS scrolls it into view and focuses it on DOMContentLoaded - admin/footer.php: htmx:afterSettle listener focuses .toast--warning after HTMX injects the toast fragment into #toast-region --- TODO.md | 8 +++++++- app/public/admin/actions/formulaire.php | 10 +++++----- app/public/admin/toast-fragment.php | 2 +- app/public/assets/css/admin.css | 1 + app/public/assets/css/form.css | 1 + app/public/partage/index.php | 26 +++++++++++++------------ app/storage/logs/form-submissions.log | 5 +++++ app/templates/admin/footer.php | 8 ++++++++ 8 files changed, 42 insertions(+), 19 deletions(-) diff --git a/TODO.md b/TODO.md index 5dc6045..ed16cea 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,6 @@ # XAMXAM TODO -## Duplicate TFE submission prevention +## Duplicate TFE submission prevention (fixes) - [x] `DuplicateThesisException` — typed exception carrying existing thesis metadata - [x] `Database::findDuplicateThesis()` — year + author + normalised-title matching (exact, prefix, Levenshtein ≤10%) - [x] `ThesisCreateController::submit()` — calls duplicate check before any DB write, throws `DuplicateThesisException` @@ -11,3 +11,9 @@ - [x] `toast.php` — renders `toast--warning` block - [x] `admin.css` — `.toast--warning` style + link colour - [x] `form.css` — `.flash-warning` style (partage form) + +## Duplicate warning display fixes +- [x] `toast-fragment.php` — 204 guard now also checks `warning`; warning was silently discarded before +- [x] `partage/index.php` — warning stored as plain text (no pre-escaping); `htmlspecialchars()` applied once at render; was double-encoded before +- [x] `partage/index.php` — `flash-warning` div gets `id` + `tabindex=-1`; inline JS scrolls and focuses it on load +- [x] `admin/footer.php` — `htmx:afterSettle` listener focuses `.toast--warning` after HTMX injects the toast fragment diff --git a/app/public/admin/actions/formulaire.php b/app/public/admin/actions/formulaire.php index 456b719..3e356ab 100644 --- a/app/public/admin/actions/formulaire.php +++ b/app/public/admin/actions/formulaire.php @@ -13,7 +13,7 @@ AdminAuth::requireLogin(); if (!isset($_POST['csrf_token'], $_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) { error_log(sprintf( - 'CSRF token validation failed in formulaire.php — POST token: %s, SESSION token: %s', + 'CSRF token validation failed in formulaire.php - POST token: %s, SESSION token: %s', $_POST['csrf_token'] ?? '(missing)', $_SESSION['csrf_token'] ?? '(missing)' )); @@ -49,10 +49,10 @@ try { // Build a warning with a clickable link to the existing thesis. $existingUrl = htmlspecialchars('/admin/edit.php?id=' . $e->existingThesisId); - $existingRef = htmlspecialchars($e->existingIdentifier . ' — ' . $e->existingTitle . ' (' . $e->existingYear . ')'); - $warningHtml = 'Doublon détecté : un TFE très similaire existe déjà. ' - . '' . $existingRef . '' - . ' Vérifiez avant de soumettre à nouveau.'; + $existingRef = htmlspecialchars($e->existingIdentifier . ' - ' . $e->existingTitle . ' (' . $e->existingYear . ')'); + $warningHtml = 'Doublon détecté : un TFE très similaire existe déjà.' + . '
' . $existingRef . '' + . '
Vérifiez avant de soumettre à nouveau.'; App::flash('warning', $warningHtml); $_SESSION['form_data'] = $_POST; diff --git a/app/public/admin/toast-fragment.php b/app/public/admin/toast-fragment.php index 2f61873..7276599 100644 --- a/app/public/admin/toast-fragment.php +++ b/app/public/admin/toast-fragment.php @@ -13,7 +13,7 @@ AdminAuth::requireLogin(); $flash = App::consumeFlash(); -if (!$flash['error'] && !$flash['success']) { +if (!$flash['error'] && !$flash['success'] && !$flash['warning']) { http_response_code(204); exit; } diff --git a/app/public/assets/css/admin.css b/app/public/assets/css/admin.css index 2dcd250..8d64998 100644 --- a/app/public/assets/css/admin.css +++ b/app/public/assets/css/admin.css @@ -186,6 +186,7 @@ background: var(--bg-secondary); border-color: var(--warning); color: var(--text-primary); + animation: toast-enter 0.35s ease-out; /* no fade-out — stays until dismissed */ } .toast--warning a { diff --git a/app/public/assets/css/form.css b/app/public/assets/css/form.css index 9fdc77d..e6913e4 100644 --- a/app/public/assets/css/form.css +++ b/app/public/assets/css/form.css @@ -360,6 +360,7 @@ label:has(+ div > input:required)::after { background: var(--warning-muted-bg, rgba(251,202,81,.12)); border-color: var(--warning-muted-border, rgba(251,202,81,.35)); color: var(--text-primary); + white-space: pre-line; } /* ── Share link badge ───────────────────────────────────────────────────── */ diff --git a/app/public/partage/index.php b/app/public/partage/index.php index 34c910a..6b8469d 100644 --- a/app/public/partage/index.php +++ b/app/public/partage/index.php @@ -1,11 +1,11 @@ — Render the share-link form (or password gate) - * /partage//submit — POST endpoint for form submissions via share link - * /partage/recapitulatif.php?id=N — Post-submission confirmation page + * /partage/ - Render the share-link form (or password gate) + * /partage//submit - POST endpoint for form submissions via share link + * /partage/recapitulatif.php?id=N - Post-submission confirmation page */ require_once __DIR__ . '/../../bootstrap.php'; @@ -83,7 +83,7 @@ if (!$validationResult['valid']) { exit; } -// Link is valid — render the form +// Link is valid - render the form $link = $validationResult['link']; renderShareLinkForm($slug, $link); @@ -222,7 +222,7 @@ function renderShareLinkForm(string $slug, array $link): void // Build old()-compatible callable from $formData (share forms use the array variant). $shareOldFn = fn(string $key, string $default = '') => old($formData, $key, $default); - // No autofocus in the share form — identity function. + // No autofocus in the share form - identity function. $shareWithAutofocusFn = fn(string $field, array $attrs = []) => $attrs; // Load all form help blocks in one query. @@ -267,7 +267,8 @@ function renderShareLinkForm(string $slug, array $link): void - + + @@ -460,7 +461,7 @@ function handleShareLinkSubmission(string $slug): void unset($_SESSION[$shareCsrfKey]); unset($_SESSION['share_verified_' . $slug]); - // Send confirmation e-mail — on delivery failure, redirect to retry page + // Send confirmation e-mail - on delivery failure, redirect to retry page $emailError = null; try { $emailSent = StudentEmail::sendConfirmation(Database::getInstance(), $thesisId, $_POST); @@ -472,7 +473,7 @@ function handleShareLinkSubmission(string $slug): void header('Location: /partage/retry-email?id=' . urlencode((string)$thesisId)); exit(); } - // Non-recipient errors (relay down, etc.) — skip email silently + // Non-recipient errors (relay down, etc.) - skip email silently $_SESSION['share_email_sent'] = false; } @@ -488,9 +489,10 @@ function handleShareLinkSubmission(string $slug): void error_log('Share link duplicate submission: ' . $e->getMessage()); // Repopulate the form and surface a clear warning to the student. - $_SESSION['_flash_warning'] = 'Votre soumission ressemble à un TFE déjà enregistré (' - . htmlspecialchars($e->existingIdentifier . ' — ' . $e->existingTitle . ', ' . $e->existingYear) - . '). Si vous pensez qu’il s’agit d’une erreur, veuillez contacter l’équipe.'; + // Store as plain text — htmlspecialchars() is applied at render time. + $_SESSION['_flash_warning'] = 'Votre soumission ressemble à un TFE déjà enregistré.' + . "\n" . $e->existingIdentifier . ' — ' . $e->existingTitle . ' (' . $e->existingYear . ')' + . "\nSi vous pensez qu'il s'agit d'une erreur, veuillez contacter l'équipe."; $_SESSION['form_data_share_' . $slug] = $_POST; $_SESSION[$shareCsrfKey] = bin2hex(random_bytes(32)); // Regenerate token diff --git a/app/storage/logs/form-submissions.log b/app/storage/logs/form-submissions.log index 49b0402..a63417e 100644 --- a/app/storage/logs/form-submissions.log +++ b/app/storage/logs/form-submissions.log @@ -9,3 +9,8 @@ {"source":"partage","action":"submit","status":"success","thesis_id":23,"identifier":"2025-020","author":"Zoé Lambert","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T11:46:49+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"} {"source":"partage","action":"submit","status":"success","thesis_id":24,"identifier":"2025-021","author":"Emma Renard","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T11:49:49+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"} {"source":"partage","action":"submit","status":"success","thesis_id":25,"identifier":"2025-001","author":"Emma Renard","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T12:17:35+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"} +{"source":"admin","action":"submit","status":"success","thesis_id":37,"identifier":"2025-012","author":"Théo Marchand","timestamp":"2026-05-04T14:56:37+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"} +{"source":"admin","action":"submit","status":"duplicate","author":"Théo Marchand","existing_thesis_id":37,"existing_identifier":"2025-012","timestamp":"2026-05-04T14:56:53+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"} +{"source":"partage","action":"submit","status":"duplicate","author":"Théo Marchand","existing_thesis_id":37,"existing_identifier":"2025-012","share_slug":"20260429-DZESJT6X","timestamp":"2026-05-04T15:01:08+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"} +{"source":"partage","action":"submit","status":"duplicate","author":"Théo Marchand","existing_thesis_id":37,"existing_identifier":"2025-012","share_slug":"20260429-DZESJT6X","timestamp":"2026-05-04T15:05:04+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"} +{"source":"admin","action":"submit","status":"duplicate","author":"Théo Marchand","existing_thesis_id":37,"existing_identifier":"2025-012","timestamp":"2026-05-04T15:05:31+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"} diff --git a/app/templates/admin/footer.php b/app/templates/admin/footer.php index f6c5028..836ef56 100644 --- a/app/templates/admin/footer.php +++ b/app/templates/admin/footer.php @@ -14,5 +14,13 @@ +