Files
xamxam/docs/file-uploads.md

8.7 KiB

File Uploads

Reference for all file upload handling in XAMXAM: accepted types, size limits, storage layout, display behaviour, and ordering.


Upload surfaces

There are three forms where files can be uploaded:

Surface Path Who uses it
Admin — add thesis /admin/add.php Administrator
Admin — edit thesis /admin/edit.php?id=N Administrator
Student submission /partage/<slug> Student via share link

All three surfaces share the same backend controller logic (ThesisCreateController / ThesisEditController) and the same validation rules.


File categories

Each uploaded file is assigned a file_type that controls how it is displayed on the public TFE page.

file_type How displayed Trigger
main Inline <iframe> with download fallback .pdf extension
image <img> image/* MIME or image extension
video <video controls> with Range support video/* MIME or video extension
audio <audio controls> with Range support audio/* MIME or audio extension
caption Not displayed — paired with preceding video .vtt extension
cover Cover thumbnail (not shown in file loop) Separate couverture input
other Download link only, never rendered Everything else

Accepted file types

TFE content files (files[] input)

Documents

Extension MIME type Display
.pdf application/pdf Inline iframe

Images

Extension MIME type Display
.jpg / .jpeg image/jpeg <img>
.png image/png <img>
.gif image/gif <img>
.webp image/webp <img>

Video

Extension MIME type Display
.mp4 video/mp4 <video>
.webm video/webm <video>
.mov video/quicktime <video> (served as video/mp4)
.ogv video/ogg <video>

Audio

Extension MIME type Display
.mp3 audio/mpeg <audio>
.ogg / .oga audio/ogg <audio>
.wav audio/wav <audio>
.flac audio/flac <audio>
.aac audio/aac <audio>
.m4a audio/mp4 <audio>

Captions (WebVTT)

Extension MIME type Behaviour
.vtt text/vtt Silently paired with the preceding <video>. The N-th .vtt file is attached to the N-th video in display order. Not shown as a standalone item.

Archives and other downloadable files

Extension MIME type Display
.zip application/zip Download link
.tar application/x-tar Download link
.gz / .tgz application/gzip Download link
Any other extension application/octet-stream Download link

Files whose MIME type is application/octet-stream are accepted only if their extension is in the known list above. Unknown extensions with an unknown MIME type are rejected.

Cover image (couverture input)

Extension MIME type
.jpg / .jpeg image/jpeg
.png image/png

Max size: 20 MB.

Banner image (banner input)

Extension MIME type
.jpg / .jpeg image/jpeg
.png image/png
.webp image/webp

Landscape format recommended (4:1 ratio). Max size: 20 MB.


Size limits

Limit Value
Per-file limit (TFE content files) 500 MB
PHP upload_max_filesize 512 MB
PHP post_max_size 520 MB
Cover image 20 MB
Banner image 20 MB

The PHP limits are set in app/public/.htaccess (Apache) and app/public/.user.ini (PHP-FPM / nginx). For environments that require higher limits, edit those two files.


File ordering

Files are displayed on the public TFE page in sort_order sequence (ascending). The sort_order column is stored in thesis_files.

Setting order on upload (add / partage forms)

The file queue in the upload form is drag-sortable via SortableJS. Drag rows into the desired display order before submitting. The order is submitted as file_orders[] hidden fields and stored on insert.

Changing order after upload (edit form)

The existing-files list on the edit form is also drag-sortable. Drag rows into the desired order and save — the new order is submitted as file_sort_order[] (an array of file IDs in the desired sequence) and persisted via Database::reorderThesisFiles().


Per-file labels

Each TFE content file can have an optional display label (a short caption or description). This is shown as a <figcaption> beneath the file on the public page.

  • On the upload queue: type in the label field below each filename before submitting.
  • On the edit form: the label input is shown inline in each file row; edited labels are saved alongside the sort order.

Labels are stored in thesis_files.display_label. If blank, the field falls back to the legacy description column.


Storage layout

Files are stored outside the webroot in app/storage/.

app/storage/
├── covers/
│   └── <random-hex>.jpg          # cover images
├── banners/
│   └── <random-hex>.jpg          # home-page banners
└── theses/
    └── <year>/
        └── <YEAR>_<AUTHOR_SLUG>/
            └── <AUTHOR_SLUG>_<sanitized-filename>.<ext>
  • Author slug: uppercase ASCII, spaces → underscores, accents stripped (e.g. EMMA_RENARD).
  • Filename: same normalisation applied to the original filename.
  • If a folder <YEAR>_<AUTHOR_SLUG> already exists a numeric suffix is appended (_1, _2, …).
  • If a filename already exists in the folder a numeric suffix is appended before the extension.

Files are never served directly from disk. All access goes through MediaController (/media?path=…), which enforces:

  • Path traversal prevention (character whitelist + realpath() jail)
  • Visibility gate: access_type_id = 3 (Interdit) → HTTP 403
  • MIME allow-list check before serving

Security notes

  • MIME type is verified via finfo (magic bytes), not the browser-supplied Content-Type.
  • Extension is additionally checked against the allow-list as a second gate.
  • Filenames are sanitised (accents stripped, non-alphanumeric → _) before writing to disk; the original name is stored in thesis_files.file_name for display.
  • Cover and banner images are stored under a random 32-hex-char name, completely decoupled from the original filename.
  • Uploaded files are chmod 0644 after move.
  • HTTP Range requests are supported for audio and video so the browser can seek without downloading the entire file.

Database schema reference

CREATE TABLE thesis_files (
    id            INTEGER PRIMARY KEY AUTOINCREMENT,
    thesis_id     INTEGER NOT NULL,
    file_type     TEXT NOT NULL,     -- 'main'|'image'|'video'|'audio'|'caption'|'cover'|'other'
    file_path     TEXT NOT NULL,     -- path relative to STORAGE_ROOT
    file_name     TEXT NOT NULL,     -- original filename (display only)
    file_size     INTEGER,           -- bytes
    mime_type     TEXT,
    description   TEXT,              -- legacy caption field
    display_label TEXT,              -- per-file caption (migration 007)
    sort_order    INTEGER NOT NULL DEFAULT 0,  -- display order (migration 007)
    uploaded_at   DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE
);

Files are queried ordered by sort_order ASC, uploaded_at ASC.


Relevant source files

File Role
app/src/Controllers/ThesisCreateController.php Upload validation + storage on create
app/src/Controllers/ThesisEditController.php Upload validation + storage on edit; reorder + label save
app/src/Controllers/MediaController.php Secure file serving with Range support
app/src/Database.php insertThesisFile, reorderThesisFiles, updateThesisFileLabel, getThesisFiles
app/templates/partials/form/fieldset-files.php Upload UI partial (add / partage forms)
app/templates/admin/edit.php Edit-form files section (sortable existing files + new upload queue)
app/templates/public/tfe.php Public rendering of all file types
app/public/assets/js/file-upload-queue.js SortableJS-backed upload queue + legacy preview
app/public/.htaccess PHP upload limits (Apache)
app/public/.user.ini PHP upload limits (PHP-FPM / nginx)
app/migrations/applied/007_thesis_files_sort_and_label.sql DB migration adding sort_order + display_label