Commit Graph

497 Commits

Author SHA1 Message Date
Isaac Connor
295673eb97 fix: reload event when DefaultVideo is stale incomplete.mp4
When an event closes the recording file is renamed from incomplete.* to
<Id>-video.* and the DB row is updated. A stale Event model still reports
DefaultVideo=incomplete.mp4, producing spurious 'File does not exist'
warnings. When the incomplete file is missing, clear the object cache and
re-read the event from the database to check the finished file before
warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:56:11 -04:00
Isaac Connor
82b098276e Print_r of the event model is not useful 2026-06-17 08:00:01 -04:00
Isaac Connor
6678674d7e fix: use utf8mb4 for CakePHP API DB connection for Unicode monitor names refs #4785
CakePHP applies the datasource 'encoding' as SET NAMES, and it was 'utf8',
MySQL's 3-byte utf8mb3 alias. Like the C++ daemon connection, this mangles
4-byte UTF-8 characters in utf8mb4 columns such as Monitors.Name to '?' on
read and truncates them on write, so the API returned and stored corrupted
names. Set it to utf8mb4 to match the schema.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:24:58 -04:00
SteveGilvarry
8801c42064 fix: address review feedback on DB SSL verify option
- API (database.php.default): only set the PDO verify flag when SSL is
  actually configured (ZM_DB_SSL_CA_CERT set), matching the web/Perl/C++
  layers. Previously a fresh install's default (1) would set the flag on a
  non-SSL connection, since the CakePHP datasource merges 'flags' uncondi-
  tionally.
- Both PHP layers: cast to string and trim before parsing the value, and use
  strict in_array, to avoid type-juggling and stray-whitespace edge cases.
- zm_db.cpp: use my_bool (not char) for the MYSQL_OPT_SSL_VERIFY_SERVER_CERT
  fallback argument, the type libmysqlclient expects. That branch only
  compiles on older clients without MYSQL_OPT_SSL_MODE, where my_bool exists.

refs #3816
2026-06-14 15:57:09 +10:00
SteveGilvarry
e60bdc67b2 feat: add ZM_DB_SSL_VERIFY_SERVER_CERT option (portable across MySQL/MariaDB)
Add a ZM_DB_SSL_VERIFY_SERVER_CERT setting so a database connection that uses
ZM_DB_SSL_CA_CERT can talk to a server with a self-signed or otherwise
non-matching certificate. When enabled, verification is by identity (the cert
must chain to the CA and its CN/SAN must match ZM_DB_HOST), consistent across
the C++ daemons, the PHP web interface, the CakePHP API and the Perl scripts.

This re-does the reverted #3817. That PR broke the build because it called
mysql_options(MYSQL_OPT_SSL_VERIFY_SERVER_CERT, ...), and that enum was removed
from the MySQL 8.0 C client in favour of MYSQL_OPT_SSL_MODE; it also passed a
c_str() where a my_bool* was expected, and referenced the PHP constant
unconditionally (fatal on PHP 8 for an upgraded install whose zm.conf predates
the option).

The option that controls server-cert verification differs by client library and
the symbols are enum values, not macros, so CMake feature-detects them by
compiling:
  - HAVE_MYSQL_OPT_SSL_MODE  (MySQL 5.7.11+/8.0, MariaDB Connector/C 3.1+)
  - HAVE_MYSQL_OPT_SSL_VERIFY_SERVER_CERT  (older MariaDB/MySQL)
zm_db.cpp uses SSL_MODE_VERIFY_IDENTITY / SSL_MODE_REQUIRED when the former is
available, else falls back to the latter with a proper my_bool.

Value handling is three-way in every layer: a truthy value verifies, a false-y
value (0/false/no/off) skips verification, and an empty/unset value leaves the
client default in place so existing installs are unchanged on upgrade. PHP, the
API datasource (via PDO flags) and the Perl DSN are all guarded with defined()
checks. Fresh installs default to 1.

Documents the full ZM_DB_* connection and SSL settings, including the hostname
verification gotcha when connecting by IP, in docs/userguide/configfiles.rst.

refs #3816
2026-06-14 13:20:00 +10:00
SteveGilvarry
18011e3e62 fix: make monitors API index portable under ONLY_FULL_GROUP_BY
The monitors index unconditionally LEFT JOINed Groups_Monitors and collapsed
the duplicate rows (monitors in multiple groups) with GROUP BY `Monitor`.`Id`.
That GROUP BY fails under ONLY_FULL_GROUP_BY on engines without functional
dependency detection (MariaDB), raising 1055 'Monitor.Name isn't in GROUP BY',
so /api/monitors.json returned a 500 while /api/monitors/<id>.json worked.

Only join Groups_Monitors when the request filters by group (matching the
existing EventsController pattern and the original commented-out intent), drop
the GROUP BY, and dedupe by monitor Id in the existing result loop to cover
multi-value GroupId filters. Portable across MySQL and MariaDB.

refs #3633
2026-06-14 00:18:26 +10:00
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
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