— Render the share-link form (or password gate) * /partage//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// $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; } // ── POST: form submission ───────────────────────────────────────────────────── if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'submit') { handleShareLinkSubmission($slug); exit; } // ── GET: render form ───────────────────────────────────────────────────────── App::boot(); // boot database + CSRF 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); ?> <?= $pageTitle ?> 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']); ?> <?= htmlspecialchars($pageTitle) ?>

🔒 Accès protégé


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]); ?> <?= htmlspecialchars($pageTitle) ?>

Soumettre un TFE

Informations du TFE 'name']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?> 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
Si cette case est cochée, votre contact apparaîtra sur la page publique de votre TFE.
Composition du jury 'jury_promoteur_ext', 'label' => 'Externe à l\'erg', 'checked' => !empty($formData['jury_promoteur_ext'])]; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>

Lecteurs·rices (optionnel) :

">
Cadre académique 2000, 'max' => date('Y') + 1]; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
Fichiers
Métadonnées complémentaires $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'; ?>
Degrés d'ouverture et licences

Je veux que mon TFE soit disponible sous les conditions suivantes :

🔓 Libre

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.

Au moins une des deux cases doit être cochée pour le degré Libre.

🔒 Interne

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.

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.

Au moins une des deux cases doit être cochée.

🚫 Interdit

Mon TFE n'est pas disponible en physique ni sur le site. Une note descriptive est disponible sur le site.

Généralités

  • L'auteur·ice peut décider entre trois degrés de partage de son travail : libre, interne, interdit.
  • L'auteur·ice peut, à tout moment, décider de restreindre le degré d'accès à son travail. Il ne peut néanmoins pas l'ouvrir davantage.
  • Le choix effectué dans ce formulaire sera d'application une semaine après la soutenance orale de l'auteur·ice. Celui-ci peut donc décider de restreindre ce choix avant sa publication (mais pas l'ouvrir).
  • L'erg se réserve le droit de restreindre le degré d'ouverture du TFE – ce en accord avec le règlement.
  • 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.
  • La diffusion « xamxam » est indépendante de la diffusion à la BAIU.
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 ──────────────────────────────────────────────────────── // Allow max 5 submissions per IP per 10 minutes (per share link) $rateLimitCacheDir = STORAGE_ROOT . '/cache/rate_limit'; if (!is_dir($rateLimitCacheDir)) { @mkdir($rateLimitCacheDir, 0755, true); } $shareRateLimitId = 'share_' . $slug . '_' . ($_SERVER['REMOTE_ADDR'] ?? 'unknown'); $rateLimit = new RateLimit(5, 600, $rateLimitCacheDir); // Use a custom identifier based on slug + IP so each share link is rate-limited independently $rateLimitFile = $rateLimitCacheDir . '/' . md5($shareRateLimitId) . '.json'; $data = []; if (file_exists($rateLimitFile)) { $content = file_get_contents($rateLimitFile); $data = json_decode($content, true) ?? []; } $now = time(); $data = array_filter($data, fn($ts) => ($now - $ts) < 600); if (count($data) >= 5) { $_SESSION['_flash_error'] = 'Trop de tentatives. Veuillez réessayer plus tard.'; header('Location: /partage/' . urlencode($slug)); exit; } $data[] = $now; if (is_writable($rateLimitCacheDir)) { file_put_contents($rateLimitFile, json_encode($data)); } // ── 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); }