fix: add missing bootstrap require in markdown-cheatsheet-fragment.php, add bottom padding to dialog

This commit is contained in:
Pontoporeia
2026-07-03 15:24:07 +02:00
parent 00a106138e
commit e6a989bb36
7 changed files with 30 additions and 175 deletions
+2 -83
View File
@@ -1,83 +1,2 @@
# TODO - [x] Fix markdown cheatsheet modal 500 error — missing `require_once bootstrap.php` in `app/public/admin/markdown-cheatsheet-fragment.php`
- [x] Add `padding-bottom` to `.md-cheatsheet-dialog` so last table row isn't flush against dialog edge
> Last updated: 2026-06-24
> Context: Security audit — fix open redirects, fragment auth, dead code, CSRF gaps
## Deferred / Blocked
- [ ] #tighten-csp Tighten CSP to remove 'unsafe-inline' after inline JS extraction
## Pending
- [ ] #rep-student-touch Replace hover student popover with tap-to-open drawer for mobile `(repertoire.php, repertoire.css, repertoire-student-popover.js)`
## Deferred / Blocked
- [ ] #tighten-csp Tighten CSP to remove 'unsafe-inline' + 'unsafe-eval' from script-src — blocked on HTMX's use of new Function() for 'unsafe-eval'; 'unsafe-inline' needs OverType init + flash-warning inline scripts moved to external files, plus maintenance.php + validate-access.php inline styles extracted
## Completed
- [x] #rep-polish Polish: scroll-position memory on HTMX swap, animation tuning `(repertoire.css)`
- [x] #icon-color-verify Verify icon colors render correctly across all pages (header, admin tables, forms, dialogs, cleanup modal) ✓
- [x] #sec-open-redirect Fix open redirect in tag.php + language.php (protocol-relative URL bypass via str_starts_with) ✓
- [x] #build-pipeline Setup biome + rolldown + lightningcss build pipeline ✓
- [x] #build-packagejson Create package.json with devDependencies ✓
- [x] #build-biomecss Update biome.json to handle CSS formatting ✓
- [x] #build-rolldown-config Create rolldown.config.mjs + build-js.mjs for JS bundling ✓
- [x] #build-lightningcss Add lightningcss CSS bundling (resolve @import chain) ✓
- [x] #build-justfile Add just build/deploy recipes + integrate build into deploy ✓
- [x] #build-head Update head.php + form-page.php + controllers to use bundled assets ✓
- [x] #build-gitignore Add dist/ to .gitignore ✓
- [x] #build-cssfix Fix stray `}` syntax error in admin.css line 305 ✓
- [x] #sec-fragments-auth Gate partagé fragments on share_active session (read-only fragment renderers — no CSRF needed) ✓
- [x] #sec-retry-csrf Add CSRF check to partage/retry-email.php POST ✓
- [x] #sec-cleanup-dead-code Remove dead App::verifyCsrf() or refactor action handlers to use it ✓
- [x] #gzip-nginx Enable gzip compression in nginx config `(nginx/xamxam.conf)`
- [x] #extract-inline-js Move inline JS to external files across 17 templates → 15 new JS files created `(app/public/assets/js/app/*.js)`
- [x] #inline-icon-helper Create `icon()` PHP helper + auto-load in bootstrap `(src/icon.php, bootstrap.php)`
- [x] #icon-fill-currentcolor Ensure all 36 icon SVGs use fill="currentColor" or stroke="currentColor" `(assets/icons/*.svg)`
- [x] #migrate-all-img-icons Replace all remaining `<img src="/assets/icons/">` with `<?= icon() ?>` across 26 template files ✓
- [x] #icon-css-cleanup Remove `<img>` filter hacks from admin.css, add explicit sizes for header nav + public search icons `(admin.css, search.css)`
- [x] #icon-em-sizing Set icon helper to output width="1em" height="1em" so icons scale with parent font-size `(icon.php)`
- [x] #inline-svg-to-icons Move all inline SVGs to asset icon files, ensure currentColor fill for proper color inheritance `(33 icons, 22 files, 67+ inline SVGs replaced, 2 dynamic SVGs remain)`
- [x] #cleanup-modal-fixes Fix nettoyage modal: SVG caret icons, margin→padding, BBBDMSans summary `(admin.css, details.css)`
- [x] #structure-formulaire-page Move "Structure du Formulaire" from contenus.php to its own dedicated page with back button `(structure-formulaire.php [new], contenus.php)`
- [x] #contenus-indexes Add index on thesis_languages(language_id) + tags(deleted_at, name); fix count queries to exclude soft-deleted theses `(Database.php, DatabaseMigrations.php, schema.sql, migrations/applied/041_thesis_languages_index.sql)`
- [x] #peertube-orphans-check Add Peertube orphan video check + relink in admin — listChannelVideos (PeerTubeService), peertube-orphans.php endpoint, UI in nettoyage dialog, relink support on edit page `(peertube-relink.php, peertube-browser.php, fichiers-fragment.php, file-upload-filepond.js)`
- [x] #tfe-optional-formats Make TFE file optional when format is Site web (1), Performance (4) or Installation (6) — fixed incorrect format IDs [3→1,4,6] + added client-side JS toggle for TFE required/asterisk. Note d'intention remains required. 🎯 `(fichiers-fragment.php, file-upload-filepond.js)`
- [x] #typography-weight-300 Set search placeholder + apropos/charte/licence <p> content to BBBDMSans weight 300 `(search.css, apropos.css)`
- [x] #toc-parts-uppercase Hardcode "PARTIES" uppercase + black bottom border on TOC label `(about.php, charte.php, licence.php, apropos.css)`
- [x] #apropos-overflow Prevent #apropos-intro and content-section children from overflowing `(apropos.css)`
- [x] #toc-navigation Fix TOC links not navigating to headings — added `apply_id_to_heading: true` to CommonMark config so IDs land on headings not hidden <a> elements; added scroll-margin-top to headings; unstuck main from flex container so sticky TOC works for full page height `(CharteController.php, LicenceController.php, apropos.css)`
- [x] #apropos-toc-style Fix TOC "Parties" label: Ductus font + lowercase, remove border-left from links, match global link style; rename .apropos-content → section.content, .apropos-section → .content-section, remove .prose wrapper `(apropos.css, about.php, charte.php, licence.php)`
- [x] #apropos-toc-confirm Fixed sticky TOC: removed `flex: 1; min-height: 0` on main for apropos-body so the sticky container is full content height; added `max-height` + `overflow-y: auto` to TOC for long lists `(apropos.css)`
- [x] #fix-finality-types Create standalone script + just command to rename finality types (Approfondi→Approfondie, Enseignement→Didactique, Spécialisé→Spécialisée) `(scripts/fix-finality-types.php, justfile)`
- [x] #context-note-synopsis Display contextual note above synopsis (italic) instead of in meta column on TFE page `(tfe.php, tfe.css)`
- [x] #decouple-contacts Decouple contact_visible (public) & contact_interne (private email): backend already decoupled; made contact_public checkbox functional in admin add/edit forms; contact_public now controls TFE page visibility `(FormBootstrap.php, ThesisCreateController.php, ThesisEditController.php, tfe.php, form.php)`
- [x] #csrf-rotation-race Stop CSRF token rotation in draft.php + remove hx-post from <form> — both broke FilePond uploads and form submission `(admin/actions/draft.php, partage/fragments/draft.php, FormBootstrap.php, pill-search.js)`
- [x] #adminold-return-type Fix adminOld closure return type from `:string` to `:string|array` `(FormBootstrap.php)`
- [x] #duration-integer-units Make duration field: integer for pages/Mo, dedicated h/m/s time inputs `(form.php, ThesisCreateController.php, tfe.php, form-base.css)`
- [x] #licence-svg-fix Fix licence details/summary SVG: width 1rem, inline-flex layout `(fieldset-licence-explanation.php, form-base.css)`
- [x] #restore-languages Un-soft-delete anglais (id=2) and néerlandais (id=71) in dev DB ✓
- [x] #php-upload-limits Increase PHP upload_max_filesize to 8G, post_max_size to 8.5G `(.user.ini)`
- [x] #formdata-fieldset-crash Remove leftover debug console.log that called new FormData(fieldset) `(admin/footer.php)`
- [x] #csp-media-iframe-fix Fix CSP `frame-ancestors 'none'` blocking PDF iframes — replaced `try_files` redirect with direct `fastcgi_pass` in `location = /media` so `add_header` CSP override survives internal nginx redirect `(nginx/xamxam.conf)`
- [x] #duration-migration Add migration to reintroduce `duration_value` and `duration_unit` columns + update views `(migrations/applied/040_duration_fields.sql)`
- [x] #duration-database Update `createThesis`, `updateThesis`, `getThesisRawFields` in Database `(Database.php)`
- [x] #duration-controllers Handle duration in `ThesisCreateController` and `ThesisEditController` `(ThesisCreateController.php, ThesisEditController.php)`
- [x] #duration-form Add duration fieldset (value + unit dropdown) to form template `(templates/partials/form/form.php)`
- [x] #duration-display Show duration on public TFE detail page `(templates/public/tfe.php)`
- [x] #duration-view Include duration in v_theses_full and v_theses_public `(migrations/applied/040_duration_fields.sql, schema.sql, schema.sql.new)`
- [x] #duration-bootstrapWire Wire duration variables through FormBootstrap adminFormVariables `(FormBootstrap.php)`
- [x] #cleanup-drafts Add periodic cleanup job for orphaned drafts (`Database.php`, `justfile`, `deploy/xamxam-cleanup.cron`, `scripts/cleanup-drafts.php`) ✓
- [x] #form-setup-helper Add `FormBootstrap` helper class to reduce bootstrap duplication across add/edit/partage `(admin/add.php)` `(admin/edit.php)`
- [x] #two-phase-commit Add two-phase commit: INSERT thesis `status='draft'`, COMMIT, move files, UPDATE to `active` `(ThesisCreateController.php)`
- [x] #filepond-preserve Preserve FilePond temp file IDs on partage validation redirect `(partage/index.php)` `(FilepondHandler.php)`
- [x] #refactor-partage Extract partage form page chrome to `templates/partage/form-page.php` `(partage/index.php)`
- [x] #htmx-migration HTMX v2 migration: OverType editors, autosave handler, backend `HX-Request` detection ✓
- [x] #filepond-crash FilePond crash on TFE upload forms: root cause fixed (`.filename``.name`), all crashes resolved ✓
- [x] #aria-errormessage WCAG AA: field-level `aria-errormessage`, `aria-invalid`, `aria-describedby` on all form fields ✓
- [x] #nojs-upload-fix No-JS file uploads: `filepond_mode` default to `0 disabled`, server-side `$_FILES` fallback ✓
- [x] #autosave-partage Autosave text fields on partage form: session draft endpoint (`fragments/draft.php`), HTMX autosave on change/input, page-load hydration, "Brouillon enregistré" indicator, draft cleared on submit ✓
- [x] #autosave-partage-wire Wire `formExtraAttrs`, `showAutosaveStatus`, draft hydration, `autosave-handler.js`, draft cleanup into partage form (`partage/index.php`, `partage/form-page.php`) ✓
- [x] #mobile-responsive Mobile-responsive form layout: `@media (max-width: 600px)` breakpoint, 44×44px touch targets ✓
- [x] #aria-fieldset-fix Remove invalid `required` attribute from `<fieldset>`, keep `aria-required="true"`, add `role="group"`
- [x] #split-form-css Split `form.css` into `form-base.css` and `form-admin.css`
- [x] #extra-css-admin Update `head.php` to support `$extraCssAdmin` for admin-only stylesheets `(head.php)`
@@ -4,6 +4,8 @@
* Rendered as a <dialog> element; caller should call .showModal() after swap. * Rendered as a <dialog> element; caller should call .showModal() after swap.
*/ */
require_once __DIR__ . '/../../bootstrap.php';
$rows = [ $rows = [
[ [
'syntax' => '# Titre 1', 'syntax' => '# Titre 1',
+1 -1
View File
@@ -2335,7 +2335,7 @@ th.admin-ap-col {
.md-cheatsheet-dialog { .md-cheatsheet-dialog {
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius-m, 8px); border-radius: var(--radius-m, 8px);
padding: 0; padding: 0 0 var(--space-m);
max-width: 640px; max-width: 640px;
width: 90vw; width: 90vw;
box-shadow: 0 8px 32px rgba(0,0,0,0.18); box-shadow: 0 8px 32px rgba(0,0,0,0.18);
+3 -3
View File
@@ -20,8 +20,8 @@
minmax(9rem, 1fr) minmax(9rem, 1fr)
minmax(7rem, 1fr) minmax(7rem, 1fr)
minmax(8rem, 1fr) minmax(8rem, 1fr)
minmax(min-content, 1fr); minmax(3rem, 0.45fr);
grid-template-rows: auto 1fr; grid-template-rows: auto minmax(0, 1fr);
gap: var(--space-s); gap: var(--space-s);
justify-content: space-between; justify-content: space-between;
padding: 0; padding: 0;
@@ -69,7 +69,7 @@
overflow-wrap: break-word; overflow-wrap: break-word;
} }
.repertoire-col > ul { .repertoire-col .rep-accordion__panel > ul {
grid-row: 2; grid-row: 2;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
+2 -55
View File
@@ -52,6 +52,8 @@
height: auto; height: auto;
display: block; display: block;
border-radius: var(--radius); border-radius: var(--radius);
user-select: none;
-webkit-user-drag: none;
} }
.tfe-file-item embed, .tfe-file-item embed,
@@ -161,12 +163,6 @@
margin: 0; margin: 0;
} }
/* PDF fallback download link */
.tfe-pdf-fallback {
font-size: var(--step--1);
margin: var(--space-3xs) 0 0;
}
/* Audio player */ /* Audio player */
.tfe-audio { .tfe-audio {
width: 100%; width: 100%;
@@ -174,55 +170,6 @@
display: block; display: block;
} }
/* Download-only files */
.tfe-download-file {
display: flex;
align-items: center;
gap: var(--space-xs);
padding: var(--space-s) var(--space-m);
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: var(--radius);
flex-wrap: wrap;
}
.tfe-download-link {
display: flex;
align-items: center;
gap: var(--space-xs);
font-size: var(--step--1);
font-weight: 500;
color: var(--text-primary);
text-decoration: none;
word-break: break-all;
}
.tfe-download-link:hover {
color: var(--accent-primary);
text-decoration: none;
}
.tfe-download-icon {
font-size: 1.3rem;
line-height: 1;
flex-shrink: 0;
}
.tfe-download-size {
font-size: var(--step--2);
color: var(--text-tertiary);
margin-left: auto;
}
.tfe-pdf-fallback a {
color: var(--text-primary);
text-decoration: none;
}
.tfe-pdf-fallback a:hover {
color: var(--accent-primary);
}
/* "Not available" and "no files" notices */ /* "Not available" and "no files" notices */
.tfe-restricted, .tfe-restricted,
.tfe-no-files { .tfe-no-files {
+5 -11
View File
@@ -108,10 +108,7 @@ class MediaController
exit; exit;
} }
// 5. Determine if download was explicitly requested // 5. Send response headers
$forceDownload = !empty($_GET['download']) && $_GET['download'] === '1';
// 6. Send response headers
header('Content-Type: ' . $mimeType); header('Content-Type: ' . $mimeType);
header('Content-Length: ' . (int) filesize($realFull)); header('Content-Length: ' . (int) filesize($realFull));
header('X-Content-Type-Options: nosniff'); header('X-Content-Type-Options: nosniff');
@@ -121,12 +118,10 @@ class MediaController
header('Cache-Control: public, max-age=86400'); header('Cache-Control: public, max-age=86400');
} elseif (in_array($ext, ['jpg','jpeg','png','gif','webp'], true)) { } elseif (in_array($ext, ['jpg','jpeg','png','gif','webp'], true)) {
header('Cache-Control: public, max-age=604800'); header('Cache-Control: public, max-age=604800');
if (!$forceDownload) {
header('Content-Disposition: inline'); header('Content-Disposition: inline');
}
} elseif ($ext === 'pdf') { } elseif ($ext === 'pdf') {
header('Cache-Control: public, max-age=86400'); header('Cache-Control: public, max-age=86400');
header('Content-Disposition: ' . ($forceDownload ? 'attachment' : 'inline')); header('Content-Disposition: inline');
} elseif (in_array($ext, ['mp4','webm','ogv','mov'], true)) { } elseif (in_array($ext, ['mp4','webm','ogv','mov'], true)) {
// Video: no cache-control range requests should work // Video: no cache-control range requests should work
header('Accept-Ranges: bytes'); header('Accept-Ranges: bytes');
@@ -135,10 +130,9 @@ class MediaController
header('Accept-Ranges: bytes'); header('Accept-Ranges: bytes');
header('Cache-Control: public, max-age=86400'); header('Cache-Control: public, max-age=86400');
} else { } else {
// Unknown / other: force download // Unknown / other: serve inline, no download
$safeFilename = preg_replace('/[^A-Za-z0-9._-]/', '_', basename($realFull)); header('Content-Disposition: inline');
header('Content-Disposition: attachment; filename="' . $safeFilename . '"'); header('Cache-Control: public, max-age=86400');
header('Cache-Control: private, no-store');
} }
// 7. Stream file (with range support for media) // 7. Stream file (with range support for media)
+14 -21
View File
@@ -253,7 +253,7 @@
<?php if (!empty($data["baiu_link"])): ?> <?php if (!empty($data["baiu_link"])): ?>
<?php <?php
$_baiuHref = htmlspecialchars($data["baiu_link"]); $_baiuHref = htmlspecialchars($data["baiu_link"]);
$_baiuLabel = preg_replace("#^https?://(www\\.)?#i", "", rtrim($data["baiu_link"], "/")); $_baiuLabel = parse_url($data["baiu_link"], PHP_URL_HOST) ?: preg_replace("#^https?://(www\\.)?#i", "", rtrim($data["baiu_link"], "/"));
?> ?>
<p class="tfe-meta-item"> <p class="tfe-meta-item">
<span class="tfe-meta-label">Lien :</span> <span class="tfe-meta-label">Lien :</span>
@@ -382,7 +382,7 @@
?> ?>
<div class="tfe-file-item"> <div class="tfe-file-item">
<?php if ($isPdf): ?> <?php if ($isPdf): ?>
<iframe src="<?= $mediaUrl ?>" <iframe src="<?= $mediaUrl ?>#toolbar=0&navpanes=0"
width="100%" height="700px" width="100%" height="700px"
style="border:none" style="border:none"
title="<?= $fileName ?>"> title="<?= $fileName ?>">
@@ -395,15 +395,11 @@
sandbox="allow-scripts allow-same-origin" sandbox="allow-scripts allow-same-origin"
loading="lazy"> loading="lazy">
</iframe> </iframe>
<p class="tfe-pdf-fallback">
<a href="<?= $mediaUrl ?>" target="_blank" rel="noopener">
Ouvrir le site dans un nouvel onglet
<span class="sr-only">(ouvre dans un nouvel onglet)</span>
</a>
</p>
<?php elseif ($isImage): ?> <?php elseif ($isImage): ?>
<img src="<?= $mediaUrl ?>" <img src="<?= $mediaUrl ?>"
alt="<?= htmlspecialchars($caption !== '' ? $caption : $data['title'] . ' — ' . ($data['authors'] ?? '')) ?>"> alt="<?= htmlspecialchars($caption !== '' ? $caption : $data['title'] . ' — ' . ($data['authors'] ?? '')) ?>"
draggable="false"
oncontextmenu="return false">
<?php elseif ($isVideo): ?> <?php elseif ($isVideo): ?>
<?php if ($isPeerTube): ?> <?php if ($isPeerTube): ?>
<?php <?php
@@ -413,7 +409,10 @@
include APP_ROOT . '/templates/partials/peertube-embed.php'; include APP_ROOT . '/templates/partials/peertube-embed.php';
?> ?>
<?php else: ?> <?php else: ?>
<video width="100%" controls> <video width="100%" controls
controlsList="nodownload nofullscreen noremoteplayback"
disablepictureinpicture
oncontextmenu="return false">
<source src="<?= $mediaUrl ?>" type="video/<?= htmlspecialchars($ext === 'mov' ? 'mp4' : $ext) ?>"> <source src="<?= $mediaUrl ?>" type="video/<?= htmlspecialchars($ext === 'mov' ? 'mp4' : $ext) ?>">
<?php if ($_vttPath): ?> <?php if ($_vttPath): ?>
<track kind="captions" <track kind="captions"
@@ -433,7 +432,9 @@
include APP_ROOT . '/templates/partials/peertube-embed.php'; include APP_ROOT . '/templates/partials/peertube-embed.php';
?> ?>
<?php else: ?> <?php else: ?>
<audio controls class="tfe-audio"> <audio controls class="tfe-audio"
controlsList="nodownload noremoteplayback"
oncontextmenu="return false">
<source src="<?= $mediaUrl ?>" type="audio/<?= htmlspecialchars(match($ext) { <source src="<?= $mediaUrl ?>" type="audio/<?= htmlspecialchars(match($ext) {
'mp3' => 'mpeg', 'mp3' => 'mpeg',
'ogg', 'oga' => 'ogg', 'ogg', 'oga' => 'ogg',
@@ -446,16 +447,8 @@
Votre navigateur ne supporte pas la lecture audio. Votre navigateur ne supporte pas la lecture audio.
</audio> </audio>
<?php endif; ?> <?php endif; ?>
<?php else: /* other — download only */ ?> <?php else: /* other — not previewable, skip */ ?>
<div class="tfe-download-file"> <?php continue; ?>
<a href="<?= $mediaUrl ?>&download=1" class="tfe-download-link">
<span class="tfe-download-icon">📎</span>
<span><?= $fileName ?></span>
</a>
<?php if (!empty($file['file_size'])): ?>
<small class="tfe-download-size"><?= number_format($file['file_size'] / 1024 / 1024, 2) ?> MB</small>
<?php endif; ?>
</div>
<?php endif; ?> <?php endif; ?>
<?php if ($caption !== '' && !$isOther): ?> <?php if ($caption !== '' && !$isOther): ?>
<figcaption><?= htmlspecialchars($caption) ?></figcaption> <figcaption><?= htmlspecialchars($caption) ?></figcaption>