diff --git a/public/admin/actions/formulaire.php b/public/admin/actions/formulaire.php index af09280..6e3af93 100644 --- a/public/admin/actions/formulaire.php +++ b/public/admin/actions/formulaire.php @@ -183,27 +183,13 @@ try { $db->setThesisJury($thesisId, $juryMembers); // ===== LINK LANGUAGES TO THESIS ===== - foreach ($languageIds as $languageId) { - $stmt = $pdo->prepare("INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?, ?)"); - $stmt->execute([$thesisId, $languageId]); - } + $db->setThesisLanguages($thesisId, $languageIds); // ===== LINK FORMATS TO THESIS ===== - foreach ($formatIds as $formatId) { - $stmt = $pdo->prepare("INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?, ?)"); - $stmt->execute([$thesisId, $formatId]); - } + $db->setThesisFormats($thesisId, $formatIds); // ===== LINK TAGS TO THESIS ===== - foreach ($keywords as $keyword) { - if (!empty($keyword)) { - $tagId = $db->findOrCreateTag($keyword); - if ($tagId) { - $stmt = $pdo->prepare("INSERT OR IGNORE INTO thesis_tags (tag_id, thesis_id) VALUES (?, ?)"); - $stmt->execute([$tagId, $thesisId]); - } - } - } + $db->setThesisTags($thesisId, $keywords); // ===== HANDLE FILE UPLOADS ===== @@ -266,28 +252,7 @@ try { } // Process banner image - if ($bannerFile && isset($bannerFile["error"]) && $bannerFile["error"] === UPLOAD_ERR_OK) { - $finfo = new finfo(FILEINFO_MIME_TYPE); - $mimeType = $finfo->file($bannerFile["tmp_name"]); - $fileExtension = strtolower(pathinfo($bannerFile["name"], PATHINFO_EXTENSION)); - $allowedBannerMimes = ['image/jpeg', 'image/png', 'image/webp']; - $allowedBannerExts = ['jpg', 'jpeg', 'png', 'webp']; - $maxBannerSize = 5 * 1024 * 1024; // 5 MB - - if (in_array($mimeType, $allowedBannerMimes) && in_array($fileExtension, $allowedBannerExts) - && $bannerFile["size"] <= $maxBannerSize) { - $randomName = bin2hex(random_bytes(16)); - $safeFileName = $randomName . "." . $fileExtension; - $targetFile = $bannerDir . $safeFileName; - if (move_uploaded_file($bannerFile["tmp_name"], $targetFile)) { - chmod($targetFile, 0644); - $db->setBannerPath($thesisId, "banners/" . $safeFileName); - error_log("Banner image uploaded: " . $safeFileName); - } - } else { - error_log("Invalid or oversized banner image: " . $bannerFile["name"]); - } - } + $db->handleBannerUpload($thesisId, $bannerFile ?: null); // Process thesis files if ($files && is_array($files["name"])) { diff --git a/public/admin/edit.php b/public/admin/edit.php index e54d730..a1c6b74 100644 --- a/public/admin/edit.php +++ b/public/admin/edit.php @@ -109,71 +109,28 @@ try { $db->setThesisJury($thesisId, $editJuryMembers); // Update languages - $pdo->prepare("DELETE FROM thesis_languages WHERE thesis_id = ?")->execute([$thesisId]); - if (isset($_POST['languages']) && is_array($_POST['languages'])) { - foreach ($_POST['languages'] as $languageId) { - $stmt = $pdo->prepare("INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?, ?)"); - $stmt->execute([$thesisId, intval($languageId)]); - } - } + $db->setThesisLanguages($thesisId, isset($_POST['languages']) && is_array($_POST['languages']) ? $_POST['languages'] : []); // Update formats - $pdo->prepare("DELETE FROM thesis_formats WHERE thesis_id = ?")->execute([$thesisId]); - if (isset($_POST['formats']) && is_array($_POST['formats'])) { - foreach ($_POST['formats'] as $formatId) { - $stmt = $pdo->prepare("INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?, ?)"); - $stmt->execute([$thesisId, intval($formatId)]); - } - } + $db->setThesisFormats($thesisId, isset($_POST['formats']) && is_array($_POST['formats']) ? $_POST['formats'] : []); // Update tags - $pdo->prepare("DELETE FROM thesis_tags WHERE thesis_id = ?")->execute([$thesisId]); $keywordsRaw = trim($_POST['tag'] ?? ''); - if (!empty($keywordsRaw)) { - $keywords = array_map('trim', explode(',', $keywordsRaw)); - $keywords = array_slice($keywords, 0, 10); // Max 10 - foreach ($keywords as $keyword) { - if (!empty($keyword)) { - $tagId = $db->findOrCreateTag($keyword); - if ($tagId) { - $stmt = $pdo->prepare("INSERT OR IGNORE INTO thesis_tags (tag_id, thesis_id) VALUES (?, ?)"); - $stmt->execute([$tagId, $thesisId]); - } - } - } - } + $editKeywords = !empty($keywordsRaw) ? array_map('trim', explode(',', $keywordsRaw)) : []; + $db->setThesisTags($thesisId, $editKeywords); $db->commit(); // Handle banner upload/removal (after commit, outside transaction) - $bannerDir = defined('STORAGE_ROOT') ? STORAGE_ROOT . "/banners/" : null; - if ($bannerDir && !file_exists($bannerDir)) { - mkdir($bannerDir, 0755, true); - } if (isset($_POST['remove_banner'])) { - // Unlink existing banner file if present $currentBannerPath = $db->getThesisBannerPath($thesisId); - if ($currentBannerPath && $bannerDir) { + if ($currentBannerPath && defined('STORAGE_ROOT')) { $absPath = STORAGE_ROOT . '/' . $currentBannerPath; if (file_exists($absPath)) unlink($absPath); } $db->setBannerPath($thesisId, null); - } elseif (isset($_FILES['banner']) && $_FILES['banner']['error'] === UPLOAD_ERR_OK && $bannerDir) { - $bannerFile = $_FILES['banner']; - $finfo = new finfo(FILEINFO_MIME_TYPE); - $mimeType = $finfo->file($bannerFile["tmp_name"]); - $fileExtension = strtolower(pathinfo($bannerFile["name"], PATHINFO_EXTENSION)); - $allowedBannerMimes = ['image/jpeg', 'image/png', 'image/webp']; - $allowedBannerExts = ['jpg', 'jpeg', 'png', 'webp']; - if (in_array($mimeType, $allowedBannerMimes) && in_array($fileExtension, $allowedBannerExts) - && $bannerFile["size"] <= 5 * 1024 * 1024) { - $randomName = bin2hex(random_bytes(16)); - $safeFileName = $randomName . '.' . $fileExtension; - if (move_uploaded_file($bannerFile["tmp_name"], $bannerDir . $safeFileName)) { - chmod($bannerDir . $safeFileName, 0644); - $db->setBannerPath($thesisId, "banners/" . $safeFileName); - } - } + } else { + $db->handleBannerUpload($thesisId, $_FILES['banner'] ?? null); } $success = "TFE mis à jour avec succès!"; diff --git a/src/Database.php b/src/Database.php index 015686c..4279c91 100644 --- a/src/Database.php +++ b/src/Database.php @@ -869,6 +869,71 @@ class Database { } } + // ======================================================================== + // JUNCTION-TABLE HELPERS (delete-then-reinsert pattern) + // ======================================================================== + + /** + * Replace all language associations for a thesis. + * @param int $thesisId + * @param int[] $languageIds IDs from the languages table + */ + public function setThesisLanguages(int $thesisId, array $languageIds): void { + $this->pdo->prepare("DELETE FROM thesis_languages WHERE thesis_id = ?")->execute([$thesisId]); + $stmt = $this->pdo->prepare( + "INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?, ?)" + ); + foreach ($languageIds as $langId) { + $id = (int)$langId; + if ($id > 0) { + $stmt->execute([$thesisId, $id]); + } + } + } + + /** + * Replace all format associations for a thesis. + * @param int $thesisId + * @param int[] $formatIds IDs from the format_types table + */ + public function setThesisFormats(int $thesisId, array $formatIds): void { + $this->pdo->prepare("DELETE FROM thesis_formats WHERE thesis_id = ?")->execute([$thesisId]); + $stmt = $this->pdo->prepare( + "INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?, ?)" + ); + foreach ($formatIds as $fmtId) { + $id = (int)$fmtId; + if ($id > 0) { + $stmt->execute([$thesisId, $id]); + } + } + } + + /** + * Replace all tag associations for a thesis. + * Tags are identified by name (findOrCreateTag is called for each). + * Empty / whitespace-only names are silently skipped. + * Maximum 10 tags are accepted; extras are ignored. + * + * @param int $thesisId + * @param string[] $tagNames + */ + public function setThesisTags(int $thesisId, array $tagNames): void { + $this->pdo->prepare("DELETE FROM thesis_tags WHERE thesis_id = ?")->execute([$thesisId]); + $stmt = $this->pdo->prepare( + "INSERT OR IGNORE INTO thesis_tags (tag_id, thesis_id) VALUES (?, ?)" + ); + $count = 0; + foreach ($tagNames as $name) { + if ($count >= 10) break; + $tagId = $this->findOrCreateTag($name); // trims, returns null for empty + if ($tagId !== null) { + $stmt->execute([$tagId, $thesisId]); + $count++; + } + } + } + // ======================================================================== // BANNER METHODS // ======================================================================== @@ -883,6 +948,64 @@ class Database { $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 // ======================================================================== diff --git a/src/cache/rate_limit/ad921d60486366258809553a3db49a4a.json b/src/cache/rate_limit/ad921d60486366258809553a3db49a4a.json index fc1eee3..bb96814 100644 --- a/src/cache/rate_limit/ad921d60486366258809553a3db49a4a.json +++ b/src/cache/rate_limit/ad921d60486366258809553a3db49a4a.json @@ -1 +1 @@ -[1774701765] \ No newline at end of file +[1774702033] \ No newline at end of file diff --git a/storage/test.db b/storage/test.db index 843b8cc..ec43233 100644 Binary files a/storage/test.db and b/storage/test.db differ