Files
xamxam/app/src/Controllers/TfeController.php
Pontoporeia dce0e0b301 schema: validate against new TFE field spec
- add exemplaire_baiu, exemplaire_erg, cc4r, remarks;
- add is_ulb to jury;
- split jury_lecteurs into interne/externe in view;
- refactor admin edit form with backoffice fields;
- update public fiche to show promoteur ULB and split lecteurs
2026-05-07 17:53:24 +02:00

279 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* TfeController
*
* Handles all data-fetching and view-variable assembly for the public TFE
* detail page (public/tfe.php).
*
* Responsibilities:
* - Validate the `id` GET parameter and load the thesis record
* - Enforce publication visibility (redirect to index on 404)
* - Resolve the OG image (banner → first image file)
* - Build the complete OG / Twitter Card tag array
* - Assemble the meta description from the synopsis
* - Collect WebVTT caption file paths for video pairing
* - Return a flat array of view variables ready for template extraction
*
* The class has NO output side-effects; all template rendering stays in
* public/tfe.php so the view layer remains easy to inspect and modify.
*/
class TfeController
{
private const BASE_URL = 'https://xamxam.erg.be';
private const META_MAX_LEN = 160;
private Database $db;
public function __construct(Database $db)
{
$this->db = $db;
}
// ── Factory ───────────────────────────────────────────────────────────────
/**
* Convenience factory: loads the Database singleton and returns a ready
* controller instance.
*/
public static function create(): self
{
require_once APP_ROOT . '/src/Database.php';
return new self(Database::getInstance());
}
// ── Main entry point ─────────────────────────────────────────────────────
/**
* Process the current request.
*
* On success returns an array of view variables.
* On failure (missing id, thesis not found) sends a redirect and exits.
*
* @return array<string, mixed>
*/
public function handle(): array
{
$thesisId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
if ($thesisId <= 0) {
$this->redirectHome();
}
try {
$data = $this->db->getThesisById($thesisId);
} catch (Exception $e) {
error_log('TfeController: ' . $e->getMessage());
$this->redirectHome();
}
if (empty($data)) {
$this->redirectHome();
}
// Access type (1 = open, 2 = restricted, 3 = forbidden)
$accessTypeId = $this->db->getThesisAccessTypeId($thesisId) ?? 1;
$isInterdit = ($accessTypeId === 3);
// Check if restricted files feature is enabled and user has access
$restrictedEnabled = $this->db->isRestrictedFilesEnabled();
$hasRestrictedAccess = false;
if ($restrictedEnabled && $accessTypeId === 2) {
// Check for cookie-based access
$cookieToken = $_COOKIE['tfe_access_' . $thesisId] ?? null;
if ($cookieToken) {
$hasRestrictedAccess = $this->db->hasValidCookieAccess($cookieToken, $thesisId);
}
}
// If access is restricted and user doesn't have valid access, hide files
$shouldHideFiles = ($restrictedEnabled && $accessTypeId === 2 && !$hasRestrictedAccess);
// Caption (WebVTT) files — N-th VTT is paired with the N-th <video>
$captionFiles = $this->collectCaptionPaths($data['files'] ?? []);
// Jury members with interne/externe split
$jury = $this->db->getThesisJury($thesisId);
$juryByRole = $this->splitJuryByRole($jury);
// Page meta
$metaDescription = $this->buildMetaDescription($data['synopsis'] ?? '');
$ogTags = $this->buildOgTags($data, $thesisId, $metaDescription);
$pageTitle = $data['title']
. (!empty($data['authors']) ? ' ' . $data['authors'] : '')
. ' XAMXAM';
return [
// Core data
'thesisId' => $thesisId,
'data' => $data,
'accessTypeId' => $accessTypeId,
'isInterdit' => $isInterdit,
'captionFiles' => $captionFiles,
'juryPresidents' => $juryByRole['presidents'],
'promoteursInternes' => $juryByRole['internes'],
'promoteursExternes' => $juryByRole['externes'],
'promoteursUlb' => $juryByRole['ulb'],
'juryLecteursInternes' => $juryByRole['lecteurs_internes'],
'juryLecteursExternes' => $juryByRole['lecteurs_externes'],
// Restricted files access
'restrictedEnabled' => $restrictedEnabled,
'hasRestrictedAccess' => $hasRestrictedAccess,
'shouldHideFiles' => $shouldHideFiles,
// Page meta
'pageTitle' => $pageTitle,
'metaDescription' => $metaDescription,
'ogTags' => $ogTags,
// Layout
'currentNav' => '',
'extraCss' => ['/assets/css/tfe.css'],
'bodyClass' => 'tfe-body',
];
}
// ── Private helpers ───────────────────────────────────────────────────────
/**
* Build a ~160-character meta description from the thesis synopsis.
*/
private function buildMetaDescription(string $synopsis): string
{
$plain = strip_tags($synopsis);
if (empty($plain)) {
return 'Mémoire de fin d\'études XAMXAM, répertoire des TFE de l\'erg.';
}
return strlen($plain) > self::META_MAX_LEN
? substr($plain, 0, self::META_MAX_LEN - 3) . '…'
: $plain;
}
/**
* Resolve the OG image URL: banner_path → first image file → empty string.
*
* @param array<int, array<string, mixed>> $files
*/
private function resolveOgImage(array $files, ?string $bannerPath): string
{
if (!empty($bannerPath)) {
return self::BASE_URL . '/media.php?path=' . rawurlencode($bannerPath);
}
foreach ($files as $file) {
$ext = strtolower(pathinfo($file['file_path'] ?? '', PATHINFO_EXTENSION));
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'], true)) {
return self::BASE_URL . '/media.php?path=' . rawurlencode($file['file_path']);
}
}
return '';
}
/**
* Build the complete $ogTags array consumed by templates/head.php.
*
* @param array<string, mixed> $data
* @return array<string, string>
*/
private function buildOgTags(array $data, int $thesisId, string $metaDescription): array
{
$ogImage = $this->resolveOgImage($data['files'] ?? [], $data['banner_path'] ?? null);
$title = $data['title'] . (!empty($data['authors']) ? ' ' . $data['authors'] : '');
$imageAlt = $data['title'] . (!empty($data['authors']) ? ' par ' . $data['authors'] : '');
return [
'type' => 'article',
'title' => $title,
'description' => $metaDescription,
'url' => self::BASE_URL . '/tfe?id=' . $thesisId,
'image' => $ogImage,
'image_alt' => $imageAlt,
'site_name' => 'XAMXAM ERG',
'article_author' => $data['authors'] ?? '',
'article_published_time' => !empty($data['year']) ? $data['year'] . '-01-01' : '',
];
}
/**
* Split jury members by role and internal/external/ULB flags.
*
* @param array<int, array<string, mixed>> $jury
* @return array{presidents: list<string>, internes: list<string>, externes: list<string>, ulb: list<string>, lecteurs_internes: list<string>, lecteurs_externes: list<string>}
*/
private function splitJuryByRole(array $jury): array
{
$result = ['presidents' => [], 'internes' => [], 'externes' => [], 'ulb' => [], 'lecteurs_internes' => [], 'lecteurs_externes' => []];
foreach ($jury as $member) {
$name = $member['name'] ?? '';
if ($name === '') {
continue;
}
switch ($member['role']) {
case 'president':
$result['presidents'][] = $name;
break;
case 'promoteur':
if ((int)($member['is_ulb'] ?? 0) === 1) {
$result['ulb'][] = $name;
} elseif ((int)$member['is_external'] === 1) {
$result['externes'][] = $name;
} else {
$result['internes'][] = $name;
}
break;
case 'lecteur':
if ((int)$member['is_external'] === 1) {
$result['lecteurs_externes'][] = $name;
} else {
$result['lecteurs_internes'][] = $name;
}
break;
}
}
return $result;
}
/**
* Return an ordered list of VTT caption file paths from the files array.
* The N-th entry corresponds to the N-th <video> element in document order.
*
* @param array<int, array<string, mixed>> $files
* @return list<string>
*/
private function collectCaptionPaths(array $files): array
{
$captions = [];
foreach ($files as $file) {
$mime = $file['mime_type'] ?? '';
$ext = strtolower(pathinfo($file['file_path'] ?? '', PATHINFO_EXTENSION));
if ($mime === 'text/vtt' || $ext === 'vtt') {
$captions[] = $file['file_path'];
}
}
return $captions;
}
// ── Response helpers ──────────────────────────────────────────────────────
/**
* Redirect to the home page and terminate. Never returns.
*/
private function redirectHome(): never
{
header('Location: /');
exit;
}
}