Replace the Event_Summaries view with a physical snapshot table
refreshed via Stale-While-Revalidate (SWR) pattern. A stored
procedure (Refresh_Summaries_SWR) uses GET_LOCK for non-blocking
concurrency and atomic table rename for zero-downtime refresh.
Database changes (db/views.sql):
- Rename Event_Summaries view to VIEW_Event_Summaries (source view)
- Add Event_Summaries snapshot table and Event_Summaries_Metadata table
- Add Refresh_Summaries_SWR stored procedure with GET_LOCK and
atomic rename pattern to prevent thundering herd
- Add MySQL EVENT for background refresh every 600 seconds
PHP call sites (web/):
- Add ensureSummariesFresh() helper in database.php with static
per-request dedup and 60s staleness check
- Call ensureSummariesFresh() before Event_Summaries queries in
console.php, _monitor_filters.php, and Monitor.php
- Add beforeFind() hook in CakePHP Event_Summary model
Perl call sites (scripts/):
- Add ensureSummariesFresh() sub in Event_Summary.pm with
per-process 60s rate-limiting
- Call ensureSummariesFresh() in Monitor.pm Event_Summary accessor
Upgrade path (db/zm_update-1.37.78.sql.in):
- Drop any prior Event_Summaries view or table before recreating
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove denormalized event summary tables and their associated triggers,
replacing them with views that query the Events table directly. This
eliminates trigger maintenance overhead and periodic reconciliation in
zmaudit/zmstats, since the views compute stats on the fly.
- Remove trigger definitions for event summary table maintenance
- Remove event summary table inserts from zm_event.cpp
- Remove event count reconciliation queries from zmaudit.pl
- Remove DELETE-on-views calls from zmstats.pl (views filter by date inherently)
- Remove Event_Summaries DELETE from Monitor.php (can't delete from a view)
- Add db/views.sql with view definitions and covering index
- Add upgrade script zm_update-1.37.78.sql.in (drop triggers, drop tables, create views)
- Update zm_create.sql.in to use views instead of tables for fresh installs
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add user= parameter to get_auth_relay() so zms can use the indexed
Username column instead of iterating all users to validate the hash
- Apply the same fix to Event.php getStreamSrc() and getThumbnailSrc()
- Tighten Monitor.php from isset() to !empty() for consistency
- In MonitorStream.js start(), check if the auth hash in the img src
matches the current auth_hash before resuming via CMD_PLAY. If stale,
fall through to rebuild the URL with fresh auth_relay. This prevents
long-running montage pages from spawning zms with expired credentials.
- Downgrade zms auth failure from Error to Warning
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The C++ User class (used by zms for streaming) had no awareness of
roles. It only checked user-direct permissions from Monitors_Permissions
and Groups_Permissions tables, completely ignoring Role_Monitors_Permissions,
Role_Groups_Permissions, and User_Roles base permissions. This caused
users who received camera permissions via Roles to be denied live stream
access, even though the PHP web interface (which has its own role-aware
checks in visibleMonitor()) showed the monitors correctly.
Changes:
- Add role_id to C++ User class, loaded via COALESCE(RoleId, 0) in all
SQL queries (find, zmLoadTokenUser, zmLoadAuthUser)
- Add loadRoleBasePermissions() to merge role's Stream/Events/Monitors/
etc. as fallback when user's own permission is PERM_NONE
- Add findByRole() to Group_Permission and Monitor_Permission classes
to query Role_Groups_Permissions and Role_Monitors_Permissions tables
- Extend User::canAccess() to check role monitor and group permissions
after user-direct permissions, matching the PHP visibleMonitor() logic
- Fix Monitor::canView() in PHP to also check role permissions when
called for a user other than the global $user
- Fix off-by-one in zmLoadTokenUser where dbrow[10] read TokenMinExpiry
out of bounds (was at index 9); adding RoleId shifts it to index 10
Fixes#4692
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Device field from the Monitors table was interpolated directly into
shell commands (qx(), backticks, exec()) without sanitization, allowing
authenticated users with monitor-edit permissions to execute arbitrary
commands as www-data via the Device Path field.
Defense in depth:
- Input validation: reject Device values not matching /^\/dev\/[\w\/.\-]+$/
at save time in both web UI and REST API
- Output sanitization: use escapeshellarg() in PHP and quote validated
values in Perl at every shell execution point
Affected locations:
- scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm (control, zmcControl)
- scripts/zmpkg.pl.in (system startup)
- web/includes/Monitor.php (zmcControl)
- web/includes/functions.php (zmcStatus, zmcCheck, validDevicePath)
- web/includes/actions/monitor.php (save action)
- web/api/app/Model/Monitor.php (daemonControl, validation rules)
- web/api/app/Controller/MonitorsController.php (daemonStatus)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents 'Undefined property' PHP warning when the Monitor object was
loaded from a database that doesn't yet have the ModelId column. Matches
the existing property_exists pattern used later in the same method.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When zone coordinates are stored as pixel values (e.g. from a missed DB
migration), the web layer now detects values > 100 and converts them to
percentages using the monitor's dimensions, mirroring the existing C++
detection logic in zm_zone.cpp. This prevents limitPoints() from clamping
pixel values to 0-100 and zones rendering incorrectly in SVG overlays.
- Add convertPixelPointsToPercent() helper in functions.php
- Call conversion before limitPoints() in zone.php and zones.php
- Update Zone::svg_polygon() to accept monitor dimensions and convert
- Pass ViewWidth/ViewHeight to svg_polygon() from Monitor::getStreamHTML()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Convert zone coordinates from absolute pixel values to percentages
(0.00-100.00) so zones automatically adapt when monitor resolution
changes. This eliminates the need to manually reconfigure zones after
resolution adjustments.
Changes:
- Add DB migration (zm_update-1.37.81.sql) to convert existing pixel
coords to percentages, recalculate area, and update Units default
- Add Zone::ParsePercentagePolygon() in C++ to parse percentage coords
and convert to pixels at runtime using monitor dimensions
- Backwards compat: C++ Zone::Load() checks Units column and uses old
pixel parser for legacy 'Pixels' zones
- Update PHP coordsToPoints/mapCoords/getPolyArea for float coords,
replace scanline area algorithm with shoelace formula
- Update JS zone editor to work in percentage coordinate space with
SVG viewBox "0 0 100 100" and non-scaling-stroke for consistent
line thickness
- Position zone SVG overlay inside imageFeed container via JS to align
with image only (not status bar)
- Support array of zone IDs in Monitor::getStreamHTML zones option
- Update monitor resize handler: percentage coords don't need rescaling,
only threshold pixel counts are adjusted
- Add 8 Catch2 unit tests for ParsePercentagePolygon
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename applies to Go2RTC, Janus, and RTSP2Web streaming options.
Update enum values from Primary/Secondary to Restream/CameraDirectPrimary/CameraDirectSecondary.
- Add db migration zm_update-1.37.79.sql to rename column and migrate data
- Update C++ enum StreamChannelOption and member stream_channel
- Update PHP getStreamChannelOptions() method
- Update all JavaScript references
- Auto-select CameraDirectPrimary when Restream option becomes disabled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The edit button overlay on the montage view stream is now conditionally
displayed based on the user's edit permission for that monitor.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>