mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 08:09:18 +02:00
- Remove separate video/audio/peertube_video/peertube_audio pools from UI - TFE pool now accepts all file types including video/audio - When PeerTube is enabled, video/audio dropped into TFE pool auto-upload to PeerTube (process.php detects MIME and uploads immediately) - PeerTube return IDs now encode type: peertube:video:UUID or peertube:audio:UUID - load.php returns placeholder SVG for PeerTube files so they appear in FilePond - Edit mode: all existing files (including PeerTube) shown in TFE FilePond pool - Remove legacy video/audio/peertube_* handling from both controllers - Remove unused vide/audio/peertube_* entries from JS QUEUE_CONFIG
273 lines
12 KiB
PHP
273 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* FilePond process endpoint — receives one file per request.
|
|
*
|
|
* POST /admin/actions/filepond/process.php
|
|
* Headers: X-CSRF-Token
|
|
* Fields: file (multipart), queue_type (string)
|
|
*
|
|
* Returns plain text file_id on success (200), or error message on failure (4xx).
|
|
*/
|
|
|
|
require_once __DIR__ . '/../../../../bootstrap.php';
|
|
require_once __DIR__ . '/../../../../src/AdminAuth.php';
|
|
require_once __DIR__ . '/../../../../src/ErrorHandler.php';
|
|
|
|
AdminAuth::requireLogin();
|
|
|
|
error_log('[filepond:process] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | files_keys=' . implode(',', array_keys($_FILES)) . ' | post_keys=' . implode(',', array_keys($_POST)));
|
|
|
|
// ── CSRF via header ──────────────────────────────────────────────────────
|
|
$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
|
error_log('[filepond:process] CSRF header present=' . ($csrfHeader !== '' ? 'yes' : 'no') . ' | session_token=' . (isset($_SESSION['csrf_token']) ? 'set' : 'missing'));
|
|
if (!isset($_SESSION['csrf_token'])
|
|
|| !hash_equals($_SESSION['csrf_token'], $csrfHeader)) {
|
|
error_log('[filepond:process] CSRF FAIL — header=' . substr($csrfHeader, 0, 8) . '... session=' . substr($_SESSION['csrf_token'] ?? '', 0, 8) . '...');
|
|
http_response_code(403);
|
|
die('Token CSRF invalide.');
|
|
}
|
|
|
|
// ── Only accept POST ─────────────────────────────────────────────────────
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
http_response_code(405);
|
|
die('Méthode non autorisée.');
|
|
}
|
|
|
|
// ── Validate presence of file ────────────────────────────────────────────
|
|
// FilePond sends one file per POST. The field name depends on the input name attribute.
|
|
//
|
|
// Single-file inputs (cover, note_intention) arrive flat:
|
|
// $_FILES = ['couverture' => ['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'][<subkey>][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) ────────────
|
|
// Handles both dedicated peertube_* queues (legacy) and video/audio in the TFE pool
|
|
$isPeerTubeQueue = str_starts_with($queueType, 'peertube_');
|
|
$isTfeAv = ($queueType === 'tfe' && preg_match('/^(video|audio)\//', $mimeType));
|
|
$shouldPeerTube = $isPeerTubeQueue || $isTfeAv;
|
|
if ($shouldPeerTube) {
|
|
require_once APP_ROOT . '/src/PeerTubeService.php';
|
|
if (PeerTubeService::isEnabled(new Database())) {
|
|
$ptFileType = preg_match('/^video\//', $mimeType) ? '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/
|
|
// Format: peertube:video:UUID or peertube:audio:UUID
|
|
$fileId = 'peertube:' . $ptFileType . ':' . $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 — save to disk normally (only for tfe pool, not dedicated peertube queues)
|
|
if ($isPeerTubeQueue) {
|
|
@unlink($targetPath);
|
|
@rmdir($tmpDir);
|
|
http_response_code(503);
|
|
die('PeerTube n\'est pas activé.');
|
|
}
|
|
// For TFE pool, fall through to normal disk save below
|
|
}
|
|
}
|
|
|
|
// ── 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;
|