feat: FilePond production hardening — extension-based validation, server-side size limits (2GB), annexe validation, drop accept attributes, FilePond file styling

This commit is contained in:
Pontoporeia
2026-05-10 20:41:37 +02:00
parent 7b5f3efe40
commit 8db7b6e9eb
23 changed files with 4770 additions and 216 deletions

View File

@@ -34,12 +34,18 @@
*/
trait ThesisFileHandler
{
/** @var string[] Warnings collected during file processing (e.g. invalid type, too large). */
private array $fileWarnings = [];
/** Maximum allowed file size for thesis files (bytes). */
private const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500 MB
/** Maximum allowed file size for PDF files specifically (bytes). */
private const MAX_PDF_SIZE = 100 * 1024 * 1024; // 100 MB
/** Maximum allowed file size for video/audio files (bytes). */
private const MAX_AV_SIZE = 2 * 1024 * 1024 * 1024; // 2 GB
/** Cover image max size. */
private const MAX_COVER_SIZE = 20 * 1024 * 1024; // 20 MB
@@ -68,6 +74,15 @@ trait ThesisFileHandler
// ── Public entry points (called by controllers) ──────────────────────────
/**
* Get warnings collected during file processing (invalid types, too large, etc.).
* @return string[]
*/
public function getFileWarnings(): array
{
return $this->fileWarnings;
}
/**
* Process a cover image upload.
*
@@ -92,6 +107,7 @@ trait ThesisFileHandler
if (!in_array($mimeType, $allowedMimes, true)
|| !in_array($ext, $allowedExts, true)
|| $upload['size'] > self::MAX_COVER_SIZE) {
$this->fileWarnings[] = "Couverture « {$upload['name']} » ignorée : format ou taille non accepté.";
error_log("ThesisFileHandler: invalid cover MIME $mimeType / $ext / {$upload['size']} bytes, skipping");
return;
}
@@ -141,10 +157,12 @@ trait ThesisFileHandler
$ext = strtolower(pathinfo($upload['name'], PATHINFO_EXTENSION));
if ($mimeType !== 'application/pdf' || $ext !== 'pdf') {
$this->fileWarnings[] = "Note d'intention « {$upload['name']} » ignorée : seul le format PDF est accepté.";
error_log("ThesisFileHandler: invalid note d'intention MIME $mimeType, skipping");
return;
}
if ($upload['size'] > self::MAX_PDF_SIZE) {
$this->fileWarnings[] = "Note d'intention « {$upload['name']} » ignorée : fichier trop volumineux (max 100 MB).";
error_log("ThesisFileHandler: note d'intention too large ({$upload['size']} bytes), skipping");
return;
}
@@ -223,19 +241,25 @@ trait ThesisFileHandler
$mimeType = 'text/vtt';
}
if ($mimeType === 'application/octet-stream' && !in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
$this->fileWarnings[] = "Fichier TFE « {$uploads['name'][$i]} » ignoré : format .$ext non accepté.";
error_log("ThesisFileHandler: TFE extension not allowed {$uploads['name'][$i]} ($ext), skipping");
continue;
}
if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true)
&& !in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
$this->fileWarnings[] = "Fichier TFE « {$uploads['name'][$i]} » ignoré : type non accepté ($mimeType).";
error_log("ThesisFileHandler: invalid TFE type {$uploads['name'][$i]} ($mimeType / $ext), skipping");
continue;
}
$isPdf = ($mimeType === 'application/pdf' || $ext === 'pdf');
$sizeLimit = $isPdf ? self::MAX_PDF_SIZE : self::MAX_FILE_SIZE;
$isAv = preg_match('/^(video|audio)\//', $mimeType) || in_array($ext, ['mp4','webm','ogv','mov','mp3','ogg','oga','wav','flac','aac','m4a']);
$sizeLimit = $isPdf ? self::MAX_PDF_SIZE : ($isAv ? self::MAX_AV_SIZE : self::MAX_FILE_SIZE);
if ($uploads['size'][$i] > $sizeLimit) {
error_log("ThesisFileHandler: TFE file too large {$uploads['name'][$i]} (" . round($uploads['size'][$i] / 1024 / 1024) . ' MB), skipping');
$limitMb = round($sizeLimit / 1024 / 1024);
$sizeMb = round($uploads['size'][$i] / 1024 / 1024);
$this->fileWarnings[] = "Fichier TFE « {$uploads['name'][$i]} » ignoré : trop volumineux ($sizeMb MB, max $limitMb MB).";
error_log("ThesisFileHandler: TFE file too large {$uploads['name'][$i]} (" . $sizeMb . ' MB), skipping');
continue;
}
@@ -364,19 +388,25 @@ trait ThesisFileHandler
$mimeType = 'text/vtt';
}
if ($mimeType === 'application/octet-stream' && !in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
$this->fileWarnings[] = "Fichier « {$uploads['name'][$i]} » ignoré : format .$ext non accepté.";
error_log("ThesisFileHandler: queue file extension not allowed {$uploads['name'][$i]} ($ext), skipping");
continue;
}
if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true)
&& !in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
$this->fileWarnings[] = "Fichier « {$uploads['name'][$i]} » ignoré : type non accepté ($mimeType).";
error_log("ThesisFileHandler: invalid queue file type {$uploads['name'][$i]} ($mimeType / $ext), skipping");
continue;
}
$isPdf = ($mimeType === 'application/pdf' || $ext === 'pdf');
$sizeLimit = $isPdf ? self::MAX_PDF_SIZE : self::MAX_FILE_SIZE;
$isAv = preg_match('/^(video|audio)\//', $mimeType) || in_array($ext, ['mp4','webm','ogv','mov','mp3','ogg','oga','wav','flac','aac','m4a']);
$sizeLimit = $isPdf ? self::MAX_PDF_SIZE : ($isAv ? self::MAX_AV_SIZE : self::MAX_FILE_SIZE);
if ($uploads['size'][$i] > $sizeLimit) {
error_log("ThesisFileHandler: queue file too large {$uploads['name'][$i]} (" . round($uploads['size'][$i] / 1024 / 1024) . ' MB), skipping');
$limitMb = round($sizeLimit / 1024 / 1024);
$sizeMb = round($uploads['size'][$i] / 1024 / 1024);
$this->fileWarnings[] = "Fichier « {$uploads['name'][$i]} » ignoré : trop volumineux ($sizeMb MB, max $limitMb MB).";
error_log("ThesisFileHandler: queue file too large {$uploads['name'][$i]} (" . $sizeMb . ' MB), skipping');
continue;
}
@@ -461,6 +491,28 @@ trait ThesisFileHandler
if ($mimeType === 'text/plain' && $ext === 'vtt') {
$mimeType = 'text/vtt';
}
if ($mimeType === 'application/octet-stream' && !in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
$this->fileWarnings[] = "Annexe « {$uploads['name'][$i]} » ignorée : format .$ext non accepté.";
error_log("ThesisFileHandler: queue annexe extension not allowed {$uploads['name'][$i]} ($ext), skipping");
continue;
}
if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true)
&& !in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
$this->fileWarnings[] = "Annexe « {$uploads['name'][$i]} » ignorée : type non accepté ($mimeType).";
error_log("ThesisFileHandler: invalid queue annexe type {$uploads['name'][$i]} ($mimeType / $ext), skipping");
continue;
}
// Annexes: PDF max 100 MB, everything else max 500 MB
$isPdf = ($mimeType === 'application/pdf' || $ext === 'pdf');
$sizeLimit = $isPdf ? self::MAX_PDF_SIZE : self::MAX_FILE_SIZE;
if ($uploads['size'][$i] > $sizeLimit) {
$limitMb = round($sizeLimit / 1024 / 1024);
$sizeMb = round($uploads['size'][$i] / 1024 / 1024);
$this->fileWarnings[] = "Annexe « {$uploads['name'][$i]} » ignorée : trop volumineuse ($sizeMb MB, max $limitMb MB).";
error_log("ThesisFileHandler: queue annexe too large {$uploads['name'][$i]} (" . $sizeMb . ' MB), skipping');
continue;
}
$padded = sprintf('%02d', $num);
$targetName = $filePrefix . '_ANNEXE_' . $padded . '.' . $ext;
@@ -530,19 +582,25 @@ trait ThesisFileHandler
$mimeType = 'text/vtt';
}
if ($mimeType === 'application/octet-stream' && !in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
$this->fileWarnings[] = "Annexe « {$uploads['name'][$i]} » ignorée : format .$ext non accepté.";
error_log("ThesisFileHandler: annexe extension not allowed {$uploads['name'][$i]} ($ext), skipping");
continue;
}
if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true)
&& !in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
$this->fileWarnings[] = "Annexe « {$uploads['name'][$i]} » ignorée : type non accepté ($mimeType).";
error_log("ThesisFileHandler: invalid annexe type {$uploads['name'][$i]} ($mimeType / $ext), skipping");
continue;
}
$isPdf = ($mimeType === 'application/pdf' || $ext === 'pdf');
$sizeLimit = $isPdf ? self::MAX_PDF_SIZE : self::MAX_FILE_SIZE;
$isAv = preg_match('/^(video|audio)\//', $mimeType) || in_array($ext, ['mp4','webm','ogv','mov','mp3','ogg','oga','wav','flac','aac','m4a']);
$sizeLimit = $isPdf ? self::MAX_PDF_SIZE : ($isAv ? self::MAX_AV_SIZE : self::MAX_FILE_SIZE);
if ($uploads['size'][$i] > $sizeLimit) {
error_log("ThesisFileHandler: annexe too large {$uploads['name'][$i]} (" . round($uploads['size'][$i] / 1024 / 1024) . " MB), skipping");
$limitMb = round($sizeLimit / 1024 / 1024);
$sizeMb = round($uploads['size'][$i] / 1024 / 1024);
$this->fileWarnings[] = "Annexe « {$uploads['name'][$i]} » ignorée : trop volumineuse ($sizeMb MB, max $limitMb MB).";
error_log("ThesisFileHandler: annexe too large {$uploads['name'][$i]} (" . $sizeMb . " MB), skipping");
continue;
}