- Replace three <span class='search-filter-label'> with proper <label for='...'> elements in
search.php filter bar; add id attributes to the corresponding <select> elements so the
label/control association is programmatic (WCAG 1.3.1, 3.3.2).
- Rewrite the rate-limit 429 early-exit in search.php from a bare one-liner echo to a full
HTML document with lang='fr', viewport meta, and inline dark styles matching maintenance.php;
inject the retry countdown into the user-facing message (Template audit F).
- Fix PHP 8.x __wakeup() deprecation in Database.php singleton guard: replace the throw
statement with trigger_error(..., E_USER_ERROR) and add an explicit void return type
(Refactor audit C).
- templates/public/head.php: add centralised OG/Twitter tag rendering via $ogTags array;
supports type, title, description, url, image, image_alt, site_name, article_author,
article_published_time; twitter:card switches between summary_large_image / summary
based on presence of og:image
- public/tfe.php: populate full article OG tags — og:type=article, canonical URL,
og:image resolved from banner_path → first image file in thesis_files → omitted,
og:image:alt, article:author, article:published_time (year-01-01); twitter:card
summary_large_image when image present
- public/index.php, search.php, apropos.php, licence.php: add basic og:type=website
tags (title, description, canonical url, site_name)
Sharing a thesis link on Slack, WhatsApp, iMessage, or any social platform will now
render a rich preview card with the thesis title, synopsis excerpt, and cover/banner image.
Create templates/public/head.php accepting $pageTitle and $extraCss (array of
stylesheet hrefs), mirroring the existing templates/admin/head.php pattern.
The partial emits: DOCTYPE, <html lang=fr>, charset/viewport meta, favicon,
modern-normalize, common.css, any extra CSS links, and the dev-only live-reload
script. The live-reload snippet was previously copy-pasted verbatim into all
five public pages.
Updated pages:
- public/index.php ($pageTitle='Posterg', $extraCss=['assets/main.css'])
- public/search.php ($pageTitle='Répertoire – Posterg', search.css)
- public/tfe.php ($pageTitle=thesis title + suffix, tfe.css)
- public/apropos.php ($pageTitle='À Propos – Posterg', apropos.css)
- public/licence.php ($pageTitle=DB title + suffix, apropos.css)
tfe.php: removed redundant htmlspecialchars() call on $pageTitle (the partial
applies it); licence.php: renamed conflicting $page variable to $dbPage to
avoid collision with the shared $pageTitle expected by the partial.
All syntax checks and test suite pass (4/4).
- common.css: add font-display: swap to Combinedd.otf @font-face (eliminates FOIT)
- common.css: remove duplicate .site-nav__right block (identical to .site-nav__link);
update nav.php to use .site-nav__link on the À Propos link
- common.css: add .site-nav__link--active rule (opacity:1 + white underline); the class
was already applied in nav.php but had no CSS definition, making it invisible
- search.php: replace fully inline-styled pagination with .pagination-wrap / .pagination-btn
/ .pagination-info classes; add aria-disabled + tabindex=-1 on disabled links;
add aria-label on prev/next links
- search.css: add pagination rule block to match, keeping styles co-located with the page
The répertoire page was loading the full v_theses_public view
(15 JOINs + 8 GROUP_CONCAT temp B-trees) via getAllPublishedTheses()
just to build the student name → thesis-id map on the index page.
Only two columns (id, authors) were ever consumed by the template.
Add Database::getPublishedAuthors(): array
- Queries thesis_authors JOIN authors directly on the theses base table
- Filters on theses.is_published = 1 using the existing index
- Returns only id + GROUP_CONCAT(authors) — no view expansion
- Results verified identical to the old getAllPublishedTheses() output
Update search.php to call getPublishedAuthors() instead.
Mark getAllPublishedTheses() @deprecated in Database.php.
All tests pass.
Remove 5 unused ID-lookup helpers (getOrientationId, getAPProgramId,
getFinalityId, getLanguageId, getFormatId) — forms have always passed
FK ids directly from <select> elements; these methods were never called
outside import.php, which now uses inline PDO queries instead.
Collapse 13 alias methods down to the single canonical name for each:
getAllOrientations, getAllAPPrograms, getAllFinalityTypes,
getAllFormatTypes, getAllLanguages, getAllLicenseTypes,
getUsedTags, findOrCreateTag
The short-name variants (getOrientations, getApPrograms, etc.) and
compat aliases (getUsedKeywords, findOrCreateKeyword, getAllLicenseTypes
delegating to getLicenseTypes) are deleted. All call-sites updated:
- public/search.php: getOrientations→getAllOrientations, etc.
- public/admin/import.php: findOrCreateKeyword→findOrCreateTag,
thesis_keywords→thesis_tags, keyword_id→tag_id (fixes stale table
reference from pre-migration-001 that bypassed the M2M rename)
- tests/Unit/DatabaseTest.php: remove alias smoke-test (test 7)
Database.php: 948 → 848 lines (-100).
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).
- tests/Unit/DatabaseTest.php: tests 5-7 for findOrCreateTag round-trip, getUsedTags column, alias
- tests/Integration/SearchTest.php: tests 4-6 for tag subquery, full-text query, count consistency
- Database: getAllPublishedTheses() bypasses 100-row search cap for student index
- search.php: uses getAllPublishedTheses() for étudiantes column; all tests pass
- 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
- Reduce all spacing and padding in header for more compact fit
- Fix back button overflow by removing width: 100% and adding overflow handling
- Make filter section more compact with smaller fonts and spacing
- Add main-wrapper div to group main and footer
- Keep rounded corners (40px) on all three sections like main.css
- Footer stays at bottom of main content area
- Fix HTML structure: footer outside main, both inside wrapper
- Transform header into compact search bar with back button
- Move filters panel underneath search bar (collapsible)
- Display results in grid layout matching main.css style
- Add pagination controls in main section
- Show result count in footer
- Prevent overflow with responsive design and proper flex constraints
- Reduce padding and font sizes for denser layout
- Rename memoire.php to tfe.php throughout codebase
- Create dedicated tfe.css with rounded header/main/footer layout
- Move metadata (orientation, AP program, finality, keywords) to header
- Move back button from header to footer
- Create shared templates/head.php for common HTML head section
- Maintain rounded borders (40px) matching main site design
- Keep purple header (#9557b5), green main (#3c856b), dark footer (#222)
- Improve content readability with centered max-width layout
- Add responsive design for mobile devices
- 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)
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