mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 08:09:18 +02:00
Guard no-JS file uploads: disabled filepond_mode by default, server-side fallback
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.
This commit is contained in:
4
TODO.md
4
TODO.md
@@ -52,8 +52,8 @@ Reference: Assessment against progressive-enhancement / WCAG-AA / "never lose da
|
||||
**Current state:** `form.php` hardcodes `<input type="hidden" name="filepond_mode" value="1">`. 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 `<input type="hidden" name="filepond_mode" value="0" disabled>` 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 `<input type="hidden" name="filepond_mode" value="0" disabled>` 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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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).
|
||||
*
|
||||
|
||||
@@ -147,7 +147,9 @@ $errorFieldName = $errorFieldName ?? null;
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="<?= $formAction ?>" method="post" enctype="multipart/form-data" class="admin-form" data-beforeunload-guard>
|
||||
<input type="hidden" name="filepond_mode" value="1">
|
||||
<!-- Default: JS-disabled mode (disabled → not submitted → server uses $_FILES path).
|
||||
On DOMContentLoaded, JS enables this input and sets value="1" → server uses FilePond path. -->
|
||||
<input type="hidden" name="filepond_mode" value="0" disabled>
|
||||
<?= $hiddenFields ?>
|
||||
|
||||
<?php if (!$adminMode): ?>
|
||||
|
||||
Reference in New Issue
Block a user