diff --git a/TODO.md b/TODO.md index 016ec22..1982f21 100644 --- a/TODO.md +++ b/TODO.md @@ -1,41 +1,16 @@ # TODO -- [x] Fix language-search-fragment: use searchLanguages() like tag fragment, remove broken predefined exclusion logic -- [x] Both fragments now follow identical patterns -- [x] Fix "Créer" button not appearing on language search: both language and tag inputs used name="q" in the same form, causing HTMX to submit the wrong (empty) value — renamed to unique names (language_search_q / tag_search_q) -- [x] Exclude Français, Anglais, Néerlandais from language-search suggestions (handled by the checkbox list) -- [x] Refactor file upload naming convention - - [x] Create shared ThesisFileHandler trait (src/Controllers/ThesisFileHandler.php) - - [x] New pattern: theses/{YYYY}/{YYYY}_{AUTHORS}_{TITLE_SLUG}/ - - [x] COUVERTURE: single cover image in thesis folder (covers/ directory deprecated) - - [x] NOTE_INTENTION: single PDF in thesis folder - - [x] TFE_{XX}: main files, contiguous numbering 01+, hierarchy PDF > video > audio > subtitles > images > other - - [x] Subtitles (VTT) placed immediately after their associated video in TFE sequence - - [x] ANNEXE_{XX}: annex files, separate numbering 01+ - - [x] Two-digit zero-padded numbering (sprintf('%02d', ...)) - - [x] Update ThesisCreateController.php: use trait, new file handling - - [x] Update ThesisEditController.php: use trait, new file handling - - [x] Remove duplicate methods (generateAuthorSlug, sanitizeFilename, etc.) from both controllers - - [x] Update Database.php: deprecate handleCoverUpload, remove banner_path from queries - - [x] Update SystemController.php: remove banners/ stats - - [x] Update schema.sql: remove banner_path column and view field - - [x] Create migration 027_drop_banner_path.sql - - [x] Update PureLogicTest.php: adapt detectFileType call signature - - [x] All pure logic tests pass -- [x] Fix license validation: only require license for non-admin when access_type_id=1 (Libre), not for Interne (2) or Interdit (3) — fixes share link submissions failing with "Veuillez sélectionner une licence" -- [x] Add xamxam@erg.be mailto link at top of student (partage) form -- [x] On validation error, append "envoyez un e-mail à xamxam@erg.be" to flash error message -- [x] Preserve uploaded file names across validation redirects: store in session, display as warning on re-render so the student knows which files to re-select -- [x] Obfuscate all email addresses and mailto: links as HTML decimal entities site-wide (EmailObfuscator class, applied in templates + Parsedown post-processing) -- [x] Fix TFE and annexes files not saved in ThesisCreateController::submit(): call handleAnnexeFiles, fix file input name mapping -- [x] Apply ALLOWED_MIME_TYPES/ALLOWED_EXTENSIONS validation in handleAnnexeFiles (same as handleTfeFiles) -- [x] Fix handleAnnexeFiles to use correct $_FILES key ('annexes' not 'files') -- [x] Add annexe handling in ThesisEditController::save() -- [x] Relax 3-keyword minimum: admin mode (create) requires 1+, edit requires 1+, student (partage) requires 3 -- [x] Add CSS for file preview items (.fp-item, .fp-thumb, .fp-icon, .fp-meta, .fp-name, .fp-size) so annexes/cover/note-intention previews wrap and display correctly -- [x] Fix TFE file input accept attribute to include video/audio/archive extensions -- [x] Make annexes file input required when "Ce TFE comporte des annexes" is checked -- [x] Add PHP-side validation: if has_annexes checked but no annexe files provided, throw error -- [x] Add HTMX inline file validation: MIME type + file size checked on change via validate-file-fragment endpoint -- [x] Create shared validation logic (validate-file-fragment-shared.php) used by both admin and partage -- [x] Add CSS for .file-validation-msg, .fv-ok, .fv-error inline validation messages +- [x] Simplify file-upload-queue.js — drop Sortable, keep only single-file previews +- [x] Create session-based upload flow (upload-tfe-file.php, remove-tfe-file.php, tfe-queue-helper.php) +- [x] Create admin wrappers for upload/remove endpoints +- [x] Register new routes in partage/index.php +- [x] Update fichiers-fragment.php — HTMX-powered file input + server-rendered queue + progress bar +- [x] Update ThesisCreateController — read TFE files from session temp +- [x] Update ThesisEditController — read TFE files from session temp +- [x] Add handleTfeFilesFromSession + writeTfeFileFromSrc + cleanupSessionUploads to ThesisFileHandler trait +- [x] Remove sortable.min.js script tags from add.php, edit.php, index.php +- [x] Clean up form.php — remove drag handles, sortable hints +- [x] Clean up fieldset-files.php — remove sortable references +- [x] Clean up CSS — remove .fq-drag-handle, .fq-ghost, .sortable-ghost +- [x] Fix closure syntax (use before return type) in tfe-queue-helper.php +- [x] Commit diff --git a/app/public/admin/add.php b/app/public/admin/add.php index 96232bb..2584de0 100644 --- a/app/public/admin/add.php +++ b/app/public/admin/add.php @@ -55,7 +55,7 @@ function wasSelected($key, $value) { $isAdmin = true; $bodyClass = 'admin-body'; $extraCss = ['/assets/css/form.css']; -$extraJs = ['/assets/js/sortable.min.js', '/assets/js/file-upload-queue.js', '/assets/js/beforeunload-guard.js']; +$extraJs = ['/assets/js/file-upload-queue.js', '/assets/js/beforeunload-guard.js']; require_once APP_ROOT . '/templates/head.php'; include APP_ROOT . '/templates/header.php'; include APP_ROOT . '/templates/admin/add.php'; diff --git a/app/public/admin/edit.php b/app/public/admin/edit.php index 9326cbf..10e88b5 100644 --- a/app/public/admin/edit.php +++ b/app/public/admin/edit.php @@ -40,7 +40,7 @@ try { $isAdmin = true; $bodyClass = 'admin-body'; $extraCss = ['/assets/css/form.css']; -$extraJs = ['/assets/js/sortable.min.js', '/assets/js/file-upload-queue.js', '/assets/js/beforeunload-guard.js']; +$extraJs = ['/assets/js/file-upload-queue.js', '/assets/js/beforeunload-guard.js']; require_once APP_ROOT . '/templates/head.php'; include APP_ROOT . '/templates/header.php'; include APP_ROOT . '/templates/admin/edit.php'; diff --git a/app/public/admin/remove-tfe-file.php b/app/public/admin/remove-tfe-file.php new file mode 100644 index 0000000..e34a813 --- /dev/null +++ b/app/public/admin/remove-tfe-file.php @@ -0,0 +1,12 @@ +\u2820' + - '" + - '' + - esc(file.name) + - "" + - '' + - humanSize(file.size) + - "" + - ''; - li.querySelector(".fq-remove").onclick = (function (i) { - return function () { - fileArray.splice(i, 1); - renderQueue(); - }; - })(idx); - queue.appendChild(li); - }); - injectHiddenFields(Array.from(queue.querySelectorAll(".fq-item"))); - } - - function injectHiddenFields(items) { - var form = picker.closest("form"); - if (!form) return; - form - .querySelectorAll(".fq-hidden-label, .fq-hidden-order") - .forEach(function (el) { - el.remove(); - }); - items.forEach(function (li, sortedIdx) { - var label = li.querySelector(".fq-label"); - var lInp = document.createElement("input"); - lInp.type = "hidden"; - lInp.name = "file_labels[]"; - lInp.value = label ? label.value : ""; - lInp.className = "fq-hidden-label"; - form.appendChild(lInp); - var oInp = document.createElement("input"); - oInp.type = "hidden"; - oInp.name = "file_orders[]"; - oInp.value = sortedIdx + 1; - oInp.className = "fq-hidden-order"; - form.appendChild(oInp); - }); - } - - // On submit, refresh hidden fields from current queue state - var form = picker.closest("form"); - if (form) - form.addEventListener("submit", function () { - injectHiddenFields(Array.from(queue.querySelectorAll(".fq-item"))); - }); - } - - // ── 2. Single-file previews (data-preview attribute) ──────────────────── + // ── Single-file previews (data-preview attribute) ──────────────────── document .querySelectorAll('input[type="file"][data-preview]') .forEach(function (input) { @@ -221,32 +101,6 @@ window.XamxamInitFileUploads = function () { }); }; }); - - // ── 3. Existing-files sortable (edit mode) ────────────────────────────── - var sortList = document.getElementById("existing-files-sortable"); - if (sortList && typeof Sortable !== "undefined") { - Sortable.create(sortList, { - animation: 150, - handle: ".admin-file-drag-handle", - ghostClass: "fq-ghost", - onEnd: function () { - sortList - .querySelectorAll('input[name="file_sort_order[]"]') - .forEach(function (el) { - el.remove(); - }); - sortList - .querySelectorAll(".admin-file-list-item[data-file-id]") - .forEach(function (li) { - var inp = document.createElement("input"); - inp.type = "hidden"; - inp.name = "file_sort_order[]"; - inp.value = li.getAttribute("data-file-id"); - li.prepend(inp); - }); - }, - }); - } }; // Bootstrap on page load diff --git a/app/public/partage/fichiers-fragment.php b/app/public/partage/fichiers-fragment.php index 90b3f5d..eac5247 100644 --- a/app/public/partage/fichiers-fragment.php +++ b/app/public/partage/fichiers-fragment.php @@ -160,18 +160,47 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
Aucun fichier sélectionné.
- Glissez-déposez les fichiers pour déterminer l'ordre d'affichage sur la page du TFE. +Index invalide.
'; + exit; +} + +// ── Delete temp file ─────────────────────────────────────────────────────── +$entry = $uploads[$index]; +$absPath = STORAGE_ROOT . '/' . $entry['tmp_path']; +if (file_exists($absPath)) { + unlink($absPath); +} + +// ── Remove from session ──────────────────────────────────────────────────── +array_splice($_SESSION['tfe_uploads'], $index, 1); + +// ── Clean up empty temp directory ────────────────────────────────────────── +$sessionId = session_id(); +$tempDir = STORAGE_ROOT . '/uploads/' . $sessionId; +$_SESSION['tfe_uploads'] = array_values($_SESSION['tfe_uploads']); + +if (empty($_SESSION['tfe_uploads']) && is_dir($tempDir)) { + // Remove dir only if empty (rmdir fails if not empty, which is fine) + @rmdir($tempDir); +} + +// ── Render updated queue ─────────────────────────────────────────────────── +require_once __DIR__ . '/tfe-queue-helper.php'; +renderQueueFragment($_SESSION['tfe_uploads'], '/partage/remove-tfe-file'); diff --git a/app/public/partage/tfe-queue-helper.php b/app/public/partage/tfe-queue-helper.php new file mode 100644 index 0000000..ce5b332 --- /dev/null +++ b/app/public/partage/tfe-queue-helper.php @@ -0,0 +1,78 @@ + "\u{1F4C4}", + 'video' => "\u{1F3AC}", + 'audio' => "\u{1F50A}", + 'zip' => "\u{1F5DC}\u{FE0F}", + 'vtt' => "\u{1F4AC}", + 'image' => "\u{1F5BC}\u{FE0F}", + 'other' => "\u{1F4CE}", + ]; + + $iconFor = function (string $name, string $mime) use ($ICON): string { + if (str_starts_with($mime, 'image/')) return $ICON['image']; + if ($mime === 'application/pdf' || str_ends_with(strtolower($name), '.pdf')) return $ICON['pdf']; + if (str_starts_with($mime, 'video/') || preg_match('/\.(mp4|webm|mov|ogv)$/i', $name)) return $ICON['video']; + if (str_starts_with($mime, 'audio/') || preg_match('/\.(mp3|ogg|oga|wav|flac|aac|m4a)$/i', $name)) return $ICON['audio']; + if (preg_match('/\.(zip|tar|gz|tgz)$/i', $name)) return $ICON['zip']; + if (preg_match('/\.vtt$/i', $name)) return $ICON['vtt']; + return $ICON['other']; + }; + + $humanSize = function (int $b): string { + return $b >= 1073741824 + ? number_format($b / 1073741824, 2) . ' GB' + : ($b >= 1048576 + ? number_format($b / 1048576, 2) . ' MB' + : ($b >= 1024 + ? number_format($b / 1024, 1) . ' KB' + : $b . ' B')); + }; + + if (empty($uploads)) { + echo '' + . 'Aucun fichier sélectionné.
'; + return; + } + + echo 'Erreur lors du téléchargement.
'; + exit; +} + +// ── MIME + size validation ───────────────────────────────────────────────── +$adminMode = ($_POST['admin_mode'] ?? '0') === '1'; +$finfo = new \finfo(FILEINFO_MIME_TYPE); +$mimeType = $finfo->file($upload['tmp_name']); +$ext = strtolower(pathinfo($upload['name'], PATHINFO_EXTENSION)); + +if ($mimeType === 'text/plain' && $ext === 'vtt') { + $mimeType = 'text/vtt'; +} + +$allowedMimes = [ + 'application/pdf', + 'image/jpeg', 'image/png', 'image/gif', 'image/webp', + 'video/mp4', 'video/webm', 'video/ogg', 'video/quicktime', + 'audio/mpeg', 'audio/mp3', 'audio/ogg', 'audio/wav', + 'audio/flac', 'audio/aac', 'audio/x-m4a', 'audio/mp4', + 'text/vtt', + 'application/zip', 'application/x-zip-compressed', + 'application/x-tar', 'application/gzip', + 'application/octet-stream', +]; +$allowedExts = [ + 'jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', + 'mp4', 'webm', 'ogv', 'mov', + 'mp3', 'ogg', 'oga', 'wav', 'flac', 'aac', 'm4a', + 'vtt', 'zip', 'tar', 'gz', 'tgz', +]; + +$mimeOk = in_array($mimeType, $allowedMimes, true) + || ($mimeType === 'application/octet-stream' && in_array($ext, $allowedExts, true)) + || in_array($ext, $allowedExts, true); + +if (!$mimeOk && !$adminMode) { + http_response_code(400); + echo 'Type de fichier non accepté : ' . htmlspecialchars($upload['name']) . '
'; + exit; +} + +$maxSize = ($mimeType === 'application/pdf' || $ext === 'pdf') ? 100 * 1024 * 1024 : 500 * 1024 * 1024; +if ($upload['size'] > $maxSize && !$adminMode) { + $maxMb = round($maxSize / 1024 / 1024); + http_response_code(400); + echo 'Fichier trop volumineux (' . round($upload['size'] / 1024 / 1024, 1) . ' MB). Maximum : ' . $maxMb . ' MB.
'; + exit; +} + +// ── Session temp directory ───────────────────────────────────────────────── +$sessionId = session_id(); +$tempDir = STORAGE_ROOT . '/uploads/' . $sessionId; +if (!is_dir($tempDir)) { + mkdir($tempDir, 0755, true); +} + +// ── Move uploaded file to temp ───────────────────────────────────────────── +$origName = basename($upload['name']); +$ext = strtolower(pathinfo($origName, PATHINFO_EXTENSION)); + +// Generate a unique temp name to avoid collisions +$uniqueId = bin2hex(random_bytes(8)); +$tmpName = 'tmp_' . $uniqueId . ($ext ? '.' . $ext : ''); +$tmpPath = $tempDir . '/' . $tmpName; + +if (!move_uploaded_file($upload['tmp_name'], $tmpPath)) { + http_response_code(500); + echo 'Erreur lors de la sauvegarde du fichier.
'; + exit; +} + +chmod($tmpPath, 0644); + +// Determine MIME type +$finfo = new \finfo(FILEINFO_MIME_TYPE); +$mimeType = $finfo->file($tmpPath); + +// ── Store in session ─────────────────────────────────────────────────────── +if (!isset($_SESSION['tfe_uploads']) || !is_array($_SESSION['tfe_uploads'])) { + $_SESSION['tfe_uploads'] = []; +} + +$_SESSION['tfe_uploads'][] = [ + 'tmp_path' => 'uploads/' . $sessionId . '/' . $tmpName, + 'orig_name' => $origName, + 'size' => $upload['size'], + 'mime' => $mimeType, +]; + +// ── Render updated queue ─────────────────────────────────────────────────── +require_once __DIR__ . '/tfe-queue-helper.php'; +renderQueueFragment($_SESSION['tfe_uploads'], '/partage/remove-tfe-file'); diff --git a/app/src/Controllers/ThesisCreateController.php b/app/src/Controllers/ThesisCreateController.php index ff4d3b4..d43a0b7 100644 --- a/app/src/Controllers/ThesisCreateController.php +++ b/app/src/Controllers/ThesisCreateController.php @@ -196,7 +196,13 @@ class ThesisCreateController $this->handleCoverUpload($thesisId, $files['couverture'] ?? null, $folderPath, $filePrefix); $this->handleNoteIntentionUpload($thesisId, $files['note_intention'] ?? null, $folderPath, $filePrefix); - $nextNum = $this->handleTfeFiles($thesisId, $files['files'] ?? null, $folderPath, $filePrefix, $post, 1); + + // TFE files come from session temp (incremental upload via HTMX) + $sessionUploads = $_SESSION['tfe_uploads'] ?? []; + $nextNum = $this->handleTfeFilesFromSession($thesisId, $sessionUploads, $folderPath, $filePrefix, 1); + // Clear session uploads after successful commit + $this->cleanupSessionUploads(); + $this->handleAnnexeFiles($thesisId, $files['annexes'] ?? null, $folderPath, $filePrefix, $post); // PeerTube file rows don't go on disk, but the uploads themselves are processed separately diff --git a/app/src/Controllers/ThesisEditController.php b/app/src/Controllers/ThesisEditController.php index 8d0af7b..63afb2a 100644 --- a/app/src/Controllers/ThesisEditController.php +++ b/app/src/Controllers/ThesisEditController.php @@ -411,8 +411,9 @@ class ThesisEditController } } - // ── New TFE files upload ───────────────────────────────────────────── - if (!empty($files['files']['name'][0])) { + // ── New TFE files upload (from session via HTMX incremental upload) ── + $sessionUploads = $_SESSION['tfe_uploads'] ?? []; + if (!empty($sessionUploads)) { // Count existing TFE files to determine starting number $tfeCount = 0; foreach ($existingFiles as $f) { @@ -420,9 +421,9 @@ class ThesisEditController && !str_starts_with($f['file_path'] ?? '', 'http')) { $tfeCount++; } - // Don't count captions as separate TFE entries — they'll be renumbered } - $this->handleTfeFiles($thesisId, $files['files'], $folderPath, $filePrefix, $post, $tfeCount + 1); + $this->handleTfeFilesFromSession($thesisId, $sessionUploads, $folderPath, $filePrefix, $tfeCount + 1); + $this->cleanupSessionUploads(); } // ── New annexe files upload ──────────────────────────────────────────── diff --git a/app/src/Controllers/ThesisFileHandler.php b/app/src/Controllers/ThesisFileHandler.php index 5805401..1d01f17 100644 --- a/app/src/Controllers/ThesisFileHandler.php +++ b/app/src/Controllers/ThesisFileHandler.php @@ -303,6 +303,102 @@ trait ThesisFileHandler return $num; } + /** + * Process TFE file uploads from session-stored temp paths. + * + * Used when files are uploaded incrementally via HTMX fragments (upload-tfe-file.php) + * rather than submitted in a single multipart form. + * + * @param int $thesisId + * @param array $uploads Array of ['orig_name', 'size', 'mime', 'tmp_path'] + * @param string $folderPath + * @param string $filePrefix + * @param int $startNum + */ + protected function handleTfeFilesFromSession(int $thesisId, array $uploads, string $folderPath, string $filePrefix, int $startNum = 1): int + { + if (empty($uploads)) { + return $startNum; + } + + $dir = STORAGE_ROOT . '/' . $folderPath; + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + + $files = []; + foreach ($uploads as $f) { + $mimeType = $f['mime']; + $absPath = STORAGE_ROOT . '/' . $f['tmp_path']; + + if (!file_exists($absPath)) { + error_log("ThesisFileHandler: session temp file missing {$f['tmp_path']}, skipping"); + continue; + } + + $ext = strtolower(pathinfo($f['orig_name'], PATHINFO_EXTENSION)); + + if ($mimeType === 'text/plain' && $ext === 'vtt') { + $mimeType = 'text/vtt'; + } + + $files[] = [ + 'mimeType' => $mimeType, + 'ext' => $ext, + 'size' => $f['size'], + 'origName' => $f['orig_name'], + 'label' => '', + 'sortOrder' => null, + 'hierarchy' => $this->tfeHierarchyRank($mimeType, $ext), + 'fileType' => $this->detectFileType($mimeType, $ext), + // Pass the absolute path so writeTfeFile knows where to copy from + 'srcPath' => $absPath, + ]; + } + + // Sort by hierarchy rank + usort($files, fn($a, $b) => $a['hierarchy'] - $b['hierarchy']); + + $videoCount = 0; + $vttQueue = []; + + foreach ($files as $f) { + if ($f['fileType'] === 'video') { + $videoCount++; + } + } + + $num = $startNum; + + foreach ($files as $f) { + if ($f['fileType'] === 'caption') { + $vttQueue[] = $f; + continue; + } + + if ($f['fileType'] === 'video') { + $this->writeTfeFileFromSrc($f, $thesisId, $dir, $folderPath, $filePrefix, $num); + $num++; + + if (!empty($vttQueue)) { + $vtt = array_shift($vttQueue); + $this->writeTfeFileFromSrc($vtt, $thesisId, $dir, $folderPath, $filePrefix, $num); + $num++; + } + } else { + $this->writeTfeFileFromSrc($f, $thesisId, $dir, $folderPath, $filePrefix, $num); + $num++; + } + } + + foreach ($vttQueue as $vtt) { + $this->writeTfeFileFromSrc($vtt, $thesisId, $dir, $folderPath, $filePrefix, $num); + $num++; + } + + return $num; + } + /** * Process annex file uploads. * @@ -421,6 +517,61 @@ trait ThesisFileHandler error_log("ThesisFileHandler: TFE uploaded → $targetName ({$f['fileType']})"); } + /** + * Write a single TFE file from a source path (session temp) to the thesis folder. + * Used by handleTfeFilesFromSession instead of move_uploaded_file. + */ + protected function writeTfeFileFromSrc(array $f, int $thesisId, string $dir, string $folderPath, string $filePrefix, int $num): void + { + $padded = sprintf('%02d', $num); + $targetName = $filePrefix . '_TFE_' . $padded . '.' . $f['ext']; + $targetPath = $dir . $targetName; + + if (!rename($f['srcPath'], $targetPath)) { + // Fallback: copy + unlink + if (!copy($f['srcPath'], $targetPath)) { + error_log("ThesisFileHandler: failed to move session TFE {$f['origName']}"); + return; + } + unlink($f['srcPath']); + } + + chmod($targetPath, 0644); + $relPath = $folderPath . $targetName; + + $this->db->insertThesisFile( + $thesisId, $f['fileType'], + $relPath, + basename($f['origName']), + $f['size'], + $f['mimeType'], + $f['label'] !== '' ? $f['label'] : null, + $f['sortOrder'] + ); + error_log("ThesisFileHandler: TFE (session) moved → $targetName ({$f['fileType']})"); + } + + /** + * Clean up session upload temp files and clear the session entry. + * Call after successful commit of TFE files. + */ + protected function cleanupSessionUploads(): void + { + $sessionId = session_id(); + $tempDir = STORAGE_ROOT . '/uploads/' . $sessionId; + + // Remove any remaining files in the temp dir + if (is_dir($tempDir)) { + $files = glob($tempDir . '/*'); + foreach ($files as $file) { + @unlink($file); + } + @rmdir($tempDir); + } + + unset($_SESSION['tfe_uploads']); + } + /** * Assign a hierarchy rank for sorting TFE files. * Lower = earlier in the sequence. diff --git a/app/src/Controllers/validate-file-fragment-shared.php b/app/src/Controllers/validate-file-fragment-shared.php index 2469d70..017de6a 100644 --- a/app/src/Controllers/validate-file-fragment-shared.php +++ b/app/src/Controllers/validate-file-fragment-shared.php @@ -17,8 +17,18 @@ $adminMode = ($_POST['admin_mode'] ?? '0') === '1'; $fieldName = $_POST['field_name'] ?? ''; // Read file from the field-name-specific key (e.g., $_FILES['couverture'], $_FILES['annexes']) -// For multi-file inputs (name ends with []), the first file is validated. +// Fall back to the first file in $_FILES if the specific key is empty +// (handles PeerTube inputs where name differs from field_name). $rawFile = $_FILES[$fieldName] ?? null; +if (!$rawFile || empty($rawFile['name'])) { + // Try any uploaded file + foreach ($_FILES as $v) { + if (!empty($v['name'])) { + $rawFile = $v; + break; + } + } +} if ($rawFile && is_array($rawFile['name'] ?? null)) { // Multi-file: flatten first entry $file = [ diff --git a/app/templates/admin/acces.php b/app/templates/admin/acces.php index 2d59799..521a88d 100644 --- a/app/templates/admin/acces.php +++ b/app/templates/admin/acces.php @@ -220,6 +220,19 @@ +%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +\\\\\\\ to: uvnvvyny 09c854ea "fix: req annexes, add HTMX inline file validation (MIME/size)" (rebased revision) ++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: uvnvvyny 09c854ea "fix: req annexes, add HTMX inline file validation (MIME/size)" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: mxvvqust 02efed58 "refactor: session-based incremental TFE upload via HTMX, drop SortableJS" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: mxvvqust 0a424ac8 "refactor: session-based incremental TFE upload via HTMX, drop SortableJS" (rebased revision) +++ $linkName = $link['name'] ?? ''; ++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; ?>.vtt sont des sous-titres et seront associés automatiquement à la vidéo précédente.
-
- Aucun fichier sélectionné.