mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
Reintroduce TFE duration metadata: DB columns, form fields, controllers, views, and migration
Add 'unsafe-eval' to CSP script-src directives (htmx requires Function())
This commit is contained in:
@@ -7,8 +7,6 @@ require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
require_once __DIR__ . '/../../../src/ShareLink.php';
|
||||
require_once __DIR__ . '/../../../src/AdminLogger.php';
|
||||
|
||||
error_log('[acces-etudiante.php] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | action=' . ($_POST['action'] ?? 'none') . ' | post_keys=' . implode(',', array_keys($_POST)));
|
||||
|
||||
App::adminGuard();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST'
|
||||
@@ -90,6 +88,15 @@ switch ($action) {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
if ($id > 0) {
|
||||
$shareLink->delete($id);
|
||||
App::redirect('/admin/acces.php', success: 'Lien supprimé définitivement.');
|
||||
} else {
|
||||
App::redirect('/admin/acces.php', error: 'Lien introuvable.');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
if ($id > 0) {
|
||||
$name = isset($_POST['name']) ? trim($_POST['name']) : null;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
error_log('[access-request.php] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | action=' . ($_POST['action'] ?? 'none') . ' | request_id=' . ($_POST['request_id'] ?? 0) . ' | post_keys=' . implode(',', array_keys($_POST)));
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|
||||
|
||||
@@ -11,8 +11,6 @@ require_once __DIR__ . '/../../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
require_once __DIR__ . '/../../../src/Database.php';
|
||||
|
||||
error_log('[account.php] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | action=' . ($_POST['action'] ?? 'change_password') . ' | post_keys=' . implode(',', array_keys($_POST)));
|
||||
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
// ── CSRF ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
*/
|
||||
require_once __DIR__ . "/../../../bootstrap.php";
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
error_log('[apropos.php] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | key=' . ($_POST['apropos_key'] ?? 'none') . ' | post_keys=' . implode(',', array_keys($_POST)));
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
$isAjax = (!empty($_SERVER['HTTP_ACCEPT']) && str_contains($_SERVER['HTTP_ACCEPT'], 'application/json'))
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
error_log('[corbeille.php] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | action=' . ($_POST['action'] ?? 'none'));
|
||||
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST'
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
error_log('[delete.php] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | is_bulk=' . (!empty($_POST['bulk']) ? '1' : '0') . ' | post_keys=' . implode(',', array_keys($_POST)));
|
||||
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
require_once __DIR__ . '/../../../src/Database.php';
|
||||
|
||||
93
app/public/admin/actions/draft.php
Normal file
93
app/public/admin/actions/draft.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin autosave draft endpoint.
|
||||
*
|
||||
* POST — receive all form fields and persist them to the session draft store.
|
||||
*
|
||||
* Drafts are scoped per mode:
|
||||
* - add: keyed by a generated token (stored in form)
|
||||
* - edit: keyed by thesis_id
|
||||
*
|
||||
* Excluded field patterns (not persisted as drafts):
|
||||
* - csrf_token
|
||||
* - FilePond metadata (filepond_mode, queue_file, filepond_*)
|
||||
* - Files-related fields
|
||||
* - Empty values
|
||||
*/
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
require_once APP_ROOT . '/src/AdminAuth.php';
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
// ── CSRF check ──────────────────────────────────────────────────────────
|
||||
if ($method !== 'POST') {
|
||||
http_response_code(405);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Méthode non autorisée.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (
|
||||
!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|
||||
|| !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])
|
||||
) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Token de sécurité invalide.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Determine draft key ─────────────────────────────────────────────────
|
||||
$draftToken = $_POST['draft_token'] ?? '';
|
||||
$thesisId = (int)($_POST['thesis_id'] ?? 0);
|
||||
|
||||
if ($draftToken !== '' && preg_match('/^[a-f0-9]{16}$/', $draftToken)) {
|
||||
$draftKey = 'admin_draft_' . $draftToken;
|
||||
} elseif ($thesisId > 0) {
|
||||
$draftKey = 'admin_draft_edit_' . $thesisId;
|
||||
} else {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Paramètres invalides.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Save all form fields ────────────────────────────────────────────────
|
||||
$excludePrefixes = [
|
||||
'csrf_token', 'share_link_token',
|
||||
'filepond_mode', 'queue_file', 'filepond_',
|
||||
];
|
||||
$excludeExact = ['draft_token', 'thesis_id', 'slug',
|
||||
'couverture', 'note_intention', 'files', 'annexes',
|
||||
'peertube_video', 'peertube_audio', 'cover_remove',
|
||||
'go', 'MAX_FILE_SIZE'];
|
||||
|
||||
$draft = [];
|
||||
foreach ($_POST as $key => $value) {
|
||||
if (in_array($key, $excludeExact, true)) continue;
|
||||
$skip = false;
|
||||
foreach ($excludePrefixes as $prefix) {
|
||||
if (str_starts_with($key, $prefix)) { $skip = true; break; }
|
||||
}
|
||||
if ($skip) continue;
|
||||
|
||||
if ($value === '' || $value === null || (is_array($value) && count($value) === 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$draft[$key] = $value;
|
||||
}
|
||||
|
||||
$_SESSION[$draftKey] = $draft;
|
||||
|
||||
// Rotate CSRF after mutation
|
||||
$newToken = bin2hex(random_bytes(32));
|
||||
$_SESSION['csrf_token'] = $newToken;
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'csrf_token' => $newToken,
|
||||
]);
|
||||
exit;
|
||||
@@ -3,8 +3,6 @@
|
||||
require_once __DIR__ . "/../../../bootstrap.php";
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
|
||||
error_log('[edit.php] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | content_type=' . ($_SERVER['CONTENT_TYPE'] ?? 'none') . ' | content_length=' . ($_SERVER['CONTENT_LENGTH'] ?? '0') . ' | post_keys=' . implode(',', array_keys($_POST)) . ' | files_count=' . count($_FILES) . ' | files_keys=' . implode(',', array_keys($_FILES)) . ' | queue_tfe=' . (isset($_FILES['queue_file']['name']['tfe']) ? count(array_filter($_FILES['queue_file']['name']['tfe'])) : 0));
|
||||
|
||||
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
@@ -37,6 +35,10 @@ try {
|
||||
// Regenerate CSRF token after successful save
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
|
||||
// Clear autosave draft + FilePond temp files
|
||||
unset($_SESSION['admin_draft_edit_' . $thesisId]);
|
||||
unset($_SESSION['filepond_tmp']);
|
||||
|
||||
AdminLogger::make()->logEdit($thesisId, $_POST['titre'] ?? $_POST['title'] ?? '');
|
||||
|
||||
App::flash('success', "TFE mis à jour avec succès!");
|
||||
|
||||
@@ -24,7 +24,6 @@ function relinkError(int $code, string $message): never {
|
||||
|
||||
// CSRF via header
|
||||
$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
error_log('[relink] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | csrf=' . (isset($_SESSION['csrf_token']) ? 'set' : 'missing') . ' | header=' . (strlen($csrfHeader) > 0 ? substr($csrfHeader, 0, 8) . '...' : 'empty') . ' | body_len=' . strlen(file_get_contents('php://input')));
|
||||
if (!isset($_SESSION['csrf_token'])
|
||||
|| !hash_equals($_SESSION['csrf_token'], $csrfHeader)) {
|
||||
relinkError(403, 'Token CSRF invalide.');
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
error_log('[form-help.php] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | key=' . ($_POST['form_help_key'] ?? 'none') . ' | post_keys=' . implode(',', array_keys($_POST)));
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
$isAjax = (!empty($_SERVER['HTTP_ACCEPT']) && str_contains($_SERVER['HTTP_ACCEPT'], 'application/json'))
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
|
||||
error_log('[formulaire.php] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | content_type=' . ($_SERVER['CONTENT_TYPE'] ?? 'none') . ' | content_length=' . ($_SERVER['CONTENT_LENGTH'] ?? '0') . ' | post_keys=' . implode(',', array_keys($_POST)) . ' | files_count=' . count($_FILES) . ' | files_keys=' . implode(',', array_keys($_FILES)) . ' | queue_tfe=' . (isset($_FILES['queue_file']['name']['tfe']) ? count(array_filter($_FILES['queue_file']['name']['tfe'])) : 0));
|
||||
|
||||
// Only suppress display_errors in production (cli-server = dev mode).
|
||||
if (php_sapi_name() !== 'cli-server') {
|
||||
ini_set('display_errors', 0);
|
||||
@@ -25,8 +23,6 @@ if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|
||||
die('Erreur de sécurité : token invalide. Veuillez recharger le formulaire.');
|
||||
}
|
||||
|
||||
error_log('[formulaire.php] full FILES array: ' . print_r($_FILES, true));
|
||||
|
||||
require_once APP_ROOT . '/src/Controllers/ThesisCreateController.php';
|
||||
require_once APP_ROOT . '/src/AppLogger.php';
|
||||
require_once APP_ROOT . '/src/AdminLogger.php';
|
||||
@@ -45,6 +41,10 @@ try {
|
||||
$logger->logSubmission('admin', $thesisId, $identifier, $authorName);
|
||||
$adminLogger->logAdd($thesisId, $identifier, $authorName);
|
||||
|
||||
// Clear autosave draft + FilePond temp files
|
||||
unset($_SESSION['admin_draft_' . ($_POST['draft_token'] ?? '')]);
|
||||
unset($_SESSION['admin_draft_add_token']);
|
||||
unset($_SESSION['filepond_tmp']);
|
||||
unset($_SESSION['csrf_token']);
|
||||
|
||||
$redirect = '../recapitulatif.php?id=' . $thesisId;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
error_log('[language.php] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | action=' . ($_POST['action'] ?? 'none') . ' | post_keys=' . implode(',', array_keys($_POST)));
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
error_log('[maintenance.php] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | action=' . ($_POST['action'] ?? 'none') . ' | post_keys=' . implode(',', array_keys($_POST)));
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST'
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
*/
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
error_log('[page.php] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | slug=' . ($_POST['slug'] ?? 'none') . ' | post_keys=' . implode(',', array_keys($_POST)));
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
$isAjax = (!empty($_SERVER['HTTP_ACCEPT']) && str_contains($_SERVER['HTTP_ACCEPT'], 'application/json'))
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
error_log('[publish.php] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | action=' . ($_POST['action'] ?? 'none') . ' | is_bulk=' . (!empty($_POST['bulk']) ? '1' : '0') . ' | post_keys=' . implode(',', array_keys($_POST)));
|
||||
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
require_once __DIR__ . '/../../../src/Database.php';
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
error_log('[settings.php] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | section=' . ($_POST['section'] ?? 'none') . ' | post_keys=' . implode(',', array_keys($_POST)));
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|
||||
|| !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
|
||||
error_log('[settings.php] CSRF FAIL | session_token=' . ($_SESSION['csrf_token'] ?? 'none') . ' | post_token=' . ($_POST['csrf_token'] ?? 'none'));
|
||||
App::flash('error', "Erreur de sécurité : token invalide.");
|
||||
header('Location: /admin/parametres.php');
|
||||
exit;
|
||||
@@ -21,7 +19,6 @@ $logger = AdminLogger::make();
|
||||
|
||||
$isHxRequest = (isset($_SERVER['HTTP_HX_REQUEST']) && $_SERVER['HTTP_HX_REQUEST'] === 'true');
|
||||
$section = $_POST['section'] ?? '';
|
||||
error_log('[settings.php] PROCESS | section=' . $section . ' | post_keys=' . implode(',', array_keys($_POST)));
|
||||
|
||||
/**
|
||||
* Return an HTML toast fragment for HTMX responses and exit.
|
||||
@@ -58,9 +55,7 @@ function hxToastError(string $message): never {
|
||||
if ($section === 'formulaire_restrictions') {
|
||||
// HTMX may not send unchecked checkboxes even with hidden 0-value inputs;
|
||||
// missing key means unchecked → treat as '0'.
|
||||
$rawPost = $_POST['restricted_files_enabled'] ?? '(missing)';
|
||||
$newValue = empty($_POST['restricted_files_enabled']) ? '0' : '1';
|
||||
error_log('[settings.php] SAVE formulaire_restrictions | restricted_files_enabled raw=' . var_export($rawPost, true) . ' | resolved=' . $newValue);
|
||||
$db->setSetting('restricted_files_enabled', $newValue);
|
||||
$logger->logFormSettingsUpdate(['restricted_files_enabled' => $newValue]);
|
||||
if ($isHxRequest) {
|
||||
@@ -76,9 +71,7 @@ if ($section === 'formulaire_restrictions') {
|
||||
];
|
||||
$newValues = [];
|
||||
foreach ($allowed as $key) {
|
||||
$raw = $_POST[$key] ?? '(missing)';
|
||||
$value = empty($_POST[$key]) ? '0' : '1';
|
||||
error_log('[settings.php] SAVE formulaire_acces | ' . $key . ' raw=' . var_export($raw, true) . ' | resolved=' . $value);
|
||||
$db->setSetting($key, $value);
|
||||
$newValues[$key] = $value;
|
||||
}
|
||||
@@ -89,14 +82,10 @@ if ($section === 'formulaire_restrictions') {
|
||||
App::flash('success', "Degrés d'ouverture mis à jour.");
|
||||
}
|
||||
} elseif ($section === 'objet_types') {
|
||||
$rawThese = $_POST['objet_these_enabled'] ?? '(missing)';
|
||||
$rawFrart = $_POST['objet_frart_enabled'] ?? '(missing)';
|
||||
$newValues = [
|
||||
'objet_these_enabled' => empty($_POST['objet_these_enabled']) ? '0' : '1',
|
||||
'objet_frart_enabled' => empty($_POST['objet_frart_enabled']) ? '0' : '1',
|
||||
];
|
||||
error_log('[settings.php] SAVE objet_types | objet_these_enabled raw=' . var_export($rawThese, true) . ' | resolved=' . $newValues['objet_these_enabled']);
|
||||
error_log('[settings.php] SAVE objet_types | objet_frart_enabled raw=' . var_export($rawFrart, true) . ' | resolved=' . $newValues['objet_frart_enabled']);
|
||||
$db->setSetting('objet_these_enabled', $newValues['objet_these_enabled']);
|
||||
$db->setSetting('objet_frart_enabled', $newValues['objet_frart_enabled']);
|
||||
$logger->logObjetTypesUpdate($newValues);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . "/../../../bootstrap.php";
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
error_log('[smtp-test.php] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | test_email=' . ($_POST['test_email'] ?? 'none') . ' | post_keys=' . implode(',', array_keys($_POST)));
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
error_log('[tag.php] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | action=' . ($_POST['action'] ?? 'none') . ' | post_keys=' . implode(',', array_keys($_POST)));
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
error_log('[visibility.php] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | action=' . ($_POST['action'] ?? 'none') . ' | is_bulk=' . (!empty($_POST['bulk']) ? '1' : '0') . ' | post_keys=' . implode(',', array_keys($_POST)));
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|
||||
|
||||
@@ -83,35 +83,33 @@ $extraJsInline = '';
|
||||
|
||||
if ($editType === 'page' || $editType === 'about_page') {
|
||||
$initialContent = $page["content"] ?? "";
|
||||
$extraJs = ["/assets/js/vendor/overtype.min.js", "/assets/js/app/autosave-handler.js"];
|
||||
$extraJs = ["/assets/js/vendor/overtype.min.js"];
|
||||
$extraJsInline = <<<'JS'
|
||||
var OT = window.OverType.default || window.OverType;
|
||||
var hidden = document.getElementById('content');
|
||||
var editor = new OT(document.getElementById('editor'), {
|
||||
value: hidden.value,
|
||||
minHeight: '400px',
|
||||
minHeight: '100%',
|
||||
spellcheck: false,
|
||||
toolbar: true,
|
||||
onChange: function(value) {
|
||||
hidden.value = value;
|
||||
hidden.dispatchEvent(new CustomEvent('overtype:change', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
JS;
|
||||
} elseif ($editType === 'form_help') {
|
||||
$initialContent = $formHelpContent;
|
||||
$extraJs = ["/assets/js/vendor/overtype.min.js", "/assets/js/app/autosave-handler.js"];
|
||||
$extraJs = ["/assets/js/vendor/overtype.min.js"];
|
||||
$extraJsInline = <<<'JS'
|
||||
var OT = window.OverType.default || window.OverType;
|
||||
var hidden = document.getElementById('content');
|
||||
var editor = new OT(document.getElementById('editor'), {
|
||||
value: hidden.value,
|
||||
minHeight: '400px',
|
||||
minHeight: '100%',
|
||||
spellcheck: false,
|
||||
toolbar: true,
|
||||
onChange: function(value) {
|
||||
hidden.value = value;
|
||||
hidden.dispatchEvent(new CustomEvent('overtype:change', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
JS;
|
||||
|
||||
@@ -123,12 +123,12 @@ extract(FormBootstrap::adminFormVariables(
|
||||
mode: 'edit',
|
||||
formAction: '/admin/actions/edit.php',
|
||||
hiddenFields:
|
||||
'<input type="hidden" name="csrf_token" value="' . htmlspecialchars($_SESSION['csrf_token']) . '">'
|
||||
. '<input type="hidden" name="thesis_id" value="' . $thesisId . '">',
|
||||
'<input type="hidden" name="csrf_token" value="' . htmlspecialchars($_SESSION['csrf_token']) . '">',
|
||||
formData: $formData,
|
||||
siteSettings: $siteSettings,
|
||||
helpBlocks: $helpBlocks,
|
||||
options: [
|
||||
'thesisId' => $thesisId,
|
||||
'filesMode' => 'edit',
|
||||
'existingWebsiteUrl' => $existingWebsiteUrl,
|
||||
'existingWebsiteLabel' => $existingWebsiteLabel,
|
||||
@@ -144,6 +144,8 @@ extract(FormBootstrap::adminFormVariables(
|
||||
'currentFiles' => $currentFiles ?? [],
|
||||
'currentContextNote' => $currentContextNote ?? null,
|
||||
'currentContactVisible' => $currentContactVisible ?? null,
|
||||
'currentDurationValue' => $currentDurationValue ?? null,
|
||||
'currentDurationUnit' => $currentDurationUnit ?? 'pages',
|
||||
'contactInterne' => $contactInterne ?? null,
|
||||
'contactPublic' => $contactPublic ?? false,
|
||||
'showCoverPreview' => true,
|
||||
@@ -158,6 +160,10 @@ $formData['license_id'] = $currentLicenseId;
|
||||
$formData['license_custom'] = $currentRaw['license_custom'] ?? '';
|
||||
$formData['cc2r'] = $currentRaw['cc2r'] ?? false;
|
||||
|
||||
// Duration variables for the form template
|
||||
$durationValue = $currentDurationValue ?? null;
|
||||
$durationUnit = $currentDurationUnit ?? 'pages';
|
||||
|
||||
// Asset arrays and page chrome
|
||||
$isAdmin = true;
|
||||
$bodyClass = 'admin-body';
|
||||
|
||||
@@ -13,8 +13,6 @@ AdminAuth::requireLogin();
|
||||
|
||||
$storageRoot = STORAGE_ROOT;
|
||||
|
||||
error_log('[file-browser] ENTRY | dir=' . ($_GET['dir'] ?? '(root)') . ' | storageRoot=' . $storageRoot);
|
||||
|
||||
// Determine which directory to browse
|
||||
$relDir = trim($_GET['dir'] ?? '', '/');
|
||||
if ($relDir !== '' && !preg_match('#^(tfe|these|frart|documents|theses)(/|$)#', $relDir)) {
|
||||
|
||||
@@ -17,7 +17,6 @@ $importResults = [];
|
||||
$importDone = false;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
|
||||
error_log('[admin/index.php] IMPORT ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | content_type=' . ($_SERVER['CONTENT_TYPE'] ?? 'none') . ' | csv_file_error=' . ($_FILES['csv_file']['error'] ?? 'none') . ' | post_keys=' . implode(',', array_keys($_POST)));
|
||||
if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
|
||||
$importErrors[] = "Erreur de sécurité : token invalide.";
|
||||
} else {
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
require_once __DIR__ . '/../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../src/AdminAuth.php';
|
||||
|
||||
error_log('[admin/login.php] ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | is_auth=' . (AdminAuth::isAuthenticated() ? '1' : '0') . ' | has_password=' . (AdminAuth::hasPassword() ? '1' : '0'));
|
||||
|
||||
if (!AdminAuth::hasPassword()) {
|
||||
header('Location: /admin/');
|
||||
exit;
|
||||
@@ -24,8 +22,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
|
||||
$pageTitle = 'Connexion';
|
||||
$isAdmin = true; $bodyClass = 'admin-body';
|
||||
$isAdmin = true; $isLogin = true; $bodyClass = 'admin-body';
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
include APP_ROOT . '/templates/header.php';
|
||||
include APP_ROOT . '/templates/admin/login.php';
|
||||
require_once APP_ROOT . '/templates/admin/footer.php';
|
||||
// Login page does not render the admin footer (no toast-region poll, no HTMX extras).
|
||||
// It closes <html> directly so there is no dangling HTMX polling the toast endpoint
|
||||
// while unauthenticated.
|
||||
echo "\n</body>\n</html>";
|
||||
|
||||
@@ -24,7 +24,10 @@ if (isset($_GET['id'])) {
|
||||
if ($thesisId !== false && $thesisId > 0) {
|
||||
try {
|
||||
$db = new Database();
|
||||
$thesis = $db->getThesis($thesisId);
|
||||
// Student-mode preview: only show published theses.
|
||||
$thesis = $studentMode
|
||||
? $db->getThesisById($thesisId)
|
||||
: $db->getThesis($thesisId);
|
||||
|
||||
if (!$thesis) {
|
||||
$error = "TFE non trouvé.";
|
||||
|
||||
@@ -9,7 +9,13 @@
|
||||
require_once __DIR__ . '/../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../src/AdminAuth.php';
|
||||
|
||||
AdminAuth::requireLogin();
|
||||
// Don't redirect unauthenticated requests — just return empty (defense-in-depth).
|
||||
// The toast-region poll fires on <hx-trigger="load">; if the user is on the
|
||||
// login page they are not authenticated yet.
|
||||
if (!AdminAuth::isAuthenticated()) {
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
|
||||
$flash = App::consumeFlash();
|
||||
|
||||
|
||||
@@ -541,7 +541,7 @@ th.admin-ap-col {
|
||||
|
||||
.admin-body main > section[aria-labelledby^="settings-"] fieldset legend {
|
||||
padding: 0;
|
||||
font-weight: 600;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
@@ -1303,7 +1303,7 @@ th.admin-ap-col {
|
||||
flex: 1 1 260px;
|
||||
}
|
||||
.param-smtp-test-row label {
|
||||
font-weight: 600;
|
||||
font-weight: 400;
|
||||
margin-bottom: var(--space-2xs);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
@@ -1909,7 +1909,7 @@ th.admin-ap-col {
|
||||
.fhb-edit-label {
|
||||
display: block;
|
||||
font-size: var(--step--2);
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--space-3xs);
|
||||
}
|
||||
|
||||
@@ -13,23 +13,32 @@ details > summary::-webkit-details-marker {
|
||||
}
|
||||
|
||||
details {
|
||||
padding: var(--space-s);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius, 6px);
|
||||
background: var(--bg-secondary, var(--surface));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
details[open] {
|
||||
padding-bottom: var(--space-s);
|
||||
}
|
||||
|
||||
details > :not(summary) {
|
||||
margin-left: var(--space-s);
|
||||
margin-right: var(--space-s);
|
||||
}
|
||||
|
||||
summary {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
color: var(--accent-secondary);
|
||||
transition: color 0.15s;
|
||||
|
||||
svg {
|
||||
fill: var(--accent-secondary);
|
||||
vertical-align: text-bottom;
|
||||
height: 1.4em;
|
||||
}
|
||||
color: var(--text-primary);
|
||||
transition: color 0.15s, background 0.15s;
|
||||
padding: var(--space-s);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
summary:hover {
|
||||
color: var(--accent-primary);
|
||||
background: var(--hover-bg, rgba(0, 0, 0, 0.03));
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: var(--space-3xs);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ── Text inputs, selects, textareas ────────────────────────────────── */
|
||||
@@ -95,7 +96,7 @@ fieldset > *:not(:last-child) { margin-bottom: var(--space-xs); }
|
||||
|
||||
legend {
|
||||
font-size: var(--step--1);
|
||||
font-weight: 600;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
|
||||
@@ -242,6 +242,6 @@
|
||||
|
||||
.admin-dialog label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
font-weight: 400;
|
||||
margin-bottom: var(--space-3xs);
|
||||
}
|
||||
|
||||
@@ -232,3 +232,68 @@
|
||||
.file-browser-file .file-browser-select-btn:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* ── Full-page editor (contenus-edit overtype) ─────────────────────── */
|
||||
|
||||
/* Make the main content area a flex column so the form fills remaining height */
|
||||
.admin-body .full-editor-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.admin-form--full-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.admin-form--full-editor #editor {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.admin-form--full-editor #editor .n-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.admin-form--full-editor #editor .n-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-form--full-editor #editor .n-input {
|
||||
height: 100% !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.admin-form--full-editor #editor .n-preview {
|
||||
height: 100% !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.full-editor-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-s);
|
||||
padding-bottom: var(--space-s);
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
margin-bottom: var(--space-s);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.full-editor-label {
|
||||
font-size: var(--step--1);
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.full-editor-toolbar .btn--primary {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
> label,
|
||||
.admin-form div:has(select:required) > label,
|
||||
.admin-form div:has(textarea:required) > label {
|
||||
font-weight: 600;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Asterisk on required field labels */
|
||||
@@ -161,6 +161,19 @@
|
||||
gap: var(--space-3xs);
|
||||
}
|
||||
|
||||
.admin-form-group--inline {
|
||||
flex-direction: row;
|
||||
align-items: end;
|
||||
gap: var(--space-xs);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.admin-form-group--inline > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3xs);
|
||||
}
|
||||
|
||||
/* ── Jury fieldset ──────────────────────────────────────────────────────── */
|
||||
|
||||
/* ── Website-URL inline fieldset (shown/hidden via HTMX) ────────────────── */
|
||||
@@ -178,7 +191,7 @@
|
||||
.admin-body fieldset fieldset.admin-jury-lecteurs > legend,
|
||||
.student-body fieldset fieldset.admin-jury-lecteurs > legend {
|
||||
font-size: var(--step--2);
|
||||
font-weight: 600;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-tertiary);
|
||||
@@ -845,7 +858,7 @@
|
||||
|
||||
.retry-email-form label {
|
||||
font-size: var(--step--1);
|
||||
font-weight: 600;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.retry-email-form input[type="email"] {
|
||||
@@ -1207,7 +1220,7 @@ legend {
|
||||
}
|
||||
|
||||
.licence-options-fieldset legend {
|
||||
font-weight: 600;
|
||||
font-weight: 400;
|
||||
font-size: var(--step--1);
|
||||
color: var(--text-secondary);
|
||||
padding: 0 var(--space-2xs);
|
||||
@@ -1227,7 +1240,7 @@ legend {
|
||||
.admin-form > div:not(.admin-form-footer) > label,
|
||||
.admin-form > div:not(.admin-form-footer) > span.admin-row-label {
|
||||
padding-top: 0;
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Labels inside fieldsets (checkbox/radio groups) */
|
||||
|
||||
@@ -11,16 +11,16 @@
|
||||
}
|
||||
|
||||
/* ---- 6-column index layout ---- */
|
||||
/* Column fractions: years=0.4 ap=1 or=1.2 fi=0.7 students=1 kw=1 */
|
||||
/* Equal-width columns except Années (years) = narrower */
|
||||
.repertoire-index {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(3rem, 0.4fr)
|
||||
minmax(12rem, 1.4fr)
|
||||
minmax(9rem, 0.8fr)
|
||||
minmax(7rem, 0.8fr)
|
||||
minmax(8rem, 0.7fr)
|
||||
minmax(7rem, 1fr);
|
||||
minmax(3rem, 0.45fr)
|
||||
minmax(12rem, 1fr)
|
||||
minmax(9rem, 1fr)
|
||||
minmax(7rem, 1fr)
|
||||
minmax(8rem, 1fr)
|
||||
minmax(min-content, 1fr);
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: var(--space-s);
|
||||
justify-content: space-between;
|
||||
@@ -78,14 +78,14 @@
|
||||
.repertoire-col > h2 {
|
||||
grid-row: 1;
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--step-0);
|
||||
font-size: var(--step--1);
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
color: var(--text-primary);
|
||||
font-weight: 398;
|
||||
margin: 0;
|
||||
padding: var(--space-xs) var(--space-2xs) var(--space-3xs);
|
||||
border-bottom: 1px solid var(--border-secondary);
|
||||
padding: var(--space-xs) 0 var(--space-3xs) 0;
|
||||
border-bottom: 1px solid var(--text-primary);
|
||||
align-self: end;
|
||||
hyphens: manual;
|
||||
word-break: normal;
|
||||
@@ -96,7 +96,7 @@
|
||||
grid-row: 2;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: var(--space-2xs) var(--space-2xs) var(--space-l);
|
||||
padding: var(--space-2xs) 0 var(--space-l) 0;
|
||||
}
|
||||
|
||||
/* Strip list chrome inside repertoire columns */
|
||||
|
||||
@@ -255,7 +255,7 @@
|
||||
|
||||
.tfe-access-request-form label {
|
||||
font-size: var(--step--1);
|
||||
font-weight: 600;
|
||||
font-weight: 400;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,14 @@
|
||||
* parse errors instead of silently swallowing them (unlike the
|
||||
* old autosave.js .catch(() => {}) pattern).
|
||||
*/
|
||||
function _handleAutosaveResponse(event) {
|
||||
function handleAutosaveResponse(event) {
|
||||
// Only handle responses from autosave endpoints (draft.php).
|
||||
// The htmx:afterRequest event bubbles, so child elements'
|
||||
// HTMX requests (e.g. licence fragment, pill-search) also
|
||||
// reach this handler. We filter by URL to avoid mixing them.
|
||||
const url = event.detail.requestConfig?.path || "";
|
||||
if (!url.includes("draft.php")) return;
|
||||
|
||||
const form = event.target.closest("form");
|
||||
const status = form ? form.querySelector("[data-autosave-status]") : null;
|
||||
|
||||
@@ -56,11 +63,17 @@ function _handleAutosaveResponse(event) {
|
||||
|
||||
// Show saving indicator while request is in flight
|
||||
document.body.addEventListener("htmx:beforeRequest", (e) => {
|
||||
const url = e.detail.requestConfig?.path || "";
|
||||
if (!url.includes("draft.php")) return;
|
||||
// The autosave request comes from the hidden probe div, so find
|
||||
// the status indicator by searching the closest form.
|
||||
const el = e.target;
|
||||
if (!el) return;
|
||||
const status = el.querySelector("[data-autosave-status]");
|
||||
const form = el?.closest?.("form");
|
||||
const status = form?.querySelector?.("[data-autosave-status]");
|
||||
if (status) {
|
||||
status.textContent = "Enregistrement…";
|
||||
status.className = "autosave-status autosave-status--saving";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -277,7 +277,11 @@ function renderShareLinkForm(string $slug, array $link): void
|
||||
// Filter out PACS from AP programs for student forms (spec: admin-only AP)
|
||||
$apPrograms = array_values(array_filter($apPrograms, fn($ap) => ($ap['code'] ?? '') !== 'PACS'));
|
||||
|
||||
$formData = $_SESSION['form_data_share_' . $slug] ?? [];
|
||||
// Hydrate form data from session draft (autosave). Flash repopulation
|
||||
// (from validation redirects) takes priority over stale draft entries.
|
||||
$draftKey = 'partage_draft_' . $slug;
|
||||
$draftData = $_SESSION[$draftKey] ?? [];
|
||||
$formData = array_merge($draftData, $_SESSION['form_data_share_' . $slug] ?? []);
|
||||
unset($_SESSION['form_data_share_' . $slug]);
|
||||
|
||||
// Determine allowed objet values for this link
|
||||
@@ -324,7 +328,8 @@ function renderShareLinkForm(string $slug, array $link): void
|
||||
// ── Shared form variables ──────────────────────────────────────────────
|
||||
$mode = 'partage';
|
||||
$formAction = '/partage/' . urlencode($slug) . '/submit';
|
||||
$hiddenFields = '<input type="hidden" name="share_link_token" value="' . htmlspecialchars($shareCsrfToken) . '">';
|
||||
$hiddenFields = '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($csrfToken) . '">'
|
||||
. '<input type="hidden" name="share_link_token" value="' . htmlspecialchars($shareCsrfToken) . '">';
|
||||
|
||||
$oldFn = $shareOldFn;
|
||||
$withAutofocusFn = $shareWithAutofocusFn;
|
||||
@@ -399,6 +404,11 @@ function renderShareLinkForm(string $slug, array $link): void
|
||||
$currentContextNote = null;
|
||||
$currentContactVisible = null;
|
||||
|
||||
// ── Autosave wiring ─────────────────────────────────────────────────┐
|
||||
$autosaveUrl = '/partage/fragments/draft.php?slug=' . urlencode($slug);
|
||||
$formExtraAttrs = '';
|
||||
$showAutosaveStatus = true;
|
||||
|
||||
include APP_ROOT . '/templates/partage/form-page.php';
|
||||
?>
|
||||
<main id="main-content">
|
||||
@@ -537,6 +547,8 @@ function handleShareLinkSubmission(string $slug): void
|
||||
unset($_SESSION['share_verified_' . $slug]);
|
||||
unset($_SESSION['share_active']);
|
||||
unset($_SESSION['share_primed_files_' . $slug]);
|
||||
// Clear autosave draft — submission succeeded
|
||||
unset($_SESSION['partage_draft_' . $slug]);
|
||||
// Clear FilePond temp file tracking — files have been moved to permanent storage
|
||||
unset($_SESSION['filepond_tmp']);
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ if ($thesisId <= 0) {
|
||||
}
|
||||
|
||||
$db = Database::getInstance();
|
||||
$thesis = $db->getThesis($thesisId);
|
||||
// Only published theses are visible via public recap (no slug-auth here).
|
||||
$thesis = $db->getThesisById($thesisId);
|
||||
if (!$thesis) {
|
||||
http_response_code(404);
|
||||
die('TFE introuvable.');
|
||||
|
||||
Reference in New Issue
Block a user