docs: add file-uploads.md — accepted types, limits, storage, ordering, security

This commit is contained in:
Pontoporeia
2026-04-30 13:22:37 +02:00
parent a83dc1c74e
commit 6a37d21f3f
2 changed files with 224 additions and 0 deletions

223
docs/file-uploads.md Normal file
View File

@@ -0,0 +1,223 @@
# 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
```sql
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` |