mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-07 11:39:18 +02:00
All admin action files (account, tag, page, edit, visibility, maintenance,
publish, formulaire) now call App::flash('error'|'success', ...) instead of
writing to raw per-page session keys ($_SESSION['error'], 'admin_error',
'edit_error', 'admin_success', 'edit_success', 'form_error').
All admin display pages (add, edit, account, tags, pages, index) now include
templates/partials/flash-messages.php instead of manually reading and
unsetting the legacy session keys and inlining their own alert HTML.
App::consumeFlash() already drained all legacy key variants as a safety net,
so the partial works correctly whether called from pages that were already
migrated or any remaining stragglers. No behaviour change for end users.
322 lines
11 KiB
PHP
322 lines
11 KiB
PHP
<?php // formulaire.php
|
|
// Bootstrap application
|
|
require_once __DIR__ . "/../../../config/bootstrap.php";
|
|
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
|
|
|
// Configure error reporting
|
|
ini_set('display_errors', 0);
|
|
ini_set('log_errors', 1);
|
|
ini_set('error_log', 'error.log');
|
|
|
|
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
|
|
AdminAuth::requireLogin();
|
|
|
|
// Verify CSRF token
|
|
if (!isset($_POST['csrf_token']) || !isset($_SESSION['csrf_token']) ||
|
|
!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
|
|
error_log("CSRF token validation failed");
|
|
die("Erreur de sécurité : token invalide. Veuillez recharger le formulaire.");
|
|
}
|
|
|
|
// Log the content of the $_FILES array
|
|
error_log("FILES array: " . print_r($_FILES, true));
|
|
|
|
require_once __DIR__ . '/../../../src/Database.php';
|
|
|
|
// Helper function to sanitize string input
|
|
function sanitize_string($input) {
|
|
// Trim and strip raw HTML tags only — htmlspecialchars belongs at render time, not storage time.
|
|
// PDO parameterised queries handle SQL injection; the templates call htmlspecialchars() on output.
|
|
return strip_tags(trim($input));
|
|
}
|
|
|
|
// Helper function to validate required field
|
|
function validate_required($value, $fieldName) {
|
|
if (empty($value)) {
|
|
throw new Exception("Le champ '$fieldName' est requis.");
|
|
}
|
|
return $value;
|
|
}
|
|
|
|
try {
|
|
// Initialize database connection
|
|
$db = new Database();
|
|
// 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");
|
|
$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'];
|
|
$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 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));
|
|
|
|
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
|
|
App::flash('error', $e->getMessage());
|
|
$_SESSION['form_data'] = $_POST;
|
|
|
|
// Redirect back to form with preserved data
|
|
header('Location: ../add.php');
|
|
exit();
|
|
}
|