Move the raw identifier-generation query and the INSERT INTO theses /
INSERT INTO thesis_authors statements out of formulaire.php into two new
Database methods:
generateThesisIdentifier(int $year): string
– counts existing theses for the year inside the open transaction so
concurrent workers cannot produce duplicate YYYY-NNN identifiers.
createThesis(array $data): int
– generates the identifier, INSERTs the thesis row, links the author
via thesis_authors (author_order=1), returns the new thesis ID.
getThesisIdentifier(int $id): string
– fetches the stored identifier for a thesis ID; used by formulaire.php
to reconstruct the upload path (storage/theses/YYYY/YYYY-NNN/).
formulaire.php now calls $db->createThesis([…]) + $db->getThesisIdentifier()
and no longer holds any raw PDO queries for the core thesis insert.
The $pdo local variable (previously $db->getPDO()) is removed entirely.
All four test suites (Unit, RateLimit, Integration, Security) pass.
HTML-escaping at write time stores &, < etc. in the DB, corrupting full-text
search, tag matching, exports, and any non-HTML consumer. PDO parameterised queries
already prevent SQL injection; templates call htmlspecialchars() on output.
sanitize_string() now does strip_tags(trim()) only — matching the pattern already
used by edit.php which never had this bug.
Also deleted the dead $problematique variable (read from POST[problématique] but
never passed to any INSERT or used anywhere in the codebase).
Implements the admin user management UI as a self-contained PHP password
change/set flow — no SSH or sudo required.
- public/admin/account.php: shows auth status (PHP hash present, credentials
file path), password change form (requires current password when one exists,
min 12 chars, confirm field), and a danger-zone form to delete the
credentials file entirely
- public/admin/actions/account.php: CSRF-guarded POST handler; verifies
current password via AdminAuth::login() before accepting a new one;
generates bcrypt (cost 12) hash; writes config/admin_credentials.php
atomically via a temp file + rename; regenerates session on success;
redirects to /admin/login.php when credentials are deleted
- templates/admin/head.php: 'Compte' nav link added (active on account.php)
- public/assets/admin.css: .admin-account-status, .admin-section-title,
.admin-field-hint, .admin-danger-zone component styles added
Note: the nginx htpasswd flow (manage-admin-users.sh) requires root on the
server and is intentionally kept as a CLI-only operation.
- 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)
Items resolved:
- #3 (HIGH): Move file uploads outside webroot to STORAGE_ROOT (/var/www/posterg/storage).
Uploads were previously stored in public/admin/actions/data/ which is web-accessible.
- #4 (HIGH): Align file paths and add media.php controller.
DB paths are now storage-relative (theses/YEAR/ID/file, covers/file).
New public/media.php serves files with path-traversal jail, MIME allow-list,
and proper caching headers. memoire.php and search.php updated to use /media.php?path=.
Also fixed: cover images were never recorded in thesis_files (broken INSERT).
- #5 (HIGH): RateLimit::getClientIdentifier() now uses REMOTE_ADDR only.
HTTP_X_FORWARDED_FOR and HTTP_CLIENT_IP are attacker-controlled headers that
allowed unlimited rate-limit bypass by rotating spoofed IPs.
- #6 (HIGH): Port public/admin/.htaccess security rules to nginx/posterg.conf.
Apache .htaccess directives are silently ignored by nginx; none were active.
CSP added to /admin/ location block, .log file denial added globally,
autoindex off made explicit. Documented in nginx/HTACCESS_TO_NGINX.md.
Supporting changes:
- config/bootstrap.php: add STORAGE_ROOT constant
- nginx/SECURITY_HEADERS.md: updated to reflect admin CSP and pending public CSP
- docs/TODO.SECURITY.md: items #3-6 moved to resolved; priority order updated