From 4b37a05be39cd0956e0ebfd39cf1196b6aba4dcd Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Thu, 11 Jun 2026 10:32:38 +0200 Subject: [PATCH] Guard no-JS file uploads: disabled filepond_mode by default, server-side fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The partage/admin form had a hardcoded filepond_mode=1 hidden input, so without JavaScript the server always entered the FilePond async path — which found no hex IDs and silently dropped all files. Three-layer fix: 1. HTML: filepond_mode input starts disabled with value=0; JS enables it and sets value=1 on DOMContentLoaded (and after HTMX swaps). Disabled inputs aren't submitted → server gets no filepond_mode → naturally falls to legacy path. 2. JS: enableFilepondMode() called on page load and hx:afterSwap so FilePond-enhanced forms always send filepond_mode=1. 3. Server (defense-in-depth): ThesisFileHandler::hasFilePondQueueData() scans POST['queue_file'] for 32-char hex IDs; ThesisCreateController and ThesisEditController use it alongside filepond_mode, so even if the flag somehow arrives without async upload IDs, the path takes over. --- TODO.md | 4 +-- .../assets/js/app/file-upload-filepond.js | 15 +++++++++++ .../Controllers/ThesisCreateController.php | 8 +++++- app/src/Controllers/ThesisEditController.php | 10 ++++--- app/src/Controllers/ThesisFileHandler.php | 27 +++++++++++++++++++ app/templates/partials/form/form.php | 4 ++- 6 files changed, 61 insertions(+), 7 deletions(-) diff --git a/TODO.md b/TODO.md index 75157be..551ea9a 100644 --- a/TODO.md +++ b/TODO.md @@ -52,8 +52,8 @@ Reference: Assessment against progressive-enhancement / WCAG-AA / "never lose da **Current state:** `form.php` hardcodes ``. Without JS, no `queue_file[]` hidden inputs are populated → server gets `filepond_mode=1` with empty queue → all files silently dropped. The form is supposed to work without JS. **To do:** -- [ ] Change the hidden input to `` by default; JS enables it and sets `value="1"` on DOMContentLoaded -- [ ] Add server-side fallback in `ThesisCreateController::submit()` and `ThesisEditController::save()`: when `filepond_mode=1` but no `queue_file` data is present, fall through to the legacy `$_FILES` path +- [x] Change the hidden input to `` by default; JS enables it and sets `value="1"` on DOMContentLoaded +- [x] Add server-side fallback in `ThesisCreateController::submit()` and `ThesisEditController::save()`: when `filepond_mode=1` but no `queue_file` data is present, fall through to the legacy `$_FILES` path - [ ] Test end-to-end: submit the partage form with JS disabled, verify files arrive via `$_FILES` ### 3. Autosave text fields on partage form diff --git a/app/public/assets/js/app/file-upload-filepond.js b/app/public/assets/js/app/file-upload-filepond.js index af3e82d..c58a358 100644 --- a/app/public/assets/js/app/file-upload-filepond.js +++ b/app/public/assets/js/app/file-upload-filepond.js @@ -615,11 +615,24 @@ console.log('[filepond] htmx detected, registering swap listeners'); window.htmx.on("htmx:beforeSwap", onHtmxBeforeSwap); window.htmx.on("htmx:afterSwap", () => { + enableFilepondMode(); _xamxamFilepondReady = false; window.XamxamInitFilePonds(); setTimeout(() => { _xamxamFilepondReady = true; }, 0); }); } + // ── Enable filepond_mode hidden input (no-JS safety) ──────────────── + // The hidden input starts as disabled / value=0 so the server falls + // back to $_FILES when JS is unavailable. Enable it now that FilePond + // will handle uploads asynchronously. + function enableFilepondMode() { + var inputs = document.querySelectorAll("input[name='filepond_mode']"); + for (var i = 0; i < inputs.length; i++) { + inputs[i].disabled = false; + inputs[i].value = "1"; + } + } + // Flag set after FilePond instances are fully initialised. // Before this flag is set, FilePond:addfile events are from initial load // (e.g. existing files loaded in edit mode) and should not mark the form dirty. @@ -629,10 +642,12 @@ if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => { + enableFilepondMode(); window.XamxamInitFilePonds(); _xamxamFilepondReady = true; }); } else { + enableFilepondMode(); window.XamxamInitFilePonds(); _xamxamFilepondReady = true; } diff --git a/app/src/Controllers/ThesisCreateController.php b/app/src/Controllers/ThesisCreateController.php index f1138ab..56bf803 100644 --- a/app/src/Controllers/ThesisCreateController.php +++ b/app/src/Controllers/ThesisCreateController.php @@ -198,7 +198,13 @@ class ThesisCreateController $folderPath = $objet . '/' . $data['annee'] . '/' . $folderName . '/'; $filePrefix = $folderName; - if (!empty($post['filepond_mode'])) { + // Determine upload path: FilePond async (JS enabled, hex IDs present) + // vs. legacy multipart (no JS, or JS failed). The hidden filepond_mode + // input starts disabled (value=0, not submitted); JS enables it on load. + // Defense-in-depth: if filepond_mode=1 but no hex IDs, fall back to $_FILES. + $useFilePond = !empty($post['filepond_mode']) && $this->hasFilePondQueueData($post); + + if ($useFilePond) { // New path: files already on server via async FilePond uploads // Cover and note_intention also go through FilePond async flow $this->handleFilePondSingleFile($thesisId, $post, 'cover', $folderPath, $filePrefix); diff --git a/app/src/Controllers/ThesisEditController.php b/app/src/Controllers/ThesisEditController.php index 13e617e..b38121d 100644 --- a/app/src/Controllers/ThesisEditController.php +++ b/app/src/Controllers/ThesisEditController.php @@ -359,8 +359,12 @@ class ThesisEditController mkdir($dirAbs, 0755, true); } + // Determine upload path: FilePond async (JS enabled, hex IDs present) + // vs. legacy multipart. Defense-in-depth fallback for no-JS scenarios. + $useFilePond = !empty($post['filepond_mode']) && $this->hasFilePondQueueData($post); + // ── Cover image (outside transaction — filesystem op) ───────────────── - if (!empty($post['filepond_mode'])) { + if ($useFilePond) { // Delete old cover only if a genuinely new cover was uploaded (hex file_id). // Existing cover preserved in FilePond sends its DB integer ID — skip. $coverIdRaw = ($post['queue_file']['cover'] ?? null); @@ -387,7 +391,7 @@ class ThesisEditController } // ── Note d'intention (replace if uploaded) ──────────────────────────── - if (!empty($post['filepond_mode'])) { + if ($useFilePond) { // Only delete + replace if a genuinely new file was uploaded (hex file_id). // Existing files preserved in the FilePond pool send their DB integer ID; // we must NOT delete them — they're already stored. @@ -454,7 +458,7 @@ class ThesisEditController } } - if (!empty($post['filepond_mode'])) { + if ($useFilePond) { // New path: files already on server via async FilePond uploads $nextNum = $tfeCount + 1; $nextNum = $this->handleFilePondQueueFiles($thesisId, $post, 'tfe', $folderPath, $filePrefix, $nextNum); diff --git a/app/src/Controllers/ThesisFileHandler.php b/app/src/Controllers/ThesisFileHandler.php index 8cc6851..e137af9 100644 --- a/app/src/Controllers/ThesisFileHandler.php +++ b/app/src/Controllers/ThesisFileHandler.php @@ -853,6 +853,33 @@ trait ThesisFileHandler // ── FilePond async file processing ────────────────────────────────────── + /** + * Check whether the POST data contains actual FilePond hex IDs (32-char hex) + * rather than standard file upload data. + * + * Without JS the hidden filepond_mode input is disabled and not submitted; + * this is a defense-in-depth fallback: if filepond_mode=1 somehow arrives but + * no async upload IDs are present, we treat it as a legacy $_FILES submission. + */ + private function hasFilePondQueueData(array $post): bool + { + $queueKeys = ['cover', 'note_intention', 'tfe', 'annexe']; + foreach ($queueKeys as $key) { + $raw = $post['queue_file'][$key] ?? null; + if ($raw === null || $raw === '') { + continue; + } + $ids = is_array($raw) ? $raw : [$raw]; + foreach ($ids as $id) { + $id = is_string($id) ? trim($id) : ''; + if ($id !== '' && preg_match('/^[a-f0-9]{32}$/', $id)) { + return true; + } + } + } + return false; + } + /** * Process a single file from the FilePond async flow (cover, note_intention). * diff --git a/app/templates/partials/form/form.php b/app/templates/partials/form/form.php index 6e266a0..5452de5 100644 --- a/app/templates/partials/form/form.php +++ b/app/templates/partials/form/form.php @@ -147,7 +147,9 @@ $errorFieldName = $errorFieldName ?? null;
- + +