mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 01:53:03 +02:00
docs: verify and refactor documentation to match current codebase
This commit is contained in:
@@ -62,3 +62,6 @@ app/public/assets/dist/
|
||||
.phpunit.result.cache
|
||||
coverage/
|
||||
|
||||
|
||||
# td (TODO manager) local database
|
||||
.todo.sqlite
|
||||
|
||||
@@ -6,36 +6,26 @@ Répertoire des travaux de fin d'études de l'[ERG](https://erg.be) (École de R
|
||||
|
||||
## Requirements
|
||||
|
||||
- PHP 8.4
|
||||
- SQLite3 (`php8.4-sqlite3`)
|
||||
- PHP ≥ 8.4 (with `ext-json`, `ext-openssl`, `ext-pdo`, `ext-sqlite3`)
|
||||
- Composer
|
||||
- Node.js / npm
|
||||
- nginx (production)
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
### MacOS
|
||||
|
||||
Logiciels:
|
||||
|
||||
- un IDE pour éditer → VSCode
|
||||
- git (ou une interface graphique) pour partager les modifications → git-gui (officiel) ou Github Desktop
|
||||
- un server web avec PHP pour visualiser le project dans le navigateur → MAMP
|
||||
|
||||
|
||||
### Workflow
|
||||
|
||||
0. Faire un changement dans ton IDE
|
||||
1. Démarrer le site via MAMP, en sélectionnant le dossier `public`
|
||||
2. Vérifier que ça marche sur le site en local, depuis ton navigateur
|
||||
3. Une fois qu'un changement spécifique est fait, `commit` les changements sur les fichiers qui sont relatif à ce changement
|
||||
4. Vérifier que vous avez syncroniser avec le `remote` → `pull` + `rebase` ! pas merge
|
||||
5. `push` les changements vers le remote
|
||||
|
||||
|
||||
```bash
|
||||
just serve # http://localhost:8000 (public) and /admin/
|
||||
# one-time
|
||||
composer install
|
||||
npm ci
|
||||
|
||||
# run the dev server (builds assets, applies migrations, opens a browser)
|
||||
just dev # http://127.0.0.1:8000 (public) and /admin/
|
||||
just stop # stop it
|
||||
```
|
||||
|
||||
Live CSS/JS rebuilds happen automatically via a chokidar watcher while `just dev` runs.
|
||||
See [`docs/development.md`](docs/development.md) for the full workflow.
|
||||
|
||||
## Deployment
|
||||
|
||||
Files are pushed to the server with rsync — there is no repo on the remote.
|
||||
@@ -68,8 +58,11 @@ just deploy-nginx
|
||||
## Security notes
|
||||
|
||||
- Admin panel protected by PHP session (`AdminAuth`) — password-only, no username
|
||||
- Uploads stored outside webroot, served via controlled `media.php`
|
||||
- Rate limiting on public search (`src/RateLimit.php`)
|
||||
- Uploads stored outside webroot, served via controlled `/media` (`MediaController`)
|
||||
- Rate limiting on public search (`app/src/RateLimit.php`)
|
||||
- See `nginx/docs/SECURITY_HEADERS.md` for security headers reference
|
||||
|
||||
## More docs
|
||||
|
||||
See [`docs/README.md`](docs/README.md) for the full documentation index.
|
||||
|
||||
|
||||
+68
-124
@@ -1,145 +1,89 @@
|
||||
# Admin Panel Structure
|
||||
|
||||
This directory contains the admin panel for managing XAMXAM thesis database.
|
||||
This directory and `app/templates/admin/` contain the admin panel for managing
|
||||
the XAMXAM TFE database.
|
||||
|
||||
## Directory Structure
|
||||
## Entry points (`app/public/admin/`)
|
||||
|
||||
```
|
||||
public/admin/
|
||||
├── index.php # List all theses (main page)
|
||||
├── add.php # Add new thesis form
|
||||
├── edit.php # Edit existing thesis form
|
||||
├── import.php # CSV import form
|
||||
├── recapitulatif.php # Recap page after submission
|
||||
├── actions/ # Backend processing scripts (no HTML output)
|
||||
│ ├── formulaire.php # Process thesis submission from add.php
|
||||
│ └── publish.php # Toggle publish/unpublish status
|
||||
├── inc/ # Shared templates
|
||||
│ ├── head.php # HTML head, CSS, navigation
|
||||
│ └── footer.php # HTML footer
|
||||
└── data/ # Upload directory (not in git)
|
||||
├── theses/ # PDF files
|
||||
└── covers/ # Cover images
|
||||
```
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `index.php` | List all theses (main page; hosts the inline CSV import + tabs for list/trash) |
|
||||
| `add.php` | Add new thesis form |
|
||||
| `edit.php` | Existing thesis form |
|
||||
| `recapitulatif.php` | Post-submission recap |
|
||||
| `cleanup.php` | Orphaned-draft / storage cleanup |
|
||||
| `system.php` | System dashboard (logs, SMTP/PeerTube status) |
|
||||
| `contenus.php` | Editable content (pages, contacts) |
|
||||
| `contenus-edit.php` | Edit a content page |
|
||||
| `acces.php` | Share-link management |
|
||||
| `file-access.php` | Restricted-file access requests |
|
||||
| `account.php` | Admin account / password |
|
||||
| `login.php` | Login (session) |
|
||||
| `import.php` | Redirects to `/admin/` (CSV import is inline in `index.php`) |
|
||||
| `status.php`, `markdown-cheatsheet-fragment.php`, `*fragment.php` | HTMX fragments / helpers |
|
||||
|
||||
## File Types
|
||||
### Backend actions (`app/public/admin/actions/`)
|
||||
|
||||
### User-Facing Templates (Root Directory)
|
||||
Files that display HTML to users:
|
||||
- **index.php** - Lists all theses with filters and bulk actions
|
||||
- **add.php** - Form to add a new thesis
|
||||
- **edit.php** - Form to edit an existing thesis
|
||||
- **import.php** - CSV import interface
|
||||
- **recapitulatif.php** - Success confirmation page
|
||||
Process forms and redirect (no HTML output):
|
||||
|
||||
### Backend Scripts (actions/)
|
||||
Files that process forms and redirect (no HTML output):
|
||||
- **formulaire.php** - Processes thesis submission from add.php
|
||||
- **publish.php** - Handles publish/unpublish actions
|
||||
|
||||
### Shared Templates (inc/)
|
||||
Reusable HTML components:
|
||||
- **head.php** - HTML head, CSS links, navigation menu
|
||||
- **footer.php** - HTML footer
|
||||
|
||||
## Workflow
|
||||
|
||||
### Adding a Thesis
|
||||
1. User visits `add.php` (displays form)
|
||||
2. User submits form to `actions/formulaire.php` (processes data)
|
||||
3. On success, redirects to `recapitulatif.php?id=123`
|
||||
4. On error, redirects back to `add.php` with error message
|
||||
|
||||
### Publishing/Unpublishing
|
||||
1. User clicks publish/unpublish button in `index.php`
|
||||
2. Form submits to `actions/publish.php` (processes action)
|
||||
3. Redirects back to `index.php` with success/error message
|
||||
|
||||
## Security
|
||||
|
||||
- All pages require HTTP Basic Auth (configured in nginx) — primary layer
|
||||
- All pages require PHP session auth (`AdminAuth::requireLogin()`) — defence-in-depth
|
||||
- CSRF tokens protect all forms
|
||||
- File uploads validated and sanitized
|
||||
- Database queries use prepared statements
|
||||
- Upload directory outside public/ in production
|
||||
|
||||
See `nginx/PHP_AUTH_LAYER.md` for details on the dual-auth architecture.
|
||||
- `formulaire.php` — thesis create submission (`ThesisCreateController::submit()`)
|
||||
- `edit.php` — thesis edit submission (`ThesisEditController::save()`)
|
||||
- `export-csv.php`, `export-db.php`, `export-files.php` — see `docs/export.md`
|
||||
- `filepond/` — FilePond async upload endpoints
|
||||
- many others: `publish`, `delete`, `corbeille` (trash), `draft`, `visibility`,
|
||||
`tag`, `language`, `form-help*`, `page`, `apropos`, `smtp-test`,
|
||||
`peertube-*`, `maintenance`, `settings`, `account`, `access-request`,
|
||||
`acces-etudiante`, `cleanup-*`
|
||||
|
||||
## Templates
|
||||
|
||||
The `inc/` folder contains shared templates:
|
||||
- `head.php` - Included at the top of each page (DOCTYPE, CSS, nav)
|
||||
- `footer.php` - Included at the bottom of each page (closing tags)
|
||||
View templates live under `app/templates/admin/` (not in `public/`):
|
||||
- `app/templates/admin/*.php` — page layouts
|
||||
- `app/templates/admin/partials/` — shared fragments (toasts, dialogs, toc, …)
|
||||
|
||||
The public/partage and form partials live in `app/templates/partials/` and
|
||||
`app/templates/partage/`.
|
||||
|
||||
## Auth
|
||||
|
||||
- **PHP session auth** (`src/AdminAuth.php`) via `AdminAuth::requireLogin()` is
|
||||
the only authentication layer. The old nginx `auth_basic` layer has been
|
||||
removed — see `docs/security.md` and `nginx/docs/PHP_AUTH_LAYER.md`.
|
||||
- All forms include a CSRF token from `$_SESSION['csrf_token']`.
|
||||
- Inputs use PDO prepared statements; uploads validated and stored outside the
|
||||
webroot (`app/storage/`).
|
||||
|
||||
## Bootstrap / routing
|
||||
|
||||
Entry pages bootstrap the app and set up the environment:
|
||||
|
||||
Usage:
|
||||
```php
|
||||
<?php include "inc/head.php" ?>
|
||||
<!-- Page content here -->
|
||||
<?php include "inc/footer.php" ?>
|
||||
```
|
||||
|
||||
## URL Structure
|
||||
|
||||
- `/admin/` - List theses (index.php)
|
||||
- `/admin/add.php` - Add new thesis
|
||||
- `/admin/edit.php?id=123` - Edit thesis #123
|
||||
- `/admin/import.php` - Import CSV
|
||||
- `/admin/recapitulatif.php?id=123` - Recap page
|
||||
|
||||
Backend actions (not directly accessed):
|
||||
- `/admin/actions/formulaire.php` - Form processor
|
||||
- `/admin/actions/publish.php` - Publish toggle
|
||||
|
||||
## Development
|
||||
|
||||
### Adding a New Page
|
||||
|
||||
1. Create the template in `/admin/yourpage.php`:
|
||||
```php
|
||||
<?php
|
||||
require_once __DIR__ . "/../../config/bootstrap.php";
|
||||
require_once __DIR__ . '/../../lib/AdminAuth.php';
|
||||
require_once __DIR__ . '/../../bootstrap.php'; // defines APP_ROOT, autoload, config
|
||||
require_once APP_ROOT . '/src/AdminAuth.php';
|
||||
AdminAuth::requireLogin();
|
||||
$pageTitle = "Your Page Title";
|
||||
?>
|
||||
<?php include "inc/head.php" ?>
|
||||
|
||||
<!-- Your content here -->
|
||||
|
||||
<?php include "inc/footer.php" ?>
|
||||
```
|
||||
|
||||
2. Add navigation link in `inc/head.php` if needed
|
||||
`APP_ROOT` is the `app/` directory. Database access is via
|
||||
`app/src/Database.php`; form logic lives in `app/src/Controllers/` and
|
||||
`app/src/Form/`.
|
||||
|
||||
### Adding a New Action
|
||||
## URL structure
|
||||
|
||||
1. Create the script in `/admin/actions/youraction.php`:
|
||||
```php
|
||||
<?php
|
||||
require_once __DIR__ . "/../../config/bootstrap.php";
|
||||
require_once __DIR__ . '/../../lib/AdminAuth.php';
|
||||
AdminAuth::requireLogin();
|
||||
- `/admin/` — list theses (index.php)
|
||||
- `/admin/add.php` — add thesis
|
||||
- `/admin/edit.php?id=N` — edit thesis
|
||||
- `/admin/cleanup.php`, `/admin/system.php`, `/admin/acces.php`, etc.
|
||||
- `/admin/actions/…` — backend processors
|
||||
|
||||
// Verify CSRF token
|
||||
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
|
||||
$_SESSION['error'] = "Security error";
|
||||
header('Location: ../index.php');
|
||||
exit;
|
||||
}
|
||||
## Development guide
|
||||
|
||||
// Process action...
|
||||
**Add a page:** create `app/templates/admin/yourpage.php`, add a thin
|
||||
`app/public/admin/yourpage.php` entry that bootstraps + requires the template,
|
||||
and add navigation in `app/templates/admin/partials/admin-toc.php`.
|
||||
|
||||
// Redirect
|
||||
header('Location: ../yourpage.php');
|
||||
exit;
|
||||
```
|
||||
**Add an action:** create `app/public/admin/actions/youraction.php` that
|
||||
bootstraps, requires login, verifies the CSRF token, performs the work, and
|
||||
redirects back to the referring admin page.
|
||||
|
||||
2. Create form in template that posts to `actions/youraction.php`
|
||||
|
||||
## Notes
|
||||
|
||||
- Bootstrap path from actions/: `__DIR__ . "/../../config/bootstrap.php"`
|
||||
- Redirects from actions/: use `../` prefix (e.g., `../index.php`)
|
||||
- Database class: `require_once __DIR__ . '/../../lib/Database.php'`
|
||||
- All forms must include CSRF token from `$_SESSION['csrf_token']`
|
||||
See `docs/development.md` for the general workflow (dev server, builds,
|
||||
tests, linting).
|
||||
|
||||
+25
-196
@@ -1,206 +1,35 @@
|
||||
# Database Documentation
|
||||
# Database documentation
|
||||
|
||||
Complete documentation for the XAMXAM thesis database.
|
||||
XAMXAM stores its data in a SQLite database. See also the top-level
|
||||
[`docs/database.md`](../../docs/database.md).
|
||||
|
||||
## 📚 Available Documentation
|
||||
## Quick start
|
||||
|
||||
### 1. **[DATABASE_SPECIFICATION.md](DATABASE_SPECIFICATION.md)** ⭐
|
||||
**Complete technical specification** - 25KB comprehensive document
|
||||
- **Database file:** `xamxam.db`
|
||||
- **Schema (baseline + seed):** `schema.sql`
|
||||
- **Migrations:** applied set under `app/migrations/applied/` (run via `just migrate`)
|
||||
|
||||
**Contents:**
|
||||
- Complete table definitions with all columns
|
||||
- Entity relationship diagrams
|
||||
- Junction table specifications
|
||||
- Lookup table values
|
||||
- Business rules and workflows
|
||||
- Sample queries and use cases
|
||||
- Instructions for requesting schema changes
|
||||
|
||||
**Use when:** You need complete technical details about the database structure.
|
||||
|
||||
---
|
||||
|
||||
### 2. **[QUICK_SCHEMA_REFERENCE.md](QUICK_SCHEMA_REFERENCE.md)** 🚀
|
||||
**Quick reference guide** - 5KB at-a-glance reference
|
||||
|
||||
**Contents:**
|
||||
- Table summary
|
||||
- Key relationships diagram
|
||||
- Core fields reference
|
||||
- Predefined lookup values
|
||||
- Common SQL queries
|
||||
- Constraint summary
|
||||
|
||||
**Use when:** You need quick lookup or common query examples.
|
||||
|
||||
---
|
||||
|
||||
### 3. **[schema.sql](schema.sql)** 💾
|
||||
**The actual SQL schema** - Executable SQL file
|
||||
|
||||
**Contents:**
|
||||
- Complete CREATE TABLE statements
|
||||
- Indexes and triggers
|
||||
- Predefined data (orientations, AP programs, etc.)
|
||||
- Views for common queries
|
||||
|
||||
**Use when:** Setting up or resetting the database.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### View Database Schema
|
||||
```bash
|
||||
# Read the quick reference
|
||||
cat database/QUICK_SCHEMA_REFERENCE.md
|
||||
|
||||
# Or full specification
|
||||
cat database/DATABASE_SPECIFICATION.md
|
||||
just query # open an interactive sqlite3 shell on xamxam.db
|
||||
just init-db # (re)create xamxam.db from schema.sql
|
||||
just reset-db # rm xamxam.db + init-db
|
||||
just migrate # run pending migrations
|
||||
```
|
||||
|
||||
### Initialize Database
|
||||
```bash
|
||||
# Create test database from schema
|
||||
just init-test-db
|
||||
## Files here
|
||||
|
||||
# Create with sample data
|
||||
just create-fixtures
|
||||
```
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `xamxam.db` | The live SQLite database |
|
||||
| `schema.sql` | Full, fully-migrated schema + seed data (regenerated from the local DB) |
|
||||
| `backups/` | `just backup-snapshot` hot-backups (`*.db.gz`) |
|
||||
| `cache/` | Runtime cache (rate limits, etc.) |
|
||||
| `logs/` | Runtime logs (admin, audit) — **outside the webroot** |
|
||||
| `covers/` | Cover images |
|
||||
| `theses/`, `tfe/`, `tmp/` | Uploaded files / staging |
|
||||
|
||||
### Query Database
|
||||
```bash
|
||||
# Open SQLite prompt
|
||||
just query-db
|
||||
## See also
|
||||
|
||||
# Show specific thesis
|
||||
just show-thesis 42
|
||||
```
|
||||
|
||||
## 📝 Making Schema Changes
|
||||
|
||||
### Step 1: Document Your Request
|
||||
|
||||
Format:
|
||||
```
|
||||
**Table:** [table_name]
|
||||
**Change Type:** [add/modify/remove]
|
||||
**What:** [description]
|
||||
**Why:** [reason/use case]
|
||||
**Example Data:** [samples]
|
||||
```
|
||||
|
||||
### Step 2: Specify Details
|
||||
|
||||
For **new columns**:
|
||||
- Column name
|
||||
- Data type (TEXT, INTEGER, BOOLEAN, DATETIME)
|
||||
- NULL/NOT NULL
|
||||
- Default value
|
||||
- Indexes needed?
|
||||
|
||||
For **new tables**:
|
||||
- Table name
|
||||
- All columns
|
||||
- Relationships to existing tables
|
||||
- Sample data
|
||||
|
||||
### Step 3: Provide Context
|
||||
|
||||
Include:
|
||||
- Use case scenario
|
||||
- Who will use it?
|
||||
- How will it be displayed?
|
||||
- Any constraints?
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
**Table:** theses
|
||||
**Change Type:** add column
|
||||
**What:** Add column to track if thesis won an award
|
||||
**Why:** Need to highlight award-winning theses on homepage
|
||||
**Column Name:** has_award
|
||||
**Data Type:** BOOLEAN
|
||||
**Default:** 0 (false)
|
||||
**Example:** 1 for "Prix du Jury 2025" winner
|
||||
```
|
||||
|
||||
## 🗂️ Database Structure Overview
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ theses │ ◄── Main table (500+ records/year)
|
||||
└──────┬──────┘
|
||||
│
|
||||
├──► authors (via thesis_authors)
|
||||
├──► supervisors (via thesis_supervisors)
|
||||
├──► keywords (via thesis_keywords)
|
||||
├──► languages (via thesis_languages)
|
||||
├──► formats (via thesis_formats)
|
||||
├──► thesis_files (attachments)
|
||||
│
|
||||
└──► Lookup tables:
|
||||
• orientations
|
||||
• ap_programs
|
||||
• finality_types
|
||||
• access_types
|
||||
• license_types
|
||||
```
|
||||
|
||||
## 📊 Key Statistics
|
||||
|
||||
- **Core tables:** 3 (theses, authors, supervisors)
|
||||
- **Junction tables:** 5 (many-to-many relationships)
|
||||
- **Lookup tables:** 7 (predefined values)
|
||||
- **Support tables:** 2 (files, pages)
|
||||
- **Views:** 2 (full data, public only)
|
||||
- **Indexes:** 11 (for performance)
|
||||
- **Triggers:** 4 (auto-update timestamps)
|
||||
|
||||
## 🔍 Common Scenarios
|
||||
|
||||
### Scenario 1: Student Submits Thesis
|
||||
1. Create record in `theses` (is_published=0)
|
||||
2. Add author to `authors`, link via `thesis_authors`
|
||||
3. Add supervisor(s) to `supervisors`, link via `thesis_supervisors`
|
||||
4. Set `orientation_id`, `ap_program_id`, `finality_id`
|
||||
5. Upload file to `thesis_files`
|
||||
6. Add keywords via `thesis_keywords`
|
||||
7. Set `submitted_at` timestamp
|
||||
|
||||
### Scenario 2: Admin Publishes Thesis
|
||||
1. Verify all required fields present
|
||||
2. Set `defense_date`
|
||||
3. Set `jury_points`
|
||||
4. Optional: add `context_note`
|
||||
5. Set `is_published = 1`
|
||||
6. Set `published_at = CURRENT_TIMESTAMP`
|
||||
|
||||
### Scenario 3: Public User Searches
|
||||
Query `v_theses_public` view with filters:
|
||||
- By year
|
||||
- By orientation
|
||||
- By keyword
|
||||
- By author name
|
||||
- Full-text search in title/synopsis
|
||||
|
||||
## 🛠️ Development Workflow
|
||||
|
||||
### Local Development
|
||||
1. Use `xamxam.db` for development
|
||||
2. Create via `just init-db`
|
||||
3. Test queries before deployment
|
||||
|
||||
## 📞 Need Help?
|
||||
|
||||
1. **Quick lookup** → Read `QUICK_SCHEMA_REFERENCE.md`
|
||||
2. **Complete details** → Read `DATABASE_SPECIFICATION.md`
|
||||
3. **Schema changes** → Follow format in this README
|
||||
4. **SQL examples** → Check `QUICK_SCHEMA_REFERENCE.md`
|
||||
|
||||
## 🔗 Related Documentation
|
||||
|
||||
- [Deployment Guide](../nginx/DEPLOYMENT_COMPLETE.md)
|
||||
- [Repository Structure](../REPOSITORY_STRUCTURE_ANALYSIS.md)
|
||||
- [Test Database Guide](../nginx/TEST_DATABASE_SETUP.md)
|
||||
- [`docs/database.md`](../../docs/database.md) — schema reference, tables, common SQL
|
||||
- [`docs/import.md`](../../docs/import.md) — CSV import format
|
||||
- [`docs/export.md`](../../docs/export.md) — CSV / DB / files export + restore
|
||||
|
||||
+26
-14
@@ -1,21 +1,33 @@
|
||||
# CSS Architecture
|
||||
|
||||
## File Structure
|
||||
## Source files (`app/public/assets/css/`)
|
||||
|
||||
- **variables.css** — all CSS custom properties (single source of truth for every color/token)
|
||||
- **common.css** — reset, header/nav, search bar, accessibility utilities (loaded on all pages)
|
||||
- **main.css** — home page
|
||||
- **search.css** — search/directory page
|
||||
- **tfe.css** — individual thesis page
|
||||
- **apropos.css** — about + licence pages
|
||||
- **system.css** — admin system dashboard
|
||||
- **admin.css** — admin section (loaded alongside `common.css` on every admin page)
|
||||
- **modern-normalize.min.css** — third-party reset (minified, do not edit)
|
||||
- **`variables.css`** — all CSS custom properties (colors, spacing, sizing tokens)
|
||||
- **`colors.css`** — colour tokens (referenced by `variables.css`)
|
||||
- **`reset.css`** / **`modern-normalize.min.css`** — resets
|
||||
- **`base.css`** — base element styles
|
||||
- **`common.css`** — shared components (header/nav, search bar, accessibility, buttons)
|
||||
- **`typography.css`** — type scale
|
||||
- **`utilities.css`** — utility classes and spacing tokens
|
||||
- **`style.css`** — main public entry point (via `@import` chain)
|
||||
- Page-specific: **`public.css`** (home), **`tfe.css`**, **`repertoire.css`**, **`content-page.css`** (about/licences), **`system.css`** (admin system dashboard), **`file-access.css`**, **`admin.css`** (admin section)
|
||||
- Form: **`form-base.css`** + **`form-admin.css`** (plus FilePond vendor CSS)
|
||||
|
||||
## Build / bundling
|
||||
|
||||
Source CSS is **bundled and minified** by `scripts/build-css.mjs` (lightningcss)
|
||||
into `app/public/assets/dist/*.min.css`:
|
||||
|
||||
- `base.min.css` — resolves the `@import` chain in `style.css` into one file (eliminates ~17 sequential imports)
|
||||
- `admin.min.css` — minified `admin.css`
|
||||
- `form.min.css` — `form-base.css` + `form-admin.css` + FilePond vendor CSS
|
||||
- plus individual `.min.css` for the page-specific files
|
||||
|
||||
Run with `just build-css` / `just dev-build`. Never commit hand-edits into
|
||||
`dist/` — they are generated.
|
||||
|
||||
## Rules
|
||||
|
||||
- Every color value lives in `variables.css` as a CSS custom property.
|
||||
- No hardcoded hex, rgb(), or rgba() in any other file.
|
||||
- All files `@import url("./variables.css")` at the top.
|
||||
- Colour values live in `variables.css`/`colors.css` as custom properties; avoid hardcoding hex/rgb in other files.
|
||||
- Admin and public share the same token names — no separate admin theme.
|
||||
- No dark-mode media query. System page uses the same light tokens as the rest of the admin section.
|
||||
- `modern-normalize.min.css` and FilePond vendor CSS are third-party (minified — do not edit).
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# Documentation index
|
||||
|
||||
This directory mixes **current reference** docs, **proposals/plans**, and
|
||||
**historical/archived** analysis. Use this index to find the right document.
|
||||
|
||||
> **Note on naming:** XAMXAM was previously *Post-ERG* (and the code was once
|
||||
> organised under `posterg-website/`, `apps/`, `front-backend/`,
|
||||
> `/var/www/posterg/`). References to `posterg`, `apps/public/inc/header.php`,
|
||||
> `posterg.db`, `just serve`, `php-live-reload`, `tests/run-tests.php`, etc. in
|
||||
> historical docs reflect the old layout. The live paths are now `app/`,
|
||||
> `app/public/`, `app/storage/xamxam.db`, `just dev`, PHPUnit, and
|
||||
> `/var/www/xamxam/` on the server.
|
||||
|
||||
## Current reference (kept up to date)
|
||||
|
||||
| Doc | Contents |
|
||||
|-----|----------|
|
||||
| [development.md](development.md) | Dev workflow, structure, builds, testing, linting |
|
||||
| [deployment.md](deployment.md) | Server setup, deploy, backups, rollback |
|
||||
| [database.md](database.md) | SQLite schema, migrations, tables, common ops |
|
||||
| [search.md](search.md) | `/search` and `/repertoire` behaviour |
|
||||
| [export.md](export.md) | CSV / DB / files export + full restore procedure |
|
||||
| [import.md](import.md) | CSV import format + behaviour |
|
||||
| [security.md](security.md) | Current security posture |
|
||||
| [file-uploads.md](file-uploads.md) | Upload surfaces, types, storage layout |
|
||||
| [CSS.md](CSS.md) | CSS architecture + build |
|
||||
| [bookmarklet.md](bookmarklet.md) | Form auto-fill test helper |
|
||||
|
||||
## Proposals / plans (proposed, not all implemented)
|
||||
|
||||
| Doc | Status |
|
||||
|-----|--------|
|
||||
| [LDAP_AUTH_PLAN.md](LDAP_AUTH_PLAN.md) + [LDAP_SPEC.md](LDAP_SPEC.md) | LDAP login — **not implemented** |
|
||||
| [monolog-plan.md](monolog-plan.md) | Single Monolog logger replacing AppLogger/AdminLogger/ErrorHandler/Audit — **plan** |
|
||||
| [de-librairisation.md](de-librairisation.md) | Replace bespoke SMTP/Markdown/HTTP/crypto with libraries (partly done) |
|
||||
| [refactoring.md](refactoring.md) | Older refactoring proposal |
|
||||
| [ANALYSIS_STRUCTURE_REORG.md](ANALYSIS_STRUCTURE_REORG.md) | Proposed structure reorg |
|
||||
| [ANALYSIS_INLINE_JS_CSS_MINIFY.md](ANALYSIS_INLINE_JS_CSS_MINIFY.md) | Inline JS/CSS/minify analysis (largely actioned by the build system) |
|
||||
| [backup-plan.md](backup-plan.md) | Backup plan — largely implemented (see deployment.md) |
|
||||
| [repertoire-mobile-propositions.md](repertoire-mobile-propositions.md) | Mobile repertoire UI proposals |
|
||||
| [cms-migration-plan.html](cms-migration-plan.html) | CMS migration plan |
|
||||
| [spec-sheet.md](spec-sheet.md) | Original requirements fiche technique |
|
||||
| `Proposition procédure licences_V2.pdf` | Licence procedure proposal (filename contains non-ASCII/combining chars) |
|
||||
|
||||
## Historical / archived (kept for context — may reference the old codebase)
|
||||
|
||||
| Doc | Contents |
|
||||
|-----|----------|
|
||||
| [LIVRAISONS_PAR_MOIS.md](LIVRAISONS_PAR_MOIS.md) | Monthly delivery log grouped by functional family |
|
||||
| [migration-history.md](migration-history.md) | History of major structural migrations |
|
||||
| [CURRENT_ISSUES.md](CURRENT_ISSUES.md) | Issue log (2026-05-10) — many since resolved |
|
||||
| [IMMEDIATE_FIX.md](IMMEDIATE_FIX.md) | One-off fix note |
|
||||
| [EVIDENCE_SUMMARY.md](EVIDENCE_SUMMARY.md) + [VM_Crash_*.md](VM_Crash_Analysis_FINAL.md) | VM crash investigation (concluded: not the app) |
|
||||
| [css.md](css.md) | Old Bulma-removal writeup (pre-dates current build system; see CSS.md) |
|
||||
| [php-vs-flask.md](php-vs-flask.md) | Language choice decision |
|
||||
| [orm-assessment.md](orm-assessment.md) | ORM evaluation |
|
||||
| [system-setup.md](system-setup.md) | Old setup notes |
|
||||
| [SETUP.md](SETUP.md), [SPECS.md](SPECS.md), [TODO.md](TODO.md) | Earlier setup/spec/todo snapshots |
|
||||
| [SMTP_550_POSTFIX_FIX.md](SMTP_550_POSTFIX_FIX.md) | SMTP troubleshooting record |
|
||||
| [testing.md](testing.md) | PHP testing best-practices writeup |
|
||||
| [test-plan.md](test-plan.md) | Manual test plan |
|
||||
| [autosave-system.md](autosave-system.md), [filepond-crash-analysis.md](filepond-crash-analysis.md), [filepond-race-investigation.md](filepond-race-investigation.md) | Feature/issue deep-dives (status noted in each file) |
|
||||
| [pi-session-2026-05-10T*.html](pi-session-2026-05-10T18-42-37-234Z_019e1332-ce31-70fa-87a1-aa3495b526a9.html) | Captured session log |
|
||||
| [ANALYSIS_INLINE_JS_CSS_MINIFY.md](ANALYSIS_INLINE_JS_CSS_MINIFY.md) | *(see proposals)* |
|
||||
| [bookmarklet.md](bookmarklet.md) | *(kept current — see above)* |
|
||||
|
||||
## Related documentation elsewhere
|
||||
|
||||
- `nginx/docs/` — nginx config, security headers, deployment, HTACCESS→nginx
|
||||
- `app/storage/README.md` — DB quick-reference (schema link)
|
||||
- `app/public/admin/README.md` — admin panel structure
|
||||
|
||||
---
|
||||
|
||||
**Maintenance guidance:** when updating code, update the matching *current
|
||||
reference* doc in the table above. Leave *historical* docs untouched (they are
|
||||
read-only context). Move newly-written analysis into the appropriate section.
|
||||
+82
-86
@@ -1,111 +1,107 @@
|
||||
# Bookmarklet
|
||||
# Bookmarklet — auto-fill test form
|
||||
|
||||
## Auto-fill TFE form (testing)
|
||||
A drag-to-bookmarks helper that pre-fills an XAMXAM thesis form with dummy data
|
||||
so a submit can be tested quickly.
|
||||
|
||||
Drag the link below to your bookmarks bar, open `/admin/add.php?mode=student`, then click it. Every field gets filled with dummy data so you can hit submit immediately.
|
||||
> The old `?mode=student` flag and the standalone student form no longer exist.
|
||||
> Student submissions now use **share links** at `/partage/<slug>` (see the
|
||||
> `share_links` table). The same form template (`app/templates/partials/form/`)
|
||||
> drives admin add/edit and the partage form, so the field names below mostly
|
||||
> match. The bookmarklet below targets the **admin** add form
|
||||
> (`/admin/add.php`).
|
||||
|
||||
### Bookmarklet
|
||||
## Bookmarklet
|
||||
|
||||
**[Remplir TFE](javascript:(function()%7Bvar%20f%3Ddocument.querySelector('form.admin-form')%3Bif(!f)return%3Bvar%20set%3Dfunction(n%2Cv)%7Bvar%20e%3Df.querySelector('[name%3D%22'%2Bn%2B'%22]')%3Bif(e)%7Be.value%3Dv%3Be.dispatchEvent(new%20Event('change'%2C%7Bbubbles%3Atrue%7D))%3B%7D%7D%3Bvar%20check%3Dfunction(n)%7Bvar%20e%3Df.querySelector('[name%3D%22'%2Bn%2B'%22]')%3Bif(e)%7Be.checked%3Dtrue%3Be.dispatchEvent(new%20Event('change'%2C%7Bbubbles%3Atrue%7D))%3B%7D%7D%3Bvar%20checkCB%3Dfunction(n%2Cv)%7Bvar%20e%3Df.querySelector('[name%3D%22'%2Bn%2B'[]%22][value%3D%22'%2Bv%2B'%22]')%3Bif(e)%7Be.checked%3Dtrue%3Be.dispatchEvent(new%20Event('change'%2C%7Bbubbles%3Atrue%7D))%3B%7D%7D%3Bset('titre'%2C'TFE%20Test%20%E2%80%94%20Impact%20des%20r%C3%A9seaux%20sociaux%20sur%20la%20pratique%20artistique%20contemporaine')%3Bset('subtitle'%2C'%C3%89tude%20de%20cas%20aupr%C3%A8s%20des%20%C3%A9tudiant%C2%B7es%20de%20l%27ERG')%3Bset('auteurice'%2C'Marie-France%20Dupont')%3Bset('mail'%2C'mfd%40testmail.be')%3Bset('synopsis'%2C'Ce%20travail%20explore%20la%20mani%C3%A8re%20dont%20les%20plateformes%20num%C3%A9riques%20ont%20transform%C3%A9%20les%20processus%20de%20cr%C3%A9ation%20artistique.')%3Bcheck('contact_public')%3Bset('jury_president'%2C'Prof.%20Jean-Luc%20Moreau')%3Bset('jury_promoteur'%2C'Dr.%20Aline%20Vandenberghe')%3Bcheck('jury_promoteur_ext')%3Bset('ann%C3%A9e'%2C'2025')%3Bset('orientation'%2C'4')%3Bset('ap'%2C'3')%3Bset('finality'%2C'1')%3BcheckCB('languages'%2C'1')%3BcheckCB('languages'%2C'2')%3BcheckCB('formats'%2C'1')%3BcheckCB('formats'%2C'3')%3Bset('tag'%2C'r%C3%A9seaux%20sociaux%2Cart%20num%C3%A9rique%2Csociologie%2Cpratique%20artistique')%3Bset('license_id'%2C'7')%3Bset('duration_info'%2C'96%20pages')%3Bset('lien'%2C'https%3A%2F%2Fexample.com%2Ftfe-test')%3Bset('access_type_id'%2C'2')%3Bcheck('cc4r')%3B%7D)())**
|
||||
Open `/admin/add.php` (logged in), then click the bookmarklet:
|
||||
|
||||
### Readable source
|
||||
```
|
||||
javascript:(function(){var set=function(n,v){var e=document.querySelector('[name="'+n+'"]');if(e){e.value=v;e.dispatchEvent(new Event('change',{bubbles:true}));}};var setFirst=function(n,v){var e=document.querySelector('[name="'+n+'"][type="text"]');if(e){e.value=v;e.dispatchEvent(new Event('change',{bubbles:true}));}};var checkCB=function(n,v){var e=document.querySelector('[name="'+n+'[]"][value="'+v+'"]');if(e){e.checked=true;e.dispatchEvent(new Event('change',{bubbles:true}));}};var check=function(n){var e=document.querySelector('[name="'+n+'"]');if(e){e.checked=true;e.dispatchEvent(new Event('change',{bubbles:true}));}};set('titre','TFE Test — Impact des réseaux sociaux');set('subtitle','Étude de cas auprès des étudiant·es de l\'ERG');set('auteurice','Marie-France Dupont');set('contact_visible','public');setFirst('jury_promoteur[]','Dr. Aline Vandenberghe');setFirst('jury_lecteur_interne[]','Prof. Jean-Luc Moreau');setFirst('jury_lecteur_externe[]','Prof. Kim Sølv');setFirst('jury_promoteur_ulb_name[]','Prof. Université');set('synopsis','Ce travail explore l\'impact des plateformes numériques sur la pratique artistique contemporaine.');set('orientation','4');set('ap','3');set('finality','1');checkCB('formats','1');checkCB('formats','3');set('license_id','8');set('duration_pages','96');check('cc2r');set('website_url','https://example.com/tfe-test');set('access_type_id','2');set('contact_interne','mfd@testmail.be');set('jury_points','16.5');})()
|
||||
```
|
||||
|
||||
## Readable source
|
||||
|
||||
```js
|
||||
(function () {
|
||||
var f = document.querySelector('form.admin-form');
|
||||
if (!f) return;
|
||||
|
||||
var set = function (name, val) {
|
||||
var e = f.querySelector('[name="' + name + '"]');
|
||||
if (e) { e.value = val; e.dispatchEvent(new Event('change', { bubbles: true })); }
|
||||
var set = function (n, v) {
|
||||
var e = document.querySelector('[name="' + n + '"]');
|
||||
if (e) { e.value = v; e.dispatchEvent(new Event('change', { bubbles: true })); }
|
||||
};
|
||||
var check = function (name) {
|
||||
var e = f.querySelector('[name="' + name + '"]');
|
||||
var setFirst = function (n, v) { // first hidden/text field of a jury array
|
||||
var e = document.querySelector('[name="' + n + '"][type="text"]');
|
||||
if (e) { e.value = v; e.dispatchEvent(new Event('change', { bubbles: true })); }
|
||||
};
|
||||
var checkCB = function (n, v) { // checkbox-list (formats[]) value
|
||||
var e = document.querySelector('[name="' + n + '[]"][value="' + v + '"]');
|
||||
if (e) { e.checked = true; e.dispatchEvent(new Event('change', { bubbles: true })); }
|
||||
};
|
||||
var checkCB = function (name, val) {
|
||||
var e = f.querySelector('[name="' + name + '[]"][value="' + val + '"]');
|
||||
var check = function (n) {
|
||||
var e = document.querySelector('[name="' + n + '"]');
|
||||
if (e) { e.checked = true; e.dispatchEvent(new Event('change', { bubbles: true })); }
|
||||
};
|
||||
|
||||
// ── Informations du TFE ──
|
||||
set('titre', 'TFE Test — Impact des réseaux sociaux sur la pratique artistique contemporaine');
|
||||
set('titre', 'TFE Test — Impact des réseaux sociaux');
|
||||
set('subtitle', 'Étude de cas auprès des étudiant·es de l\'ERG');
|
||||
set('auteurice', 'Marie-France Dupont');
|
||||
set('mail', 'mfd@testmail.be');
|
||||
set('synopsis', 'Ce travail explore la manière dont les plateformes numériques ont transformé les processus de création artistique.');
|
||||
check('contact_public');
|
||||
set('auteurice', 'Marie-France Dupont'); // comma-separated
|
||||
set('contact_visible', 'public'); // admin mode; partage uses 'mail'
|
||||
|
||||
// ── Jury ──
|
||||
set('jury_president', 'Prof. Jean-Luc Moreau');
|
||||
set('jury_promoteur', 'Dr. Aline Vandenberghe');
|
||||
check('jury_promoteur_ext');
|
||||
// ── Jury (array fields, one row each) ──
|
||||
setFirst('jury_promoteur[]', 'Dr. Aline Vandenberghe');
|
||||
setFirst('jury_lecteur_interne[]', 'Prof. Jean-Luc Moreau');
|
||||
setFirst('jury_lecteur_externe[]', 'Prof. Kim Sølv');
|
||||
setFirst('jury_promoteur_ulb_name[]', 'Prof. Université');
|
||||
|
||||
// ── Cadre académique ──
|
||||
set('année', '2025');
|
||||
set('orientation', '4'); // Installation-Performance
|
||||
set('ap', '3'); // Atelier Pratiques Situées
|
||||
set('finality', '1'); // Approfondi
|
||||
checkCB('languages', '1'); // Français
|
||||
checkCB('languages', '2'); // Anglais
|
||||
checkCB('formats', '1'); // Site web
|
||||
checkCB('formats', '3'); // Vidéo
|
||||
set('tag', 'réseaux sociaux, art numérique, sociologie, pratique artistique');
|
||||
set('synopsis', 'Ce travail explore l\'impact des plateformes numériques sur la pratique artistique contemporaine.');
|
||||
set('orientation', '4'); // Installation-Performance
|
||||
set('ap', '3'); // Atelier Pratiques Situées
|
||||
set('finality', '1'); // Approfondie
|
||||
checkCB('formats', '1'); // Site web
|
||||
checkCB('formats', '3'); // Vidéo
|
||||
|
||||
// ── Métadonnées complémentaires ──
|
||||
set('license_id', '7'); // Tous droits réservés
|
||||
set('duration_info', '96 pages');
|
||||
set('lien', 'https://example.com/tfe-test');
|
||||
// ── Métadonnées ──
|
||||
set('license_id', '8'); // Tous droits réservés
|
||||
set('duration_pages', '96');
|
||||
check('cc2r'); // CC2r licence
|
||||
set('website_url', 'https://example.com/tfe-test');
|
||||
set('access_type_id', '2'); // Interne
|
||||
|
||||
// ── Licences (student mode only) ──
|
||||
check('cc4r');
|
||||
set('contact_interne', 'mfd@testmail.be');
|
||||
set('jury_points', '16.5');
|
||||
})();
|
||||
```
|
||||
|
||||
### Lookup table reference (schema.sql seed IDs)
|
||||
## Notes & current field names (verified against the form partials)
|
||||
|
||||
| Table | ID | Value |
|
||||
|---|---|---|
|
||||
| **orientations** | 1 | Arts Numériques |
|
||||
| | 2 | Dessin |
|
||||
| | 3 | Cinéma d'animation |
|
||||
| | 4 | Installation-Performance |
|
||||
| | 5 | Peinture |
|
||||
| | 6 | Photographie |
|
||||
| | 7 | Sculpture |
|
||||
| | 8 | Vidéographie |
|
||||
| | 9 | Graphisme |
|
||||
| | 10 | Typographie |
|
||||
| | 11 | Design Numérique |
|
||||
| | 12 | Illustration |
|
||||
| | 13 | Bande-Dessinée |
|
||||
| | 14 | Sérigraphie |
|
||||
| | 15 | Gravure |
|
||||
| **ap_programs** | 1 | Narration Spéculative |
|
||||
| | 2 | Design et Politique du Multiple |
|
||||
| | 3 | Atelier Pratiques Situées |
|
||||
| | 4 | LIENS |
|
||||
| **finality_types** | 1 | Approfondi |
|
||||
| | 2 | Enseignement |
|
||||
| | 3 | Spécialisé |
|
||||
| **languages** | 1 | Français |
|
||||
| | 2 | Anglais |
|
||||
| **format_types** | 1 | Site web |
|
||||
| | 2 | Audio |
|
||||
| | 3 | Vidéo |
|
||||
| | 4 | Performance |
|
||||
| | 5 | Objet éditorial |
|
||||
| | 6 | Installation |
|
||||
| | 7 | Autre |
|
||||
| **license_types** | 1 | CC BY 4.0 |
|
||||
| | 2 | CC BY-SA 4.0 |
|
||||
| | 3 | CC BY-ND 4.0 |
|
||||
| | 4 | CC BY-NC 4.0 |
|
||||
| | 5 | CC BY-NC-SA 4.0 |
|
||||
| | 6 | CC BY-NC-ND 4.0 |
|
||||
| | 7 | Tous droits réservés |
|
||||
| | 8 | Domaine public |
|
||||
| **access_types** | 1 | Libre |
|
||||
| | 2 | Interne |
|
||||
| | 3 | Interdit |
|
||||
| Field | Notes |
|
||||
|-------|-------|
|
||||
| `titre`, `subtitle`, `auteurice` | `auteurice` is comma-separated |
|
||||
| `contact_visible` | admin add/edit; the partage form uses `mail` |
|
||||
| `jury_promoteur[]`, `jury_lecteur_interne[]`, `jury_lecteur_externe[]`, `jury_promoteur_ulb_name[]` | one `input[type=text]` per row (`setFirst` targets the first) |
|
||||
| `orientation`, `ap`, `finality` | `<select>`; values are DB ids |
|
||||
| `formats[]` | checkbox-list |
|
||||
| `languages[]`, `tags[]` | **pill-search** (type-ahead with hidden `name=…[]` pills) — not pre-filled by this simple helper; add pills through the search UI or dispatch the pill-create event |
|
||||
| `license_id` | `<select>` of `license_types` |
|
||||
| `cc2r` | checkbox (renamed from `cc4r`) |
|
||||
| `want_license` | hidden `0`; set to `1` to show the licence explanation block |
|
||||
| `duration_pages` | combined duration: pages int; time uses `duration_h` + `duration_m` (values in minutes) |
|
||||
| `website_url`, `website_label` | the "lien" (site web) fields |
|
||||
| `access_type_id` | radio: `""`, `1`=Libre, `2`=Interne, `3`=Interdit |
|
||||
| `contact_interne`, `exemplaire_baiu`, `exemplaire_erg`, `is_published`, `context_note`, `jury_points`, `remarks` | other form fields |
|
||||
|
||||
### Removed / renamed
|
||||
|
||||
- `cc4r` → `cc2r`
|
||||
- `duration_info` → split into `duration_pages` + `duration_h`/`duration_m`
|
||||
- `jury_president`, single `jury_promoteur` → jury arrays above (`jury_promoteur[]` etc.)
|
||||
- `lien` → `website_url`; `mail` in admin mode → `contact_visible`
|
||||
- `contact_public` checkbox → handled via `authors.show_contact` / `contact_visible`
|
||||
- `?mode=student` → student flows go through `/partage/<slug>`
|
||||
|
||||
### Lookup ids
|
||||
|
||||
`orientations` 4=`Installation-Performance`; `ap_programs` 3=`Atelier Pratiques
|
||||
Situées`; `finality_types` 1=`Approfondie`; `format_types` 1=`Site web`,
|
||||
3=`Vidéo`; `license_types` 8=`Tous droits réservés`; `access_types` 2=`Interne`.
|
||||
|
||||
> IDs may shift after DB migrations that re-number reference rows — verify
|
||||
> against the current `app/storage/schema.sql` seed data if a lookup fails.
|
||||
|
||||
+174
-244
@@ -1,323 +1,253 @@
|
||||
# Database Reference
|
||||
|
||||
Post-ERG SQLite database — schema, configuration, and operations.
|
||||
XAMXAM SQLite database — schema, configuration, and operations.
|
||||
|
||||
**Version:** 1.0 · **Engine:** SQLite 3 · **Mode:** WAL
|
||||
The database lives at **`app/storage/xamxam.db`**. The canonical schema is
|
||||
**`app/storage/schema.sql`** (base tables + seed data); incremental changes are
|
||||
applied through versioned **migrations** in `app/migrations/` (see the
|
||||
*Schema changes* section).
|
||||
|
||||
> Legacy docs referenced `posterg.db`, `database/schema.sql`, and
|
||||
> `config/bootstrap.php`. Those are obsolete — the schema now lives under
|
||||
> `app/storage/` and migrations are handled by `app/migrations/run.php`.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
## Engine & mode
|
||||
|
||||
- **Engine:** SQLite 3
|
||||
- **Mode:** WAL (`journal_mode=wal`) for safe concurrent reads + hot backups
|
||||
- **Driver:** PDO (`ext-pdo` + `ext-sqlite3`), wrapped by `app/src/Database.php`
|
||||
|
||||
---
|
||||
|
||||
## Development quick start
|
||||
|
||||
```bash
|
||||
cd database/
|
||||
sqlite3 posterg.db < schema.sql # Create DB
|
||||
sqlite3 posterg.db "SELECT name FROM sqlite_master WHERE type='table';"
|
||||
sqlite3 posterg.db "SELECT * FROM orientations;" # Verify seed data
|
||||
just migrate # run pending migrations (creates DB from schema if missing)
|
||||
just init-db # create DB from app/storage/schema.sql
|
||||
just reset-db # rm xamxam.db + init-db
|
||||
just query # open an interactive sqlite3 shell on app/storage/xamxam.db
|
||||
sqlite3 app/storage/xamxam.db ".tables"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
## Migrations
|
||||
|
||||
Database paths are centralized in `config/bootstrap.php`:
|
||||
Migrations live in `app/migrations/`. They are run via
|
||||
`app/migrations/run.php` (wrapped by `just migrate`) and tracked in the
|
||||
`_migrations` table. Apply the same way in production with
|
||||
`just deploy-migrate`.
|
||||
|
||||
- **Development**: `APP_ROOT . '/storage/test.db'` (gitignored)
|
||||
- **Production**: `APP_ROOT . '/storage/posterg.db'`
|
||||
|
||||
The `Database` class (`src/Database.php`) auto-detects: if `test.db` exists → use it, otherwise → use `posterg.db`. Override with `DB_ENV` env var (`test` or `prod`) or pass a custom path to the constructor.
|
||||
|
||||
---
|
||||
|
||||
## Schema Overview
|
||||
|
||||
### Entity Relationship
|
||||
Each migration is a numbered file:
|
||||
|
||||
```
|
||||
authors ──1:N──► thesis_authors ──N:1──► theses
|
||||
supervisors ──1:N──► thesis_supervisors ──N:1──► theses
|
||||
keywords ──1:N──► thesis_keywords ──N:1──► theses
|
||||
languages ──1:N──► thesis_languages ──N:1──► theses
|
||||
format_types ──1:N──► thesis_formats ──N:1──► theses
|
||||
orientations ──N:1──► theses
|
||||
ap_programs ──N:1──► theses
|
||||
finality_types ──N:1──► theses
|
||||
access_types ──N:1──► theses
|
||||
license_types ──N:1──► theses
|
||||
thesis_files ──N:1──► theses
|
||||
app/migrations/applied/
|
||||
├── 001_add_objet_column.sql
|
||||
├── …
|
||||
├── 041_combined_duration.sql
|
||||
├── 042_fix_contact_columns.sql
|
||||
└── 043_relabel_promoteur_ulb.php # PHP migrations can run logic too
|
||||
```
|
||||
|
||||
### Table Categories
|
||||
|
||||
| Category | Tables |
|
||||
|----------|--------|
|
||||
| **Core** | `theses`, `authors`, `supervisors`, `thesis_files`, `pages` |
|
||||
| **Lookup** | `orientations` (15), `ap_programs` (4), `finality_types` (3), `languages` (2+), `format_types` (7), `access_types` (3), `license_types`, `keywords` (dynamic) |
|
||||
| **Junction** | `thesis_authors`, `thesis_supervisors`, `thesis_keywords`, `thesis_languages`, `thesis_formats` |
|
||||
| **Views** | `v_theses_full` (admin), `v_theses_public` (published only) |
|
||||
`schema.sql` is the fully-migrated baseline; it is regenerated from the local
|
||||
DB so it always reflects the applied set of migrations.
|
||||
|
||||
---
|
||||
|
||||
## Core Tables
|
||||
## Tables
|
||||
|
||||
### `theses`
|
||||
31 tables. Grouped by purpose:
|
||||
|
||||
| Column | Type | Required | Description |
|
||||
|--------|------|----------|-------------|
|
||||
| `id` | INTEGER PK | auto | Primary key |
|
||||
| `identifier` | TEXT UNIQUE | no | Human-readable ID (e.g., "2025-002") |
|
||||
| `title` | TEXT | **yes** | Thesis title |
|
||||
| `subtitle` | TEXT | no | Optional subtitle |
|
||||
| `year` | INTEGER | **yes** | Academic year |
|
||||
| `is_doctoral` | BOOLEAN | no | 0=TFE, 1=Doctoral |
|
||||
| `orientation_id` | INTEGER FK | no | → `orientations` |
|
||||
| `ap_program_id` | INTEGER FK | no | → `ap_programs` |
|
||||
| `finality_id` | INTEGER FK | no | → `finality_types` |
|
||||
| `synopsis` | TEXT | no | ~200 word summary |
|
||||
| `context_note` | TEXT | no | Jury president note (max 150 words) |
|
||||
| `remarks` | TEXT | no | Internal remarks |
|
||||
| `duration_minutes` | INTEGER | no | For audio/video |
|
||||
| `duration_pages` | INTEGER | no | For written works |
|
||||
| `file_size_info` | TEXT | no | Free-form size description |
|
||||
| `access_type_id` | INTEGER FK | no | → `access_types` |
|
||||
| `license_id` | INTEGER FK | no | → `license_types` |
|
||||
| `jury_points` | DECIMAL(4,2) | no | Grade (0–20) |
|
||||
| `jury_note_added` | BOOLEAN | no | Jury context note flag |
|
||||
| `submitted_at` | DATETIME | no | Student submission |
|
||||
| `defense_date` | DATETIME | no | Defense date |
|
||||
| `published_at` | DATETIME | no | Publication date |
|
||||
| `is_published` | BOOLEAN | no | Publication status |
|
||||
| `baiu_link` | TEXT | no | Institutional repository link |
|
||||
| `created_at` | DATETIME | auto | Record creation |
|
||||
| `updated_at` | DATETIME | auto | Last update (trigger) |
|
||||
### Core
|
||||
|
||||
**Indexes:** `idx_theses_year`, `idx_theses_published`, `idx_theses_identifier`, `idx_theses_orientation`, `idx_theses_ap_program`, `idx_theses_access_type`
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `theses` | The main entity (a TFE or doctoral work) |
|
||||
| `authors` | People who authored a thesis |
|
||||
| `supervisors` | Jury members (promoteur / lecteur / président) |
|
||||
| `thesis_files` | Uploaded files attached to a thesis |
|
||||
| `theses` subtypes | via `objet` column (`tfe`, `these`, …) and `is_doctoral` |
|
||||
|
||||
### `authors`
|
||||
### Lookup / reference
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | INTEGER PK | Auto |
|
||||
| `name` | TEXT NOT NULL | Full name |
|
||||
| `email` | TEXT | Contact email (optional) |
|
||||
| `created_at` / `updated_at` | DATETIME | Auto timestamps |
|
||||
| Table | Notes |
|
||||
|-------|-------|
|
||||
| `orientations` (15) | Arts Numériques, Dessin, …, Gravure |
|
||||
| `ap_programs` (5) | Narration Spéculative, DPM, APS, LIENS, PACS |
|
||||
| `finality_types` (3) | Approfondie, Enseignement, Spécialisée |
|
||||
| `languages` | français, anglais, néerlandais, italian (lowercase; expandable) |
|
||||
| `format_types` | Audio, Vidéo, Site web, Performance, Objet éditorial, Installation, Autre |
|
||||
| `access_types` (3) | Libre / Interne / Interdit |
|
||||
| `license_types` | CC BY 4.0 …, Domaine public, Tous droits réservés |
|
||||
| `tags` | Keywords (dynamic, lowercase-normalised, max 10/thesis) |
|
||||
|
||||
**Index:** `idx_authors_email`
|
||||
### Junction (many-to-many)
|
||||
|
||||
### `supervisors`
|
||||
`thesis_authors`, `thesis_supervisors`, `thesis_languages`,
|
||||
`thesis_formats`, `thesis_tags`.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | INTEGER PK | Auto |
|
||||
| `name` | TEXT NOT NULL | Full name |
|
||||
| `created_at` / `updated_at` | DATETIME | Auto timestamps |
|
||||
### Sharing / access
|
||||
|
||||
### `thesis_files`
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `share_links` | Student/partage submission links (slug, password, expiry, archived) |
|
||||
| `file_access_requests` | Requests to access restricted files |
|
||||
| `file_access_tokens` | One-time access tokens |
|
||||
| `file_access_sessions` | Session tokens for granted access |
|
||||
| `file_access_audit` | Audit trail of access events |
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | INTEGER PK | Auto |
|
||||
| `thesis_id` | INTEGER FK | → `theses` (CASCADE) |
|
||||
| `file_type` | TEXT | `main`, `annex`, `written_part`, `other` |
|
||||
| `file_path` | TEXT | Relative path |
|
||||
| `file_name` | TEXT | Original filename |
|
||||
| `file_size` | INTEGER | Size in bytes |
|
||||
| `mime_type` | TEXT | MIME type |
|
||||
| `description` | TEXT | Optional |
|
||||
| `uploaded_at` | DATETIME | Upload timestamp |
|
||||
### Integrations / settings / content
|
||||
|
||||
### `pages`
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `peertube_settings` | PeerTube video-hosting config (instance, channel, labels) |
|
||||
| `smtp_settings` | SMTP relay config + notify email (singleton `id=1`) |
|
||||
| `site_settings` | Feature flags (access types, objets, upload, admin password hash) |
|
||||
| `pages` | Editable content pages (`about`, `charte`, `licenses`) |
|
||||
| `apropos_contents` | Structured contact / about content |
|
||||
| `form_help_blocks` | Help texts on admin/partage forms |
|
||||
| `system_cache` | Key/value cache |
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | INTEGER PK | Auto |
|
||||
| `slug` | TEXT UNIQUE | URL identifier |
|
||||
| `title` | TEXT NOT NULL | Page title |
|
||||
| `content` | TEXT | Markdown/HTML |
|
||||
| `is_published` | BOOLEAN | Default 1 |
|
||||
| `created_at` / `updated_at` | DATETIME | Auto timestamps |
|
||||
### Auditing
|
||||
|
||||
**Pre-loaded:** `charte`, `about`, `licenses`, `contact`
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `admin_audit_log` | Audit log of admin operations (resource, action, status, IP/UA) |
|
||||
| `audit_log` | Low-level row audit (actor, action, before/after JSON) |
|
||||
|
||||
---
|
||||
|
||||
## Lookup Tables
|
||||
## Key columns — `theses`
|
||||
|
||||
### `orientations` (15 predefined)
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `id` | INTEGER PK | Auto |
|
||||
| `identifier` | TEXT | Human-readable id (e.g. `2025-003`) |
|
||||
| `title` | TEXT NOT NULL | |
|
||||
| `subtitle` | TEXT | |
|
||||
| `year` | INTEGER NOT NULL | |
|
||||
| `is_doctoral` | BOOLEAN | `0`=TFE, `1`=doctoral |
|
||||
| `objet` | TEXT | `'tfe'` default; other objets (`these`, `frart`) gated by site_settings |
|
||||
| `orientation_id` / `ap_program_id` / `finality_id` | INT FK | Lookup refs |
|
||||
| `synopsis` / `context_note` / `remarks` | TEXT | |
|
||||
| `duration_pages`, `duration_minutes` | INT | Combined duration handled at app level |
|
||||
| `has_annexes` | BOOLEAN | |
|
||||
| `access_type_id` | INT FK | Libre / Interne / Interdit |
|
||||
| `license_id` | INT FK | → `license_types` |
|
||||
| `cc2r` | BOOLEAN | CC2r licence checkbox |
|
||||
| `license_custom` | TEXT | Free-form licence text |
|
||||
| `jury_points` | DECIMAL(4,2) | Grade / 20 |
|
||||
| `jury_note_added` | BOOLEAN | |
|
||||
| `contact_visible` | TEXT | `'public'`/`'internal'` control for author contact |
|
||||
| `submitted_at`, `defense_date`, `published_at` | DATETIME | Lifecycle timestamps |
|
||||
| `is_published` | BOOLEAN | Public visibility flag |
|
||||
| `status` | TEXT | Default `'active'` |
|
||||
| `baiu_link` | TEXT | Institutional repo link |
|
||||
| `exemplaire_baiu`, `exemplaire_erg` | BOOLEAN | Physical copy flags |
|
||||
| `deleted_at` | DATETIME | Soft delete |
|
||||
| `created_at` / `updated_at` | DATETIME | Auto (`updated_at` trigger) |
|
||||
|
||||
Arts Numériques, Dessin, Cinéma d'animation, Installation-Performance, Peinture, Photographie, Sculpture, Vidéographie, Graphisme, Typographie, Design Numérique, Illustration, Bande-Dessinée, Sérigraphie, Gravure
|
||||
### `thesis_supervisors`
|
||||
|
||||
### `ap_programs` (4)
|
||||
Tracked via an explicit `role`/flag system rather than separate entity tables:
|
||||
|
||||
| Code | Name |
|
||||
|------|------|
|
||||
| — | Narration Spéculative |
|
||||
| DPM | Design et Politique du Multiple |
|
||||
| APS | Atelier Pratiques Situées |
|
||||
| LIENS | Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes |
|
||||
|
||||
### `finality_types` (3)
|
||||
|
||||
Approfondi, Enseignement, Spécialisé
|
||||
|
||||
### `format_types` (7)
|
||||
|
||||
Site web, Audio, Vidéo, Performance, Objet éditorial, Installation, Autre
|
||||
|
||||
### `access_types` (3)
|
||||
|
||||
| Name | Description |
|
||||
|------|-------------|
|
||||
| Libre | Full access online + library |
|
||||
| Interne | Physical only; note online |
|
||||
| Interdit | No access; note only |
|
||||
|
||||
**Business rule:** Access can only be restricted (Libre → Interne → Interdit), never opened.
|
||||
|
||||
### `languages`
|
||||
|
||||
Français, Anglais (expandable)
|
||||
|
||||
### `keywords`
|
||||
|
||||
Dynamic, grows organically. Max 10 per thesis (application-enforced).
|
||||
|
||||
---
|
||||
|
||||
## Junction Tables
|
||||
|
||||
All use composite PKs (`thesis_id`, `*_id`) with `ON DELETE CASCADE`.
|
||||
|
||||
| Table | Links | Order column |
|
||||
|-------|-------|-------------|
|
||||
| `thesis_authors` | theses ↔ authors | `author_order` |
|
||||
| `thesis_supervisors` | theses ↔ supervisors | `supervisor_order` |
|
||||
| `thesis_keywords` | theses ↔ keywords | — |
|
||||
| `thesis_languages` | theses ↔ languages | — |
|
||||
| `thesis_formats` | theses ↔ format_types | — |
|
||||
|
||||
**Indexes on junction tables:** `idx_thesis_keywords_thesis`, `idx_thesis_keywords_keyword` (and equivalents for authors)
|
||||
| Column | Notes |
|
||||
|--------|-------|
|
||||
| `role` | `promoteur`, `lecteur`, `president` |
|
||||
| `is_external` | Lecturer is external |
|
||||
| `is_ulb` | Promoteur is from the university (UCLouvain) side |
|
||||
|
||||
---
|
||||
|
||||
## Views
|
||||
|
||||
### `v_theses_full` — Admin view
|
||||
|
||||
All theses with joined relationships (GROUP_CONCAT for authors, supervisors, keywords, languages, formats, plus human-readable names for orientation, AP, finality, access type, license).
|
||||
|
||||
### `v_theses_public` — Public view
|
||||
|
||||
Same as `v_theses_full` filtered to `is_published = 1`. Unpublished theses never exposed.
|
||||
- **`v_theses_full`** — all theses with joined, human-readable fields
|
||||
(orientations, AP, finality, access, license, authors, supervisors with
|
||||
role splits, languages, formats, tags/keywords, contact fields).
|
||||
- **`v_theses_public`** — `v_theses_full` filtered to `is_published = 1`.
|
||||
- **`v_smtp_active`** — active SMTP settings row.
|
||||
|
||||
---
|
||||
|
||||
## Automatic Features
|
||||
## Scaffolded / helper tables
|
||||
|
||||
- **Auto-increment IDs:** All PKs use `AUTOINCREMENT`
|
||||
- **Auto timestamps:** `created_at` defaults to `CURRENT_TIMESTAMP`; `updated_at` refreshed by triggers on UPDATE
|
||||
- **Cascade deletes:** Deleting a thesis removes all junction + file records
|
||||
- `_migrations` — applied migration bookkeeping (used by the runner).
|
||||
|
||||
---
|
||||
|
||||
## Common Operations
|
||||
## Business rules
|
||||
|
||||
### Querying
|
||||
- **Access:** can only be *restricted* (Libre → Interne → Interdit), never opened back up — enforced in the UI/flow.
|
||||
- **Keywords (`tags`):** lowercase-normalised, de-duplicated, min 3 (form) / max 10 per thesis.
|
||||
- **Soft deletes:** reference tables such as `languages` carry `deleted_at`; hard `DELETE` is avoided where recoverability matters.
|
||||
- **Files:** stored outside the webroot under `app/storage/`; DB rows use storage-relative paths (`theses/<year>/<year>_<AUTHOR>/…`).
|
||||
|
||||
---
|
||||
|
||||
## Common operations
|
||||
|
||||
```sql
|
||||
-- Published theses
|
||||
SELECT * FROM v_theses_public ORDER BY year DESC;
|
||||
|
||||
-- Single thesis (admin)
|
||||
-- Single thesis (admin view)
|
||||
SELECT * FROM v_theses_full WHERE id = ?;
|
||||
|
||||
-- By year + orientation
|
||||
SELECT * FROM v_theses_public WHERE year = 2025 AND orientation = 'Arts Numériques';
|
||||
|
||||
-- By keyword
|
||||
-- Search by keyword (tag)
|
||||
SELECT DISTINCT t.* FROM theses t
|
||||
JOIN thesis_keywords tk ON t.id = tk.thesis_id
|
||||
JOIN keywords k ON tk.keyword_id = k.id
|
||||
WHERE k.keyword = 'écologie' AND t.is_published = 1;
|
||||
JOIN thesis_tags tt ON t.id = tt.thesis_id
|
||||
JOIN tags g ON tt.tag_id = g.id
|
||||
WHERE g.name = 'écologie' AND t.is_published = 1;
|
||||
|
||||
-- Theses per year
|
||||
SELECT year, COUNT(*) FROM theses WHERE is_published = 1 GROUP BY year ORDER BY year DESC;
|
||||
|
||||
-- Unpublished (admin)
|
||||
SELECT identifier, title, submitted_at FROM theses
|
||||
WHERE submitted_at IS NOT NULL AND is_published = 0 ORDER BY submitted_at DESC;
|
||||
```
|
||||
|
||||
### Inserting
|
||||
|
||||
```sql
|
||||
INSERT INTO authors (name, email) VALUES ('Marie Dupont', 'marie@example.com');
|
||||
INSERT INTO theses (identifier, title, year, orientation_id, finality_id, synopsis)
|
||||
VALUES ('2026-001', 'Mon Titre', 2026, 8, 1, 'Synopsis...');
|
||||
INSERT INTO thesis_authors (thesis_id, author_id, author_order) VALUES (1, 5, 1);
|
||||
INSERT OR IGNORE INTO keywords (keyword) VALUES ('performance');
|
||||
INSERT INTO thesis_keywords (thesis_id, keyword_id)
|
||||
SELECT 1, id FROM keywords WHERE keyword = 'performance';
|
||||
```
|
||||
|
||||
### Updating
|
||||
|
||||
```sql
|
||||
-- Publish a thesis
|
||||
UPDATE theses SET is_published = 1, published_at = CURRENT_TIMESTAMP WHERE id = 5;
|
||||
UPDATE theses SET jury_points = 16.5, context_note = '…', jury_note_added = 1 WHERE id = 5;
|
||||
```
|
||||
|
||||
> Note: keyword/contact fields in `v_theses_*` are `GROUP_CONCAT` aggregates —
|
||||
> prefer joining the junction tables (`thesis_tags`/`tags`) when you need
|
||||
> per-row access.
|
||||
|
||||
---
|
||||
|
||||
## Backup & maintenance
|
||||
|
||||
```bash
|
||||
just backup # SQL dump → app/storage/backup_<timestamp>.sql
|
||||
just backup-snapshot # WAL-safe hot backup + gzip → app/storage/backups/
|
||||
```
|
||||
|
||||
On the server, backups are scheduled to `/var/backups/xamxam/` via
|
||||
`/etc/cron.d/xamxam-backup` (see [deployment.md](deployment.md)).
|
||||
|
||||
Manual maintenance:
|
||||
|
||||
```bash
|
||||
sqlite3 app/storage/xamxam.db "VACUUM;" # after large deletes
|
||||
sqlite3 app/storage/xamxam.db "ANALYZE;"
|
||||
sqlite3 app/storage/xamxam.db "PRAGMA integrity_check;" # expect "ok"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backup & Maintenance
|
||||
## Schema changes
|
||||
|
||||
### Backup
|
||||
1. Back up first: `cp app/storage/xamxam.db app/storage/xamxam.db.bak`
|
||||
2. Add a numbered migration file under `app/migrations/applied/`
|
||||
(`.sql` for DDL/DML, `.php` when data logic is needed).
|
||||
3. Run `just migrate` (or `just deploy-migrate` on the server) — idempotent,
|
||||
tracked via `_migrations`.
|
||||
4. When appropriate, regenerate `app/storage/schema.sql` from the applied set
|
||||
so it stays a faithful baseline.
|
||||
|
||||
```bash
|
||||
# File copy (simplest)
|
||||
cp posterg.db backups/posterg_$(date +%Y%m%d).db
|
||||
|
||||
# SQL dump (portable)
|
||||
sqlite3 posterg.db .dump > backups/posterg_$(date +%Y%m%d).sql
|
||||
```
|
||||
|
||||
### Maintenance
|
||||
|
||||
```bash
|
||||
sqlite3 posterg.db "VACUUM;" # Reclaim space (after large deletes, monthly)
|
||||
sqlite3 posterg.db "ANALYZE;" # Update query stats (after schema/data changes)
|
||||
sqlite3 posterg.db "PRAGMA integrity_check;" # Verify → should output "ok"
|
||||
sqlite3 posterg.db "PRAGMA journal_mode=WAL;" # Enable WAL for better concurrency
|
||||
```
|
||||
|
||||
### Recovery
|
||||
|
||||
```bash
|
||||
sqlite3 posterg.db ".recover" | sqlite3 recovered.db # Corrupted DB
|
||||
sqlite3 posterg.db .dump | sqlite3 new.db # Dump + reimport
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- All critical foreign keys and search fields are indexed
|
||||
- Views pre-compute joins for common queries
|
||||
- For 1000+ theses: ensure WAL mode, run `ANALYZE` periodically, consider `VACUUM`
|
||||
- Cache size: `PRAGMA cache_size=-64000;` (64MB)
|
||||
- Memory-mapped I/O: `PRAGMA mmap_size=268435456;` (256MB)
|
||||
|
||||
---
|
||||
|
||||
## Schema Changes
|
||||
|
||||
### Making changes
|
||||
|
||||
1. Always backup first: `cp posterg.db posterg_before.db`
|
||||
2. Test on backup: `sqlite3 posterg_test.db < migration.sql`
|
||||
3. Use transactions: wrap ALTER/INSERT in `BEGIN; … COMMIT;`
|
||||
4. Document in `storage/migrations/` with numbered SQL files
|
||||
|
||||
### Change request format
|
||||
Change request format:
|
||||
|
||||
```
|
||||
Table: [table_name]
|
||||
|
||||
+153
-156
@@ -1,204 +1,201 @@
|
||||
# Deployment
|
||||
|
||||
Server setup, deployment, and rollback procedures for Post-ERG.
|
||||
Server setup, deployment, backups, and rollback procedures for XAMXAM.
|
||||
|
||||
---
|
||||
|
||||
## One-Time Server Setup
|
||||
## Overview
|
||||
|
||||
Run before first deploy:
|
||||
- Production host: `xamxam` (over SSH), app root `/var/www/xamxam/`
|
||||
- Files are pushed with `rsync` — **there is no git repo on the remote**
|
||||
- Web / FPM user: `www-data`, app group: `xamxam`
|
||||
- The **DocumentRoot** is `app/public/`, but on the server the code lives flat
|
||||
under `/var/www/xamxam/` (Composer autoload path adjusted to `src/`)
|
||||
- SQLite database at `/var/www/xamxam/storage/xamxam.db`
|
||||
|
||||
Deployment is orchestrated through the `justfile` (`deploy` group).
|
||||
|
||||
---
|
||||
|
||||
## One-time server setup
|
||||
|
||||
```bash
|
||||
just setup-server
|
||||
ssh xamxam
|
||||
sudo mkdir -p /var/www/xamxam
|
||||
sudo chown www-data:xamxam /var/www/xamxam
|
||||
sudo chmod 775 /var/www/xamxam
|
||||
exit
|
||||
```
|
||||
|
||||
This creates `/var/www/posterg/` with correct ownership/permissions:
|
||||
- Owner: `www-data:posterg`
|
||||
- Directories: **2775** (setgid — new files inherit `posterg` group)
|
||||
- Files: **664**
|
||||
- Database files: **660**
|
||||
Then from local, deploy once and apply the nginx config + verify permissions:
|
||||
|
||||
> **Important:** After running `setup-server`, log out and back in on the server (or `newgrp posterg`) so group membership is active before deploying.
|
||||
```bash
|
||||
just deploy
|
||||
just deploy-nginx
|
||||
```
|
||||
|
||||
### Why setgid (2775)?
|
||||
For a full initial rollout including backup + cleanup cron jobs:
|
||||
|
||||
rsync uses `--chown=www-data:posterg`. Both `padlock` and `www-data` must write to dirs. With `2775 + group=posterg`, new subdirs inherit the group automatically.
|
||||
```bash
|
||||
just deploy-all-first # deploy + deploy-backup + deploy-cleanup-cron
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deploying
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `just deploy` | Full deploy: build + code + Composer deps + migrations + env + permissions check |
|
||||
| `just deploy-code` | rsync app files + nginx config + permissions (no Composer, no migrations) |
|
||||
| `just deploy-deps` | Sync composer.{json,lock} → server, then `composer install`/`dump-autoload` |
|
||||
| `just deploy-migrate` | Run pending DB migrations on the server |
|
||||
| `just deploy-env` | Upload `app/.env` (only if the remote `.env` is absent — never overwrites a key) |
|
||||
| `just deploy-nginx` | Upload + apply + reload nginx config |
|
||||
| `just deploy-db` | Push local `xamxam.db` → remote (**refuses** if a remote DB already exists) |
|
||||
| `just deploy-verify-permissions` | Check ownership / permissions on the server |
|
||||
|
||||
> ℹ️ **First deploy?** After `just deploy`, run `just deploy-backup` to install
|
||||
> the backup script + cron jobs.
|
||||
|
||||
### Environment file & re-encryption
|
||||
|
||||
`app/.env` holds secrets (e.g. `APP_KEY`). `deploy-env` will **not** overwrite a
|
||||
remote `.env` that already has an `APP_KEY`. If you rotate `APP_KEY`, re-encrypt
|
||||
the SMTP password and push the new key:
|
||||
|
||||
```bash
|
||||
just deploy # Push all app files
|
||||
just deploy-db # Push initial database (aborts if remote DB exists)
|
||||
just deploy-nginx # Push + apply nginx config
|
||||
just reencrypt-password <new_base64_key> # runs scripts/reencrypt-smtp-password.php on server
|
||||
just deploy-env
|
||||
```
|
||||
|
||||
### First-Time Deployment
|
||||
---
|
||||
|
||||
Since we moved from `/var/www/html/` to `/var/www/posterg/`:
|
||||
## Backups
|
||||
|
||||
1. **Setup server directory** (one time):
|
||||
```bash
|
||||
just setup-server
|
||||
```
|
||||
SQLite backups are taken with a WAL-safe hot backup (`sqlite3 .backup`) then
|
||||
gzipped to `/var/backups/xamxam/` on the server.
|
||||
|
||||
2. **Deploy application**:
|
||||
```bash
|
||||
just deploy
|
||||
```
|
||||
Uploads to `/var/www/posterg/`, excludes tests/docs/vendor.
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `just deploy-backup` | Install backup script + cron jobs (one-shot) |
|
||||
| `just deploy-backup-script` | Install `/usr/local/bin/backup-sqlite.sh` |
|
||||
| `just deploy-backup-cron` | Install `/etc/cron.d/xamxam-backup` (hourly 30d + daily 90d) + dirs/log |
|
||||
| `just deploy-check-backup-log` | Tail `/var/log/sqlite-backup.log` |
|
||||
| `just deploy-list-backups` | List backups on the server |
|
||||
| `just trigger-backup` | Run the backup script now |
|
||||
| `just test-restore <path.gz>` | Fetch + decompress + verify a remote snapshot |
|
||||
|
||||
3. **Deploy nginx config**:
|
||||
```bash
|
||||
just deploy-nginx
|
||||
ssh posterg
|
||||
sudo bash /tmp/deploy-production.sh
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
Retention: hourly backups kept 30 days, nightly (02:00) backups kept 90 days.
|
||||
Backup files: `/var/backups/xamxam/db-<timestamp>.db.gz`.
|
||||
|
||||
4. **Verify**:
|
||||
```bash
|
||||
just server-status
|
||||
curl -I https://posterg.erg.be/ # 200 ✓
|
||||
curl -I https://posterg.erg.be/admin/ # 200 ✓
|
||||
curl -I https://posterg.erg.be/storage/ # 404 ✓
|
||||
```
|
||||
Draft cleanup is handled by a separate cron (`/etc/cron.d/xamxam-cleanup`),
|
||||
installed via `just deploy-cleanup-cron`, logging to
|
||||
`/var/log/xamxam-cleanup.log`. Verify with `just deploy-check-cleanup-log`.
|
||||
|
||||
### Subsequent Deployments
|
||||
---
|
||||
|
||||
## Permissions model
|
||||
|
||||
Managed/simulated by the justfile and verified by `deploy-verify-permissions`:
|
||||
|
||||
- Ownership: `www-data:xamxam`
|
||||
- Directories: **2775** (setgid — new files inherit the `xamxam` group)
|
||||
- Regular files: **664**
|
||||
- `storage/xamxam.db` and other `*.db`: **660**
|
||||
- `app/.env`: **640**
|
||||
|
||||
The nginx/`deploy-server.sh` step (`just deploy-nginx`, or
|
||||
`sudo DEPLOY_USER=$USER bash /tmp/deploy-server.sh` via `just deploy-script`)
|
||||
fixes permissions and installs the nginx config.
|
||||
|
||||
---
|
||||
|
||||
## Storage migration
|
||||
|
||||
If paths moved (e.g. legacy upload locations), a one-off migration script can
|
||||
rewrite stored paths on the server:
|
||||
|
||||
```bash
|
||||
just deploy-migrate-storage # apply
|
||||
just deploy-migrate-storage --dry-run # dry-run only
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rollback
|
||||
|
||||
Because there is no repo on the remote, rollback means restoring files and/or
|
||||
restoring the database from a backup.
|
||||
|
||||
### Restore the database
|
||||
|
||||
```bash
|
||||
ssh xamxam
|
||||
sudo systemctl stop nginx
|
||||
cp /var/backups/xamxam/db-<timestamp>.db.gz /tmp/restore.db.gz
|
||||
gunzip -c /tmp/restore.db.gz > /var/www/xamxam/storage/xamxam.db
|
||||
chown www-data:xamxam /var/www/xamxam/storage/xamxam.db
|
||||
chmod 660 /var/www/xamxam/storage/xamxam.db
|
||||
sudo systemctl start nginx
|
||||
```
|
||||
|
||||
### Restore application files
|
||||
|
||||
Re-run `just deploy` from a good local state. In jj you can jump back to a
|
||||
previous commit and redeploy:
|
||||
|
||||
```bash
|
||||
jj log # find a known-good change
|
||||
jj edit <previous-change-id> # work from that revision
|
||||
just deploy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Server Directory Structure
|
||||
|
||||
```
|
||||
/var/www/posterg/ # Application root (private)
|
||||
├── public/ # DocumentRoot (nginx points here)
|
||||
│ ├── index.php
|
||||
│ ├── search.php
|
||||
│ ├── memoire.php
|
||||
│ ├── admin/
|
||||
│ └── assets/
|
||||
├── includes/ # Templates (private)
|
||||
├── config/ # Configuration (private)
|
||||
├── storage/ # Database + uploads (private)
|
||||
│ ├── posterg.db
|
||||
│ └── theses/
|
||||
├── src/ # PHP classes (private)
|
||||
└── scripts/ # Admin tools (private)
|
||||
```
|
||||
|
||||
**Nginx DocumentRoot:** `/var/www/posterg/public/`
|
||||
|
||||
Only `public/` is web-accessible. Everything else is physically private.
|
||||
|
||||
---
|
||||
|
||||
## Managing Admin Users
|
||||
## Verify after deploy
|
||||
|
||||
```bash
|
||||
ssh posterg "sudo bash /var/www/posterg/scripts/manage-admin-users.sh"
|
||||
```
|
||||
|
||||
Interactive menu for adding/changing/deleting htpasswd entries at `/etc/nginx/.htpasswd-posterg`.
|
||||
|
||||
---
|
||||
|
||||
## Security Verification
|
||||
|
||||
After every deploy, verify private files are inaccessible:
|
||||
|
||||
```bash
|
||||
curl -I https://posterg.erg.be/storage/test.db # Must 404
|
||||
curl -I https://posterg.erg.be/config/bootstrap.php # Must 404
|
||||
curl -I https://posterg.erg.be/src/Database.php # Must 404
|
||||
curl -I https://xamxam.erg.be/ # expect 200
|
||||
curl -I https://xamxam.erg.be/admin/ # expect 200
|
||||
curl -I https://xamxam.erg.be/storage/ # expect 404 (blocked)
|
||||
just deploy-verify-permissions # expect "All permissions OK"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### rsync Permission Denied
|
||||
|
||||
```bash
|
||||
just setup-server # Fixes directory permissions
|
||||
# Then log out/in on server and retry
|
||||
```
|
||||
|
||||
Manual fix:
|
||||
```bash
|
||||
ssh posterg
|
||||
sudo chown -R www-data:posterg /var/www/posterg
|
||||
sudo find /var/www/posterg -type d -exec chmod 2775 {} \;
|
||||
sudo find /var/www/posterg -type f -exec chmod 664 {} \;
|
||||
sudo chmod 660 /var/www/posterg/storage/*.db
|
||||
```
|
||||
|
||||
### Nginx 403 Forbidden
|
||||
|
||||
```bash
|
||||
ssh posterg
|
||||
sudo find /var/www/posterg -type d -exec chmod 2775 {} \;
|
||||
sudo find /var/www/posterg -type f -exec chmod 664 {} \;
|
||||
sudo chmod 660 /var/www/posterg/storage/*.db
|
||||
```
|
||||
|
||||
### Database Permission Error
|
||||
|
||||
```bash
|
||||
ssh posterg
|
||||
sudo chown www-data:posterg /var/www/posterg/storage/posterg.db
|
||||
sudo chmod 660 /var/www/posterg/storage/posterg.db
|
||||
```
|
||||
|
||||
### Nginx 500 / Site 404
|
||||
|
||||
Check nginx DocumentRoot:
|
||||
```bash
|
||||
ssh posterg "grep 'root ' /etc/nginx/sites-available/posterg"
|
||||
# Should show: root /var/www/posterg/public;
|
||||
```
|
||||
|
||||
### Admin 404
|
||||
|
||||
Nginx may still use old `/formulaire/` location. Update `nginx/posterg.conf` to use `/admin/`.
|
||||
- **502 Bad Gateway**
|
||||
```bash
|
||||
sudo systemctl status php8.4-fpm
|
||||
sudo systemctl restart php8.4-fpm
|
||||
```
|
||||
- **Nginx config error**
|
||||
```bash
|
||||
sudo nginx -t
|
||||
```
|
||||
- **`deploy-db` refuses to run**
|
||||
The remote DB already exists. Remove it manually only if you intend to
|
||||
overwrite production data.
|
||||
- **Composer did not pick up new classes**
|
||||
`deploy-deps` runs `composer dump-autoload` when the lock checksum is
|
||||
unchanged; if that still fails, force a reinstall.
|
||||
- **Backup/schedule not running**
|
||||
Confirm the cron files are installed (`/etc/cron.d/xamxam-backup`,
|
||||
`/etc/cron.d/xamxam-cleanup`) and the log files are writable by `www-data`.
|
||||
|
||||
---
|
||||
|
||||
## Rollback
|
||||
|
||||
If something goes wrong:
|
||||
|
||||
```bash
|
||||
# Restore old nginx config
|
||||
ssh posterg
|
||||
sudo cp /etc/nginx/sites-available/posterg.backup /etc/nginx/sites-available/posterg
|
||||
sudo systemctl reload nginx
|
||||
|
||||
# Or restore old site (if backed up)
|
||||
sudo rm -rf /var/www/posterg
|
||||
sudo mv /var/www/html.backup /var/www/html
|
||||
```
|
||||
|
||||
With jj:
|
||||
```bash
|
||||
jj log
|
||||
jj edit <previous-change-id>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands Reference
|
||||
## Selected commands reference
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `just setup-server` | Create `/var/www/posterg/` (first time only) |
|
||||
| `just deploy` | Deploy application files |
|
||||
| `just deploy-nginx` | Update nginx configuration |
|
||||
| `just deploy-db` | Deploy database file |
|
||||
| `just server-status` | Check server health |
|
||||
| `just server-logs` | View server logs |
|
||||
| `just deploy` | Full deploy (build + code + deps + migrate + env + perms) |
|
||||
| `just deploy-nginx` | Apply nginx config |
|
||||
| `just deploy-backup` | Install backup script + cron |
|
||||
| `just deploy-cleanup-cron` | Install orphaned-draft cleanup cron |
|
||||
| `just deploy-list-backups` | List server backups |
|
||||
| `just trigger-backup` | Run backup now |
|
||||
| `just reencrypt-password <key>` | Re-encrypt SMTP password after key rotation |
|
||||
| `just test-restore <path.gz>` | Verify a snapshot |
|
||||
|
||||
+113
-227
@@ -1,272 +1,158 @@
|
||||
# Development Guide
|
||||
|
||||
Setup, workflow, testing, and live reload for Post-ERG development.
|
||||
Setup, workflow, building assets, and testing for XAMXAM development.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
## Requirements
|
||||
|
||||
- **PHP** ≥ 8.4 with `ext-json`, `ext-openssl`, `ext-pdo` (and `ext-sqlite3` for the local DB)
|
||||
- **Composer** for PHP dependencies
|
||||
- **Node.js / npm** for frontend asset builds (rolldown, lightningcss, biome)
|
||||
- **SQLite3** CLI (for `just query`, `just init-db`)
|
||||
|
||||
## One-time setup
|
||||
|
||||
From the repo root, install dependencies (manually — there is no `just` recipe
|
||||
for these):
|
||||
|
||||
```bash
|
||||
just setup # One time: clone php-live-reload + setup directories
|
||||
just serve # Start dev server at http://localhost:8000
|
||||
composer install # PHP deps (vendor/)
|
||||
npm ci # JS build deps (node_modules/)
|
||||
just migrate # create/update the SQLite DB from schema + migrations
|
||||
```
|
||||
|
||||
One unified server serves both:
|
||||
- **Public site:** http://localhost:8000
|
||||
- **Admin panel:** http://localhost:8000/admin/
|
||||
|
||||
Live reload is enabled automatically — browser refreshes when you save files.
|
||||
|
||||
---
|
||||
`just setup` exists but its backing script (`scripts/setup-dev.sh`) is **stale**
|
||||
— it still clones the old `php-live-reload` library and creates legacy
|
||||
`admin/data/` directories. Live-reload now ships inside the app
|
||||
(`app/public/live-reload.php`), and assets are built with rolldown/lightningcss,
|
||||
not the live-reload watcher. Prefer the explicit `composer install` + `npm ci`
|
||||
above.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
posterg-website/
|
||||
├── public/ # DocumentRoot (web-accessible)
|
||||
│ ├── index.php # Homepage
|
||||
│ ├── search.php # Search/répertoire
|
||||
│ ├── memoire.php # Thesis detail
|
||||
│ ├── admin/ # Admin panel
|
||||
│ └── assets/ # CSS, fonts, images
|
||||
├── includes/ # Template partials (header, footer)
|
||||
├── config/ # Configuration (bootstrap.php)
|
||||
├── src/ # PHP classes (Database, AdminAuth, RateLimit)
|
||||
├── storage/ # Database + uploads (private)
|
||||
├── database/ # Schema + migrations
|
||||
├── tests/ # Test suite
|
||||
├── nginx/ # Server configuration
|
||||
├── scripts/ # Deployment/admin scripts
|
||||
└── vendor/ # Third-party (gitignored, dev only)
|
||||
xamxam/
|
||||
├── app/ # All application code (the project root on the server)
|
||||
│ ├── bootstrap.php # App bootstrapping (constants, autoload, config)
|
||||
│ ├── router.php # Router for the PHP built-in dev server
|
||||
│ ├── public/ # DocumentRoot (web-accessible only)
|
||||
│ │ ├── index.php # Front controller / entry point
|
||||
│ │ ├── request-access.php
|
||||
│ │ ├── live-reload.php
|
||||
│ │ ├── admin/ # Admin panel
|
||||
│ │ ├── partage/ # Student submission via share links
|
||||
│ │ └── assets/ # Built CSS/JS, fonts, images (dist/ is generated)
|
||||
│ ├── src/ # PHP classes
|
||||
│ │ ├── Controllers/ # Request controllers (Home, Search, Tfe, Export, …)
|
||||
│ │ ├── Form/ # Form helper
|
||||
│ │ ├── Database.php # DB access + queries
|
||||
│ │ ├── AdminAuth.php # Session auth for admin
|
||||
│ │ ├── RateLimit.php
|
||||
│ │ ├── FilepondHandler.php
|
||||
│ │ ├── PeerTubeService.php
|
||||
│ │ └── … # (see list in src/)
|
||||
│ ├── templates/ # Template partials (public, admin, partials, partage)
|
||||
│ ├── storage/ # Database + uploads (private)
|
||||
│ │ ├── xamxam.db # SQLite database
|
||||
│ │ ├── schema.sql # Base schema
|
||||
│ │ ├── theses/ tfe/ # Uploaded files
|
||||
│ │ └── cache/ logs/ # Runtime data
|
||||
│ └── migrations/ # Migration runner + applied migrations
|
||||
├── scripts/ # Build / deploy / utility scripts
|
||||
├── tests/phpunit/ # PHPUnit tests
|
||||
├── nginx/ # Server configuration + docs
|
||||
├── deploy/ # Cron configs (backup, cleanup)
|
||||
├── justfile # Task runner
|
||||
├── composer.json / package.json
|
||||
└── TODO.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Starting Development
|
||||
### Start the dev server
|
||||
|
||||
```bash
|
||||
just serve
|
||||
just dev
|
||||
```
|
||||
|
||||
### Making Changes
|
||||
This runs a one-shot asset build, applies pending migrations, opens
|
||||
`http://127.0.0.1:8000/` (public) and `/admin/` (admin), then starts the PHP
|
||||
built-in server (`php -S 127.0.0.1:8000`) with a `chokidar` watcher that
|
||||
rebuilds CSS/JS on change.
|
||||
|
||||
1. Edit PHP/CSS files — browser auto-refreshes
|
||||
2. Run tests: `just test`
|
||||
3. Check syntax: `just syntax`
|
||||
4. Deploy: `just deploy`
|
||||
- **Public site:** `http://127.0.0.1:8000/`
|
||||
- **Admin panel:** `http://127.0.0.1:8000/admin/`
|
||||
|
||||
### Database Operations
|
||||
Stop it with `just stop`.
|
||||
|
||||
If you only need the server without browsers/ui, run `just dev-build` first,
|
||||
then start the PHP built-in server manually with the `php -d … -S` command
|
||||
shown in the `dev` recipe (or add a `just serve` alias locally).
|
||||
|
||||
### Frontend assets
|
||||
|
||||
Source CSS lives in `app/public/assets/css/`, source JS in
|
||||
`app/public/assets/js/app/`. They are **bundled and minified** into
|
||||
`app/public/assets/dist/` by `scripts/build*.mjs` (rolldown + lightningcss).
|
||||
|
||||
```bash
|
||||
just stats # View database stats
|
||||
just query # Open SQLite shell
|
||||
just show 42 # Show thesis by ID
|
||||
just reset-db # Reset database
|
||||
just fixtures # Create sample data
|
||||
just backup # Backup database
|
||||
just dev-build # one-shot build (quicker output than full)
|
||||
just build # full build
|
||||
just build-css # CSS only
|
||||
just build-js # JS only
|
||||
just build-check # verify build output is up to date
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Live Reload
|
||||
|
||||
### What It Does
|
||||
|
||||
Automatically refreshes your browser when you save PHP/CSS/JS files. No browser extension needed.
|
||||
|
||||
### Setup (One Time)
|
||||
|
||||
```bash
|
||||
just setup
|
||||
```
|
||||
|
||||
Clones `php-live-reload` into `vendor/` (gitignored).
|
||||
|
||||
### How It Works
|
||||
|
||||
- Conditionally included in `header.php` when `php_sapi_name() === 'cli-server'`
|
||||
- JavaScript polls server for file changes → browser refreshes
|
||||
- **Never active in production** (different SAPI, vendor/ not deployed)
|
||||
|
||||
### Detection
|
||||
|
||||
```php
|
||||
<?php if (php_sapi_name() === 'cli-server'): ?>
|
||||
<script src="/vendor/php-live-reload/php-live-reload/live-reload.js"></script>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
```bash
|
||||
ls -la vendor/php-live-reload/ # Check installed
|
||||
just setup # Reinstall if missing
|
||||
curl -s http://localhost:8000/ | grep live-reload # Verify script included
|
||||
```
|
||||
|
||||
---
|
||||
There is no live-reloading in production — `app/public/live-reload.php` only
|
||||
activates under the PHP built-in server.
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── run-tests.php # Main test runner
|
||||
├── Unit/ # Unit tests
|
||||
│ ├── DatabaseTest.php
|
||||
│ └── RateLimitTest.php
|
||||
├── Integration/ # Integration tests
|
||||
│ └── SearchTest.php
|
||||
└── Security/ # Security tests
|
||||
└── SecurityTest.php
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
PHPUnit is used. Configuration lives in `phpunit.xml`, tests in `tests/phpunit/`
|
||||
(with `tests/bootstrap.php` and `tests/TestDatabase.php` helpers).
|
||||
|
||||
```bash
|
||||
just test # Run all tests
|
||||
just test-unit # Unit tests only
|
||||
just test-integration # Integration tests only
|
||||
just test-security # Security tests only
|
||||
just syntax # Check PHP syntax
|
||||
just test # run all PHPUnit tests
|
||||
just test-coverage # run with HTML coverage into coverage/
|
||||
```
|
||||
|
||||
### Writing Tests
|
||||
|
||||
1. Choose type: Unit / Integration / Security
|
||||
2. Create test file in appropriate `tests/` subdirectory
|
||||
3. Follow template:
|
||||
|
||||
```php
|
||||
<?php
|
||||
require_once __DIR__ . '/../../src/Database.php';
|
||||
|
||||
echo "Test Name\n";
|
||||
echo "=========\n\n";
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
echo "✓ PASS: Test description\n";
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
echo "❌ FAIL: " . $e->getMessage() . "\n";
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
4. Add to `tests/run-tests.php` `$testFiles` array
|
||||
5. Run: `just test`
|
||||
|
||||
---
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Create a New Page
|
||||
|
||||
```php
|
||||
<?php
|
||||
require_once __DIR__ . '/../config/bootstrap.php';
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
$db = App::boot();
|
||||
include APP_ROOT . '/includes/header.php';
|
||||
?>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title">New Page</h1>
|
||||
</div>
|
||||
</section>
|
||||
<?php include APP_ROOT . '/includes/footer.php'; ?>
|
||||
```
|
||||
|
||||
### Add a Database Method
|
||||
|
||||
1. Edit `src/Database.php`
|
||||
2. Add method to the class
|
||||
3. Write test in `tests/Unit/`
|
||||
4. Run: `just test-unit`
|
||||
|
||||
### Update CSS
|
||||
|
||||
1. Edit `public/assets/posterg.css`
|
||||
2. Browser auto-refreshes
|
||||
3. Increment cache-bust in header: `posterg.css?v=N`
|
||||
|
||||
---
|
||||
|
||||
## Justfile Commands
|
||||
|
||||
### Development
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `just setup` | Setup dev environment (one-time) |
|
||||
| `just serve` | Start dev server with live reload |
|
||||
| `just stop` | Stop dev server |
|
||||
| `just logs` | View dev logs |
|
||||
|
||||
### Testing
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `just test` | Run all tests |
|
||||
| `just test-unit` | Unit tests only |
|
||||
| `just test-integration` | Integration tests only |
|
||||
| `just test-security` | Security tests only |
|
||||
| `just syntax` | Check PHP syntax |
|
||||
|
||||
### Database
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `just stats` | Database statistics |
|
||||
| `just query` | Open SQLite shell |
|
||||
| `just show <id>` | Show thesis by ID |
|
||||
| `just reset-db` | Reset test database |
|
||||
| `just fixtures` | Create sample data |
|
||||
| `just backup` | Backup database |
|
||||
|
||||
### Deployment
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `just deploy` | Deploy complete site |
|
||||
| `just deploy-nginx` | Deploy nginx config |
|
||||
| `just deploy-db` | Deploy database |
|
||||
| `just server-status` | Check server health |
|
||||
| `just server-logs` | View server logs |
|
||||
|
||||
---
|
||||
|
||||
## Debugging
|
||||
### Linting / formatting
|
||||
|
||||
```bash
|
||||
just logs # View error logs
|
||||
tail -f error.log # Direct log monitoring
|
||||
|
||||
# PHP errors in browser (temporary):
|
||||
# Add to PHP file:
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
# Database issues:
|
||||
just stats # Check DB exists and has data
|
||||
just query # Open SQLite shell
|
||||
just lint-php # phpstan (static analysis) + php-cs-fixer (coding standards)
|
||||
just lint-css # biome lint on app/public/assets/css/
|
||||
just lint-js # biome lint on app/public/assets/js/app/ + scripts/
|
||||
just lint # all linters
|
||||
just fix # auto-fix (biome + php-cs-fixer)
|
||||
```
|
||||
|
||||
### Server Won't Start
|
||||
## Database Operations
|
||||
|
||||
```bash
|
||||
just stop # Kill existing process
|
||||
just setup # Reinstall php-live-reload
|
||||
just migrate # run pending migrations
|
||||
just init-db # create/reset DB from app/storage/schema.sql
|
||||
just reset-db # rm DB + init-db
|
||||
just query # open an interactive SQLite shell
|
||||
just backup # SQL dump into app/storage/backup_<timestamp>.sql
|
||||
just backup-snapshot # WAL-safe hot backup + gzip into storage/backups/
|
||||
just cleanup-drafts [--no-dry-run] # remove orphaned drafts > 24h
|
||||
```
|
||||
|
||||
### Database Errors
|
||||
See [database.md](database.md) for the full schema reference.
|
||||
|
||||
## Deployment
|
||||
|
||||
See [deployment.md](deployment.md). Files are pushed to the server with
|
||||
`just deploy` (rsync → `xamxam:/var/www/xamxam/`). There is no git repo on
|
||||
the remote.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
```bash
|
||||
just reset-db # Reset from schema
|
||||
just fixtures # Repopulate with sample data
|
||||
just test # Verify everything works
|
||||
just logs # tail the dev error log (error.log)
|
||||
just stop # kill the dev server / asset watcher
|
||||
```
|
||||
|
||||
If the browser doesn't hot-reload, confirm the asset watcher is still running
|
||||
and the rebuild succeeded (`just dev-build`).
|
||||
|
||||
+6
-4
@@ -106,7 +106,7 @@ sudo systemctl stop nginx
|
||||
# Remplacer le fichier SQLite
|
||||
cp xamxam-db-AAAA-MM-JJ.sqlite /var/www/xamxam/storage/xamxam.db
|
||||
chown www-data:www-data /var/www/xamxam/storage/xamxam.db
|
||||
chmod 640 /var/www/xamxam/storage/xamxam.db
|
||||
chmod 660 /var/www/xamxam/storage/xamxam.db
|
||||
```
|
||||
|
||||
### 2. Restaurer les fichiers
|
||||
@@ -183,9 +183,11 @@ d'exécution raisonnable (`max_execution_time ≥ 60s`) est recommandé.
|
||||
|
||||
### Colonnes du CSV d'export
|
||||
|
||||
Le CSV utilise la même structure que l'import (21 colonnes). Il est
|
||||
directement ré-importable sans modification. Voir [`docs/import.md`](import.md)
|
||||
pour le détail des colonnes.
|
||||
Le CSV utilise les mêmes colonnes que l'import (les en-têtes proviennent de
|
||||
`ExportController::CSV_HEADERS`, 26 colonnes dont `CC2r`, `Exemplaire BAIU` et
|
||||
`Exemplaire ERG`). Il est directement ré-importable — l'import ignore les
|
||||
colonnes purement d'export. Voir [`docs/import.md`](import.md) pour le détail
|
||||
des colonnes et de la logique d'import.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+27
-26
@@ -93,35 +93,34 @@ Files whose MIME type is `application/octet-stream` are accepted **only if their
|
||||
|
||||
Max size: **20 MB**.
|
||||
|
||||
### Banner image (`banner` input)
|
||||
### Banner image
|
||||
|
||||
| Extension | MIME type |
|
||||
|-----------|-----------|
|
||||
| `.jpg` / `.jpeg` | `image/jpeg` |
|
||||
| `.png` | `image/png` |
|
||||
| `.webp` | `image/webp` |
|
||||
|
||||
Landscape format recommended (4:1 ratio). Max size: **20 MB**.
|
||||
> Removed — the home-page banner was merged into covers (migration
|
||||
> `028_drop_banner_path.sql`). There is no separate banner upload anymore.
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
Per-field limits are enforced in `app/src/Controllers/validate-file-fragment-shared.php`
|
||||
(server-side `finfo` + size check on every upload):
|
||||
|
||||
The PHP limits are set in:
|
||||
| Field | Max size | Accepted content |
|
||||
|-------|----------|------------------|
|
||||
| `tfe` (main files) | **500 MB** default; **PDF capped at 100 MB**; **video/audio up to 5 GB** | PDF, images, video, audio, archives |
|
||||
| `annexes` | **500 MB** (PDF capped at 100 MB, video/audio up to 5 GB) | PDF, archives, images, media |
|
||||
| `couverture` (cover) | **20 MB** | JPG / PNG / WEBP |
|
||||
| `note_intention` | **100 MB** | PDF |
|
||||
|
||||
| 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) |
|
||||
(Admins bypass validation entirely — `admin_mode=1`.)
|
||||
|
||||
The PHP engine limits are set as follows:
|
||||
|
||||
| File | Applies to | `upload_max_filesize` | `post_max_size` |
|
||||
|------|------------|----------------------|------------------|
|
||||
| `app/public/.user.ini` | PHP-FPM / nginx | 8192M | 8704M |
|
||||
| `app/public/.htaccess` | Apache (`mod_php`) | 512M | 1024M |
|
||||
| `justfile` — `dev` recipe | PHP built-in dev server (`php -S` ignores the files above, so limits are passed via `-d` flags) | 8192M | 8704M |
|
||||
|
||||
For environments that require different limits, edit all three.
|
||||
|
||||
@@ -160,8 +159,6 @@ 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>/
|
||||
@@ -203,8 +200,9 @@ CREATE TABLE thesis_files (
|
||||
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)
|
||||
display_label TEXT, -- per-file caption
|
||||
sort_order INTEGER NOT NULL DEFAULT 0, -- display order
|
||||
file_hash TEXT, -- stored hash (optional)
|
||||
uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE
|
||||
);
|
||||
@@ -225,7 +223,10 @@ Files are queried ordered by `sort_order ASC, uploaded_at ASC`.
|
||||
| `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/src/Controllers/FilepondHandler.php` + `app/public/assets/js/app/file-upload-filepond.js` | FilePond-backed upload queue |
|
||||
| `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` |
|
||||
|
||||
> Note: the upload UI was migrated from the legacy SortableJS `file-upload-queue.js`
|
||||
> to **FilePond** (`file-upload-filepond.js` + `FilepondHandler.php`).
|
||||
|
||||
+89
-134
@@ -1,153 +1,108 @@
|
||||
# CSV Import Format Specification
|
||||
# CSV Import
|
||||
|
||||
## File Format
|
||||
Import of theses from the admin panel, plus the CSV structure and behaviour.
|
||||
|
||||
- **Encoding**: UTF-8
|
||||
- **Delimiter**: Comma (`,`)
|
||||
- **Header Rows**: First 4 rows are skipped during import
|
||||
- Row 1: Empty
|
||||
- Row 2: Headers (French labels)
|
||||
- Row 3: Description row
|
||||
- Row 4: Column names
|
||||
- **Data Rows**: Start from row 5 onwards
|
||||
> **Where it lives:** the CSV import is handled **inline** in
|
||||
> `app/public/admin/index.php` — there is no separate `import.php` action. The
|
||||
> `/admin/import.php` file only redirects to `/admin/`.
|
||||
|
||||
## Column Structure
|
||||
---
|
||||
|
||||
The CSV must contain exactly 21 columns in this order:
|
||||
## File format
|
||||
|
||||
| Index | Field Name | Required | Type | Description |
|
||||
|-------|------------|----------|------|-------------|
|
||||
| 0 | identifier | No | String | Unique identifier for the thesis |
|
||||
| 1 | title | **Yes** | String | Thesis title |
|
||||
| 2 | subtitle | No | String | Thesis subtitle |
|
||||
| 3 | authors | No | String | Author(s), comma-separated for multiple |
|
||||
| 4 | contact | No | String | Contact email (associated with first author) |
|
||||
| 5 | supervisors | No | String | Supervisor(s), comma-separated for multiple |
|
||||
| 6 | formats | No | String | Format(s), comma-separated for multiple |
|
||||
| 7 | year | **Yes** | Integer | Year of thesis (e.g., 2024) |
|
||||
| 8 | ap | No | String | AP program code (see AP Codes section) |
|
||||
| 9 | orientation | No | String | Orientation code (see Orientation Codes section) |
|
||||
| 10 | finality | No | String | Finality name |
|
||||
| 11 | keywords | No | String | Keywords, comma-separated (max 10) |
|
||||
| 12 | synopsis | No | Text | Synopsis/abstract of the thesis |
|
||||
| 13 | context | No | Text | Context note |
|
||||
| 14 | remarks | No | Text | Additional remarks |
|
||||
| 15 | language | No | String | Language (e.g., Français, English, Nederlands) |
|
||||
| 16 | access | No | String | Access authorization |
|
||||
| 17 | license | No | String | License information |
|
||||
| 18 | size_info | No | String | File size information |
|
||||
| 19 | jury_points | No | Float | Jury score (out of 20) |
|
||||
| 20 | baiu_link | No | String | Link to BAIU (institutional archive) |
|
||||
- **Encoding:** UTF-8
|
||||
- **Delimiter:** comma (`,`), quoting with double quotes (`""` escapes a quote)
|
||||
- **Header detection:** the importer scans up to **8 rows** looking for a header
|
||||
row containing at least **11 recognised column names**. Matching is flexible:
|
||||
prefix/substring/variant matching (e.g. `contact.visible` matches `contact`,
|
||||
`Licence` ↔ `License`, `Promoteur·ice(s) université` matches `ulb`).
|
||||
- **No header fallback:** if no header row is found, the importer falls back to
|
||||
**positional** interpretation, skipping the first **4 rows** (the legacy
|
||||
template layout with label/description rows).
|
||||
|
||||
## Field Details
|
||||
So import files are robust to column reordering as long as meaningful headers
|
||||
are present; legacy fixed-position files (skip-4) also still work.
|
||||
|
||||
### Required Fields
|
||||
- **title**: Must not be empty
|
||||
- **year**: Must not be empty and must be a valid integer
|
||||
---
|
||||
|
||||
### Multi-Value Fields
|
||||
These fields accept multiple values separated by commas:
|
||||
- **authors**: e.g., `"John Doe, Jane Smith"`
|
||||
- **supervisors**: e.g., `"Prof. A, Prof. B"`
|
||||
- **keywords**: Maximum 10 keywords, e.g., `"art, design, digital"`
|
||||
- **formats**: e.g., `"PDF, Video, Installation"`
|
||||
## Recognised columns
|
||||
|
||||
### Orientation Codes
|
||||
Valid orientation codes and their full names:
|
||||
Headings are matched by the keywords below (case-insensitive). They mirror the
|
||||
**export** header names in `ExportController::CSV_HEADERS`.
|
||||
|
||||
| Export header (order in export) | Imported as | Notes |
|
||||
|---|---|---|
|
||||
| `Identifiant` | identifier | Optional; if it already exists, the row is skipped |
|
||||
| `Titre` | title | **Required** |
|
||||
| `Sous-titre` | subtitle | |
|
||||
| `Auteur·ice(s)` | authors | comma-separated; first author gets `Contact` |
|
||||
| `Contact` | contact | email; `OUI`/`NON` artefacts emptied |
|
||||
| `Promoteur·ice(s) interne` | promoteur (interne) | role `promoteur`, `is_ulb=0` |
|
||||
| `Lecteur·ice(s) interne` | lecteur (interne) | role `lecteur`, `is_external=0` |
|
||||
| `Lecteur·ice(s) externe` | lecteur (externe) | role `lecteur`, `is_external=1` |
|
||||
| `Promoteur·ice(s) université` | promoteur (ulb) | role `promoteur`, `is_ulb=1` |
|
||||
| `Format(s)` | formats | comma-separated; matched to `format_types` |
|
||||
| `Année` | année | **Required** unless derivable from `Identifiant` (`2024-003` → 2024) |
|
||||
| `AP` | ap | AP program code or name |
|
||||
| `Orientation` | orientation | code or full name (see below) |
|
||||
| `Finalité` | finalité | matched to `finality_types` |
|
||||
| `Mots-clés` | mots-clés | comma-separated; lowercase-normalised; max 10 |
|
||||
| `Synopsis` | synopsis | |
|
||||
| `Contexte` | contexte | context note |
|
||||
| `Remarques` | remarques | |
|
||||
| `Langue` | langue | comma-separated; stored lowercase; created if missing |
|
||||
| `Autorisation` | autorisation | access type name → `access_types`; default `Libre` if unknown |
|
||||
| `Licence` / `License` | license | licence name |
|
||||
| `Points sur 20` | points | float (jury points) |
|
||||
| `Lien BAIU` | lien baiu | institutional link |
|
||||
| `CC2r`, `Exemplaire BAIU`, `Exemplaire ERG` | *(export only)* | written by export; ignored on import |
|
||||
|
||||
> Positional-fallback indices (0-based): `0` Identifier, `1` Titre, `2`
|
||||
> Sous-titre, `3` Auteur, `4` Contact, `5` Promoteurs, `6` Lecteurs internes,
|
||||
> `7` Lecteurs externes, `8` Promoteurs ULB, `9` Formats, `10` Année, `11` AP,
|
||||
> `12` Orientation, `13` Finalité, `14` Mots-clés, `15` Synopsis, `16` Contexte,
|
||||
> `17` Remarques, `18` Langue, `19` Autorisation, `20` Licence, `21` Points,
|
||||
> `22` Lien BAIU.
|
||||
|
||||
The three trailing **export-only** columns (CC2r / Exemplaire BAIU / Exemplaire
|
||||
ERG) are not currently read back on import.
|
||||
|
||||
---
|
||||
|
||||
## Orientation & AP values
|
||||
|
||||
Orientation codes (legacy short form) and aliases are normalised to canonical
|
||||
DB names. Recognised codes:
|
||||
|
||||
```
|
||||
SC = Sculpture
|
||||
VI = Vidéographie
|
||||
CA = Cinéma d'animation
|
||||
IP = Installation-Performance
|
||||
PE = Peinture
|
||||
PH = Photographie
|
||||
DE = Dessin
|
||||
AN = Arts Numériques
|
||||
GR = Graphisme
|
||||
TY = Typographie
|
||||
DN = Design Numérique
|
||||
IL = Illustration
|
||||
BD = Bande-Dessinée
|
||||
SE = Sérigraphie
|
||||
GV = Gravure
|
||||
AN=Arts Numériques, DE=Dessin, CA=Cinéma d'animation, IP=Installation-Performance,
|
||||
PE=Peinture, PH=Photographie, SC=Sculpture, VI=Vidéographie, GR=Graphisme,
|
||||
TY=Typographie, DN=Design Numérique, IL=Illustration, BD=Bande-Dessinée,
|
||||
SE=Sérigraphie, GV=Gravure
|
||||
```
|
||||
|
||||
### AP Codes
|
||||
Valid AP program codes:
|
||||
- `DPM`
|
||||
- `LIENS`
|
||||
- `APS`
|
||||
AP programs (by code or name): `NS` Narration Spéculative, `DPM` Design et
|
||||
Politique du Multiple, `APS` Atelier Pratiques Situées, `LIENS`, `PACS`.
|
||||
|
||||
(These codes must match exactly what exists in the `ap_programs` table)
|
||||
---
|
||||
|
||||
### Language Values
|
||||
Languages should be provided with capital first letter:
|
||||
- `Français`
|
||||
- `English`
|
||||
- `Nederlands`
|
||||
- etc.
|
||||
## Import behaviour
|
||||
|
||||
### Format Values
|
||||
Common format values (case-insensitive, will be normalized):
|
||||
- `PDF`
|
||||
- `Video`
|
||||
- `Audio`
|
||||
- `Installation`
|
||||
- `Web`
|
||||
- etc.
|
||||
- **Transaction per row** — a failed row is rolled back and logged; import
|
||||
continues.
|
||||
- **Required:** `Titre` and `Année` (year may be derived from the identifier).
|
||||
- Empty rows (no title and no identifier) are skipped.
|
||||
- Authors, supervisors, languages and tags are auto-created if missing.
|
||||
- **Tag normalisation:** `strtolower`, collapse multiple spaces, de-duplicate,
|
||||
cap at 10.
|
||||
- Duplicate `Identifiant` → row skipped (not re-inserted).
|
||||
- Results summary reports: imported count, skipped count, and per-row
|
||||
`✓`/`✗` messages.
|
||||
|
||||
## Import Behavior
|
||||
---
|
||||
|
||||
### Row Processing
|
||||
1. Empty rows (no title and no identifier) are skipped
|
||||
2. Each row is processed in a transaction
|
||||
3. If a row fails, it is skipped and logged, but processing continues
|
||||
## Restore procedure
|
||||
|
||||
### Data Validation
|
||||
- If title or year is missing, the row is rejected
|
||||
- Invalid orientation codes result in no orientation being set (null)
|
||||
- Invalid AP codes result in no AP program being set (null)
|
||||
- Keywords are limited to first 10 if more are provided
|
||||
|
||||
### Data Normalization
|
||||
- All string fields are trimmed of whitespace
|
||||
- Language and format values are normalized (first letter capitalized, rest lowercase)
|
||||
- Empty strings are converted to NULL in the database
|
||||
|
||||
### Entity Creation
|
||||
- Authors, supervisors, and keywords are automatically created if they don't exist
|
||||
- Existing authors are matched by name
|
||||
- Contact email is only associated with the first author
|
||||
|
||||
## Example CSV Structure
|
||||
|
||||
```csv
|
||||
|
||||
Identifiant,Titre,Sous-titre,Auteur·ice(s),Contact,Promoteur·ice(s),Format,Année,AP,Orientation,Finalité,Mots-clés,Synopsis,Contexte,Remarques,Langue,Autorisation,License,taille,Points sur 20,lien BAIU
|
||||
|
||||
TFE-2024-001,Mon projet artistique,Exploration du numérique,"Alice Dupont, Bob Martin",alice@example.com,Prof. Smith,PDF,2024,DPM,AN,Création,art numérique,digital art,interactive installation,Un projet explorant l'intersection de l'art et de la technologie,Réalisé dans le cadre du master,Très bon projet,Français,Public,CC-BY,250MB,16.5,https://baiu.example.org/12345
|
||||
TFE-2024-002,Design graphique moderne,,Charlie Brown,charlie@example.com,"Prof. A, Prof. B","PDF, Print",2024,LIENS,GR,Design,typographie,graphisme,design,Une exploration de la typographie contemporaine,,,English,Restricted,All rights reserved,50MB,15,
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
1. **Encoding problems**: Ensure file is saved as UTF-8
|
||||
2. **Missing columns**: All 21 columns must be present, even if empty
|
||||
3. **Line breaks in fields**: Ensure fields containing newlines are properly quoted
|
||||
4. **Quote escaping**: Use double quotes (`""`) to escape quotes within fields
|
||||
|
||||
### Import Results
|
||||
After import, the system will display:
|
||||
- Number of theses successfully imported
|
||||
- Number of rows skipped due to errors
|
||||
- Detailed line-by-line results with success (✓) or error (✗) indicators
|
||||
|
||||
## Notes
|
||||
|
||||
- The import process preserves the order of authors, supervisors, and keywords
|
||||
- The first author gets the contact email if provided
|
||||
- Duplicate detection is not performed - each import creates new entries
|
||||
- Failed rows do not stop the import process
|
||||
- All errors are logged to the server error log
|
||||
Full restore combines the three exports (CSV + DB + files). See
|
||||
[export.md](export.md) for the step-by-step restore of the database and files;
|
||||
the CSV is re-importable as described above when rebuilding from scratch.
|
||||
|
||||
+68
-142
@@ -1,172 +1,98 @@
|
||||
# Search Feature Documentation
|
||||
# Search & Répertoire — Documentation
|
||||
|
||||
## Overview
|
||||
The search feature allows users to search across theses using multiple criteria including full-text search and advanced filters.
|
||||
Two public browsing surfaces, both handled by `app/src/Controllers/SearchController.php`
|
||||
and routed by `app/src/Dispatcher.php`:
|
||||
|
||||
## Files Created/Modified
|
||||
| Route | Handler | Purpose |
|
||||
|-------|---------|---------|
|
||||
| `/search`, `/search.php` | `handleSearch()` | Full-text query + classic single filters |
|
||||
| `/repertoire`, `/repertoire.php` | `handleRepertoire()` | Browseable directory with multi-select filters |
|
||||
|
||||
### New Files
|
||||
1. **search.php** - Main search interface page
|
||||
2. **create_test_db.php** - Script to generate test database with sample data
|
||||
3. **search.md** - This documentation file
|
||||
Both only ever expose **published** theses (`is_published = 1`).
|
||||
|
||||
### Modified Files
|
||||
1. **Database.php** - Added search methods:
|
||||
- `searchTheses()` - Search with multiple filters
|
||||
- `countSearchResults()` - Count matching results
|
||||
- `getAvailableYears()` - Get all years from published theses
|
||||
- `getOrientations()` - Get all orientations
|
||||
- `getApPrograms()` - Get all AP programs
|
||||
- `getFinalityTypes()` - Get all finality types
|
||||
- `getUsedKeywords()` - Get keywords used in published theses
|
||||
- `getFormatTypes()` - Get all format types
|
||||
- `getLanguages()` - Get all languages
|
||||
---
|
||||
|
||||
2. **inc/header.php** - Added "Rechercher" link to navigation
|
||||
## HandleSearch (`/search`)
|
||||
|
||||
## Searchable Fields
|
||||
`handleSearch()` reads from `$_GET` and renders `app/templates/public/search.php`
|
||||
with the results fragment (`app/templates/partials/search-results.php`).
|
||||
|
||||
The search feature allows filtering by:
|
||||
Searchable text fields (via `Database::searchTheses()`):
|
||||
- Title, subtitle, synopsis, author names, supervisor names, tags/keywords.
|
||||
|
||||
1. **Full-text query** - Searches across:
|
||||
- Title
|
||||
- Subtitle
|
||||
- Synopsis
|
||||
- Author names
|
||||
- Supervisor names
|
||||
- Keywords
|
||||
Single-value filters (`collectSearchParams()`):
|
||||
- `query` — free text
|
||||
- `year` — exact year
|
||||
- `orientation` — artistic orientation
|
||||
- `ap_program` — AP program
|
||||
- `finality` — finality type
|
||||
- `format` — format
|
||||
- `keyword` — tag/keyword
|
||||
|
||||
2. **Year** - Filter by specific year
|
||||
Results are paginated (`limit = 20` default); the **search bar**
|
||||
(`app/templates/partials/search-bar.php`) submits a GET form to `/search`.
|
||||
|
||||
3. **Orientation** - Filter by artistic orientation:
|
||||
- Arts Numériques, Dessin, Cinéma d'animation, Installation-Performance
|
||||
- Peinture, Photographie, Sculpture, Vidéographie
|
||||
- Graphisme, Typographie, Design Numérique, Illustration
|
||||
- Bande-Dessinée, Sérigraphie, Gravure
|
||||
---
|
||||
|
||||
4. **AP Program** - Filter by atelier pratique:
|
||||
- Narration Spéculative
|
||||
- Design et Politique du Multiple (DPM)
|
||||
- Atelier Pratiques Situées (APS)
|
||||
- Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes (LIENS)
|
||||
## HandleRepertoire (`/repertoire`)
|
||||
|
||||
5. **Finality** - Filter by master finality:
|
||||
- Approfondi
|
||||
- Enseignement
|
||||
- Spécialisé
|
||||
`handleRepertoire()` reads multi-select filter arrays from `$_GET` and renders
|
||||
`app/templates/public/repertoire.php`, which uses the shared results partial
|
||||
and `app/templates/partials/repertoire-index.php`.
|
||||
|
||||
6. **Format** - Filter by work format:
|
||||
- Site web, Audio, Vidéo, Performance
|
||||
- Objet éditorial, Installation, Autre
|
||||
Multi-select filters (`collectFilterParams()`, each an array, `_GET` keys):
|
||||
- `fy[]` — years (validated to 1900–2100)
|
||||
- `ap[]` — AP program names
|
||||
- `or[]` — orientations
|
||||
- `fi[]` — finalities
|
||||
- `kw[]` — keywords/tags
|
||||
|
||||
7. **Language** - Filter by language (Français, Anglais)
|
||||
Each value is trimmed, length-capped (≤ 100), de-duplicated, and passed through
|
||||
as sanitised strings — no direct user input reaches SQL.
|
||||
|
||||
8. **Keyword** - Filter by specific keyword
|
||||
There is also an HTMX **student preview** popover at
|
||||
`/repertoire/student-preview` (`handleStudentPreview()` → `student-preview.php`).
|
||||
|
||||
9. **Type** - Filter by thesis type:
|
||||
- TFE (final thesis projects)
|
||||
- Doctoral theses
|
||||
---
|
||||
|
||||
## Testing the Search Feature
|
||||
## Rate limiting
|
||||
|
||||
### 1. Create Test Database
|
||||
Run the script to generate sample data:
|
||||
```bash
|
||||
cd /home/padlock/dev/posterg-website/front-backend
|
||||
php create_test_db.php
|
||||
```
|
||||
Search is rate-limited via `app/src/RateLimit.php`. See the nginx config
|
||||
(`nginx/xamxam.conf`) for the matching server-side limits.
|
||||
|
||||
This will create `test.db` in the `formulaire/` directory with:
|
||||
- 6 sample theses (various years, orientations, and programs)
|
||||
- 5 sample authors
|
||||
- 3 sample supervisors
|
||||
- 20 keywords
|
||||
- Complete relationships (authors, supervisors, keywords, formats, languages)
|
||||
---
|
||||
|
||||
### 2. Access the Search Page
|
||||
Navigate to: `search.php`
|
||||
## Database access
|
||||
|
||||
### 3. Test Search Scenarios
|
||||
- **Full-text + single filters:** `Database::searchTheses(array $params, $limit, $offset)`
|
||||
and `Database::countSearchResults(array $params)`.
|
||||
- **Keyword/tag autocomplete:** `Database::searchTags(string $query)`.
|
||||
- **Supervisor autocomplete:** `Database::searchSupervisors($query, $role)`.
|
||||
- **Language autocomplete:** `Database::searchLanguages(string $query)`.
|
||||
|
||||
#### Scenario 1: Full-text Search
|
||||
- Enter "urbain" in the search field
|
||||
- Should find: "Espaces Urbains et Narration Collective"
|
||||
Queries operate on `v_theses_public`; keyword matching joins the `thesis_tags` /
|
||||
`tags` tables (keywords are stored as lowercase-normalised **tags**, not a
|
||||
`keywords`/`thesis_keywords` set — see [database.md](database.md)).
|
||||
|
||||
#### Scenario 2: Filter by Year
|
||||
- Select year: 2024
|
||||
- Should find: 3 theses from 2024
|
||||
All queries use PDO prepared statements and escape `%`/`_` for `LIKE`
|
||||
(`Database::escapeLikeString`) to prevent wildcard injection.
|
||||
|
||||
#### Scenario 3: Filter by Orientation
|
||||
- Select orientation: "Installation-Performance"
|
||||
- Should find: 2 theses
|
||||
---
|
||||
|
||||
#### Scenario 4: Filter by AP Program
|
||||
- Select AP: "Narration Spéculative"
|
||||
- Should find: 2 theses
|
||||
## Performance notes
|
||||
|
||||
#### Scenario 5: Combined Filters
|
||||
- Enter "performance" in search field
|
||||
- Select year: 2024
|
||||
- Should find: 1 thesis ("Corps et Technologies")
|
||||
- Critical text/filter columns and the junction tables are indexed
|
||||
(`idx_theses_pub_year`, `idx_theses_*`, `idx_thesis_tags_*`, …).
|
||||
- `v_theses_public` pre-computes the joins for the common read path.
|
||||
- The repertoire filters operate on indexed lookup columns.
|
||||
|
||||
#### Scenario 6: Keyword Search
|
||||
- Select keyword: "écologie"
|
||||
- Should find: "Écologies Affectives"
|
||||
---
|
||||
|
||||
## Database Schema Reference
|
||||
## Future enhancements (not yet implemented)
|
||||
|
||||
The search uses the `v_theses_public` view which combines:
|
||||
- Main thesis data from `theses` table
|
||||
- Related authors via `thesis_authors` junction table
|
||||
- Related supervisors via `thesis_supervisors` junction table
|
||||
- Related keywords via `thesis_keywords` junction table
|
||||
- Related formats via `thesis_formats` junction table
|
||||
- Related languages via `thesis_languages` junction table
|
||||
- Predefined values from lookup tables (orientations, ap_programs, finality_types, etc.)
|
||||
The historical `search.md` listed potential automplete/faceted-search/export
|
||||
ideas. Status:
|
||||
|
||||
## Features
|
||||
|
||||
### Pagination
|
||||
- Results are paginated (20 items per page)
|
||||
- Previous/Next navigation
|
||||
- Numbered page links
|
||||
|
||||
### Result Display
|
||||
- Shows total number of results
|
||||
- Card-based layout matching the main index page
|
||||
- Displays: title, author, year, synopsis excerpt
|
||||
- Links to full thesis detail page
|
||||
|
||||
### User Experience
|
||||
- All filters are optional
|
||||
- Filters can be combined
|
||||
- "Réinitialiser" button to clear all filters
|
||||
- Maintains filter state during pagination
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- All user inputs are sanitized using `htmlspecialchars()`
|
||||
- SQL queries use prepared statements with parameter binding
|
||||
- No direct SQL injection risk
|
||||
- Only published theses are searchable (`is_published = 1`)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
1. **Auto-complete** - Suggest keywords/authors as user types
|
||||
2. **Faceted search** - Show filter counts (e.g., "Peinture (12)")
|
||||
3. **Sort options** - Sort by year, title, relevance
|
||||
4. **Save searches** - Allow users to bookmark search queries
|
||||
5. **Export results** - Export search results as CSV/JSON
|
||||
6. **Advanced boolean search** - Support AND/OR/NOT operators
|
||||
7. **Search highlights** - Highlight matching terms in results
|
||||
8. **Related theses** - Show similar works based on keywords
|
||||
9. **Statistics** - Show search analytics and popular queries
|
||||
10. **AJAX search** - Live search without page reload
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- Uses SQLite LIKE operator for text matching (case-insensitive)
|
||||
- Searches across GROUP_CONCAT fields in the view for many-to-many relationships
|
||||
- Efficient use of indexes defined in schema.sql
|
||||
- Compatible with existing Database.php singleton pattern
|
||||
- Auto-complete for tags exists at the form level (`searchTags`)
|
||||
- Faceted counts, saved searches, result export, and advanced boolean operators
|
||||
are **not** implemented
|
||||
|
||||
+71
-58
@@ -1,82 +1,95 @@
|
||||
# Security
|
||||
|
||||
Vulnerability analysis and resolution status for posterg-website.
|
||||
Current security posture for XAMXAM.
|
||||
|
||||
> Based on security audit (2026-02-08). All items tracked below.
|
||||
> This supersedes the earlier `security.md` (2026-02-08 audit). The original
|
||||
> 16-item audit is closed; the items below reflect the current state.
|
||||
|
||||
---
|
||||
|
||||
## Resolved
|
||||
## Authentication — admin
|
||||
|
||||
### Infrastructure / Deployment
|
||||
- **PHP session auth:** `app/src/AdminAuth.php`. Password-only (no username).
|
||||
Credentials previously in a gitignored PHP file; the current build stores the
|
||||
bcrypt hash in `site_settings.admin_password_hash` (manageable from
|
||||
`/admin/account`).
|
||||
- `AdminAuth::requireLogin()` guards every admin action/route.
|
||||
- Session cookies hardened: `HttpOnly`, `SameSite=Strict`, `Secure`,
|
||||
`Path=/admin`; regenerated on login.
|
||||
- nginx `auth_basic` layer has been removed; the PHP session layer is the only
|
||||
gate. (LDAP-based login is a proposed future enhancement — see
|
||||
`LDAP_AUTH_PLAN.md` / `LDAP_SPEC.md`. It is **not** implemented.)
|
||||
|
||||
| # | Issue | Severity | Resolution |
|
||||
|---|-------|----------|------------|
|
||||
| 1 | No HTTPS — admin credentials exposed in transit | 🔴 CRITICAL | TLS terminated upstream by reverse proxy. nginx.conf doesn't need to handle TLS directly. |
|
||||
| 3 | Uploaded files stored inside webroot | 🟠 HIGH | Storage moved to `STORAGE_ROOT` (`/var/www/posterg/storage/`), defined in `config/bootstrap.php`. |
|
||||
| 4 | File path mismatch — media broken & insecure | 🟠 HIGH | DB paths now storage-relative. New `public/media.php` serves files safely. `memoire.php` and `search.php` use `/media.php?path=…`. Cover recording fixed. |
|
||||
| 5 | Rate limiter bypassed by IP spoofing (`X-Forwarded-For`) | 🟠 HIGH | `src/RateLimit.php` `getClientIdentifier()` uses `REMOTE_ADDR` only. |
|
||||
| 6 | `.htaccess` rules silently ignored by nginx | 🟠 HIGH | All rules ported to `nginx/posterg.conf`. See `nginx/HTACCESS_TO_NGINX.md`. |
|
||||
| 13 | Deprecated `X-XSS-Protection` header | 🔵 LOW | Removed from `nginx/posterg.conf`. |
|
||||
## Transport & headers
|
||||
|
||||
### Frontend / Assets
|
||||
Enforced in `nginx/xamxam.conf` (see `nginx/docs/SECURITY_HEADERS.md`):
|
||||
|
||||
| # | Issue | Severity | Resolution |
|
||||
|---|-------|----------|------------|
|
||||
| 10 | CDN stylesheet without SRI | 🟡 MEDIUM | CDN will not be used in production. Self-hosted, eliminating supply-chain risk. |
|
||||
- **HSTS** (`Strict-Transport-Security`, 730 days, preload)
|
||||
- **CSP** — `default-src 'self'; … frame-ancestors 'none'` on public pages;
|
||||
`frame-ancestors 'self'` where the app embeds allowed content; admin CSP
|
||||
includes `script-src 'unsafe-inline'` for the OverType editor. `object-src 'none'`.
|
||||
- `X-Frame-Options: DENY` (clickjacking)
|
||||
- `X-Content-Type-Options: nosniff`
|
||||
- `Referrer-Policy: strict-origin-when-cross-origin`
|
||||
- `Permissions-Policy` (geo/mic/camera disabled)
|
||||
- `Cross-Origin-Opener-Policy` / `Cross-Origin-Resource-Policy: same-origin`
|
||||
- `X-Robots-Tag: noindex, nofollow` on `/admin/`
|
||||
|
||||
### Code Quality / Defence in Depth
|
||||
`X-XSS-Protection` is intentionally **omitted** (deprecated; see
|
||||
`nginx/docs/SECURITY_HEADERS.md` for rationale).
|
||||
|
||||
| # | Issue | Severity | Resolution |
|
||||
|---|-------|----------|------------|
|
||||
| 14 | Missing `rel="noreferrer"` on external links | 🔵 LOW | `rel="noopener noreferrer"` applied in `public/admin/thanks.php`. |
|
||||
| 15 | Unescaped integer outputs | 🔵 LOW | Explicit `(int)` casts added in `public/index.php` and `public/search.php`. |
|
||||
| 16 | Redundant `DATABASE_PATH` constant | 🔵 LOW | Removed from `config/bootstrap.php`. |
|
||||
## Rate limiting
|
||||
|
||||
### Admin Panel — Authentication & Sessions
|
||||
Defined in the nginx config `limit_req_zone` and enforced by
|
||||
`app/src/RateLimit.php`:
|
||||
|
||||
| # | Issue | Severity | Resolution |
|
||||
|---|-------|----------|------------|
|
||||
| 2 | No PHP-level authentication in admin | 🔴 CRITICAL | `src/AdminAuth.php` implements session guard with `password_verify` + `session_regenerate_id`. All admin files call `AdminAuth::requireLogin()`. Credentials in gitignored `config/admin_credentials.php`. No-op when constant absent (dev/cli-server). |
|
||||
| 8 | Session cookies not hardened | 🟡 MEDIUM | Resolved with #2. `AdminAuth::startSession()` sets `HttpOnly=true`, `SameSite=Strict`, `Secure=true` (off on cli-server), `Path=/admin`, `Lifetime=0`. |
|
||||
- General requests: `30 r/m`
|
||||
- Search endpoint: `30 r/m`
|
||||
- Admin panel: `300 r/m` (burst 30)
|
||||
|
||||
---
|
||||
The PHP limiter uses `REMOTE_ADDR` only (not `X-Forwarded-For`) to avoid IP
|
||||
spoofing.
|
||||
|
||||
## In Progress
|
||||
## Files & storage
|
||||
|
||||
| # | Issue | Severity | Status |
|
||||
|---|-------|----------|--------|
|
||||
| 7 | LIKE wildcard injection in admin search | 🟡 MEDIUM | Public `Database::searchTheses()` escapes `%` and `_` correctly. Same pattern must be applied to admin search and any other raw LIKE queries. |
|
||||
- Uploads live **outside the webroot** under `app/storage/` (`tfe/`, `theses/`),
|
||||
served on demand via `MediaController`/`FileAccessController` through
|
||||
controlled endpoints (`/media`), not direct static access.
|
||||
- nginx blocks `/storage`, `/src`, `/templates`, DB/sql/env/md files, and hidden
|
||||
files. The DocumentRoot is `app/public/` only.
|
||||
- Restricted-file downloads are gated by a request/approval/token flow
|
||||
(`file_access_*` tables).
|
||||
- Logs write to `app/storage/logs/` — outside the webroot, not publicly served.
|
||||
|
||||
---
|
||||
## Injection & output
|
||||
|
||||
## Not Yet Implemented
|
||||
- **SQL:** all queries use PDO prepared statements.
|
||||
- **LIKE wildcards:** `Database::escapeLikeString()` escapes `%` and `_`
|
||||
(public search and related queries).
|
||||
- **XSS:** `htmlspecialchars()` on all user-controlled output; integer/ID
|
||||
inputs cast.
|
||||
- **CSRF:** per-session tokens (`bin2hex(random_bytes(32))`), compared with
|
||||
`hash_equals()`.
|
||||
- **File uploads:** MIME validated (`finfo`); FilePond prevalidation +
|
||||
server-side checks in `FilepondHandler`.
|
||||
- **Markdown/HTML:** user content (pages, help blocks) rendered via
|
||||
`league/commonmark`; HTML in admin-editable content is expected.
|
||||
|
||||
| # | Issue | Severity | Files |
|
||||
|---|-------|----------|-------|
|
||||
| 11 | Missing Content-Security-Policy on public pages | 🟡 MEDIUM | `nginx/posterg.conf` → add CSP header to main server block |
|
||||
| 9 | `error.log` in web-accessible path | 🟡 MEDIUM | `public/admin/actions/formulaire.php` → use absolute path outside webroot |
|
||||
| 12 | CSV import missing server-side MIME validation | 🟡 MEDIUM | `public/admin/import.php` → add `finfo` MIME check |
|
||||
## Defence in depth / misc
|
||||
|
||||
---
|
||||
- External links use `rel="noopener noreferrer"`.
|
||||
- Decryption/`Crypto` failures are logged without leaking secrets; SMTP
|
||||
password is stored encrypted in `smtp_settings` and rotated via
|
||||
`just reencrypt-password`.
|
||||
- Admin operations are recorded in `admin_audit_log` (resource, action,
|
||||
status, IP, User-Agent).
|
||||
|
||||
## Priority Order
|
||||
## Areas to keep monitored
|
||||
|
||||
1. ~~🔴 CRITICAL~~ — All done (items 1–2)
|
||||
2. 🟡 **MEDIUM** — Items 7, 9, 11, 12 remaining
|
||||
3. ~~🔵 LOW~~ — All done (items 13–16)
|
||||
- Tightening the public CSP (`frame-ancestors 'none'` vs `'self'` on embed
|
||||
routes) is an active topic — see `TODO.md`.
|
||||
- Sensitive file downloads and their expiry/token handling are worth periodic
|
||||
review as usage grows.
|
||||
|
||||
---
|
||||
|
||||
## Good Practices Already in Place
|
||||
|
||||
- ✅ SQL injection: all queries use PDO prepared statements
|
||||
- ✅ XSS output: `htmlspecialchars()` on all user-controlled output
|
||||
- ✅ CSRF: tokens with `bin2hex(random_bytes(32))`, validated with `hash_equals()`
|
||||
- ✅ File upload: MIME type validated with `finfo`
|
||||
- ✅ Input validation: year, IDs, pagination cast to integers
|
||||
- ✅ LIKE wildcard escaping in public search (`Database::escapeLikeString`)
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2026-02-08*
|
||||
See also: `nginx/docs/SECURITY_HEADERS.md`, `nginx/docs/PHP_AUTH_LAYER.md`,
|
||||
`nginx/docs/PRODUCTION_DEPLOYMENT.md`.
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# Nginx Configuration - Post-ERG
|
||||
# Nginx Configuration - XAMXAM
|
||||
|
||||
This directory contains nginx configuration and documentation for the Post-ERG thesis website.
|
||||
This directory contains nginx configuration and documentation for the XAMXAM TFE website.
|
||||
|
||||
## 📁 Files
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
| Header | Value | Purpose |
|
||||
|--------|-------|---------|
|
||||
| `Strict-Transport-Security` | `max-age=63072000; includeSubDomains; preload;` | HSTS — forces HTTPS |
|
||||
| `Content-Security-Policy` | `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';` | Restrict resource origins; block embedding |
|
||||
| `X-Frame-Options` | `SAMEORIGIN` | Prevent clickjacking |
|
||||
| `Content-Security-Policy` | `default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';` | Restrict resource origins; block embedding (public pages) |
|
||||
| `X-Frame-Options` | `DENY` | Prevent clickjacking |
|
||||
| `X-Content-Type-Options` | `nosniff` | Prevent MIME-type sniffing |
|
||||
| `Referrer-Policy` | `strict-origin-when-cross-origin` | Limit referrer leakage |
|
||||
| `Permissions-Policy` | `geolocation=(), microphone=(), camera=()` | Disable unused browser APIs |
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
| Header | Value | Purpose |
|
||||
|--------|-------|---------|
|
||||
| `Content-Security-Policy` | `default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';` | Restrict resource origins; allows inline scripts for OverType editor |
|
||||
| `Content-Security-Policy` | `default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';` | Restrict resource origins; allows inline scripts for OverType editor |
|
||||
| `X-Robots-Tag` | `noindex, nofollow` | Prevent search-engine indexing of admin |
|
||||
|
||||
These were previously declared in `public/admin/.htaccess` as Apache
|
||||
@@ -38,5 +38,5 @@ to expose response bodies that would otherwise be blocked. Sending it provides
|
||||
no protection and may introduce risk.
|
||||
|
||||
**Correct mitigation:** a proper `Content-Security-Policy` header (now done for
|
||||
`/admin/`; public-page CSP is todo item #11).
|
||||
both `/admin/` and public pages; embed routes use `frame-ancestors 'self'`).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user