Files
xamxam/public/admin/import.php
Pontoporeia f18e3381ea admin.css: rewrite from scratch using only variables.css tokens
The file had accumulated severe corruption in its lower half (garbled
selector text, variable names spliced into property values, orphaned
declarations, broken nesting) alongside hardcoded hex colours throughout.

Rewrote the entire file cleanly:
- Every colour is now a var() referencing a token defined in variables.css:
  --accent-primary/secondary/foreground, --accent-blue/green/yellow/red,
  --bg-secondary/tertiary, --border-primary, --text-primary/secondary/tertiary,
  --error, --warning, --success, --accent-muted.
- Zero raw hex values remain in admin.css.
- Removed the corrupted/dead CSS from the bottom half and reconstructed
  all selectors from what the templates actually use (audited via grep).
- Fixed structural issues: broken border shorthand, nested rules that
  were not valid CSS, orphaned declaration blocks.
- New/restored rules: .admin-maintenance-bar (was corrupted),
  .status-access variants (was corrupted), .admin-section-title--danger,
  .admin-danger-zone, .admin-account-status (all reconstructed cleanly).
- .admin-btn--warning and .admin-btn--danger now use var(--accent-yellow)
  and var(--accent-red) instead of hardcoded dark hex values.
- .admin-btn-remove hover now uses var(--error) instead of #e55.
- .admin-btn-unpublish now uses var(--bg-secondary)/var(--text-tertiary)
  instead of hardcoded grey hex values.
- select option background colours removed (browser chrome, not styleable
  cross-platform).

Templates: replace 4 inline var(--admin-text-muted) with var(--text-secondary)
in index.php, thanks.php, import.php.
2026-04-06 15:32:41 +02:00

370 lines
16 KiB
PHP

<?php
// Bootstrap application
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../src/AdminAuth.php';
// CSV Import page for Post-ERG thesis database
// This page allows importing thesis data from CSV files
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
AdminAuth::requireLogin();
// Generate CSRF token
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
require_once __DIR__ . '/../../src/Database.php';
$pageTitle = "Import";
$message = '';
$errors = [];
$importedCount = 0;
$skippedCount = 0;
$importResults = [];
// Handle CSV upload and import
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
// Verify CSRF token
if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
$errors[] = "Erreur de sécurité : token invalide.";
} else {
try {
$db = new Database();
$pdo = $db->getPDO();
// Check file upload
if ($_FILES['csv_file']['error'] !== UPLOAD_ERR_OK) {
throw new Exception("Erreur lors du téléversement du fichier.");
}
// Read CSV file
$csvFile = $_FILES['csv_file']['tmp_name'];
$handle = fopen($csvFile, 'r');
if (!$handle) {
throw new Exception("Impossible d'ouvrir le fichier CSV.");
}
// Skip first two rows (empty and headers)
fgetcsv($handle, 0, ',', '"', ''); // Empty row
$headers = fgetcsv($handle, 0, ',', '"', ''); // Header row
fgetcsv($handle, 0, ',', '"', ''); // Description row
$headers = fgetcsv($handle, 0, ',', '"', ''); // Actual column names
// Map CSV columns
$columnMap = [
0 => 'identifier', // Identifiant
1 => 'title', // Titre
2 => 'subtitle', // Sous-titre
3 => 'authors', // Auteur·ice(s)
4 => 'contact', // Contact
5 => 'supervisors', // Promoteur·ice(s)
6 => 'formats', // Format
7 => 'year', // Année
8 => 'ap', // AP
9 => 'orientation', // Orientation
10 => 'finality', // Finalité
11 => 'keywords', // Mots-clés
12 => 'synopsis', // Synopsis
13 => 'context', // Contexte
14 => 'remarks', // Remarques
15 => 'language', // Langue
16 => 'access', // Autorisation
17 => 'license', // License
18 => 'size_info', // taille
19 => 'jury_points', // Points sur 20
20 => 'baiu_link', // lien BAIU
];
// Orientation abbreviation mapping
$orientationMap = [
'SC' => 'Sculpture',
'VI' => 'Vidéographie',
'CA' => 'Cinéma d\'animation',
'IP' => 'Installation-Performance',
'PE' => 'Peinture',
'PH' => 'Photographie',
'DE' => 'Dessin',
'AN' => 'Arts Numériques',
'GR' => 'Graphisme',
'TY' => 'Typographie',
'DN' => 'Design Numérique',
'IL' => 'Illustration',
'BD' => 'Bande-Dessinée',
'SE' => 'Sérigraphie',
'GV' => 'Gravure',
];
// Process each row
$lineNumber = 5; // Start after headers
while (($row = fgetcsv($handle, 0, ',', '"', '')) !== false) {
$lineNumber++;
// Skip empty rows
if (empty($row[0]) && empty($row[1])) {
continue;
}
try {
$db->beginTransaction();
// Extract data
$identifier = trim($row[0] ?? '');
$title = trim($row[1] ?? '');
$subtitle = trim($row[2] ?? '');
$authorsRaw = trim($row[3] ?? '');
$contact = trim($row[4] ?? '');
$supervisorsRaw = trim($row[5] ?? '');
$formatsRaw = trim($row[6] ?? '');
$year = intval($row[7] ?? 0);
$apCode = trim($row[8] ?? '');
$orientationCode = trim($row[9] ?? '');
$finalityName = trim($row[10] ?? '');
$keywordsRaw = trim($row[11] ?? '');
$synopsis = trim($row[12] ?? '');
$context = trim($row[13] ?? '');
$remarks = trim($row[14] ?? '');
$languageRaw = trim($row[15] ?? '');
$access = trim($row[16] ?? '');
$license = trim($row[17] ?? '');
$sizeInfo = trim($row[18] ?? '');
$juryPoints = !empty($row[19]) ? floatval($row[19]) : null;
$baiuLink = trim($row[20] ?? '');
// Validate required fields
if (empty($title) || empty($year)) {
throw new Exception("Ligne $lineNumber: Titre et année requis.");
}
// Map orientation
$orientationName = isset($orientationMap[$orientationCode]) ? $orientationMap[$orientationCode] : null;
$orientationId = null;
if ($orientationName) {
$stmtOr = $pdo->prepare("SELECT id FROM orientations WHERE name = ?");
$stmtOr->execute([$orientationName]);
$rowOr = $stmtOr->fetch();
$orientationId = $rowOr ? $rowOr['id'] : null;
}
// Map AP program
$apProgramId = null;
if (!empty($apCode)) {
$stmt = $pdo->prepare("SELECT id FROM ap_programs WHERE code = ?");
$stmt->execute([$apCode]);
$result = $stmt->fetch();
if ($result) {
$apProgramId = $result['id'];
}
}
// Map finality
$finalityId = null;
if (!empty($finalityName)) {
$stmtFin = $pdo->prepare("SELECT id FROM finality_types WHERE name = ?");
$stmtFin->execute([$finalityName]);
$rowFin = $stmtFin->fetch();
$finalityId = $rowFin ? $rowFin['id'] : null;
}
// Map access type (Autorisation column)
// CSV values are expected to match access_types.name: "Libre", "Interne", "Interdit"
$accessTypeId = null;
if (!empty($access)) {
$stmtAcc = $pdo->prepare("SELECT id FROM access_types WHERE name = ?");
$stmtAcc->execute([ucfirst(strtolower($access))]);
$rowAcc = $stmtAcc->fetch();
$accessTypeId = $rowAcc ? $rowAcc['id'] : null;
}
// Default to Libre (id=1) when not specified so imported theses are visible
if ($accessTypeId === null) {
$accessTypeId = 1;
}
// Skip if identifier already exists
if (!empty($identifier)) {
$stmtCheck = $pdo->prepare("SELECT id FROM theses WHERE identifier = ?");
$stmtCheck->execute([$identifier]);
if ($stmtCheck->fetch()) {
$db->rollback();
$skippedCount++;
$importResults[] = "⚠ Ligne $lineNumber: identifiant \"$identifier\" déjà présent, ignoré.";
continue;
}
}
// Insert thesis
$stmt = $pdo->prepare("
INSERT INTO theses (
identifier, title, subtitle, year,
orientation_id, ap_program_id, finality_id,
synopsis, context_note, remarks,
file_size_info, jury_points, baiu_link,
access_type_id, is_published,
submitted_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, CURRENT_TIMESTAMP)
");
$stmt->execute([
!empty($identifier) ? $identifier : null,
$title,
!empty($subtitle) ? $subtitle : null,
$year,
$orientationId,
$apProgramId,
$finalityId,
!empty($synopsis) ? $synopsis : null,
!empty($context) ? $context : null,
!empty($remarks) ? $remarks : null,
!empty($sizeInfo) ? $sizeInfo : null,
$juryPoints,
!empty($baiuLink) ? $baiuLink : null,
$accessTypeId
]);
$thesisId = $pdo->lastInsertId();
// Add authors
if (!empty($authorsRaw)) {
$authors = array_map('trim', explode(',', $authorsRaw));
foreach ($authors as $index => $authorName) {
if (!empty($authorName)) {
$authorId = $db->findOrCreateAuthor($authorName, $index === 0 ? $contact : null);
$stmt = $pdo->prepare("INSERT INTO thesis_authors (thesis_id, author_id, author_order) VALUES (?, ?, ?)");
$stmt->execute([$thesisId, $authorId, $index + 1]);
}
}
}
// Add supervisors
if (!empty($supervisorsRaw)) {
$supervisors = array_map('trim', explode(',', $supervisorsRaw));
foreach ($supervisors 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]);
}
}
}
// Add keywords
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) {
$stmtTag = $pdo->prepare("INSERT INTO thesis_tags (thesis_id, tag_id) VALUES (?, ?)");
$stmtTag->execute([$thesisId, $tagId]);
}
}
}
}
// Add language
if (!empty($languageRaw)) {
$stmtLang = $pdo->prepare("SELECT id FROM languages WHERE name = ?");
$stmtLang->execute([ucfirst(strtolower($languageRaw))]);
$rowLang = $stmtLang->fetch();
$languageId = $rowLang ? $rowLang['id'] : null;
if ($languageId) {
$stmt = $pdo->prepare("INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?, ?)");
$stmt->execute([$thesisId, $languageId]);
}
}
// Add formats
if (!empty($formatsRaw)) {
$formats = array_map('trim', explode(',', $formatsRaw));
foreach ($formats as $formatName) {
if (!empty($formatName)) {
$stmtFmt = $pdo->prepare("SELECT id FROM format_types WHERE name = ?");
$stmtFmt->execute([ucfirst(strtolower($formatName))]);
$rowFmt = $stmtFmt->fetch();
$formatId = $rowFmt ? $rowFmt['id'] : null;
if ($formatId) {
$stmt = $pdo->prepare("INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?, ?)");
$stmt->execute([$thesisId, $formatId]);
}
}
}
}
$db->commit();
$importedCount++;
$importResults[] = " Ligne $lineNumber: \"$title\" importé (ID: $thesisId)";
} catch (Exception $e) {
$db->rollback();
$skippedCount++;
$importResults[] = " Ligne $lineNumber: " . $e->getMessage();
error_log("Import error on line $lineNumber: " . $e->getMessage());
}
}
fclose($handle);
$message = "Import terminé : $importedCount TFE importés, $skippedCount ignorés.";
} catch (Exception $e) {
$errors[] = $e->getMessage();
error_log("CSV import error: " . $e->getMessage());
}
}
// Regenerate CSRF token
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<?php $isAdmin = true; $bodyClass = 'admin-body'; require_once APP_ROOT . '/templates/head.php'; ?>
<?php include APP_ROOT . '/templates/header.php'; ?>
<main id="main-content">
<h1>Importer une liste de TFE</h1>
<?php if (!empty($errors)): ?>
<div role="alert" data-type="error">
<strong>⚠ Erreurs :</strong>
<ul style="margin:.5rem 0 0;padding-left:1.2rem;">
<?php foreach ($errors as $err): ?>
<li><?= htmlspecialchars($err) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if ($message): ?>
<p role="status" data-type="success">✓ <?= htmlspecialchars($message) ?></p>
<?php endif; ?>
<form action="import.php" method="post" enctype="multipart/form-data" class="admin-import-area admin-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<div>
<label>Fichier CSV :</label>
<div class="admin-file-input">
<input type="file" id="csv_file" name="csv_file" accept=".csv" required>
<small style="margin-top:.5rem;">
Colonnes attendues : Identifiant, Titre, Sous-titre, Auteur·ice(s), Contact, Promoteur·ice(s), Format, Année, AP, Orientation, Finalité, Mots-clés, Synopsis, Contexte, Remarques, Langue, Autorisation, License, taille, Points sur 20, lien BAIU<br>
— Deux premières lignes ignorées (en-tête) — Séparateur : virgule — Encodage : UTF-8
</small>
</div>
</div>
<div class="admin-submit-wrap">
<button type="submit" class="admin-btn">Importer</button>
</div>
</form>
<?php if (!empty($importResults)): ?>
<div style="margin-top:2rem;">
<h2 style="font-size:1rem;font-weight:600;margin-bottom:.75rem;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.06em;">Résultats de l'import</h2>
<div class="info-message">
<pre><?php foreach ($importResults as $r) echo htmlspecialchars($r) . "\n"; ?></pre>
</div>
</div>
<?php endif; ?>
</main>
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>