mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-07 03:29:19 +02:00
591 lines
29 KiB
PHP
591 lines
29 KiB
PHP
<?php
|
||
/**
|
||
* Partage — Entry point for shared student submission forms.
|
||
*
|
||
* Routes:
|
||
* /partage/<slug> — Render the share-link form (or password gate)
|
||
* /partage/<slug>/submit — POST endpoint for form submissions via share link
|
||
* /partage/thanks.php?id=N — Post-submission confirmation page
|
||
*/
|
||
require_once __DIR__ . '/../../config/bootstrap.php';
|
||
|
||
// Parse the requested path from REQUEST_URI
|
||
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
|
||
// Remove query string
|
||
$basePath = parse_url($requestUri, PHP_URL_PATH);
|
||
// Extract the part after /partage/
|
||
$path = trim(str_replace('/partage/', '', $basePath), '/');
|
||
|
||
// Split into parts: /partage/<slug>/<action>
|
||
$parts = explode('/', $path);
|
||
$slug = $parts[0] ?? '';
|
||
$action = $parts[1] ?? '';
|
||
|
||
// Validate slug format: YYYYMMDD-XXXXXXXX (17 chars)
|
||
if (!preg_match('#^\d{8}-[A-Z0-9+/]{8}$#', $slug)) {
|
||
App::boot();
|
||
$_SESSION['_flash_error'] = 'Ce lien de partage n\'est pas valide.';
|
||
header('Location: /');
|
||
exit;
|
||
}
|
||
|
||
// Boot for all requests: starts session, initialises DB, ensures CSRF token.
|
||
App::boot();
|
||
|
||
// ── POST: form submission ─────────────────────────────────────────────────────
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'submit') {
|
||
handleShareLinkSubmission($slug);
|
||
exit;
|
||
}
|
||
|
||
// ── GET: render form ─────────────────────────────────────────────────────────
|
||
|
||
require_once APP_ROOT . '/src/ShareLink.php';
|
||
$shareLinkModel = new ShareLink(Database::getInstance());
|
||
|
||
$validationResult = $shareLinkModel->validateLink($slug);
|
||
|
||
if (!$validationResult['valid']) {
|
||
$reason = $validationResult['reason'];
|
||
|
||
if ($reason === 'not_found') {
|
||
$_SESSION['_flash_error'] = 'Ce lien de partage n\'existe pas ou a été supprimé.';
|
||
header('Location: /');
|
||
exit;
|
||
}
|
||
|
||
if ($reason === 'disabled') {
|
||
renderShareLinkError('Lien désactivé', 'Ce lien de partage a été désactivé par un administrateur.');
|
||
exit;
|
||
}
|
||
|
||
if ($reason === 'expired') {
|
||
renderShareLinkError('Lien expiré', 'Ce lien de partage a expiré.');
|
||
exit;
|
||
}
|
||
|
||
if ($reason === 'needs_password') {
|
||
// Show password gate
|
||
$link = $validationResult['link'];
|
||
requirePasswordGate($link, $slug);
|
||
exit;
|
||
}
|
||
|
||
$_SESSION['_flash_error'] = 'Erreur inattendue.';
|
||
header('Location: /');
|
||
exit;
|
||
}
|
||
|
||
// Link is valid — render the form
|
||
$link = $validationResult['link'];
|
||
renderShareLinkForm($slug, $link);
|
||
|
||
// ── Functions ─────────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Render a styled error page for invalid/expired/disabled share links.
|
||
*/
|
||
function renderShareLinkError(string $title, string $message): void
|
||
{
|
||
$pageTitle = htmlspecialchars($title);
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title><?= $pageTitle ?></title>
|
||
<link rel="stylesheet" href="<?= App::assetV('/assets/css/system.css') ?>">
|
||
<style>
|
||
.share-error {
|
||
max-width: 500px;
|
||
margin: 5rem auto;
|
||
padding: 2.5rem;
|
||
text-align: center;
|
||
background: #fff;
|
||
border: 2px solid #d00;
|
||
border-radius: 8px;
|
||
}
|
||
.share-error h1 {
|
||
color: #d00;
|
||
font-size: 1.5rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
.share-error p {
|
||
font-size: 1.1rem;
|
||
color: #444;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
.share-error a {
|
||
display: inline-block;
|
||
padding: 0.6rem 1.5rem;
|
||
background: #333;
|
||
color: white;
|
||
text-decoration: none;
|
||
border-radius: 4px;
|
||
}
|
||
.share-error a:hover {
|
||
background: #555;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="share-error">
|
||
<h1><?= $pageTitle ?></h1>
|
||
<p><?= htmlspecialchars($message) ?></p>
|
||
<a href="/">← Retour à l'accueil</a>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
<?php
|
||
}
|
||
|
||
function requirePasswordGate(array $link, string $slug): void
|
||
{
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['share_password'])) {
|
||
require_once APP_ROOT . '/src/ShareLink.php';
|
||
$shareLinkModel = new ShareLink(Database::getInstance());
|
||
|
||
if ($shareLinkModel->verifyPassword($link, $_POST['share_password'])) {
|
||
// Store verified status in session
|
||
$_SESSION['share_verified_' . $slug] = true;
|
||
// Redirect to clear POST data
|
||
header('Location: /partage/' . $slug);
|
||
exit;
|
||
} else {
|
||
$_SESSION['_flash_error'] = 'Mot de passe incorrect.';
|
||
header('Location: /partage/' . $slug);
|
||
exit;
|
||
}
|
||
}
|
||
|
||
$pageTitle = 'Accès protégé';
|
||
|
||
// Consume flash errors from wrong-password redirects
|
||
$flashError = $_SESSION['_flash_error'] ?? null;
|
||
unset($_SESSION['_flash_error']);
|
||
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title><?= htmlspecialchars($pageTitle) ?></title>
|
||
<link rel="stylesheet" href="<?= App::assetV('/assets/css/system.css') ?>">
|
||
<style>
|
||
.password-gate {
|
||
max-width: 400px;
|
||
margin: 4rem auto;
|
||
padding: 2rem;
|
||
text-align: center;
|
||
}
|
||
.password-gate h1 { margin-bottom: 1.5rem; }
|
||
.password-gate input[type="password"] {
|
||
width: 100%;
|
||
padding: 0.5rem;
|
||
margin: 0.5rem 0 1rem;
|
||
font-size: 1rem;
|
||
}
|
||
.password-gate button {
|
||
padding: 0.75rem 2rem;
|
||
font-size: 1rem;
|
||
cursor: pointer;
|
||
}
|
||
.password-error {
|
||
color: red;
|
||
margin-bottom: 1rem;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="password-gate">
|
||
<h1>🔒 Accès protégé</h1>
|
||
<?php if ($flashError): ?>
|
||
<p class="password-error"><?= htmlspecialchars($flashError) ?></p>
|
||
<?php endif; ?>
|
||
<form method="post">
|
||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token'] ?? '') ?>">
|
||
<label for="share_password">Ce lien est protégé par un mot de passe :</label>
|
||
<input type="password" id="share_password" name="share_password" required autofocus>
|
||
<br>
|
||
<button type="submit">Accéder au formulaire</button>
|
||
</form>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
<?php
|
||
}
|
||
|
||
function renderShareLinkForm(string $slug, array $link): void
|
||
{
|
||
require_once APP_ROOT . '/src/ThesisCreateController.php';
|
||
|
||
try {
|
||
$ctrl = ThesisCreateController::make();
|
||
extract($ctrl->loadFormData());
|
||
} catch (Exception $e) {
|
||
error_log('Failed to load form data: ' . $e->getMessage());
|
||
die('Erreur lors du chargement du formulaire.');
|
||
}
|
||
|
||
$formData = $_SESSION['form_data_share_' . $slug] ?? [];
|
||
unset($_SESSION['form_data_share_' . $slug]);
|
||
|
||
// Generate a CSRF token specific to this share link (stored in session)
|
||
$shareCsrfKey = 'share_csrf_' . $slug;
|
||
if (empty($_SESSION[$shareCsrfKey])) {
|
||
$_SESSION[$shareCsrfKey] = bin2hex(random_bytes(32));
|
||
}
|
||
$shareCsrfToken = $_SESSION[$shareCsrfKey];
|
||
|
||
$pageTitle = 'Soumettre un TFE';
|
||
|
||
// Determine if previously verified by password
|
||
$isVerified = !empty($_SESSION['share_verified_' . $slug]);
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title><?= htmlspecialchars($pageTitle) ?></title>
|
||
<link rel="stylesheet" href="<?= App::assetV('/assets/css/system.css') ?>">
|
||
<link rel="stylesheet" href="<?= App::assetV('/assets/css/admin.css') ?>">
|
||
<style>
|
||
.student-body {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
padding: 2rem 1rem;
|
||
}
|
||
.licence-explanation {
|
||
background: #f9f9f9;
|
||
border-left: 4px solid #666;
|
||
padding: 1.5rem;
|
||
margin: 2rem 0;
|
||
}
|
||
.licence-info h3 { margin-top: 1.5rem; }
|
||
.licence-degree { margin: 1.5rem 0; padding: 1rem; background: white; border: 1px solid #ddd; border-radius: 4px; }
|
||
.licence-note { color: #666; font-size: 0.9rem; margin-top: 0.5rem; }
|
||
.licence-generalites { margin-top: 2rem; }
|
||
.form-footer {
|
||
margin-top: 2rem;
|
||
padding-top: 1rem;
|
||
border-top: 2px solid #333;
|
||
}
|
||
.share-badge {
|
||
display: inline-block;
|
||
padding: 0.25rem 0.75rem;
|
||
background: #e0f0ff;
|
||
border: 1px solid #99c;
|
||
border-radius: 3px;
|
||
font-size: 0.85rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
h1 { margin-bottom: 0.5rem; }
|
||
.mode-toggle { font-size: 0.9rem; }
|
||
</style>
|
||
</head>
|
||
<body class="admin-body student-body">
|
||
<main id="main-content">
|
||
<div class="thesis-add-header">
|
||
<h1>Soumettre un TFE</h1>
|
||
<?php if ($isVerified): ?>
|
||
<span class="share-badge">🔓 Accès partagé</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?php
|
||
// Show flash messages from error redirect
|
||
$flashError = $_SESSION['_flash_error'] ?? null;
|
||
$flashSuccess = $_SESSION['_flash_success'] ?? null;
|
||
unset($_SESSION['_flash_error'], $_SESSION['_flash_success']);
|
||
?>
|
||
<?php if ($flashError): ?>
|
||
<div class="flash-error" role="alert"><?= htmlspecialchars($flashError) ?></div>
|
||
<?php endif; ?>
|
||
<?php if ($flashSuccess): ?>
|
||
<div class="flash-success" role="alert"><?= htmlspecialchars($flashSuccess) ?></div>
|
||
<?php endif; ?>
|
||
|
||
<form action="/partage/<?= urlencode($slug) ?>/submit" method="post" enctype="multipart/form-data" class="admin-form">
|
||
<input type="hidden" name="share_link_token" value="<?= htmlspecialchars($shareCsrfToken) ?>">
|
||
|
||
<!-- ═══════════════════ Informations du TFE ═══════════════════ -->
|
||
<fieldset>
|
||
<legend>Informations du TFE</legend>
|
||
|
||
<?php $name = 'titre'; $label = 'Titre :'; $value = old($formData, 'titre'); $required = true; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||
<?php $name = 'subtitle'; $label = 'Sous-titre (si applicable) :'; $value = old($formData, 'subtitle'); $required = false; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = old($formData, 'auteurice'); $required = true; $attrs = ['autocomplete' => 'name']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||
<?php $name = 'mail'; $label = 'Contact(s) (optionnel) [mail/site/insta/etc.] :'; $value = old($formData, 'mail'); $attrs = ['autocomplete' => 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||
|
||
<div class="admin-form-group">
|
||
<label class="admin-checkbox-label">
|
||
<input type="checkbox" name="contact_public" value="1"
|
||
<?= isset($formData['contact_public']) ? 'checked' : '' ?>>
|
||
Je veux que mon contact soit accessible à toustes depuis la plateforme xamxam
|
||
</label>
|
||
<small>Si cette case est cochée, votre contact apparaîtra sur la page publique de votre TFE.</small>
|
||
</div>
|
||
|
||
<div>
|
||
<label for="synopsis">Synopsis :</label>
|
||
<textarea id="synopsis" name="synopsis" rows="7" required><?= old($formData, 'synopsis') ?></textarea>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<!-- ═══════════════════ Composition du jury ═══════════════════ -->
|
||
<fieldset>
|
||
<legend>Composition du jury</legend>
|
||
|
||
<?php $name = 'jury_president'; $label = 'Président·e du jury :'; $value = old($formData, 'jury_president'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||
<?php $name = 'jury_promoteur'; $label = 'Promoteur·ice :'; $value = old($formData, 'jury_promoteur'); $hintCheckbox = ['name' => 'jury_promoteur_ext', 'label' => 'Externe à l\'erg', 'checked' => !empty($formData['jury_promoteur_ext'])]; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||
|
||
<div class="jury-additional">
|
||
<p>Lecteurs·rices (optionnel) :</p>
|
||
<?php for ($i = 0; $i < 4; $i++): ?>
|
||
<div class="jury-additional-row">
|
||
<input type="text"
|
||
name="jury_lecteurs[]"
|
||
placeholder="<?= $i === 0 ? 'Nom du lecteur·ice' : ''; ?>"
|
||
value="<?= old($formData, "jury_lecteurs:$i") ?>">
|
||
<label class="admin-checkbox-label">
|
||
<input type="checkbox" name="jury_lecteurs_ext[<?= $i ?>]" value="1"
|
||
<?= !empty($formData["jury_lecteurs_ext:$i"]) ? 'checked' : '' ?>>
|
||
Externe
|
||
</label>
|
||
</div>
|
||
<?php endfor; ?>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<!-- ═══════════════════ Cadre académique ═══════════════════ -->
|
||
<fieldset>
|
||
<legend>Cadre académique</legend>
|
||
|
||
<?php
|
||
$name = 'année'; $label = 'Année :'; $value = old($formData, 'année'); $required = true;
|
||
$type = 'number';
|
||
$placeholder = date('Y');
|
||
$attrs = ['min' => 2000, 'max' => date('Y') + 1];
|
||
include APP_ROOT . '/templates/partials/form/text-field.php';
|
||
?>
|
||
|
||
<?php $name = 'orientation'; $label = 'Orientation :'; $options = $orientations; $selected = isset($formData['orientation']) ? $formData['orientation'] : ''; $required = true; $placeholder = ''; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||
<?php $name = 'ap'; $label = 'Atelier pluridisciplinaire :'; $options = $apPrograms; $selected = isset($formData['ap']) ? $formData['ap'] : ''; $required = true; $placeholder = ''; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||
<?php $name = 'finality'; $label = 'Finalité du master :'; $options = $finalityTypes; $selected = isset($formData['finality']) ? $formData['finality'] : ''; $required = true; $placeholder = ''; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||
|
||
<?php $name = 'languages'; $label = 'Langue(s) :'; $options = $languages; $checked = $formData['languages'] ?? []; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
|
||
<?php $name = 'formats'; $label = 'Format(s) :'; $options = $formatTypes; $checked = $formData['formats'] ?? []; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
|
||
|
||
<?php $name = 'tag'; $label = 'Mots-clés :'; $value = old($formData, 'tag'); $placeholder = 'sociologie, anthropologie, ...'; $hint = 'Séparez par des virgules. Max 10 mots-clés.'; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||
</fieldset>
|
||
|
||
<!-- ═══════════════════ Fichiers ═══════════════════ -->
|
||
<fieldset>
|
||
<legend>Fichiers</legend>
|
||
|
||
<?php $name = 'couverture'; $label = 'Image de couverture :'; $accept = 'image/jpeg,image/png'; $hint = 'JPG, PNG. Taille max : 10 MB.'; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
||
<?php $name = 'banner'; $label = 'Image bannière (accueil) :'; $accept = 'image/jpeg,image/png,image/webp'; $hint = 'JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 5 MB.'; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
||
<?php $name = 'files'; $label = 'Fichiers du TFE :'; $accept = '.pdf,.jpg,.jpeg,.png,.mp4,.zip,.vtt'; $hint = 'PDF, JPG, PNG, MP4, ZIP. Max 50 MB par fichier. Pour les vidéos, un fichier .vtt de sous-titres peut être joint (il sera associé automatiquement à la vidéo correspondante).'; $multiple = true; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
||
</fieldset>
|
||
|
||
<!-- ═══════════════════ Métadonnées complémentaires ═══════════════════ -->
|
||
<fieldset>
|
||
<legend>Métadonnées complémentaires</legend>
|
||
|
||
<?php $name = 'license_id'; $label = 'Licence :'; $options = $licenseTypes; $selected = isset($formData['license_id']) ? $formData['license_id'] : ''; $placeholder = '— Inconnue —'; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||
|
||
<?php $name = 'duration_info'; $label = 'Durée / Taille :'; $value = old($formData, 'duration_info'); $placeholder = 'Ex : 84 pages'; $hint = 'Durée (minutes) ou nombre de pages.'; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||
|
||
<?php $name = 'lien'; $label = 'Lien (site / ressource) :'; $value = old($formData, 'lien'); $type = 'url'; $placeholder = 'https://...'; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||
|
||
<?php
|
||
$accessOptions = array_map(function($at) {
|
||
return ['id' => $at['id'], 'name' => $at['name']];
|
||
}, $enabledAccessTypes);
|
||
$defaultAccessType = 2;
|
||
$selectedAccessType = isset($formData['access_type_id'])
|
||
? (int)$formData['access_type_id']
|
||
: $defaultAccessType;
|
||
$name = 'access_type_id';
|
||
$label = 'Visibilité / Accès :';
|
||
$options = $accessOptions;
|
||
$selected = $selectedAccessType;
|
||
$placeholder = null;
|
||
$required = true;
|
||
$attrs = [];
|
||
include APP_ROOT . '/templates/partials/form/select-field.php';
|
||
?>
|
||
</fieldset>
|
||
|
||
<!-- ═══════════════════ Degrés d'ouverture ═══════════════════ -->
|
||
<fieldset class="licence-explanation">
|
||
<legend>Degrés d'ouverture et licences</legend>
|
||
|
||
<div class="licence-info">
|
||
<h3>Je veux que mon TFE soit disponible sous les conditions suivantes :</h3>
|
||
|
||
<div class="licence-degree">
|
||
<h4>🔓 Libre</h4>
|
||
<p>Mon TFE est en libre accès à tout le monde sur la plateforme des TFE ainsi que dans la bibliothèque de l'erg. Je suis conscient·e des responsabilités et obligations légales qui viennent avec une diffusion externe – et acquiesce avoir lu la documentation prévue à cet effet par l'erg, ainsi qu'avoir discuté des enjeux d'une publication avec l'équipe pédagogique. J'accepte de partager mes droits de diffusion avec l'erg, ce uniquement dans le cadre d'une diffusion sur la plateforme xamxam.</p>
|
||
<ul>
|
||
<li><label><input type="checkbox" name="cc4r" value="1"> J'accepte les conditions collectives de réutilisation (CC4r) <em class="hint">(pas obligatoire)</em></label></li>
|
||
<li><label><input type="checkbox" name="specific_license" value="1"> Je souhaite appliquer une licence spécifique à mon travail <em class="hint">(pas obligatoire)</em></label></li>
|
||
</ul>
|
||
<p class="licence-note"><em>Au moins une des deux cases doit être cochée pour le degré Libre.</em></p>
|
||
</div>
|
||
|
||
<div class="licence-degree">
|
||
<h4>🔒 Interne</h4>
|
||
<p>Mon TFE et ma note d'intention ne sont accessibles que sur place en physique ainsi que sur la plateforme xamxam par la communauté erg. Une note descriptive est disponible sur le site à toustes. J'autorise une (ré-)utilisation et diffusion dans un contexte académique et didactique au sein de l'erg.</p>
|
||
<p class="licence-note"><em>La diffusion limitée est protégée par le cadre académique/didactique, le travail pourrait donc être diffusé en interne et être cité par d'autres étudiant·es sans implications légales pour l'auteur·ice ni pour l'école.</em></p>
|
||
<ul>
|
||
<li><label><input type="checkbox" name="cc4r" value="1"> J'accepte les conditions collectives de réutilisation (CC4r) <em class="hint">(pas obligatoire)</em></label></li>
|
||
<li><label><input type="checkbox" name="specific_license" value="1"> Je souhaite appliquer une licence spécifique à mon travail <em class="hint">(pas obligatoire)</em></label></li>
|
||
</ul>
|
||
<p class="licence-note"><em>Au moins une des deux cases doit être cochée.</em></p>
|
||
</div>
|
||
|
||
<div class="licence-degree">
|
||
<h4>🚫 Interdit</h4>
|
||
<p>Mon TFE n'est pas disponible en physique ni sur le site. Une note descriptive est disponible sur le site.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="licence-generalites">
|
||
<h3>Généralités</h3>
|
||
<ul>
|
||
<li>L'auteur·ice peut décider entre trois degrés de partage de son travail : <strong>libre</strong>, <strong>interne</strong>, <strong>interdit</strong>.</li>
|
||
<li>L'auteur·ice peut, à tout moment, décider de <strong>restreindre</strong> le degré d'accès à son travail. Il ne peut néanmoins pas l'ouvrir davantage.</li>
|
||
<li>Le choix effectué dans ce formulaire sera d'application <strong>une semaine après la soutenance orale</strong> de l'auteur·ice. Celui-ci peut donc décider de restreindre ce choix avant sa publication (mais pas l'ouvrir).</li>
|
||
<li>L'erg se réserve le droit de restreindre le degré d'ouverture du TFE – ce en accord avec le règlement.</li>
|
||
<li>Dans tous les cas, l'auteur·ice garde les droits d'auteurs, de diffusion, d'utilisation, etc. de son travail – sauf si la licence choisie restreindrait ses droits.</li>
|
||
<li>La diffusion « xamxam » est indépendante de la diffusion à la BAIU.</li>
|
||
</ul>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<div class="form-footer">
|
||
<button type="submit" name="go">Soumettre</button>
|
||
</div>
|
||
</form>
|
||
</main>
|
||
</body>
|
||
</html>
|
||
<?php
|
||
}
|
||
|
||
/**
|
||
* Handle share link form submission.
|
||
*/
|
||
function handleShareLinkSubmission(string $slug): void
|
||
{
|
||
// Session already started by App::boot() on the GET path; start here
|
||
// only if somehow not yet active (e.g. direct POST without prior GET).
|
||
if (session_status() === PHP_SESSION_NONE) {
|
||
session_start();
|
||
}
|
||
|
||
require_once APP_ROOT . '/src/ShareLink.php';
|
||
require_once APP_ROOT . '/src/RateLimit.php';
|
||
|
||
$shareLinkModel = new ShareLink(Database::getInstance());
|
||
|
||
$link = $shareLinkModel->findBySlug($slug);
|
||
|
||
if ($link === null || !$link['is_active'] || ($link['expires_at'] !== null && strtotime($link['expires_at']) < time())) {
|
||
$_SESSION['_flash_error'] = 'Ce lien n\'est plus valide.';
|
||
header('Location: /partage/' . urlencode($slug));
|
||
exit;
|
||
}
|
||
|
||
// ── Rate limiting ────────────────────────────────────────────────────────
|
||
// 5 submissions per IP per 10 minutes, keyed per share link.
|
||
$rateLimitCacheDir = STORAGE_ROOT . '/cache/rate_limit';
|
||
$shareRateLimitId = 'share_' . $slug . '_' . ($_SERVER['REMOTE_ADDR'] ?? 'unknown');
|
||
$rateLimit = new RateLimit(5, 600, $rateLimitCacheDir);
|
||
|
||
if (!$rateLimit->checkKey($shareRateLimitId)) {
|
||
$_SESSION['_flash_error'] = 'Trop de tentatives. Veuillez réessayer plus tard.';
|
||
header('Location: /partage/' . urlencode($slug));
|
||
exit;
|
||
}
|
||
// ── End rate limiting ────────────────────────────────────────────────────
|
||
|
||
// Check password verification if link has a password
|
||
if ($link['password_hash'] !== null && empty($_SESSION['share_verified_' . $slug])) {
|
||
if (isset($_POST['share_password_submit'])) {
|
||
if ($shareLinkModel->verifyPassword($link, $_POST['share_password_submit'])) {
|
||
$_SESSION['share_verified_' . $slug] = true;
|
||
} else {
|
||
$_SESSION['_flash_error'] = 'Mot de passe incorrect.';
|
||
header('Location: /partage/' . urlencode($slug));
|
||
exit;
|
||
}
|
||
} else {
|
||
$_SESSION['_flash_error'] = 'Vous devez entrer le mot de passe avant de soumettre le formulaire.';
|
||
header('Location: /partage/' . urlencode($slug));
|
||
exit;
|
||
}
|
||
}
|
||
|
||
// Validate share-link CSRF token
|
||
$shareCsrfKey = 'share_csrf_' . $slug;
|
||
if (!isset($_POST['share_link_token'], $_SESSION[$shareCsrfKey])
|
||
|| !hash_equals($_SESSION[$shareCsrfKey], $_POST['share_link_token'])) {
|
||
error_log('Share link CSRF validation failed for ' . $slug);
|
||
$_SESSION['_flash_error'] = 'Token de sécurité invalide.';
|
||
header('Location: /partage/' . urlencode($slug));
|
||
exit;
|
||
}
|
||
|
||
require_once APP_ROOT . '/src/ThesisCreateController.php';
|
||
|
||
try {
|
||
$ctrl = ThesisCreateController::make();
|
||
$thesisId = $ctrl->submit($_POST, $_FILES);
|
||
|
||
// Mark the link as used
|
||
$shareLinkModel = new ShareLink(Database::getInstance());
|
||
$shareLinkModel->incrementUsage($link['id']);
|
||
|
||
// Clean up share-specific session data
|
||
unset($_SESSION[$shareCsrfKey]);
|
||
unset($_SESSION['share_verified_' . $slug]);
|
||
|
||
// Redirect to thanks page
|
||
header('Location: /partage/thanks.php?id=' . urlencode((string)$thesisId));
|
||
exit();
|
||
} catch (Exception $e) {
|
||
error_log('Share link submission error: ' . $e->getMessage());
|
||
|
||
$_SESSION['_flash_error'] = $e->getMessage();
|
||
$_SESSION['form_data_share_' . $slug] = $_POST;
|
||
$_SESSION[$shareCsrfKey] = bin2hex(random_bytes(32)); // Regenerate token
|
||
|
||
// Redirect back to the form
|
||
header('Location: /partage/' . urlencode($slug));
|
||
exit();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Helper to retrieve old form data (with support for array keys via : delimiter)
|
||
*/
|
||
function old(array $data, string $key, string $default = ''): string {
|
||
// Support nested keys like "jury_lecteurs:0"
|
||
$parts = explode(':', $key);
|
||
$value = $data;
|
||
foreach ($parts as $part) {
|
||
if (is_array($value) && isset($value[$part])) {
|
||
$value = $value[$part];
|
||
} else {
|
||
$value = $default;
|
||
break;
|
||
}
|
||
}
|
||
return is_array($value) ? htmlspecialchars(json_encode($value)) : htmlspecialchars((string)$value);
|
||
}
|