From 8db7b6e9eb92d1548fe00ade0545f626fe113c4b Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Sun, 10 May 2026 20:41:37 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20FilePond=20production=20hardening=20?= =?UTF-8?q?=E2=80=94=20extension-based=20validation,=20server-side=20size?= =?UTF-8?q?=20limits=20(2GB),=20annexe=20validation,=20drop=20accept=20att?= =?UTF-8?q?ributes,=20FilePond=20file=20styling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 33 + app/migrations/021_peertube_settings.sql | 21 - .../025_fix_oui_non_artefacts.sql | 0 .../pending/027_drop_banner_path.sql | 10 - .../pending/028_drop_banner_path.sql | 81 + app/public/admin/add.php | 4 +- app/public/admin/edit.php | 4 +- .../css/filepond-plugin-image-preview.min.css | 8 + app/public/assets/css/form.css | 60 +- app/public/assets/js/file-upload-filepond.js | 271 +- .../filepond-plugin-file-validate-size.min.js | 9 + .../filepond-plugin-file-validate-type.min.js | 9 + ...epond-plugin-image-exif-orientation.min.js | 9 + .../js/filepond-plugin-image-preview.min.js | 9 + app/public/partage/fichiers-fragment.php | 62 +- app/public/partage/index.php | 48 +- .../Controllers/ThesisCreateController.php | 9 +- app/src/Controllers/ThesisFileHandler.php | 70 +- app/templates/admin/acces.php | 91 + app/templates/public/tfe.php | 34 - justfile | 2 +- ..._019e1332-ce31-70fa-87a1-aa3495b526a9.html | 4113 +++++++++++++++++ scripts/ensure-db.php | 29 + 23 files changed, 4770 insertions(+), 216 deletions(-) delete mode 100644 app/migrations/021_peertube_settings.sql rename app/migrations/{pending => applied}/025_fix_oui_non_artefacts.sql (100%) delete mode 100644 app/migrations/pending/027_drop_banner_path.sql create mode 100644 app/migrations/pending/028_drop_banner_path.sql create mode 100644 app/public/assets/css/filepond-plugin-image-preview.min.css create mode 100644 app/public/assets/js/filepond-plugin-file-validate-size.min.js create mode 100644 app/public/assets/js/filepond-plugin-file-validate-type.min.js create mode 100644 app/public/assets/js/filepond-plugin-image-exif-orientation.min.js create mode 100644 app/public/assets/js/filepond-plugin-image-preview.min.js create mode 100644 pi-session-2026-05-10T18-42-37-234Z_019e1332-ce31-70fa-87a1-aa3495b526a9.html create mode 100644 scripts/ensure-db.php diff --git a/TODO.md b/TODO.md index f6c606d..ad96266 100644 --- a/TODO.md +++ b/TODO.md @@ -26,3 +26,36 @@ - [x] Add FilePond pools for couverture + note_intention (extracted from file-field.php inner
) - [x] Fix video/audio pools: allowMultiple: true, not single-file - [x] Add QUEUE_CONFIG for cover (20MB single) and note_intention (100MB PDF single) +- [x] Disable dedicated video/audio upload slots — video/audio files now go through TFE FilePond input + - [x] Comment out slot-video and slot-audio in fichiers-fragment.php (keep code, render always-hidden) + - [x] Remove HTMX swap triggers from Vidéo/Audio checkboxes + - [x] Clean up slot-video/slot-audio from file-upload-filepond.js beforeSwap handler + - [x] Fix missing endif after removing elseif chain (parse error) +- [x] Fix annexe validation error + FilePond type validation + styling + - [x] Make annexe pool always visible (remove checkbox+HTMX swap, always on, optional) + - [x] Remove mandatory annexe file validation from ThesisCreateController + - [x] Add extension-based file type validation in beforeAddFile (needed because storeAsFile: true skips FilePond MIME detection) + - [x] Fix FilePond dark theme: override item/file colors, buttons, progress indicator to match site theme + - [x] Add drag-over highlight style for drop area +- [x] FilePond production hardening + - [x] Fix beforeAddFile return format: return true/false, not {status, main, sub} (FilePond API contract) + - [x] Replace manual validation with FilePond plugins: FileValidateType, FileValidateSize + - [x] Download FilePond plugin assets: file-validate-type, file-validate-size, image-preview, image-exif-orientation + - [x] Add order serialization: hidden inputs (queue_order[type]) synced from pond.getFiles() + - [x] Fix HTMX cleanup: generic destroyFilePondsIn(target) for all beforeSwap events, not just known IDs + - [x] Fix duplicate initialization: use FilePond.find(input) instead of dataset checks + - [x] Centralize validation config in QUEUE_CONFIG (acceptedFileTypes, maxFileSize per type) + - [x] Add per-extension size limits for TFE queue (PDF=100MB, video/audio=2GB, default 500MB) + - [x] Add comprehensive French labels (labelFileProcessing, labelTapToCancel, etc.) + - [x] Register plugins on all entrypoints (admin/add, admin/edit, partage/index) + - [x] Remove duplicate init scripts from fichiers-fragment.php + - [x] Server-side MIME verification already in place (finfo-based validation in ThesisFileHandler) +- [x] Fix undefined $isExternalUrl and disable PeerTube in tfe.php +- [x] Fix migration 028: drop banner_path from theses (handle dependent view) + - [x] Create ensure-db.php to init fresh DB from schema.sql when missing + - [x] Remove broken 027_drop_banner_path.sql, move 025 to applied + - [x] Move stray 021_peertube_settings.sql to applied/ + - [x] Update deploy justfile to run ensure-db.php before migrations +- [x] Fix promoteurice array repopulation in partage form + - [x] Fix old() to return raw arrays (not json_encode) for repopulation + - [x] Handle jury_promoteur[] and jury_promoteur_ulb_name[] as arrays in partage/index.php diff --git a/app/migrations/021_peertube_settings.sql b/app/migrations/021_peertube_settings.sql deleted file mode 100644 index 4dd9664..0000000 --- a/app/migrations/021_peertube_settings.sql +++ /dev/null @@ -1,21 +0,0 @@ --- Migration 021: PeerTube integration --- Creates the peertube_settings singleton table and the peertube_upload_enabled feature flag. --- The upload flag defaults to 0 (disabled) so existing deployments are unaffected. - -CREATE TABLE IF NOT EXISTS peertube_settings ( - id INTEGER PRIMARY KEY CHECK (id = 1), -- singleton row - instance_url TEXT NOT NULL DEFAULT '', - username TEXT NOT NULL DEFAULT '', - password TEXT NOT NULL DEFAULT '', -- AES-256-GCM encrypted via Crypto.php - channel_id INTEGER NOT NULL DEFAULT 1, - privacy INTEGER NOT NULL DEFAULT 1, -- 1=Public 2=Unlisted 3=Private - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP -); - --- Insert the singleton placeholder row so UPDATE always finds it -INSERT OR IGNORE INTO peertube_settings (id) VALUES (1); - --- Feature flag: disabled by default (waiting for upload quota) -INSERT INTO site_settings (key, value, updated_at) -VALUES ('peertube_upload_enabled', '0', CURRENT_TIMESTAMP) -ON CONFLICT(key) DO NOTHING; diff --git a/app/migrations/pending/025_fix_oui_non_artefacts.sql b/app/migrations/applied/025_fix_oui_non_artefacts.sql similarity index 100% rename from app/migrations/pending/025_fix_oui_non_artefacts.sql rename to app/migrations/applied/025_fix_oui_non_artefacts.sql diff --git a/app/migrations/pending/027_drop_banner_path.sql b/app/migrations/pending/027_drop_banner_path.sql deleted file mode 100644 index d4baf59..0000000 --- a/app/migrations/pending/027_drop_banner_path.sql +++ /dev/null @@ -1,10 +0,0 @@ --- Migration 027: drop banner_path column from theses table. --- Banners were merged into covers in migration 016; the column has been --- vestigial since. This is safe to run even if the column is already absent. --- Safe to re-run: IF EXISTS makes it idempotent. - --- SQLite does not support DROP COLUMN directly in older versions; --- we use the ALTER TABLE … DROP COLUMN syntax (supported since SQLite 3.35.0). --- If this fails on an older SQLite, the column stays as-is (harmless). - -ALTER TABLE theses DROP COLUMN banner_path; diff --git a/app/migrations/pending/028_drop_banner_path.sql b/app/migrations/pending/028_drop_banner_path.sql new file mode 100644 index 0000000..c8153a4 --- /dev/null +++ b/app/migrations/pending/028_drop_banner_path.sql @@ -0,0 +1,81 @@ +-- Migration 028: drop banner_path from theses and v_theses_full. +-- +-- 027_drop_banner_path failed because v_theses_full references banner_path. +-- This migration: +-- 1. Drops dependent views +-- 2. Drops the column +-- 3. Recreates the view without banner_path +-- Safe to re-run (views are re-created fresh each time, column drop is idempotent via error skip). + +-- Drop dependent views first (v_theses_public depends on v_theses_full) +DROP VIEW IF EXISTS v_theses_public; +DROP VIEW IF EXISTS v_theses_full; + +-- Drop column (may fail if already absent → run.php skips that error) +ALTER TABLE theses DROP COLUMN banner_path; + +-- Recreate v_theses_full without banner_path +CREATE VIEW v_theses_full AS +SELECT + t.id, + t.identifier, + t.title, + t.subtitle, + t.year, + t.is_doctoral, + t.objet, + o.name as orientation, + ap.name as ap_program, + ft.name as finality_type, + t.synopsis, + t.context_note, + at.name as access_type, + lt.name as license_type, + t.license_id, + t.license_custom, + t.access_type_id, + t.jury_points, + t.submitted_at, + t.defense_date, + t.published_at, + t.is_published, + t.baiu_link, + t.exemplaire_baiu, + t.exemplaire_erg, + t.cc2r, + t.remarks, + t.jury_note_added, + GROUP_CONCAT(DISTINCT a.name ORDER BY a.name ASC) as authors, + GROUP_CONCAT(DISTINCT s.name) as supervisors, + GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'president' THEN s.name END) as jury_president, + GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'promoteur' AND ts.is_ulb = 0 THEN s.name END) as jury_promoteurs, + GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'promoteur' AND ts.is_ulb = 1 THEN s.name END) as jury_promoteurs_ulb, + GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' AND ts.is_external = 0 THEN s.name END) as jury_lecteurs_internes, + GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' AND ts.is_external = 1 THEN s.name END) as jury_lecteurs_externes, + GROUP_CONCAT(DISTINCT UPPER(SUBSTR(l.name,1,1)) || SUBSTR(l.name,2)) as languages, + GROUP_CONCAT(DISTINCT fmt.name) as formats, + GROUP_CONCAT(DISTINCT tg.name) as keywords, + (SELECT a2.email FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as contact_interne, + (SELECT a2.show_contact FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as contact_public +FROM theses t +LEFT JOIN orientations o ON t.orientation_id = o.id +LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id +LEFT JOIN finality_types ft ON t.finality_id = ft.id +LEFT JOIN access_types at ON t.access_type_id = at.id +LEFT JOIN license_types lt ON t.license_id = lt.id +LEFT JOIN thesis_authors ta ON t.id = ta.thesis_id +LEFT JOIN authors a ON ta.author_id = a.id +LEFT JOIN thesis_supervisors ts ON t.id = ts.thesis_id +LEFT JOIN supervisors s ON ts.supervisor_id = s.id +LEFT JOIN thesis_languages tl ON t.id = tl.thesis_id +LEFT JOIN languages l ON tl.language_id = l.id +LEFT JOIN thesis_formats tf ON t.id = tf.thesis_id +LEFT JOIN format_types fmt ON tf.format_id = fmt.id +LEFT JOIN thesis_tags tt ON t.id = tt.thesis_id +LEFT JOIN tags tg ON tt.tag_id = tg.id +GROUP BY t.id; + +-- Recreate v_theses_public (depends on v_theses_full) +CREATE VIEW v_theses_public AS +SELECT * FROM v_theses_full +WHERE is_published = 1; diff --git a/app/public/admin/add.php b/app/public/admin/add.php index 825eea4..81f5014 100644 --- a/app/public/admin/add.php +++ b/app/public/admin/add.php @@ -54,8 +54,8 @@ function wasSelected($key, $value) { $isAdmin = true; $bodyClass = 'admin-body'; -$extraCss = ['/assets/css/form.css', '/assets/css/filepond.min.css']; -$extraJs = ['/assets/js/filepond.min.js', '/assets/js/file-upload-filepond.js', '/assets/js/beforeunload-guard.js']; +$extraCss = ['/assets/css/form.css', '/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css']; +$extraJs = ['/assets/js/filepond.min.js', '/assets/js/filepond-plugin-file-validate-type.min.js', '/assets/js/filepond-plugin-file-validate-size.min.js', '/assets/js/filepond-plugin-image-preview.min.js', '/assets/js/filepond-plugin-image-exif-orientation.min.js', '/assets/js/file-upload-filepond.js', '/assets/js/beforeunload-guard.js']; require_once APP_ROOT . '/templates/head.php'; include APP_ROOT . '/templates/header.php'; include APP_ROOT . '/templates/admin/add.php'; diff --git a/app/public/admin/edit.php b/app/public/admin/edit.php index 5d8f890..3a2d4a8 100644 --- a/app/public/admin/edit.php +++ b/app/public/admin/edit.php @@ -39,8 +39,8 @@ try { } $isAdmin = true; $bodyClass = 'admin-body'; -$extraCss = ['/assets/css/form.css', '/assets/css/filepond.min.css']; -$extraJs = ['/assets/js/filepond.min.js', '/assets/js/file-upload-filepond.js', '/assets/js/beforeunload-guard.js']; +$extraCss = ['/assets/css/form.css', '/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css']; +$extraJs = ['/assets/js/filepond.min.js', '/assets/js/filepond-plugin-file-validate-type.min.js', '/assets/js/filepond-plugin-file-validate-size.min.js', '/assets/js/filepond-plugin-image-preview.min.js', '/assets/js/filepond-plugin-image-exif-orientation.min.js', '/assets/js/file-upload-filepond.js', '/assets/js/beforeunload-guard.js']; require_once APP_ROOT . '/templates/head.php'; include APP_ROOT . '/templates/header.php'; include APP_ROOT . '/templates/admin/edit.php'; diff --git a/app/public/assets/css/filepond-plugin-image-preview.min.css b/app/public/assets/css/filepond-plugin-image-preview.min.css new file mode 100644 index 0000000..3255db6 --- /dev/null +++ b/app/public/assets/css/filepond-plugin-image-preview.min.css @@ -0,0 +1,8 @@ +/*! + * FilePondPluginImagePreview 4.6.12 + * Licensed under MIT, https://opensource.org/licenses/MIT/ + * Please visit https://pqina.nl/filepond/ for details. + */ + +/* eslint-disable */ +.filepond--image-preview-markup{position:absolute;left:0;top:0}.filepond--image-preview-wrapper{z-index:2}.filepond--image-preview-overlay{display:block;position:absolute;left:0;top:0;width:100%;min-height:5rem;max-height:7rem;margin:0;opacity:0;z-index:2;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.filepond--image-preview-overlay svg{width:100%;height:auto;color:inherit;max-height:inherit}.filepond--image-preview-overlay-idle{mix-blend-mode:multiply;color:rgba(40,40,40,.85)}.filepond--image-preview-overlay-success{mix-blend-mode:normal;color:#369763}.filepond--image-preview-overlay-failure{mix-blend-mode:normal;color:#c44e47}@supports (-webkit-marquee-repetition:infinite) and ((-o-object-fit:fill) or (object-fit:fill)){.filepond--image-preview-overlay-idle{mix-blend-mode:normal}}.filepond--image-preview-wrapper{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;position:absolute;left:0;top:0;right:0;height:100%;margin:0;border-radius:.45em;overflow:hidden;background:rgba(0,0,0,.01)}.filepond--image-preview{position:absolute;left:0;top:0;z-index:1;display:flex;align-items:center;height:100%;width:100%;pointer-events:none;background:#222;will-change:transform,opacity}.filepond--image-clip{position:relative;overflow:hidden;margin:0 auto}.filepond--image-clip[data-transparency-indicator=grid] canvas,.filepond--image-clip[data-transparency-indicator=grid] img{background-color:#fff;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg' fill='%23eee'%3E%3Cpath d='M0 0h50v50H0M50 50h50v50H50'/%3E%3C/svg%3E");background-size:1.25em 1.25em}.filepond--image-bitmap,.filepond--image-vector{position:absolute;left:0;top:0;will-change:transform}.filepond--root[data-style-panel-layout~=integrated] .filepond--image-preview-wrapper{border-radius:0}.filepond--root[data-style-panel-layout~=integrated] .filepond--image-preview{height:100%;display:flex;justify-content:center;align-items:center}.filepond--root[data-style-panel-layout~=circle] .filepond--image-preview-wrapper{border-radius:99999rem}.filepond--root[data-style-panel-layout~=circle] .filepond--image-preview-overlay{top:auto;bottom:0;-webkit-transform:scaleY(-1);transform:scaleY(-1)}.filepond--root[data-style-panel-layout~=circle] .filepond--file .filepond--file-action-button[data-align*=bottom]:not([data-align*=center]){margin-bottom:.325em}.filepond--root[data-style-panel-layout~=circle] .filepond--file [data-align*=left]{left:calc(50% - 3em)}.filepond--root[data-style-panel-layout~=circle] .filepond--file [data-align*=right]{right:calc(50% - 3em)}.filepond--root[data-style-panel-layout~=circle] .filepond--progress-indicator[data-align*=bottom][data-align*=left],.filepond--root[data-style-panel-layout~=circle] .filepond--progress-indicator[data-align*=bottom][data-align*=right]{margin-bottom:.5125em}.filepond--root[data-style-panel-layout~=circle] .filepond--progress-indicator[data-align*=bottom][data-align*=center]{margin-top:0;margin-bottom:.1875em;margin-left:.1875em} \ No newline at end of file diff --git a/app/public/assets/css/form.css b/app/public/assets/css/form.css index 3bf3497..4ab566c 100644 --- a/app/public/assets/css/form.css +++ b/app/public/assets/css/form.css @@ -546,24 +546,41 @@ margin-bottom: 0; } +/* Drop area panel */ .filepond--panel-root { background: var(--bg-secondary); - border: 1px dashed var(--border-primary); + border: 2px dashed var(--border-primary); + border-radius: var(--radius); } +/* Drop label text */ .filepond--drop-label { color: var(--text-secondary); font-size: var(--step--1); } +/* "Browse" link */ .filepond--label-action { color: var(--accent-primary); - text-decoration: underline; + text-decoration-color: var(--accent-primary); } +/* File item — keep white for contrast against drop area */ .filepond--item-panel { - background: var(--bg-secondary); + background-color: var(--bg-primary); border: 1px solid var(--border-primary); + border-radius: var(--radius); +} + +/* File item text — dark on white */ +.filepond--file { + color: var(--text-primary); + background-color: var(--bg-tertiary); + border: 1px solid var(--accent-primary); +} + +.filepond--file .filepond--file-status { + color: var(--text-secondary); } .filepond--file-info-main { @@ -574,12 +591,41 @@ color: var(--text-tertiary); } +/* Action buttons — dark background, white icons */ .filepond--file-action-button { - color: var(--text-secondary); + cursor: pointer; + color: #ffffff; + background-color: rgba(0, 0, 0, 0.45); } -.filepond--file-action-button:hover { - color: var(--error); +.filepond--file-action-button:hover, +.filepond--file-action-button:focus { + color: #ffffff; + background-color: rgba(0, 0, 0, 0.65); + box-shadow: 0 0 0 0.125em rgba(0, 0, 0, 0.2); +} + +/* Progress indicator */ +.filepond--progress-indicator { + color: #ffffff; +} + +/* Drag-over highlight */ +.filepond--hopper[data-hopper-state="drag-over"] .filepond--panel-root { + border-color: var(--accent-primary); + background: var(--accent-muted); +} + +/* Error state */ +[data-filepond-item-state*="error"] .filepond--item-panel, +[data-filepond-item-state*="invalid"] .filepond--item-panel { + background-color: var(--error-muted-bg); + border-color: var(--error); +} + +/* Processing complete */ +[data-filepond-item-state="processing-complete"] .filepond--item-panel { + background-color: var(--bg-primary); } /* ── Existing-files list (edit form) ─────────────────────────────────────── */ @@ -1075,7 +1121,7 @@ a.recap-file-name:hover { background: var(--bg-primary); border: 1px solid var(--border-primary); border-radius: var(--radius); - box-shadow: 0 4px 16px rgba(0,0,0,0.12); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); max-height: 220px; overflow-y: auto; display: none; diff --git a/app/public/assets/js/file-upload-filepond.js b/app/public/assets/js/file-upload-filepond.js index 06e68dc..c011178 100644 --- a/app/public/assets/js/file-upload-filepond.js +++ b/app/public/assets/js/file-upload-filepond.js @@ -9,44 +9,87 @@ * 3. storeAsFile: true preserves native multipart form submission. * Server receives files via $_FILES indexed by each input's name attribute * (e.g. queue_file[tfe][], queue_file[video][], etc.). - * 4. Validation rules are derived from ALLOWED_BY_TYPE (same as before). + * 4. Type + size validation: via native FilePond options + FileValidateType/Size plugins. + * beforeAddFile is used ONLY for hybrid validation (e.g. per-type size limits) + * and returns true/false per the FilePond API contract. + * 5. Order serialization: hidden inputs track file order from pond.getFiles(). + * 6. HTMX cleanup: generic destroyFilePondsIn(target) for all swaps, not just known IDs. */ (function () { "use strict"; // ── Per-queue-type configuration ──────────────────────────────────── + // Single source of truth for validation. These specificatons are also + // reflected in the PHP-synthesised accept attributes on inputs. var QUEUE_CONFIG = { tfe: { - exts: ["jpg","jpeg","png","gif","webp","pdf","mp4","webm","ogv","mov","mp3","ogg","oga","wav","flac","aac","m4a","vtt","zip","tar","gz","tgz"], - maxSize: function (f) { return (/\.pdf$/i.test(f.name) ? 100 : 500) * 1024 * 1024; }, - multiple: true, + acceptedFileTypes: [ + "image/jpeg", "image/png", "image/gif", "image/webp", + "application/pdf", + "video/mp4", "video/webm", "video/ogg", "video/quicktime", + "audio/mpeg", "audio/ogg", "audio/flac", "audio/x-wav", "audio/aac", "audio/mp4", + "text/vtt", + "application/zip", "application/x-tar", "application/gzip" + ], + labelFileTypeNotAllowed: "Format non accepté", + fileValidateTypeLabelExpectedTypes: "PDF, Images, Vidéos, Audio, VTT, Archives", + maxFileSize: "500MB", + labelMaxFileSizeExceeded: "Fichier trop volumineux", + labelMaxFileSize: "Taille max: {filesize}", + allowMultiple: true, + // Per-extension size limits: certain types get higher caps. + perExtensionMaxSize: { + pdf: "100MB", + mp4: "2GB", webm: "2GB", ogv: "2GB", mov: "2GB", + mp3: "2GB", ogg: "2GB", oga: "2GB", wav: "2GB", flac: "2GB", aac: "2GB", m4a: "2GB" + } }, video: { - exts: ["mp4","webm","ogv","mov"], - maxSize: function () { return 500 * 1024 * 1024; }, - multiple: true, + acceptedFileTypes: ["video/mp4", "video/webm", "video/ogg", "video/quicktime"], + labelFileTypeNotAllowed: "Format non accepté", + fileValidateTypeLabelExpectedTypes: "MP4, WebM, OGV, MOV", + maxFileSize: "500MB", + labelMaxFileSizeExceeded: "Fichier trop volumineux", + labelMaxFileSize: "Taille max: {filesize}", + allowMultiple: true }, audio: { - exts: ["mp3","ogg","oga","wav","flac","aac","m4a"], - maxSize: function () { return 500 * 1024 * 1024; }, - multiple: true, + acceptedFileTypes: ["audio/mpeg", "audio/ogg", "audio/flac", "audio/x-wav", "audio/aac", "audio/mp4"], + labelFileTypeNotAllowed: "Format non accepté", + fileValidateTypeLabelExpectedTypes: "MP3, OGG, FLAC, WAV, AAC, M4A", + maxFileSize: "500MB", + labelMaxFileSizeExceeded: "Fichier trop volumineux", + labelMaxFileSize: "Taille max: {filesize}", + allowMultiple: true }, annexe: { - exts: ["pdf","zip","tar","gz","tgz"], - maxSize: function () { return 500 * 1024 * 1024; }, - multiple: true, + acceptedFileTypes: ["application/pdf", "application/zip", "application/x-tar", "application/gzip"], + labelFileTypeNotAllowed: "Format non accepté", + fileValidateTypeLabelExpectedTypes: "PDF, ZIP, TAR, GZ", + maxFileSize: "500MB", + labelMaxFileSizeExceeded: "Fichier trop volumineux", + labelMaxFileSize: "Taille max: {filesize}", + allowMultiple: true }, cover: { - exts: ["jpg","jpeg","png","webp"], - maxSize: function () { return 20 * 1024 * 1024; }, - multiple: false, + acceptedFileTypes: ["image/jpeg", "image/png", "image/webp"], + labelFileTypeNotAllowed: "Seulement JPG, PNG ou WEBP", + fileValidateTypeLabelExpectedTypes: "JPG, PNG, WEBP", + maxFileSize: "20MB", + labelMaxFileSizeExceeded: "Fichier trop volumineux", + labelMaxFileSize: "Taille max: {filesize}", + allowMultiple: false }, note_intention: { - exts: ["pdf"], - maxSize: function () { return 100 * 1024 * 1024; }, - multiple: false, + acceptedFileTypes: ["application/pdf"], + labelFileTypeNotAllowed: "Seulement PDF", + fileValidateTypeLabelExpectedTypes: "PDF", + maxFileSize: "100MB", + labelMaxFileSizeExceeded: "Fichier trop volumineux", + labelMaxFileSize: "Taille max: {filesize}", + allowMultiple: false }, }; @@ -61,51 +104,119 @@ "note_intention": "note_intention", }; - function ext(fn) { - var m = fn.match(/\.([^./]+)$/); + // ── Helpers ─────────────────────────────────────────────────────────── + + /** + * Parse a size string like "500MB" or "2GB" to bytes. + */ + function parseSize(str) { + var m = str.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB)$/i); + if (!m) return 0; + var val = parseFloat(m[1]); + var unit = m[2].toUpperCase(); + var mult = {B: 1, KB: 1024, MB: 1024*1024, GB: 1024*1024*1024, TB: 1024*1024*1024*1024}; + return Math.round(val * (mult[unit] || 1)); + } + + /** + * Get extension from filename (lowercase). + */ + function getExt(name) { + var m = name.match(/\.([^./]+)$/); return m ? m[1].toLowerCase() : ""; } + // ── Order serialization ─────────────────────────────────────────────── + + /** + * Create/update a hidden input that serializes the file order for a queue. + * Name: queue_order[] + * Value: pipe-separated list of file names. + */ + function syncOrderInput(queueType, pond) { + var form = pond.element ? pond.element.closest("form") : null; + if (!form) return; + + var orderInput = form.querySelector("input[name='queue_order[" + queueType + "]']"); + var files = pond.getFiles(); + if (files.length === 0) { + if (orderInput) orderInput.remove(); + return; + } + + var names = []; + for (var i = 0; i < files.length; i++) { + names.push(files[i].filename || files[i].file.name); + } + + if (!orderInput) { + orderInput = document.createElement("input"); + orderInput.type = "hidden"; + orderInput.name = "queue_order[" + queueType + "]"; + form.appendChild(orderInput); + } + orderInput.value = names.join("|"); + } + // ── FilePond configuration per queue type ───────────────────────────── function buildFilePondOptions(queueType, input) { var cfg = QUEUE_CONFIG[queueType]; if (!cfg) return null; - var mimeMap = { - jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", - gif: "image/gif", webp: "image/webp", - pdf: "application/pdf", - mp4: "video/mp4", webm: "video/webm", ogv: "video/ogg", mov: "video/quicktime", - mp3: "audio/mpeg", ogg: "audio/ogg", oga: "audio/ogg", wav: "audio/wav", - flac: "audio/flac", aac: "audio/aac", m4a: "audio/mp4", - vtt: "text/vtt", - zip: "application/zip", tar: "application/x-tar", gz: "application/gzip", tgz: "application/gzip", - }; - var accepted = cfg.exts.map(function(e) { return mimeMap[e] || ("." + e); }); + // Per-type max size overrides (for TFE: PDF=100MB, video/audio=2GB) + var perExtMax = cfg.perExtensionMaxSize || {}; return { - allowMultiple: cfg.multiple, + allowMultiple: cfg.allowMultiple, allowReorder: true, + allowProcess: false, storeAsFile: true, + + // ── Native FilePond validation ── + acceptedFileTypes: cfg.acceptedFileTypes, + labelFileTypeNotAllowed: cfg.labelFileTypeNotAllowed, + fileValidateTypeLabelExpectedTypes: cfg.fileValidateTypeLabelExpectedTypes, + maxFileSize: cfg.maxFileSize, + labelMaxFileSizeExceeded: cfg.labelMaxFileSizeExceeded, + labelMaxFileSize: cfg.labelMaxFileSize, + + // ── French labels ── labelIdle: "Glissez-déposez vos fichiers ou Parcourir", - acceptedFileTypes: accepted, - labelFileTypeNotAllowed: "Type de fichier non accepté", - fileValidateTypeLabelExpectedTypes: "Types acceptés : " + cfg.exts.map(function(e) { return "." + e; }).join(", "), - maxFileSize: function () { return "500MB"; }, + labelFileProcessing: "Chargement en cours", + labelFileProcessingComplete: "Chargement terminé", + labelFileProcessingAborted: "Chargement annulé", + labelFileProcessingError: "Erreur lors du chargement", + labelTapToCancel: "Appuyez pour annuler", + labelTapToRetry: "Appuyez pour réessayer", + labelTapToUndo: "Appuyez pour annuler", + labelButtonRemoveItem: "Supprimer", + labelButtonAbortItemLoad: "Annuler", + labelButtonRetryItemLoad: "Réessayer", + labelButtonProcessItem: "Charger", + + // ── Per-extension size validation (hybrid: FilePond validates global maxFileSize, + // beforeAddFile enforces per-extension limits via false return) ── beforeAddFile: function (item) { var f = item.file; - var max = cfg.maxSize(f); - if (f.size > max) { - var maxMb = Math.round(max / 1024 / 1024); - return { - status: "error", - main: "Fichier trop volumineux (" + (f.size / 1024 / 1024).toFixed(1) + " MB)", - sub: "Maximum : " + maxMb + " MB." - }; + var ext = getExt(f.name); + if (ext && perExtMax[ext]) { + var limit = parseSize(perExtMax[ext]); + if (limit > 0 && f.size > limit) { + // Return false per FilePond API contract — the FileValidateSize + // plugin sets the error state via maxFileSize, but per-extension + // cap violations must be rejected here. + return false; + } } return true; }, + + // ── Order serialization on add/remove/reorder ── + onaddfile: function () { syncOrderInput(queueType, this); }, + onremovefile: function () { syncOrderInput(queueType, this); }, + onreorderfiles: function () { syncOrderInput(queueType, this); }, + onupdatefiles: function () { syncOrderInput(queueType, this); }, }; } @@ -121,15 +232,12 @@ */ window.XamxamInitFilePonds = function () { document.querySelectorAll(".tfe-file-picker").forEach(function (input) { - // Skip already upgraded inputs - if (input.dataset.filepondUpgraded) return; - // Skip if input is inside an existing FilePond root - if (input.closest(".filepond--root")) return; + // Canonical duplicate check: FilePond.find() is the authoritative source + if (FilePond.find(input)) return; var id = input.id; var queueType = INPUT_ID_TO_TYPE[id]; if (!queueType) { - // Try data-queue-type on the input itself queueType = input.dataset.queueType || null; } if (!queueType) return; @@ -137,56 +245,77 @@ var options = buildFilePondOptions(queueType, input); if (!options) return; - // Preserve the input's original name for form submission options.name = input.getAttribute("name") || input.name || ""; var pond = FilePond.create(input, options); - input.dataset.filepondUpgraded = "1"; - // Track by id for cleanup var key = id || queueType; _ponds[key] = pond; + + // Initial order serialization (for existing files in edit mode — none expected) + syncOrderInput(queueType, pond); }); - } + }; /** * Destroy FilePond instances inside a given container element. - * Used before HTMX swaps to avoid leaks. + * Generic: handles ANY HTMX swap target, not just known IDs. */ function destroyFilePondsIn(el) { if (!el) return; - // Find FilePond-upgraded inputs inside this element - el.querySelectorAll(".tfe-file-picker[data-filepond-upgraded]").forEach(function (input) { - // Destroy the FilePond instance if it exists - var id = input.id; - var pond = id ? _ponds[id] : null; + el.querySelectorAll(".tfe-file-picker").forEach(function (input) { + var pond = FilePond.find(input); if (pond) { - try { pond.destroy(); } catch (_) {} - delete _ponds[id]; + try { + // Remove order input before destroying + var form = input.closest("form"); + if (form) { + var id = input.id; + var queueType = INPUT_ID_TO_TYPE[id] || input.dataset.queueType || null; + if (queueType) { + var orderInput = form.querySelector("input[name='queue_order[" + queueType + "]']"); + if (orderInput) orderInput.remove(); + } + } + pond.destroy(); + } catch (_) {} + } + // Clean up tracking + if (input.id && _ponds[input.id]) { + delete _ponds[input.id]; } - delete input.dataset.filepondUpgraded; }); } // ── HTMX integration ───────────────────────────────────────────────── /** - * Before HTMX swaps a slot element that may contain FilePond instances, - * destroy them to avoid leaks and file-state conflicts. + * Generic beforeSwap handler: destroy FilePonds in ANY swapped target. + * This prevents detached FilePond instances from leaking listeners. */ function onHtmxBeforeSwap(evt) { var target = evt.detail.target; - if (!target) return; - var id = target.id || ""; - // Only care about slot elements that may contain FilePond file inputs - if (id === "slot-video" || id === "slot-audio" || id === "annexes-input-block" || id === "format-extras-block") { + if (target) { destroyFilePondsIn(target); } } // ── Bootstrap ───────────────────────────────────────────────────────── - // Hook into HTMX events if htmx is loaded + // Register FilePond plugins (idempotent) + if (typeof FilePondPluginFileValidateType !== "undefined") { + FilePond.registerPlugin(FilePondPluginFileValidateType); + } + if (typeof FilePondPluginFileValidateSize !== "undefined") { + FilePond.registerPlugin(FilePondPluginFileValidateSize); + } + if (typeof FilePondPluginImagePreview !== "undefined") { + FilePond.registerPlugin(FilePondPluginImagePreview); + } + if (typeof FilePondPluginImageExifOrientation !== "undefined") { + FilePond.registerPlugin(FilePondPluginImageExifOrientation); + } + if (window.htmx) { window.htmx.on("htmx:beforeSwap", onHtmxBeforeSwap); window.htmx.on("htmx:afterSwap", function () { @@ -194,7 +323,6 @@ }); } - // Initialise on DOM ready if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", function () { window.XamxamInitFilePonds(); @@ -208,7 +336,6 @@ window.__xamxamDirty = true; }); - // Clean dirty flag on form submit (matches beforeunload-guard.js) document.addEventListener("submit", function (e) { var form = e.target; if (form && form.hasAttribute && form.hasAttribute("data-beforeunload-guard")) { diff --git a/app/public/assets/js/filepond-plugin-file-validate-size.min.js b/app/public/assets/js/filepond-plugin-file-validate-size.min.js new file mode 100644 index 0000000..17fcb9d --- /dev/null +++ b/app/public/assets/js/filepond-plugin-file-validate-size.min.js @@ -0,0 +1,9 @@ +/*! + * FilePondPluginFileValidateSize 2.2.8 + * Licensed under MIT, https://opensource.org/licenses/MIT/ + * Please visit https://pqina.nl/filepond/ for details. + */ + +/* eslint-disable */ + +!function(e,i){"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(e=e||self).FilePondPluginFileValidateSize=i()}(this,function(){"use strict";var e=function(e){var i=e.addFilter,E=e.utils,l=E.Type,_=E.replaceInString,n=E.toNaturalFileSize;return i("ALLOW_HOPPER_ITEM",function(e,i){var E=i.query;if(!E("GET_ALLOW_FILE_SIZE_VALIDATION"))return!0;var l=E("GET_MAX_FILE_SIZE");if(null!==l&&e.size>l)return!1;var _=E("GET_MIN_FILE_SIZE");return!(null!==_&&e.size<_)}),i("LOAD_FILE",function(e,i){var E=i.query;return new Promise(function(i,l){if(!E("GET_ALLOW_FILE_SIZE_VALIDATION"))return i(e);var I=E("GET_FILE_VALIDATE_SIZE_FILTER");if(I&&!I(e))return i(e);var t=E("GET_MAX_FILE_SIZE");if(null!==t&&e.size>t)l({status:{main:E("GET_LABEL_MAX_FILE_SIZE_EXCEEDED"),sub:_(E("GET_LABEL_MAX_FILE_SIZE"),{filesize:n(t,".",E("GET_FILE_SIZE_BASE"),E("GET_FILE_SIZE_LABELS",E))})}});else{var L=E("GET_MIN_FILE_SIZE");if(null!==L&&e.sizea)return void l({status:{main:E("GET_LABEL_MAX_TOTAL_FILE_SIZE_EXCEEDED"),sub:_(E("GET_LABEL_MAX_TOTAL_FILE_SIZE"),{filesize:n(a,".",E("GET_FILE_SIZE_BASE"),E("GET_FILE_SIZE_LABELS",E))})}});i(e)}}})}),{options:{allowFileSizeValidation:[!0,l.BOOLEAN],maxFileSize:[null,l.INT],minFileSize:[null,l.INT],maxTotalFileSize:[null,l.INT],fileValidateSizeFilter:[null,l.FUNCTION],labelMinFileSizeExceeded:["File is too small",l.STRING],labelMinFileSize:["Minimum file size is {filesize}",l.STRING],labelMaxFileSizeExceeded:["File is too large",l.STRING],labelMaxFileSize:["Maximum file size is {filesize}",l.STRING],labelMaxTotalFileSizeExceeded:["Maximum total size exceeded",l.STRING],labelMaxTotalFileSize:["Maximum total file size is {filesize}",l.STRING]}}};return"undefined"!=typeof window&&void 0!==window.document&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:e})),e}); diff --git a/app/public/assets/js/filepond-plugin-file-validate-type.min.js b/app/public/assets/js/filepond-plugin-file-validate-type.min.js new file mode 100644 index 0000000..f2cb360 --- /dev/null +++ b/app/public/assets/js/filepond-plugin-file-validate-type.min.js @@ -0,0 +1,9 @@ +/*! + * FilePondPluginFileValidateType 1.2.9 + * Licensed under MIT, https://opensource.org/licenses/MIT/ + * Please visit https://pqina.nl/filepond/ for details. + */ + +/* eslint-disable */ + +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).FilePondPluginFileValidateType=t()}(this,function(){"use strict";var e=function(e){var t=e.addFilter,n=e.utils,i=n.Type,T=n.isString,E=n.replaceInString,l=n.guesstimateMimeType,o=n.getExtensionFromFilename,r=n.getFilenameFromURL,u=function(e,t){return e.some(function(e){return/\*$/.test(e)?(n=e,(/^[^/]+/.exec(t)||[]).pop()===n.slice(0,-2)):e===t;var n})},a=function(e,t,n){if(0===t.length)return!0;var i=function(e){var t="";if(T(e)){var n=r(e),i=o(n);i&&(t=l(i))}else t=e.type;return t}(e);return n?new Promise(function(T,E){n(e,i).then(function(e){u(t,e)?T():E()}).catch(E)}):u(t,i)};return t("SET_ATTRIBUTE_TO_OPTION_MAP",function(e){return Object.assign(e,{accept:"acceptedFileTypes"})}),t("ALLOW_HOPPER_ITEM",function(e,t){var n=t.query;return!n("GET_ALLOW_FILE_TYPE_VALIDATION")||a(e,n("GET_ACCEPTED_FILE_TYPES"))}),t("LOAD_FILE",function(e,t){var n=t.query;return new Promise(function(t,i){if(n("GET_ALLOW_FILE_TYPE_VALIDATION")){var T=n("GET_ACCEPTED_FILE_TYPES"),l=n("GET_FILE_VALIDATE_TYPE_DETECT_TYPE"),o=a(e,T,l),r=function(){var e,t=T.map((e=n("GET_FILE_VALIDATE_TYPE_LABEL_EXPECTED_TYPES_MAP"),function(t){return null!==e[t]&&(e[t]||t)})).filter(function(e){return!1!==e}),l=t.filter(function(e,n){return t.indexOf(e)===n});i({status:{main:n("GET_LABEL_FILE_TYPE_NOT_ALLOWED"),sub:E(n("GET_FILE_VALIDATE_TYPE_LABEL_EXPECTED_TYPES"),{allTypes:l.join(", "),allButLastType:l.slice(0,-1).join(", "),lastType:l[l.length-1]})}})};if("boolean"==typeof o)return o?t(e):r();o.then(function(){t(e)}).catch(r)}else t(e)})}),{options:{allowFileTypeValidation:[!0,i.BOOLEAN],acceptedFileTypes:[[],i.ARRAY],labelFileTypeNotAllowed:["File is of invalid type",i.STRING],fileValidateTypeLabelExpectedTypes:["Expects {allButLastType} or {lastType}",i.STRING],fileValidateTypeLabelExpectedTypesMap:[{},i.OBJECT],fileValidateTypeDetectType:[null,i.FUNCTION]}}};return"undefined"!=typeof window&&void 0!==window.document&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:e})),e}); diff --git a/app/public/assets/js/filepond-plugin-image-exif-orientation.min.js b/app/public/assets/js/filepond-plugin-image-exif-orientation.min.js new file mode 100644 index 0000000..90cc07f --- /dev/null +++ b/app/public/assets/js/filepond-plugin-image-exif-orientation.min.js @@ -0,0 +1,9 @@ +/*! + * FilePondPluginImageExifOrientation 1.0.11 + * Licensed under MIT, https://opensource.org/licenses/MIT/ + * Please visit https://pqina.nl/filepond/ for details. + */ + +/* eslint-disable */ + +!function(A,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(A=A||self).FilePondPluginImageExifOrientation=e()}(this,function(){"use strict";var A=65496,e=65505,n=1165519206,t=18761,i=274,r=65280,o=function(A,e){var n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return A.getUint16(e,n)},a=function(A,e){var n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return A.getUint32(e,n)},u="undefined"!=typeof window&&void 0!==window.document,d=void 0,f=u?new Image:{};f.onload=function(){return d=f.naturalWidth>f.naturalHeight},f.src="data:image/jpg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4QA6RXhpZgAATU0AKgAAAAgAAwESAAMAAAABAAYAAAEoAAMAAAABAAIAAAITAAMAAAABAAEAAAAAAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////wAALCAABAAIBASIA/8QAJgABAAAAAAAAAAAAAAAAAAAAAxABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQAAPwBH/9k=";var l=function(u){var f=u.addFilter,l=u.utils,c=l.Type,g=l.isFile;return f("DID_LOAD_ITEM",function(u,f){var l=f.query;return new Promise(function(f,c){var s=u.file;if(!(g(s)&&function(A){return/^image\/jpeg/.test(A.type)}(s)&&l("GET_ALLOW_IMAGE_EXIF_ORIENTATION")&&d))return f(u);(function(u){return new Promise(function(d,f){var l=new FileReader;l.onload=function(u){var f=new DataView(u.target.result);if(o(f,0)===A){for(var l=f.byteLength,c=2;c0&&void 0!==arguments[0]?arguments[0]:0,y:arguments.length>1&&void 0!==arguments[1]?arguments[1]:0}},h=function(e,t){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1,r=arguments.length>3?arguments[3]:void 0;return"string"==typeof e?parseFloat(e)*i:"number"==typeof e?e*(r?t[r]:Math.min(t.width,t.height)):void 0},u=function(e){return null!=e},l=function(e,t){return Object.keys(t).forEach(function(i){return e.setAttribute(i,t[i])})},d=function(e,t){var i=document.createElementNS("http://www.w3.org/2000/svg",e);return t&&l(i,t),i},f={contain:"xMidYMid meet",cover:"xMidYMid slice"},p={left:"start",center:"middle",right:"end"},g=function(e){return function(t){return d(e,{id:t.id})}},m={image:function(e){var t=d("image",{id:e.id,"stroke-linecap":"round","stroke-linejoin":"round",opacity:"0"});return t.onload=function(){t.setAttribute("opacity",e.opacity||1)},t.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",e.src),t},rect:g("rect"),ellipse:g("ellipse"),text:g("text"),path:g("path"),line:function(e){var t=d("g",{id:e.id,"stroke-linecap":"round","stroke-linejoin":"round"}),i=d("line");t.appendChild(i);var r=d("path");t.appendChild(r);var a=d("path");return t.appendChild(a),t}},y={rect:function(e){return l(e,Object.assign({},e.rect,e.styles))},ellipse:function(e){var t=e.rect.x+.5*e.rect.width,i=e.rect.y+.5*e.rect.height,r=.5*e.rect.width,a=.5*e.rect.height;return l(e,Object.assign({cx:t,cy:i,rx:r,ry:a},e.styles))},image:function(e,t){l(e,Object.assign({},e.rect,e.styles,{preserveAspectRatio:f[t.fit]||"none"}))},text:function(e,t,i,r){var a=h(t.fontSize,i,r),n=t.fontFamily||"sans-serif",o=t.fontWeight||"normal",c=p[t.textAlign]||"start";l(e,Object.assign({},e.rect,e.styles,{"stroke-width":0,"font-weight":o,"font-size":a,"font-family":n,"text-anchor":c})),e.text!==t.text&&(e.text=t.text,e.textContent=t.text.length?t.text:" ")},path:function(e,t,i,r){var a;l(e,Object.assign({},e.styles,{fill:"none",d:(a=t.points.map(function(e){return{x:h(e.x,i,r,"width"),y:h(e.y,i,r,"height")}}),a.map(function(e,t){return"".concat(0===t?"M":"L"," ").concat(e.x," ").concat(e.y)}).join(" "))}))},line:function(e,t,i,r){l(e,Object.assign({},e.rect,e.styles,{fill:"none"}));var a=e.childNodes[0],u=e.childNodes[1],d=e.childNodes[2],f=e.rect,p={x:e.rect.x+e.rect.width,y:e.rect.y+e.rect.height};if(l(a,{x1:f.x,y1:f.y,x2:p.x,y2:p.y}),t.lineDecoration){u.style.display="none",d.style.display="none";var g=function(e){var t=Math.sqrt(e.x*e.x+e.y*e.y);return 0===t?{x:0,y:0}:s(e.x/t,e.y/t)}({x:p.x-f.x,y:p.y-f.y}),m=h(.05,i,r);if(-1!==t.lineDecoration.indexOf("arrow-begin")){var y=n(g,m),E=o(f,y),v=c(f,2,E),w=c(f,-2,E);l(u,{style:"display:block;",d:"M".concat(v.x,",").concat(v.y," L").concat(f.x,",").concat(f.y," L").concat(w.x,",").concat(w.y)})}if(-1!==t.lineDecoration.indexOf("arrow-end")){var _=n(g,-m),I=o(p,_),M=c(p,2,I),x=c(p,-2,I);l(d,{style:"display:block;",d:"M".concat(M.x,",").concat(M.y," L").concat(p.x,",").concat(p.y," L").concat(x.x,",").concat(x.y)})}}}},E=function(e,t,i,r,a){"path"!==t&&(e.rect=function(e,t){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1,r=h(e.x,t,i,"width")||h(e.left,t,i,"width"),a=h(e.y,t,i,"height")||h(e.top,t,i,"height"),n=h(e.width,t,i,"width"),o=h(e.height,t,i,"height"),c=h(e.right,t,i,"width"),s=h(e.bottom,t,i,"height");return u(a)||(a=u(o)&&u(s)?t.height-o-s:s),u(r)||(r=u(n)&&u(c)?t.width-n-c:c),u(n)||(n=u(r)&&u(c)?t.width-r-c:0),u(o)||(o=u(a)&&u(s)?t.height-a-s:0),{x:r||0,y:a||0,width:n||0,height:o||0}}(i,r,a)),e.styles=function(e,t,i){var r=e.borderStyle||e.lineStyle||"solid",a=e.backgroundColor||e.fontColor||"transparent",n=e.borderColor||e.lineColor||"transparent",o=h(e.borderWidth||e.lineWidth,t,i);return{"stroke-linecap":e.lineCap||"round","stroke-linejoin":e.lineJoin||"round","stroke-width":o||0,"stroke-dasharray":"string"==typeof r?"":r.map(function(e){return h(e,t,i)}).join(","),stroke:n,fill:a,opacity:e.opacity||1}}(i,r,a),y[t](e,i,r,a)},v=["x","y","left","top","right","bottom","width","height"],w=function(e){var t=i(e,2),r=t[0],a=t[1],n=a.points?{}:v.reduce(function(e,t){var i;return e[t]="string"==typeof(i=a[t])&&/%/.test(i)?parseFloat(i)/100:i,e},{});return[r,Object.assign({zIndex:0},a,n)]},_=function(e,t){return e[1].zIndex>t[1].zIndex?1:e[1].zIndex.5?1-r.x:r.x,n=r.y>.5?1-r.y:r.y,o=2*a*e.width,c=2*n*e.height,s=function(e,t){var i=e.width,r=e.height,a=A(i,t),n=A(r,t),o=M(e.x+Math.abs(a.x),e.y-Math.abs(a.y)),c=M(e.x+e.width+Math.abs(n.y),e.y+Math.abs(n.x)),s=M(e.x-Math.abs(n.y),e.y+e.height-Math.abs(n.x));return{width:T(o,c),height:T(o,s)}}(t,i);return Math.max(s.width/o,s.height/c)},P=function(e,t){var i=e.width,r=i*t;return r>e.height&&(i=(r=e.height)/t),{x:.5*(e.width-i),y:.5*(e.height-r),width:i,height:r}},C=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=t.zoom,r=t.rotation,a=t.center,n=t.aspectRatio;n||(n=e.height/e.width);var o=function(e,t){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1,r=e.height/e.width,a=t,n=1,o=r;o>a&&(n=(o=a)/r);var c=Math.max(1/n,a/o),s=e.width/(i*c*n);return{width:s,height:s*t}}(e,n,i),c={x:.5*o.width,y:.5*o.height},s={x:0,y:0,width:o.width,height:o.height,center:c},h=void 0===t.scaleToFit||t.scaleToFit,u=i*R(e,P(s,n),r,h?a:{x:.5,y:.5});return{widthFloat:o.width/u,heightFloat:o.height/u,width:Math.round(o.width/u),height:Math.round(o.height/u)}},k={type:"spring",stiffness:.5,damping:.45,mass:10},D=function(e){return e.utils.createView({name:"image-clip",tag:"div",ignoreRect:!0,mixins:{apis:["crop","markup","resize","width","height","dirty","background"],styles:["width","height","opacity"],animations:{opacity:{type:"tween",duration:250}}},didWriteView:function(e){var t=e.root,i=e.props;i.background&&(t.element.style.backgroundColor=i.background)},create:function(t){var i=t.root,r=t.props;i.ref.image=i.appendChildView(i.createChildView(function(e){return e.utils.createView({name:"image-canvas-wrapper",tag:"div",ignoreRect:!0,mixins:{apis:["crop","width","height"],styles:["originX","originY","translateX","translateY","scaleX","scaleY","rotateZ"],animations:{originX:k,originY:k,scaleX:k,scaleY:k,translateX:k,translateY:k,rotateZ:k}},create:function(t){var i=t.root,r=t.props;r.width=r.image.width,r.height=r.image.height,i.ref.bitmap=i.appendChildView(i.createChildView(function(e){return e.utils.createView({name:"image-bitmap",ignoreRect:!0,mixins:{styles:["scaleX","scaleY"]},create:function(e){var t=e.root,i=e.props;t.appendChild(i.image)}})}(e),{image:r.image}))},write:function(e){var t=e.root,i=e.props.crop.flip,r=t.ref.bitmap;r.scaleX=i.horizontal?-1:1,r.scaleY=i.vertical?-1:1}})}(e),Object.assign({},r))),i.ref.createMarkup=function(){i.ref.markup||(i.ref.markup=i.appendChildView(i.createChildView(I(e),Object.assign({},r))))},i.ref.destroyMarkup=function(){i.ref.markup&&(i.removeChildView(i.ref.markup),i.ref.markup=null)};var a=i.query("GET_IMAGE_PREVIEW_TRANSPARENCY_INDICATOR");null!==a&&(i.element.dataset.transparencyIndicator="grid"===a?a:"color")},write:function(e){var t=e.root,i=e.props,r=e.shouldOptimize,a=i.crop,n=i.markup,o=i.resize,c=i.dirty,s=i.width,h=i.height;t.ref.image.crop=a;var u={x:0,y:0,width:s,height:h,center:{x:.5*s,y:.5*h}},l={width:t.ref.image.width,height:t.ref.image.height},d={x:a.center.x*l.width,y:a.center.y*l.height},f={x:u.center.x-l.width*a.center.x,y:u.center.y-l.height*a.center.y},p=2*Math.PI+a.rotation%(2*Math.PI),g=a.aspectRatio||l.height/l.width,m=void 0===a.scaleToFit||a.scaleToFit,y=R(l,P(u,g),p,m?a.center:{x:.5,y:.5}),E=a.zoom*y;n&&n.length?(t.ref.createMarkup(),t.ref.markup.width=s,t.ref.markup.height=h,t.ref.markup.resize=o,t.ref.markup.dirty=c,t.ref.markup.markup=n,t.ref.markup.crop=C(l,a)):t.ref.markup&&t.ref.destroyMarkup();var v=t.ref.image;if(r)return v.originX=null,v.originY=null,v.translateX=null,v.translateY=null,v.rotateZ=null,v.scaleX=null,void(v.scaleY=null);v.originX=d.x,v.originY=d.y,v.translateX=f.x,v.translateY=f.y,v.rotateZ=p,v.scaleX=E,v.scaleY=E}})},G=0,V=function(){self.onmessage=function(e){createImageBitmap(e.data.message.file).then(function(t){self.postMessage({id:e.data.id,message:t},[t])})}},O=function(){self.onmessage=function(e){for(var t=e.data.message.imageData,i=e.data.message.colorMatrix,r=t.data,a=r.length,n=i[0],o=i[1],c=i[2],s=i[3],h=i[4],u=i[5],l=i[6],d=i[7],f=i[8],p=i[9],g=i[10],m=i[11],y=i[12],E=i[13],v=i[14],w=i[15],_=i[16],I=i[17],M=i[18],x=i[19],T=0,A=0,R=0,P=0,C=0;T=5&&r<=8){var o=[i,t];t=o[0],i=o[1]}return function(e,t,i,r){-1!==r&&e.transform.apply(e,b[r](t,i))}(n,t,i,r),n.drawImage(e,0,0,t,i),a},L=function(e){return/^image/.test(e.type)&&!/svg/.test(e.type)},N=function(e){var t=Math.min(10/e.width,10/e.height),i=document.createElement("canvas"),r=i.getContext("2d"),a=i.width=Math.ceil(e.width*t),n=i.height=Math.ceil(e.height*t);r.drawImage(e,0,0,a,n);var o=null;try{o=r.getImageData(0,0,a,n).data}catch(e){return null}for(var c=o.length,s=0,h=0,u=0,l=0;l\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n';if(document.querySelector("base")){var a=new URL(window.location.href.replace(window.location.hash,"")).href;r=r.replace(/url\(\#/g,"url("+a+"#")}G++,t.element.classList.add("filepond--image-preview-overlay-".concat(i.status)),t.element.innerHTML=r.replace(/__UID__/g,G)},mixins:{styles:["opacity"],animations:{opacity:{type:"spring",mass:25}}}}),i=function(e){return e.utils.createView({name:"image-preview",tag:"div",ignoreRect:!0,mixins:{apis:["image","crop","markup","resize","dirty","background"],styles:["translateY","scaleX","scaleY","opacity"],animations:{scaleX:k,scaleY:k,translateY:k,opacity:{type:"tween",duration:400}}},create:function(t){var i=t.root,r=t.props;i.ref.clip=i.appendChildView(i.createChildView(D(e),{id:r.id,image:r.image,crop:r.crop,markup:r.markup,resize:r.resize,dirty:r.dirty,background:r.background}))},write:function(e){var t=e.root,i=e.props,r=e.shouldOptimize,a=t.ref.clip,n=i.image,o=i.crop,c=i.markup,s=i.resize,h=i.dirty;if(a.crop=o,a.markup=c,a.resize=s,a.dirty=h,a.opacity=r?0:1,!r&&!t.rect.element.hidden){var u=n.height/n.width,l=o.aspectRatio||u,d=t.rect.inner.width,f=t.rect.inner.height,p=t.query("GET_IMAGE_PREVIEW_HEIGHT"),g=t.query("GET_IMAGE_PREVIEW_MIN_HEIGHT"),m=t.query("GET_IMAGE_PREVIEW_MAX_HEIGHT"),y=t.query("GET_PANEL_ASPECT_RATIO"),E=t.query("GET_ALLOW_MULTIPLE");y&&!E&&(p=d*y,l=y);var v=null!==p?p:Math.max(g,Math.min(d*l,m)),w=v/l;w>d&&(v=(w=d)*l),v>f&&(v=f,w=f/l),a.width=w,a.height=v}}})}(e),r=e.utils.createWorker,a=function(e,t,i){return new Promise(function(a){e.ref.imageData||(e.ref.imageData=i.getContext("2d").getImageData(0,0,i.width,i.height));var n=function(e){var t;try{t=new ImageData(e.width,e.height)}catch(i){t=document.createElement("canvas").getContext("2d").createImageData(e.width,e.height)}return t.data.set(new Uint8ClampedArray(e.data)),t}(e.ref.imageData);if(!t||20!==t.length)return i.getContext("2d").putImageData(n,0,0),a();var o=r(O);o.post({imageData:n,colorMatrix:t},function(e){i.getContext("2d").putImageData(e,0,0),o.terminate(),a()},[n.data.buffer])})},n=function(e){var t=e.root,r=e.props,a=e.image,n=r.id,o=t.query("GET_ITEM",{id:n});if(o){var c,s,h=o.getMetadata("crop")||{center:{x:.5,y:.5},flip:{horizontal:!1,vertical:!1},zoom:1,rotation:0,aspectRatio:null},u=t.query("GET_IMAGE_TRANSFORM_CANVAS_BACKGROUND_COLOR"),l=!1;t.query("GET_IMAGE_PREVIEW_MARKUP_SHOW")&&(c=o.getMetadata("markup")||[],s=o.getMetadata("resize"),l=!0);var d=t.appendChildView(t.createChildView(i,{id:n,image:a,crop:h,resize:s,markup:c,dirty:l,background:u,opacity:0,scaleX:1.15,scaleY:1.15,translateY:15}),t.childViews.length);t.ref.images.push(d),d.opacity=1,d.scaleX=1,d.scaleY=1,d.translateY=0,setTimeout(function(){t.dispatch("DID_IMAGE_PREVIEW_SHOW",{id:n})},250)}},o=function(e){var t=e.root;t.ref.overlayShadow.opacity=1,t.ref.overlayError.opacity=0,t.ref.overlaySuccess.opacity=0},c=function(e){var t=e.root;t.ref.overlayShadow.opacity=.25,t.ref.overlayError.opacity=1};return e.utils.createView({name:"image-preview-wrapper",create:function(e){var i=e.root;i.ref.images=[],i.ref.imageData=null,i.ref.imageViewBin=[],i.ref.overlayShadow=i.appendChildView(i.createChildView(t,{opacity:0,status:"idle"})),i.ref.overlaySuccess=i.appendChildView(i.createChildView(t,{opacity:0,status:"success"})),i.ref.overlayError=i.appendChildView(i.createChildView(t,{opacity:0,status:"failure"}))},styles:["height"],apis:["height"],destroy:function(e){e.root.ref.images.forEach(function(e){e.image.width=1,e.image.height=1})},didWriteView:function(e){e.root.ref.images.forEach(function(e){e.dirty=!1})},write:e.utils.createRoute({DID_IMAGE_PREVIEW_DRAW:function(e){var t=e.root,i=t.ref.images[t.ref.images.length-1];i.translateY=0,i.scaleX=1,i.scaleY=1,i.opacity=1},DID_IMAGE_PREVIEW_CONTAINER_CREATE:function(e){var t=e.root,i=e.props.id,r=t.query("GET_ITEM",i);if(r){var a,n,o,c=URL.createObjectURL(r.file);a=c,n=function(e,r){t.dispatch("DID_IMAGE_PREVIEW_CALCULATE_SIZE",{id:i,width:e,height:r})},(o=new Image).onload=function(){var e=o.naturalWidth,t=o.naturalHeight;o=null,n(e,t)},o.src=a}},DID_FINISH_CALCULATE_PREVIEWSIZE:function(e){var t=e.root,i=e.props,o=i.id,c=t.query("GET_ITEM",o);if(c){var s,h,u,l=URL.createObjectURL(c.file),d=function(){var e;(e=l,new Promise(function(t,i){var r=new Image;r.crossOrigin="Anonymous",r.onload=function(){t(r)},r.onerror=function(e){i(e)},r.src=e})).then(f)},f=function(e){URL.revokeObjectURL(l);var r=(c.getMetadata("exif")||{}).orientation||-1,o=e.width,s=e.height;if(o&&s){if(r>=5&&r<=8){var h=[s,o];o=h[0],s=h[1]}var u=Math.max(1,.75*window.devicePixelRatio),d=t.query("GET_IMAGE_PREVIEW_ZOOM_FACTOR")*u,f=s/o,p=t.rect.element.width,g=t.rect.element.height,m=p,y=m*f;f>1?y=(m=Math.min(o,p*d))*f:m=(y=Math.min(s,g*d))/f;var E=S(e,m,y,r),v=function(){var r=t.query("GET_IMAGE_PREVIEW_CALCULATE_AVERAGE_IMAGE_COLOR")?N(data):null;c.setMetadata("color",r,!0),"close"in e&&e.close(),t.ref.overlayShadow.opacity=1,n({root:t,props:i,image:E})},w=c.getMetadata("filter");w?a(t,w,E).then(v):v()}};if(s=c.file,h=window.navigator.userAgent.match(/Firefox\/([0-9]+)\./),!(null!==(u=h?parseInt(h[1]):null)&&u<=58)&&"createImageBitmap"in window&&L(s)){var p=r(V);p.post({file:c.file},function(e){p.terminate(),e?f(e):d()})}else d()}},DID_UPDATE_ITEM_METADATA:function(e){var t=e.root,i=e.props,r=e.action;if(/crop|filter|markup|resize/.test(r.change.key)&&t.ref.images.length){var o=t.query("GET_ITEM",{id:i.id});if(o)if(/filter/.test(r.change.key)){var c=t.ref.images[t.ref.images.length-1];a(t,r.change.value,c.image)}else{if(/crop|markup|resize/.test(r.change.key)){var s=o.getMetadata("crop"),h=t.ref.images[t.ref.images.length-1];if(s&&s.aspectRatio&&h.crop&&h.crop.aspectRatio&&Math.abs(s.aspectRatio-h.crop.aspectRatio)>1e-5){var u=function(e){var t=e.root,i=t.ref.images.shift();return i.opacity=0,i.translateY=-15,t.ref.imageViewBin.push(i),i}({root:t});n({root:t,props:i,image:(l=u.image,(d=d||document.createElement("canvas")).width=l.width,d.height=l.height,d.getContext("2d").drawImage(l,0,0),d)})}else!function(e){var t=e.root,i=e.props,r=t.query("GET_ITEM",{id:i.id});if(r){var a=t.ref.images[t.ref.images.length-1];a.crop=r.getMetadata("crop"),a.background=t.query("GET_IMAGE_TRANSFORM_CANVAS_BACKGROUND_COLOR"),t.query("GET_IMAGE_PREVIEW_MARKUP_SHOW")&&(a.dirty=!0,a.resize=r.getMetadata("resize"),a.markup=r.getMetadata("markup"))}}({root:t,props:i})}var l,d}}},DID_THROW_ITEM_LOAD_ERROR:c,DID_THROW_ITEM_PROCESSING_ERROR:c,DID_THROW_ITEM_INVALID:c,DID_COMPLETE_ITEM_PROCESSING:function(e){var t=e.root;t.ref.overlayShadow.opacity=.25,t.ref.overlaySuccess.opacity=1},DID_START_ITEM_PROCESSING:o,DID_REVERT_ITEM_PROCESSING:o},function(e){var t=e.root,i=t.ref.imageViewBin.filter(function(e){return 0===e.opacity});t.ref.imageViewBin=t.ref.imageViewBin.filter(function(e){return e.opacity>0}),i.forEach(function(e){return function(e,t){e.removeChildView(t),t.image.width=1,t.image.height=1,t._destroy()}(t,e)}),i.length=0})})},H=function(e){var t=e.addFilter,i=e.utils,r=i.Type,a=i.createRoute,n=i.isFile,o=z(e);return t("CREATE_VIEW",function(e){var t=e.is,i=e.view,r=e.query;if(t("file")&&r("GET_ALLOW_IMAGE_PREVIEW")){var c=function(e){e.root.ref.shouldRescale=!0};i.registerWriter(a({DID_RESIZE_ROOT:c,DID_STOP_RESIZE:c,DID_LOAD_ITEM:function(e){var t=e.root,a=e.props.id,c=r("GET_ITEM",a);if(c&&n(c.file)&&!c.archived){var s=c.file;if(function(e){return/^image/.test(e.type)}(s)&&r("GET_IMAGE_PREVIEW_FILTER_ITEM")(c)){var h="createImageBitmap"in(window||{}),u=r("GET_IMAGE_PREVIEW_MAX_FILE_SIZE");if(!(!h&&u&&s.size>u)){t.ref.imagePreview=i.appendChildView(i.createChildView(o,{id:a}));var l=t.query("GET_IMAGE_PREVIEW_HEIGHT");l&&t.dispatch("DID_UPDATE_PANEL_HEIGHT",{id:c.id,height:l});var d=!h&&s.size>r("GET_IMAGE_PREVIEW_MAX_INSTANT_PREVIEW_FILE_SIZE");t.dispatch("DID_IMAGE_PREVIEW_CONTAINER_CREATE",{id:a},d)}}}},DID_IMAGE_PREVIEW_CALCULATE_SIZE:function(e){var t=e.root,i=e.action;t.ref.imageWidth=i.width,t.ref.imageHeight=i.height,t.ref.shouldRescale=!0,t.ref.shouldDrawPreview=!0,t.dispatch("KICK")},DID_UPDATE_ITEM_METADATA:function(e){var t=e.root;"crop"===e.action.change.key&&(t.ref.shouldRescale=!0)}},function(e){var t=e.root,i=e.props;t.ref.imagePreview&&(t.rect.element.hidden||(t.ref.shouldRescale&&(!function(e,t){if(e.ref.imagePreview){var i=t.id,r=e.query("GET_ITEM",{id:i});if(r){var a=e.query("GET_PANEL_ASPECT_RATIO"),n=e.query("GET_ITEM_PANEL_ASPECT_RATIO"),o=e.query("GET_IMAGE_PREVIEW_HEIGHT");if(!(a||n||o)){var c=e.ref,s=c.imageWidth,h=c.imageHeight;if(s&&h){var u=e.query("GET_IMAGE_PREVIEW_MIN_HEIGHT"),l=e.query("GET_IMAGE_PREVIEW_MAX_HEIGHT"),d=(r.getMetadata("exif")||{}).orientation||-1;if(d>=5&&d<=8){var f=[h,s];s=f[0],h=f[1]}if(!L(r.file)||e.query("GET_IMAGE_PREVIEW_UPSCALE")){var p=2048/s;s*=p,h*=p}var g=h/s,m=(r.getMetadata("crop")||{}).aspectRatio||g,y=Math.max(u,Math.min(h,l)),E=e.rect.element.width,v=Math.min(E*m,y);e.dispatch("DID_UPDATE_PANEL_HEIGHT",{id:r.id,height:v})}}}}}(t,i),t.ref.shouldRescale=!1),t.ref.shouldDrawPreview&&(requestAnimationFrame(function(){requestAnimationFrame(function(){t.dispatch("DID_FINISH_CALCULATE_PREVIEWSIZE",{id:i.id})})}),t.ref.shouldDrawPreview=!1)))}))}}),{options:{allowImagePreview:[!0,r.BOOLEAN],imagePreviewFilterItem:[function(){return!0},r.FUNCTION],imagePreviewHeight:[null,r.INT],imagePreviewMinHeight:[44,r.INT],imagePreviewMaxHeight:[256,r.INT],imagePreviewMaxFileSize:[null,r.INT],imagePreviewZoomFactor:[2,r.INT],imagePreviewUpscale:[!1,r.BOOLEAN],imagePreviewMaxInstantPreviewFileSize:[1e6,r.INT],imagePreviewTransparencyIndicator:[null,r.STRING],imagePreviewCalculateAverageImageColor:[!1,r.BOOLEAN],imagePreviewMarkupShow:[!0,r.BOOLEAN],imagePreviewMarkupFilter:[function(){return!0},r.FUNCTION]}}};return"undefined"!=typeof window&&void 0!==window.document&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:H})),H}); diff --git a/app/public/partage/fichiers-fragment.php b/app/public/partage/fichiers-fragment.php index 535a9c4..886a783 100644 --- a/app/public/partage/fichiers-fragment.php +++ b/app/public/partage/fichiers-fragment.php @@ -98,20 +98,8 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']); hx-trigger="change" hx-include="[name='formats[]'], [name='website_url'], [name='admin_mode'], [name='edit_mode'], [name='_cover']" hx-swap="outerHTML" - - hx-post="" - hx-target="#slot-video" - hx-select="#slot-video" - hx-trigger="change" - hx-include="[name='formats[]'], [name='website_url'], [name='admin_mode'], [name='edit_mode'], [name='_cover']" - hx-swap="outerHTML" - - hx-post="" - hx-target="#slot-audio" - hx-select="#slot-audio" - hx-trigger="change" - hx-include="[name='formats[]'], [name='website_url'], [name='admin_mode'], [name='edit_mode'], [name='_cover']" - > + +> @@ -206,7 +194,6 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
JPG, PNG ou WEBP. Format 4:3 recommandé. Max 20 MB. @@ -220,7 +207,6 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']);
> @@ -235,47 +221,30 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']); > - PDF (max 100 MB) · Images (JPG/PNG/GIF/WEBP) · Vidéo · Audio · VTT · Archives. + PDF (max 100 MB) · Images (max 500 MB) · Vidéo & Audio (max 2 GB) · VTT · Archives (max 500 MB). Glissez pour réordonner. PDFs trop lourds ? https://bentopdf.com/
- +
-
- -
- + +
- +
> + class="tfe-file-picker"> PDF ou archives ZIP/TAR. Max 500 MB. Glissez pour réordonner.
- -
@@ -296,7 +265,8 @@ $hasAnnexesChecked = !empty($_POST['has_annexes']); - + + + - + + + - - - diff --git a/app/public/partage/index.php b/app/public/partage/index.php index b1c984e..870686d 100644 --- a/app/public/partage/index.php +++ b/app/public/partage/index.php @@ -317,10 +317,29 @@ function renderShareLinkForm(string $slug, array $link): void $synopsisExtra = ob_get_clean(); // Jury data from repopulation - $juryPromoteur = old($formData, 'jury_promoteur'); + $juryPromoteur = null; $juryPromoteurs = []; - $juryPromoteurUlb = old($formData, 'jury_promoteur_ulb_name'); + $juryPromoteurUlb = null; $juryPromoteursUlb = []; + // promoteurices may be submitted as arrays (multiple entries) + $promoteursRaw = old($formData, 'jury_promoteur'); + if (is_array($promoteursRaw)) { + foreach ($promoteursRaw as $name) { + $name = trim($name ?? ''); + if ($name !== '') $juryPromoteurs[] = ['name' => $name]; + } + } elseif (is_string($promoteursRaw) && trim($promoteursRaw) !== '') { + $juryPromoteur = $promoteursRaw; + } + $promoteursUlbRaw = old($formData, 'jury_promoteur_ulb_name'); + if (is_array($promoteursUlbRaw)) { + foreach ($promoteursUlbRaw as $name) { + $name = trim($name ?? ''); + if ($name !== '') $juryPromoteursUlb[] = ['name' => $name]; + } + } elseif (is_string($promoteursUlbRaw) && trim($promoteursUlbRaw) !== '') { + $juryPromoteurUlb = $promoteursUlbRaw; + } $lecteursInternes = []; $lecteursExternes = []; for ($i = 0; $i < 10; $i++) { @@ -376,7 +395,12 @@ function renderShareLinkForm(string $slug, array $link): void + + + + + @@ -497,6 +521,12 @@ function handleShareLinkSubmission(string $slug): void $ctrl = ThesisCreateController::make(); $thesisId = $ctrl->submit($_POST, $_FILES); + // Collect file processing warnings (invalid types, too large, etc.) + $fileWarnings = $ctrl->getFileWarnings(); + if ($fileWarnings) { + $_SESSION['_flash_warning'] = implode("\n", $fileWarnings); + } + $identifier = $ctrl->getIdentifier($thesisId); $logger->logSubmission('partage', $thesisId, $identifier, $authorName, [ 'share_slug' => $slug, @@ -572,19 +602,23 @@ function handleShareLinkSubmission(string $slug): void /** * Helper to retrieve old form data (with support for array keys via : delimiter) */ -function old(array $data, string $key, string $default = ''): string { +/** + * Retrieve old form data for repopulation. + * Returns raw value (no escaping) — callers must htmlspecialchars() when rendering. + * For arrays, returns the array as-is so callers can iterate. + */ +function old(array $data, string $key, $default = '') { // Support nested keys like "jury_lecteurs:0" $parts = explode(':', $key); $value = $data; foreach ($parts as $part) { - if (is_array($value) && isset($value[$part])) { + if (is_array($value) && array_key_exists($part, $value)) { $value = $value[$part]; } else { - $value = $default; - break; + return $default; } } - return is_array($value) ? htmlspecialchars(json_encode($value)) : htmlspecialchars((string)$value); + return $value; } /** diff --git a/app/src/Controllers/ThesisCreateController.php b/app/src/Controllers/ThesisCreateController.php index 4c32069..a7004c2 100644 --- a/app/src/Controllers/ThesisCreateController.php +++ b/app/src/Controllers/ThesisCreateController.php @@ -521,15 +521,8 @@ class ThesisCreateController $exemplaireErg = !empty($post['exemplaire_erg']); $cc2r = !empty($post['cc2r']); - // Annexes validation: if has_annexes is checked, queue_file[annexe] must have at least one file + // Annexes are optional — no validation required $hasAnnexes = !empty($post['has_annexes']); - if (!$adminMode && $hasAnnexes) { - $queueAnnexes = $this->extractFilesSubArray($files['queue_file'] ?? [], 'annexe'); - $hasAnnexeFiles = is_array($queueAnnexes['name'] ?? null) && count(array_filter($queueAnnexes['name'])) > 0; - if (!$hasAnnexeFiles) { - throw new Exception('Veuillez fournir au moins un fichier d\'annexe.'); - } - } return compact( 'authorNames', diff --git a/app/src/Controllers/ThesisFileHandler.php b/app/src/Controllers/ThesisFileHandler.php index 7affc34..beff829 100644 --- a/app/src/Controllers/ThesisFileHandler.php +++ b/app/src/Controllers/ThesisFileHandler.php @@ -34,12 +34,18 @@ */ trait ThesisFileHandler { + /** @var string[] Warnings collected during file processing (e.g. invalid type, too large). */ + private array $fileWarnings = []; + /** 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 + /** Maximum allowed file size for video/audio files (bytes). */ + private const MAX_AV_SIZE = 2 * 1024 * 1024 * 1024; // 2 GB + /** Cover image max size. */ private const MAX_COVER_SIZE = 20 * 1024 * 1024; // 20 MB @@ -68,6 +74,15 @@ trait ThesisFileHandler // ── Public entry points (called by controllers) ────────────────────────── + /** + * Get warnings collected during file processing (invalid types, too large, etc.). + * @return string[] + */ + public function getFileWarnings(): array + { + return $this->fileWarnings; + } + /** * Process a cover image upload. * @@ -92,6 +107,7 @@ trait ThesisFileHandler if (!in_array($mimeType, $allowedMimes, true) || !in_array($ext, $allowedExts, true) || $upload['size'] > self::MAX_COVER_SIZE) { + $this->fileWarnings[] = "Couverture « {$upload['name']} » ignorée : format ou taille non accepté."; error_log("ThesisFileHandler: invalid cover MIME $mimeType / $ext / {$upload['size']} bytes, skipping"); return; } @@ -141,10 +157,12 @@ trait ThesisFileHandler $ext = strtolower(pathinfo($upload['name'], PATHINFO_EXTENSION)); if ($mimeType !== 'application/pdf' || $ext !== 'pdf') { + $this->fileWarnings[] = "Note d'intention « {$upload['name']} » ignorée : seul le format PDF est accepté."; error_log("ThesisFileHandler: invalid note d'intention MIME $mimeType, skipping"); return; } if ($upload['size'] > self::MAX_PDF_SIZE) { + $this->fileWarnings[] = "Note d'intention « {$upload['name']} » ignorée : fichier trop volumineux (max 100 MB)."; error_log("ThesisFileHandler: note d'intention too large ({$upload['size']} bytes), skipping"); return; } @@ -223,19 +241,25 @@ trait ThesisFileHandler $mimeType = 'text/vtt'; } if ($mimeType === 'application/octet-stream' && !in_array($ext, self::ALLOWED_EXTENSIONS, true)) { + $this->fileWarnings[] = "Fichier TFE « {$uploads['name'][$i]} » ignoré : format .$ext non accepté."; error_log("ThesisFileHandler: TFE extension not allowed {$uploads['name'][$i]} ($ext), skipping"); continue; } if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true) && !in_array($ext, self::ALLOWED_EXTENSIONS, true)) { + $this->fileWarnings[] = "Fichier TFE « {$uploads['name'][$i]} » ignoré : type non accepté ($mimeType)."; error_log("ThesisFileHandler: invalid TFE type {$uploads['name'][$i]} ($mimeType / $ext), skipping"); continue; } $isPdf = ($mimeType === 'application/pdf' || $ext === 'pdf'); - $sizeLimit = $isPdf ? self::MAX_PDF_SIZE : self::MAX_FILE_SIZE; + $isAv = preg_match('/^(video|audio)\//', $mimeType) || in_array($ext, ['mp4','webm','ogv','mov','mp3','ogg','oga','wav','flac','aac','m4a']); + $sizeLimit = $isPdf ? self::MAX_PDF_SIZE : ($isAv ? self::MAX_AV_SIZE : self::MAX_FILE_SIZE); if ($uploads['size'][$i] > $sizeLimit) { - error_log("ThesisFileHandler: TFE file too large {$uploads['name'][$i]} (" . round($uploads['size'][$i] / 1024 / 1024) . ' MB), skipping'); + $limitMb = round($sizeLimit / 1024 / 1024); + $sizeMb = round($uploads['size'][$i] / 1024 / 1024); + $this->fileWarnings[] = "Fichier TFE « {$uploads['name'][$i]} » ignoré : trop volumineux ($sizeMb MB, max $limitMb MB)."; + error_log("ThesisFileHandler: TFE file too large {$uploads['name'][$i]} (" . $sizeMb . ' MB), skipping'); continue; } @@ -364,19 +388,25 @@ trait ThesisFileHandler $mimeType = 'text/vtt'; } if ($mimeType === 'application/octet-stream' && !in_array($ext, self::ALLOWED_EXTENSIONS, true)) { + $this->fileWarnings[] = "Fichier « {$uploads['name'][$i]} » ignoré : format .$ext non accepté."; error_log("ThesisFileHandler: queue file extension not allowed {$uploads['name'][$i]} ($ext), skipping"); continue; } if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true) && !in_array($ext, self::ALLOWED_EXTENSIONS, true)) { + $this->fileWarnings[] = "Fichier « {$uploads['name'][$i]} » ignoré : type non accepté ($mimeType)."; error_log("ThesisFileHandler: invalid queue file type {$uploads['name'][$i]} ($mimeType / $ext), skipping"); continue; } $isPdf = ($mimeType === 'application/pdf' || $ext === 'pdf'); - $sizeLimit = $isPdf ? self::MAX_PDF_SIZE : self::MAX_FILE_SIZE; + $isAv = preg_match('/^(video|audio)\//', $mimeType) || in_array($ext, ['mp4','webm','ogv','mov','mp3','ogg','oga','wav','flac','aac','m4a']); + $sizeLimit = $isPdf ? self::MAX_PDF_SIZE : ($isAv ? self::MAX_AV_SIZE : self::MAX_FILE_SIZE); if ($uploads['size'][$i] > $sizeLimit) { - error_log("ThesisFileHandler: queue file too large {$uploads['name'][$i]} (" . round($uploads['size'][$i] / 1024 / 1024) . ' MB), skipping'); + $limitMb = round($sizeLimit / 1024 / 1024); + $sizeMb = round($uploads['size'][$i] / 1024 / 1024); + $this->fileWarnings[] = "Fichier « {$uploads['name'][$i]} » ignoré : trop volumineux ($sizeMb MB, max $limitMb MB)."; + error_log("ThesisFileHandler: queue file too large {$uploads['name'][$i]} (" . $sizeMb . ' MB), skipping'); continue; } @@ -461,6 +491,28 @@ trait ThesisFileHandler if ($mimeType === 'text/plain' && $ext === 'vtt') { $mimeType = 'text/vtt'; } + if ($mimeType === 'application/octet-stream' && !in_array($ext, self::ALLOWED_EXTENSIONS, true)) { + $this->fileWarnings[] = "Annexe « {$uploads['name'][$i]} » ignorée : format .$ext non accepté."; + error_log("ThesisFileHandler: queue annexe extension not allowed {$uploads['name'][$i]} ($ext), skipping"); + continue; + } + if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true) + && !in_array($ext, self::ALLOWED_EXTENSIONS, true)) { + $this->fileWarnings[] = "Annexe « {$uploads['name'][$i]} » ignorée : type non accepté ($mimeType)."; + error_log("ThesisFileHandler: invalid queue annexe type {$uploads['name'][$i]} ($mimeType / $ext), skipping"); + continue; + } + + // Annexes: PDF max 100 MB, everything else max 500 MB + $isPdf = ($mimeType === 'application/pdf' || $ext === 'pdf'); + $sizeLimit = $isPdf ? self::MAX_PDF_SIZE : self::MAX_FILE_SIZE; + if ($uploads['size'][$i] > $sizeLimit) { + $limitMb = round($sizeLimit / 1024 / 1024); + $sizeMb = round($uploads['size'][$i] / 1024 / 1024); + $this->fileWarnings[] = "Annexe « {$uploads['name'][$i]} » ignorée : trop volumineuse ($sizeMb MB, max $limitMb MB)."; + error_log("ThesisFileHandler: queue annexe too large {$uploads['name'][$i]} (" . $sizeMb . ' MB), skipping'); + continue; + } $padded = sprintf('%02d', $num); $targetName = $filePrefix . '_ANNEXE_' . $padded . '.' . $ext; @@ -530,19 +582,25 @@ trait ThesisFileHandler $mimeType = 'text/vtt'; } if ($mimeType === 'application/octet-stream' && !in_array($ext, self::ALLOWED_EXTENSIONS, true)) { + $this->fileWarnings[] = "Annexe « {$uploads['name'][$i]} » ignorée : format .$ext non accepté."; error_log("ThesisFileHandler: annexe extension not allowed {$uploads['name'][$i]} ($ext), skipping"); continue; } if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true) && !in_array($ext, self::ALLOWED_EXTENSIONS, true)) { + $this->fileWarnings[] = "Annexe « {$uploads['name'][$i]} » ignorée : type non accepté ($mimeType)."; error_log("ThesisFileHandler: invalid annexe type {$uploads['name'][$i]} ($mimeType / $ext), skipping"); continue; } $isPdf = ($mimeType === 'application/pdf' || $ext === 'pdf'); - $sizeLimit = $isPdf ? self::MAX_PDF_SIZE : self::MAX_FILE_SIZE; + $isAv = preg_match('/^(video|audio)\//', $mimeType) || in_array($ext, ['mp4','webm','ogv','mov','mp3','ogg','oga','wav','flac','aac','m4a']); + $sizeLimit = $isPdf ? self::MAX_PDF_SIZE : ($isAv ? self::MAX_AV_SIZE : self::MAX_FILE_SIZE); if ($uploads['size'][$i] > $sizeLimit) { - error_log("ThesisFileHandler: annexe too large {$uploads['name'][$i]} (" . round($uploads['size'][$i] / 1024 / 1024) . " MB), skipping"); + $limitMb = round($sizeLimit / 1024 / 1024); + $sizeMb = round($uploads['size'][$i] / 1024 / 1024); + $this->fileWarnings[] = "Annexe « {$uploads['name'][$i]} » ignorée : trop volumineuse ($sizeMb MB, max $limitMb MB)."; + error_log("ThesisFileHandler: annexe too large {$uploads['name'][$i]} (" . $sizeMb . " MB), skipping"); continue; } diff --git a/app/templates/admin/acces.php b/app/templates/admin/acces.php index efa3809..d792602 100644 --- a/app/templates/admin/acces.php +++ b/app/templates/admin/acces.php @@ -376,6 +376,97 @@ +%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +\\\\\\\ to: rptplqsr 5f9568bd "Add FilePond pools for couverture, note_intention, video, audio" (rebased revision) ++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: rptplqsr 5f9568bd "Add FilePond pools for couverture, note_intention, video, audio" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: wvkvvpmv 33919970 "fix annexe validation, FilePond type validation, and styling" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: wvkvvpmv 5044078a "fix annexe validation, FilePond type validation, and styling" (rebased revision) +++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: wvkvvpmv 5044078a "fix annexe validation, FilePond type validation, and styling" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: tqwpzqtq 83c9c5ff (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: tqwpzqtq b2fcb530 (rebased revision) +++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: tqwpzqtq b2fcb530 (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: stmuuwmv 679ece97 "add server-side video/audio size limits (2 GB) and fix missing annexe queue validation" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: stmuuwmv 45c8d894 "add server-side video/audio size limits (2 GB) and fix missing annexe queue validation" (rebased revision) +++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: stmuuwmv 45c8d894 "add server-side video/audio size limits (2 GB) and fix missing annexe queue validation" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: wmzntpxp b9fa7c04 "add filepond-plugin-file-validate-type + server-side file warnings" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: wmzntpxp aa25c473 "add filepond-plugin-file-validate-type + server-side file warnings" (rebased revision) +++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: wmzntpxp aa25c473 "add filepond-plugin-file-validate-type + server-side file warnings" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: rlqsxozn a2d3f72a "fix file validation: use extension-based check in beforeAddFile, drop plugin" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: rlqsxozn e69b00bf "fix file validation: use extension-based check in beforeAddFile, drop plugin" (rebased revision) +++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: rlqsxozn e69b00bf "fix file validation: use extension-based check in beforeAddFile, drop plugin" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: vqnonzxp 9b795ebc "remove accept attributes from FilePond inputs, rely on beforeAddFile validation" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: vqnonzxp fc15ae85 "remove accept attributes from FilePond inputs, rely on beforeAddFile validation" (rebased revision) +++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: vqnonzxp fc15ae85 "remove accept attributes from FilePond inputs, rely on beforeAddFile validation" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: xznsyswm ecbb6c5f "FilePond production hardening" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: xznsyswm a408afdc "FilePond production hardening" (rebased revision) +++ $linkName = $link['name'] ?? ''; ++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; ?> diff --git a/app/templates/public/tfe.php b/app/templates/public/tfe.php index 24d13ad..18bda9d 100644 --- a/app/templates/public/tfe.php +++ b/app/templates/public/tfe.php @@ -434,7 +434,6 @@ $isAudio = in_array($ext, ['mp3','ogg','oga','wav','flac','aac','m4a'], true) || $fileType === 'audio'; $isPdf = ($ext === 'pdf') || $fileType === 'main'; $isWebsite = ($fileType === 'website'); - $isPeerTube = ($isExternalUrl && str_contains($filePath, '/videos/watch/')); $isOther = !($isImage || $isVideo || $isAudio || $isPdf || $isWebsite); $_vttPath = null; @@ -477,22 +476,6 @@ <?= htmlspecialchars($caption !== '' ? $caption : $data['title'] . ' — ' . ($data['authors'] ?? '')) ?> - - -

- - Ouvrir dans PeerTube - (ouvre dans un nouvel onglet) - -

- - - - -

- - Ouvrir dans PeerTube - (ouvre dans un nouvel onglet) - -

- -
diff --git a/justfile b/justfile index 3cfed30..bf58195 100644 --- a/justfile +++ b/justfile @@ -62,7 +62,7 @@ deploy: --exclude 'var/logs/*' \ app/ xamxam:/var/www/xamxam/ ssh xamxam "mkdir -p /var/www/xamxam/var/{cache,logs,tmp}" - ssh xamxam "cd /var/www/xamxam && php migrations/run.php /var/www/xamxam/storage/xamxam.db" + ssh xamxam "cd /var/www/xamxam && php scripts/ensure-db.php /var/www/xamxam/storage/xamxam.db && php migrations/run.php /var/www/xamxam/storage/xamxam.db" # Sync .env separately (excluded above to avoid accidental overwrite on subsequent deploys) @just deploy-env diff --git a/pi-session-2026-05-10T18-42-37-234Z_019e1332-ce31-70fa-87a1-aa3495b526a9.html b/pi-session-2026-05-10T18-42-37-234Z_019e1332-ce31-70fa-87a1-aa3495b526a9.html new file mode 100644 index 0000000..f01e92a --- /dev/null +++ b/pi-session-2026-05-10T18-42-37-234Z_019e1332-ce31-70fa-87a1-aa3495b526a9.html @@ -0,0 +1,4113 @@ + + + + + + Session Export + + + + + +
+ + +
+
+
+
+
+ +
+
+ + + + + + + + + + + + + diff --git a/scripts/ensure-db.php b/scripts/ensure-db.php new file mode 100644 index 0000000..e0be656 --- /dev/null +++ b/scripts/ensure-db.php @@ -0,0 +1,29 @@ +#!/usr/bin/env php +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +$sql = file_get_contents($schemaPath); +$db->exec($sql); + +echo "Database created from schema: $dbPath\n";