mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-08-10 15:21:22 +02:00
- Remove Mo option from duration, keep only pages and minutes - Redesign duration fieldset: separate Pages input + Durée h:m inputs, both can be set together - Fix minutes input visibility: wider inputs (6ch), proper CSS layout - Add has_annexes checkbox to fichiers fragment + DB column + controllers - Display duration on admin backoffice recap page - Display duration on public partage recap page - Update public TFE page for new combined duration format - Migration 041: add duration_pages, duration_minutes, has_annexes columns; migrate data; recreate views
666 lines
30 KiB
PHP
666 lines
30 KiB
PHP
<?php
|
||
|
||
require_once __DIR__ . '/ThesisFileHandler.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
|
||
{
|
||
use ThesisFileHandler;
|
||
|
||
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';
|
||
require_once APP_ROOT . '/src/ErrorHandler.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->getPredefinedLanguages();
|
||
$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'] ?? '';
|
||
$currentContactVisible = $rawRow['contact_visible'] ?? '';
|
||
$currentDurationPages = $rawRow['duration_pages'] ?? null;
|
||
$currentDurationMins = $rawRow['duration_minutes'] ?? null;
|
||
|
||
// Author contact info (from view)
|
||
$contactInterne = $thesis['contact_interne'] ?? '';
|
||
$contactPublic = (bool)($thesis['contact_public'] ?? 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,
|
||
'currentContactVisible' => $currentContactVisible,
|
||
'currentDurationPages' => $currentDurationPages,
|
||
'currentDurationMins' => $currentDurationMins,
|
||
'contactInterne' => $contactInterne,
|
||
'contactPublic' => $contactPublic,
|
||
'currentRaw' => $rawRow,
|
||
'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, 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.');
|
||
}
|
||
|
||
// ── Basic validation (same required fields as create) ──────────────────
|
||
$errors = [];
|
||
$titre = trim($post['titre'] ?? '');
|
||
if ($titre === '') {
|
||
$errors[] = 'Le titre du TFE est requis.';
|
||
}
|
||
$auteurice = trim($post['auteurice'] ?? '');
|
||
if ($auteurice === '') {
|
||
$errors[] = "L'auteur·ice est requis.";
|
||
}
|
||
$synopsis = trim($post['synopsis'] ?? '');
|
||
if ($synopsis === '') {
|
||
$errors[] = 'Le synopsis est requis.';
|
||
}
|
||
$annee = intval($post['année'] ?? 0);
|
||
if ($annee < 2000 || $annee > ((int)date('Y') + 1)) {
|
||
$errors[] = "L'année est invalide.";
|
||
}
|
||
$orientationId = intval($post['orientation'] ?? 0);
|
||
$apProgramId = intval($post['ap'] ?? 0);
|
||
$finalityId = intval($post['finality'] ?? 0);
|
||
|
||
if (!empty($errors)) {
|
||
throw new RuntimeException(implode(' ', $errors));
|
||
}
|
||
|
||
$this->db->beginTransaction();
|
||
|
||
try {
|
||
// ── 1. Thesis metadata ────────────────────────────────────────────
|
||
$meta = [
|
||
'title' => trim($post['titre'] ?? ''),
|
||
'subtitle' => trim($post['subtitle'] ?? ''),
|
||
'year' => intval($post['année'] ?? 0),
|
||
'orientation_id' => ($v = intval($post['orientation'] ?? 0)) > 0 ? $v : null,
|
||
'ap_program_id' => ($v = intval($post['ap'] ?? 0)) > 0 ? $v : null,
|
||
'finality_id' => ($v = intval($post['finality'] ?? 0)) > 0 ? $v : null,
|
||
'synopsis' => trim($post['synopsis'] ?? ''),
|
||
'context_note' => trim($post['context_note'] ?? ''),
|
||
'contact_visible' => trim($post['contact_visible'] ?? ''),
|
||
'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']),
|
||
'remarks' => trim($post['remarks'] ?? ''),
|
||
'jury_points' => $post['jury_points'] ?? null,
|
||
'exemplaire_baiu' => !empty($post['exemplaire_baiu']),
|
||
'exemplaire_erg' => !empty($post['exemplaire_erg']),
|
||
'cc2r' => !empty($post['cc2r']),
|
||
'license_custom' => trim($post['license_custom'] ?? ''),
|
||
'duration_pages' => (isset($post['duration_pages']) && $post['duration_pages'] !== '') ? (int)$post['duration_pages'] : null,
|
||
'duration_minutes' => (isset($post['duration_minutes']) && $post['duration_minutes'] !== '') ? (int)$post['duration_minutes'] : null,
|
||
'has_annexes' => !empty($post['has_annexes']) ? 1 : 0,
|
||
];
|
||
// Regenerate identifier if year changed or if identifier prefix doesn't match year
|
||
$oldThesis = $this->db->getThesis($thesisId);
|
||
$oldYear = (int)($oldThesis['year'] ?? 0);
|
||
$newYear = $meta['year'];
|
||
$oldIdentifier = $oldThesis['identifier'] ?? '';
|
||
$oldIdentifierYear = ($oldIdentifier !== '' && preg_match('/^(\d{4})/', $oldIdentifier, $m)) ? (int)$m[1] : 0;
|
||
if ($newYear >= 2000 && ($newYear !== $oldYear || $oldIdentifierYear !== $newYear)) {
|
||
$newIdentifier = $this->db->generateThesisIdentifier($newYear);
|
||
$meta['identifier'] = $newIdentifier;
|
||
$reason = $newYear !== $oldYear
|
||
? 'Year changed ' . $oldYear . ' → ' . $newYear
|
||
: 'Mismatched identifier ' . $oldIdentifier . ' for year=' . $newYear;
|
||
error_log('[ThesisEdit] ' . $reason . ', new identifier: ' . $newIdentifier);
|
||
}
|
||
|
||
$this->db->updateThesis($thesisId, $meta);
|
||
error_log('[ThesisEdit] Step 1 OK — thesis_id=' . $thesisId);
|
||
|
||
// ── 2. Authors (alphabetically sorted) ─────────────────────────────
|
||
$authorsRaw = trim($post['auteurice'] ?? '');
|
||
// contact_interne = private email of the first author (backoffice field)
|
||
$contactInterne = trim($post['contact_interne'] ?? '');
|
||
$firstAuthorEmail = $contactInterne !== '' ? $contactInterne : null;
|
||
// contact_public: whether to show the public contact on the TFE page
|
||
$showContact = !empty($post['contact_public']);
|
||
$authorNames = [];
|
||
if ($authorsRaw !== '') {
|
||
$authorNames = array_values(array_filter(array_map('trim', explode(',', $authorsRaw)), fn ($n) => $n !== ''));
|
||
sort($authorNames, SORT_NATURAL);
|
||
}
|
||
$authorEntries = [];
|
||
foreach ($authorNames as $i => $name) {
|
||
$authorEntries[] = [
|
||
'name' => $name,
|
||
'email' => $i === 0 ? $firstAuthorEmail : null,
|
||
'show_contact' => $i === 0 && $showContact,
|
||
];
|
||
}
|
||
$this->db->setThesisAuthors($thesisId, $authorEntries);
|
||
error_log('[ThesisEdit] Step 2 OK — authors=' . json_encode($authorNames));
|
||
|
||
// ── 3. Jury ───────────────────────────────────────────────────────
|
||
$juryMembers = $this->collectJuryMembers($post);
|
||
$this->db->setThesisJury($thesisId, $juryMembers);
|
||
error_log('[ThesisEdit] Step 3 OK — jury=' . count($juryMembers));
|
||
|
||
// ── 4. Languages ──────────────────────────────────────────────────
|
||
$langIds = isset($post['languages']) && is_array($post['languages'])
|
||
? $post['languages']
|
||
: [];
|
||
// language_autre: pill-based component sends an array; also handle legacy comma-separated string
|
||
$autreRaw = $post['language_autre'] ?? '';
|
||
if (is_array($autreRaw)) {
|
||
foreach ($autreRaw as $langName) {
|
||
$langName = trim($langName);
|
||
if ($langName !== '') {
|
||
$langIds[] = (string)$this->db->getOrCreateLanguage($langName);
|
||
}
|
||
}
|
||
} elseif (is_string($autreRaw) && trim($autreRaw) !== '') {
|
||
foreach (array_map('trim', explode(',', $autreRaw)) as $langName) {
|
||
if ($langName !== '') {
|
||
$langIds[] = (string)$this->db->getOrCreateLanguage($langName);
|
||
}
|
||
}
|
||
}
|
||
$this->db->setThesisLanguages($thesisId, $langIds);
|
||
error_log('[ThesisEdit] Step 4 OK — languages=' . json_encode($langIds));
|
||
|
||
// ── 5. Formats ────────────────────────────────────────────────────
|
||
$formatIds = isset($post['formats']) && is_array($post['formats'])
|
||
? $post['formats']
|
||
: [];
|
||
$this->db->setThesisFormats($thesisId, $formatIds);
|
||
error_log('[ThesisEdit] Step 5 OK — formats=' . json_encode($formatIds));
|
||
|
||
// ── 6. Tags ───────────────────────────────────────────────────────
|
||
$normalizeTag = fn (string $t): string => strtolower(trim(preg_replace('/\s+/', ' ', $t)));
|
||
$keywords = [];
|
||
if (isset($post['tag']) && is_array($post['tag'])) {
|
||
$keywords = array_values(array_unique(array_map(
|
||
$normalizeTag,
|
||
array_map(fn ($t) => (string)$t, $post['tag'])
|
||
)));
|
||
} else {
|
||
$keywordsRaw = trim($post['tag'] ?? '');
|
||
if ($keywordsRaw !== '') {
|
||
$keywords = array_map($normalizeTag, explode(',', $keywordsRaw));
|
||
}
|
||
}
|
||
$keywords = array_values(array_unique($keywords));
|
||
$keywords = array_filter($keywords, fn ($t) => $t !== '');
|
||
$keywords = array_slice($keywords, 0, 10);
|
||
if (count($keywords) < 1) {
|
||
throw new Exception('Veuillez indiquer au moins 1 mot-clé.');
|
||
}
|
||
$this->db->setThesisTags($thesisId, $keywords);
|
||
error_log('[ThesisEdit] Step 6 OK — tags=' . json_encode($keywords));
|
||
|
||
$this->db->commit();
|
||
error_log('[ThesisEdit] COMMIT OK — thesis_id=' . $thesisId);
|
||
|
||
} catch (Exception $e) {
|
||
ErrorHandler::log('thesis_edit_tx', $e, ['thesis_id' => $thesisId]);
|
||
$this->db->rollback();
|
||
throw $e;
|
||
}
|
||
|
||
// ── Resolve thesis folder path (reuse existing or build new) ────────
|
||
$year = intval($post['année'] ?? date('Y'));
|
||
$title = trim($post['titre'] ?? '');
|
||
$authors = trim($post['auteurice'] ?? '');
|
||
$thesis = $this->db->getThesis($thesisId);
|
||
$objet = $thesis['objet'] ?? 'tfe';
|
||
|
||
$tf = $this->buildThesisFolder($year, $authors, $title, $objet);
|
||
$folderPath = $tf['folderPath'];
|
||
$filePrefix = $tf['filePrefix'];
|
||
|
||
// Reuse existing folder if this thesis already has files on disk
|
||
$existingFiles = $this->db->getThesisFiles($thesisId);
|
||
foreach ($existingFiles as $f) {
|
||
$fp = $f['file_path'] ?? '';
|
||
if (preg_match('#^(theses|documents|tfe|these|frart)/#', $fp)) {
|
||
$parts = explode('/', $fp);
|
||
if (count($parts) >= 3) {
|
||
$parentDir = $parts[0];
|
||
$folderName = $parts[2];
|
||
$folderPath = $parentDir . '/' . $year . '/' . $folderName . '/';
|
||
$filePrefix = $folderName;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Ensure the folder exists
|
||
$dirAbs = STORAGE_ROOT . '/' . $folderPath;
|
||
if (!is_dir($dirAbs)) {
|
||
mkdir($dirAbs, 0755, true);
|
||
}
|
||
|
||
// Determine upload path: FilePond async (JS enabled, hex IDs present)
|
||
// vs. legacy multipart. Defense-in-depth fallback for no-JS scenarios.
|
||
$useFilePond = !empty($post['filepond_mode']) && $this->hasFilePondQueueData($post);
|
||
|
||
// ── Cover image (outside transaction — filesystem op) ─────────────────
|
||
if ($useFilePond) {
|
||
// Delete old cover only if a genuinely new cover was uploaded (hex file_id).
|
||
// Existing cover preserved in FilePond sends its DB integer ID — skip.
|
||
$coverIdRaw = ($post['queue_file']['cover'] ?? null);
|
||
$coverId = is_array($coverIdRaw) ? ($coverIdRaw[0] ?? null) : $coverIdRaw;
|
||
$isNewCover = $coverId !== null && $coverId !== '' && preg_match('/^[a-f0-9]{32}$/', (string)$coverId);
|
||
if ($isNewCover) {
|
||
foreach ($existingFiles as $f) {
|
||
if ($f['file_type'] === 'cover') {
|
||
$this->deleteThesisFileToTrash((int)$f['id'], $thesisId);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
$this->handleFilePondSingleFile($thesisId, $post, 'cover', $folderPath, $filePrefix);
|
||
} elseif (!empty($post['remove_cover'])) {
|
||
foreach ($existingFiles as $f) {
|
||
if ($f['file_type'] === 'cover') {
|
||
$this->deleteThesisFileToTrash((int)$f['id'], $thesisId);
|
||
break;
|
||
}
|
||
}
|
||
} else {
|
||
$this->handleCoverUpload($thesisId, $files['couverture'] ?? null, $folderPath, $filePrefix);
|
||
}
|
||
|
||
// ── Note d'intention (replace if uploaded) ────────────────────────────
|
||
if ($useFilePond) {
|
||
// Only delete + replace if a genuinely new file was uploaded (hex file_id).
|
||
// Existing files preserved in the FilePond pool send their DB integer ID;
|
||
// we must NOT delete them — they're already stored.
|
||
$noteIdRaw = ($post['queue_file']['note_intention'] ?? null);
|
||
$noteId = is_array($noteIdRaw) ? ($noteIdRaw[0] ?? null) : $noteIdRaw;
|
||
$isNewNote = $noteId !== null && $noteId !== '' && preg_match('/^[a-f0-9]{32}$/', (string)$noteId);
|
||
if ($isNewNote) {
|
||
foreach ($existingFiles as $f) {
|
||
if ($f['file_type'] === 'note_intention') {
|
||
$this->deleteThesisFileToTrash((int)$f['id'], $thesisId);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
$this->handleFilePondSingleFile($thesisId, $post, 'note_intention', $folderPath, $filePrefix);
|
||
} else {
|
||
// Legacy path
|
||
if (!empty($files['note_intention']['tmp_name'] ?? null) && ($files['note_intention']['error'] ?? -1) === UPLOAD_ERR_OK) {
|
||
foreach ($existingFiles as $f) {
|
||
if ($f['file_type'] === 'note_intention') {
|
||
$this->deleteThesisFileToTrash((int)$f['id'], $thesisId);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
$this->handleNoteIntentionUpload($thesisId, $files['note_intention'] ?? null, $folderPath, $filePrefix);
|
||
}
|
||
|
||
// ── 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;
|
||
}
|
||
$this->deleteThesisFileToTrash($fileId, $thesisId);
|
||
}
|
||
|
||
// ── 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 TFE/video/audio files upload (choose path based on filepond_mode)
|
||
// Count existing TFE files (not cover, note_intention, website, annex, caption, PeerTube)
|
||
$tfeCount = 0;
|
||
foreach ($existingFiles as $f) {
|
||
if (!in_array($f['file_type'] ?? '', ['cover', 'note_intention', 'website', 'annex', 'caption'], true)
|
||
&& !str_starts_with($f['file_path'] ?? '', 'http')
|
||
&& !str_starts_with($f['file_path'] ?? '', 'peertube_ids:')) {
|
||
$tfeCount++;
|
||
}
|
||
}
|
||
|
||
if ($useFilePond) {
|
||
// New path: files already on server via async FilePond uploads
|
||
$nextNum = $tfeCount + 1;
|
||
$nextNum = $this->handleFilePondQueueFiles($thesisId, $post, 'tfe', $folderPath, $filePrefix, $nextNum);
|
||
$this->handleFilePondQueueFiles($thesisId, $post, 'annexe', $folderPath, $filePrefix, 0);
|
||
} else {
|
||
// Legacy path: files arrive via multipart $_FILES
|
||
$queueFiles = $files['queue_file'] ?? [];
|
||
$qTfe = $this->extractFilesSubArray($queueFiles, 'tfe');
|
||
$qAnnexe = $this->extractFilesSubArray($queueFiles, 'annexe');
|
||
|
||
$startNum = $tfeCount + 1;
|
||
$startNum = $this->handleTfeQueueFiles($thesisId, $qTfe, $folderPath, $filePrefix, $startNum);
|
||
$this->handleAnnexeQueueFiles($thesisId, $qAnnexe, $folderPath, $filePrefix);
|
||
|
||
// Legacy annexe files (direct upload, non-queue path — kept for backwards compat)
|
||
if (isset($files['annexes']) && is_array($files['annexes']['name'] ?? null)) {
|
||
$this->handleAnnexeFiles($thesisId, $files['annexes'], $folderPath, $filePrefix, $post);
|
||
}
|
||
}
|
||
|
||
// ── Website URL — add or update ──────────────────────────────────────
|
||
$this->handleWebsiteUrl($thesisId, $post);
|
||
}
|
||
|
||
// ── 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';
|
||
}
|
||
if (str_contains($message, 'orientation')) {
|
||
return 'orientation';
|
||
}
|
||
if (str_contains($message, 'atelier')) {
|
||
return 'ap';
|
||
}
|
||
if (str_contains($message, 'finalité')) {
|
||
return 'finality';
|
||
}
|
||
if (str_contains($message, 'langue')) {
|
||
return 'languages';
|
||
}
|
||
if (str_contains($message, 'format')) {
|
||
return 'formats';
|
||
}
|
||
if (str_contains($message, 'licence')) {
|
||
return 'license_id';
|
||
}
|
||
if (str_contains($message, 'promoteur')) {
|
||
return 'jury_promoteur';
|
||
}
|
||
if (str_contains($message, 'lecteur·ice interne')) {
|
||
return 'jury_lecteur_interne[]';
|
||
}
|
||
if (str_contains($message, 'lecteur·ice externe')) {
|
||
return 'jury_lecteur_externe[]';
|
||
}
|
||
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 = [];
|
||
|
||
// Promoteurs internes (accept both scalar and array)
|
||
$promoteurs = $post['jury_promoteur'] ?? null;
|
||
if ($promoteurs !== null && !is_array($promoteurs)) {
|
||
$promoteurs = [$promoteurs];
|
||
}
|
||
if (is_array($promoteurs)) {
|
||
foreach ($promoteurs as $name) {
|
||
$name = trim($name ?? '');
|
||
if ($name !== '') {
|
||
$members[] = ['name' => $name, 'role' => 'promoteur', 'is_external' => 0, 'is_ulb' => 0];
|
||
}
|
||
}
|
||
}
|
||
|
||
// Promoteurs ULB (accept both scalar and array)
|
||
$promoteursUlb = $post['jury_promoteur_ulb_name'] ?? null;
|
||
if ($promoteursUlb !== null && !is_array($promoteursUlb)) {
|
||
$promoteursUlb = [$promoteursUlb];
|
||
}
|
||
if (is_array($promoteursUlb)) {
|
||
foreach ($promoteursUlb as $name) {
|
||
$name = trim($name ?? '');
|
||
if ($name !== '') {
|
||
$members[] = ['name' => $name, 'role' => 'promoteur', 'is_external' => 1, 'is_ulb' => 1];
|
||
}
|
||
}
|
||
}
|
||
|
||
// Lecteurs internes
|
||
foreach ($post['jury_lecteur_interne'] ?? [] as $name) {
|
||
$name = trim($name);
|
||
if ($name !== '') {
|
||
$members[] = ['name' => $name, 'role' => 'lecteur', 'is_external' => 0];
|
||
}
|
||
}
|
||
|
||
// Lecteurs externes
|
||
foreach ($post['jury_lecteur_externe'] ?? [] as $name) {
|
||
$name = trim($name);
|
||
if ($name !== '') {
|
||
$members[] = ['name' => $name, 'role' => 'lecteur', 'is_external' => 1];
|
||
}
|
||
}
|
||
|
||
// Backwards compat: old jury_lecteurs[]
|
||
if (isset($post['jury_lecteurs'])) {
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* Add or update a website URL thesis_file row.
|
||
*
|
||
* If a website row already exists for this thesis, it is replaced.
|
||
* Otherwise a new row is inserted.
|
||
*/
|
||
private function handleWebsiteUrl(int $thesisId, array $post): void
|
||
{
|
||
$websiteUrl = trim($post['website_url'] ?? '');
|
||
|
||
// Remove existing website rows (website URLs have no disk file)
|
||
$existingFiles = $this->db->getThesisFiles($thesisId);
|
||
foreach ($existingFiles as $f) {
|
||
if ($f['file_type'] === 'website') {
|
||
$this->db->deleteThesisFile((int)$f['id'], $thesisId);
|
||
}
|
||
}
|
||
|
||
if ($websiteUrl === '') {
|
||
return;
|
||
}
|
||
|
||
// Validate URL
|
||
$websiteUrl = filter_var($websiteUrl, FILTER_VALIDATE_URL);
|
||
if ($websiteUrl === false) {
|
||
error_log('ThesisEditController: invalid website URL, skipping');
|
||
return;
|
||
}
|
||
|
||
$label = trim($post['website_label'] ?? '');
|
||
$sortOrder = isset($post['website_order']) ? (int)$post['website_order'] : null;
|
||
$fileName = rtrim(preg_replace('#^https?://#i', '', $websiteUrl), '/');
|
||
|
||
$this->db->insertThesisFile(
|
||
$thesisId,
|
||
'website',
|
||
$websiteUrl,
|
||
$fileName,
|
||
0,
|
||
'text/html',
|
||
$label !== '' ? $label : null,
|
||
$sortOrder
|
||
);
|
||
error_log("ThesisEditController: website stored → $websiteUrl");
|
||
}
|
||
}
|