mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
fix: add help email, preserve file names on validation error, license fix
The share link (partage) form does not expose a license field and does not send access_type_id (defaults to 2/Interne). Server-side validation was unconditionally requiring a license for non-admin submissions, causing all share link submissions to fail. Now the license check is gated on adminMode=false AND accessTypeId=1 (Libre), matching the client-side HTMX fragment behaviour in licence-fragment.php. Also fixed a use-before-definition where accessTypeId was referenced before being assigned. Student form improvements: - Add xamxam@erg.be mailto link at top of form - On validation error, append "Si le problème persiste, envoyez un e-mail à xamxam@erg.be" to the flash message - Preserve uploaded file names across validation redirects: store in session (share_primed_files_<slug>), display as warning on form re-render so the student knows which files to re-select - License: only required for non-admin when access_type_id=1 (Libre), not for Interne (2) or Interdit (3). Fixes share link submissions failing with "Veuillez sélectionner une licence". Also fixed use-before-definition of accessTypeId.
This commit is contained in:
591
app/src/Controllers/ThesisFileHandler.php
Normal file
591
app/src/Controllers/ThesisFileHandler.php
Normal file
@@ -0,0 +1,591 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Trait ThesisFileHandler
|
||||
*
|
||||
* Shared file-upload logic used by ThesisCreateController and
|
||||
* ThesisEditController. All on-disk files are stored under:
|
||||
*
|
||||
* theses/{YYYY}/{YYYY}_{AUTHORS}_{TITLE_SLUG}/
|
||||
*
|
||||
* Filenames follow:
|
||||
*
|
||||
* {YYYY}_{AUTHORS}_{TITLE_SLUG}_{SEMANTIC}.ext (single file)
|
||||
* {YYYY}_{AUTHORS}_{TITLE_SLUG}_{SEMANTIC}_{XX}.ext (numbered, 01-based)
|
||||
*
|
||||
* Semantic groups:
|
||||
* COUVERTURE – single cover image
|
||||
* NOTE_INTENTION – single PDF
|
||||
* TFE_{XX} – main thesis files (contiguous numbering)
|
||||
* ANNEXE_{XX} – annex files (separate numbering)
|
||||
*
|
||||
* TFE hierarchy (determines numbering order):
|
||||
* 1. PDFs
|
||||
* 2. Videos
|
||||
* 3. Audio
|
||||
* 4. Subtitles (VTT placed right after their video)
|
||||
* 5. Images
|
||||
* 6. Archives / other
|
||||
*
|
||||
* PeerTube and website URLs are stored as thesis_files rows with
|
||||
* file_path containing the URL; no on-disk file is created.
|
||||
*
|
||||
* The original user-provided filename is preserved in thesis_files.file_name.
|
||||
*/
|
||||
trait ThesisFileHandler
|
||||
{
|
||||
/** Maximum allowed file size for thesis files (bytes). */
|
||||
private const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500 MB
|
||||
|
||||
/** Maximum allowed file size for PDF files specifically (bytes). */
|
||||
private const MAX_PDF_SIZE = 100 * 1024 * 1024; // 100 MB
|
||||
|
||||
/** Cover image max size. */
|
||||
private const MAX_COVER_SIZE = 20 * 1024 * 1024; // 20 MB
|
||||
|
||||
/** MIME types accepted for thesis files. */
|
||||
private const ALLOWED_MIME_TYPES = [
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
|
||||
'application/pdf',
|
||||
'video/mp4', 'video/webm', 'video/ogg', 'video/quicktime',
|
||||
'audio/mpeg', 'audio/mp3', 'audio/ogg', 'audio/wav',
|
||||
'audio/flac', 'audio/aac', 'audio/x-m4a', 'audio/mp4',
|
||||
'text/vtt',
|
||||
'application/zip', 'application/x-zip-compressed',
|
||||
'application/x-tar', 'application/gzip',
|
||||
'application/octet-stream',
|
||||
];
|
||||
|
||||
/** File extensions accepted for thesis files. */
|
||||
private const ALLOWED_EXTENSIONS = [
|
||||
'jpg', 'jpeg', 'png', 'gif', 'webp',
|
||||
'pdf',
|
||||
'mp4', 'webm', 'ogv', 'mov',
|
||||
'mp3', 'ogg', 'oga', 'wav', 'flac', 'aac', 'm4a',
|
||||
'vtt',
|
||||
'zip', 'tar', 'gz', 'tgz',
|
||||
];
|
||||
|
||||
// ── Public entry points (called by controllers) ──────────────────────────
|
||||
|
||||
/**
|
||||
* Process a cover image upload.
|
||||
*
|
||||
* @param int $thesisId
|
||||
* @param array|null $upload Single-file $_FILES entry.
|
||||
* @param string $folderPath Relative path from STORAGE_ROOT to the thesis folder (e.g. "theses/2025/2025_SMITH_Mon_Titre/").
|
||||
* @param string $filePrefix The prefix shared by all files in this folder (e.g. "2025_SMITH_Mon_Titre").
|
||||
*/
|
||||
protected function handleCoverUpload(int $thesisId, ?array $upload, string $folderPath, string $filePrefix): void
|
||||
{
|
||||
if (!$upload || !isset($upload['error']) || $upload['error'] !== UPLOAD_ERR_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
||||
$mimeType = $finfo->file($upload['tmp_name']);
|
||||
$ext = strtolower(pathinfo($upload['name'], PATHINFO_EXTENSION));
|
||||
|
||||
$allowedMimes = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
$allowedExts = ['jpg', 'jpeg', 'png', 'webp'];
|
||||
|
||||
if (!in_array($mimeType, $allowedMimes, true)
|
||||
|| !in_array($ext, $allowedExts, true)
|
||||
|| $upload['size'] > self::MAX_COVER_SIZE) {
|
||||
error_log("ThesisFileHandler: invalid cover MIME $mimeType / $ext / {$upload['size']} bytes, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
$dir = STORAGE_ROOT . '/' . $folderPath;
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
$targetName = $filePrefix . '_COUVERTURE.' . $ext;
|
||||
$targetPath = $dir . $targetName;
|
||||
|
||||
if (!move_uploaded_file($upload['tmp_name'], $targetPath)) {
|
||||
error_log("ThesisFileHandler: failed to move cover to $targetPath");
|
||||
return;
|
||||
}
|
||||
|
||||
chmod($targetPath, 0644);
|
||||
$relPath = $folderPath . $targetName;
|
||||
|
||||
$this->db->insertThesisFile(
|
||||
$thesisId, 'cover',
|
||||
$relPath,
|
||||
basename($upload['name']),
|
||||
$upload['size'],
|
||||
$mimeType
|
||||
);
|
||||
error_log("ThesisFileHandler: cover uploaded → $relPath");
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the note d'intention upload (single PDF).
|
||||
*
|
||||
* @param int $thesisId
|
||||
* @param array|null $upload Single-file $_FILES entry.
|
||||
* @param string $folderPath Relative path from STORAGE_ROOT to the thesis folder.
|
||||
* @param string $filePrefix The shared file prefix.
|
||||
*/
|
||||
protected function handleNoteIntentionUpload(int $thesisId, ?array $upload, string $folderPath, string $filePrefix): void
|
||||
{
|
||||
if (!$upload || !isset($upload['error']) || $upload['error'] !== UPLOAD_ERR_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
||||
$mimeType = $finfo->file($upload['tmp_name']);
|
||||
$ext = strtolower(pathinfo($upload['name'], PATHINFO_EXTENSION));
|
||||
|
||||
if ($mimeType !== 'application/pdf' || $ext !== 'pdf') {
|
||||
error_log("ThesisFileHandler: invalid note d'intention MIME $mimeType, skipping");
|
||||
return;
|
||||
}
|
||||
if ($upload['size'] > self::MAX_PDF_SIZE) {
|
||||
error_log("ThesisFileHandler: note d'intention too large ({$upload['size']} bytes), skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
$dir = STORAGE_ROOT . '/' . $folderPath;
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
$targetName = $filePrefix . '_NOTE_INTENTION.pdf';
|
||||
$targetPath = $dir . $targetName;
|
||||
|
||||
if (!move_uploaded_file($upload['tmp_name'], $targetPath)) {
|
||||
error_log("ThesisFileHandler: failed to move note d'intention to $targetPath");
|
||||
return;
|
||||
}
|
||||
|
||||
chmod($targetPath, 0644);
|
||||
$relPath = $folderPath . $targetName;
|
||||
|
||||
$this->db->insertThesisFile(
|
||||
$thesisId, 'note_intention',
|
||||
$relPath,
|
||||
basename($upload['name']),
|
||||
$upload['size'],
|
||||
'application/pdf'
|
||||
);
|
||||
error_log("ThesisFileHandler: note d'intention uploaded → $relPath");
|
||||
}
|
||||
|
||||
/**
|
||||
* Process multiple TFE file uploads (files[] — the main set).
|
||||
*
|
||||
* Files are stored with TFE_{XX} semantics. Numbering is contiguous
|
||||
* across all sub-types in order: PDF → video → audio → subtitles → images → archives.
|
||||
* Subtitles (VTT) are placed immediately after their associated video.
|
||||
*
|
||||
* @param int $thesisId
|
||||
* @param array|null $uploads Multi-file $_FILES entry.
|
||||
* @param string $folderPath Relative path from STORAGE_ROOT to the thesis folder.
|
||||
* @param string $filePrefix The shared file prefix.
|
||||
* @param array $post $_POST for per-file labels/orders.
|
||||
* @param int $startNum Starting number for TFE_XX (usually 01).
|
||||
*/
|
||||
protected function handleTfeFiles(int $thesisId, ?array $uploads, string $folderPath, string $filePrefix, array $post = [], int $startNum = 1): int
|
||||
{
|
||||
if (!$uploads || !is_array($uploads['name'] ?? null)) {
|
||||
return $startNum;
|
||||
}
|
||||
|
||||
$dir = STORAGE_ROOT . '/' . $folderPath;
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
$fileLabels = $post['file_labels'] ?? [];
|
||||
$fileOrders = $post['file_orders'] ?? [];
|
||||
|
||||
// Collect all files, classify, sort by hierarchy
|
||||
$files = [];
|
||||
$count = count($uploads['name']);
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
if (($uploads['error'][$i] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
|
||||
continue;
|
||||
}
|
||||
if (($uploads['error'][$i] ?? -1) !== UPLOAD_ERR_OK) {
|
||||
error_log("ThesisFileHandler: TFE upload error {$uploads['error'][$i]} for {$uploads['name'][$i]}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
||||
$mimeType = $finfo->file($uploads['tmp_name'][$i]);
|
||||
$ext = strtolower(pathinfo($uploads['name'][$i], PATHINFO_EXTENSION));
|
||||
|
||||
if ($mimeType === 'text/plain' && $ext === 'vtt') {
|
||||
$mimeType = 'text/vtt';
|
||||
}
|
||||
if ($mimeType === 'application/octet-stream' && !in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
|
||||
error_log("ThesisFileHandler: TFE extension not allowed {$uploads['name'][$i]} ($ext), skipping");
|
||||
continue;
|
||||
}
|
||||
if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true)
|
||||
&& !in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
|
||||
error_log("ThesisFileHandler: invalid TFE type {$uploads['name'][$i]} ($mimeType / $ext), skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
$isPdf = ($mimeType === 'application/pdf' || $ext === 'pdf');
|
||||
$sizeLimit = $isPdf ? self::MAX_PDF_SIZE : self::MAX_FILE_SIZE;
|
||||
if ($uploads['size'][$i] > $sizeLimit) {
|
||||
error_log("ThesisFileHandler: TFE file too large {$uploads['name'][$i]} (" . round($uploads['size'][$i] / 1024 / 1024) . ' MB), skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
$files[] = [
|
||||
'index' => $i,
|
||||
'mimeType' => $mimeType,
|
||||
'ext' => $ext,
|
||||
'size' => $uploads['size'][$i],
|
||||
'tmpName' => $uploads['tmp_name'][$i],
|
||||
'origName' => $uploads['name'][$i],
|
||||
'label' => trim($fileLabels[$i] ?? ''),
|
||||
'sortOrder' => isset($fileOrders[$i]) ? (int)$fileOrders[$i] : null,
|
||||
'hierarchy' => $this->tfeHierarchyRank($mimeType, $ext),
|
||||
'fileType' => $this->detectFileType($mimeType, $ext),
|
||||
];
|
||||
}
|
||||
|
||||
// Sort by hierarchy rank
|
||||
usort($files, fn($a, $b) => $a['hierarchy'] - $b['hierarchy']);
|
||||
|
||||
// Assign contiguous TFE_XX numbers
|
||||
$videoCount = 0;
|
||||
$vttQueue = [];
|
||||
|
||||
// First pass: count videos to know where to insert VTTs
|
||||
foreach ($files as $f) {
|
||||
if ($f['fileType'] === 'video') {
|
||||
$videoCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$num = $startNum;
|
||||
$vttIdx = 0;
|
||||
|
||||
foreach ($files as $f) {
|
||||
// VTT files are inserted right after their corresponding video
|
||||
if ($f['fileType'] === 'caption') {
|
||||
$vttQueue[] = $f;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($f['fileType'] === 'video') {
|
||||
// Write the video file
|
||||
$this->writeTfeFile($f, $thesisId, $dir, $folderPath, $filePrefix, $num);
|
||||
$num++;
|
||||
|
||||
// Write any waiting VTT for this video
|
||||
if (!empty($vttQueue)) {
|
||||
$vtt = array_shift($vttQueue);
|
||||
$this->writeTfeFile($vtt, $thesisId, $dir, $folderPath, $filePrefix, $num);
|
||||
$num++;
|
||||
}
|
||||
} else {
|
||||
$this->writeTfeFile($f, $thesisId, $dir, $folderPath, $filePrefix, $num);
|
||||
$num++;
|
||||
}
|
||||
}
|
||||
|
||||
// Any remaining VTTs (orphaned — write at end)
|
||||
foreach ($vttQueue as $vtt) {
|
||||
$this->writeTfeFile($vtt, $thesisId, $dir, $folderPath, $filePrefix, $num);
|
||||
$num++;
|
||||
}
|
||||
|
||||
return $num;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process annex file uploads.
|
||||
*
|
||||
* @param int $thesisId
|
||||
* @param array|null $uploads Multi-file $_FILES entry.
|
||||
* @param string $folderPath Relative path from STORAGE_ROOT to the thesis folder.
|
||||
* @param string $filePrefix The shared file prefix.
|
||||
* @param array $post $_POST for per-file labels/orders.
|
||||
*/
|
||||
protected function handleAnnexeFiles(int $thesisId, ?array $uploads, string $folderPath, string $filePrefix, array $post = []): void
|
||||
{
|
||||
if (!$uploads || !is_array($uploads['name'] ?? null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dir = STORAGE_ROOT . '/' . $folderPath;
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
$fileLabels = $post['annexe_labels'] ?? [];
|
||||
$fileOrders = $post['annexe_orders'] ?? [];
|
||||
|
||||
$num = 1;
|
||||
$count = count($uploads['name']);
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
if (($uploads['error'][$i] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
|
||||
continue;
|
||||
}
|
||||
if (($uploads['error'][$i] ?? -1) !== UPLOAD_ERR_OK) {
|
||||
error_log("ThesisFileHandler: annexe upload error {$uploads['error'][$i]} for {$uploads['name'][$i]}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
||||
$mimeType = $finfo->file($uploads['tmp_name'][$i]);
|
||||
$ext = strtolower(pathinfo($uploads['name'][$i], PATHINFO_EXTENSION));
|
||||
|
||||
if ($mimeType === 'text/plain' && $ext === 'vtt') {
|
||||
$mimeType = 'text/vtt';
|
||||
}
|
||||
|
||||
$sizeLimit = (($mimeType === 'application/pdf' || $ext === 'pdf') ? self::MAX_PDF_SIZE : self::MAX_FILE_SIZE);
|
||||
if ($uploads['size'][$i] > $sizeLimit) {
|
||||
error_log("ThesisFileHandler: annexe too large {$uploads['name'][$i]} (" . round($uploads['size'][$i] / 1024 / 1024) . ' MB), skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
$padded = sprintf('%02d', $num);
|
||||
$targetName = $filePrefix . '_ANNEXE_' . $padded . '.' . $ext;
|
||||
$targetPath = $dir . $targetName;
|
||||
|
||||
if (!move_uploaded_file($uploads['tmp_name'][$i], $targetPath)) {
|
||||
error_log("ThesisFileHandler: failed to move annexe {$uploads['name'][$i]}");
|
||||
continue;
|
||||
}
|
||||
|
||||
chmod($targetPath, 0644);
|
||||
$relPath = $folderPath . $targetName;
|
||||
$label = trim($fileLabels[$i] ?? '');
|
||||
$sortOrder = isset($fileOrders[$i]) ? (int)$fileOrders[$i] : null;
|
||||
$fileType = ($ext === 'vtt' || $mimeType === 'text/vtt') ? 'caption' : 'annex';
|
||||
|
||||
$this->db->insertThesisFile(
|
||||
$thesisId, $fileType,
|
||||
$relPath,
|
||||
basename($uploads['name'][$i]),
|
||||
$uploads['size'][$i],
|
||||
$mimeType,
|
||||
$label !== '' ? $label : null,
|
||||
$sortOrder
|
||||
);
|
||||
error_log("ThesisFileHandler: annexe uploaded → $targetName");
|
||||
$num++;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Write a single TFE file to disk and record in DB.
|
||||
*/
|
||||
protected function writeTfeFile(array $f, int $thesisId, string $dir, string $folderPath, string $filePrefix, int $num): void
|
||||
{
|
||||
$padded = sprintf('%02d', $num);
|
||||
$targetName = $filePrefix . '_TFE_' . $padded . '.' . $f['ext'];
|
||||
$targetPath = $dir . $targetName;
|
||||
|
||||
if (!move_uploaded_file($f['tmpName'], $targetPath)) {
|
||||
error_log("ThesisFileHandler: failed to move TFE {$f['origName']}");
|
||||
return;
|
||||
}
|
||||
|
||||
chmod($targetPath, 0644);
|
||||
$relPath = $folderPath . $targetName;
|
||||
|
||||
$this->db->insertThesisFile(
|
||||
$thesisId, $f['fileType'],
|
||||
$relPath,
|
||||
basename($f['origName']),
|
||||
$f['size'],
|
||||
$f['mimeType'],
|
||||
$f['label'] !== '' ? $f['label'] : null,
|
||||
$f['sortOrder']
|
||||
);
|
||||
error_log("ThesisFileHandler: TFE uploaded → $targetName ({$f['fileType']})");
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a hierarchy rank for sorting TFE files.
|
||||
* Lower = earlier in the sequence.
|
||||
*
|
||||
* 0 = PDF
|
||||
* 1 = video
|
||||
* 2 = audio
|
||||
* 3 = caption (VTT)
|
||||
* 4 = image
|
||||
* 5 = archive / other
|
||||
*/
|
||||
private function tfeHierarchyRank(string $mimeType, string $ext): int
|
||||
{
|
||||
if ($mimeType === 'application/pdf' || $ext === 'pdf') {
|
||||
return 0;
|
||||
}
|
||||
if ($ext === 'vtt' || $mimeType === 'text/vtt') {
|
||||
return 3;
|
||||
}
|
||||
if (str_starts_with($mimeType, 'video/') || in_array($ext, ['mp4','webm','ogv','mov'], true)) {
|
||||
return 1;
|
||||
}
|
||||
if (str_starts_with($mimeType, 'audio/') || in_array($ext, ['mp3','ogg','oga','wav','flac','aac','m4a'], true)) {
|
||||
return 2;
|
||||
}
|
||||
if (str_starts_with($mimeType, 'image/') || in_array($ext, ['jpg','jpeg','png','gif','webp'], true)) {
|
||||
return 4;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the logical file_type from MIME type / extension.
|
||||
*/
|
||||
protected function detectFileType(string $mimeType, string $ext): string
|
||||
{
|
||||
if ($ext === 'vtt' || $mimeType === 'text/vtt') {
|
||||
return 'caption';
|
||||
}
|
||||
if ($mimeType === 'application/pdf' || $ext === 'pdf') {
|
||||
return 'main';
|
||||
}
|
||||
if (str_starts_with($mimeType, 'video/') || in_array($ext, ['mp4','webm','ogv','mov'], true)) {
|
||||
return 'video';
|
||||
}
|
||||
if (str_starts_with($mimeType, 'audio/') || in_array($ext, ['mp3','ogg','oga','wav','flac','aac','m4a'], true)) {
|
||||
return 'audio';
|
||||
}
|
||||
if (str_starts_with($mimeType, 'image/') || in_array($ext, ['jpg','jpeg','png','gif','webp'], true)) {
|
||||
return 'image';
|
||||
}
|
||||
return 'other';
|
||||
}
|
||||
|
||||
// ── String / filesystem helpers ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Generate a filesystem-safe author slug from comma-separated author names.
|
||||
* Sorted alphabetically, joined with dash, accent-stripped, uppercase.
|
||||
*/
|
||||
protected function generateAuthorSlug(string $authorNames): string
|
||||
{
|
||||
$names = array_values(array_filter(array_map('trim', explode(',', $authorNames)), fn($n) => $n !== ''));
|
||||
sort($names, SORT_NATURAL);
|
||||
$joined = implode('-', $names);
|
||||
|
||||
$normalized = $joined;
|
||||
if (function_exists('iconv')) {
|
||||
$normalized = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $normalized);
|
||||
}
|
||||
$accents = [
|
||||
'à' => 'a', 'â' => 'a', 'ä' => 'a',
|
||||
'é' => 'e', 'è' => 'e', 'ê' => 'e', 'ë' => 'e',
|
||||
'î' => 'i', 'ï' => 'i',
|
||||
'ô' => 'o', 'ö' => 'o',
|
||||
'ù' => 'u', 'û' => 'u', 'ü' => 'u',
|
||||
'ç' => 'c',
|
||||
'À' => 'A', 'Â' => 'A', 'Ä' => 'A',
|
||||
'É' => 'E', 'È' => 'E', 'Ê' => 'E', 'Ë' => 'E',
|
||||
'Î' => 'I', 'Ï' => 'I',
|
||||
'Ô' => 'O', 'Ö' => 'O',
|
||||
'Ù' => 'U', 'Û' => 'U', 'Ü' => 'U',
|
||||
'Ç' => 'C',
|
||||
];
|
||||
$normalized = strtr($normalized, $accents);
|
||||
$slug = preg_replace('/[^A-Za-z0-9]+/', '_', $normalized);
|
||||
$slug = trim($slug, '_');
|
||||
$slug = strtoupper($slug);
|
||||
if ($slug === '') {
|
||||
$slug = 'AUTHOR';
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a filesystem-safe title slug (truncated to 60 chars).
|
||||
*/
|
||||
protected function generateTitleSlug(string $title): string
|
||||
{
|
||||
$normalized = $title;
|
||||
if (function_exists('iconv')) {
|
||||
$normalized = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $normalized);
|
||||
}
|
||||
$accents = [
|
||||
'à' => 'a', 'â' => 'a', 'ä' => 'a', 'æ' => 'ae',
|
||||
'é' => 'e', 'è' => 'e', 'ê' => 'e', 'ë' => 'e',
|
||||
'î' => 'i', 'ï' => 'i',
|
||||
'ô' => 'o', 'ö' => 'o', 'œ' => 'oe',
|
||||
'ù' => 'u', 'û' => 'u', 'ü' => 'u',
|
||||
'ç' => 'c',
|
||||
'À' => 'A', 'Â' => 'A', 'Ä' => 'A', 'Æ' => 'AE',
|
||||
'É' => 'E', 'È' => 'E', 'Ê' => 'E', 'Ë' => 'E',
|
||||
'Î' => 'I', 'Ï' => 'I',
|
||||
'Ô' => 'O', 'Ö' => 'O', 'Œ' => 'OE',
|
||||
'Ù' => 'U', 'Û' => 'U', 'Ü' => 'U',
|
||||
'Ç' => 'C',
|
||||
"'" => '', '"' => '', '«' => '', '»' => '',
|
||||
];
|
||||
$normalized = strtr($normalized, $accents);
|
||||
$slug = preg_replace('/[^A-Za-z0-9]+/', '_', $normalized);
|
||||
$slug = trim($slug, '_');
|
||||
$slug = strtoupper($slug);
|
||||
if (strlen($slug) > 60) {
|
||||
$slug = substr($slug, 0, 60);
|
||||
}
|
||||
if ($slug === '') {
|
||||
$slug = 'TFE';
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the folder path and file prefix for a thesis.
|
||||
*
|
||||
* Folder: theses/{YYYY}/{YYYY}_{AUTHORS}_{TITLE_SLUG}/
|
||||
* Prefix: {YYYY}_{AUTHORS}_{TITLE_SLUG}
|
||||
*
|
||||
* @return array{folderPath: string, filePrefix: string, folderName: string}
|
||||
*/
|
||||
protected function buildThesisFolder(int $year, string $authorsStr, string $title): array
|
||||
{
|
||||
$authorSlug = $this->generateAuthorSlug($authorsStr);
|
||||
$titleSlug = $this->generateTitleSlug($title);
|
||||
|
||||
$folderName = $year . '_' . $authorSlug . '_' . $titleSlug;
|
||||
$filePrefix = $folderName;
|
||||
$folderPath = 'theses/' . $year . '/' . $folderName . '/';
|
||||
|
||||
return [
|
||||
'folderPath' => $folderPath,
|
||||
'filePrefix' => $filePrefix,
|
||||
'folderName' => $folderName,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the thesis folder exists (create if needed).
|
||||
* If the folder already exists, append a numeric suffix.
|
||||
*/
|
||||
protected function ensureUniqueFolder(string $folderPath): string
|
||||
{
|
||||
$baseDir = STORAGE_ROOT . '/' . dirname($folderPath) . '/';
|
||||
$candidate = basename(rtrim($folderPath, '/'));
|
||||
|
||||
if (!is_dir($baseDir . $candidate)) {
|
||||
mkdir($baseDir . $candidate, 0755, true);
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
$suffix = 1;
|
||||
while (is_dir($baseDir . $candidate . '_' . $suffix)) {
|
||||
$suffix++;
|
||||
}
|
||||
$candidate .= '_' . $suffix;
|
||||
mkdir($baseDir . $candidate, 0755, true);
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user