merge banners into covers: remove banner field, migrate files, add covers to search/home/repertoire cards

This commit is contained in:
Pontoporeia
2026-05-08 10:46:02 +02:00
parent e3896811c4
commit f3d9615562
15 changed files with 198 additions and 407 deletions

View File

@@ -10,7 +10,7 @@
* - Parse and validate GET parameters (`page`, `year`)
* - Determine the display mode (default random-latest / year-filtered / paginated all)
* - Run the appropriate Database queries
* - Batch-load cover images for theses without a banner_path
* - Batch-load cover images for displayed theses
* - Assemble OG / meta tag array
* - Return a flat array of view variables ready for template extraction
*
@@ -91,18 +91,11 @@ class HomeController
$totalItems = $this->db->countPublishedTheses();
}
// Batch-load cover images for theses that have no banner_path
// Batch-load cover images for all displayed theses
if (!empty($itemsToLoad)) {
$needCover = array_column(
array_filter(
$itemsToLoad,
static fn ($t) => empty($t['banner_path']),
),
'id',
$coverMap = $this->db->getCoverPathsForTheses(
array_column($itemsToLoad, 'id')
);
if (!empty($needCover)) {
$coverMap = $this->db->getCoverPathsForTheses($needCover);
}
}
} catch (Exception $e) {
error_log('HomeController: ' . $e->getMessage());

View File

@@ -118,6 +118,7 @@ class SearchController
'results' => $results,
'validationError' => $validationError,
'baseParams' => $baseParams,
'coverMap' => $coverMap,
// Filter dropdowns
'years' => $years,
@@ -225,6 +226,8 @@ class SearchController
exit();
}
$coverMap = $this->db->getCoverPathsForTheses(array_column($theses, 'id'));
header('Cache-Control: public, max-age=300');
include APP_ROOT . '/templates/partials/student-preview.php';
exit();

View File

@@ -9,7 +9,7 @@
* 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)
* - Resolve the OG image (cover file → 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
@@ -155,16 +155,20 @@ class TfeController
}
/**
* Resolve the OG image URL: banner_path → first image file → empty string.
* Resolve the OG image URL: cover file → first image file → empty string.
*
* @param array<int, array<string, mixed>> $files
*/
private function resolveOgImage(array $files, ?string $bannerPath): string
private function resolveOgImage(array $files): string
{
if (!empty($bannerPath)) {
return self::BASE_URL . '/media.php?path=' . rawurlencode($bannerPath);
// Prefer the dedicated cover
foreach ($files as $file) {
if (($file['file_type'] ?? '') === 'cover') {
return self::BASE_URL . '/media.php?path=' . rawurlencode($file['file_path']);
}
}
// Fall back to first image file
foreach ($files as $file) {
$ext = strtolower(pathinfo($file['file_path'] ?? '', PATHINFO_EXTENSION));
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'], true)) {
@@ -183,7 +187,7 @@ class TfeController
*/
private function buildOgTags(array $data, int $thesisId, string $metaDescription): array
{
$ogImage = $this->resolveOgImage($data['files'] ?? [], $data['banner_path'] ?? null);
$ogImage = $this->resolveOgImage($data['files'] ?? []);
$title = $data['title'] . (!empty($data['authors']) ? ' ' . $data['authors'] : '');
$imageAlt = $data['title'] . (!empty($data['authors']) ? ' par ' . $data['authors'] : '');

View File

@@ -132,7 +132,7 @@ class ThesisCreateController
* 3. INSERT thesis row + link author (inside transaction)
* 4. Link jury, languages, formats, tags (inside transaction)
* 5. COMMIT
* 6. Handle file uploads: cover, banner, thesis files (outside transaction)
* 6. Handle file uploads: cover, thesis files (outside transaction)
*
* @param array $post Sanitised $_POST array.
* @param array $files $_FILES array.
@@ -213,7 +213,6 @@ class ThesisCreateController
// ── 5. File uploads (outside transaction — filesystem ops) ────────────
$this->handleCoverUpload($thesisId, $files['couverture'] ?? null);
$this->db->handleBannerUpload($thesisId, $files['banner'] ?? null);
$this->handleThesisFiles($thesisId, $data['annee'], $identifier, $files['files'] ?? null, $authorSlug, $post);
// ── 6. Website URL — stored as thesis_files row ──────────────────────

View File

@@ -320,20 +320,6 @@ class ThesisEditController
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);

View File

@@ -1551,79 +1551,9 @@ class Database
}
// ========================================================================
// BANNER METHODS
// COVER METHODS (formerly also BANNER METHODS — banners merged into covers)
// ========================================================================
/**
* Set (or clear) the banner_path for a thesis.
*/
public function setBannerPath(int $thesisId, ?string $path): void
{
$stmt = $this->pdo->prepare(
'UPDATE theses SET banner_path = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'
);
$stmt->execute([$path, $thesisId]);
}
/**
* Process a banner image upload for a thesis.
*
* Validates MIME type, extension, and file size, then saves the file to the
* banners/ directory under STORAGE_ROOT and calls setBannerPath().
*
* Returns the relative path (e.g. "banners/abc123.jpg") on success,
* or null if the file array is absent, has an error, fails validation,
* or cannot be moved.
*
* @param int $thesisId Target thesis ID
* @param array|null $uploadedFile Entry from $_FILES (e.g. $_FILES['banner'])
* @return string|null Relative path stored in the DB, or null
*/
public function handleBannerUpload(int $thesisId, ?array $uploadedFile): ?string
{
if (!$uploadedFile || ($uploadedFile['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
return null;
}
$allowedMimes = ['image/jpeg', 'image/png', 'image/webp'];
$allowedExts = ['jpg', 'jpeg', 'png', 'webp'];
$maxBytes = 5 * 1024 * 1024; // 5 MB
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($uploadedFile['tmp_name']);
$ext = strtolower(pathinfo($uploadedFile['name'], PATHINFO_EXTENSION));
if (!in_array($mimeType, $allowedMimes, true) ||
!in_array($ext, $allowedExts, true) ||
$uploadedFile['size'] > $maxBytes) {
error_log('handleBannerUpload: rejected ' . $uploadedFile['name'] . " ($mimeType, {$uploadedFile['size']} bytes)");
return null;
}
$bannerDir = defined('STORAGE_ROOT') ? STORAGE_ROOT . '/banners/' : null;
if (!$bannerDir) {
error_log('handleBannerUpload: STORAGE_ROOT not defined');
return null;
}
if (!file_exists($bannerDir)) {
mkdir($bannerDir, 0755, true);
}
$safeName = bin2hex(random_bytes(16)) . '.' . $ext;
$targetPath = $bannerDir . $safeName;
if (!move_uploaded_file($uploadedFile['tmp_name'], $targetPath)) {
error_log('handleBannerUpload: move_uploaded_file failed for ' . $uploadedFile['name']);
return null;
}
chmod($targetPath, 0644);
$relativePath = 'banners/' . $safeName;
$this->setBannerPath($thesisId, $relativePath);
error_log("handleBannerUpload: saved $relativePath");
return $relativePath;
}
// ========================================================================
// ENCAPSULATED QUERY HELPERS
// ========================================================================
@@ -1658,20 +1588,6 @@ class Database
return $row !== false ? $row : null;
}
/**
* Return the banner_path for a thesis, or null.
* Used when we need just the banner path without the full view expansion.
*/
public function getThesisBannerPath(int $thesisId): ?string
{
$stmt = $this->pdo->prepare(
'SELECT banner_path FROM theses WHERE id = ? LIMIT 1'
);
$stmt->execute([$thesisId]);
$val = $stmt->fetchColumn();
return ($val !== false && $val !== null) ? (string)$val : null;
}
/**
* Batch-load cover file paths for a set of thesis IDs.
* Returns [thesis_id => file_path] for IDs that have a cover in thesis_files.
@@ -1891,19 +1807,10 @@ class Database
/**
* Delete a single thesis and all its related data (cascade via FK).
* Also removes the banner file from disk if present.
* Removes thesis files from disk (covers are stored in thesis_files and handled here).
*/
public function deleteThesis(int $thesisId): void
{
// Clean up banner file
$bannerPath = $this->getThesisBannerPath($thesisId);
if ($bannerPath !== null) {
$fullPath = defined('STORAGE_ROOT') ? STORAGE_ROOT . '/' . $bannerPath : null;
if ($fullPath && file_exists($fullPath)) {
@unlink($fullPath);
}
}
// Clean up thesis files from disk
$files = $this->getThesisFiles($thesisId);
foreach ($files as $file) {
@@ -1928,13 +1835,6 @@ class Database
// Clean up files for each thesis
foreach ($thesisIds as $id) {
$bannerPath = $this->getThesisBannerPath($id);
if ($bannerPath !== null) {
$fullPath = defined('STORAGE_ROOT') ? STORAGE_ROOT . '/' . $bannerPath : null;
if ($fullPath && file_exists($fullPath)) {
@unlink($fullPath);
}
}
$files = $this->getThesisFiles($id);
foreach ($files as $file) {
if (!empty($file['file_path']) && file_exists($file['file_path'])) {
@@ -2058,9 +1958,9 @@ class Database
return null;
}
$allowedMimes = ['image/jpeg', 'image/png'];
$allowedExts = ['jpg', 'jpeg', 'png'];
$maxBytes = 10 * 1024 * 1024; // 10 MB
$allowedMimes = ['image/jpeg', 'image/png', 'image/webp'];
$allowedExts = ['jpg', 'jpeg', 'png', 'webp'];
$maxBytes = 20 * 1024 * 1024; // 20 MB
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($upload['tmp_name']);