beginTransaction(); // ===== VALIDATE AND SANITIZE INPUT DATA ===== // Author information $auteurName = validate_required(sanitize_string($_POST["auteurice"] ?? ''), "Nom/Prénom/Pseudo"); $mail = $_POST["mail"] ?? ''; if (!empty($mail)) { // Could be email or social media handle $mail = sanitize_string($mail); } // Year validation $annee = filter_var($_POST["année"] ?? '', FILTER_VALIDATE_INT); if ($annee === false || $annee < 2000 || $annee > (int)date('Y') + 1) { throw new Exception("Année invalide. Veuillez entrer une année valide."); } // Academic details $orientationId = filter_var($_POST["orientation"] ?? '', FILTER_VALIDATE_INT); if ($orientationId === false) { throw new Exception("Veuillez sélectionner une orientation."); } $apProgramId = filter_var($_POST["ap"] ?? '', FILTER_VALIDATE_INT); if ($apProgramId === false) { throw new Exception("Veuillez sélectionner un Atelier Pratique."); } $finalityId = filter_var($_POST["finality"] ?? '', FILTER_VALIDATE_INT); if ($finalityId === false) { throw new Exception("Veuillez sélectionner une finalité."); } // Thesis content $titre = validate_required(sanitize_string($_POST["titre"] ?? ''), "Titre du mémoire"); $subtitle = sanitize_string($_POST["subtitle"] ?? ''); $synopsis = validate_required(sanitize_string($_POST["synopsis"] ?? ''), "Synopsis"); $durationInfo = sanitize_string($_POST["duration_info"] ?? ''); // Jury members $juryMembers = []; if (!empty(trim($_POST['jury_president'] ?? ''))) { $juryMembers[] = ['name' => trim($_POST['jury_president']), 'role' => 'president', 'is_external' => 0]; } if (!empty(trim($_POST['jury_promoteur'] ?? ''))) { $juryMembers[] = ['name' => trim($_POST['jury_promoteur']), 'role' => 'promoteur', 'is_external' => isset($_POST['jury_promoteur_ext']) ? 1 : 0]; } foreach ($_POST['jury_lecteurs'] ?? [] as $i => $name) { $name = trim($name); if ($name !== '') { $juryMembers[] = ['name' => $name, 'role' => 'lecteur', 'is_external' => isset($_POST['jury_lecteurs_ext'][$i]) ? 1 : 0]; } } // Keywords (max 10) $tagRaw = sanitize_string($_POST["tag"] ?? ''); $keywords = !empty($tagRaw) ? array_map('trim', explode(',', $tagRaw)) : []; if (count($keywords) > 10) { throw new Exception("Maximum 10 mots-clés autorisés."); } // Languages (at least one required) $languageIds = $_POST["languages"] ?? []; if (empty($languageIds)) { throw new Exception("Veuillez sélectionner au moins une langue."); } $languageIds = array_map('intval', $languageIds); // Formats (optional, multiple selection) $formatIds = isset($_POST["formats"]) ? array_map('intval', $_POST["formats"]) : []; // License $licenseId = filter_var($_POST['license_id'] ?? '', FILTER_VALIDATE_INT) ?: null; // External link $lien = $_POST["lien"] ?? ''; if (!empty($lien)) { $lien = filter_var($lien, FILTER_VALIDATE_URL); if ($lien === false) { throw new Exception("Lien URL invalide."); } } // File uploads $couverture = $_FILES["couverture"] ?? null; $bannerFile = $_FILES["banner"] ?? null; $files = $_FILES["files"] ?? null; // ===== CREATE OR FIND AUTHOR ===== $authorId = $db->findOrCreateAuthor($auteurName, $mail); error_log("Author ID: $authorId"); // ===== INSERT THESIS RECORD + LINK AUTHOR ===== $thesisId = $db->createThesis([ 'year' => $annee, 'orientation_id' => $orientationId, 'ap_program_id' => $apProgramId, 'finality_id' => $finalityId, 'title' => $titre, 'subtitle' => $subtitle, 'synopsis' => $synopsis, 'file_size_info' => $durationInfo, 'baiu_link' => $lien, 'license_id' => $licenseId, 'author_id' => $authorId, ]); $identifier = $db->getThesisIdentifier($thesisId); error_log("Thesis ID: $thesisId (identifier: $identifier)"); // ===== LINK JURY TO THESIS ===== $db->setThesisJury($thesisId, $juryMembers); // ===== LINK LANGUAGES TO THESIS ===== $db->setThesisLanguages($thesisId, $languageIds); // ===== LINK FORMATS TO THESIS ===== $db->setThesisFormats($thesisId, $formatIds); // ===== LINK TAGS TO THESIS ===== $db->setThesisTags($thesisId, $keywords); // ===== HANDLE FILE UPLOADS ===== // Create necessary directories — outside the webroot (security items #3 & #4). // Files are served through /media.php, never directly via a URL path. $uploadBaseDir = STORAGE_ROOT . "/theses/{$annee}/{$identifier}/"; $coverDir = STORAGE_ROOT . "/covers/"; $bannerDir = STORAGE_ROOT . "/banners/"; if (!file_exists($uploadBaseDir)) { mkdir($uploadBaseDir, 0755, true); } if (!file_exists($coverDir)) { mkdir($coverDir, 0755, true); } if (!file_exists($bannerDir)) { mkdir($bannerDir, 0755, true); } // Define security constraints $allowedMimeTypes = ['image/jpeg', 'image/png', 'application/pdf', 'video/mp4', 'application/zip', 'text/vtt']; $allowedExtensions = ['jpg', 'jpeg', 'png', 'pdf', 'mp4', 'zip', 'vtt']; $maxFileSize = 50 * 1024 * 1024; // 50 MB // Process cover image $coverPath = null; if ($couverture && isset($couverture["error"]) && $couverture["error"] === UPLOAD_ERR_OK) { $finfo = new finfo(FILEINFO_MIME_TYPE); $mimeType = $finfo->file($couverture["tmp_name"]); $fileExtension = strtolower(pathinfo($couverture["name"], PATHINFO_EXTENSION)); // Only allow image files for cover if (in_array($mimeType, ['image/jpeg', 'image/png']) && in_array($fileExtension, ['jpg', 'jpeg', 'png'])) { // Generate random filename $randomName = bin2hex(random_bytes(16)); $safeFileName = $randomName . "." . $fileExtension; $targetFile = $coverDir . $safeFileName; if (move_uploaded_file($couverture["tmp_name"], $targetFile)) { chmod($targetFile, 0644); // Path stored relative to STORAGE_ROOT; served via /media.php?path=... $coverPath = "covers/" . $safeFileName; // Record cover in thesis_files (type = 'cover') $db->insertThesisFile( $thesisId, 'cover', $coverPath, basename($couverture["name"]), $couverture["size"], $mimeType ); error_log("Cover image uploaded: " . $safeFileName); } } else { error_log("Invalid cover image type: " . $mimeType); } } // Process banner image $db->handleBannerUpload($thesisId, $bannerFile ?: null); // Process thesis files if ($files && is_array($files["name"])) { for ($i = 0; $i < count($files["name"]); $i++) { // Skip if no file was uploaded for this slot if ($files["error"][$i] === UPLOAD_ERR_NO_FILE) { continue; } if ($files["error"][$i] !== UPLOAD_ERR_OK) { error_log("File upload error code " . $files["error"][$i] . ": " . $files["name"][$i]); continue; } // Validate file $finfo = new finfo(FILEINFO_MIME_TYPE); $mimeType = $finfo->file($files["tmp_name"][$i]); $fileExtension = strtolower(pathinfo($files["name"][$i], PATHINFO_EXTENSION)); // finfo may return 'text/plain' for WebVTT on some systems; normalise by extension. if ($mimeType === 'text/plain' && $fileExtension === 'vtt') { $mimeType = 'text/vtt'; } if (!in_array($mimeType, $allowedMimeTypes) || !in_array($fileExtension, $allowedExtensions)) { error_log("Invalid file type: " . $files["name"][$i] . " (MIME: $mimeType)"); continue; } if ($files["size"][$i] > $maxFileSize) { error_log("File too large: " . $files["name"][$i]); continue; } // Generate random filename $randomName = bin2hex(random_bytes(16)); $safeFileName = $randomName . "." . $fileExtension; $targetFile = $uploadBaseDir . $safeFileName; if (move_uploaded_file($files["tmp_name"][$i], $targetFile)) { chmod($targetFile, 0644); // Determine file type $fileType = 'other'; if ($fileExtension === 'vtt') { $fileType = 'caption'; // WebVTT caption sidecar } elseif (strpos(strtolower($files["name"][$i]), 'annex') !== false) { $fileType = 'annex'; } elseif ($fileExtension === 'pdf') { $fileType = 'main'; } // Insert file record — path relative to STORAGE_ROOT $db->insertThesisFile( $thesisId, $fileType, "theses/{$annee}/{$identifier}/" . $safeFileName, basename($files["name"][$i]), $files["size"][$i], $mimeType ); error_log("File uploaded: " . $safeFileName); } else { error_log("Failed to move file: " . $files["name"][$i]); } } } // ===== COMMIT TRANSACTION ===== $db->commit(); error_log("Thesis submission completed successfully: $identifier"); // Clear CSRF token unset($_SESSION['csrf_token']); // Redirect to thank you page header('Location: ../thanks.php?id=' . urlencode($thesisId)); exit(); } catch (Exception $e) { // Rollback transaction on error if (isset($db)) { $db->rollback(); } error_log("Form processing error: " . $e->getMessage()); // Save error message and form data to session App::flash('error', $e->getMessage()); $_SESSION['form_data'] = $_POST; // WCAG 3.3.1 — identify which field caused the error and request autofocus. // Mapping is based on the exception messages thrown above. $msg = $e->getMessage(); $autofocusField = null; if (str_contains($msg, "Nom/Prénom/Pseudo")) $autofocusField = 'auteurice'; elseif (str_contains($msg, "Titre du mémoire")) $autofocusField = 'titre'; elseif (str_contains($msg, "Synopsis")) $autofocusField = 'synopsis'; elseif (str_contains($msg, "Année invalide")) $autofocusField = 'année'; elseif (str_contains($msg, "orientation")) $autofocusField = 'orientation'; elseif (str_contains($msg, "Atelier Pratique")) $autofocusField = 'ap'; elseif (str_contains($msg, "finalité")) $autofocusField = 'finality'; elseif (str_contains($msg, "langue")) $autofocusField = 'languages'; elseif (str_contains($msg, "mots-clés")) $autofocusField = 'tag'; elseif (str_contains($msg, "Lien URL")) $autofocusField = 'lien'; if ($autofocusField !== null) { App::flashAutofocus($autofocusField); } // Redirect back to form with preserved data header('Location: ../add.php'); exit(); }