The ZM_OPT_USE_AUTH gate in index.php exempted view=login and view=none
so those pages can render without a session. The same condition also
guarded the ajax dispatcher, so view=none&request=<name> (or
view=login&request=<name>) reached web/ajax/<name>.php with no user.
ajax/stats.php's raw branch has no permission check of its own and
returned Events rows (Name, Cause, Notes, DiskSpace) joined with the
monitor name to anonymous clients.
Apply the login/none exemption only when there is no request, so any
request= without a user gets the 401. No ajax handler is meant to be
reached unauthenticated: login and logout are POST actions on views,
skin.js skips its polling on the login and none views, and the auth
revalidation probe already uses view=request and expects a 401/403 for
a dead session.
Also check canView('Events') at the top of ajax/stats.php, matching
getStatsTableHTML() used by its non-raw branch, so the raw branch no
longer depends on the gate alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 6699ae212ea8c5d7b9ee29f38c05c0135635fe8f)
Every request through index.php starts a session and always dirties it:
zm_session_set_remote_addr() writes remoteAddr, and index.php stores skin,
css and navbar_type. ZMSessionHandler::write() then persisted that session
unconditionally, so any request arriving without a ZMSESSID cookie left a
Sessions row behind that nothing would ever load again.
Viewing an event polls the event's server every ZM_WEB_REFRESH_STATUS
seconds via monitorUrl, which is absolute when the monitor has a Server
row. Those cross-origin ajax polls carry auth in the URL and no cookie, so
each one added a Sessions row every few seconds. Bot scans of the login
page did the same.
Persist a session only when the client presented our cookie, or when
zm_session_persist() marks it as one we are issuing: login, and the
postLoginQuery stashed before redirecting to the login page.
Verified on a live install by logging row counts from the save handler:
three cookieless requests skipped the write and left the count unchanged,
while a cookie-jar run wrote on the request that returned the cookie.
php -l clean on both files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GAFKf86P78WqniPEP2b45J
auth.php ended in a 130-line block at file scope, so merely including the file
authenticated the current request: it read $_REQUEST, opened a session, queried
the database, and on a login could rewrite the user's stored password hash and
populate $_SESSION. Any caller that wanted one of the functions in the file got
all of that as a side effect, and the order of includes decided when it ran.
HostController requires auth.php twice purely to reach generateAuthHash() and
validateToken().
Move the block into zm_authenticate_request() and call it explicitly from the
two places that want it, web/index.php and AppController::beforeFilter(). The
function returns the ZM\User or null and still sets the global $user, so the
views, ajax handlers and API controllers that read that global are unaffected.
HostController now gets only the function definitions from its requires, which
is all it ever wanted.
Inside a function the five `unset($user)` calls would drop the local binding
and leave the global set, so they become `$user = null` - the idiom the rest of
the file already uses for this, and one that keeps isset($user) false for the
gate at index.php:255. The block's other locals ($ret, $username, $password,
$sql) no longer leak into the caller's scope, which in beforeFilter() means they
can no longer collide with the variables of the same name it assigns just after.
The body is otherwise unchanged; `git diff -w` shows only the wrapper, those
five assignments and the return.
Tests: tests/php/test_auth_no_include_side_effects.php tokenises auth.php and
asserts nothing executes at file scope, with a fixture check so a broken
detector cannot pass vacuously. 4 assertions, all pass. Verified it reports the
pre-refactor file's file-scope block, so it would have caught this.
Not covered by tests: the login, logout, auth-hash and API token flows this
touches. auth.php cannot be included without a database (User.php pulls in
database.php, which connects at include time), so the check is structural.
Needs manual testing on an installed tree before merging.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01477mR97vfnK6zczbHgzq6T
Write a single continuous fragmented MP4 per event and generate an HLS
m3u8 manifest with byte-range references into that file. This enables
seamless browser playback via video.js's built-in http-streaming (VHS)
without needing separate segment files.
C++ changes:
- VideoStore tracks fragment boundaries (moof+mdat byte offsets and
durations) as packets are written, by monitoring avio_tell() around
keyframe writes in write_packet()
- Add writeM3U8() method that generates EXT-X-VERSION:7 byte-range
manifests with EXT-X-MAP for the init segment
- Event writes a live m3u8 (no EXT-X-ENDLIST) on each new fragment
for in-progress viewing, and a final VOD manifest at event close
- Change movflags to frag_keyframe+empty_moov+default_base_moof
(default_base_moof required by HLS fMP4 spec, faststart removed
as it's meaningless with empty_moov)
PHP/web changes:
- New view_hls.php endpoint serves pre-built m3u8 with auth tokens
- event.php detects index.m3u8 and uses HLS as primary source with
direct MP4 as fallback
- CSRF exemption for view_hls in index.php
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Modified web/index.php to pass postLoginQuery as URL parameter during redirect
- Updated web/skins/classic/views/login.php to check both $_REQUEST and $_SESSION for postLoginQuery
- This fixes the timing issue where session variable may not be available when login form renders
Co-authored-by: SteveGilvarry <7613276+SteveGilvarry@users.noreply.github.com>