mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 11:09:18 +02:00
feat: prevent duplicate TFE submissions with logging and user feedback
- Add DuplicateThesisException (typed, carries existing thesis metadata) - Add Database::findDuplicateThesis(): matches on year + author + normalised title (exact, prefix, Levenshtein ≤10% of longer string) - ThesisCreateController::submit() runs duplicate check before any DB write and throws DuplicateThesisException on match - AppLogger::logDuplicate() writes status=duplicate entries to the JSON-lines log for audit purposes - App::flash/consumeFlash extended to support 'warning' flash type - admin/actions/formulaire.php: catches DuplicateThesisException, logs it, flashes an HTML warning toast with a clickable link to the existing thesis, and repopulates the form fields - partage/index.php: same catch block; surfaces a plain-text flash-warning banner on the student form with identifier, title, and year of the match; form is repopulated via session - toast.php: renders toast--warning variant - admin.css: .toast--warning + link colour rules - form.css: .flash-warning style for the partage form
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ThesisEditController
|
||||
*
|
||||
@@ -68,12 +69,12 @@ class ThesisEditController
|
||||
public function load(int $thesisId): array
|
||||
{
|
||||
if ($thesisId <= 0) {
|
||||
throw new InvalidArgumentException("ID invalide");
|
||||
throw new InvalidArgumentException('ID invalide');
|
||||
}
|
||||
|
||||
$thesis = $this->db->getThesis($thesisId);
|
||||
if (!$thesis) {
|
||||
throw new RuntimeException("TFE non trouvé");
|
||||
throw new RuntimeException('TFE non trouvé');
|
||||
}
|
||||
|
||||
$currentLanguages = $this->db->getThesisLanguageIds($thesisId);
|
||||
@@ -157,7 +158,7 @@ class ThesisEditController
|
||||
public function save(int $thesisId, array $post, array $files): void
|
||||
{
|
||||
if ($thesisId <= 0) {
|
||||
throw new InvalidArgumentException("ID de TFE invalide.");
|
||||
throw new InvalidArgumentException('ID de TFE invalide.');
|
||||
}
|
||||
|
||||
$this->db->beginTransaction();
|
||||
@@ -253,7 +254,9 @@ class ThesisEditController
|
||||
$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);
|
||||
if (file_exists($abs)) {
|
||||
@unlink($abs);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -267,11 +270,15 @@ class ThesisEditController
|
||||
? array_map('intval', $post['delete_files'])
|
||||
: [];
|
||||
foreach ($deleteIds as $fileId) {
|
||||
if ($fileId <= 0) continue;
|
||||
if ($fileId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$filePath = $this->db->deleteThesisFile($fileId, $thesisId);
|
||||
if ($filePath && defined('STORAGE_ROOT')) {
|
||||
$abs = STORAGE_ROOT . '/' . $filePath;
|
||||
if (file_exists($abs)) @unlink($abs);
|
||||
if (file_exists($abs)) {
|
||||
@unlink($abs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +291,9 @@ class ThesisEditController
|
||||
if (!empty($post['file_label']) && is_array($post['file_label'])) {
|
||||
foreach ($post['file_label'] as $fileId => $label) {
|
||||
$fileId = (int)$fileId;
|
||||
if ($fileId <= 0) continue;
|
||||
if ($fileId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$this->db->updateThesisFileLabel($fileId, $thesisId, trim($label) ?: null);
|
||||
}
|
||||
}
|
||||
@@ -360,7 +369,9 @@ class ThesisEditController
|
||||
|
||||
$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] ?? 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;
|
||||
@@ -409,8 +420,12 @@ class ThesisEditController
|
||||
|
||||
$relPath = "theses/{$year}/{$folderName}/" . $candidate;
|
||||
$this->db->insertThesisFile(
|
||||
$thesisId, $fileType, $relPath,
|
||||
basename($originalName), $uploads['size'][$i], $mimeType,
|
||||
$thesisId,
|
||||
$fileType,
|
||||
$relPath,
|
||||
basename($originalName),
|
||||
$uploads['size'][$i],
|
||||
$mimeType,
|
||||
$label !== '' ? $label : null,
|
||||
$sortOrder
|
||||
);
|
||||
@@ -423,11 +438,21 @@ class ThesisEditController
|
||||
*/
|
||||
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';
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -437,8 +462,8 @@ class ThesisEditController
|
||||
{
|
||||
$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',
|
||||
'à' => '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), '_'));
|
||||
@@ -451,11 +476,13 @@ class ThesisEditController
|
||||
$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',
|
||||
'à' => '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';
|
||||
if ($n === '') {
|
||||
$n = 'file';
|
||||
}
|
||||
return $ext !== '' ? $n . '.' . strtolower($ext) : $n;
|
||||
}
|
||||
|
||||
@@ -480,10 +507,18 @@ class ThesisEditController
|
||||
*/
|
||||
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';
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user