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 * - '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); $orientations = $this->db->getAllOrientations(); $apPrograms = $this->db->getAllAPPrograms(); $finalityTypes = $this->db->getAllFinalityTypes(); $languages = $this->db->getAllLanguages(); $formatTypes = $this->db->getAllFormatTypes(); $licenseTypes = $this->db->getAllLicenseTypes(); $accessTypes = $this->db->getAccessTypes(); $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, 'orientations' => $orientations, 'apPrograms' => $apPrograms, 'finalityTypes' => $finalityTypes, 'languages' => $languages, 'formatTypes' => $formatTypes, 'licenseTypes' => $licenseTypes, 'accessTypes' => $accessTypes, '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); } } // ── 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 */ 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; } }