Harden auth, add subtitles, vendor all assets locally
- Session cookies are HttpOnly + SameSite=Lax (Secure on HTTPS), with strict session-ID mode; the CSRF token from GET api/auth.php must now accompany every state-changing request (X-CSRF-Token header, or a csrf body field for the sendBeacon progress save), rotated at login. - Failed sign-ins are throttled per username and IP via login_attempts (created lazily, so existing installs need no migration): 5 failures a username / 20 an IP inside 15 minutes answer 429 with Retry-After. - A video's sibling .srt now appears as a CC button in the player; captions.php converts it to WebVTT (comma timestamps, legacy encodings, and the already-VTT files with out-of-spec 6-digit fractions). - Plyr and Inter ship in assets/vendor/ instead of coming from CDNs, and anime.js is gone along with all scripted animations and the motion setting — the app now runs with no internet access at all. - .htaccess denies direct access to videos/, includes/, config.php and schema.sql, so video URLs cannot bypass login on Apache. Claude-Session: https://claude.ai/code/session_01SePf61iazhCLJBefdRLBDm
This commit is contained in:
@@ -692,6 +692,62 @@ function quizFilePath(array $quiz): ?string
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute, containment-checked path of a video's sibling subtitle file
|
||||
* ("Lesson.mp4" -> "Lesson.srt"), or null if there isn't one.
|
||||
*/
|
||||
function srtFilePath(array $video): ?string
|
||||
{
|
||||
$rel = preg_replace('/\.mp4$/i', '.srt', $video['path']);
|
||||
if ($rel === null || $rel === $video['path']) {
|
||||
return null;
|
||||
}
|
||||
$file = realpath(rtrim(VIDEOS_ROOT, '/') . '/' . $rel);
|
||||
$root = realpath(VIDEOS_ROOT);
|
||||
if ($file === false || $root === false
|
||||
|| !str_starts_with($file, $root . DIRECTORY_SEPARATOR) || !is_file($file)) {
|
||||
return null;
|
||||
}
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* SubRip -> WebVTT (the only caption format browsers take in <track>).
|
||||
* The formats differ by a header line and the decimal separator in
|
||||
* timestamps; cue numbers are legal VTT cue identifiers and pass through.
|
||||
*/
|
||||
function srtToVtt(string $srt): string
|
||||
{
|
||||
if (str_starts_with($srt, "\xEF\xBB\xBF")) {
|
||||
$srt = substr($srt, 3);
|
||||
}
|
||||
if (!preg_match('//u', $srt)) { // not valid UTF-8 — legacy single-byte file
|
||||
$recoded = function_exists('iconv')
|
||||
? @iconv('Windows-1252', 'UTF-8//TRANSLIT', $srt)
|
||||
: (function_exists('mb_convert_encoding')
|
||||
? mb_convert_encoding($srt, 'UTF-8', 'Windows-1252') : false);
|
||||
if (is_string($recoded)) {
|
||||
$srt = $recoded;
|
||||
}
|
||||
}
|
||||
$srt = str_replace(["\r\n", "\r"], "\n", $srt);
|
||||
|
||||
// Normalise timestamps to HH:MM:SS.mmm. SRT uses a comma; generated files
|
||||
// in the wild also carry single-digit hours and six-digit fractions, and
|
||||
// the WebVTT parsing algorithm rejects any fraction that isn't exactly
|
||||
// three digits — such cues would silently drop.
|
||||
$srt = preg_replace_callback(
|
||||
'/(\d+):(\d{2}):(\d{2})[.,](\d+)/',
|
||||
fn($m) => sprintf('%02d:%s:%s.%s', $m[1], $m[2], $m[3],
|
||||
substr(str_pad($m[4], 3, '0', STR_PAD_RIGHT), 0, 3)),
|
||||
$srt
|
||||
);
|
||||
|
||||
// Some ".srt" files are already WebVTT — don't stack a second header.
|
||||
$srt = trim($srt);
|
||||
return (str_starts_with($srt, 'WEBVTT') ? $srt : "WEBVTT\n\n" . $srt) . "\n";
|
||||
}
|
||||
|
||||
/** Breadcrumb chain root -> ... -> folder. */
|
||||
function folderBreadcrumbs(array $folder): array
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user