All admin action files (account, tag, page, edit, visibility, maintenance,
publish, formulaire) now call App::flash('error'|'success', ...) instead of
writing to raw per-page session keys ($_SESSION['error'], 'admin_error',
'edit_error', 'admin_success', 'edit_success', 'form_error').
All admin display pages (add, edit, account, tags, pages, index) now include
templates/partials/flash-messages.php instead of manually reading and
unsetting the legacy session keys and inlining their own alert HTML.
App::consumeFlash() already drained all legacy key variants as a safety net,
so the partial works correctly whether called from pages that were already
migrated or any remaining stragglers. No behaviour change for end users.
Four reusable PHP partials extracted to templates/partials/form/:
- text-field.php — single-line input (text/number/url); wraps input+hint in div,
skips the inner wrapper when no hint is present. Supports $type,
$placeholder, $required, $attrs, $hint, $id overrides.
- select-field.php — <select> with leading empty option; matches $selected against
option id OR option name string (handles view-sourced data where
orientation/ap/finality come back as name strings, not FK ids).
- checkbox-list.php — checkbox group (languages, formats); renders .admin-checkbox-list
with typed-string comparison so int ids from DB match string values.
- file-field.php — file input with accept/multiple/hint; appends [] to name when
$multiple is true.
Both add.php and edit.php rewritten to use the partials:
- ~15 repeated text-field divs collapsed to single-line include calls
- ~6 repeated select divs collapsed to single-line include calls
- 4 checkbox-list blocks collapsed to 2 calls each
- 3 file input blocks collapsed to single-line include calls
- Textarea fields (synopsis, context_note) kept inline — no partial for <textarea>
- Banner preview block in edit.php kept inline — conditional UI not generalised
Line count: add.php 251→93 (-158), edit.php 289→171 (-118)
Remove five presentational classes from admin forms and replace with
structural CSS selectors scoped to .admin-form:
- .admin-form-row → .admin-form > div:not(.admin-submit-wrap)
Grid layout (260px label col + 1fr input col) applied directly to div
children of the form; submit-wrap div excluded via :not().
- .admin-label → .admin-form > div:not(.admin-submit-wrap) > label
Scoped to the direct label child of each form row div; does not bleed
into nested checkbox labels inside .admin-checkbox-list.
- .admin-input / .admin-select / .admin-textarea
→ .admin-form input:not([type=checkbox|radio|file|hidden|submit])
→ .admin-form select
→ .admin-form textarea
Also extended to .admin-inline-form input/select (tags page) so the
tags table inputs retain identical base styling and focus colour.
Templates updated: add.php, edit.php, login.php, account.php,
pages-edit.php, import.php, tags.php,
templates/partials/form/jury-fieldset.php — all class= attributes for
the five removed classes stripped.
import.php: added 'admin-form' class alongside 'admin-import-area' so
its single file-input row gets the grid row treatment; submit div was
already using admin-submit-wrap so it is correctly excluded.
No visual change — selectors target the same elements as before.
- admin.css: replace .admin-alert / .admin-alert--error / .admin-alert--success
selectors with [role="alert"][data-type="error"] and [role="status"][data-type="success"]
- All 10 admin templates updated: <div class="admin-alert admin-alert--{type}">
becomes <p role="alert|status" data-type="error|success"> (or <div> for the
import.php multi-item list that contains a <ul>)
- flash-messages.php partial updated to match
- WCAG benefit: role="alert" is an ARIA live region — errors are announced
immediately by screen readers without focus movement (fixes WCAG 3.3.1, 4.1.2)
- role="status" (polite live region) used for success messages — announced
without interrupting the user
- Removes two BEM modifier classes; CSS now targets element semantics directly
The jury composition fieldset (président·e, promoteur·ice + external checkbox, dynamic
lecteur·ices list with JS add/remove) was copy-pasted verbatim between the two longest
admin forms.
- Created templates/partials/form/jury-fieldset.php
- Consumes $juryPresident, $juryPromoteur, $juryPromoteurExt, $juryLecteurs[]
- Handles both add-mode (falls back to old()/wasSelected() flash helpers) and
edit-mode (pre-populates from DB-loaded variables)
- $juryIdx initialised from max(count($juryLecteurs), 1) — correct for both modes
- add.php: 311 → 251 lines (-60); entire fieldset + <script> replaced with one require
- edit.php: 359 → 289 lines (-70); PHP variable extraction kept inline before require
Replace four presentational class names in admin.css with structural selectors
that target native HTML elements already present in every admin template:
.admin-main → .admin-body main
.admin-page-title → .admin-body main > h1
.admin-table → .admin-body table
.admin-fieldset → .admin-body fieldset
.admin-fieldset-legend → .admin-body legend
Also migrate the .admin-main > section / h2 / dl / dt / dd block to
.admin-body main > section so the thanks-page section styles survive.
Add .admin-body main > table { margin-top: 1.5rem } to absorb the inline
style="margin-top:1.5rem" that was on tags.php's <table class="admin-table">.
All 10 affected admin templates updated (add, edit, account, index, import,
pages, pages-edit, tags, system, thanks) — class attributes removed where
the element alone is now the selector. Zero visual changes.
- admin.css: remove .admin-hint and .admin-field-hint class rules; add
.admin-body form small with the same font-size/color/margin properties
plus display:block so it stacks below sibling inputs; stub comment left
where .admin-field-hint was to document the change
- add.php: 5× <p class="admin-hint"> → <small>
- edit.php: 3× <p class="admin-hint"> → <small>
- import.php: <div class="admin-hint"> → <small> (block hint below CSV input)
- pages-edit.php: class="admin-hint" removed from already-correct <small>
- account.php: <p class="admin-field-hint"> → <small>
Hint text is now styled purely via the semantic element selector; no class
required on any hint element in admin templates.
- login.php: removed style= on .admin-form-row and .admin-label (already covered
by .admin-login-box scoped rules); extracted submit-wrap spacing and full-width
button to .admin-login-box .admin-submit-wrap and .admin-login-box .admin-btn
- account.php: style="margin-top:3rem" on danger-zone heading moved to
.admin-section-title--danger modifier; <span style="color:..."> replaced with
<small> element styled via .admin-danger-zone__description small
- add.php / edit.php / pages-edit.php: all style="align-items:start" removed from
.admin-form-row (redundant — already the CSS default at line 116 of admin.css);
banner preview inline styles extracted to .admin-banner-preview / .admin-banner-preview img;
add-jury button margin extracted to .admin-add-jury-btn; cancel links use .admin-cancel-link
Zero inline style= attributes remain in login, account, add, edit, pages-edit.
admin/index.php — status badges (WCAG 1.4.1 Use of Colour):
- Published badge: prefix ● symbol (aria-hidden) + aria-label="Statut : Publié"
- Pending badge: prefix ◌ symbol (aria-hidden) + aria-label="Statut : En attente"
- Access badges (Libre/Interne/Interdit): prefix ○/◑/● symbol per type (aria-hidden)
+ aria-label="Accès : [type]"; symbol chosen from a PHP map keyed on the slug
Each badge now communicates its state through shape AND colour, not colour alone.
admin/index.php — ✕ Réinitialiser link (WCAG 2.5.3 / 1.1.1):
- ✕ wrapped in <span aria-hidden="true"> so the decorative symbol is skipped by
screen readers; accessible name remains "Réinitialiser"
admin/add.php + admin/edit.php — jury remove buttons (WCAG 2.5.3):
- All four ✕ remove buttons (2 static template rows + 2 JS-generated innerHTML strings)
given aria-label="Supprimer ce lecteur"; the bare ✕ Unicode character has no
speech equivalent so the aria-label replaces rather than supplements the label
SQLite performance (Database::__construct):
- PRAGMA journal_mode = WAL: eliminates full-DB read locks on write, safe
for concurrent PHP-FPM workers
- PRAGMA synchronous = NORMAL: durable on commit without full fsync per write
- PRAGMA cache_size = -8000: ~8 MB page cache per connection
Accessibility foundation (WCAG 2.1 AA):
- common.css: add .sr-only utility, .skip-link (hidden until focused),
global :focus-visible (2px purple outline, 2px offset),
prefers-reduced-motion guard; remove bare outline:none from
.site-search__input
- admin.css: same :focus-visible, skip-link, and motion guard scoped to
admin purple; remove outline:none from .admin-input/.admin-select/
.admin-textarea and .admin-filters select (both had :focus border rules
already, so focus is still visually communicated)
- search.css: remove outline:none from .search-filter-select (already has
:focus border-color rule)
- All 5 public pages (index, search, tfe, apropos, licence): add
<a href="#main-content" class="skip-link"> as first child of <body>;
add id="main-content" to <main>
- templates/admin/head.php: same skip link; aria-label="Navigation admin"
on <nav>; id="main-content" on all 10 admin <main> elements
All 4 test suites pass (unit, integration, security, rate-limit).
- Flat purple-gradient nav bar with POSTERG/RÉPERTOIRE/À PROPOS links
- Full-width search bar with icon, bottom-border only, below nav
- Home: white bg, media card grid (thumbnail + author/title label below)
- Répertoire: 4-column index (Années/Catégories/Étudiantes/Mots-clés)
- TFE: 2-column layout (large text left, media right)
- À Propos: 2-column, large monospace text, new apropos.php page
- Admin: dark theme (#1a1a1a), purple gradient nav, bottom-border inputs
- New shared partials: templates/nav.php, templates/search-bar.php
- Rewrote all CSS: common, main, search, tfe, apropos, admin
- Created /templates for main site (header.php, footer.php)
- Created /templates/admin for admin section (head.php, footer.php)
- Removed /public/includes and /public/admin/inc
- Updated all references in code and docs
- Tests passing ✅
Cleaner separation: /public only contains web-accessible files (PHP entry points + assets)
- lib/AdminAuth.php: new class with requireLogin(), login(), logout(),
isAuthenticated(); starts session with hardened cookie params
(HttpOnly, SameSite=Strict, Secure, Path=/admin) — also resolves
item #8 (session cookie hardening)
- requireLogin() auto-authenticates from nginx Basic Auth credentials
($_SERVER['PHP_AUTH_PW']) so the user only sees one browser prompt;
falls back to /admin/login.php if the proxy is absent/misconfigured
- config/admin_credentials.php: gitignored credential store; define
ADMIN_PASSWORD_HASH with a bcrypt hash to enable PHP auth
- config/admin_credentials.example.php: template for the above
- config/bootstrap.php: auto-loads admin_credentials.php if present
- .gitignore: exclude config/admin_credentials.php
- public/admin/login.php: fallback login form (shown only when nginx
Basic Auth is bypassed / proxy absent)
- public/admin/logout.php: session destruction + redirect to login
- All 7 admin PHP files: replace session_start() with
AdminAuth::requireLogin() (defence-in-depth behind nginx Basic Auth)
- public/admin/inc/head.php: Déconnexion button when ADMIN_PASSWORD_HASH
is defined
- nginx/PHP_AUTH_LAYER.md: documents dual-auth architecture, UX flow,
and setup instructions
- docs/TODO.SECURITY.md: items #2 and #8 moved to Resolved; priority
order updated (all CRITICAL done)