mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
WCAG 4.1.2: add WebVTT caption support for <video> elements on tfe.php
Problem: <video> elements on tfe.php had no <track kind="captions"> element, violating WCAG 4.1.2 (name, role, value) for video content. Changes: - public/tfe.php: collect all text/vtt files from the thesis file list before rendering; skip standalone rendering of .vtt entries; for each MP4 emit a <track kind="captions" srclang="fr" label="Sous-titres" default> pointing to the N-th VTT file (N-th video paired with N-th caption in document order) - public/media.php: add text/vtt to allowed MIME list; normalise finfo text/plain -> text/vtt for .vtt files; add vtt branch to cache/header block (Content-Type: text/vtt; charset=utf-8, 1-day cache) - public/admin/actions/formulaire.php: allow .vtt uploads (text/vtt MIME, vtt extension); normalise text/plain finfo result; set file_type='caption' for VTT files so they are distinguishable from other thesis files - public/admin/add.php: extend files field accept attr to include .vtt; update hint text to document the VTT sidecar convention VTT files uploaded under theses/ inherit the same access_type visibility gate in media.php as all other thesis content (403 for access_type_id=3).
This commit is contained in:
3
TODO.md
3
TODO.md
@@ -11,6 +11,9 @@ Pending tasks have been split into topic files under [`todo/`](todo/README.md):
|
||||
|
||||
## Recently completed (this session)
|
||||
|
||||
- [x] WCAG 4.1.2 `<video>` captions — `tfe.php` now emits `<track kind="captions">` for each MP4 when a `.vtt` sidecar exists (N-th VTT paired with N-th video). `formulaire.php` accepts `.vtt` uploads (`file_type='caption'`, MIME normalised). `media.php` serves `text/vtt` with correct headers and visibility gating. Admin `add.php` file-field hint documents the `.vtt` upload convention.
|
||||
|
||||
|
||||
- [x] `admin/edit.php` — WCAG 4.1.2: removed `mb_strimwidth` truncation from `$accessOptions` mapping; access type `<select>` options now include full description text (`name — description`) so the accessible name is unambiguous for screen readers
|
||||
- [x] `public/assets/favicon.svg` — created public favicon: brand-purple (`#9557b5`) rounded square with white "P" lettermark; distinct from `admin_favicon.svg` (archive-restore icon in `#c104fc`)
|
||||
- [x] `templates/head.php` — favicon `<link>` now selects `favicon.svg` (public) vs `admin_favicon.svg` (admin) based on `$isAdmin`; closes `todo/01-css-semantic-refactor.md` favicon task
|
||||
|
||||
@@ -186,8 +186,8 @@ try {
|
||||
}
|
||||
|
||||
// Define security constraints
|
||||
$allowedMimeTypes = ['image/jpeg', 'image/png', 'application/pdf', 'video/mp4', 'application/zip'];
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'pdf', 'mp4', 'zip'];
|
||||
$allowedMimeTypes = ['image/jpeg', 'image/png', 'application/pdf', 'video/mp4', 'application/zip', 'text/vtt'];
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'pdf', 'mp4', 'zip', 'vtt'];
|
||||
$maxFileSize = 50 * 1024 * 1024; // 50 MB
|
||||
|
||||
// Process cover image
|
||||
@@ -248,6 +248,11 @@ try {
|
||||
$mimeType = $finfo->file($files["tmp_name"][$i]);
|
||||
$fileExtension = strtolower(pathinfo($files["name"][$i], PATHINFO_EXTENSION));
|
||||
|
||||
// finfo may return 'text/plain' for WebVTT on some systems; normalise by extension.
|
||||
if ($mimeType === 'text/plain' && $fileExtension === 'vtt') {
|
||||
$mimeType = 'text/vtt';
|
||||
}
|
||||
|
||||
if (!in_array($mimeType, $allowedMimeTypes) || !in_array($fileExtension, $allowedExtensions)) {
|
||||
error_log("Invalid file type: " . $files["name"][$i] . " (MIME: $mimeType)");
|
||||
continue;
|
||||
@@ -266,9 +271,11 @@ try {
|
||||
if (move_uploaded_file($files["tmp_name"][$i], $targetFile)) {
|
||||
chmod($targetFile, 0644);
|
||||
|
||||
// Determine file type (simplified - could be enhanced)
|
||||
// Determine file type
|
||||
$fileType = 'other';
|
||||
if (strpos(strtolower($files["name"][$i]), 'annex') !== false) {
|
||||
if ($fileExtension === 'vtt') {
|
||||
$fileType = 'caption'; // WebVTT caption sidecar
|
||||
} elseif (strpos(strtolower($files["name"][$i]), 'annex') !== false) {
|
||||
$fileType = 'annex';
|
||||
} elseif ($fileExtension === 'pdf') {
|
||||
$fileType = 'main';
|
||||
|
||||
@@ -94,7 +94,7 @@ function wasSelected($key, $value) {
|
||||
|
||||
<?php $name = 'banner'; $label = 'Image bannière (accueil) :'; $accept = 'image/jpeg,image/png,image/webp'; $hint = 'JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 5 MB.'; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
||||
|
||||
<?php $name = 'files'; $label = 'Fichiers du TFE :'; $accept = '.pdf,.jpg,.jpeg,.png,.mp4,.zip'; $hint = 'PDF, JPG, PNG, MP4, ZIP. Max 50 MB par fichier.'; $multiple = true; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
||||
<?php $name = 'files'; $label = 'Fichiers du TFE :'; $accept = '.pdf,.jpg,.jpeg,.png,.mp4,.zip,.vtt'; $hint = 'PDF, JPG, PNG, MP4, ZIP. Max 50 MB par fichier. Pour les vidéos, un fichier .vtt de sous-titres peut être joint (il sera associé automatiquement à la vidéo correspondante).'; $multiple = true; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
||||
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" name="go" class="admin-btn">Soumettre</button>
|
||||
|
||||
@@ -78,8 +78,15 @@ $allowedMimes = [
|
||||
'application/pdf',
|
||||
'video/mp4',
|
||||
'application/zip',
|
||||
'text/vtt', // WebVTT caption sidecar files
|
||||
];
|
||||
|
||||
// finfo may return 'text/plain' for WebVTT files on some systems;
|
||||
// re-classify by extension so we don't block them.
|
||||
if ($mimeType === 'text/plain' && strtolower(pathinfo($realFull, PATHINFO_EXTENSION)) === 'vtt') {
|
||||
$mimeType = 'text/vtt';
|
||||
}
|
||||
|
||||
if (!in_array($mimeType, $allowedMimes, true)) {
|
||||
http_response_code(403);
|
||||
exit;
|
||||
@@ -100,6 +107,10 @@ if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif'], true)) {
|
||||
// PDFs: cache for 1 day, display inline
|
||||
header('Cache-Control: public, max-age=86400');
|
||||
header('Content-Disposition: inline');
|
||||
} elseif ($ext === 'vtt') {
|
||||
// WebVTT captions: serve as text/vtt, cache 1 day
|
||||
header('Content-Type: text/vtt; charset=utf-8');
|
||||
header('Cache-Control: public, max-age=86400');
|
||||
} else {
|
||||
// Everything else: no public caching
|
||||
header('Cache-Control: private, no-store');
|
||||
|
||||
@@ -196,6 +196,19 @@ $bodyClass = 'tfe-body';
|
||||
<?php
|
||||
$accessTypeId = $db->getThesisAccessTypeId($thesisId) ?? 1;
|
||||
$isInterdit = ($accessTypeId === 3);
|
||||
|
||||
// Collect any WebVTT caption files uploaded for this thesis.
|
||||
// The N-th VTT file is paired with the N-th video in document order.
|
||||
$_captionFiles = [];
|
||||
foreach ($data['files'] ?? [] as $_cf) {
|
||||
$__mime = $_cf['mime_type'] ?? '';
|
||||
$__ext2 = strtolower(pathinfo($_cf['file_path'], PATHINFO_EXTENSION));
|
||||
if ($__mime === 'text/vtt' || $__ext2 === 'vtt') {
|
||||
$_captionFiles[] = $_cf['file_path'];
|
||||
}
|
||||
}
|
||||
$_videoIndex = 0;
|
||||
unset($_cf, $__mime, $__ext2);
|
||||
?>
|
||||
<?php if ($isInterdit): ?>
|
||||
<p class="tfe-restricted">
|
||||
@@ -203,7 +216,11 @@ $bodyClass = 'tfe-body';
|
||||
</p>
|
||||
<?php elseif (!empty($data['files'])): ?>
|
||||
<?php foreach ($data['files'] as $file): ?>
|
||||
<?php $ext = strtolower(pathinfo($file['file_path'], PATHINFO_EXTENSION)); ?>
|
||||
<?php
|
||||
$ext = strtolower(pathinfo($file['file_path'], PATHINFO_EXTENSION));
|
||||
// VTT caption files are consumed inline by <video>; skip standalone rendering.
|
||||
if ($ext === 'vtt') continue;
|
||||
?>
|
||||
<figure>
|
||||
<?php if ($ext === 'pdf'): ?>
|
||||
<embed src="/media.php?path=<?= urlencode($file['file_path']) ?>"
|
||||
@@ -221,8 +238,20 @@ $bodyClass = 'tfe-body';
|
||||
: ($data['title'] . ' — ' . ($data['authors'] ?? ''))
|
||||
) ?>">
|
||||
<?php elseif ($ext === 'mp4'): ?>
|
||||
<?php
|
||||
// Pair this video with the N-th VTT file (if one was uploaded).
|
||||
$_vttPath = $_captionFiles[$_videoIndex] ?? null;
|
||||
$_videoIndex++;
|
||||
?>
|
||||
<video width="100%" controls>
|
||||
<source src="/media.php?path=<?= urlencode($file['file_path']) ?>" type="video/mp4">
|
||||
<?php if ($_vttPath): ?>
|
||||
<track kind="captions"
|
||||
src="/media.php?path=<?= urlencode($_vttPath) ?>"
|
||||
srclang="fr"
|
||||
label="Sous-titres"
|
||||
default>
|
||||
<?php endif; ?>
|
||||
</video>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($file['description'])): ?>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
- [x] **Custom "Externe" checkbox for jury members has no group context** — all jury "Externe" checkboxes now carry explicit `aria-label` (e.g. `"Promoteur·ice — externe"`, `"Lecteur·ice N — externe"`); both static PHP-rendered rows and dynamically added rows via `addJuryRow()` receive the label
|
||||
|
||||
- [ ] **`<video>` elements on `tfe.php` have no captions** — add `<track kind="captions">` slot in template; document caption requirement in admin upload form
|
||||
- [x] **`<video>` elements on `tfe.php` have no captions** — `<track kind="captions">` now emitted for each MP4 when a `.vtt` sidecar has been uploaded alongside it; N-th VTT file is paired with N-th video in document order. `formulaire.php` accepts `.vtt` uploads (MIME `text/vtt`, `file_type = 'caption'`); `media.php` serves VTT with correct `Content-Type`; admin `add.php` file-field hint documents the `.vtt` convention.
|
||||
|
||||
- [x] **Admin `<select>` for visibility/access in `edit.php` uses truncated option text** — removed `mb_strimwidth` call; option text now uses full description (`name — description`) so screen-reader accessible name is complete and unambiguous
|
||||
|
||||
|
||||
Reference in New Issue
Block a user