# 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-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` |
