Commit Graph
80 Commits
Author SHA1 Message Date
Pontoporeia f4a3e26901 Add thesis status column for two-phase commit lifecycle tracking 2026-06-11 12:09:43 +02:00
Pontoporeia cbd369bc72 Split form.css into form-base.css and form-admin.css, drop dead upload-progress code
Also introduces $extraCssAdmin support in head.php for admin-only
stylesheets (form-admin.css, filepond CSS, system.css). Admin pages
now use $extraCssAdmin for admin-only assets and $extraCss for
shared stylesheets like form-base.css.
2026-06-11 11:28:58 +02:00
Pontoporeia 99125cc8e3 Add autosave draft system for partage form with HTMX-based session persistence
- New fragment endpoint POST/GET /partage/fragments/draft.php:
  saves all form fields to PHP session, excludes file/csrf/slug fields
  GET returns JSON for JS hydration on page load
  rotates both global CSRF and share CSRF tokens in sync

- form.php accepts optional $formExtraAttrs and $showAutosaveStatus:
  allows injecting HTMX attributes and 'Brouillon enregistré' indicator

- renderShareLinkForm adds hx-post with change/input debounce trigger,
  loads autosave-handler.js, hydrate fields from draft on page load

- Draft cleared on successful form submission in handleShareLinkSubmission

- autosave-handler.js now also updates share_link_token hidden input
  when rotating CSRF token (partage form uses both csrf_token and share_link_token)

- Added .autosave-status CSS to form.css (was admin.css-only)

- Updated fragment routing to accept GET requests (needed for draft hydration)
2026-06-11 11:04:49 +02:00
Pontoporeia 4b37a05be3 Guard no-JS file uploads: disabled filepond_mode by default, server-side fallback
The partage/admin form had a hardcoded filepond_mode=1 hidden input,
so without JavaScript the server always entered the FilePond async
path — which found no hex IDs and silently dropped all files.

Three-layer fix:
1. HTML: filepond_mode input starts disabled with value=0; JS enables
   it and sets value=1 on DOMContentLoaded (and after HTMX swaps).
   Disabled inputs aren't submitted → server gets no filepond_mode
   → naturally falls to legacy  path.
2. JS: enableFilepondMode() called on page load and hx:afterSwap so
   FilePond-enhanced forms always send filepond_mode=1.
3. Server (defense-in-depth): ThesisFileHandler::hasFilePondQueueData()
   scans POST['queue_file'] for 32-char hex IDs; ThesisCreateController
   and ThesisEditController use it alongside filepond_mode, so even if
   the flag somehow arrives without async upload IDs, the  path
   takes over.
2026-06-11 10:32:50 +02:00
Pontoporeia 4a2b000fca Add Charte static page (public + admin editing) 2026-06-10 00:18:49 +02:00
Pontoporeia 317547ac93 Fix #4 v2: decouple contact_interne from contact_visible in ThesisCreateController
validateAndSanitise() no longer cross-contaminates:
- contact_interne overwrote mail, which then copied to contact_visible
- Fixed: contactInterne from contact_interne (admin) or confirmation_email (student)
- Fixed: contactVisible from contact_visible (admin) or mail (student)
- Fixed: submit() uses contactInterne as author email, not mail
2026-06-10 00:18:49 +02:00
Pontoporeia c4a550f9d1 Rework contenus-edit: auto-save, OverType toolbar, dynamic sidebar links
- Auto-save: new autosave.js with 1.5s debounce, watches all forms with
  data-autosave, POSTs to form action with Accept: application/json, shows
  saving/saved/error status indicator
- All action handlers (page.php, apropos.php, form-help.php) now detect
  JSON Accept header and return {success, csrf_token} or {error} responses
- OverType toolbar enabled (toolbar:true) on all three markdown editors
  (page, about_page, form_help)
- Sidebar links: replaced fixed erg_site_url / source_code_url rows with
  dynamic sidebar_links array of {label, url} objects. Add/remove via JS.
  Fallback migration reads legacy keys if sidebar_links is empty.
- Updated AboutController and about.php template to render dynamic links
- Updated apropos.css: unified .apropos-toc-link replacing .apropos-toc-erg
  and .apropos-toc-source
- New CSS: autosave-status states, sidebar-link-row layout
- Removed all Enregistrer + Annuler buttons — auto-save and h1 back-arrow
  make them redundant
2026-06-10 00:18:40 +02:00
Pontoporeia 021c58925e fix: auto-regenerate thesis identifier on any year-prefix mismatch, support .php migrations in runner
ThesisEditController::save() previously only regenerated the identifier when
the year field changed during an edit. If a thesis had its year corrected in
a past edit (or via other means) and the identifier still carried the old
year prefix, subsequent edits that didn't touch the year field would leave
the mismatched identifier in place.

Now saves() also checks whether the existing identifier's 4-digit prefix
matches the thesis year, and regenerates if not — regardless of whether year
changed in the current edit.

The migration runner (run.php) only scanned for .sql files, so PHP migrations
(013, 016, 018, 038) were never auto-applied. Extended the runner to also
discover and execute .php migrations in a subprocess. If a PHP migration fails
with an idempotent error (no such column, already exists, duplicate column),
the runner treats it as already-applied and continues rather than aborting
— preventing a stale migration like 016 (banner_path already dropped by 028)
from blocking migrations that come after it alphabetically (e.g. 038).

Updated migrations 016 and 038 to accept an optional $argv[1] DB path.
Fixed 016 to gracefully handle the banner_path column already being gone
(exit 0 instead of fatal).
2026-06-10 00:17:30 +02:00
Pontoporeia 07370b7221 search: ajout filtres finalité et format, boutons plus compacts et Réinitialiser en neutre 2026-06-10 00:17:00 +02:00
Pontoporeia 3016c199bd Fix edit form: is_published reset, contact decoupling, note label, author name case
- Fix #1: Add is_published to getThesisRawFields() SELECT so the publish
  checkbox stays checked when editing an already-published TFE.
- Fix #2: Rename 'Note contextuelle' → 'Note contextuelle relative à
  soutenance' in all templates and StudentEmail.
- Fix #3: Update findOrCreateAuthor to also UPDATE the author name when
  a record is found by name (fixes inability to capitalise names).
- Fix #4/#5: Decouple contact_interne (private author email) from
  contact_visible (public contact on TFE page). Add migration 037 to
  add contact_visible TEXT column to theses table and rebuild
  v_theses_full view. Update all controllers, templates, and DB methods
  to treat them independently.
- Fix #6: Investigated libre→interne restriction — no code barrier
  found; likely resolved by is_published fix.
2026-06-10 00:17:00 +02:00
Pontoporeia 3d524226a1 formulaire: correctifs identifiant/année, contact, fichiers optionnels
- Identifiant: mise à jour automatique quand l'année change en back-office (updateThesis + ThesisEditController)
- Contact: hint enrichi (1 seul contact, formatage Instagram/Mastodon)
- Fichiers: TFE rendu optionnel pour Site web/Performance/Installation (note d'intention reste obligatoire)
2026-06-10 00:17:00 +02:00
Pontoporeia a2092b58a7 fix: supprimer les vidéos PeerTube lors de la suppression d'un TFE
- Ajout de PeerTubeService::deleteVideo() qui appelle DELETE /api/v1/videos/{uuid}
- deleteThesisFileToTrash() appelle maintenant deleteVideo() pour les fichiers peertube_ids:
- hardDeleteThesis() supprime aussi les vidéos PeerTube associées
2026-06-10 00:16:22 +02:00
Pontoporeia 312d9eab0e À propos: contacts flexibles, liens sidebar éditables, grille contacts admin, et bouton supprimer
- Contacts: on peut laisser vide le nom OU le rôle (plus besoin des deux)
- Sidebar: les liens « site de l'erg » et « code source » sont éditables depuis /admin/contenus-edit.php?slug=about
- Admin: les champs Nom/Email/Lien des contacts s'affichent en grille 3 colonnes
- Admin: icône corbeille (admin-icon-btn--delete) pour supprimer un contact, avec réindexation automatique
- Database::getAproposContent() gère maintenant les valeurs string (URLs) en plus des arrays
- Database::saveAproposContent() accepte array|string
2026-06-10 00:16:22 +02:00
Pontoporeia 71949425c7 TFE page: remove underlines from all links, lowercase keywords/languages/formats, inclusive writing, prevent keyword mid-word breaks, editable restriction messages in admin 2026-06-10 00:15:41 +02:00
Pontoporeia df70fba5d4 feat: convert all file inputs to FilePond for standardized uploading
- Add csv_import queue type (storeAsFile, no async upload) for CSV import dialog
- Convert file-field.php partial to FilePond with field-name→queue-type mapping
- Conditionally skip server config for storeAsFile queues in buildFilePondOptions
- Skip FilePond init for inputs inside closed <dialog> elements
- Trigger FilePond init when import dialog opens
- Load FilePond CSS/JS assets on admin index page
2026-06-09 13:12:22 +02:00
Pontoporeia 6246174fc5 Export: add LINK.txt with PeerTube watch URLs to file ZIP export 2026-05-31 17:46:02 +02:00
Pontoporeia ae66c2baad Integrate Monolog: replace four logging systems with single PSR-3 factory
- Add monolog/monolog dependency (^3.10)  
- Create app/Logger.php central factory with channels: app, admin, error, audit
- Each channel gets RotatingFileHandler (30-day retention) with pass-through LineFormatter
  preserving existing JSON format contracts
- Rewrite AppLogger as thin facade delegating to Logger::get('app')
- Rewrite ErrorHandler::log() to delegate to Logger::get('error')
- Rewrite AdminLogger file output to delegate to Logger::get('admin'), keep DB writes
- Add Monolog file shadow to Audit via Logger::get('audit') (Option A per monolog-plan)
- Log level controlled by LOG_LEVEL env var (defaults: DEBUG in cli-server, WARNING otherwise)
- Graceful NullHandler fallback when log directory is not writable
- Update SystemController LOG_FILES: remove php_error, add app/admin/error/audit
- JSON app logs parsed to readable one-liners in the log viewer
- Remove nginx config tab (parametres + fragment + template + css)
- Friendly empty-state message when app log files don't exist yet (notYet)
- PHP tail fallback when exec() unavailable
- All 228 PHPUnit tests pass, no call sites changed
2026-05-20 12:28:31 +02:00
Pontoporeia 5e75cacad7 Phase 1: Replace Parsedown with league/commonmark (4 call sites) 2026-05-20 01:02:09 +02:00
Pontoporeia 728f05502c Combine phpstan, cs-check, cs-fix into lint-php recipe; fix lint issues + test failures + duplicate detection bug 2026-05-20 00:31:19 +02:00
Pontoporeia defc919cd0 cleanup modal: list stale files to remove; storage restructure: documents/ → {objet}/ 2026-05-19 23:58:51 +02:00
Pontoporeia 678f9fc804 Index page: remove Mots-clés button, move export to bulk selection, fix ZipArchive error, move DB export to paramètres, sticky thead
- Remove 'Mots-clés' button from toolbar (redundant with admin sidebar tags)
- Replace export dialog with 'Exporter CSV' + 'Exporter fichiers' buttons in bulk selection bar
- Export dispatcher now accepts ?ids=1,2,3 for per-selection export
- All ExportController/Database methods accept optional thesisIds array
- Graceful error message when ZipArchive extension is missing on server
- Move DB export (SQLite download) to paramètres → Maintenance section
- Sticky table column headers (position: sticky, top: 0, z-index: 5) for index page table
2026-05-19 23:58:51 +02:00
Pontoporeia 79eddf5d5a feat: fix file deletion on save + trash policy + documents/ prefix + relink browser
1. note_intention: Delete old file only when a genuinely new upload arrives
   (32-char hex file_id), not when the FilePond pool preserves an existing
   file by sending its DB integer ID.  Previously the DB integer ID
   triggered $hasNewNote=true, which deleted the existing note_intention
   from disk+DB, then handleFilePondSingleFile couldn't re-process it
   because the regex requires a hex pattern.  Same fix applied to cover.

2. All file deletions now use deleteThesisFileToTrash() which renames
   files to tmp/_trash/ instead of unlinking.  The trash preserves
   original filenames prefixed with DB id for traceability.  Skips
   website URLs and PeerTube refs (no disk file).

3. Storage prefix changed from theses/ to documents/ to reflect that
   the folder holds all document types (determined by file_type in DB).
   MediaController visibility gate supports both prefixes for backward
   compat with existing files.

4. File browser + relink feature for orphaned files:
   - /admin/fragments/file-browser.php — HTMX tree browser for
     storage/documents/ and storage/theses/
   - /admin/actions/filepond/relink.php — POST endpoint that inserts
     a thesis_files row pointing to existing on-disk file
   - Per-pool "📂 Relier" buttons (edit mode only)
   - JS: XamxamOpenFileBrowser / XamxamRelinkFile with FilePond integration
   - CSS: .relink-modal dialog + .file-browser tree styles
2026-05-19 00:08:06 +02:00
Pontoporeia 6f7a02244f 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.
2026-05-19 00:08:06 +02:00
Pontoporeia 6e7c0c00e3 refactor: merge video/audio FilePond pools into TFE input
- Remove separate video/audio/peertube_video/peertube_audio pools from UI
- TFE pool now accepts all file types including video/audio
- When PeerTube is enabled, video/audio dropped into TFE pool auto-upload
  to PeerTube (process.php detects MIME and uploads immediately)
- PeerTube return IDs now encode type: peertube:video:UUID or peertube:audio:UUID
- load.php returns placeholder SVG for PeerTube files so they appear in FilePond
- Edit mode: all existing files (including PeerTube) shown in TFE FilePond pool
- Remove legacy  video/audio/peertube_* handling from both controllers
- Remove unused vide/audio/peertube_* entries from JS QUEUE_CONFIG
2026-05-19 00:08:06 +02:00
Pontoporeia 2e9ebfc684 filepond: implement async server-ID upload architecture with nested queue support + PeerTube integration
Replace `storeAsFile:true` with a full async FilePond round-trip pipeline using opaque server-side file IDs.

* Added 4 new PHP endpoints under `/admin/actions/filepond/`:

  * `process.php` — upload/process single file and return opaque `file_id`
  * `revert.php` — delete pending tmp uploads before form submit
  * `load.php` — stream existing files by DB ID for FilePond preload
  * `remove.php` — soft-delete `thesis_files` rows
* `process.php` improvements:

  * accept arbitrary FilePond field names instead of hardcoded `file`
  * support PHP-nested multi-file queue inputs (`queue_file[tfe][]`)
  * explicit unwrapping of nested `$_FILES` structures
  * add `audio/mp3` to audio + `peertube_audio` MIME whitelists
  * immediate upload of `peertube_*` files to PeerTube, returning `peertube:{uuid}` IDs
  * extensive `error_log()` instrumentation for request, CSRF, MIME, upload, and save stages
* `revert.php` now accepts `peertube:` IDs without local cleanup
* `ThesisFileHandler`:

  * add `handleFilePondQueueFiles()` + `handleFilePondSingleFile()`
  * process async uploads from `storage/tmp/filepond/` via opaque `file_id`
  * inline handling of `peertube:{uuid}` IDs with direct `thesis_files` insertion
  * remove obsolete deferred PeerTube queue-processing flow
* `ThesisCreateController` + `ThesisEditController`:

  * gate async path behind `filepond_mode=1`
  * preserve legacy multipart flow as fallback
* `file-upload-filepond.js`:

  * remove `storeAsFile:true`
  * add `buildServerConfig()` for async endpoint wiring
  * fix `syncOrderInput()` to use `serverId`
  * add `onprocessfile` hook
  * add `fileValidateSizeFilterItem` for per-extension size caps
  * preload existing uploads via `data-existing-files` + `server.load`
  * replace static `INPUT_ID_TO_TYPE` map with `data-queue-type`
  * add extensive `console.log()` debugging across upload pipeline stages
* `upload-progress.js`:

  * block form submission while uploads are pending
  * update `collectFileNames()` to read processed FilePond items
* Templates/layout:

  * add `data-queue-type`
  * add `data-existing-files`
  * add global CSRF meta tag outside admin-only context
  * add `filepond_mode` hidden input
  * add CSRF token/meta support for partage pages
  * move website URL field below file upload block
* `.gitignore`: exclude `storage/tmp/` from version control
2026-05-19 00:08:06 +02:00
Pontoporeia b56d073210 refactor: extract inline JS into app/ modules, remove dead overtype-webcomponent
- Remove overtype-webcomponent.min.js (zero references)
- Extract copyLogContent + fallbackCopy + HTMX tab-updater → app/admin-logs.js
  (removes duplicate from both system.php and parametres.php)
- Extract copyUrl → app/clipboard.js (shared by acces.php)
- Extract tag/language pill-search logic → app/pill-search.js
  Generalized with data-pill-search attributes, auto-inits via
  DOMContentLoaded + htmx:afterSwap
- Extract access-request form handler → app/access-request.js
  (was inline in templates/public/tfe.php)

Files created: admin-logs.js, clipboard.js, pill-search.js, access-request.js
Files modified: 9 templates/controllers to drop inline scripts and
  reference external JS files
2026-05-19 00:08:06 +02:00
Pontoporeia df12af8423 fix: validation error messages hidden by generic fallback in ErrorHandler::userMessage
ErrorHandler::userMessage only handled RuntimeException, but all validation
throws in ThesisCreateController and ThesisEditController use plain Exception.
This caused user-friendly messages like 'Le champ Nom/Prénom/Pseudo est requis'
to fall through to the 'Une erreur inattendue est survenue…' generic message.

Fix: add Exception check (after PDOException, since PDOException extends it)
so all validation exceptions pass their message through.
2026-05-19 00:08:06 +02:00
Pontoporeia 206a6427e7 style: unify recap+edit file figure styling — two-column grid dl, vertical spacing, admin-back-btn sizing, standardise file display and delete-to-trash SVG icons 2026-05-19 00:08:06 +02:00
Pontoporeia 927ee2fe2a feat: upload progress bar — fieldset layout, accent colors, file name display, completion animation, 800ms redirect delay; decorelate formats from fichiers; server-side poll via token; bump PeerTube embed audio player 2026-05-19 00:08:06 +02:00
Pontoporeia cdec3e96a6 fix PeerTube upload: final working solution — simple multipart POST with CURLFile; iterated through Google-resumable PATCH protocol debugging (HTTP version negotiation, chunk body encoding, off-by-one fixes) before settling on simpler POST approach 2026-05-19 00:08:06 +02:00
Pontoporeia 83a5a508ea feat: PeerTube integration — alternate audio/video labels, FilePond pools, shared SMTP credentials, channel by name, test button, resumable upload, embed improvements, fix alt labels/curl_close/deprecation 2026-05-19 00:08:06 +02:00
Pontoporeia 48da914bc8 fix: obfuscate email in contact links, raise rate limits, make Libre toggleable
- about.php: use EmailObfuscator::email() for contact email link text instead of htmlspecialchars
- SearchController: raise rate limit from 30 to 300 req/min
- request-access.php: raise rate limit from 3 to 30 req/10min
- partage/index.php: raise rate limit from 5 to 50 req/10min
- contenus.php: make Libre option toggleable (remove disabled class), move to top of Degré d'ouverture, remove temporary note about next academic year
2026-05-19 00:08:06 +02:00
Pontoporeia 406752bc6f Improve recap page + fix CSV import for jury roles
recapitulatif.php (partage):
- Center .thanks-success and add bottom margin/padding
- Display ALL fields: identifier, synopsis, languages, formats,
  jury (all roles), baiu link, license, access type
- Add validation notice asking user to verify info, with
  xamxam@erg.be contact link (email obfuscated)

StudentEmail:
- Add 'Note contextuelle' and license_custom to email recap
- Rename 'Promoteur·ice(s)' to 'Promoteur·ice(s) interne'
- Change email message to ask student to verify info + contact
  for errors

CSV export/import:
- Add 3 new CSV columns: Lecteur·ice(s) interne,
  Lecteur·ice(s) externe, Promoteur·ice(s) ULB
- Export splits supervisors by role/is_external/is_ulb into
  separate columns
- Import inserts supervisors with correct role, is_external,
  and is_ulb flags (was: all treated as generic supervisors)
- Add header matching for short distinguishers (ulb, externe)
  via str_contains fallback
2026-05-19 00:08:06 +02:00
Pontoporeia 8db7b6e9eb feat: FilePond production hardening — extension-based validation, server-side size limits (2GB), annexe validation, drop accept attributes, FilePond file styling 2026-05-19 00:08:05 +02:00
Pontoporeia 13d26ded66 Replace HTMX+PHP file upload queues with client-side JS
Drops the session-backed HTMX incremental upload system in favour of a
single JS module that manages `File` objects client-side and injects
them into `FormData` on submit.

Key changes:

* `file-upload-queue.js`: client-side queues with validation, reorder
  (SortableJS), removal, dirty-state tracking, and fetch-based submit
  with manual redirect handling
* `fichiers-fragment.php`: empty queue containers for JS-managed queues;
  HTMX format switching still works with queue rehydration after swap;
  annexe uploads now support multiple files
* Form UI cleanup: moved existing files and cover preview into the
  `Fichiers` fieldset (edit mode); removed redundant queue labels while
  keeping labels for single-file inputs (`couverture`,
  `note d'intention`); added delete buttons for existing files
* `ThesisFileHandler.php`: added
  `handleTfeQueueFiles()`/`handleAnnexeQueueFiles()` reading from
  `$_FILES['queue_file']`; introduced `extractFilesSubArray()` for
  nested upload arrays; removed session-based queue handling
* `ThesisCreateController.php` &
  `ThesisEditController.php`: switched to extracted
  `['queue_file']` uploads
* `beforeunload-guard.js`: now also watches
  `window.__xamxamDirty`
* Deleted obsolete PHP upload/remove/reorder queue endpoints for
  `partage` and `admin`
* Cleaned up route dispatch in `partage/index.php`
* Misc form and styling updates in templates/CSS
* Added `docs/cms-migration-plan.html`
2026-05-19 00:08:05 +02:00
Pontoporeia ca7707cd47 refactor: session-based incremental TFE upload via HTMX, drop SortableJS
Replace the client-side FileArray + Sortable drag-to-reorder with a
server-side session-based upload flow:

- New endpoints: /partage/upload-tfe-file, /partage/remove-tfe-file
  (and /admin/ variants) — single-file incremental upload via HTMX
  multipart/form-data with progress bar support
- Session storage: uploaded files go to STORAGE_ROOT/uploads/{session_id}/
  with metadata in $_SESSION['tfe_uploads']
- file-upload-queue.js reduced to single-file previews only (couverture,
  note_intention, annexes thumbnails)
- ThesisFileHandler gains handleTfeFilesFromSession + writeTfeFileFromSrc
  + cleanupSessionUploads for final commit from session temp
- Sortable.min.js removed from all script tags; drag handles and ghost
  CSS removed
- No file_orders[]/file_labels[] hidden field injection needed
- Upload queue survives page refresh (server-owned list)

This eliminates the SortableJS dependency entirely while keeping the
same UX: pick files, see them in a queue, remove individual files.
2026-05-19 00:08:05 +02:00
Pontoporeia e06a317499 fix: req annexes, add HTMX inline file validation (MIME/size)
- Annexes file input now required when 'has_annexes' checkbox is checked
- PHP-side validation: if has_annexes but no files, throw error
- HTMX inline file validation: POSTs to validate-file-fragment on file change
  - Validates MIME type against per-field whitelists (couverture, note_intention,
    tfe, annexes)
  - Validates file size with PDF-specific 100MB limit
  - Supports both single-file and multi-file inputs
  - Returns green ✓ or red ✕ inline validation messages
- Shared validation logic in src/Controllers/validate-file-fragment-shared.php
- Admin wrapper: admin/validate-file-fragment.php (with AdminAuth guard)
- Partage route: /partage/validate-file-fragment (dispatched via index.php)
- CSS: .file-validation-msg, .fv-ok (green), .fv-error (red)
- file-field.php: accepts $fieldName for per-input validation type,
  auto-detects admin/partage validate URL
2026-05-19 00:08:05 +02:00
Pontoporeia a1a5d4609f fix: TFE and annexes files not saved, plus keyword validation and file preview CSS
- ThesisCreateController::submit() was missing call to handleAnnexeFiles
- ThesisEditController::save() was missing annexe upload handling
- handleAnnexeFiles now applies ALLOWED_MIME_TYPES/ALLOWED_EXTENSIONS validation
  (same restrictions as TFE files, formerly only size was checked)
- Use correct $_FILES key 'annexes' (matching the form input name)
- Relax keyword minimum: admin create/edit require 1+, student (partage) requires 3
- Add CSS styles for file preview items (.fp-item, .fp-thumb, .fp-icon,
  .fp-meta, .fp-name, .fp-size) so multi-file previews (annexes, etc.) wrap correctly
- Fix TFE file input accept attribute in fichiers-fragment.php to include
  video/audio/archive extensions
2026-05-19 00:08:05 +02:00
Pontoporeia 38dc8de9d8 feat: obfuscate all email addresses and mailto links as HTML entities
Added EmailObfuscator class (src/EmailObfuscator.php) that converts
email addresses to HTML decimal entities (e.g. &#102;&#111;&#111;@...)
so browsers render them correctly but bots and scrapers see gibberish.

Methods:
- email($addr): obfuscate for display in HTML content
- mailto($addr): return obfuscated mailto: href
- obfuscateHtml($html): post-process rendered HTML to obfuscate all
  mailto: links (used after Parsedown/Markdown rendering)

Applied to:
- partage/index.php: mailto link at top + error scenarios via _flash_contact
  flag rendered in form.php (outside htmlspecialchars to avoid double-escape)
- admin/acces.php: request email mailto links
- admin/file-access.php: request email mailto links
- public/about.php: contact email mailto links
- public/tfe.php: author contact mailto links
- AboutController: Parsedown output post-processing
- LicenceController: Parsedown output post-processing
- Dispatcher::render(): require_once EmailObfuscator for all public views

Also fixed _flash_contact session flag in form.php partial to show
contact email line on share link validation errors (separate from
flash_error/warning to bypass htmlspecialchars double-escaping).
2026-05-19 00:08:05 +02:00
Pontoporeia ab6e266807 fix: add help email, preserve file names on validation error, license fix
The share link (partage) form does not expose a license field and does
not send access_type_id (defaults to 2/Interne). Server-side validation
was unconditionally requiring a license for non-admin submissions,
causing all share link submissions to fail.

Now the license check is gated on adminMode=false AND accessTypeId=1
(Libre), matching the client-side HTMX fragment behaviour in
licence-fragment.php. Also fixed a use-before-definition where
accessTypeId was referenced before being assigned.

Student form improvements:
- Add xamxam@erg.be mailto link at top of form
- On validation error, append "Si le problème persiste, envoyez un
  e-mail à xamxam@erg.be" to the flash message
- Preserve uploaded file names across validation redirects: store in
  session (share_primed_files_<slug>), display as warning on form
  re-render so the student knows which files to re-select

- License: only required for non-admin when access_type_id=1 (Libre),
  not for Interne (2) or Interdit (3). Fixes share link submissions
  failing with "Veuillez sélectionner une licence". Also fixed
  use-before-definition of accessTypeId.
2026-05-19 00:08:05 +02:00
Pontoporeia 048a14bc2e Add language-search component for Autre Langue input + active search in lists
Mirrors the mots-clé tag-search system: dropdown suggestions from
existing languages via HTMX, pill display with bin-icon remove buttons,
'Créer' option for new languages. Replaces the plain text input.

- New partial: templates/partials/form/language-search.php
- New fragment: public/partage/language-search-fragment.php
- Admin wrapper: public/admin/language-search-fragment.php
- Updated language-autre-fragment to return just the required asterisk indicator
- Updated both controllers to handle language_autre as array (pill-based)
  with backward-compatible string path
- Updated edit form to compute selectedOtherLanguages from DB
- Registered new route in partage/index.php
- Fix CSV importer: split comma-separated language column into individual entries
- Add htmx active search to admin index, title line-clamp, predefined languages only in checkboxes
- Admin index: filter form now uses htmx triggers (input delay:300ms on search,
  change on selects) to actively search without page reload
- Sort links include hx-push-url for back-button support
- Added loading indicator bar (.admin-search-indicator)
- Title column: line-clamp at 2 lines with overflow hidden, native title attr
  tooltip for full text
- Language checkboxes now show only 3 predefined languages (Français, Anglais,
  Néerlandais); all others go via the Autre langue search component
- Added Database::getPredefinedLanguages() and excluded predefined from
  language-search-fragment suggestions
- Included hidden sort/dir inputs in table-wrap so sort state preserved across
  filter changes
- Fix language-search: block 'Créer' for predefined languages in dropdown
  The 'Créer' option in the language-search dropdown now also checks against the
  predefined set (français, anglais, néerlandais) to avoid offering creation of
  languages that already exist as checkboxes.
2026-05-19 00:08:05 +02:00
Pontoporeia fa30aab368 Rename author_email→contact_interne, author_show_contact→contact_public across view/controllers/templates
- v_theses_full: author_email→contact_interne, author_show_contact→contact_public
- Updated schema.sql and live DB view
- Renamed all PHP variables: currentAuthorEmail→contactInterne, currentAuthorShowContact→contactPublic
- Restored contact_interne backoffice field with proper wiring (takes precedence over mail field)
- Updated admin/add.php, admin/edit.php, partage/index.php, public/tfe.php templates
2026-05-19 00:08:05 +02:00
Pontoporeia 8a4b2541fb Fix: email clearing in findOrCreateAuthor, htmlspecialchars(null) crash in old(), dead contact_interne field, access_type_id radio clearing
- findOrCreateAuthor: always update email column (pass null when empty/falsy) so clearing an email actually persists
- admin/add.php & admin/edit.php old(): add null guard before htmlspecialchars, cast to string
- jury-fieldset.php: guard against old() returning array for scalar-checked jury_lecteur keys
- formulaire.php: only suppress display_errors in production (not cli-server dev mode)
- Removed dead contact_interne field from backoffice form (no DB column, never saved)
- Removed dead contactInterne validation from ThesisCreateController
- Added "— Non défini" radio option for access_type_id in admin mode for clearing
- Fixed strict int-vs-string comparison breaking radio button checked detection
2026-05-19 00:08:05 +02:00
Pontoporeia 6cc0e407f3 Error tests, FK violations fix
- ErrorHandler tests: 77 assertions covering FK extraction, normalization, dedup, edge cases. Fix FK table map for child tables.
- Fix FK violation: (int)null → 0 in createThesis for orientation/ap/finality/license FK columns. Add FK value logging to updateThesis.
- Add CURRENT_ISSUES.md with summary of FK violation, dev debugging, and tag dedup status for next conversation
2026-05-19 00:08:05 +02:00
Pontoporeia a80b2c08bf Admin mobile block: fix inline style beating media query 2026-05-19 00:08:05 +02:00
Pontoporeia c4a23d5c2d Remove duration_pages/duration_minutes/file_size_info; rename cc4r → cc2r in DB and code 2026-05-19 00:08:05 +02:00
Pontoporeia cc0ae32df0 fix: resolve partage form submission issues
- Replace mb_strlen/mb_substr/mb_strtolower with strlen/substr/strtolower
  (mbstring extension missing on server, causing fatal error)
- Scope annexes checkbox HTMX swap to #annexes-input-block with hx-select
  (prevents duplicating entire page inside Fichiers fieldset)
- Split format+fichiers response: #format-fichiers-block (stable) and
  #format-extras-block (swappable, inside Fichiers fieldset). Format
  checkboxes use hx-select to extract only the extras, preserving file queue.
- Keep format extras inline in Fichiers fieldset (no sub-fieldsets). Remove
  website legend input (URL only).
- When PeerTube upload disabled, show direct file upload inputs for
  video/audio (name=files[]).
- Add "Glissez-déposez" sort hint below TFE file queue.
- Fix .fq-name overflow with width:0;min-width:100% chain.
- Remove legend placeholder from .fq-item.
- Merge "Récits et expérimentation" AP into "Narration Spéculative".
  Rename PACS to "Pratique de lart - outils critiques, arts et contexte
  simultanés".
- Remove président·e field from jury fieldset, form templates, and
  controller validation. Keep DB column and display logic for existing data.
2026-05-19 00:08:05 +02:00
Pontoporeia 8f4f9d00b4 Refactor: Form improvements and cleanup: note contextuel, annexes, fichiers
1. fix: form improvements — multiple promoteurices, asterisks, contact dedup, bentopdf

- Multiple promoteurice (interne + ULB): both fieldsets now support dynamic
  add/remove rows (same pattern as lecteurs). field names changed to arrays
  (jury_promoteur[], jury_promoteur_ulb_name[]). Controllers accept both
  scalar and array forms for backwards compat.
- ULB promoteurice: when finality=Approfondi, asterisk appears on legend
  and first ULB input is marked required (JS toggle). Non-Approfondi hides
  the fieldset and clears values.
- Contact visibility duplication: removed redundant contact_public checkbox
  from admin add/edit forms (showContact=false). The 'mail' field in
  fieldset-tfe-info already serves this purpose.
- Asterisk fixes: website URL field now has asterisk+required when Site web
  format selected. Video/audio already had correct required handling.
- bentopdf link: clearer full URL 'https://bentopdf.com/' in both
  fichiers-fragment.php and form.php (edit mode)

2. refactor: merge Note contextuelle into Backoffice, add Lien BAIU, reorder fields

Backoffice fieldset now contains in order:
1. Note contextuelle (was standalone fieldset)
2. Points du jury
3. Remarques
4. Lien BAIU (moved from Métadonnées complémentaires)
5. Exemplaire physique BAIU
6. Exemplaire physique ERG
7. Contact interne


Métadonnées complémentaires now only has: pages, minutes, annexes checkbox.
Removed dead showContextNote variable from form.php, add.php, edit.php.
Controller baiu_link still mapped to input name "lien" (no migration needed).

3. refactor: move annexes checkbox from Métadonnées into Fichiers fieldset

- Removed 'Ce TFE comporte des annexes' checkbox from
  fieldset-metadata.php.
- Added annexes checkbox + conditional file input to
  fichiers-fragment.php. When checked, an HTMX swap reveals
  the 'annexes' file input (multiple, PDF or ZIP/TAR, max 500 MB).
- form.php seeds ['has_annexes'] for initial fragment render.
- Métadonnées complémentaires now only contains pages + minutes.
2026-05-13 17:59:13 +02:00
Pontoporeia 03c5fd217e feat: dual upload system — direct file storage + PeerTube API integration
Adds a parallel PeerTube upload system behind a feature flag (disabled by default
until upload quota is granted). When disabled, the existing direct file upload
path works unchanged.

Files:
- src/PeerTubeService.php — credential storage (encrypted), OAuth2 token
  retrieval, multipart upload to /api/v1/videos/upload
- migrations/021_peertube_settings.sql — peertube_settings singleton table
  + peertube_upload_enabled site_setting (default 0)
- admin/actions/settings.php — peertube section handler
- admin/parametres.php / templates/admin/parametres.php — PeerTube UI section
- partage/fichiers-fragment.php — shows file inputs when enabled, TODO notice otherwise
- ThesisCreateController / ThesisEditController — handlePeerTubeUpload()
- tfe.php — PeerTube iframe embed detection
- AdminLogger — logPeerTubeUpdate()
2026-05-13 17:59:13 +02:00
Pontoporeia e6829994b6 Refactor + feat: unify format/fichiers HTMX fragment, reorder format types, add file constraints, fix admin auth
* **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.
2026-05-13 17:59:13 +02:00