['name' => 'img.png', 'tmp_name' => '/tmp/...', ...]] // // Multi-file queue inputs (queue_file[tfe][], queue_file[annexe][], etc.) arrive nested: // $_FILES = ['queue_file' => ['name' => ['tfe' => ['file1.pdf']], 'tmp_name' => ['tfe' => ['/tmp/...']], ...]] // // We extract the first available file entry regardless of nesting depth. $upload = null; // Try flat structure first (single-file inputs) foreach ($_FILES as $key => $info) { if (is_array($info) && isset($info['tmp_name']) && is_string($info['tmp_name'])) { $upload = $info; break; } } // Try nested queue structure: $_FILES['queue_file']['tmp_name'][][0] if ($upload === null && isset($_FILES['queue_file']['tmp_name'])) { foreach ($_FILES['queue_file']['tmp_name'] as $subKey => $subValue) { if (is_array($subValue) && isset($subValue[0]) && is_string($subValue[0])) { $upload = [ 'name' => $_FILES['queue_file']['name'][$subKey][0] ?? '', 'tmp_name' => $_FILES['queue_file']['tmp_name'][$subKey][0] ?? '', 'error' => $_FILES['queue_file']['error'][$subKey][0] ?? UPLOAD_ERR_NO_FILE, 'size' => $_FILES['queue_file']['size'][$subKey][0] ?? 0, ]; break; } } } if ($upload === null) { error_log('[filepond:process] No usable file found. _FILES: ' . substr(json_encode($_FILES, JSON_PARTIAL_OUTPUT_ON_ERROR), 0, 500)); http_response_code(400); die('Aucun fichier reçu.'); } $err = $upload['error'] ?? -1; if ($err !== UPLOAD_ERR_OK) { error_log('[filepond:process] Upload error ' . $err . ' for ' . ($upload['name'] ?? '?')); http_response_code(400); die('Erreur de téléversement (code ' . $err . ').'); } $queueType = trim($_POST['queue_type'] ?? ''); error_log('[filepond:process] Received file | name=' . $upload['name'] . ' | size=' . $upload['size'] . ' | queue_type=' . $queueType); // ── MIME / extension whitelist (mirrored from ThesisFileHandler) ───────── const ALLOWED_MIME_TYPES = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf', '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', ]; const ALLOWED_EXTENSIONS = [ 'jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'mp4', 'webm', 'ogv', 'mov', 'mp3', 'ogg', 'oga', 'wav', 'flac', 'aac', 'm4a', 'vtt', 'zip', 'tar', 'gz', 'tgz', ]; // Per-queue-type constraints const QUEUE_MIME_MAP = [ 'cover' => ['image/jpeg', 'image/png', 'image/webp'], 'note_intention' => ['application/pdf'], 'tfe' => null, // full whitelist 'video' => ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime'], 'audio' => ['audio/mpeg', 'audio/mp3', 'audio/ogg', 'audio/flac', 'audio/x-wav', 'audio/aac', 'audio/mp4'], 'annexe' => ['application/pdf', 'application/zip', 'application/x-zip-compressed', 'application/x-tar', 'application/gzip', 'application/octet-stream'], 'peertube_video' => ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime'], 'peertube_audio' => ['audio/mpeg', 'audio/mp3', 'audio/ogg', 'audio/flac', 'audio/x-wav', 'audio/aac', 'audio/mp4'], ]; const QUEUE_SIZE_LIMITS = [ 'cover' => 20 * 1024 * 1024, // 20 MB 'note_intention' => 100 * 1024 * 1024, // 100 MB 'tfe' => 500 * 1024 * 1024, // 500 MB (per-file, but per-extension overrides below) 'video' => 500 * 1024 * 1024, // 500 MB 'audio' => 500 * 1024 * 1024, // 500 MB 'annexe' => 500 * 1024 * 1024, // 500 MB 'peertube_video' => 500 * 1024 * 1024, // 500 MB 'peertube_audio' => 500 * 1024 * 1024, // 500 MB ]; // Per-extension overrides for TFE (from ThesisFileHandler constants) const AV_EXTENSIONS = ['mp4', 'webm', 'ogv', 'mov', 'mp3', 'ogg', 'oga', 'wav', 'flac', 'aac', 'm4a']; const MAX_PDF_SIZE = 100 * 1024 * 1024; // 100 MB const MAX_AV_SIZE = 2 * 1024 * 1024 * 1024; // 2 GB // ── MIME detection ─────────────────────────────────────────────────────── $finfo = new finfo(FILEINFO_MIME_TYPE); $mimeType = $finfo->file($upload['tmp_name']); $ext = strtolower(pathinfo($upload['name'], PATHINFO_EXTENSION)); error_log('[filepond:process] MIME detected | mime=' . $mimeType . ' | ext=' . $ext); // Fix common mismatches if ($mimeType === 'text/plain' && $ext === 'vtt') { $mimeType = 'text/vtt'; } if ($mimeType === 'audio/mpeg' && $ext === 'mp3') { $mimeType = 'audio/mp3'; } // ── Validate MIME / extension ──────────────────────────────────────────── $allowedMimes = QUEUE_MIME_MAP[$queueType] ?? null; if ($allowedMimes !== null) { if (!in_array($mimeType, $allowedMimes, true)) { http_response_code(415); die("Type de fichier non accepté ($mimeType)."); } } else { // Full whitelist if (!in_array($mimeType, ALLOWED_MIME_TYPES, true) && !in_array($ext, ALLOWED_EXTENSIONS, true)) { http_response_code(415); die("Type de fichier non accepté ($mimeType / .$ext)."); } } // ── Validate size ──────────────────────────────────────────────────────── $sizeLimit = QUEUE_SIZE_LIMITS[$queueType] ?? MAX_PDF_SIZE; // Per-extension overrides for TFE queue (PDF=100MB, AV=2GB) if ($queueType === 'tfe') { if ($ext === 'pdf' || $mimeType === 'application/pdf') { $sizeLimit = MAX_PDF_SIZE; } elseif (in_array($ext, AV_EXTENSIONS, true) || str_starts_with($mimeType, 'video/') || str_starts_with($mimeType, 'audio/')) { $sizeLimit = MAX_AV_SIZE; } } if ($upload['size'] > $sizeLimit) { $limitMb = round($sizeLimit / 1024 / 1024); $sizeMb = round($upload['size'] / 1024 / 1024); http_response_code(413); die("Fichier trop volumineux ($sizeMb MB, max $limitMb MB)."); } // ── Generate unique file_id ────────────────────────────────────────────── $fileId = bin2hex(random_bytes(16)); // 32-char hex // ── Save to tmp/filepond/{file_id}/ ────────────────────────────────────── $tmpDir = STORAGE_ROOT . '/tmp/filepond/' . $fileId; if (!mkdir($tmpDir, 0755, true)) { error_log('[filepond:process] Failed to create tmp dir: ' . $tmpDir); http_response_code(500); die('Erreur serveur — impossible de stocker le fichier.'); } $originalName = basename($upload['name']); $targetPath = $tmpDir . '/' . $originalName; if (!move_uploaded_file($upload['tmp_name'], $targetPath)) { error_log('[filepond:process] move_uploaded_file FAILED | from=' . $upload['tmp_name'] . ' | to=' . $targetPath); rmdir($tmpDir); // clean up empty dir http_response_code(500); die('Erreur serveur — échec du déplacement du fichier.'); } chmod($targetPath, 0644); error_log('[filepond:process] File saved to tmp | file_id=' . $fileId . ' | path=' . $targetPath); // ── PeerTube: upload immediately (don't wait for form submit) ──────────── $isPeerTube = str_starts_with($queueType, 'peertube_'); if ($isPeerTube) { require_once APP_ROOT . '/src/PeerTubeService.php'; if (PeerTubeService::isEnabled(new Database())) { $ptFileType = ($queueType === 'peertube_video') ? 'video' : 'audio'; try { $result = PeerTubeService::upload( new Database(), $targetPath, $originalName, $originalName, // title — will be overridden by form submit metadata '' ); // Return a special ID prefix so the controller knows not to look in tmp/ $fileId = 'peertube:' . $result['uuid']; // Clean up temp file — PeerTube has its own copy now @unlink($targetPath); @rmdir($tmpDir); error_log('[filepond:process] PeerTube upload OK | uuid=' . $result['uuid'] . ' | url=' . $result['watchUrl']); header('Content-Type: text/plain; charset=utf-8'); echo $fileId; exit; } catch (\Throwable $e) { @unlink($targetPath); @rmdir($tmpDir); error_log('[filepond:process] PeerTube upload FAILED: ' . $e->getMessage()); http_response_code(500); die('Erreur lors du téléversement vers PeerTube.'); } } else { // PeerTube not enabled — reject the upload @unlink($targetPath); @rmdir($tmpDir); http_response_code(503); die('PeerTube n\'est pas activé.'); } } // ── Write manifest ─────────────────────────────────────────────────────── $manifest = [ 'queue_type' => $queueType, 'original_name' => $originalName, 'mime' => $mimeType, 'ext' => $ext, 'size' => $upload['size'], 'session_id' => session_id(), 'uploaded_at' => date('c'), ]; file_put_contents($tmpDir . '/manifest.json', json_encode($manifest, JSON_UNESCAPED_SLASHES)); // ── Return file_id as plain text ───────────────────────────────────────── error_log('[filepond:process] SUCCESS | file_id=' . $fileId . ' | queue_type=' . $queueType . ' | name=' . $originalName); header('Content-Type: text/plain; charset=utf-8'); echo $fileId;