getPDO(); // Begin transaction - all or nothing $db->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"); $problematique = sanitize_string($_POST["problématique"] ?? ''); $durationInfo = sanitize_string($_POST["duration_info"] ?? ''); // Supervisor(s) $promoteuriceRaw = sanitize_string($_POST["promoteurice"] ?? ''); $supervisorNames = !empty($promoteuriceRaw) ? array_map('trim', explode(',', $promoteuriceRaw)) : []; // 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; $files = $_FILES["files"] ?? null; // ===== CREATE OR FIND AUTHOR ===== $authorId = $db->findOrCreateAuthor($auteurName, $mail); error_log("Author ID: $authorId"); // ===== INSERT THESIS RECORD ===== // Generate unique identifier (YYYY-NNN format) $stmt = $pdo->prepare("SELECT COUNT(*) as count FROM theses WHERE year = ?"); $stmt->execute([$annee]); $count = $stmt->fetch()['count'] + 1; $identifier = sprintf("%d-%03d", $annee, $count); $stmt = $pdo->prepare(" INSERT INTO theses ( identifier, title, subtitle, year, orientation_id, ap_program_id, finality_id, synopsis, file_size_info, baiu_link, license_id, submitted_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) "); $stmt->execute([ $identifier, $titre, !empty($subtitle) ? $subtitle : null, $annee, $orientationId, $apProgramId, $finalityId, $synopsis, !empty($durationInfo) ? $durationInfo : null, !empty($lien) ? $lien : null, $licenseId ]); $thesisId = $pdo->lastInsertId(); error_log("Thesis ID: $thesisId"); // ===== LINK AUTHOR TO THESIS ===== $stmt = $pdo->prepare("INSERT INTO thesis_authors (thesis_id, author_id, author_order) VALUES (?, ?, 1)"); $stmt->execute([$thesisId, $authorId]); // ===== LINK SUPERVISORS TO THESIS ===== foreach ($supervisorNames as $index => $supervisorName) { if (!empty($supervisorName)) { $supervisorId = $db->findOrCreateSupervisor($supervisorName); $stmt = $pdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, supervisor_order) VALUES (?, ?, ?)"); $stmt->execute([$thesisId, $supervisorId, $index + 1]); } } // ===== LINK LANGUAGES TO THESIS ===== foreach ($languageIds as $languageId) { $stmt = $pdo->prepare("INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?, ?)"); $stmt->execute([$thesisId, $languageId]); } // ===== LINK FORMATS TO THESIS ===== foreach ($formatIds as $formatId) { $stmt = $pdo->prepare("INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?, ?)"); $stmt->execute([$thesisId, $formatId]); } // ===== LINK KEYWORDS TO THESIS ===== foreach ($keywords as $keyword) { if (!empty($keyword)) { $keywordId = $db->findOrCreateKeyword($keyword); if ($keywordId) { $stmt = $pdo->prepare("INSERT INTO thesis_keywords (thesis_id, keyword_id) VALUES (?, ?)"); $stmt->execute([$thesisId, $keywordId]); } } } // ===== 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/"; if (!file_exists($uploadBaseDir)) { mkdir($uploadBaseDir, 0755, true); } if (!file_exists($coverDir)) { mkdir($coverDir, 0755, true); } // Define security constraints $allowedMimeTypes = ['image/jpeg', 'image/png', 'application/pdf', 'video/mp4', 'application/zip']; $allowedExtensions = ['jpg', 'jpeg', 'png', 'pdf', 'mp4', 'zip']; $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 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)); 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 (simplified - could be enhanced) $fileType = 'other'; if (strpos(strtolower($files["name"][$i]), 'annex') !== false) { $fileType = 'annex'; } else if ($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 $_SESSION['form_error'] = $e->getMessage(); $_SESSION['form_data'] = $_POST; // Redirect back to form with preserved data header('Location: ../add.php'); exit(); }