mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 11:09:18 +02:00
- Add DuplicateThesisException (typed, carries existing thesis metadata) - Add Database::findDuplicateThesis(): matches on year + author + normalised title (exact, prefix, Levenshtein ≤10% of longer string) - ThesisCreateController::submit() runs duplicate check before any DB write and throws DuplicateThesisException on match - AppLogger::logDuplicate() writes status=duplicate entries to the JSON-lines log for audit purposes - App::flash/consumeFlash extended to support 'warning' flash type - admin/actions/formulaire.php: catches DuplicateThesisException, logs it, flashes an HTML warning toast with a clickable link to the existing thesis, and repopulates the form fields - partage/index.php: same catch block; surfaces a plain-text flash-warning banner on the student form with identifier, title, and year of the match; form is repopulated via session - toast.php: renders toast--warning variant - admin.css: .toast--warning + link colour rules - form.css: .flash-warning style for the partage form
567 lines
24 KiB
PHP
567 lines
24 KiB
PHP
<?php
|
||
|
||
/**
|
||
* ThesisEditController
|
||
*
|
||
* Centralises all data-fetching and mutation logic for the admin thesis-edit
|
||
* workflow (admin/edit.php + admin/actions/edit.php).
|
||
*
|
||
* Responsibilities:
|
||
* - Loading thesis data and lookup tables for the edit form view
|
||
* - Validating and persisting POST submissions (thesis metadata, authors,
|
||
* jury, languages, formats, tags, banner)
|
||
* - WCAG 3.3.1: mapping validation exceptions to autofocus field hints
|
||
*
|
||
* The class has NO output side-effects; all redirects, flash writes, and
|
||
* template rendering stay in the thin dispatcher files so the view layer
|
||
* remains easy to inspect and modify.
|
||
*/
|
||
class ThesisEditController
|
||
{
|
||
private Database $db;
|
||
|
||
public function __construct(Database $db)
|
||
{
|
||
$this->db = $db;
|
||
}
|
||
|
||
// ── Factory ───────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Convenience factory — instantiates Database and returns a ready
|
||
* controller. Accepts an optional existing Database instance so callers
|
||
* that already hold one (e.g. during testing) can avoid a second
|
||
* connection.
|
||
*/
|
||
public static function create(?Database $db = null): self
|
||
{
|
||
require_once APP_ROOT . '/src/Database.php';
|
||
|
||
return new self($db ?? Database::getInstance());
|
||
}
|
||
|
||
// ── Read / view data ─────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Load all data required to render the edit form.
|
||
*
|
||
* Returns a flat array of view variables:
|
||
* - 'thesis' – thesis row (from getThesis)
|
||
* - 'currentLanguages' – int[]
|
||
* - 'currentFormats' – int[]
|
||
* - 'jury' – jury rows
|
||
* - 'currentFiles' – all thesis_files rows (cover + thesis files)
|
||
* - 'currentCover' – single thesis_files row for cover, or null
|
||
* - 'orientations' – lookup rows
|
||
* - 'apPrograms' – lookup rows
|
||
* - 'finalityTypes' – lookup rows
|
||
* - 'languages' – lookup rows
|
||
* - 'formatTypes' – lookup rows
|
||
* - 'licenseTypes' – lookup rows
|
||
* - 'accessTypes' – lookup rows
|
||
* - 'currentLicenseId' – int|null
|
||
* - 'currentAccessTypeId'– int|null
|
||
* - 'currentContextNote' – string
|
||
* - 'pageTitle' – string
|
||
*
|
||
* @throws Exception if the thesis is not found or a DB error occurs.
|
||
*/
|
||
public function load(int $thesisId): array
|
||
{
|
||
if ($thesisId <= 0) {
|
||
throw new InvalidArgumentException('ID invalide');
|
||
}
|
||
|
||
$thesis = $this->db->getThesis($thesisId);
|
||
if (!$thesis) {
|
||
throw new RuntimeException('TFE non trouvé');
|
||
}
|
||
|
||
$currentLanguages = $this->db->getThesisLanguageIds($thesisId);
|
||
$currentFormats = $this->db->getThesisFormatIds($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();
|
||
$apPrograms = $this->db->getAllAPPrograms();
|
||
$finalityTypes = $this->db->getAllFinalityTypes();
|
||
$languages = $this->db->getAllLanguages();
|
||
$formatTypes = $this->db->getAllFormatTypes();
|
||
$licenseTypes = $this->db->getAllLicenseTypes();
|
||
$enabledAccessTypes = $this->db->getEnabledFormAccessTypes();
|
||
|
||
$rawRow = $this->db->getThesisRawFields($thesisId);
|
||
$currentLicenseId = $rawRow['license_id'] ?? null;
|
||
$currentAccessTypeId = $rawRow['access_type_id'] ?? null;
|
||
$currentContextNote = $rawRow['context_note'] ?? '';
|
||
|
||
// Author contact info (from view)
|
||
$currentAuthorEmail = $thesis['author_email'] ?? '';
|
||
$currentAuthorShowContact = (bool)($thesis['author_show_contact'] ?? false);
|
||
|
||
return [
|
||
'thesis' => $thesis,
|
||
'currentLanguages' => $currentLanguages,
|
||
'currentFormats' => $currentFormats,
|
||
'jury' => $jury,
|
||
'currentFiles' => $currentFiles,
|
||
'currentCover' => $currentCover,
|
||
'orientations' => $orientations,
|
||
'apPrograms' => $apPrograms,
|
||
'finalityTypes' => $finalityTypes,
|
||
'languages' => $languages,
|
||
'formatTypes' => $formatTypes,
|
||
'licenseTypes' => $licenseTypes,
|
||
'enabledAccessTypes' => $enabledAccessTypes,
|
||
'currentLicenseId' => $currentLicenseId,
|
||
'currentAccessTypeId' => $currentAccessTypeId,
|
||
'currentContextNote' => $currentContextNote,
|
||
'currentAuthorEmail' => $currentAuthorEmail,
|
||
'currentAuthorShowContact' => $currentAuthorShowContact,
|
||
'pageTitle' => 'Éditer TFE - ' . htmlspecialchars($thesis['title']),
|
||
];
|
||
}
|
||
|
||
// ── Write / action ────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Validate and persist a thesis-edit POST submission.
|
||
*
|
||
* Runs the full update inside a transaction:
|
||
* 1. Thesis metadata (title, subtitle, year, orientation, ap, finality,
|
||
* synopsis, context_note, file_size_info, baiu_link, license_id,
|
||
* access_type_id, is_published)
|
||
* 2. Authors (setThesisAuthors)
|
||
* 3. Jury (setThesisJury)
|
||
* 4. Languages (setThesisLanguages)
|
||
* 5. Formats (setThesisFormats)
|
||
* 6. Tags (setThesisTags)
|
||
* Then handles banner upload/removal outside the transaction.
|
||
*
|
||
* @param int $thesisId Validated thesis ID (> 0).
|
||
* @param array $post Sanitised $_POST array.
|
||
* @param array $files $_FILES array (expects 'banner' key).
|
||
*
|
||
* @throws Exception on validation or DB error (caller must rollback if
|
||
* the transaction is still open, but this method rolls
|
||
* back internally before re-throwing).
|
||
*/
|
||
public function save(int $thesisId, array $post, array $files): void
|
||
{
|
||
if ($thesisId <= 0) {
|
||
throw new InvalidArgumentException('ID de TFE invalide.');
|
||
}
|
||
|
||
$this->db->beginTransaction();
|
||
|
||
try {
|
||
// ── 1. Thesis metadata ────────────────────────────────────────────
|
||
$this->db->updateThesis($thesisId, [
|
||
'title' => trim($post['titre'] ?? ''),
|
||
'subtitle' => trim($post['subtitle'] ?? ''),
|
||
'year' => intval($post['année'] ?? 0),
|
||
'orientation_id' => intval($post['orientation'] ?? 0),
|
||
'ap_program_id' => intval($post['ap'] ?? 0),
|
||
'finality_id' => intval($post['finality'] ?? 0),
|
||
'synopsis' => trim($post['synopsis'] ?? ''),
|
||
'context_note' => trim($post['context_note'] ?? ''),
|
||
'file_size_info' => trim($post['duration_info'] ?? ''),
|
||
'baiu_link' => trim($post['lien'] ?? ''),
|
||
'license_id' => filter_var($post['license_id'] ?? '', FILTER_VALIDATE_INT) ?: null,
|
||
'access_type_id' => filter_var($post['access_type_id'] ?? '', FILTER_VALIDATE_INT) ?: null,
|
||
'is_published' => isset($post['is_published']),
|
||
]);
|
||
|
||
// ── 2. Authors ────────────────────────────────────────────────────
|
||
$authorsRaw = trim($post['auteurice'] ?? '');
|
||
$showContact = !empty($post['contact_public']);
|
||
$authorEntries = [];
|
||
if ($authorsRaw !== '') {
|
||
foreach (array_map('trim', explode(',', $authorsRaw)) as $i => $name) {
|
||
if ($name !== '') {
|
||
$authorEntries[] = [
|
||
'name' => $name,
|
||
'email' => $i === 0 ? ($post['mail'] ?? null) : null,
|
||
'show_contact' => $i === 0 ? $showContact : false,
|
||
];
|
||
}
|
||
}
|
||
}
|
||
$this->db->setThesisAuthors($thesisId, $authorEntries);
|
||
|
||
// ── 3. Jury ───────────────────────────────────────────────────────
|
||
$juryMembers = $this->collectJuryMembers($post);
|
||
$this->db->setThesisJury($thesisId, $juryMembers);
|
||
|
||
// ── 4. Languages ──────────────────────────────────────────────────
|
||
$this->db->setThesisLanguages(
|
||
$thesisId,
|
||
isset($post['languages']) && is_array($post['languages'])
|
||
? $post['languages']
|
||
: []
|
||
);
|
||
|
||
// ── 5. Formats ────────────────────────────────────────────────────
|
||
$this->db->setThesisFormats(
|
||
$thesisId,
|
||
isset($post['formats']) && is_array($post['formats'])
|
||
? $post['formats']
|
||
: []
|
||
);
|
||
|
||
// ── 6. Tags ───────────────────────────────────────────────────────
|
||
$keywordsRaw = trim($post['tag'] ?? '');
|
||
$keywords = $keywordsRaw !== ''
|
||
? array_map('trim', explode(',', $keywordsRaw))
|
||
: [];
|
||
$this->db->setThesisTags($thesisId, $keywords);
|
||
|
||
$this->db->commit();
|
||
|
||
} catch (Exception $e) {
|
||
$this->db->rollback();
|
||
throw $e;
|
||
}
|
||
|
||
// ── Banner (outside transaction — filesystem op) ──────────────────────
|
||
if (isset($post['remove_banner'])) {
|
||
$currentBannerPath = $this->db->getThesisBannerPath($thesisId);
|
||
if ($currentBannerPath && defined('STORAGE_ROOT')) {
|
||
$absPath = STORAGE_ROOT . '/' . $currentBannerPath;
|
||
if (file_exists($absPath)) {
|
||
unlink($absPath);
|
||
}
|
||
}
|
||
$this->db->setBannerPath($thesisId, null);
|
||
} else {
|
||
$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);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Reorder existing files ────────────────────────────────────────────
|
||
if (!empty($post['file_sort_order']) && is_array($post['file_sort_order'])) {
|
||
$this->db->reorderThesisFiles($thesisId, $post['file_sort_order']);
|
||
}
|
||
|
||
// ── Update display labels for existing files ──────────────────────────
|
||
if (!empty($post['file_label']) && is_array($post['file_label'])) {
|
||
foreach ($post['file_label'] as $fileId => $label) {
|
||
$fileId = (int)$fileId;
|
||
if ($fileId <= 0) {
|
||
continue;
|
||
}
|
||
$this->db->updateThesisFileLabel($fileId, $thesisId, trim($label) ?: null);
|
||
}
|
||
}
|
||
|
||
// ── 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', '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',
|
||
];
|
||
$allowedExts = [
|
||
'jpg', 'jpeg', 'png', 'gif', 'webp',
|
||
'pdf',
|
||
'mp4', 'webm', 'ogv', 'mov',
|
||
'mp3', 'ogg', 'oga', 'wav', 'flac', 'aac', 'm4a',
|
||
'vtt',
|
||
'zip', 'tar', 'gz', 'tgz',
|
||
];
|
||
$maxBytes = 500 * 1024 * 1024; // 500 MB
|
||
|
||
$year = (int)($post['année'] ?? date('Y'));
|
||
$authorName = trim($post['auteurice'] ?? 'unknown');
|
||
$authorSlug = $this->generateAuthorSlug($authorName);
|
||
|
||
// Per-file labels and sort orders submitted alongside the upload inputs
|
||
$fileLabels = $post['file_labels'] ?? [];
|
||
$fileOrders = $post['file_orders'] ?? [];
|
||
|
||
// 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';
|
||
}
|
||
|
||
// Allow any ext-matched file even if finfo returns application/octet-stream
|
||
if (!in_array($mimeType, $allowedMimes, true) && !in_array($ext, $allowedExts, true)) {
|
||
error_log("ThesisEditController: invalid type {$uploads['name'][$i]} ($mimeType / $ext), 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 = $this->detectFileType($mimeType, $ext, $originalName);
|
||
$label = trim($fileLabels[$i] ?? '');
|
||
$sortOrder = isset($fileOrders[$i]) ? (int)$fileOrders[$i] : null;
|
||
|
||
$relPath = "theses/{$year}/{$folderName}/" . $candidate;
|
||
$this->db->insertThesisFile(
|
||
$thesisId,
|
||
$fileType,
|
||
$relPath,
|
||
basename($originalName),
|
||
$uploads['size'][$i],
|
||
$mimeType,
|
||
$label !== '' ? $label : null,
|
||
$sortOrder
|
||
);
|
||
error_log("ThesisEditController: uploaded → $candidate ($fileType)");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Determine the logical file_type from MIME type, extension, and original filename.
|
||
*/
|
||
private function detectFileType(string $mimeType, string $ext, string $originalName): string
|
||
{
|
||
if ($ext === 'vtt' || $mimeType === 'text/vtt') {
|
||
return 'caption';
|
||
}
|
||
if (str_starts_with($mimeType, 'audio/') || in_array($ext, ['mp3','ogg','oga','wav','flac','aac','m4a'], true)) {
|
||
return 'audio';
|
||
}
|
||
if (str_starts_with($mimeType, 'video/') || in_array($ext, ['mp4','webm','ogv','mov'], true)) {
|
||
return 'video';
|
||
}
|
||
if ($mimeType === 'application/pdf' || $ext === 'pdf') {
|
||
return 'main';
|
||
}
|
||
if (str_starts_with($mimeType, 'image/') || in_array($ext, ['jpg','jpeg','png','gif','webp'], true)) {
|
||
return 'image';
|
||
}
|
||
return 'other';
|
||
}
|
||
|
||
// ── 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 ─────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Map a validation exception message to the name of the field that should
|
||
* receive autofocus when the form is re-rendered.
|
||
*
|
||
* Returns null when no field mapping is found.
|
||
*/
|
||
public static function autofocusFieldForError(string $message): ?string
|
||
{
|
||
if (str_contains($message, 'titre') || str_contains($message, 'Titre')) {
|
||
return 'titre';
|
||
}
|
||
if (str_contains($message, 'année') || str_contains($message, 'Année')) {
|
||
return 'année';
|
||
}
|
||
if (str_contains($message, 'synopsis') || str_contains($message, 'Synopsis')) {
|
||
return 'synopsis';
|
||
}
|
||
if (str_contains($message, 'auteur') || str_contains($message, 'Auteur')) {
|
||
return 'auteurice';
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// ── Private helpers ───────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Build the jury-members array from POST data.
|
||
*
|
||
* @param array $post Raw $_POST.
|
||
* @return array<int, array{name: string, role: string, is_external: int}>
|
||
*/
|
||
private function collectJuryMembers(array $post): array
|
||
{
|
||
$members = [];
|
||
|
||
if (!empty(trim($post['jury_president'] ?? ''))) {
|
||
$members[] = [
|
||
'name' => trim($post['jury_president']),
|
||
'role' => 'president',
|
||
'is_external' => 0,
|
||
];
|
||
}
|
||
|
||
if (!empty(trim($post['jury_promoteur'] ?? ''))) {
|
||
$members[] = [
|
||
'name' => trim($post['jury_promoteur']),
|
||
'role' => 'promoteur',
|
||
'is_external' => isset($post['jury_promoteur_ext']) ? 1 : 0,
|
||
];
|
||
}
|
||
|
||
foreach ($post['jury_lecteurs'] ?? [] as $i => $name) {
|
||
$name = trim($name);
|
||
if ($name !== '') {
|
||
$members[] = [
|
||
'name' => $name,
|
||
'role' => 'lecteur',
|
||
'is_external' => isset($post['jury_lecteurs_ext'][$i]) ? 1 : 0,
|
||
];
|
||
}
|
||
}
|
||
|
||
return $members;
|
||
}
|
||
}
|