When zmc is killed or crashes without writing EndDateTime, three code
paths invent a fake end of NOW(), so an event from hours ago appears to
extend across the entire down-time. Montage review then paints a bar
that makes it look like recorded video exists where it doesn't.
Length is flushed to the DB every few seconds during recording, so even
crashed events have an accurate last-known duration. Fall back to
StartDateTime + Length when EndDateTime IS NULL, and only fall back to
NOW() when Length is also 0 (event has no recorded data yet).
- web/api/app/Model/Event.php: EndTimeSecs and EndTime virtual fields,
which is what the montagereview JS actually reads via the API.
- web/ajax/events.php: same fix in the AJAX events list SQL.
- web/skins/classic/views/montagereview.php: \$eventsSql kept in sync
even though it is no longer executed directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a curated, per-encoder parameter-template library to ZoneMinder:
- Monitor edit page: a new Template row above the EncoderParameters
textarea offers per-encoder templates (Balanced / Archival / Low
Power / Low CPU). Apply merges the template's params into the
textarea, preserving user-only keys. Advisory lint flags option
keys that aren't recognised for the selected encoder. Switching
encoders offers a same-name template on the new encoder via a
native confirm.
- Options page: a new Encoder Templates tab with full CRUD —
list / edit / copy / delete — backed by a new CakePHP REST API
at /api/encoder_templates.
- Storage: a new EncoderTemplates DB table seeded with 14 shipped
defaults across libx264 / libx265 / h264_nvenc / hevc_nvenc /
h264_vaapi / hevc_vaapi. The table is mutable; ZM upgrades do not
re-seed user-edited rows.
- valid_keys (the lint allow-list) stays in PHP code as ffmpeg
vocabulary, not user data.
- Default params explicitly include pix_fmt to avoid the yuvj420p
HEVC HW-decode rejection issue we hit earlier.
No C++ change. The textarea content is parsed by the existing
av_dict_parse_string call in src/zm_videostore.cpp.
version.txt -> 1.39.6.
Specs: docs/superpowers/specs/2026-05-0{1,2}-*.md
Plans: docs/superpowers/plans/2026-05-0{1,2}-*.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to 419846c87 (GHSA-g66m-77fq-79v9). The Device path check was
applied to all monitor Types in three places, but the Device column is
only passed to a shell for Type='Local'. Non-Local monitors (Ffmpeg,
Remote, Libvlc, cURL, VNC) may legitimately hold legacy values such as
an RTSP URL in that column and should not be rejected or warned about.
- scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm: control() dropped the
spurious Warning for non-Local monitors that was flooding zmwatch
logs. The Error/early-return path is preserved for Local.
- web/includes/actions/monitor.php: save action only runs
validDevicePath() when Type=='Local'.
- web/api/app/Model/Monitor.php: replaced the unconditional regex rule
with a validDevicePath() method that checks Type before enforcing
the /dev/ pattern.
Also add client-side validation matching the server rule, so Local
monitors get immediate feedback instead of a round-trip error:
- web/skins/classic/views/monitor.php: HTML5 pattern attribute on the
Device input. Escaped for the v-flag regex engine used by pattern=.
- web/skins/classic/views/js/monitor.js.php: validateForm() now also
rejects Device values that don't match the /dev/ pattern.
Co-Authored-By: Claude Opus 4.6 (1M context) <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>
- Revert accidental Users.RoleId FK change from CASCADE back to SET NULL
- Remove System != 'None' gate in beforeFilter; any authenticated user
can manage their own notifications, per-row ownership checks suffice
- Add allowMethod('post', 'put') guard to edit() for consistent REST behavior
- Change PushState validation from allowEmpty to required=false
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add FOREIGN KEY on UserId -> Users.Id with ON DELETE CASCADE
(both in fresh schema and migration)
- Reject push token registration when auth is disabled
(UserId would be null, violating NOT NULL constraint)
- Add $belongsTo association to User in Notification model
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
App::uses('AppModel', 'CameraModel') tells CakePHP to look for AppModel
in a non-existent 'CameraModel' package. The correct second argument is
'Model', which points to app/Model/AppModel.php where the base class
actually lives.
This was likely a copy-paste error — every other model in the codebase
correctly uses App::uses('AppModel', 'Model'). The bug may go unnoticed
when another model loads AppModel first via CakePHP's autoloader, but
causes a fatal error if CameraModel is the first model resolved in a
request (e.g. hitting the camera models API endpoint directly).
Add a User Roles system where roles define reusable permission templates.
When a user has a role assigned, the role provides fallback permissions
(user's direct permissions take precedence; role is used when user has 'None').
Database changes:
- Add User_Roles table with same permission fields as Users
- Add Role_Groups_Permissions table for per-role group overrides
- Add Role_Monitors_Permissions table for per-role monitor overrides
- Add RoleId foreign key to Users table
Permission resolution order:
1. User's direct Monitor/Group permissions (if not 'Inherit')
2. Role's Monitor/Group permissions (if user has role)
3. Role's base permission (if user's is 'None')
4. User's base permission (fallback)
Includes:
- PHP models: User_Role, Role_Group_Permission, Role_Monitor_Permission
- Role management UI in Options > Roles tab
- Role selector in user edit form
- REST API endpoints for roles CRUD
- Translation strings for en_gb
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>