maintenance: allow /partage through gate, fix fragment routing, add visibility table in admin

Extract shared filepond logic into src/FilepondHandler.php class.
Admin filepond endpoints delegate to the handler after AdminAuth check.
New partage filepond endpoints at /partage/actions/filepond/ verify
share_active session flag + CSRF token, no admin auth required.

JS reads filepond-base meta tag to determine endpoint path:
- Admin pages: /admin/actions/filepond (via head.php isAdmin check)
- Partage form: /partage/actions/filepond (explicit meta)

partage/index.php sets share_active = true on form render, cleans up on
successful submit. Partage process endpoint rate-limited to 30/5min per
session. No nginx changes needed — /partage/ location already handles
PHP without auth_basic.
This commit is contained in:
Pontoporeia
2026-05-12 15:19:32 +02:00
parent da153fc604
commit 6f7a02244f
22 changed files with 15010 additions and 532 deletions

View File

@@ -0,0 +1,35 @@
<?php
/**
* FilePond load endpoint — streams an existing thesis file back to FilePond (partage).
*
* GET /partage/actions/filepond/load.php?id={db_id}
*
* Auth: requires an active partage session (share_active flag).
*
* Used in edit mode to restore saved files into the FilePond UI.
*/
require_once __DIR__ . '/../../../../bootstrap.php';
require_once __DIR__ . '/../../../../src/FilepondHandler.php';
// ── Start session ────────────────────────────────────────────────────────
if (session_status() === PHP_SESSION_NONE) {
$isSecure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => $isSecure,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
}
// ── Auth: must have an active partage session ────────────────────────────
if (empty($_SESSION['share_active'])) {
http_response_code(403);
die('Accès refusé.');
}
$handler = new FilepondHandler('[filepond:partage]');
$handler->handleLoad();

View File

@@ -0,0 +1,56 @@
<?php
/**
* FilePond process endpoint — receives one file per request (partage).
*
* POST /partage/actions/filepond/process.php
* Headers: X-CSRF-Token
* Fields: file (multipart), queue_type (string)
*
* Auth: requires an active partage session (share_active flag) + CSRF token.
*
* Returns plain text file_id on success (200), or error message on failure (4xx).
*/
require_once __DIR__ . '/../../../../bootstrap.php';
require_once __DIR__ . '/../../../../src/FilepondHandler.php';
// ── Start session ────────────────────────────────────────────────────────
if (session_status() === PHP_SESSION_NONE) {
$isSecure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => $isSecure,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
}
// ── Auth: must have an active partage session ────────────────────────────
if (empty($_SESSION['share_active'])) {
http_response_code(403);
die('Accès refusé.');
}
// ── Rate limit: 30 uploads per 5 min per session ────────────────────────
require_once APP_ROOT . '/src/RateLimit.php';
$rateLimit = new RateLimit(30, 300, STORAGE_ROOT . '/cache/rate_limit');
$rateLimitId = 'fp_share_process_' . session_id();
if (!$rateLimit->checkKey($rateLimitId)) {
error_log('[filepond:partage:process] Rate limit hit');
http_response_code(429);
die('Trop de requêtes. Veuillez patienter.');
}
// ── CSRF via header ──────────────────────────────────────────────────────
$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!isset($_SESSION['csrf_token'])
|| !hash_equals($_SESSION['csrf_token'], $csrfHeader)) {
error_log('[filepond:partage:process] CSRF FAIL');
http_response_code(403);
die('Token CSRF invalide.');
}
$handler = new FilepondHandler('[filepond:partage]');
$handler->handleProcess();

View File

@@ -0,0 +1,42 @@
<?php
/**
* FilePond remove endpoint — soft-deletes a thesis_files row (partage).
*
* DELETE /partage/actions/filepond/remove.php
* Body: JSON { "db_id": 123 }
*
* Auth: requires an active partage session (share_active flag) + CSRF token.
*/
require_once __DIR__ . '/../../../../bootstrap.php';
require_once __DIR__ . '/../../../../src/FilepondHandler.php';
// ── Start session ────────────────────────────────────────────────────────
if (session_status() === PHP_SESSION_NONE) {
$isSecure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => $isSecure,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
}
// ── Auth: must have an active partage session ────────────────────────────
if (empty($_SESSION['share_active'])) {
http_response_code(403);
die('Accès refusé.');
}
// ── CSRF via header ──────────────────────────────────────────────────────
$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!isset($_SESSION['csrf_token'])
|| !hash_equals($_SESSION['csrf_token'], $csrfHeader)) {
http_response_code(403);
die('Token CSRF invalide.');
}
$handler = new FilepondHandler('[filepond:partage]');
$handler->handleRemove();

View File

@@ -0,0 +1,42 @@
<?php
/**
* FilePond revert endpoint — deletes a just-uploaded tmp file (partage).
*
* DELETE /partage/actions/filepond/revert.php
* Body: plain text file_id
*
* Auth: requires an active partage session (share_active flag) + CSRF token.
*/
require_once __DIR__ . '/../../../../bootstrap.php';
require_once __DIR__ . '/../../../../src/FilepondHandler.php';
// ── Start session ────────────────────────────────────────────────────────
if (session_status() === PHP_SESSION_NONE) {
$isSecure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => $isSecure,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
}
// ── Auth: must have an active partage session ────────────────────────────
if (empty($_SESSION['share_active'])) {
http_response_code(403);
die('Accès refusé.');
}
// ── CSRF via header ──────────────────────────────────────────────────────
$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!isset($_SESSION['csrf_token'])
|| !hash_equals($_SESSION['csrf_token'], $csrfHeader)) {
http_response_code(403);
die('Token CSRF invalide.');
}
$handler = new FilepondHandler('[filepond:partage]');
$handler->handleRevert();

View File

@@ -23,10 +23,11 @@ $slug = $parts[0] ?? '';
$action = $parts[1] ?? '';
// Special route: /partage/fragments/* (HTMX fragments under fragments/ subdirectory)
if (str_starts_with($slug, 'fragments/') && $_SERVER['REQUEST_METHOD'] === 'POST') {
if ($slug === 'fragments' && $_SERVER['REQUEST_METHOD'] === 'POST') {
App::boot();
$fragmentFile = __DIR__ . '/' . basename($slug);
if (file_exists($fragmentFile)) {
$fragmentBase = $action;
$fragmentFile = __DIR__ . '/fragments/' . $fragmentBase;
if ($fragmentBase !== '' && file_exists($fragmentFile)) {
require_once $fragmentFile;
} else {
http_response_code(404);
@@ -133,6 +134,7 @@ if (!$validationResult['valid']) {
// If already verified in session, skip the gate and render the form directly
if (!empty($_SESSION['share_verified_' . $slug])) {
error_log('[ShareLink] Session already verified for slug=' . $slug . ', rendering form');
$_SESSION['share_active'] = true;
$link = $validationResult['link'];
renderShareLinkForm($slug, $link);
exit;
@@ -151,6 +153,7 @@ if (!$validationResult['valid']) {
}
// Link is valid - render the form
$_SESSION['share_active'] = true;
$link = $validationResult['link'];
renderShareLinkForm($slug, $link);
@@ -217,6 +220,7 @@ function requirePasswordGate(array $link, string $slug): void
if ($shareLinkModel->verifyPassword($link, $_POST['share_password'])) {
// Store verified status in session
$_SESSION['share_verified_' . $slug] = true;
$_SESSION['share_active'] = true;
error_log('[ShareLink] Password verified OK for slug=' . $slug . ', redirecting to form');
// Redirect to clear POST data
header('Location: /partage/' . $slug);
@@ -417,6 +421,7 @@ function renderShareLinkForm(string $slug, array $link): void
<?php if (!empty($_SESSION['csrf_token'])): ?>
<meta name="csrf-token" content="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<?php endif; ?>
<meta name="filepond-base" content="/partage/actions/filepond">
<link rel="stylesheet" href="<?= App::assetV('/assets/css/common.css') ?>">
<link rel="stylesheet" href="<?= App::assetV('/assets/css/form.css') ?>">
<link rel="stylesheet" href="<?= App::assetV('/assets/css/filepond.min.css') ?>">
@@ -511,6 +516,7 @@ function handleShareLinkSubmission(string $slug): void
if (isset($_POST['share_password_submit'])) {
if ($shareLinkModel->verifyPassword($link, $_POST['share_password_submit'])) {
$_SESSION['share_verified_' . $slug] = true;
$_SESSION['share_active'] = true;
} else {
$_SESSION['_flash_error'] = 'Mot de passe incorrect.';
header('Location: /partage/' . urlencode($slug));
@@ -565,6 +571,7 @@ function handleShareLinkSubmission(string $slug): void
// Clean up share-specific session data
unset($_SESSION[$shareCsrfKey]);
unset($_SESSION['share_verified_' . $slug]);
unset($_SESSION['share_active']);
// Send confirmation e-mail - on delivery failure, redirect to retry page
$emailError = null;