mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-26 00:29:18 +02:00
fix: req annexes, add HTMX inline file validation (MIME/size)
- Annexes file input now required when 'has_annexes' checkbox is checked
- PHP-side validation: if has_annexes but no files, throw error
- HTMX inline file validation: POSTs to validate-file-fragment on file change
- Validates MIME type against per-field whitelists (couverture, note_intention,
tfe, annexes)
- Validates file size with PDF-specific 100MB limit
- Supports both single-file and multi-file inputs
- Returns green ✓ or red ✕ inline validation messages
- Shared validation logic in src/Controllers/validate-file-fragment-shared.php
- Admin wrapper: admin/validate-file-fragment.php (with AdminAuth guard)
- Partage route: /partage/validate-file-fragment (dispatched via index.php)
- CSS: .file-validation-msg, .fv-ok (green), .fv-error (red)
- file-field.php: accepts $fieldName for per-input validation type,
auto-detects admin/partage validate URL
This commit is contained in:
@@ -512,6 +512,12 @@ class ThesisCreateController
|
||||
$exemplaireErg = !empty($post['exemplaire_erg']);
|
||||
$cc2r = !empty($post['cc2r']);
|
||||
|
||||
// Annexes validation: if has_annexes is checked, at least one annexe file must be provided
|
||||
$hasAnnexes = !empty($post['has_annexes']);
|
||||
if (!$adminMode && $hasAnnexes && empty($_FILES['annexes']['name'][0])) {
|
||||
throw new Exception('Veuillez fournir au moins un fichier d\'annexe.');
|
||||
}
|
||||
|
||||
return compact(
|
||||
'authorNames',
|
||||
'mail',
|
||||
|
||||
191
app/src/Controllers/validate-file-fragment-shared.php
Normal file
191
app/src/Controllers/validate-file-fragment-shared.php
Normal file
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
/**
|
||||
* validate-file-fragment.php
|
||||
*
|
||||
* HTMX fragment: validates a single uploaded file against MIME type and size
|
||||
* constraints. Returns an inline error message or clears the file input wrapper.
|
||||
*
|
||||
* Expected POST (multipart/form-data):
|
||||
* file — the uploaded file
|
||||
* field_name — 'couverture' | 'note_intention' | 'tfe' | 'annexes'
|
||||
* admin_mode — '1' to skip validation (admins can upload anything)
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../bootstrap.php';
|
||||
|
||||
$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.
|
||||
$rawFile = $_FILES[$fieldName] ?? null;
|
||||
if ($rawFile && is_array($rawFile['name'] ?? null)) {
|
||||
// Multi-file: flatten first entry
|
||||
$file = [
|
||||
'name' => $rawFile['name'][0] ?? null,
|
||||
'tmp_name' => $rawFile['tmp_name'][0] ?? null,
|
||||
'error' => $rawFile['error'][0] ?? UPLOAD_ERR_NO_FILE,
|
||||
'size' => $rawFile['size'][0] ?? 0,
|
||||
];
|
||||
// Validate ALL selected files (batch validation)
|
||||
$allFiles = [];
|
||||
$count = count($rawFile['name'] ?? []);
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$allFiles[] = [
|
||||
'name' => $rawFile['name'][$i] ?? null,
|
||||
'tmp_name' => $rawFile['tmp_name'][$i] ?? null,
|
||||
'error' => $rawFile['error'][$i] ?? UPLOAD_ERR_NO_FILE,
|
||||
'size' => $rawFile['size'][$i] ?? 0,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$file = $rawFile;
|
||||
$allFiles = $file ? [$file] : [];
|
||||
}
|
||||
|
||||
// ── No file provided — clear any existing validation state ──────────────────
|
||||
if (empty($allFiles)) {
|
||||
echo '';
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($adminMode) {
|
||||
// Admins: no validation, always OK
|
||||
$count = count($allFiles);
|
||||
echo '<span class="fv-ok">✓ ' . $count . ' fichier' . ($count > 1 ? 's' : '') . ' accepté' . ($count > 1 ? 's' : '') . '</span>';
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── MIME + extension whitelist per field type ────────────────────────────────
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
|
||||
$constraints = match ($fieldName) {
|
||||
'couverture' => [
|
||||
'mimes' => ['image/jpeg', 'image/png', 'image/webp'],
|
||||
'exts' => ['jpg', 'jpeg', 'png', 'webp'],
|
||||
'maxSize' => 20 * 1024 * 1024, // 20 MB
|
||||
'label' => 'Image de couverture',
|
||||
'allowedDesc' => 'JPG, PNG ou WEBP',
|
||||
'maxSizeDesc' => '20 MB',
|
||||
],
|
||||
'note_intention' => [
|
||||
'mimes' => ['application/pdf'],
|
||||
'exts' => ['pdf'],
|
||||
'maxSize' => 100 * 1024 * 1024, // 100 MB
|
||||
'label' => 'Note d\'intention',
|
||||
'allowedDesc' => 'PDF uniquement',
|
||||
'maxSizeDesc' => '100 MB',
|
||||
],
|
||||
'tfe' => [
|
||||
'mimes' => [
|
||||
'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',
|
||||
],
|
||||
'exts' => [
|
||||
'jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf',
|
||||
'mp4', 'webm', 'ogv', 'mov',
|
||||
'mp3', 'ogg', 'oga', 'wav', 'flac', 'aac', 'm4a',
|
||||
'vtt', 'zip', 'tar', 'gz', 'tgz',
|
||||
],
|
||||
'maxSize' => 500 * 1024 * 1024, // 500 MB
|
||||
'label' => 'Fichier TFE',
|
||||
'allowedDesc' => 'PDF, images, vidéos, audio, archives',
|
||||
'maxSizeDesc' => '500 MB',
|
||||
],
|
||||
'annexes' => [
|
||||
'mimes' => [
|
||||
'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',
|
||||
],
|
||||
'exts' => [
|
||||
'jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf',
|
||||
'mp4', 'webm', 'ogv', 'mov',
|
||||
'mp3', 'ogg', 'oga', 'wav', 'flac', 'aac', 'm4a',
|
||||
'vtt', 'zip', 'tar', 'gz', 'tgz',
|
||||
],
|
||||
'maxSize' => 500 * 1024 * 1024, // 500 MB
|
||||
'label' => 'Annexe',
|
||||
'allowedDesc' => 'PDF, archives, images',
|
||||
'maxSizeDesc' => '500 MB',
|
||||
],
|
||||
default => [
|
||||
'mimes' => [],
|
||||
'exts' => [],
|
||||
'maxSize' => 500 * 1024 * 1024,
|
||||
'label' => 'Fichier',
|
||||
'allowedDesc' => 'tous types',
|
||||
'maxSizeDesc' => '500 MB',
|
||||
],
|
||||
};
|
||||
|
||||
// ── Validate each file ──────────────────────────────────────────────────────
|
||||
$errors = [];
|
||||
$oks = [];
|
||||
foreach ($allFiles as $idx => $f) {
|
||||
if (($f['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mimeType = $finfo->file($f['tmp_name']);
|
||||
$ext = strtolower(pathinfo($f['name'], PATHINFO_EXTENSION));
|
||||
$size = $f['size'];
|
||||
|
||||
if ($mimeType === 'text/plain' && $ext === 'vtt') {
|
||||
$mimeType = 'text/vtt';
|
||||
}
|
||||
|
||||
// PDF size limit
|
||||
$effMaxSize = $constraints['maxSize'];
|
||||
$effMaxDesc = $constraints['maxSizeDesc'];
|
||||
if ($mimeType === 'application/pdf' || $ext === 'pdf') {
|
||||
$effMaxSize = min($effMaxSize, 100 * 1024 * 1024);
|
||||
$effMaxDesc = '100 MB';
|
||||
}
|
||||
|
||||
// Validate MIME
|
||||
$mimeOk = false;
|
||||
if ($mimeType === 'application/octet-stream' && !in_array($ext, $constraints['exts'], true)) {
|
||||
$mimeOk = false;
|
||||
} elseif (in_array($mimeType, $constraints['mimes'], true)) {
|
||||
$mimeOk = true;
|
||||
} elseif (in_array($ext, $constraints['exts'], true)) {
|
||||
$mimeOk = true;
|
||||
}
|
||||
|
||||
if (!$mimeOk) {
|
||||
$errors[] = '✕ <em>' . htmlspecialchars($f['name']) . '</em> : type non accepté.'
|
||||
. ' Formats acceptés : ' . htmlspecialchars($constraints['allowedDesc']) . '.';
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($size > $effMaxSize) {
|
||||
$mb = round($size / 1024 / 1024, 1);
|
||||
$errors[] = '✕ <em>' . htmlspecialchars($f['name']) . '</em> : fichier trop volumineux ('
|
||||
. $mb . ' MB). Maximum : ' . htmlspecialchars($effMaxDesc) . '.';
|
||||
continue;
|
||||
}
|
||||
|
||||
$oks[] = '✓ <em>' . htmlspecialchars($f['name']) . '</em> : ' . round($size / 1024 / 1024, 1) . ' MB';
|
||||
}
|
||||
|
||||
// ── Output ────────────────────────────────────────────────────────────────────
|
||||
if (!empty($errors)) {
|
||||
echo '<span class="fv-error">' . implode('<br>', $errors) . '</span>';
|
||||
} elseif (!empty($oks)) {
|
||||
echo '<span class="fv-ok">' . implode('<br>', $oks) . '</span>';
|
||||
} else {
|
||||
echo '';
|
||||
}
|
||||
Reference in New Issue
Block a user