mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 08:09:18 +02:00
- Remove separate video/audio/peertube_video/peertube_audio pools from UI - TFE pool now accepts all file types including video/audio - When PeerTube is enabled, video/audio dropped into TFE pool auto-upload to PeerTube (process.php detects MIME and uploads immediately) - PeerTube return IDs now encode type: peertube:video:UUID or peertube:audio:UUID - load.php returns placeholder SVG for PeerTube files so they appear in FilePond - Edit mode: all existing files (including PeerTube) shown in TFE FilePond pool - Remove legacy video/audio/peertube_* handling from both controllers - Remove unused vide/audio/peertube_* entries from JS QUEUE_CONFIG
677 lines
28 KiB
PHP
677 lines
28 KiB
PHP
<?php
|
||
|
||
require_once __DIR__ . '/ThesisFileHandler.php';
|
||
|
||
/**
|
||
* ThesisCreateController
|
||
*
|
||
* Centralises all validation, data-fetching, and persistence logic for the
|
||
* admin "add new thesis" workflow (admin/add.php + admin/actions/formulaire.php).
|
||
*
|
||
* Responsibilities:
|
||
* - Loading lookup tables for the add-form view (loadFormData)
|
||
* - Validating and sanitising POST submissions
|
||
* - Creating the thesis record, linking authors / jury / languages / formats /
|
||
* tags in a single database transaction
|
||
* - Handling cover image, note d'intention, TFE, annexe, and PeerTube uploads
|
||
* - WCAG 3.3.1: mapping validation exception messages to autofocus field hints
|
||
*
|
||
* The class has NO output side-effects; all redirects, flash writes, session
|
||
* mutations, and template rendering stay in the thin dispatcher files so the
|
||
* view layer remains easy to inspect and modify.
|
||
*/
|
||
class ThesisCreateController
|
||
{
|
||
use ThesisFileHandler;
|
||
|
||
private Database $db;
|
||
|
||
public function __construct(Database $db)
|
||
{
|
||
$this->db = $db;
|
||
}
|
||
|
||
// ── Factory ───────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Convenience factory — instantiates Database and returns a ready
|
||
* controller instance.
|
||
*/
|
||
public static function make(): self
|
||
{
|
||
require_once APP_ROOT . '/src/Database.php';
|
||
require_once APP_ROOT . '/src/ErrorHandler.php';
|
||
|
||
return new self(new Database());
|
||
}
|
||
|
||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Get the identifier string for a thesis (caller supplies ID).
|
||
*/
|
||
public function getIdentifier(int $thesisId): string
|
||
{
|
||
return $this->db->getThesisIdentifier($thesisId);
|
||
}
|
||
|
||
// ── Read / view data ─────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Load all lookup tables required to render the add-thesis form.
|
||
*
|
||
* Returns a flat array of view variables:
|
||
* - 'orientations' – orientation lookup rows
|
||
* - 'apPrograms' – AP program lookup rows
|
||
* - 'finalityTypes' – finality type lookup rows
|
||
* - 'languages' – language lookup rows
|
||
* - 'formatTypes' – format type lookup rows
|
||
* - 'licenseTypes' – license type lookup rows
|
||
*
|
||
* @return array<string, mixed>
|
||
* @throws Exception on DB error.
|
||
*/
|
||
public function loadFormData(): array
|
||
{
|
||
return [
|
||
'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(),
|
||
];
|
||
}
|
||
|
||
// ── Write / action ────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Validate and persist a new-thesis POST submission.
|
||
*
|
||
* On success, returns the new thesis ID so the caller can redirect to
|
||
* recapitulatif.php?id=<n>. On validation or DB failure, throws an Exception
|
||
* (caller must flash the message and redirect back to the form).
|
||
*
|
||
* Execution order:
|
||
* 1. Validate + sanitise POST fields
|
||
* 2. Find/create author record
|
||
* 3. INSERT thesis row + link author (inside transaction)
|
||
* 4. Link jury, languages, formats, tags (inside transaction)
|
||
* 5. COMMIT
|
||
* 6. Handle file uploads: cover, thesis files (outside transaction)
|
||
*
|
||
* @param array $post Sanitised $_POST array.
|
||
* @param array $files $_FILES array.
|
||
* @return int The newly created thesis ID.
|
||
* @throws Exception On validation or DB error.
|
||
*/
|
||
public function submit(array $post, array $files, bool $adminMode = false): int
|
||
{
|
||
// ── 1. Validate + sanitise ────────────────────────────────────────────
|
||
$data = $this->validateAndSanitise($post, $adminMode);
|
||
|
||
// ── 1b. Duplicate detection ───────────────────────────────────────────
|
||
require_once APP_ROOT . '/src/DuplicateThesisException.php';
|
||
$duplicate = $this->db->findDuplicateThesis($data['titre'], $data['authorNames'], $data['annee']);
|
||
if ($duplicate !== null) {
|
||
throw new DuplicateThesisException(
|
||
$duplicate['id'],
|
||
$duplicate['identifier'],
|
||
$duplicate['title'],
|
||
$duplicate['author'],
|
||
$duplicate['year']
|
||
);
|
||
}
|
||
|
||
// ── 2. Build author entries (alphabetically sorted) ───────────────────
|
||
$authorEntries = [];
|
||
foreach ($data['authorNames'] as $i => $name) {
|
||
$authorEntries[] = [
|
||
'name' => $name,
|
||
'email' => $i === 0 ? ($data['mail'] ?: null) : null,
|
||
'show_contact' => $i === 0 ? $data['showContact'] : false,
|
||
];
|
||
}
|
||
$allAuthorsStr = implode(', ', $data['authorNames']);
|
||
|
||
// ── 3–4. DB writes in a transaction ───────────────────────────────────
|
||
$this->db->beginTransaction();
|
||
|
||
try {
|
||
$thesisId = $this->db->createThesis([
|
||
'year' => $data['annee'],
|
||
'orientation_id' => $data['orientationId'],
|
||
'ap_program_id' => $data['apProgramId'],
|
||
'finality_id' => $data['finalityId'],
|
||
'title' => $data['titre'],
|
||
'subtitle' => $data['subtitle'],
|
||
'synopsis' => $data['synopsis'],
|
||
'context_note' => $data['contextNote'],
|
||
'baiu_link' => $data['lien'],
|
||
'license_id' => $data['licenseId'],
|
||
'license_custom' => $data['licenseCustom'],
|
||
'access_type_id' => $data['accessTypeId'],
|
||
'objet' => $data['objet'],
|
||
'remarks' => $data['remarks'],
|
||
'jury_points' => $data['juryPoints'],
|
||
'exemplaire_baiu' => $data['exemplaireBaiu'],
|
||
'exemplaire_erg' => $data['exemplaireErg'],
|
||
'cc2r' => $data['cc2r'],
|
||
]);
|
||
|
||
$identifier = $this->db->getThesisIdentifier($thesisId);
|
||
error_log("[ThesisCreate] Step 1 OK — thesis_id=$thesisId ($identifier) | authors=" . count($authorEntries));
|
||
|
||
$this->db->setThesisAuthors($thesisId, $authorEntries);
|
||
error_log("[ThesisCreate] Step 2 OK — authors=" . json_encode($data['authorNames']));
|
||
|
||
$this->db->setThesisJury($thesisId, $data['juryMembers']);
|
||
error_log("[ThesisCreate] Step 3 OK — jury=" . count($data['juryMembers']));
|
||
|
||
$this->db->setThesisLanguages($thesisId, $data['languageIds']);
|
||
error_log("[ThesisCreate] Step 4 OK — languages=" . json_encode($data['languageIds']));
|
||
|
||
$this->db->setThesisFormats($thesisId, $data['formatIds']);
|
||
error_log("[ThesisCreate] Step 5 OK — formats=" . json_encode($data['formatIds']));
|
||
|
||
$this->db->setThesisTags($thesisId, $data['keywords']);
|
||
error_log("[ThesisCreate] Step 6 OK — tags=" . json_encode($data['keywords']));
|
||
|
||
$this->db->commit();
|
||
error_log("[ThesisCreate] COMMIT OK — thesis_id=$thesisId");
|
||
|
||
} catch (Exception $e) {
|
||
ErrorHandler::log('thesis_create_tx', $e, ['thesis_id' => $thesisId ?? null]);
|
||
$this->db->rollback();
|
||
throw $e;
|
||
}
|
||
|
||
// ── 5. File uploads (outside transaction — filesystem ops) ────────────
|
||
$tf = $this->buildThesisFolder($data['annee'], $allAuthorsStr, $data['titre']);
|
||
$folderName = $this->ensureUniqueFolder($tf['folderPath']);
|
||
// Rebuild path with potentially modified folder name
|
||
$folderPath = 'theses/' . $data['annee'] . '/' . $folderName . '/';
|
||
$filePrefix = $folderName;
|
||
|
||
if (!empty($post['filepond_mode'])) {
|
||
// New path: files already on server via async FilePond uploads
|
||
// Cover and note_intention also go through FilePond async flow
|
||
$this->handleFilePondSingleFile($thesisId, $post, 'cover', $folderPath, $filePrefix);
|
||
$this->handleFilePondSingleFile($thesisId, $post, 'note_intention', $folderPath, $filePrefix);
|
||
$nextNum = $this->handleFilePondQueueFiles($thesisId, $post, 'tfe', $folderPath, $filePrefix, 1);
|
||
$this->handleFilePondQueueFiles($thesisId, $post, 'annexe', $folderPath, $filePrefix, 0);
|
||
} else {
|
||
// Legacy path: files arrive via multipart $_FILES
|
||
$this->handleCoverUpload($thesisId, $files['couverture'] ?? null, $folderPath, $filePrefix);
|
||
$this->handleNoteIntentionUpload($thesisId, $files['note_intention'] ?? null, $folderPath, $filePrefix);
|
||
$queueFiles = $files['queue_file'] ?? [];
|
||
$qTfe = $this->extractFilesSubArray($queueFiles, 'tfe');
|
||
$qAnnexe = $this->extractFilesSubArray($queueFiles, 'annexe');
|
||
|
||
$nextNum = $this->handleTfeQueueFiles($thesisId, $qTfe, $folderPath, $filePrefix, 1);
|
||
$this->handleAnnexeQueueFiles($thesisId, $qAnnexe, $folderPath, $filePrefix);
|
||
}
|
||
|
||
// ── 6. Website URL — stored as thesis_files row ──────────────────────
|
||
$this->handleWebsiteUrl($thesisId, $post);
|
||
|
||
return $thesisId;
|
||
}
|
||
|
||
// ── 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 after an error.
|
||
*
|
||
* Returns null when no field mapping is found.
|
||
*/
|
||
public static function autofocusFieldForError(string $message): ?string
|
||
{
|
||
if (str_contains($message, "Auteur·ice")) {
|
||
return 'auteurice';
|
||
}
|
||
if (str_contains($message, 'Titre du TFE')) {
|
||
return 'titre';
|
||
}
|
||
if (str_contains($message, 'Synopsis')) {
|
||
return 'synopsis';
|
||
}
|
||
if (str_contains($message, 'Année invalide')) {
|
||
return 'année';
|
||
}
|
||
if (str_contains($message, 'orientation')) {
|
||
return 'orientation';
|
||
}
|
||
if (str_contains($message, 'Atelier Pratique')) {
|
||
return 'ap';
|
||
}
|
||
if (str_contains($message, 'finalité')) {
|
||
return 'finality';
|
||
}
|
||
if (str_contains($message, 'langue')) {
|
||
return 'languages';
|
||
}
|
||
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[]';
|
||
}
|
||
if (str_contains($message, 'format')) {
|
||
return 'formats';
|
||
}
|
||
if (str_contains($message, 'mots-clés')) {
|
||
return 'tag';
|
||
}
|
||
if (str_contains($message, 'licence')) {
|
||
return 'license_id';
|
||
}
|
||
if (str_contains($message, 'Lien URL')) {
|
||
return 'lien';
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
// ── Private: validation ───────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Validate and sanitise all POST fields for a new thesis submission.
|
||
*
|
||
* Returns a flat associative array of clean values.
|
||
*
|
||
* @param array $post Raw $_POST.
|
||
* @return array<string, mixed>
|
||
* @throws Exception on validation failure.
|
||
*/
|
||
private function validateAndSanitise(array $post, bool $adminMode = false): array
|
||
{
|
||
// Split authors by comma, trim, filter empty, sort alphabetically.
|
||
$authorRaw = $this->sanitiseString($post['auteurice'] ?? '');
|
||
$authorNames = [];
|
||
if ($authorRaw !== '') {
|
||
$authorNames = array_filter(array_map('trim', explode(',', $authorRaw)), fn ($n) => $n !== '');
|
||
$authorNames = array_values($authorNames);
|
||
sort($authorNames, SORT_NATURAL);
|
||
}
|
||
if (empty($authorNames)) {
|
||
throw new Exception("Le champ 'Auteur·ice(s)' est requis.");
|
||
}
|
||
|
||
// contact_interne (backoffice) takes precedence over mail (tfe-info fieldset)
|
||
$contactInterne = trim($post['contact_interne'] ?? '');
|
||
$mail = !empty($post['mail']) ? $this->sanitiseString($post['mail']) : '';
|
||
if ($contactInterne !== '') {
|
||
$mail = $contactInterne;
|
||
}
|
||
// contact_public: respected if present (admin form); defaults to true for student forms
|
||
// where the spec says contact is always visible when provided.
|
||
if (array_key_exists('contact_public', $post)) {
|
||
$showContact = !empty($post['contact_public']);
|
||
} else {
|
||
$showContact = $mail !== '';
|
||
}
|
||
|
||
$annee = filter_var($post['année'] ?? '', FILTER_VALIDATE_INT);
|
||
if ($annee === false || $annee < 2000 || $annee > ((int) date('Y') + 1)) {
|
||
throw new Exception('Année invalide. Veuillez entrer une année valide.');
|
||
}
|
||
|
||
$orientationId = filter_var($post['orientation'] ?? '', FILTER_VALIDATE_INT) ?: null;
|
||
if (!$adminMode && !$orientationId) {
|
||
throw new Exception('Veuillez sélectionner une orientation.');
|
||
}
|
||
|
||
$apProgramId = filter_var($post['ap'] ?? '', FILTER_VALIDATE_INT) ?: null;
|
||
if (!$adminMode && !$apProgramId) {
|
||
throw new Exception('Veuillez sélectionner un Atelier Pratique.');
|
||
}
|
||
|
||
$finalityId = filter_var($post['finality'] ?? '', FILTER_VALIDATE_INT) ?: null;
|
||
if (!$adminMode && !$finalityId) {
|
||
throw new Exception('Veuillez sélectionner une finalité.');
|
||
}
|
||
|
||
$titre = $this->validateRequired($this->sanitiseString($post['titre'] ?? ''), 'Titre du TFE');
|
||
$subtitle = $this->sanitiseString($post['subtitle'] ?? '');
|
||
$synopsis = $this->validateRequired($this->sanitiseString($post['synopsis'] ?? ''), 'Synopsis');
|
||
|
||
// Jury members — new structure: separate interne/externe lecteurs
|
||
$juryMembers = [];
|
||
$hasPromoteur = false;
|
||
$hasPromoteurUlb = false;
|
||
$hasLecteurInt = false;
|
||
$hasLecteurExt = false;
|
||
// 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 !== '') {
|
||
$juryMembers[] = ['name' => $name, 'role' => 'promoteur', 'is_external' => 0, 'is_ulb' => 0];
|
||
$hasPromoteur = true;
|
||
}
|
||
}
|
||
}
|
||
// 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 !== '') {
|
||
$juryMembers[] = ['name' => $name, 'role' => 'promoteur', 'is_external' => 1, 'is_ulb' => 1];
|
||
$hasPromoteurUlb = true;
|
||
}
|
||
}
|
||
}
|
||
foreach ($post['jury_lecteur_interne'] ?? [] as $name) {
|
||
$name = trim($name);
|
||
if ($name !== '') {
|
||
$juryMembers[] = ['name' => $name, 'role' => 'lecteur', 'is_external' => 0];
|
||
$hasLecteurInt = true;
|
||
}
|
||
}
|
||
foreach ($post['jury_lecteur_externe'] ?? [] as $name) {
|
||
$name = trim($name);
|
||
if ($name !== '') {
|
||
$juryMembers[] = ['name' => $name, 'role' => 'lecteur', 'is_external' => 1];
|
||
$hasLecteurExt = true;
|
||
}
|
||
}
|
||
// Keep backwards compat with old jury_lecteurs (from old-style forms)
|
||
if (empty($juryMembers) || isset($post['jury_lecteurs'])) {
|
||
foreach ($post['jury_lecteurs'] ?? [] as $i => $name) {
|
||
$name = trim($name);
|
||
if ($name !== '') {
|
||
$juryMembers[] = [
|
||
'name' => $name,
|
||
'role' => 'lecteur',
|
||
'is_external' => isset($post['jury_lecteurs_ext'][$i]) ? 1 : 0,
|
||
];
|
||
if (isset($post['jury_lecteurs_ext'][$i]) && $post['jury_lecteurs_ext'][$i]) {
|
||
$hasLecteurExt = true;
|
||
} else {
|
||
$hasLecteurInt = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (!$adminMode && !$hasPromoteur) {
|
||
throw new Exception('Veuillez indiquer au moins un·e promoteur·ice interne.');
|
||
}
|
||
if (!$adminMode && !$hasLecteurInt) {
|
||
throw new Exception('Veuillez indiquer au moins un·e lecteur·ice interne.');
|
||
}
|
||
if (!$adminMode && !$hasLecteurExt) {
|
||
throw new Exception('Veuillez indiquer au moins un·e lecteur·ice externe.');
|
||
}
|
||
|
||
// Keywords (max 10, min 3) — lowercased, spaces collapsed, deduplicated
|
||
$keywords = [];
|
||
$normalizeTag = fn(string $t): string => strtolower(trim(preg_replace('/\s+/', ' ', $t)));
|
||
if (isset($post['tag']) && is_array($post['tag'])) {
|
||
$keywords = array_values(array_unique(array_map(
|
||
$normalizeTag,
|
||
array_map(fn($t) => (string)$t, $post['tag'])
|
||
)));
|
||
$keywords = array_filter($keywords, fn($t) => $t !== '');
|
||
$keywords = array_slice($keywords, 0, 10);
|
||
} else {
|
||
$tagRaw = $this->sanitiseString($post['tag'] ?? '');
|
||
if ($tagRaw !== '') {
|
||
$keywords = array_map($normalizeTag, explode(',', $tagRaw));
|
||
}
|
||
}
|
||
$keywords = array_values(array_unique($keywords));
|
||
$keywords = array_filter($keywords, fn($t) => $t !== '');
|
||
$keywords = array_slice($keywords, 0, 10);
|
||
if (count($keywords) > 10) {
|
||
throw new Exception('Maximum 10 mots-clés autorisés.');
|
||
}
|
||
if (!$adminMode && count($keywords) < 3) {
|
||
throw new Exception('Veuillez indiquer au moins 3 mots-clés.');
|
||
}
|
||
|
||
// Languages (at least one required)
|
||
$languageIds = isset($post['languages']) && is_array($post['languages'])
|
||
? array_map('intval', $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 !== '') {
|
||
$languageIds[] = $this->db->getOrCreateLanguage($langName);
|
||
}
|
||
}
|
||
} elseif (is_string($autreRaw) && trim($autreRaw) !== '') {
|
||
foreach (array_map('trim', explode(',', $autreRaw)) as $langName) {
|
||
if ($langName !== '') {
|
||
$languageIds[] = $this->db->getOrCreateLanguage($langName);
|
||
}
|
||
}
|
||
}
|
||
if (!$adminMode && empty($languageIds)) {
|
||
throw new Exception('Veuillez sélectionner au moins une langue.');
|
||
}
|
||
|
||
// Formats (at least one required)
|
||
$formatIds = isset($post['formats']) && is_array($post['formats'])
|
||
? array_map('intval', $post['formats'])
|
||
: [];
|
||
if (!$adminMode && empty($formatIds)) {
|
||
throw new Exception('Veuillez sélectionner au moins un format.');
|
||
}
|
||
|
||
// Access type — must be one of the enabled types; default 2 (Interne)
|
||
$accessTypeId = filter_var($post['access_type_id'] ?? '', FILTER_VALIDATE_INT);
|
||
if ($accessTypeId === false || $accessTypeId <= 0) {
|
||
$accessTypeId = 2; // Interne
|
||
}
|
||
|
||
$licenseId = filter_var($post['license_id'] ?? '', FILTER_VALIDATE_INT) ?: null;
|
||
$licenseCustom = trim($post['license_custom'] ?? '');
|
||
// License is only required for non-admin when access type is Libre (1).
|
||
// For Interne (2) and Interdit (3), it's optional.
|
||
if (!$adminMode && $accessTypeId === 1 && !$licenseId && $licenseCustom === '') {
|
||
throw new Exception('Veuillez sélectionner une licence ou en préciser une.');
|
||
}
|
||
|
||
// Objet — restricted to valid values
|
||
$validObjet = ['tfe', 'thèse', 'frart'];
|
||
$objet = in_array($post['objet'] ?? '', $validObjet, true) ? $post['objet'] : 'tfe';
|
||
|
||
// External link (optional)
|
||
$lien = '';
|
||
if (!empty($post['lien'])) {
|
||
$lien = filter_var($post['lien'], FILTER_VALIDATE_URL);
|
||
if ($lien === false) {
|
||
throw new Exception('Lien URL invalide.');
|
||
}
|
||
}
|
||
|
||
// Note contextuelle (optional, max 1500 chars)
|
||
$contextNote = $this->sanitiseString($post['context_note'] ?? '');
|
||
if (strlen($contextNote) > 1500) {
|
||
$contextNote = substr($contextNote, 0, 1500);
|
||
}
|
||
|
||
// Backoffice fields (admin only)
|
||
$remarks = trim($post['remarks'] ?? '');
|
||
$juryPoints = $post['jury_points'] ?? null;
|
||
if ($juryPoints !== null && $juryPoints !== '') {
|
||
$juryPoints = filter_var($juryPoints, FILTER_VALIDATE_FLOAT);
|
||
if ($juryPoints === false || $juryPoints < 0 || $juryPoints > 20) {
|
||
throw new Exception('La note du jury doit être comprise entre 0 et 20.');
|
||
}
|
||
}
|
||
$exemplaireBaiu = !empty($post['exemplaire_baiu']);
|
||
$exemplaireErg = !empty($post['exemplaire_erg']);
|
||
$cc2r = !empty($post['cc2r']);
|
||
|
||
// Annexes are optional — no validation required
|
||
$hasAnnexes = !empty($post['has_annexes']);
|
||
|
||
return compact(
|
||
'authorNames',
|
||
'mail',
|
||
'showContact',
|
||
'annee',
|
||
'orientationId',
|
||
'apProgramId',
|
||
'finalityId',
|
||
'titre',
|
||
'subtitle',
|
||
'synopsis',
|
||
'juryMembers',
|
||
'keywords',
|
||
'languageIds',
|
||
'formatIds',
|
||
'licenseId',
|
||
'licenseCustom',
|
||
'lien',
|
||
'accessTypeId',
|
||
'objet',
|
||
'contextNote',
|
||
'remarks',
|
||
'juryPoints',
|
||
'exemplaireBaiu',
|
||
'exemplaireErg',
|
||
'cc2r'
|
||
);
|
||
}
|
||
|
||
// ── Private: file uploads ─────────────────────────────────────────────────
|
||
|
||
// ── Private: input helpers ────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Trim and strip HTML tags from a string value.
|
||
* htmlspecialchars is applied at render time, not here.
|
||
*/
|
||
private function sanitiseString(string $input): string
|
||
{
|
||
return strip_tags(trim($input));
|
||
}
|
||
|
||
/**
|
||
* Assert that a string value is non-empty.
|
||
*
|
||
* @throws Exception if $value is empty.
|
||
*/
|
||
private function validateRequired(string $value, string $fieldName): string
|
||
{
|
||
if ($value === '') {
|
||
throw new Exception("Le champ '$fieldName' est requis.");
|
||
}
|
||
|
||
return $value;
|
||
}
|
||
|
||
/**
|
||
* Upload PeerTube video/audio files from FilePond queue.
|
||
*
|
||
* Files arrive via PHP's nested $_FILES structure from
|
||
* <input name="queue_file[peertube_video][]">.
|
||
*
|
||
* @param int $thesisId Thesis to attach the results to.
|
||
* @param string $title Title to use on PeerTube.
|
||
* @param array|null $uploads Flat $_FILES-style array from extractFilesSubArray().
|
||
* @param string $fileType 'video' or 'audio'.
|
||
*/
|
||
protected function handlePeerTubeQueueFiles(int $thesisId, string $title, ?array $uploads, string $fileType): void
|
||
{
|
||
if (!$uploads || !is_array($uploads['name'] ?? null)) {
|
||
return;
|
||
}
|
||
|
||
require_once APP_ROOT . '/src/PeerTubeService.php';
|
||
if (!PeerTubeService::isEnabled($this->db)) {
|
||
return;
|
||
}
|
||
|
||
$count = count($uploads['name']);
|
||
for ($i = 0; $i < $count; $i++) {
|
||
if (($uploads['error'][$i] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
$result = PeerTubeService::upload(
|
||
$this->db,
|
||
$uploads['tmp_name'][$i],
|
||
$uploads['name'][$i],
|
||
$title,
|
||
''
|
||
);
|
||
|
||
// Store as peertube_ids:{uuid} so the embed template can extract the UUID
|
||
$storedPath = 'peertube_ids:' . $result['uuid'];
|
||
$this->db->insertThesisFile(
|
||
$thesisId,
|
||
$fileType,
|
||
$storedPath,
|
||
basename($uploads['name'][$i]),
|
||
$uploads['size'][$i],
|
||
$uploads['type'][$i] ?? 'application/octet-stream',
|
||
null,
|
||
null
|
||
);
|
||
error_log("ThesisCreateController: PeerTube upload OK → " . $result['watchUrl']);
|
||
} catch (\Throwable $e) {
|
||
error_log('ThesisCreateController: PeerTube upload failed — ' . $e->getMessage());
|
||
// Non-fatal: thesis already saved; admin can re-upload manually.
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Store a website URL as a thesis_files row (file_type = 'website').
|
||
*
|
||
* The URL is stored in file_path; no filesystem operation is performed.
|
||
* label and sort_order from the POST are preserved.
|
||
*/
|
||
protected function handleWebsiteUrl(int $thesisId, array $post): void
|
||
{
|
||
$websiteUrl = trim($post['website_url'] ?? '');
|
||
if ($websiteUrl === '') {
|
||
return;
|
||
}
|
||
|
||
// Validate URL
|
||
$websiteUrl = filter_var($websiteUrl, FILTER_VALIDATE_URL);
|
||
if ($websiteUrl === false) {
|
||
error_log('ThesisCreateController: 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("ThesisCreateController: website stored → $websiteUrl");
|
||
}
|
||
}
|