- A quiz question can now carry the diagram it refers to: a Markdown
image line anywhere inside the question block is parsed out of the
text and rendered under it (referrerpolicy="no-referrer", lazy). The
URL points at the course provider, so an offline install gets a note
and a link instead of an empty box.
- New optional SESSION_LIFETIME (30 days in the example config) replaces
the browser-session cookie plus php.ini's 24-minute idle limit, which
signed people out over a long pause. Configs that predate it behave as
before. Behind a proxy trusted enough for CLIENT_IP_HEADER, the cookie
is also marked Secure from X-Forwarded-Proto — TLS ending at the proxy
no longer costs the flag.
- Error responses no longer echo the exception message unless DEBUG: a
PDOException names the DB user, host and tables, and that handler also
runs for visitors who never signed in.
- The .htaccess denials are anchored to a path segment rather than the
root, so videos/ and includes/ stay unreachable under a prefix such as
/coursehub/ instead of silently opening up.
- A rescan takes a MySQL advisory lock; a second one answers "already
running" rather than racing the first and flagging its rows missing. A
file swapped in at the same path with a different size drops its
cached duration so the probe reads the new length.
- stream.php discards output buffering and zlib compression (both break
the promised Content-Length) and answers HEAD with headers alone.
- A progress save with no duration keeps the length already stored
instead of nulling it, and the folder view builds its rollup once.
- In the SPA, a view whose data arrives after the route moved on is
discarded — previously a slow load could paint over a newer screen or
start a heartbeat for a video nobody was watching. Also: the quiz
submit button can't be double-fired, leaving a video during the
autoplay countdown no longer jumps ahead later, the final position is
always saved on 'ended', the playlist scrolls the current entry into
its own box, the login field takes focus, views start at the top, and
a stray "%" in the hash no longer throws.
- Dropped the unused requireLogin(); registerUser() answers "taken"
instead of a 500 when two people race for the same name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXMrtWNUV7NU1pCUHBG4t1
- 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.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SePf61iazhCLJBefdRLBDm