mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-07 11:39:18 +02:00
- lib/AdminAuth.php: new class with requireLogin(), login(), logout(), isAuthenticated(); starts session with hardened cookie params (HttpOnly, SameSite=Strict, Secure, Path=/admin) — also resolves item #8 (session cookie hardening) - requireLogin() auto-authenticates from nginx Basic Auth credentials ($_SERVER['PHP_AUTH_PW']) so the user only sees one browser prompt; falls back to /admin/login.php if the proxy is absent/misconfigured - config/admin_credentials.php: gitignored credential store; define ADMIN_PASSWORD_HASH with a bcrypt hash to enable PHP auth - config/admin_credentials.example.php: template for the above - config/bootstrap.php: auto-loads admin_credentials.php if present - .gitignore: exclude config/admin_credentials.php - public/admin/login.php: fallback login form (shown only when nginx Basic Auth is bypassed / proxy absent) - public/admin/logout.php: session destruction + redirect to login - All 7 admin PHP files: replace session_start() with AdminAuth::requireLogin() (defence-in-depth behind nginx Basic Auth) - public/admin/inc/head.php: Déconnexion button when ADMIN_PASSWORD_HASH is defined - nginx/PHP_AUTH_LAYER.md: documents dual-auth architecture, UX flow, and setup instructions - docs/TODO.SECURITY.md: items #2 and #8 moved to Resolved; priority order updated (all CRITICAL done)
340 lines
12 KiB
PHP
340 lines
12 KiB
PHP
<?php // formulaire.php
|
|
// Bootstrap application
|
|
require_once __DIR__ . "/../../config/bootstrap.php";
|
|
require_once __DIR__ . '/../../lib/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__ . '/../../lib/Database.php';
|
|
|
|
// Helper function to sanitize string input
|
|
function sanitize_string($input) {
|
|
return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8');
|
|
}
|
|
|
|
// 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();
|
|
$pdo = $db->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"]) : [];
|
|
|
|
// 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,
|
|
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
|
|
]);
|
|
|
|
$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();
|
|
}
|