Files
xamxam/public/admin/add.php
Pontoporeia 42af4644c5 perf+a11y: WAL mode for SQLite, skip links, :focus-visible, .sr-only
SQLite performance (Database::__construct):
- PRAGMA journal_mode = WAL: eliminates full-DB read locks on write, safe
  for concurrent PHP-FPM workers
- PRAGMA synchronous = NORMAL: durable on commit without full fsync per write
- PRAGMA cache_size = -8000: ~8 MB page cache per connection

Accessibility foundation (WCAG 2.1 AA):
- common.css: add .sr-only utility, .skip-link (hidden until focused),
  global :focus-visible (2px purple outline, 2px offset),
  prefers-reduced-motion guard; remove bare outline:none from
  .site-search__input
- admin.css: same :focus-visible, skip-link, and motion guard scoped to
  admin purple; remove outline:none from .admin-input/.admin-select/
  .admin-textarea and .admin-filters select (both had :focus border rules
  already, so focus is still visually communicated)
- search.css: remove outline:none from .search-filter-select (already has
  :focus border-color rule)
- All 5 public pages (index, search, tfe, apropos, licence): add
  <a href="#main-content" class="skip-link"> as first child of <body>;
  add id="main-content" to <main>
- templates/admin/head.php: same skip link; aria-label="Navigation admin"
  on <nav>; id="main-content" on all 10 admin <main> elements

All 4 test suites pass (unit, integration, security, rate-limit).
2026-03-27 13:45:01 +01:00

311 lines
14 KiB
PHP

<?php
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../src/AdminAuth.php';
AdminAuth::requireLogin();
if (empty($_SESSION["csrf_token"])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}
$pageTitle = "Ajouter un TFE";
require_once __DIR__ . '/../../src/Database.php';
try {
$db = new Database();
$orientations = $db->getAllOrientations();
$apPrograms = $db->getAllAPPrograms();
$finalityTypes = $db->getAllFinalityTypes();
$languages = $db->getAllLanguages();
$formatTypes = $db->getAllFormatTypes();
$licenseTypes = $db->getAllLicenseTypes();
} catch (Exception $e) {
error_log("Failed to load form data: " . $e->getMessage());
die("Erreur lors du chargement du formulaire.");
}
$error = $_SESSION["form_error"] ?? null;
$formData = $_SESSION["form_data"] ?? [];
unset($_SESSION["form_error"], $_SESSION["form_data"]);
function old($key, $default = "") {
global $formData;
return isset($formData[$key]) ? htmlspecialchars($formData[$key]) : $default;
}
function wasSelected($key, $value) {
global $formData;
if (!isset($formData[$key])) return false;
if (is_array($formData[$key])) return in_array($value, $formData[$key]);
return $formData[$key] == $value;
}
?>
<?php require_once APP_ROOT . '/templates/admin/head.php'; ?>
<main class="admin-main" id="main-content">
<h1 class="admin-page-title">Ajouter un TFE</h1>
<?php if ($error): ?>
<div class="admin-alert admin-alert--error">⚠ <?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form action="actions/formulaire.php" method="post" enctype="multipart/form-data" class="admin-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION["csrf_token"]) ?>">
<!-- Titre -->
<div class="admin-form-row">
<label class="admin-label" for="titre">Titre :</label>
<input class="admin-input" type="text" id="titre" name="titre"
value="<?= old('titre') ?>" required>
</div>
<!-- Sous-titre -->
<div class="admin-form-row">
<label class="admin-label" for="subtitle">Sous-titre (si applicable) :</label>
<input class="admin-input" type="text" id="subtitle" name="subtitle"
value="<?= old('subtitle') ?>">
</div>
<!-- Auteur·ice -->
<div class="admin-form-row">
<label class="admin-label" for="auteurice">Auteur·ice(s) :</label>
<input class="admin-input" type="text" id="auteurice" name="auteurice"
value="<?= old('auteurice') ?>" required>
</div>
<!-- Contact -->
<div class="admin-form-row">
<label class="admin-label" for="mail">Contact(s) (optionnel) [mail/site/insta/etc.] :</label>
<input class="admin-input" type="text" id="mail" name="mail"
value="<?= old('mail') ?>">
</div>
<!-- Composition du jury -->
<fieldset class="admin-fieldset">
<legend class="admin-fieldset-legend">Composition du jury</legend>
<!-- Président·e -->
<div class="admin-form-row">
<label class="admin-label" for="jury_president">Président·e :</label>
<input class="admin-input" type="text" id="jury_president" name="jury_president"
value="<?= old('jury_president') ?>"
placeholder="Nom du/de la président·e (interne)">
</div>
<!-- Promoteur·ice -->
<div class="admin-form-row">
<label class="admin-label" for="jury_promoteur">Promoteur·ice :</label>
<div class="admin-jury-row">
<input class="admin-input" type="text" id="jury_promoteur" name="jury_promoteur"
value="<?= old('jury_promoteur') ?>" placeholder="Nom">
<label class="admin-checkbox-label admin-jury-ext">
<input type="checkbox" name="jury_promoteur_ext" value="1"
<?= wasSelected('jury_promoteur_ext', '1') ? 'checked' : '' ?>>
Externe
</label>
</div>
</div>
<!-- Lecteur·ices (dynamic) -->
<div class="admin-form-row" style="align-items:start;">
<label class="admin-label">Lecteur·ices :</label>
<div id="jury-lecteurs-list" class="admin-jury-list">
<!-- rows injected by JS; start with one empty row -->
<div class="admin-jury-entry">
<input class="admin-input" type="text" name="jury_lecteurs[]" placeholder="Nom">
<label class="admin-checkbox-label admin-jury-ext">
<input type="checkbox" name="jury_lecteurs_ext[0]" value="1"> Externe
</label>
<button type="button" class="admin-btn-remove" onclick="removeJuryRow(this)">✕</button>
</div>
</div>
<button type="button" class="admin-btn-secondary" style="margin-top:.5rem;"
onclick="addJuryRow()">+ Ajouter un·e lecteur·ice</button>
</div>
</fieldset>
<script>
var juryIdx = 1;
function addJuryRow() {
var list = document.getElementById('jury-lecteurs-list');
var div = document.createElement('div');
div.className = 'admin-jury-entry';
div.innerHTML = '<input class="admin-input" type="text" name="jury_lecteurs[]" placeholder="Nom">'
+ '<label class="admin-checkbox-label admin-jury-ext">'
+ '<input type="checkbox" name="jury_lecteurs_ext[' + juryIdx + ']" value="1"> Externe'
+ '</label>'
+ '<button type="button" class="admin-btn-remove" onclick="removeJuryRow(this)">✕</button>';
list.appendChild(div);
juryIdx++;
}
function removeJuryRow(btn) {
btn.closest('.admin-jury-entry').remove();
}
</script>
<!-- Année -->
<div class="admin-form-row">
<label class="admin-label" for="année">Année :</label>
<input class="admin-input" type="number" id="année" name="année"
min="2000" max="<?= date('Y') + 1 ?>"
placeholder="<?= date('Y') ?>"
value="<?= old('année') ?>" required>
</div>
<!-- Orientation -->
<div class="admin-form-row">
<label class="admin-label" for="orientation">Orientation :</label>
<select class="admin-select" id="orientation" name="orientation" required>
<option value=""></option>
<?php foreach ($orientations as $o): ?>
<option value="<?= htmlspecialchars($o['id']) ?>"
<?= wasSelected('orientation', $o['id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($o['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<!-- AP -->
<div class="admin-form-row">
<label class="admin-label" for="ap">Atelier pluridisciplinaire :</label>
<select class="admin-select" id="ap" name="ap" required>
<option value=""></option>
<?php foreach ($apPrograms as $ap): ?>
<option value="<?= htmlspecialchars($ap['id']) ?>"
<?= wasSelected('ap', $ap['id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($ap['name']) ?><?php if ($ap['code']): ?> (<?= htmlspecialchars($ap['code']) ?>)<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
</div>
<!-- Finalité -->
<div class="admin-form-row">
<label class="admin-label" for="finality">Finalité du master :</label>
<select class="admin-select" id="finality" name="finality" required>
<option value=""></option>
<?php foreach ($finalityTypes as $f): ?>
<option value="<?= htmlspecialchars($f['id']) ?>"
<?= wasSelected('finality', $f['id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($f['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<!-- Langue(s) -->
<div class="admin-form-row">
<label class="admin-label">Langue(s) :</label>
<div class="admin-checkbox-list">
<?php foreach ($languages as $lang): ?>
<label class="admin-checkbox-label">
<input type="checkbox" name="languages[]"
value="<?= htmlspecialchars($lang['id']) ?>"
<?= wasSelected('languages', $lang['id']) ? 'checked' : '' ?>>
<?= htmlspecialchars($lang['name']) ?>
</label>
<?php endforeach; ?>
</div>
</div>
<!-- Format(s) -->
<div class="admin-form-row">
<label class="admin-label">Format(s) :</label>
<div class="admin-checkbox-list">
<?php foreach ($formatTypes as $fmt): ?>
<label class="admin-checkbox-label">
<input type="checkbox" name="formats[]"
value="<?= htmlspecialchars($fmt['id']) ?>"
<?= wasSelected('formats', $fmt['id']) ? 'checked' : '' ?>>
<?= htmlspecialchars($fmt['name']) ?>
</label>
<?php endforeach; ?>
</div>
</div>
<!-- Mots-clés -->
<div class="admin-form-row">
<label class="admin-label" for="tag">Mots-clés :</label>
<div>
<input class="admin-input" type="text" id="tag" name="tag"
placeholder="sociologie, anthropologie, ..."
value="<?= old('tag') ?>">
<p class="admin-hint">Séparez par des virgules. Max 10 mots-clés.</p>
</div>
</div>
<!-- Synopsis -->
<div class="admin-form-row" style="align-items:start;">
<label class="admin-label" for="synopsis">Synopsis :</label>
<textarea class="admin-textarea" id="synopsis" name="synopsis"
rows="7" required><?= old('synopsis') ?></textarea>
</div>
<!-- Licence -->
<div class="admin-form-row">
<label class="admin-label" for="license_id">Licence :</label>
<select class="admin-select" id="license_id" name="license_id">
<option value="">— Inconnue —</option>
<?php foreach ($licenseTypes as $lt): ?>
<option value="<?= htmlspecialchars($lt['id']) ?>"
<?= wasSelected('license_id', $lt['id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($lt['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<!-- Durée/Taille -->
<div class="admin-form-row">
<label class="admin-label" for="duration_info">Durée / Taille :</label>
<div>
<input class="admin-input" type="text" id="duration_info" name="duration_info"
placeholder="Ex : 84 pages"
value="<?= old('duration_info') ?>">
<p class="admin-hint">Durée (minutes) ou nombre de pages.</p>
</div>
</div>
<!-- Lien -->
<div class="admin-form-row">
<label class="admin-label" for="lien">Lien (site / ressource) :</label>
<input class="admin-input" type="url" id="lien" name="lien"
placeholder="https://..."
value="<?= old('lien') ?>">
</div>
<!-- Image couverture -->
<div class="admin-form-row" style="align-items:start;">
<label class="admin-label">Image de couverture :</label>
<div class="admin-file-input">
<input type="file" id="couverture" name="couverture" accept="image/jpeg,image/png">
<p class="admin-hint">JPG, PNG. Taille max : 10 MB.</p>
</div>
</div>
<!-- Image bannière -->
<div class="admin-form-row" style="align-items:start;">
<label class="admin-label">Image bannière (accueil) :</label>
<div class="admin-file-input">
<input type="file" id="banner" name="banner" accept="image/jpeg,image/png,image/webp">
<p class="admin-hint">JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 5 MB.</p>
</div>
</div>
<!-- Fichiers -->
<div class="admin-form-row" style="align-items:start;">
<label class="admin-label">Fichiers du TFE :</label>
<div class="admin-file-input">
<input type="file" id="files" name="files[]" multiple
accept=".pdf,.jpg,.jpeg,.png,.mp4,.zip">
<p class="admin-hint">PDF, JPG, PNG, MP4, ZIP. Max 50 MB par fichier.</p>
</div>
</div>
<div class="admin-submit-wrap">
<button type="submit" name="go" class="admin-btn">Soumettre</button>
</div>
</form>
</main>
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>