511 Commits

Author SHA1 Message Date
Isaac Connor
61c4c6606b perf: scope Event neighbor queries to Id only
The view() action sets recursive=1 on the Event model, which the
subsequent find('neighbors') calls inherited. That made each of the four
neighbor lookups (prev/next, prevOfMonitor/nextOfMonitor) SELECT every
column from Events plus LEFT JOIN Monitor and Storage, then fire a
separate Frames hasMany query per neighbor row. Only Event.Id is used
downstream.

Pass fields=Event.Id and recursive=-1 on each neighbor call so the
generated SQL is just:

  SELECT Event.Id FROM Events AS Event WHERE Event.Id < ?
    ORDER BY Event.Id DESC LIMIT 1

The per-monitor variant uses Events_MonitorId_idx which already covers
(MonitorId, Id) via InnoDB's implicit PK suffix, so no schema change is
needed.
2026-05-15 23:01:00 -04:00
Isaac Connor
7b9c1ee1d2 fix: derive event end time from Length when EndDateTime is missing
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>
2026-05-14 06:46:01 -04:00
Isaac Connor
fbf73de262 fix: align auth hash validation with generation and warn on user mismatch
- AppController.php: stop overwriting $_SESSION['remoteAddr'] with bare
  REMOTE_ADDR right after zm_session_start() already populated it from
  HTTP_X_FORWARDED_FOR. The clobber bound generated hashes to the proxy
  IP, but getAuthUser() validates against XFF, so any hash produced
  inside the legacy stateful API path was DOA behind a reverse proxy.
- getAuthUser(): prefer the URL user= parameter over
  \$_SESSION['username'] for filtering, matching what zms's
  zmLoadAuthUser does, and honor ZM_CASE_INSENSITIVE_USERNAMES on the
  primary filter. Warn when the URL user= disagrees with the session
  username (stale hash, cross-tab contamination, or tampered request).
- Add a Debug input dump on entry and an Info-level failure line that
  reports filterUser, XFF, REMOTE_ADDR, rowsTried and the TTL window so
  the next 401 surfaces which input is wrong.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 06:46:01 -04:00
Isaac Connor
c4073c964c feat: encoder parameter templates with editor + REST API closes #4778 closes #4802
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>
2026-05-06 17:24:17 -04:00
Isaac Connor
bb9e74a2f9 fix: only validate Device path for Local monitors
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>
2026-04-08 08:44:07 -04:00
Isaac Connor
ffe6362dc3 fix: harden web interface against injection and SSRF vulnerabilities
FilterTerm.php:
- Replace eval() with safe compare() method for SystemLoad, DiskPercent,
  and DiskBlocks filter conditions (RCE via crafted op/val)
- Validate operator against allowlist in constructor
- Sanitize collate field to alphanumeric/underscore only (SQLi)

onvifprobe.php:
- Use escapeshellarg() on interface, device_ep, soapversion, username,
  and password arguments passed to execONVIF() (command injection)

Event.php:
- Use escapeshellarg() on all arguments to zmvideo.pl instead of
  escapeshellcmd() on the whole command (command injection via format)
- Anchor scale regex with ^ and $ to prevent partial matches

image.php:
- Restrict proxy URL scheme to http/https only (SSRF via file:// etc)

filterdebug.php:
- Use already-sanitized $fid instead of raw $_REQUEST['fid'] (XSS)

MonitorsController.php:
- Use escapeshellarg() on token, username, password, and monitor id
  in zmu shell command instead of escapeshellcmd() on whole command

HostController.php:
- Use escapeshellarg() on path in du command (command injection via mid)
- Remove space from daemon name allowlist (argument injection)

EventsController.php:
- Remove single quotes from interval expression regex (SQLi)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:30:49 -04:00
Isaac Connor
419846c875 fix: sanitize monitor Device path to prevent command injection (GHSA-g66m-77fq-79v9)
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>
2026-03-08 13:19:03 -04:00
Pliable Pixels
fea1c850ac fix: address Copilot review feedback on Notifications API refs #4684
- 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>
2026-03-05 20:39:54 -05:00
Pliable Pixels
9c455cc29d fix: make UserId nullable for no-auth mode refs #4684
- UserId is now DEFAULT NULL instead of NOT NULL
- FK changed to ON DELETE SET NULL (keep token if user deleted)
- Removed auth guard from add() — no-auth mode stores NULL UserId
- No-auth mode already treated as admin by _isAdmin(), so scoping
  works correctly (sees all tokens)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:09:32 -05:00
Pliable Pixels
c6effc12ab fix: add FK constraint, auth guard, and belongsTo for Notifications refs #4684
- 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>
2026-03-05 20:04:03 -05:00
Pliable Pixels
de1f31c6e2 feat: add Notification model, controller, and route refs #4684
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:47:29 -05:00
Isaac Connor
b036408a5b Fix RCE vulnerability via API config edit privilege escalation
Add RBAC checks to ConfigsController edit() and delete() requiring
System=Edit permission, matching the pattern used by other controllers.
Harden System/Readonly column checks with !empty() to handle missing
columns gracefully. Fix command injection in Event.php by using
ZM_PATH_FFMPEG constant with escapeshellarg() instead of hardcoded
unsanitized ffmpeg call. Add is_executable() validation at all exec()
sites using ZM_PATH_FFMPEG as defense-in-depth against poisoned config
values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 13:51:30 -05:00
Isaac Connor
84728cec6d Add Create to canEdit 2026-02-26 07:21:13 -05:00
Nic Boet
d27b565a8d fix: correct App::uses package path in CameraModel
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).
2026-02-15 15:38:08 -05:00
Isaac Connor
4a730d6934 fix: remove vulnerable phpunit dev dependency (CVE-2026-24765)
Remove phpunit/phpunit from require-dev in web/api/composer.json.
The pinned ^3.7 version is vulnerable to unsafe deserialization in
PHPT code coverage handling. Since ZoneMinder does not run CakePHP
unit tests in CI, the dependency is unused.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:59:03 -05:00
Isaac Connor
4e60cb96a7 feat: add User Roles feature for reusable permission templates
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>
2026-01-29 13:34:27 -05:00
Isaac Connor
70781753b9 Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-01-26 09:53:34 -05:00
Isaac Connor
8c3669a2c2 Fix out of date user->MonitorIds 2026-01-26 09:48:22 -05:00
Pliable Pixels
be99efa959 fix: add event ID to tags response. ref #4569 2026-01-26 06:40:42 -05:00
Pliable Pixels
82e1f20cff API: Support multiple Event IDs in TagsController index
Allow comma-separated Event IDs when querying tags, e.g.:
/api/tags/index/Events.Id:123,456,789.json

This converts the comma-separated string to an integer array,
enabling a SQL IN clause for efficient multi-event tag retrieval.

Fixes #4567

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 14:00:46 -05:00
Isaac Connor
ba0aa29cdb Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-01-16 14:55:33 -05:00
Isaac Connor
0ecb344723 Don't contain Tag so we don't include Tag and event_Tag in results. 2026-01-05 17:49:46 -05:00
Isaac Connor
93c81961ed Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-01-05 17:47:23 -05:00
Isaac Connor
44496e8430 Merge pull request #4519 from SteveGilvarry/EventTags2
Add Tags to event search and return tag data with events
2026-01-05 17:47:12 -05:00
Isaac Connor
edb105bb32 Update bootstrap.php.in to use zm_config instead of zm_configvals 2026-01-05 11:14:21 -05:00
Steve Gilvarry
58a0e68731 Add Tags to event search and return tag data with events 2026-01-05 21:43:27 +11:00
Isaac Connor
68f91acf10 Remove zm_configvals. Just use zm_config. Move code into loadConfig. 2025-12-22 13:17:01 -05:00
Isaac Connor
1a1e2e7543 Don't add contains so that it doesn't load all associations for the Tag 2025-12-19 17:50:19 -05:00
Isaac Connor
9c79b1ad6e Make filtering by Tags.Id work 2025-12-19 17:50:10 -05:00
Isaac Connor
f2b84e0a28 Make Filtering by Events.Id work 2025-12-19 17:50:01 -05:00
Isaac Connor
9f6d1053b0 Don't add a space if there is no operator 2025-12-19 17:49:22 -05:00
Isaac Connor
adfcbd1d69 Add route for tags 2025-12-19 16:48:52 -05:00
Isaac Connor
cdbeea439b Add named paramter filtering to Configs api index. Add updating returned config entry with values from zm_config which may have been overridden in /etc/zm/conf.d 2025-12-19 16:48:25 -05:00
Isaac Connor
d8ccd1cdfa Fix Monitor=>Frame in associations 2025-12-19 16:48:15 -05:00
Isaac Connor
2e3faf4a01 Add fix for nginx api rewriting 2025-11-24 17:46:51 -05:00
Isaac Connor
dff38af8af Filter out certain querystring parameters that are for pagination not filtering. Fixes all events not listing in zmNinja 2025-11-19 11:07:19 -05:00
Isaac Connor
9351835815 Update use of allowedmonitors 2025-11-19 09:24:46 -05:00
Isaac Connor
28e4fa4d7f Replace Function=>Capturing 2025-11-17 11:55:58 -05:00
Isaac Connor
dbbe2cbcb8 Add EndDateTIme IS NULL condition when using DateTime 2025-10-30 13:53:28 -04:00
Isaac Connor
802ccdcaa9 Use event->can_view to restrict viewing event 2025-10-23 13:56:33 -04:00
Isaac Connor
65d51c7d0a Add support for DateTime as a filter, which means either StartDateTime OR EndDateTime. This allows montagereview to include video that starts BEFORE the requested time but ends after 2025-10-23 13:53:57 -04:00
Isaac Connor
2a11b14bce Remove Function reference 2025-09-06 11:34:09 -04:00
Isaac Connor
1dde898154 Add support for NOT using named params, using query string instead 2025-09-05 12:42:13 -04:00
Isaac Connor
6427511109 Add IN support to api 2025-07-22 16:49:23 -04:00
Isaac Connor
d5e81d3c4a Fix incorrect use of eventObjas an array. 2025-07-19 18:29:22 -04:00
Isaac Connor
97877335db Add MaxScoreFrameId to the events table and automatically update it during event creation so we don't have to look it up later. Finding the frame with the max score is actually a very heavy query, so lets not do it. 2025-07-15 16:17:36 -04:00
Isaac Connor
c3d4b12286 Do pagination if either page or limit is specified. Otherwise return all 2025-07-11 17:40:54 -04:00
Isaac Connor
5929ccce8d Fix the rename of configvals to zm_configvals 2025-07-09 17:24:19 -04:00
Isaac Connor
7a3a5ca58a If request includes pagination= then restore the old pagination code. 2025-07-09 17:24:02 -04:00
Isaac Connor
974e9fd0f2 When editing a monitor via api, if Capturing=None don't start zmc. Fixes #4348 2025-07-04 16:28:39 -04:00