mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-08-10 23:31:21 +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:
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/ThesisFileHandler.php';
|
||||
|
||||
/**
|
||||
* ThesisEditController
|
||||
*
|
||||
@@ -18,6 +20,8 @@
|
||||
*/
|
||||
class ThesisEditController
|
||||
{
|
||||
use ThesisFileHandler;
|
||||
|
||||
private Database $db;
|
||||
|
||||
public function __construct(Database $db)
|
||||
@@ -305,10 +309,39 @@ class ThesisEditController
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// ── Resolve thesis folder path (reuse existing or build new) ────────
|
||||
$year = intval($post['année'] ?? date('Y'));
|
||||
$title = trim($post['titre'] ?? '');
|
||||
$authors = trim($post['auteurice'] ?? '');
|
||||
|
||||
$tf = $this->buildThesisFolder($year, $authors, $title);
|
||||
$folderPath = $tf['folderPath'];
|
||||
$filePrefix = $tf['filePrefix'];
|
||||
|
||||
// Reuse existing folder if this thesis already has files on disk
|
||||
$existingFiles = $this->db->getThesisFiles($thesisId);
|
||||
foreach ($existingFiles as $f) {
|
||||
$fp = $f['file_path'] ?? '';
|
||||
if (str_starts_with($fp, 'theses/')) {
|
||||
$parts = explode('/', $fp);
|
||||
if (count($parts) >= 3) {
|
||||
$folderName = $parts[2];
|
||||
$folderPath = 'theses/' . $year . '/' . $folderName . '/';
|
||||
$filePrefix = $folderName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the folder exists
|
||||
$dirAbs = STORAGE_ROOT . '/' . $folderPath;
|
||||
if (!is_dir($dirAbs)) {
|
||||
mkdir($dirAbs, 0755, true);
|
||||
}
|
||||
|
||||
// ── Cover image (outside transaction — filesystem op) ─────────────────
|
||||
if (isset($post['remove_cover'])) {
|
||||
$allFiles = $this->db->getThesisFiles($thesisId);
|
||||
foreach ($allFiles as $f) {
|
||||
foreach ($existingFiles as $f) {
|
||||
if ($f['file_type'] === 'cover') {
|
||||
$this->db->deleteThesisFile((int)$f['id'], $thesisId);
|
||||
if (!empty($f['file_path']) && defined('STORAGE_ROOT')) {
|
||||
@@ -321,9 +354,27 @@ class ThesisEditController
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->db->handleCoverUpload($thesisId, $files['couverture'] ?? null);
|
||||
$this->handleCoverUpload($thesisId, $files['couverture'] ?? null, $folderPath, $filePrefix);
|
||||
}
|
||||
|
||||
// ── Note d'intention (replace if uploaded) ────────────────────────────
|
||||
// Remove old note_intention row+file if new one is uploaded
|
||||
if (!empty($files['note_intention']['tmp_name'] ?? null) && ($files['note_intention']['error'] ?? -1) === UPLOAD_ERR_OK) {
|
||||
foreach ($existingFiles as $f) {
|
||||
if ($f['file_type'] === 'note_intention') {
|
||||
$this->db->deleteThesisFile((int)$f['id'], $thesisId);
|
||||
if (!empty($f['file_path']) && defined('STORAGE_ROOT')) {
|
||||
$abs = STORAGE_ROOT . '/' . $f['file_path'];
|
||||
if (file_exists($abs)) {
|
||||
@unlink($abs);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->handleNoteIntentionUpload($thesisId, $files['note_intention'] ?? null, $folderPath, $filePrefix);
|
||||
|
||||
// ── Delete individual thesis files ────────────────────────────────────
|
||||
$deleteIds = isset($post['delete_files']) && is_array($post['delete_files'])
|
||||
? array_map('intval', $post['delete_files'])
|
||||
@@ -360,9 +411,18 @@ class ThesisEditController
|
||||
}
|
||||
}
|
||||
|
||||
// ── New thesis files upload ───────────────────────────────────────────
|
||||
// ── New TFE files upload ─────────────────────────────────────────────
|
||||
if (!empty($files['files']['name'][0])) {
|
||||
$this->handleThesisFiles($thesisId, $post, $files['files']);
|
||||
// Count existing TFE files to determine starting number
|
||||
$tfeCount = 0;
|
||||
foreach ($existingFiles as $f) {
|
||||
if (!in_array($f['file_type'] ?? '', ['cover', 'note_intention', 'website', 'annex', 'caption'], true)
|
||||
&& !str_starts_with($f['file_path'] ?? '', 'http')) {
|
||||
$tfeCount++;
|
||||
}
|
||||
// Don't count captions as separate TFE entries — they'll be renumbered
|
||||
}
|
||||
$this->handleTfeFiles($thesisId, $files['files'], $folderPath, $filePrefix, $post, $tfeCount + 1);
|
||||
}
|
||||
|
||||
// ── PeerTube video / audio uploads ────────────────────────────────────
|
||||
@@ -373,206 +433,6 @@ class ThesisEditController
|
||||
$this->handleWebsiteUrl($thesisId, $post);
|
||||
}
|
||||
|
||||
// ── Private: file uploads ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Process multiple new thesis-file uploads.
|
||||
*
|
||||
* Files are stored in the existing folder used by this thesis (detected
|
||||
* from any current thesis_files row), or a new one is created following
|
||||
* the same {year}_{authorSlug} convention as ThesisCreateController.
|
||||
*/
|
||||
private function handleThesisFiles(int $thesisId, array $post, array $uploads): void
|
||||
{
|
||||
$allowedMimes = [
|
||||
'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',
|
||||
];
|
||||
$allowedExts = [
|
||||
'jpg', 'jpeg', 'png', 'gif', 'webp',
|
||||
'pdf',
|
||||
'mp4', 'webm', 'ogv', 'mov',
|
||||
'mp3', 'ogg', 'oga', 'wav', 'flac', 'aac', 'm4a',
|
||||
'vtt',
|
||||
'zip', 'tar', 'gz', 'tgz',
|
||||
];
|
||||
$maxBytes = 500 * 1024 * 1024; // 500 MB
|
||||
$maxPdfBytes = 100 * 1024 * 1024; // 100 MB for PDFs
|
||||
|
||||
$year = (int)($post['année'] ?? date('Y'));
|
||||
$authorName = trim($post['auteurice'] ?? 'unknown');
|
||||
|
||||
// Sort the raw comma-separated string alphabetically, then slugify.
|
||||
$names = array_values(array_filter(array_map('trim', explode(',', $authorName)), fn ($n) => $n !== ''));
|
||||
sort($names, SORT_NATURAL);
|
||||
$authorSlug = $this->generateAuthorSlug(implode(', ', $names));
|
||||
|
||||
// Per-file labels and sort orders submitted alongside the upload inputs
|
||||
$fileLabels = $post['file_labels'] ?? [];
|
||||
$fileOrders = $post['file_orders'] ?? [];
|
||||
|
||||
// Reuse existing folder if possible
|
||||
$existingFiles = $this->db->getThesisFiles($thesisId);
|
||||
$uploadDir = null;
|
||||
$folderName = null;
|
||||
foreach ($existingFiles as $f) {
|
||||
if (str_starts_with($f['file_path'] ?? '', 'theses/')) {
|
||||
$parts = explode('/', $f['file_path']);
|
||||
if (count($parts) >= 3) {
|
||||
$folderName = $parts[2];
|
||||
$uploadDir = STORAGE_ROOT . "/theses/{$year}/{$folderName}/";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($uploadDir === null) {
|
||||
$folderName = $this->ensureUniqueFolder($year, $authorSlug);
|
||||
$uploadDir = STORAGE_ROOT . "/theses/{$year}/{$folderName}/";
|
||||
}
|
||||
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
$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("ThesisEditController: 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';
|
||||
}
|
||||
|
||||
// Allow any ext-matched file even if finfo returns application/octet-stream
|
||||
if (!in_array($mimeType, $allowedMimes, true) && !in_array($ext, $allowedExts, true)) {
|
||||
error_log("ThesisEditController: invalid type {$uploads['name'][$i]} ($mimeType / $ext), skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
$isPdf = ($mimeType === 'application/pdf' || $ext === 'pdf');
|
||||
$sizeLimit = $isPdf ? $maxPdfBytes : $maxBytes;
|
||||
if ($uploads['size'][$i] > $sizeLimit) {
|
||||
error_log("ThesisEditController: file too large {$uploads['name'][$i]} (" . round($uploads['size'][$i] / 1024 / 1024) . ' MB), skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
$originalName = $uploads['name'][$i];
|
||||
$sanitized = $this->sanitizeFilename($originalName);
|
||||
$prefix = $authorSlug . '_' . $sanitized;
|
||||
$candidate = $prefix;
|
||||
$suffix = 1;
|
||||
while (file_exists($uploadDir . $candidate)) {
|
||||
$candidate = $authorSlug . '_' . pathinfo($sanitized, PATHINFO_FILENAME) . '_' . $suffix . '.' . $ext;
|
||||
$suffix++;
|
||||
}
|
||||
|
||||
$targetPath = $uploadDir . $candidate;
|
||||
if (!move_uploaded_file($uploads['tmp_name'][$i], $targetPath)) {
|
||||
error_log("ThesisEditController: failed to move {$originalName}");
|
||||
continue;
|
||||
}
|
||||
|
||||
chmod($targetPath, 0644);
|
||||
|
||||
$fileType = $this->detectFileType($mimeType, $ext, $originalName);
|
||||
$label = trim($fileLabels[$i] ?? '');
|
||||
$sortOrder = isset($fileOrders[$i]) ? (int)$fileOrders[$i] : null;
|
||||
|
||||
$relPath = "theses/{$year}/{$folderName}/" . $candidate;
|
||||
$this->db->insertThesisFile(
|
||||
$thesisId,
|
||||
$fileType,
|
||||
$relPath,
|
||||
basename($originalName),
|
||||
$uploads['size'][$i],
|
||||
$mimeType,
|
||||
$label !== '' ? $label : null,
|
||||
$sortOrder
|
||||
);
|
||||
error_log("ThesisEditController: uploaded → $candidate ($fileType)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the logical file_type from MIME type, extension, and original filename.
|
||||
*/
|
||||
private function detectFileType(string $mimeType, string $ext, string $originalName): string
|
||||
{
|
||||
if ($ext === 'vtt' || $mimeType === 'text/vtt') {
|
||||
return 'caption';
|
||||
}
|
||||
if (str_starts_with($mimeType, 'audio/') || in_array($ext, ['mp3','ogg','oga','wav','flac','aac','m4a'], true)) {
|
||||
return 'audio';
|
||||
}
|
||||
if (str_starts_with($mimeType, 'video/') || in_array($ext, ['mp4','webm','ogv','mov'], true)) {
|
||||
return 'video';
|
||||
}
|
||||
if ($mimeType === 'application/pdf' || $ext === 'pdf') {
|
||||
return 'main';
|
||||
}
|
||||
if (str_starts_with($mimeType, 'image/') || in_array($ext, ['jpg','jpeg','png','gif','webp'], true)) {
|
||||
return 'image';
|
||||
}
|
||||
return 'other';
|
||||
}
|
||||
|
||||
// ── Private: string helpers ───────────────────────────────────────────────
|
||||
|
||||
private function generateAuthorSlug(string $authorName): string
|
||||
{
|
||||
$n = function_exists('iconv') ? iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $authorName) : $authorName;
|
||||
$accents = [
|
||||
'à' => 'a','â' => 'a','ä' => 'a','é' => 'e','è' => 'e','ê' => 'e','ë' => 'e',
|
||||
'î' => 'i','ï' => 'i','ô' => 'o','ö' => 'o','ù' => 'u','û' => 'u','ü' => 'u','ç' => 'c',
|
||||
];
|
||||
$n = strtr($n, $accents);
|
||||
$slug = strtoupper(trim(preg_replace('/[^A-Za-z0-9]+/', '_', $n), '_'));
|
||||
return $slug !== '' ? $slug : 'AUTHOR';
|
||||
}
|
||||
|
||||
private function sanitizeFilename(string $filename): string
|
||||
{
|
||||
$ext = pathinfo($filename, PATHINFO_EXTENSION);
|
||||
$name = pathinfo($filename, PATHINFO_FILENAME);
|
||||
$n = function_exists('iconv') ? iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $name) : $name;
|
||||
$accents = [
|
||||
'à' => 'a','â' => 'a','ä' => 'a','é' => 'e','è' => 'e','ê' => 'e','ë' => 'e',
|
||||
'î' => 'i','ï' => 'i','ô' => 'o','ö' => 'o','ù' => 'u','û' => 'u','ü' => 'u','ç' => 'c',
|
||||
];
|
||||
$n = trim(preg_replace('/[^A-Za-z0-9]+/', '_', strtr($n, $accents)), '_');
|
||||
if ($n === '') {
|
||||
$n = 'file';
|
||||
}
|
||||
return $ext !== '' ? $n . '.' . strtolower($ext) : $n;
|
||||
}
|
||||
|
||||
private function ensureUniqueFolder(int $year, string $authorSlug): string
|
||||
{
|
||||
$baseDir = STORAGE_ROOT . '/theses/' . $year . '/';
|
||||
$candidate = $year . '_' . $authorSlug;
|
||||
$suffix = 1;
|
||||
while (is_dir($baseDir . $candidate)) {
|
||||
$candidate = $year . '_' . $authorSlug . '_' . $suffix++;
|
||||
}
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
// ── WCAG 3.3.1 helper ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user