mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 08:09:18 +02:00
Fixes three root causes of FilePond errors on TFE upload forms: 1. server.process.onerror accessed .status on a string (XHR response text body) — now extracts the body safely. 2. server.load was a bare URL string with no error handling — converted to object with onload/onerror to prevent FilePond internal _write crash when load.php returns HTTP errors. 3. destroyFilePondsIn now aborts in-flight processing before pond.destroy() to prevent stale XHR callbacks firing on a torn-down FilePond instance. Server-side: FilepondHandler now emits Content-Type: text/plain on all responses (PHP defaults to text/html on die(), confusing FilePond's response parser).
625 lines
26 KiB
PHP
625 lines
26 KiB
PHP
<?php
|
|
|
|
/**
|
|
* FilePond async upload handler — shared logic.
|
|
*
|
|
* The four FilePond server endpoints (process, load, remove, revert) are
|
|
* called from both the admin panel and the student partage form.
|
|
*
|
|
* Auth is checked by the caller before invoking these methods:
|
|
* - Admin endpoints: nginx auth_basic + AdminAuth::requireLogin()
|
|
* - Partagé endpoints: session_start() + verify share_active + CSRF
|
|
*
|
|
* All paths in this file assume the session is already started and CSRF is
|
|
* verified by the caller.
|
|
*/
|
|
|
|
class FilepondHandler
|
|
{
|
|
// ── MIME / extension whitelist (mirrored from ThesisFileHandler) ─────────
|
|
|
|
public 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',
|
|
];
|
|
|
|
public 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
|
|
public 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'],
|
|
];
|
|
|
|
public const QUEUE_SIZE_LIMITS = [
|
|
'cover' => 20 * 1024 * 1024, // 20 MB
|
|
'note_intention' => 100 * 1024 * 1024, // 100 MB
|
|
'tfe' => 1024 * 1024 * 1024, // 1 GB (default for non-AV, non-PDF)
|
|
'video' => 8 * 1024 * 1024 * 1024, // 8 GB
|
|
'audio' => 8 * 1024 * 1024 * 1024, // 8 GB
|
|
'annexe' => 1024 * 1024 * 1024, // 1 GB
|
|
'peertube_video' => 8 * 1024 * 1024 * 1024, // 8 GB
|
|
'peertube_audio' => 8 * 1024 * 1024 * 1024, // 8 GB
|
|
];
|
|
|
|
public const AV_EXTENSIONS = ['mp4', 'webm', 'ogv', 'mov', 'mp3', 'ogg', 'oga', 'wav', 'flac', 'aac', 'm4a'];
|
|
public const MAX_PDF_SIZE = 100 * 1024 * 1024; // 100 MB
|
|
public const MAX_AV_SIZE = 8 * 1024 * 1024 * 1024; // 8 GB
|
|
|
|
// ── Log prefix for distinguishing admin vs partage ───────────────────────
|
|
|
|
private string $logPrefix;
|
|
|
|
public function __construct(string $logPrefix = '[filepond]')
|
|
{
|
|
$this->logPrefix = $logPrefix;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// PROCESS — receive one file, validate, store to tmp, return file_id
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
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') {
|
|
http_response_code(405);
|
|
die('Méthode non autorisée.');
|
|
}
|
|
|
|
$upload = $this->extractUpload();
|
|
if ($upload === null) {
|
|
error_log($this->logPrefix . ':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($this->logPrefix . ':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($this->logPrefix . ':process Received file | name=' . $upload['name'] . ' | size=' . $upload['size'] . ' | queue_type=' . $queueType);
|
|
|
|
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
|
$mimeType = $finfo->file($upload['tmp_name']);
|
|
$ext = strtolower(pathinfo($upload['name'], PATHINFO_EXTENSION));
|
|
error_log($this->logPrefix . ':process MIME detected | mime=' . $mimeType . ' | ext=' . $ext);
|
|
|
|
if ($mimeType === 'text/plain' && $ext === 'vtt') {
|
|
$mimeType = 'text/vtt';
|
|
}
|
|
if ($mimeType === 'audio/mpeg' && $ext === 'mp3') {
|
|
$mimeType = 'audio/mp3';
|
|
}
|
|
|
|
$this->validateMimeExt($queueType, $mimeType, $ext);
|
|
$this->validateSize($queueType, $mimeType, $ext, $upload['size']);
|
|
|
|
$fileId = bin2hex(random_bytes(16));
|
|
$tmpDir = STORAGE_ROOT . '/tmp/filepond/' . $fileId;
|
|
if (!mkdir($tmpDir, 0755, true)) {
|
|
error_log($this->logPrefix . ':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($this->logPrefix . ':process move_uploaded_file FAILED | from=' . $upload['tmp_name'] . ' | to=' . $targetPath);
|
|
rmdir($tmpDir);
|
|
http_response_code(500);
|
|
die('Erreur serveur — échec du déplacement du fichier.');
|
|
}
|
|
chmod($targetPath, 0644);
|
|
error_log($this->logPrefix . ':process File saved to tmp | file_id=' . $fileId . ' | path=' . $targetPath);
|
|
|
|
// Track temp file in session so it survives page reloads
|
|
if (session_status() === PHP_SESSION_ACTIVE) {
|
|
$_SESSION['filepond_tmp'][$queueType] = $_SESSION['filepond_tmp'][$queueType] ?? [];
|
|
$_SESSION['filepond_tmp'][$queueType][] = $fileId;
|
|
}
|
|
|
|
$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,
|
|
''
|
|
);
|
|
$fileId = 'peertube:' . $ptFileType . ':' . $result['uuid'];
|
|
@unlink($targetPath);
|
|
@rmdir($tmpDir);
|
|
error_log($this->logPrefix . ':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($this->logPrefix . ':process PeerTube upload FAILED: ' . $e->getMessage());
|
|
http_response_code(500);
|
|
die('Erreur lors du téléversement vers PeerTube.');
|
|
}
|
|
} else {
|
|
if ($isPeerTubeQueue) {
|
|
@unlink($targetPath);
|
|
@rmdir($tmpDir);
|
|
http_response_code(503);
|
|
die('PeerTube n\'est pas activé.');
|
|
}
|
|
}
|
|
}
|
|
|
|
$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));
|
|
|
|
error_log($this->logPrefix . ':process SUCCESS | file_id=' . $fileId . ' | queue_type=' . $queueType . ' | name=' . $originalName);
|
|
echo $fileId;
|
|
exit;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// LOAD — stream an existing thesis file back to FilePond
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
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.');
|
|
}
|
|
|
|
$fileId = trim($_GET['id'] ?? '');
|
|
|
|
// Hex IDs (32 chars) → temp files from tmp/filepond/
|
|
if (preg_match('/^[a-f0-9]{32}$/', $fileId)) {
|
|
$this->loadTempFile($fileId);
|
|
// loadTempFile exits; never returns
|
|
}
|
|
|
|
// 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.');
|
|
}
|
|
|
|
error_log($this->logPrefix . ':load ENTRY | db_id=' . $dbId);
|
|
|
|
$pdo = Database::getInstance()->getConnection();
|
|
$stmt = $pdo->prepare('SELECT * FROM thesis_files WHERE id = ?');
|
|
$stmt->execute([$dbId]);
|
|
$fileRow = $stmt->fetch();
|
|
|
|
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.');
|
|
}
|
|
|
|
$filePath = $fileRow['file_path'] ?? '';
|
|
$fileName = $fileRow['file_name'] ?? basename($filePath);
|
|
$mimeType = $fileRow['mime_type'] ?? 'application/octet-stream';
|
|
|
|
if (str_starts_with($filePath, 'peertube_ids:')) {
|
|
$uuid = substr($filePath, strlen('peertube_ids:'));
|
|
$isVideo = ($fileRow['file_type'] ?? '') === 'video';
|
|
$svg = $isVideo
|
|
? '<svg xmlns="http://www.w3.org/2000/svg" width="180" height="120" viewBox="0 0 180 120"><rect width="180" height="120" fill="#1a1a2e"/><polygon points="70,35 70,85 125,60" fill="#e94560"/><text x="90" y="110" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#aaa">PeerTube ' . htmlspecialchars($uuid) . '</text></svg>'
|
|
: '<svg xmlns="http://www.w3.org/2000/svg" width="180" height="120" viewBox="0 0 180 120"><rect width="180" height="120" fill="#1a1a2e"/><circle cx="55" cy="60" r="20" fill="none" stroke="#4ecca3" stroke-width="3"/><line x1="72" y1="48" x2="95" y2="38" stroke="#4ecca3" stroke-width="3"/><line x1="72" y1="60" x2="110" y2="60" stroke="#4ecca3" stroke-width="3"/><line x1="72" y1="72" x2="95" y2="82" stroke="#4ecca3" stroke-width="3"/><text x="90" y="110" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#aaa">PeerTube ' . htmlspecialchars($uuid) . '</text></svg>';
|
|
header('Content-Type: image/svg+xml');
|
|
header('Content-Length: ' . strlen($svg));
|
|
header('Content-Disposition: inline; filename="peertube.svg"');
|
|
header('Cache-Control: no-cache');
|
|
echo $svg;
|
|
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.');
|
|
}
|
|
|
|
$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.');
|
|
}
|
|
|
|
$fileSize = filesize($absPath);
|
|
error_log($this->logPrefix . ':load OK | db_id=' . $dbId . ' | path=' . $filePath . ' | mime=' . $mimeType . ' | size=' . $fileSize);
|
|
header('Content-Type: ' . $mimeType);
|
|
header('Content-Length: ' . $fileSize);
|
|
header('Content-Disposition: inline; filename="' . addslashes($fileName) . '"');
|
|
header('Cache-Control: no-cache');
|
|
readfile($absPath);
|
|
exit;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// REMOVE — soft-delete a thesis_files row (edit mode)
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
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.');
|
|
}
|
|
|
|
$body = json_decode(file_get_contents('php://input'), true);
|
|
$dbId = filter_var($body['db_id'] ?? '', FILTER_VALIDATE_INT);
|
|
|
|
if ($dbId === false || $dbId <= 0) {
|
|
http_response_code(400);
|
|
die('ID de fichier invalide.');
|
|
}
|
|
|
|
$pdo = Database::getInstance()->getConnection();
|
|
$stmt = $pdo->prepare('SELECT * FROM thesis_files WHERE id = ?');
|
|
$stmt->execute([$dbId]);
|
|
$fileRow = $stmt->fetch();
|
|
|
|
if (!$fileRow) {
|
|
http_response_code(404);
|
|
die('Fichier introuvable.');
|
|
}
|
|
|
|
$filePath = $fileRow['file_path'] ?? '';
|
|
if ($filePath !== ''
|
|
&& !str_starts_with($filePath, 'peertube_ids:')
|
|
&& !str_starts_with($filePath, 'http://')
|
|
&& !str_starts_with($filePath, 'https://')) {
|
|
|
|
$absPath = STORAGE_ROOT . '/' . $filePath;
|
|
if (file_exists($absPath)) {
|
|
$trashDir = STORAGE_ROOT . '/tmp/_trash';
|
|
if (!is_dir($trashDir)) {
|
|
mkdir($trashDir, 0755, true);
|
|
}
|
|
$trashPath = $trashDir . '/' . $dbId . '_' . basename($filePath);
|
|
rename($absPath, $trashPath);
|
|
}
|
|
}
|
|
|
|
$delStmt = $pdo->prepare('DELETE FROM thesis_files WHERE id = ?');
|
|
$delStmt->execute([$dbId]);
|
|
|
|
http_response_code(200);
|
|
exit;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// REVERT — delete a tmp file (user removes before form submit)
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
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.');
|
|
}
|
|
|
|
$fileId = trim(file_get_contents('php://input'));
|
|
|
|
if (str_starts_with($fileId, 'peertube:')) {
|
|
http_response_code(200);
|
|
exit;
|
|
}
|
|
|
|
if ($fileId === '' || !preg_match('/^[a-f0-9]{32}$/', $fileId)) {
|
|
http_response_code(400);
|
|
die('ID de fichier invalide.');
|
|
}
|
|
|
|
$tmpDir = STORAGE_ROOT . '/tmp/filepond/' . $fileId;
|
|
$manifestPath = $tmpDir . '/manifest.json';
|
|
|
|
if (!is_dir($tmpDir) || !file_exists($manifestPath)) {
|
|
http_response_code(404);
|
|
exit;
|
|
}
|
|
|
|
$manifest = json_decode(file_get_contents($manifestPath), true);
|
|
if (!is_array($manifest) || ($manifest['session_id'] ?? '') !== session_id()) {
|
|
http_response_code(403);
|
|
die('Session invalide.');
|
|
}
|
|
|
|
// Remove from session tracking
|
|
if (session_status() === PHP_SESSION_ACTIVE) {
|
|
$this->removeFromSessionTmp($fileId);
|
|
}
|
|
|
|
$it = new RecursiveDirectoryIterator($tmpDir, RecursiveDirectoryIterator::SKIP_DOTS);
|
|
$files_it = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
|
|
foreach ($files_it as $file) {
|
|
if ($file->isDir()) {
|
|
rmdir($file->getRealPath());
|
|
} else {
|
|
unlink($file->getRealPath());
|
|
}
|
|
}
|
|
rmdir($tmpDir);
|
|
|
|
http_response_code(200);
|
|
exit;
|
|
}
|
|
|
|
// ── Internal helpers ──────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Get temp files for the current session and a specific queue type.
|
|
*
|
|
* Returns an array suitable for injection into FilePond's data-existing-files
|
|
* JSON attribute, so temp files survive page reloads.
|
|
*/
|
|
public static function getSessionTempFiles(string $queueType): array
|
|
{
|
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
|
return [];
|
|
}
|
|
|
|
$fileIds = $_SESSION['filepond_tmp'][$queueType] ?? [];
|
|
if (empty($fileIds)) {
|
|
return [];
|
|
}
|
|
|
|
$result = [];
|
|
$missing = [];
|
|
foreach ($fileIds as $fileId) {
|
|
$tmpDir = STORAGE_ROOT . '/tmp/filepond/' . $fileId;
|
|
$manifestPath = $tmpDir . '/manifest.json';
|
|
|
|
if (!is_dir($tmpDir) || !file_exists($manifestPath)) {
|
|
$missing[] = $fileId;
|
|
continue;
|
|
}
|
|
|
|
$manifest = json_decode(file_get_contents($manifestPath), true);
|
|
if (!is_array($manifest)) {
|
|
$missing[] = $fileId;
|
|
continue;
|
|
}
|
|
|
|
if (($manifest['session_id'] ?? '') !== session_id()) {
|
|
$missing[] = $fileId;
|
|
continue;
|
|
}
|
|
|
|
// Find the actual file
|
|
$actualFile = null;
|
|
$dh = opendir($tmpDir);
|
|
while (($entry = readdir($dh)) !== false) {
|
|
if ($entry === '.' || $entry === '..' || $entry === 'manifest.json') {
|
|
continue;
|
|
}
|
|
$actualFile = $tmpDir . '/' . $entry;
|
|
break;
|
|
}
|
|
closedir($dh);
|
|
|
|
if ($actualFile === null || !file_exists($actualFile)) {
|
|
$missing[] = $fileId;
|
|
continue;
|
|
}
|
|
|
|
$result[] = [
|
|
'source' => $fileId,
|
|
'options' => [
|
|
'type' => 'local',
|
|
'file' => [
|
|
'name' => $manifest['original_name'] ?? basename($actualFile),
|
|
'size' => (int)($manifest['size'] ?? filesize($actualFile)),
|
|
'type' => $manifest['mime'] ?? 'application/octet-stream',
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
// Clean up session entries for missing files
|
|
if (!empty($missing)) {
|
|
$_SESSION['filepond_tmp'][$queueType] = array_values(
|
|
array_diff($fileIds, $missing)
|
|
);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Load a temp file (hex file_id) — streams the file from tmp/filepond/.
|
|
*/
|
|
private function loadTempFile(string $fileId): never
|
|
{
|
|
$tmpDir = STORAGE_ROOT . '/tmp/filepond/' . $fileId;
|
|
$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.');
|
|
}
|
|
|
|
$actualFile = null;
|
|
$dh = opendir($tmpDir);
|
|
while (($entry = readdir($dh)) !== false) {
|
|
if ($entry === '.' || $entry === '..' || $entry === 'manifest.json') {
|
|
continue;
|
|
}
|
|
$actualFile = $tmpDir . '/' . $entry;
|
|
break;
|
|
}
|
|
closedir($dh);
|
|
|
|
if ($actualFile === null || !file_exists($actualFile)) {
|
|
header('Content-Type: text/plain; charset=utf-8');
|
|
http_response_code(404);
|
|
die('Fichier temporaire introuvable.');
|
|
}
|
|
|
|
$mimeType = $manifest['mime'] ?? mime_content_type($actualFile);
|
|
$fileSize = filesize($actualFile);
|
|
$fileName = $manifest['original_name'] ?? basename($actualFile);
|
|
|
|
error_log($this->logPrefix . ':load TEMP | file_id=' . $fileId . ' | name=' . $fileName . ' | size=' . $fileSize);
|
|
header('Content-Type: ' . $mimeType);
|
|
header('Content-Length: ' . $fileSize);
|
|
header('Content-Disposition: inline; filename="' . addslashes($fileName) . '"');
|
|
header('Cache-Control: no-cache');
|
|
readfile($actualFile);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Remove a file_id from the session temp tracking array.
|
|
*/
|
|
private function removeFromSessionTmp(string $fileId): void
|
|
{
|
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
|
return;
|
|
}
|
|
foreach ($_SESSION['filepond_tmp'] ?? [] as $queueType => $ids) {
|
|
$idx = array_search($fileId, $ids, true);
|
|
if ($idx !== false) {
|
|
array_splice($_SESSION['filepond_tmp'][$queueType], $idx, 1);
|
|
if (empty($_SESSION['filepond_tmp'][$queueType])) {
|
|
unset($_SESSION['filepond_tmp'][$queueType]);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract the first available file from $_FILES regardless of nesting depth.
|
|
*/
|
|
private function extractUpload(): ?array
|
|
{
|
|
foreach ($_FILES as $info) {
|
|
if (is_array($info) && isset($info['tmp_name']) && is_string($info['tmp_name'])) {
|
|
return $info;
|
|
}
|
|
}
|
|
|
|
if (isset($_FILES['queue_file']['tmp_name'])) {
|
|
foreach ($_FILES['queue_file']['tmp_name'] as $subValue) {
|
|
if (is_array($subValue) && isset($subValue[0]) && is_string($subValue[0])) {
|
|
$subKey = array_key_first($_FILES['queue_file']['tmp_name']);
|
|
return [
|
|
'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,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
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)) {
|
|
http_response_code(415);
|
|
die("Type de fichier non accepté ($mimeType).");
|
|
}
|
|
} else {
|
|
if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true)
|
|
&& !in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
|
|
http_response_code(415);
|
|
die("Type de fichier non accepté ($mimeType / .$ext).");
|
|
}
|
|
}
|
|
}
|
|
|
|
private function validateSize(string $queueType, string $mimeType, string $ext, int $size): void
|
|
{
|
|
$sizeLimit = self::QUEUE_SIZE_LIMITS[$queueType] ?? self::MAX_PDF_SIZE;
|
|
|
|
if ($queueType === 'tfe') {
|
|
if ($ext === 'pdf' || $mimeType === 'application/pdf') {
|
|
$sizeLimit = self::MAX_PDF_SIZE;
|
|
} elseif (in_array($ext, self::AV_EXTENSIONS, true)
|
|
|| str_starts_with($mimeType, 'video/')
|
|
|| str_starts_with($mimeType, 'audio/')) {
|
|
$sizeLimit = self::MAX_AV_SIZE;
|
|
}
|
|
}
|
|
|
|
if ($size > $sizeLimit) {
|
|
$limitMb = round($sizeLimit / 1024 / 1024);
|
|
$sizeMb = round($size / 1024 / 1024);
|
|
http_response_code(413);
|
|
die("Fichier trop volumineux ($sizeMb MB, max $limitMb MB).");
|
|
}
|
|
}
|
|
}
|