From e6829994b6d186a2155f19e6b5fa60da67e43869 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Fri, 8 May 2026 13:03:18 +0200 Subject: [PATCH] Refactor + feat: unify format/fichiers HTMX fragment, reorder format types, add file constraints, fix admin auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * **Unified Format + Fichiers into a single HTMX fragment** * Introduced `app/public/partage/fichiers-fragment.php` as shared dynamic block returning both format checkboxes and adaptive “Fichiers” fieldset * Logic adapts inputs based on selected formats: * no selection / upload formats → standard file inputs * “Site web” → URL fields only * “Site web + upload” → file inputs + URL sub-fieldset * Added admin wrapper: `app/public/admin/fichiers-fragment.php` (gated via `admin_mode=1`) * Added `app/public/admin/format-website-fragment.php` for edit-mode website URL toggling * Wired route `/partage/fichiers-fragment` in `app/public/partage/index.php` * Refactored `form.php` (add/edit partage) to use single `#format-fichiers-block` instead of separate fragments * Edit mode format checkboxes now target `format-website-fragment.php` → `#edit-website-url-fieldset` * Added `$hxInclude` support in `checkbox-list.php` for configurable HTMX includes * **Format system migration + ordering** * Migration `020_format_types_sort_and_rename.sql`: * added `sort_order` column to `format_types` * inserted new format **Image** * defined ordering: Écriture · Image · Audio · Vidéo · Site web · Performance · Objet éditorial · Installation · Autre * `Database.php`: format queries now use `ORDER BY sort_order, id` * `fichiers-fragment.php`: * uses ordered format list * resolves Image/Vidéo/Audio by name * introduces `$hasImage` flag * preserves `admin_mode` across HTMX requests * **File constraints and UX updates** * Enforced **100 MB PDF limit** * `ThesisCreateController`: `MAX_PDF_SIZE = 100MB` for PDFs only * `ThesisEditController`: same PDF-specific constraint applied * Other file types remain capped at 500 MB * Updated UI hints in `fichiers-fragment.php` and edit form: * explicitly mention 100 MB PDF limit * added reference to `bentopdf.com` for compression guidance * `file-field.php`: added `$hintRaw` to allow HTML rendering in hints * **Admin authentication fix** * Fixed missing auth in admin fragments * Added `require_once AdminAuth.php` * Replaced direct usage with `AdminAuth::requireLogin()` * Applied consistent pattern with existing fragment authentication approach * **Migrations included** * `019_add_ecriture_format.sql` * `020_format_types_sort_and_rename.sql` * **Files affected** * Controllers: `ThesisCreateController`, `ThesisEditController` * DB layer: `Database.php` * Public fragments: `partage/fichiers-fragment.php`, `admin/fichiers-fragment.php`, `admin/format-website-fragment.php` * Templates: `form.php`, `checkbox-list.php`, `file-field.php` * Routing: `partage/index.php` * Misc: `TODO.md` This consolidates format normalization, HTMX UI simplification, file validation rules, and admin stability fixes into a single coherent system update. --- TODO.md | 23 ++ .../applied/019_add_ecriture_format.sql | 2 + .../020_format_types_sort_and_rename.sql | 13 ++ app/public/admin/fichiers-fragment.php | 17 ++ app/public/admin/format-website-fragment.php | 57 +++++ app/public/partage/fichiers-fragment.php | 205 ++++++++++++++++++ app/public/partage/index.php | 9 +- .../Controllers/ThesisCreateController.php | 9 +- app/src/Controllers/ThesisEditController.php | 9 +- app/src/Database.php | 2 +- app/templates/partials/form/checkbox-list.php | 16 +- app/templates/partials/form/file-field.php | 5 +- app/templates/partials/form/form.php | 72 +++--- 13 files changed, 390 insertions(+), 49 deletions(-) create mode 100644 app/migrations/applied/019_add_ecriture_format.sql create mode 100644 app/migrations/applied/020_format_types_sort_and_rename.sql create mode 100644 app/public/admin/fichiers-fragment.php create mode 100644 app/public/admin/format-website-fragment.php create mode 100644 app/public/partage/fichiers-fragment.php diff --git a/TODO.md b/TODO.md index fc35e1e..f327347 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,29 @@ ## Completed +- [x] PDF 100 MB limit + bentopdf mention + - [x] `ThesisCreateController`: `MAX_PDF_SIZE = 100 MB`; PDFs checked against it, other files still 500 MB + - [x] `ThesisEditController`: same per-PDF limit applied + - [x] `fichiers-fragment.php`: note d'intention and TFE hints mention 100 MB PDF limit + bentopdf.com link + - [x] `form.php` edit-mode new-files hint updated + - [x] `file-field.php`: added `$hintRaw` flag to allow HTML in hints + +- [x] Format types: reorder, rename, add Image/Écriture + - [x] Migration 019: add Écriture + - [x] Migration 020: add `sort_order` column, rename Autre → Etc. / Autre, add Image, set display order (Écriture · Image · Audio · Vidéo · Site web · Performance · Objet éditorial · Installation · Etc. / Autre) + - [x] `Database.php` format_types query uses `ORDER BY sort_order, id` + - [x] `fichiers-fragment.php` uses `ORDER BY sort_order, id`; Image/Vidéo/Audio IDs resolved via name map + - [ ] TODO: Vidéo + Audio — PeerTube API upload (notice shown in form for now) + +- [x] Combined Format + Fichiers into HTMX-swappable block + - [x] `partage/fichiers-fragment.php` — new combined fragment: format checkboxes + fichiers fieldset that adapts based on selected formats (upload inputs / URL fields / both) + - [x] Route `/partage/fichiers-fragment` added to `partage/index.php` + - [x] `admin/fichiers-fragment.php` — admin-gated wrapper for the same fragment (sets `admin_mode=1`) + - [x] `admin/format-website-fragment.php` — admin-gated fragment for edit-mode website URL fieldset toggle + - [x] `form.php` — add/partage mode: replaced separate Format + Fichiers + website-url-fieldset with single `#format-fichiers-block` server-rendered via shared fragment + - [x] `form.php` — edit mode: Format checkboxes wire to `admin/format-website-fragment.php` → `#edit-website-url-fieldset` (existing-file management untouched) + - [x] `checkbox-list.php` — added `$hxInclude` variable (defaults to `'this, #website-url-fieldset'`) so callers can customise included fields + - [x] TDD analysis + new test suites - [x] **Bug fixed**: `SearchController::handleSearch()` — `$coverMap` undefined variable + never populated for search results - [x] `ShareLinkTest` (13 tests) — `generateSlug`, all `validateLink` branches, `verifyPassword`, `incrementUsage`, `objet_restriction` diff --git a/app/migrations/applied/019_add_ecriture_format.sql b/app/migrations/applied/019_add_ecriture_format.sql new file mode 100644 index 0000000..55d565e --- /dev/null +++ b/app/migrations/applied/019_add_ecriture_format.sql @@ -0,0 +1,2 @@ +-- Migration 019: Add "Écriture" format type +INSERT INTO format_types (name) VALUES ('Écriture'); diff --git a/app/migrations/applied/020_format_types_sort_and_rename.sql b/app/migrations/applied/020_format_types_sort_and_rename.sql new file mode 100644 index 0000000..fd84387 --- /dev/null +++ b/app/migrations/applied/020_format_types_sort_and_rename.sql @@ -0,0 +1,13 @@ +-- Migration 020: Add sort_order to format_types, rename/reorder entries, add Image +ALTER TABLE format_types ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 99; + +UPDATE format_types SET sort_order = 1 WHERE name = 'Écriture'; +UPDATE format_types SET sort_order = 3 WHERE name = 'Audio'; +UPDATE format_types SET sort_order = 4 WHERE name = 'Vidéo'; +UPDATE format_types SET sort_order = 5 WHERE name = 'Site web'; +UPDATE format_types SET sort_order = 6 WHERE name = 'Performance'; +UPDATE format_types SET sort_order = 7 WHERE name = 'Objet éditorial'; +UPDATE format_types SET sort_order = 8 WHERE name = 'Installation'; +UPDATE format_types SET sort_order = 9 WHERE name = 'Autre'; + +INSERT OR IGNORE INTO format_types (name, sort_order) VALUES ('Image', 2); diff --git a/app/public/admin/fichiers-fragment.php b/app/public/admin/fichiers-fragment.php new file mode 100644 index 0000000..9cb52d1 --- /dev/null +++ b/app/public/admin/fichiers-fragment.php @@ -0,0 +1,17 @@ +getConnection(); + +$stmt = $db->prepare('SELECT id FROM format_types WHERE name = ? LIMIT 1'); +$stmt->execute(['Site web']); +$websiteFormatId = $stmt->fetchColumn(); + +if (!$websiteFormatId) { + echo ''; + exit; +} + +$selectedFormats = isset($_POST['formats']) && is_array($_POST['formats']) + ? array_map('intval', $_POST['formats']) + : []; + +if (!in_array((int)$websiteFormatId, $selectedFormats, true)) { + echo ''; + exit; +} + +$websiteUrl = htmlspecialchars($_POST['website_url'] ?? ''); +$websiteLabel = htmlspecialchars($_POST['website_label'] ?? ''); +?> +
+ Site web +
+ +
+ + Si le TFE est un site web, entrez son URL ici. Il sera affiché comme un site embarqué sur la page du TFE. +
+
+
+ + +
+
diff --git a/app/public/partage/fichiers-fragment.php b/app/public/partage/fichiers-fragment.php new file mode 100644 index 0000000..bc8f198 --- /dev/null +++ b/app/public/partage/fichiers-fragment.php @@ -0,0 +1,205 @@ +getConnection(); + +// Load all format types in display order +$allFormats = $db->query('SELECT id, name FROM format_types ORDER BY sort_order, id') + ->fetchAll(PDO::FETCH_ASSOC); + +// Build name→id map for format logic +$formatIdByName = []; +foreach ($allFormats as $f) { + $formatIdByName[$f['name']] = (int)$f['id']; +} + +$siteWebId = $formatIdByName['Site web'] ?? null; +$videoId = $formatIdByName['Vidéo'] ?? null; +$audioId = $formatIdByName['Audio'] ?? null; +$imageId = $formatIdByName['Image'] ?? null; + +$selectedFormats = isset($_POST['formats']) && is_array($_POST['formats']) + ? array_map('intval', $_POST['formats']) + : []; + +$adminMode = ($_POST['admin_mode'] ?? '0') === '1'; + +$hasSiteWeb = $siteWebId && in_array($siteWebId, $selectedFormats, true); +$hasVideo = $videoId && in_array($videoId, $selectedFormats, true); +$hasAudio = $audioId && in_array($audioId, $selectedFormats, true); +$hasImage = $imageId && in_array($imageId, $selectedFormats, true); + +// Show standard file inputs unless *only* Site web is selected +$hasNonWebFormat = !empty(array_filter( + $selectedFormats, + fn($id) => $id !== $siteWebId +)); +$showUploadBlock = $hasNonWebFormat || !$hasSiteWeb; + +$websiteUrl = htmlspecialchars($_POST['website_url'] ?? ''); +$websiteLabel = htmlspecialchars($_POST['website_label'] ?? ''); + +$hxPost = $adminMode ? '/admin/fichiers-fragment.php' : '/partage/fichiers-fragment'; +?> +
+ + + +
+ Format(s) +
+ Format(s) du TFE :*' : '' ?> +
+ hx-post="" + hx-target="#format-fichiers-block" + hx-trigger="change" + hx-include="this, [name='website_url'], [name='website_label'], [name='admin_mode']" + hx-swap="outerHTML"> + Format(s) du TFE +
    + +
  • + +
  • + +
+
+
+
+ + +
+ Fichiers + + + + + + bentopdf.com.'; + $hintRaw = true; // allow the tag through + $required = !$adminMode; + include APP_ROOT . '/templates/partials/form/file-field.php'; + ?> + + + + + + + + +
+ Site web +
+ +
+ + Le TFE sera affiché comme un site embarqué sur sa page publique. +
+
+
+ + +
+
+ + + + +
+ Vidéo +
+

+ 🚧 À venir : l'upload vidéo sera géré directement via l'API PeerTube. + La vidéo sera hébergée sur l'instance PeerTube de l'école et intégrée + comme lecteur embarqué sur la page du TFE. +

+

+ En attendant, déposez votre vidéo dans le champ TFE ci-dessus (ZIP si besoin). +

+
+
+ + + + +
+ Audio +
+

+ 🚧 À venir : l'upload audio sera géré via l'API PeerTube. + Le fichier audio sera hébergé sur l'instance PeerTube de l'école et + intégré comme lecteur embarqué sur la page du TFE. +

+

+ En attendant, déposez votre fichier audio dans le champ TFE ci-dessus (ZIP si besoin). +

+
+
+ + +
+ +
diff --git a/app/public/partage/index.php b/app/public/partage/index.php index 280eae6..1580bfa 100644 --- a/app/public/partage/index.php +++ b/app/public/partage/index.php @@ -28,13 +28,20 @@ if ($slug === 'language-autre-fragment' && $_SERVER['REQUEST_METHOD'] === 'POST' exit; } -// Special route: /partage/format-website-fragment (HTMX fragment, no auth needed) +// Special route: /partage/format-website-fragment (HTMX fragment, legacy — kept for safety) if ($slug === 'format-website-fragment' && $_SERVER['REQUEST_METHOD'] === 'POST') { App::boot(); require_once __DIR__ . '/format-website-fragment.php'; exit; } +// Special route: /partage/fichiers-fragment (HTMX fragment — format-aware fichiers block) +if ($slug === 'fichiers-fragment' && $_SERVER['REQUEST_METHOD'] === 'POST') { + App::boot(); + require_once __DIR__ . '/fichiers-fragment.php'; + exit; +} + // Special route: /partage/recapitulatif?id=N if ($slug === 'recapitulatif' || $slug === 'recapitulatif.php') { App::boot(); diff --git a/app/src/Controllers/ThesisCreateController.php b/app/src/Controllers/ThesisCreateController.php index da7aad1..49c2327 100644 --- a/app/src/Controllers/ThesisCreateController.php +++ b/app/src/Controllers/ThesisCreateController.php @@ -23,6 +23,9 @@ class ThesisCreateController /** Maximum allowed file size for thesis files (bytes). */ private const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500 MB + /** Maximum allowed file size for PDF files specifically (bytes). */ + private const MAX_PDF_SIZE = 100 * 1024 * 1024; // 100 MB + /** MIME types accepted for thesis files. */ private const ALLOWED_MIME_TYPES = [ // Images @@ -640,8 +643,10 @@ class ThesisCreateController continue; } - if ($uploads['size'][$i] > self::MAX_FILE_SIZE) { - error_log("ThesisCreateController: file too large {$uploads['name'][$i]}, skipping"); + $isPdf = ($mimeType === 'application/pdf' || $ext === 'pdf'); + $sizeLimit = $isPdf ? self::MAX_PDF_SIZE : self::MAX_FILE_SIZE; + if ($uploads['size'][$i] > $sizeLimit) { + error_log("ThesisCreateController: file too large {$uploads['name'][$i]} (" . round($uploads['size'][$i] / 1024 / 1024) . ' MB), skipping'); continue; } diff --git a/app/src/Controllers/ThesisEditController.php b/app/src/Controllers/ThesisEditController.php index 7bb4c9b..1bd99e4 100644 --- a/app/src/Controllers/ThesisEditController.php +++ b/app/src/Controllers/ThesisEditController.php @@ -368,7 +368,8 @@ class ThesisEditController 'vtt', 'zip', 'tar', 'gz', 'tgz', ]; - $maxBytes = 500 * 1024 * 1024; // 500 MB + $maxBytes = 500 * 1024 * 1024; // 500 MB + $maxPdfBytes = 100 * 1024 * 1024; // 100 MB for PDFs $year = (int)($post['année'] ?? date('Y')); $authorName = trim($post['auteurice'] ?? 'unknown'); @@ -429,8 +430,10 @@ class ThesisEditController continue; } - if ($uploads['size'][$i] > $maxBytes) { - error_log("ThesisEditController: file too large {$uploads['name'][$i]}, skipping"); + $isPdf = ($mimeType === 'application/pdf' || $ext === 'pdf'); + $sizeLimit = $isPdf ? $maxPdfBytes : $maxBytes; + if ($uploads['size'][$i] > $sizeLimit) { + error_log("ThesisEditController: file too large {$uploads['name'][$i]} (" . round($uploads['size'][$i] / 1024 / 1024) . ' MB), skipping'); continue; } diff --git a/app/src/Database.php b/app/src/Database.php index e6d64a9..6da1082 100644 --- a/app/src/Database.php +++ b/app/src/Database.php @@ -725,7 +725,7 @@ class Database */ public function getAllFormatTypes(): array { - $stmt = $this->pdo->query('SELECT * FROM format_types ORDER BY name'); + $stmt = $this->pdo->query('SELECT * FROM format_types ORDER BY sort_order, id'); return $stmt->fetchAll(); } diff --git a/app/templates/partials/form/checkbox-list.php b/app/templates/partials/form/checkbox-list.php index 554b69d..7829e58 100644 --- a/app/templates/partials/form/checkbox-list.php +++ b/app/templates/partials/form/checkbox-list.php @@ -16,13 +16,15 @@ * string $hxPost — optional hx-post URL for HTMX live update * string $hxTarget — optional hx-target CSS selector for HTMX swap * string $hxSwap — optional hx-swap value; default 'outerHTML' + * string $hxInclude — optional hx-include selector; default 'this, #website-url-fieldset' */ -$checked = $checked ?? []; -$required = $required ?? false; -$hxPost = $hxPost ?? ''; -$hxTarget = $hxTarget ?? ''; -$hxSwap = $hxSwap ?? 'outerHTML'; +$checked = $checked ?? []; +$required = $required ?? false; +$hxPost = $hxPost ?? ''; +$hxTarget = $hxTarget ?? ''; +$hxSwap = $hxSwap ?? 'outerHTML'; +$hxInclude = $hxInclude ?? 'this, #website-url-fieldset'; ?>
*' : '' ?> @@ -32,7 +34,7 @@ $hxSwap = $hxSwap ?? 'outerHTML'; hx-post="" hx-target="" hx-trigger="change" - hx-include="this, #website-url-fieldset" + hx-include="" hx-swap="" > @@ -52,4 +54,4 @@ $hxSwap = $hxSwap ?? 'outerHTML';
">
- + - + + + + +
Format(s)
- - +
Fichiers @@ -386,46 +407,30 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? []; accept=".pdf,.jpg,.jpeg,.png,.gif,.webp,.mp4,.webm,.mov,.ogv,.mp3,.ogg,.oga,.wav,.flac,.aac,.m4a,.zip,.tar,.gz,.vtt" class="tfe-file-picker"> - Types acceptés : PDF · JPG/PNG/GIF/WEBP · MP4/WebM/MOV (vidéo) · MP3/OGG/WAV/FLAC (audio) · ZIP/TAR (archives) · autres fichiers (téléchargement uniquement). Max 500 MB par fichier. + PDF (max 100 MB) · Images (JPG/PNG/GIF/WEBP) · ZIP/TAR (max 500 MB) · autres fichiers. + PDFs trop lourds ? bentopdf.com

Aucun nouveau fichier sélectionné.

- -
- - - - -