mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-07 03:29:19 +02:00
admin edit.php: add cover image + thesis file management fields
- Database: add deleteThesisFile() and handleCoverUpload() methods - ThesisEditController::load(): expose currentFiles + currentCover to view - ThesisEditController::save(): handle couverture upload/removal, per-file deletion (delete_files[]), and new thesis file uploads - edit.php template: new Fichiers fieldset with cover preview+remove, existing files list with delete checkboxes, new file upload input (mirrors add.php / partage.php)
This commit is contained in:
9
TODO.md
9
TODO.md
@@ -1,5 +1,14 @@
|
|||||||
# TFE Access Restriction Feature
|
# TFE Access Restriction Feature
|
||||||
|
|
||||||
|
## Admin Edit Form — File Management
|
||||||
|
- [x] Add cover image upload/preview/remove to edit.php
|
||||||
|
- [x] Add existing thesis files listing with per-file delete checkboxes
|
||||||
|
- [x] Add new thesis files upload field (PDF, JPG, PNG, MP4, ZIP, VTT)
|
||||||
|
- [x] Add `deleteThesisFile()` and `handleCoverUpload()` to Database.php
|
||||||
|
- [x] Update `ThesisEditController::save()` to handle cover, file deletion, new uploads
|
||||||
|
- [x] Update `ThesisEditController::load()` to expose `currentFiles` + `currentCover`
|
||||||
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
Add access restriction for TFE attached files based on user email domain, with admin validation workflow.
|
Add access restriction for TFE attached files based on user email domain, with admin validation workflow.
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ class ThesisEditController
|
|||||||
* - 'currentLanguages' – int[]
|
* - 'currentLanguages' – int[]
|
||||||
* - 'currentFormats' – int[]
|
* - 'currentFormats' – int[]
|
||||||
* - 'jury' – jury rows
|
* - 'jury' – jury rows
|
||||||
|
* - 'currentFiles' – all thesis_files rows (cover + thesis files)
|
||||||
|
* - 'currentCover' – single thesis_files row for cover, or null
|
||||||
* - 'orientations' – lookup rows
|
* - 'orientations' – lookup rows
|
||||||
* - 'apPrograms' – lookup rows
|
* - 'apPrograms' – lookup rows
|
||||||
* - 'finalityTypes' – lookup rows
|
* - 'finalityTypes' – lookup rows
|
||||||
@@ -77,6 +79,16 @@ class ThesisEditController
|
|||||||
$currentLanguages = $this->db->getThesisLanguageIds($thesisId);
|
$currentLanguages = $this->db->getThesisLanguageIds($thesisId);
|
||||||
$currentFormats = $this->db->getThesisFormatIds($thesisId);
|
$currentFormats = $this->db->getThesisFormatIds($thesisId);
|
||||||
$jury = $this->db->getThesisJury($thesisId);
|
$jury = $this->db->getThesisJury($thesisId);
|
||||||
|
$currentFiles = $this->db->getThesisFiles($thesisId);
|
||||||
|
|
||||||
|
// Separate out the cover entry for convenience
|
||||||
|
$currentCover = null;
|
||||||
|
foreach ($currentFiles as $f) {
|
||||||
|
if ($f['file_type'] === 'cover') {
|
||||||
|
$currentCover = $f;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$orientations = $this->db->getAllOrientations();
|
$orientations = $this->db->getAllOrientations();
|
||||||
$apPrograms = $this->db->getAllAPPrograms();
|
$apPrograms = $this->db->getAllAPPrograms();
|
||||||
@@ -100,6 +112,8 @@ class ThesisEditController
|
|||||||
'currentLanguages' => $currentLanguages,
|
'currentLanguages' => $currentLanguages,
|
||||||
'currentFormats' => $currentFormats,
|
'currentFormats' => $currentFormats,
|
||||||
'jury' => $jury,
|
'jury' => $jury,
|
||||||
|
'currentFiles' => $currentFiles,
|
||||||
|
'currentCover' => $currentCover,
|
||||||
'orientations' => $orientations,
|
'orientations' => $orientations,
|
||||||
'apPrograms' => $apPrograms,
|
'apPrograms' => $apPrograms,
|
||||||
'finalityTypes' => $finalityTypes,
|
'finalityTypes' => $finalityTypes,
|
||||||
@@ -230,6 +244,180 @@ class ThesisEditController
|
|||||||
} else {
|
} else {
|
||||||
$this->db->handleBannerUpload($thesisId, $files['banner'] ?? null);
|
$this->db->handleBannerUpload($thesisId, $files['banner'] ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Cover image (outside transaction — filesystem op) ─────────────────
|
||||||
|
if (isset($post['remove_cover'])) {
|
||||||
|
$allFiles = $this->db->getThesisFiles($thesisId);
|
||||||
|
foreach ($allFiles as $f) {
|
||||||
|
if ($f['file_type'] === 'cover') {
|
||||||
|
$this->db->deleteThesisFile((int)$f['id'], $thesisId);
|
||||||
|
if (!empty($f['file_path']) && defined('STORAGE_ROOT')) {
|
||||||
|
$abs = STORAGE_ROOT . '/' . $f['file_path'];
|
||||||
|
if (file_exists($abs)) @unlink($abs);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->db->handleCoverUpload($thesisId, $files['couverture'] ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Delete individual thesis files ────────────────────────────────────
|
||||||
|
$deleteIds = isset($post['delete_files']) && is_array($post['delete_files'])
|
||||||
|
? array_map('intval', $post['delete_files'])
|
||||||
|
: [];
|
||||||
|
foreach ($deleteIds as $fileId) {
|
||||||
|
if ($fileId <= 0) continue;
|
||||||
|
$filePath = $this->db->deleteThesisFile($fileId, $thesisId);
|
||||||
|
if ($filePath && defined('STORAGE_ROOT')) {
|
||||||
|
$abs = STORAGE_ROOT . '/' . $filePath;
|
||||||
|
if (file_exists($abs)) @unlink($abs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── New thesis files upload ───────────────────────────────────────────
|
||||||
|
if (!empty($files['files']['name'][0])) {
|
||||||
|
$this->handleThesisFiles($thesisId, $post, $files['files']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Private: file uploads ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process multiple new thesis-file uploads.
|
||||||
|
*
|
||||||
|
* Files are stored in the existing folder used by this thesis (detected
|
||||||
|
* from any current thesis_files row), or a new one is created following
|
||||||
|
* the same {year}_{authorSlug} convention as ThesisCreateController.
|
||||||
|
*/
|
||||||
|
private function handleThesisFiles(int $thesisId, array $post, array $uploads): void
|
||||||
|
{
|
||||||
|
$allowedMimes = [
|
||||||
|
'image/jpeg', 'image/png', 'application/pdf',
|
||||||
|
'video/mp4', 'application/zip', 'text/vtt',
|
||||||
|
];
|
||||||
|
$allowedExts = ['jpg', 'jpeg', 'png', 'pdf', 'mp4', 'zip', 'vtt'];
|
||||||
|
$maxBytes = 50 * 1024 * 1024; // 50 MB
|
||||||
|
|
||||||
|
$year = (int)($post['année'] ?? date('Y'));
|
||||||
|
$authorName = trim($post['auteurice'] ?? 'unknown');
|
||||||
|
$authorSlug = $this->generateAuthorSlug($authorName);
|
||||||
|
|
||||||
|
// Reuse existing folder if possible
|
||||||
|
$existingFiles = $this->db->getThesisFiles($thesisId);
|
||||||
|
$uploadDir = null;
|
||||||
|
$folderName = null;
|
||||||
|
foreach ($existingFiles as $f) {
|
||||||
|
if (str_starts_with($f['file_path'] ?? '', 'theses/')) {
|
||||||
|
$parts = explode('/', $f['file_path']);
|
||||||
|
if (count($parts) >= 3) {
|
||||||
|
$folderName = $parts[2];
|
||||||
|
$uploadDir = STORAGE_ROOT . "/theses/{$year}/{$folderName}/";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($uploadDir === null) {
|
||||||
|
$folderName = $this->ensureUniqueFolder($year, $authorSlug);
|
||||||
|
$uploadDir = STORAGE_ROOT . "/theses/{$year}/{$folderName}/";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_dir($uploadDir)) {
|
||||||
|
mkdir($uploadDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = count($uploads['name']);
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
if (($uploads['error'][$i] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) continue;
|
||||||
|
if (($uploads['error'][$i] ?? -1) !== UPLOAD_ERR_OK) {
|
||||||
|
error_log("ThesisEditController: upload error {$uploads['error'][$i]} for {$uploads['name'][$i]}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||||
|
$mimeType = $finfo->file($uploads['tmp_name'][$i]);
|
||||||
|
$ext = strtolower(pathinfo($uploads['name'][$i], PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
if ($mimeType === 'text/plain' && $ext === 'vtt') {
|
||||||
|
$mimeType = 'text/vtt';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($mimeType, $allowedMimes, true) || !in_array($ext, $allowedExts, true)) {
|
||||||
|
error_log("ThesisEditController: invalid type {$uploads['name'][$i]} ($mimeType), skipping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($uploads['size'][$i] > $maxBytes) {
|
||||||
|
error_log("ThesisEditController: file too large {$uploads['name'][$i]}, skipping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalName = $uploads['name'][$i];
|
||||||
|
$sanitized = $this->sanitizeFilename($originalName);
|
||||||
|
$prefix = $authorSlug . '_' . $sanitized;
|
||||||
|
$candidate = $prefix;
|
||||||
|
$suffix = 1;
|
||||||
|
while (file_exists($uploadDir . $candidate)) {
|
||||||
|
$candidate = $authorSlug . '_' . pathinfo($sanitized, PATHINFO_FILENAME) . '_' . $suffix . '.' . $ext;
|
||||||
|
$suffix++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetPath = $uploadDir . $candidate;
|
||||||
|
if (!move_uploaded_file($uploads['tmp_name'][$i], $targetPath)) {
|
||||||
|
error_log("ThesisEditController: failed to move {$originalName}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
chmod($targetPath, 0644);
|
||||||
|
|
||||||
|
$fileType = 'other';
|
||||||
|
if ($ext === 'vtt') $fileType = 'caption';
|
||||||
|
elseif (stripos($originalName, 'annex') !== false) $fileType = 'annex';
|
||||||
|
elseif ($ext === 'pdf') $fileType = 'main';
|
||||||
|
|
||||||
|
$relPath = "theses/{$year}/{$folderName}/" . $candidate;
|
||||||
|
$this->db->insertThesisFile($thesisId, $fileType, $relPath, basename($originalName), $uploads['size'][$i], $mimeType);
|
||||||
|
error_log("ThesisEditController: uploaded → $candidate ($fileType)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Private: string helpers ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function generateAuthorSlug(string $authorName): string
|
||||||
|
{
|
||||||
|
$n = function_exists('iconv') ? iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $authorName) : $authorName;
|
||||||
|
$accents = [
|
||||||
|
'à'=>'a','â'=>'a','ä'=>'a','é'=>'e','è'=>'e','ê'=>'e','ë'=>'e',
|
||||||
|
'î'=>'i','ï'=>'i','ô'=>'o','ö'=>'o','ù'=>'u','û'=>'u','ü'=>'u','ç'=>'c',
|
||||||
|
];
|
||||||
|
$n = strtr($n, $accents);
|
||||||
|
$slug = strtoupper(trim(preg_replace('/[^A-Za-z0-9]+/', '_', $n), '_'));
|
||||||
|
return $slug !== '' ? $slug : 'AUTHOR';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sanitizeFilename(string $filename): string
|
||||||
|
{
|
||||||
|
$ext = pathinfo($filename, PATHINFO_EXTENSION);
|
||||||
|
$name = pathinfo($filename, PATHINFO_FILENAME);
|
||||||
|
$n = function_exists('iconv') ? iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $name) : $name;
|
||||||
|
$accents = [
|
||||||
|
'à'=>'a','â'=>'a','ä'=>'a','é'=>'e','è'=>'e','ê'=>'e','ë'=>'e',
|
||||||
|
'î'=>'i','ï'=>'i','ô'=>'o','ö'=>'o','ù'=>'u','û'=>'u','ü'=>'u','ç'=>'c',
|
||||||
|
];
|
||||||
|
$n = trim(preg_replace('/[^A-Za-z0-9]+/', '_', strtr($n, $accents)), '_');
|
||||||
|
if ($n === '') $n = 'file';
|
||||||
|
return $ext !== '' ? $n . '.' . strtolower($ext) : $n;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureUniqueFolder(int $year, string $authorSlug): string
|
||||||
|
{
|
||||||
|
$baseDir = STORAGE_ROOT . '/theses/' . $year . '/';
|
||||||
|
$candidate = $year . '_' . $authorSlug;
|
||||||
|
$suffix = 1;
|
||||||
|
while (is_dir($baseDir . $candidate)) {
|
||||||
|
$candidate = $year . '_' . $authorSlug . '_' . $suffix++;
|
||||||
|
}
|
||||||
|
return $candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── WCAG 3.3.1 helper ─────────────────────────────────────────────────────
|
// ── WCAG 3.3.1 helper ─────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1744,6 +1744,95 @@ class Database {
|
|||||||
return $this->pdo->lastInsertId();
|
return $this->pdo->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a single thesis file record by its ID and optionally remove the
|
||||||
|
* file from disk. Returns the file_path that was deleted (or null if not
|
||||||
|
* found), so the caller can clean up the filesystem.
|
||||||
|
*
|
||||||
|
* @param int $fileId Primary key of thesis_files row.
|
||||||
|
* @param int $thesisId Owning thesis ID (used as a safety guard).
|
||||||
|
* @return string|null The file_path that was stored, or null.
|
||||||
|
*/
|
||||||
|
public function deleteThesisFile(int $fileId, int $thesisId): ?string
|
||||||
|
{
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
"SELECT file_path FROM thesis_files WHERE id = ? AND thesis_id = ? LIMIT 1"
|
||||||
|
);
|
||||||
|
$stmt->execute([$fileId, $thesisId]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
if (!$row) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$this->pdo->prepare("DELETE FROM thesis_files WHERE id = ?")->execute([$fileId]);
|
||||||
|
return $row['file_path'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the cover image for a thesis: removes any existing cover record
|
||||||
|
* (and its file from disk), then inserts the new one.
|
||||||
|
*
|
||||||
|
* @param int $thesisId
|
||||||
|
* @param array|null $upload Single-file $_FILES entry.
|
||||||
|
* @return string|null Relative path of the new cover, or null.
|
||||||
|
*/
|
||||||
|
public function handleCoverUpload(int $thesisId, ?array $upload): ?string
|
||||||
|
{
|
||||||
|
if (!$upload || ($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedMimes = ['image/jpeg', 'image/png'];
|
||||||
|
$allowedExts = ['jpg', 'jpeg', 'png'];
|
||||||
|
$maxBytes = 10 * 1024 * 1024; // 10 MB
|
||||||
|
|
||||||
|
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||||
|
$mimeType = $finfo->file($upload['tmp_name']);
|
||||||
|
$ext = strtolower(pathinfo($upload['name'], PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
if (!in_array($mimeType, $allowedMimes, true)
|
||||||
|
|| !in_array($ext, $allowedExts, true)
|
||||||
|
|| $upload['size'] > $maxBytes) {
|
||||||
|
error_log("handleCoverUpload: rejected {$upload['name']} ($mimeType, {$upload['size']} bytes)");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove existing cover record + file
|
||||||
|
$existing = $this->pdo->prepare(
|
||||||
|
"SELECT id, file_path FROM thesis_files WHERE thesis_id = ? AND file_type = 'cover' LIMIT 1"
|
||||||
|
);
|
||||||
|
$existing->execute([$thesisId]);
|
||||||
|
if ($old = $existing->fetch()) {
|
||||||
|
$this->pdo->prepare("DELETE FROM thesis_files WHERE id = ?")->execute([$old['id']]);
|
||||||
|
if (!empty($old['file_path']) && defined('STORAGE_ROOT')) {
|
||||||
|
$abs = STORAGE_ROOT . '/' . $old['file_path'];
|
||||||
|
if (file_exists($abs)) @unlink($abs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$coverDir = defined('STORAGE_ROOT') ? STORAGE_ROOT . '/covers/' : null;
|
||||||
|
if (!$coverDir) {
|
||||||
|
error_log('handleCoverUpload: STORAGE_ROOT not defined');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!is_dir($coverDir)) {
|
||||||
|
mkdir($coverDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$safeName = bin2hex(random_bytes(16)) . '.' . $ext;
|
||||||
|
$targetPath = $coverDir . $safeName;
|
||||||
|
|
||||||
|
if (!move_uploaded_file($upload['tmp_name'], $targetPath)) {
|
||||||
|
error_log("handleCoverUpload: move_uploaded_file failed for {$upload['name']}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
chmod($targetPath, 0644);
|
||||||
|
$relPath = 'covers/' . $safeName;
|
||||||
|
$this->insertThesisFile($thesisId, 'cover', $relPath, basename($upload['name']), $upload['size'], $mimeType);
|
||||||
|
error_log("handleCoverUpload: saved $relPath");
|
||||||
|
return $relPath;
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// EXPORT HELPERS — used by ExportController
|
// EXPORT HELPERS — used by ExportController
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|||||||
@@ -95,6 +95,60 @@
|
|||||||
|
|
||||||
<?php $name = 'lien'; $label = 'Lien externe :'; $value = htmlspecialchars($thesis['baiu_link'] ?? ''); $type = 'url'; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
<?php $name = 'lien'; $label = 'Lien externe :'; $value = htmlspecialchars($thesis['baiu_link'] ?? ''); $type = 'url'; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||||
|
|
||||||
|
<!-- Fichiers -->
|
||||||
|
<fieldset>
|
||||||
|
<legend>Fichiers</legend>
|
||||||
|
|
||||||
|
<!-- Cover image -->
|
||||||
|
<div>
|
||||||
|
<label>Image de couverture :</label>
|
||||||
|
<div>
|
||||||
|
<?php if (!empty($currentCover)): ?>
|
||||||
|
<div class="admin-banner-preview">
|
||||||
|
<img src="/media.php?path=<?= urlencode($currentCover['file_path']) ?>"
|
||||||
|
alt="Couverture actuelle" style="max-height:180px;">
|
||||||
|
<label class="admin-checkbox-label">
|
||||||
|
<input type="checkbox" name="remove_cover" value="1"> Supprimer la couverture
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php $name = 'couverture'; $label = empty($currentCover) ? 'Image de couverture :' : 'Remplacer la couverture :'; $accept = 'image/jpeg,image/png'; $hint = 'JPG, PNG. Max 10 MB.'; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Existing thesis files -->
|
||||||
|
<?php
|
||||||
|
$thesisFilesList = array_filter($currentFiles, fn($f) => $f['file_type'] !== 'cover');
|
||||||
|
?>
|
||||||
|
<?php if (!empty($thesisFilesList)): ?>
|
||||||
|
<div class="admin-form-group">
|
||||||
|
<label>Fichiers existants :</label>
|
||||||
|
<ul class="admin-file-list">
|
||||||
|
<?php foreach ($thesisFilesList as $f): ?>
|
||||||
|
<li class="admin-file-list-item">
|
||||||
|
<span class="admin-file-info">
|
||||||
|
<span class="admin-file-type">[<?= htmlspecialchars($f['file_type']) ?>]</span>
|
||||||
|
<a href="/media.php?path=<?= urlencode($f['file_path']) ?>" target="_blank" rel="noopener">
|
||||||
|
<?= htmlspecialchars($f['file_name'] ?? basename($f['file_path'])) ?>
|
||||||
|
</a>
|
||||||
|
<?php if (!empty($f['file_size'])): ?>
|
||||||
|
<small>(<?= number_format($f['file_size'] / 1024 / 1024, 2) ?> MB)</small>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
<label class="admin-checkbox-label admin-file-delete">
|
||||||
|
<input type="checkbox" name="delete_files[]" value="<?= (int)$f['id'] ?>">
|
||||||
|
Supprimer
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- New thesis files -->
|
||||||
|
<?php $name = 'files'; $label = 'Ajouter des fichiers :'; $accept = '.pdf,.jpg,.jpeg,.png,.mp4,.zip,.vtt'; $hint = 'PDF, JPG, PNG, MP4, ZIP. Max 50 MB par fichier. Pour les vidéos, un fichier .vtt peut être joint.'; $multiple = true; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<!-- Image bannière (custom: includes current banner preview + remove checkbox) -->
|
<!-- Image bannière (custom: includes current banner preview + remove checkbox) -->
|
||||||
<div>
|
<div>
|
||||||
<label>Image bannière (accueil) :</label>
|
<label>Image bannière (accueil) :</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user