diff --git a/TODO.md b/TODO.md index a76ca97..1534a3c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,9 @@ # Current tasks +## acces.php conflict marker cleanup +- [x] Remove 206k lines of nested jj conflict markers from acces.php (resolved from clean nzllwsxo base) +- [x] Restore missing features: create-result dialog, locked_year field, auto-generated password UI, file restrictions section, admin TOC wrapper + ## Save fixes (files disappearing on edit/terminer) - [x] Fix: note_intention deleted on save — handleFilePondSingleFile treats existing DB id as new upload, deletes existing, then can't re-process (integer vs hex mismatch) - [x] Fix: cover removal now uses trash, same hex-vs-integer guard as note_intention @@ -16,6 +20,8 @@ - [x] Frontend: modal with folder browser, triggered by a "Relier" button next to each FilePond pool - [x] JS: integrate relink button into FilePond UI (XamxamOpenFileBrowser + XamxamRelinkFile) - [x] CSS: .relink-modal + .file-browser styles in form.css +- [x] Fix: relinked file not appearing in FilePond pool — add file metadata to addFile() options and extensive diag logging +- [ ] Migration: rename existing theses/ directories to documents/ on disk and update DB paths ## Trash policy - [x] FilePond remove moves to tmp/_trash (already implemented in handleRemove) @@ -71,6 +77,12 @@ - [x] `partage/index.php`: fix fragment routing — `$slug` was `'fragments'` but check used `str_starts_with($slug, 'fragments/')`, causing HTMX fragments to redirect to / (main page) - [x] Deploy: `just deploy` + `just deploy-nginx` +## File browser fixes +- [x] Fix: top-folder navigation regex doesn't match bare `documents`/`theses` (requires trailing slash) +- [x] Replace emoji icons (📁📄) with proper SVG icons (folder, pdf, file-archive, text-file) +- [x] Fix relink endpoint: always return JSON (even on errors), guard finfo class, add diagnostic logging +- [x] Fix JS relink error handler to parse JSON error responses + ## Previous items - [x] Step 1 — Build 4 PHP endpoints (process.php, revert.php, load.php, remove.php) diff --git a/app/public/admin/actions/filepond/relink.php b/app/public/admin/actions/filepond/relink.php index a33cf22..b7bd67f 100644 --- a/app/public/admin/actions/filepond/relink.php +++ b/app/public/admin/actions/filepond/relink.php @@ -14,23 +14,29 @@ require_once __DIR__ . '/../../../../bootstrap.php'; require_once __DIR__ . '/../../../../src/AdminAuth.php'; AdminAuth::requireLogin(); +// Always return JSON, even on errors +function relinkError(int $code, string $message): never { + http_response_code($code); + header('Content-Type: application/json'); + echo json_encode(['ok' => false, 'error' => $message]); + exit; +} + // CSRF via header $csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''; +error_log('[relink] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | csrf=' . (isset($_SESSION['csrf_token']) ? 'set' : 'missing') . ' | header=' . (strlen($csrfHeader) > 0 ? substr($csrfHeader, 0, 8) . '...' : 'empty') . ' | body_len=' . strlen(file_get_contents('php://input'))); if (!isset($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $csrfHeader)) { - http_response_code(403); - die('Token CSRF invalide.'); + relinkError(403, 'Token CSRF invalide.'); } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { - http_response_code(405); - die('Méthode non autorisée.'); + relinkError(405, 'Méthode non autorisée.'); } $body = json_decode(file_get_contents('php://input'), true); if (!is_array($body)) { - http_response_code(400); - die('JSON invalide.'); + relinkError(400, 'JSON invalide.'); } $thesisId = filter_var($body['thesis_id'] ?? '', FILTER_VALIDATE_INT); @@ -42,14 +48,12 @@ $queueType = trim($body['queue_type'] ?? ''); $mimeType = trim($body['mime_type'] ?? 'application/octet-stream'); if (!$thesisId || $filePath === '') { - http_response_code(400); - die('Paramètres invalides (thesis_id + file_path requis).'); + relinkError(400, 'Paramètres invalides (thesis_id + file_path requis).'); } // Security: only allow paths under documents/ or theses/ if (!preg_match('#^(documents|theses)/#', $filePath)) { - http_response_code(403); - die('Chemin de fichier non autorisé.'); + relinkError(403, 'Chemin de fichier non autorisé.'); } $absPath = STORAGE_ROOT . '/' . $filePath; @@ -57,19 +61,21 @@ $realPath = realpath($absPath); $realStorage = realpath(STORAGE_ROOT); if ($realPath === false || !str_starts_with($realPath, $realStorage)) { - http_response_code(404); - die('Fichier introuvable ou chemin interdit.'); + error_log('[relink] FILE NOT FOUND | absPath=' . $absPath . ' | realPath=' . var_export($realPath, true) . ' | realStorage=' . $realStorage); + relinkError(404, 'Fichier introuvable ou chemin interdit.'); } if (!is_file($realPath)) { - http_response_code(404); - die('Le chemin ne pointe pas vers un fichier.'); + relinkError(404, 'Le chemin ne pointe pas vers un fichier.'); } // Detect MIME if not provided -if ($mimeType === 'application/octet-stream') { +if ($mimeType === 'application/octet-stream' && class_exists('finfo')) { $finfo = new finfo(FILEINFO_MIME_TYPE); - $mimeType = $finfo->file($realPath); + $detected = $finfo->file($realPath); + if ($detected !== false && $detected !== '') { + $mimeType = $detected; + } } // Map queue_type to file_type if not explicitly given @@ -91,8 +97,7 @@ $pdo = $db->getConnection(); $stmt = $pdo->prepare('SELECT id FROM thesis_files WHERE thesis_id = ? AND file_path = ?'); $stmt->execute([$thesisId, $filePath]); if ($stmt->fetch()) { - http_response_code(409); - die('Ce fichier est déjà lié à ce TFE.'); + relinkError(409, 'Ce fichier est déjà lié à ce TFE.'); } $db->insertThesisFile( diff --git a/app/public/admin/fragments/file-browser.php b/app/public/admin/fragments/file-browser.php index be6d003..2570795 100644 --- a/app/public/admin/fragments/file-browser.php +++ b/app/public/admin/fragments/file-browser.php @@ -13,9 +13,11 @@ AdminAuth::requireLogin(); $storageRoot = STORAGE_ROOT; +error_log('[file-browser] ENTRY | dir=' . ($_GET['dir'] ?? '(root)') . ' | storageRoot=' . $storageRoot); + // Determine which directory to browse $relDir = trim($_GET['dir'] ?? '', '/'); -if ($relDir !== '' && !preg_match('#^(documents|theses)/#', $relDir)) { +if ($relDir !== '' && !preg_match('#^(documents|theses)(/|$)#', $relDir)) { $relDir = ''; } @@ -83,6 +85,24 @@ if ($relDir !== '') { } $rootDirs = ['documents', 'theses']; + +// SVG icon for a given extension +function fileIcon(string $ext): string { + $ext = strtolower($ext); + if ($ext === 'pdf') { + return ''; + } + if (in_array($ext, ['zip', 'tar', 'gz', 'bz2', 'xz', '7z', 'rar'], true)) { + return ''; + } + // Default text-file icon for all other extensions + return ''; +} + +// SVG folder icon (same for all directories) +function folderIcon(): string { + return ''; +} ?>
@@ -94,7 +114,7 @@ $rootDirs = ['documents', 'theses'];
  • - 📁 + /
  • @@ -104,7 +124,7 @@ $rootDirs = ['documents', 'theses'];
    - Accès + Mot de passe +
    + + + Le mot de passe est généré automatiquement et ne peut pas être modifié. +
    - Laissez vide pour qu'il n'expire jamais.
    -

    - Un mot de passe sera généré automatiquement. -

    -
    Mot de passe
    - + style="flex:1;font-family:monospace;font-size:var(--step--1);padding:var(--space-xs);border:1px solid var(--border-color);border-radius:3px;background:var(--bg-secondary);"> +
    + Un mot de passe sera généré automatiquement.
    - -

    - Communiquez ce lien et ce mot de passe à l'étudiant·e. Le mot de passe ne sera plus affiché ensuite. -

    - @@ -206159,9 +465,18 @@
    + style="width:100%;padding:var(--space-2xs);border:1px solid var(--border-color);border-radius:3px;background:var(--bg-secondary);"> Le mot de passe est généré automatiquement et ne peut pas être modifié.
    +
    + Cadre académique +
    + + + Si renseignée, le formulaire étudiant masquera le champ Année et utilisera cette valeur. +
    +
    @@ -206175,8 +490,6 @@ - -
    @@ -206223,28 +536,47 @@