mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 11:09:18 +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)
|
## 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] `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] `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
|
- [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
|
// Define security constraints
|
||||||
$allowedMimeTypes = ['image/jpeg', 'image/png', 'application/pdf', 'video/mp4', 'application/zip'];
|
$allowedMimeTypes = ['image/jpeg', 'image/png', 'application/pdf', 'video/mp4', 'application/zip', 'text/vtt'];
|
||||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'pdf', 'mp4', 'zip'];
|
$allowedExtensions = ['jpg', 'jpeg', 'png', 'pdf', 'mp4', 'zip', 'vtt'];
|
||||||
$maxFileSize = 50 * 1024 * 1024; // 50 MB
|
$maxFileSize = 50 * 1024 * 1024; // 50 MB
|
||||||
|
|
||||||
// Process cover image
|
// Process cover image
|
||||||
@@ -248,6 +248,11 @@ try {
|
|||||||
$mimeType = $finfo->file($files["tmp_name"][$i]);
|
$mimeType = $finfo->file($files["tmp_name"][$i]);
|
||||||
$fileExtension = strtolower(pathinfo($files["name"][$i], PATHINFO_EXTENSION));
|
$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)) {
|
if (!in_array($mimeType, $allowedMimeTypes) || !in_array($fileExtension, $allowedExtensions)) {
|
||||||
error_log("Invalid file type: " . $files["name"][$i] . " (MIME: $mimeType)");
|
error_log("Invalid file type: " . $files["name"][$i] . " (MIME: $mimeType)");
|
||||||
continue;
|
continue;
|
||||||
@@ -266,9 +271,11 @@ try {
|
|||||||
if (move_uploaded_file($files["tmp_name"][$i], $targetFile)) {
|
if (move_uploaded_file($files["tmp_name"][$i], $targetFile)) {
|
||||||
chmod($targetFile, 0644);
|
chmod($targetFile, 0644);
|
||||||
|
|
||||||
// Determine file type (simplified - could be enhanced)
|
// Determine file type
|
||||||
$fileType = 'other';
|
$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';
|
$fileType = 'annex';
|
||||||
} elseif ($fileExtension === 'pdf') {
|
} elseif ($fileExtension === 'pdf') {
|
||||||
$fileType = 'main';
|
$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 = '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">
|
<div class="admin-form-footer">
|
||||||
<button type="submit" name="go" class="admin-btn">Soumettre</button>
|
<button type="submit" name="go" class="admin-btn">Soumettre</button>
|
||||||
|
|||||||
@@ -78,8 +78,15 @@ $allowedMimes = [
|
|||||||
'application/pdf',
|
'application/pdf',
|
||||||
'video/mp4',
|
'video/mp4',
|
||||||
'application/zip',
|
'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)) {
|
if (!in_array($mimeType, $allowedMimes, true)) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
exit;
|
exit;
|
||||||
@@ -100,6 +107,10 @@ if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif'], true)) {
|
|||||||
// PDFs: cache for 1 day, display inline
|
// PDFs: cache for 1 day, display inline
|
||||||
header('Cache-Control: public, max-age=86400');
|
header('Cache-Control: public, max-age=86400');
|
||||||
header('Content-Disposition: inline');
|
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 {
|
} else {
|
||||||
// Everything else: no public caching
|
// Everything else: no public caching
|
||||||
header('Cache-Control: private, no-store');
|
header('Cache-Control: private, no-store');
|
||||||
|
|||||||
@@ -196,6 +196,19 @@ $bodyClass = 'tfe-body';
|
|||||||
<?php
|
<?php
|
||||||
$accessTypeId = $db->getThesisAccessTypeId($thesisId) ?? 1;
|
$accessTypeId = $db->getThesisAccessTypeId($thesisId) ?? 1;
|
||||||
$isInterdit = ($accessTypeId === 3);
|
$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): ?>
|
<?php if ($isInterdit): ?>
|
||||||
<p class="tfe-restricted">
|
<p class="tfe-restricted">
|
||||||
@@ -203,7 +216,11 @@ $bodyClass = 'tfe-body';
|
|||||||
</p>
|
</p>
|
||||||
<?php elseif (!empty($data['files'])): ?>
|
<?php elseif (!empty($data['files'])): ?>
|
||||||
<?php foreach ($data['files'] as $file): ?>
|
<?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>
|
<figure>
|
||||||
<?php if ($ext === 'pdf'): ?>
|
<?php if ($ext === 'pdf'): ?>
|
||||||
<embed src="/media.php?path=<?= urlencode($file['file_path']) ?>"
|
<embed src="/media.php?path=<?= urlencode($file['file_path']) ?>"
|
||||||
@@ -221,8 +238,20 @@ $bodyClass = 'tfe-body';
|
|||||||
: ($data['title'] . ' — ' . ($data['authors'] ?? ''))
|
: ($data['title'] . ' — ' . ($data['authors'] ?? ''))
|
||||||
) ?>">
|
) ?>">
|
||||||
<?php elseif ($ext === 'mp4'): ?>
|
<?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>
|
<video width="100%" controls>
|
||||||
<source src="/media.php?path=<?= urlencode($file['file_path']) ?>" type="video/mp4">
|
<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>
|
</video>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (!empty($file['description'])): ?>
|
<?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
|
- [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
|
- [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