php -S (built-in dev server) ignores .htaccess and .user.ini entirely. The POST Content-Length limit was still 8M from /etc/php/php.ini. Pass upload_max_filesize=512M, post_max_size=520M, memory_limit=256M, max_execution_time=300, max_input_time=300 directly on the CLI.
8.9 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:
| File | Applies to |
|---|---|
app/public/.htaccess |
Apache (mod_php) |
app/public/.user.ini |
PHP-FPM / nginx |
justfile — serve recipe |
PHP built-in dev server (php -S ignores both files above, so limits are passed via -d flags) |
For environments that require different limits, edit all three.
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-suppliedContent-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 inthesis_files.file_namefor display. - Cover and banner images are stored under a random 32-hex-char name, completely decoupled from the original filename.
- Uploaded files are
chmod 0644after 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 |