mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-08-10 07:11:18 +02:00
fix: add missing bootstrap require in markdown-cheatsheet-fragment.php, add bottom padding to dialog
This commit is contained in:
@@ -1,83 +1,2 @@
|
||||
# TODO
|
||||
|
||||
> 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)` ✓
|
||||
|
||||
- [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
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* Rendered as a <dialog> element; caller should call .showModal() after swap.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../bootstrap.php';
|
||||
|
||||
$rows = [
|
||||
[
|
||||
'syntax' => '# Titre 1',
|
||||
|
||||
@@ -2335,7 +2335,7 @@ th.admin-ap-col {
|
||||
.md-cheatsheet-dialog {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-m, 8px);
|
||||
padding: 0;
|
||||
padding: 0 0 var(--space-m);
|
||||
max-width: 640px;
|
||||
width: 90vw;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.18);
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
minmax(9rem, 1fr)
|
||||
minmax(7rem, 1fr)
|
||||
minmax(8rem, 1fr)
|
||||
minmax(min-content, 1fr);
|
||||
grid-template-rows: auto 1fr;
|
||||
minmax(3rem, 0.45fr);
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: var(--space-s);
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
@@ -69,7 +69,7 @@
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.repertoire-col > ul {
|
||||
.repertoire-col .rep-accordion__panel > ul {
|
||||
grid-row: 2;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
@@ -52,6 +52,8 @@
|
||||
height: auto;
|
||||
display: block;
|
||||
border-radius: var(--radius);
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
.tfe-file-item embed,
|
||||
@@ -161,12 +163,6 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* PDF fallback download link */
|
||||
.tfe-pdf-fallback {
|
||||
font-size: var(--step--1);
|
||||
margin: var(--space-3xs) 0 0;
|
||||
}
|
||||
|
||||
/* Audio player */
|
||||
.tfe-audio {
|
||||
width: 100%;
|
||||
@@ -174,55 +170,6 @@
|
||||
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 */
|
||||
.tfe-restricted,
|
||||
.tfe-no-files {
|
||||
|
||||
@@ -108,10 +108,7 @@ class MediaController
|
||||
exit;
|
||||
}
|
||||
|
||||
// 5. Determine if download was explicitly requested
|
||||
$forceDownload = !empty($_GET['download']) && $_GET['download'] === '1';
|
||||
|
||||
// 6. Send response headers
|
||||
// 5. Send response headers
|
||||
header('Content-Type: ' . $mimeType);
|
||||
header('Content-Length: ' . (int) filesize($realFull));
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
@@ -121,12 +118,10 @@ class MediaController
|
||||
header('Cache-Control: public, max-age=86400');
|
||||
} elseif (in_array($ext, ['jpg','jpeg','png','gif','webp'], true)) {
|
||||
header('Cache-Control: public, max-age=604800');
|
||||
if (!$forceDownload) {
|
||||
header('Content-Disposition: inline');
|
||||
}
|
||||
} elseif ($ext === 'pdf') {
|
||||
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)) {
|
||||
// Video: no cache-control range requests should work
|
||||
header('Accept-Ranges: bytes');
|
||||
@@ -135,10 +130,9 @@ class MediaController
|
||||
header('Accept-Ranges: bytes');
|
||||
header('Cache-Control: public, max-age=86400');
|
||||
} else {
|
||||
// Unknown / other: force download
|
||||
$safeFilename = preg_replace('/[^A-Za-z0-9._-]/', '_', basename($realFull));
|
||||
header('Content-Disposition: attachment; filename="' . $safeFilename . '"');
|
||||
header('Cache-Control: private, no-store');
|
||||
// Unknown / other: serve inline, no download
|
||||
header('Content-Disposition: inline');
|
||||
header('Cache-Control: public, max-age=86400');
|
||||
}
|
||||
|
||||
// 7. Stream file (with range support for media)
|
||||
|
||||
@@ -253,7 +253,7 @@
|
||||
<?php if (!empty($data["baiu_link"])): ?>
|
||||
<?php
|
||||
$_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">
|
||||
<span class="tfe-meta-label">Lien :</span>
|
||||
@@ -382,7 +382,7 @@
|
||||
?>
|
||||
<div class="tfe-file-item">
|
||||
<?php if ($isPdf): ?>
|
||||
<iframe src="<?= $mediaUrl ?>"
|
||||
<iframe src="<?= $mediaUrl ?>#toolbar=0&navpanes=0"
|
||||
width="100%" height="700px"
|
||||
style="border:none"
|
||||
title="<?= $fileName ?>">
|
||||
@@ -395,15 +395,11 @@
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
loading="lazy">
|
||||
</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): ?>
|
||||
<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 if ($isPeerTube): ?>
|
||||
<?php
|
||||
@@ -413,7 +409,10 @@
|
||||
include APP_ROOT . '/templates/partials/peertube-embed.php';
|
||||
?>
|
||||
<?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) ?>">
|
||||
<?php if ($_vttPath): ?>
|
||||
<track kind="captions"
|
||||
@@ -433,7 +432,9 @@
|
||||
include APP_ROOT . '/templates/partials/peertube-embed.php';
|
||||
?>
|
||||
<?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) {
|
||||
'mp3' => 'mpeg',
|
||||
'ogg', 'oga' => 'ogg',
|
||||
@@ -446,16 +447,8 @@
|
||||
Votre navigateur ne supporte pas la lecture audio.
|
||||
</audio>
|
||||
<?php endif; ?>
|
||||
<?php else: /* other — download only */ ?>
|
||||
<div class="tfe-download-file">
|
||||
<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 else: /* other — not previewable, skip */ ?>
|
||||
<?php continue; ?>
|
||||
<?php endif; ?>
|
||||
<?php if ($caption !== '' && !$isOther): ?>
|
||||
<figcaption><?= htmlspecialchars($caption) ?></figcaption>
|
||||
|
||||
Reference in New Issue
Block a user