diff --git a/TODO.md b/TODO.md index b05d0b9..a60557c 100644 --- a/TODO.md +++ b/TODO.md @@ -13,3 +13,11 @@ Reference: `docs/autosave-system.md` → "HTMX v2 Migration Plan" section. - [x] Fix backend `$isAjax` detection: also recognize `HX-Request` header (page.php, apropos.php, form-help.php) - [x] Form-help inline editors: add OverType toolbar + HTMX auto-save + remove save buttons - [x] Markdown cheatsheet modal: reusable dialog on all OverType editors + +## FilePond error fixes + +- [x] Fix `server.process.onerror`: don't access `response.status` on string (response is XHR text body) +- [x] Fix `server.load`: convert from string URL to object with proper `onload`/`onerror` handlers +- [x] Fix `server.process.onload`: guard against non-ID responses (e.g. HTML error pages disguised as 200) +- [x] Fix `destroyFilePondsIn`: abort in-flight uploads before destroying to prevent stale XHR callbacks crashing `_write` +- [x] Fix `FilepondHandler.php`: set `Content-Type: text/plain` header at top of `handleProcess`, `handleLoad`, `handleRevert`, `handleRemove` so PHP doesn't default to `text/html` on `die()` diff --git a/app/public/assets/js/app/file-upload-filepond.js b/app/public/assets/js/app/file-upload-filepond.js index 8804174..a5e6726 100644 --- a/app/public/assets/js/app/file-upload-filepond.js +++ b/app/public/assets/js/app/file-upload-filepond.js @@ -252,17 +252,21 @@ }, onload: (response) => { var id = response.trim(); + // Guard: if the server returned an error message disguised as 200, + // treat it as a processing error so FilePond doesn't treat it as a serverId. + if (id.length > 64 || /[<>\n\r]/.test(id)) { + console.error("[filepond] process onload | unexpected response | body=" + id.substring(0, 200)); + throw new Error("Réponse serveur inattendue."); + } console.log(`[filepond] process onload | serverId=${id}`); return id; // file_id stored as serverId }, onerror: (response) => { - console.error( - "[filepond] process onerror | status=" + - response.status + - " | body=" + - response, - ); - return response; + // response is the raw XHR response text (string), not an XHR object. + // Log it and return a human-readable error message. + var body = typeof response === 'string' ? response : (response && response.body ? response.body : String(response || '')); + console.error("[filepond] process onerror | body=" + body); + return body || "Erreur lors du téléversement."; }, }, @@ -274,11 +278,25 @@ console.log("[filepond] revert OK"); }, onerror: (r) => { - console.error(`[filepond] revert ERROR | body=${r}`); + var body = typeof r === 'string' ? r : (r && r.body ? r.body : ''); + console.error(`[filepond] revert ERROR | body=${body || r}`); }, }, - load: `${base}/load.php?id=`, + load: { + url: `${base}/load.php?id=`, + method: "GET", + onload: (response) => { + // response is the blob from the server; pass through unchanged + return response; + }, + onerror: (response) => { + var body = typeof response === 'string' ? response : (response && response.body ? response.body : String(response || '')); + console.error("[filepond] load onerror | body=" + body); + // Return a descriptive error — FilePond will fire an error event. + return body || "Fichier introuvable."; + }, + }, // FilePond appends the source value (db_id) automatically remove: (source, load, error) => { @@ -500,6 +518,19 @@ } } } + // Abort any in-flight uploads before destroying to prevent + // FilePond internal crashes when XHR callbacks fire on a + // torn-down instance ("can't access property main"). + var files = pond.getFiles(); + for (var i = 0; i < files.length; i++) { + var f = files[i]; + if (f.status === 4 || f.status === 2 || f.status === 3) { + // FileStatus: PROCESSING=4, PROCESSING_QUEUED=2, PROCESSING=4 + // (FilePond 4.x internal: 4 = processing) + // Abort processing to avoid stale XHR callbacks + try { pond.removeFile(f); } catch (_abort) {} + } + } pond.destroy(); } catch (_) {} } diff --git a/app/src/FilepondHandler.php b/app/src/FilepondHandler.php index ab06f6a..a185d04 100644 --- a/app/src/FilepondHandler.php +++ b/app/src/FilepondHandler.php @@ -81,6 +81,10 @@ class FilepondHandler public function handleProcess(): never { + // All responses from this endpoint must be text/plain + // (PHP defaults to text/html, which confuses FilePond on error). + header('Content-Type: text/plain; charset=utf-8'); + error_log($this->logPrefix . ':process ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | files_keys=' . implode(',', array_keys($_FILES)) . ' | post_keys=' . implode(',', array_keys($_POST))); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { @@ -197,7 +201,6 @@ class FilepondHandler file_put_contents($tmpDir . '/manifest.json', json_encode($manifest, JSON_UNESCAPED_SLASHES)); error_log($this->logPrefix . ':process SUCCESS | file_id=' . $fileId . ' | queue_type=' . $queueType . ' | name=' . $originalName); - header('Content-Type: text/plain; charset=utf-8'); echo $fileId; exit; } @@ -209,6 +212,7 @@ class FilepondHandler public function handleLoad(): never { if ($_SERVER['REQUEST_METHOD'] !== 'GET') { + header('Content-Type: text/plain; charset=utf-8'); http_response_code(405); die('Méthode non autorisée.'); } @@ -224,6 +228,7 @@ class FilepondHandler // Numeric IDs → DB files $dbId = filter_var($fileId, FILTER_VALIDATE_INT); if ($dbId === false || $dbId <= 0) { + header('Content-Type: text/plain; charset=utf-8'); http_response_code(400); die('ID invalide.'); } @@ -237,6 +242,7 @@ class FilepondHandler if (!$fileRow) { error_log($this->logPrefix . ':load DB NOT FOUND | db_id=' . $dbId); + header('Content-Type: text/plain; charset=utf-8'); http_response_code(404); die('Fichier introuvable.'); } @@ -259,6 +265,7 @@ class FilepondHandler exit; } if (str_starts_with($filePath, 'http://') || str_starts_with($filePath, 'https://')) { + header('Content-Type: text/plain; charset=utf-8'); http_response_code(404); die('URL — pas de flux direct.'); } @@ -266,6 +273,7 @@ class FilepondHandler $absPath = STORAGE_ROOT . '/' . $filePath; if (!file_exists($absPath) || !is_readable($absPath)) { error_log($this->logPrefix . ':load DISK MISSING | db_id=' . $dbId . ' | absPath=' . $absPath); + header('Content-Type: text/plain; charset=utf-8'); http_response_code(404); die('Fichier absent du disque.'); } @@ -286,6 +294,8 @@ class FilepondHandler public function handleRemove(): never { + header('Content-Type: text/plain; charset=utf-8'); + if ($_SERVER['REQUEST_METHOD'] !== 'DELETE') { http_response_code(405); die('Méthode non autorisée.'); @@ -339,6 +349,8 @@ class FilepondHandler public function handleRevert(): never { + header('Content-Type: text/plain; charset=utf-8'); + if ($_SERVER['REQUEST_METHOD'] !== 'DELETE') { http_response_code(405); die('Méthode non autorisée.'); @@ -480,12 +492,14 @@ class FilepondHandler $manifestPath = $tmpDir . '/manifest.json'; if (!is_dir($tmpDir) || !file_exists($manifestPath)) { + header('Content-Type: text/plain; charset=utf-8'); http_response_code(404); die('Fichier temporaire introuvable.'); } $manifest = json_decode(file_get_contents($manifestPath), true); if (!is_array($manifest) || ($manifest['session_id'] ?? '') !== session_id()) { + header('Content-Type: text/plain; charset=utf-8'); http_response_code(403); die('Session invalide.'); } @@ -502,6 +516,7 @@ class FilepondHandler closedir($dh); if ($actualFile === null || !file_exists($actualFile)) { + header('Content-Type: text/plain; charset=utf-8'); http_response_code(404); die('Fichier temporaire introuvable.'); } @@ -569,6 +584,7 @@ class FilepondHandler private function validateMimeExt(string $queueType, string $mimeType, string $ext): void { + // Content-Type already set by handleProcess() header $allowedMimes = self::QUEUE_MIME_MAP[$queueType] ?? null; if ($allowedMimes !== null) { if (!in_array($mimeType, $allowedMimes, true)) { diff --git a/app/storage/logs/admin-2026-06-09.log b/app/storage/logs/admin-2026-06-09.log index c17c843..cd5147a 100644 --- a/app/storage/logs/admin-2026-06-09.log +++ b/app/storage/logs/admin-2026-06-09.log @@ -26,3 +26,6 @@ {"timestamp":"2026-06-09T19:01:33+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"page","action":"edit","status":"success","context":{"slug":"licenses"}} {"timestamp":"2026-06-09T19:12:39+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"form_structure","action":"edit","status":"success","context":{"section":"fieldset_access"}} {"timestamp":"2026-06-09T19:12:43+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"form_structure","action":"edit","status":"success","context":{"section":"fieldset_access"}} +{"timestamp":"2026-06-09T19:26:57+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"page","action":"edit","status":"success","context":{"slug":"about"}} +{"timestamp":"2026-06-09T19:27:00+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"page","action":"edit","status":"success","context":{"slug":"about"}} +{"timestamp":"2026-06-09T19:33:27+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"share_link","action":"create","status":"success","context":{"slug":"20260609-IHRZDYKJ","has_password":true,"expires_at":null,"objet_restriction":"tfe"}}