Commit Graph
87 Commits
Author SHA1 Message Date
Isaac Connor eafdffcd95 feat: add per-monitor actions that drive a light or a speaker on alarm
A camera that detects motion should be able to sound a speaker, and the
speaker is rarely the camera: it is a separate device with its own address,
credentials, stream and Controls entry. Model it as a monitor and let a
monitor's alarm drive actions on other monitors.

Add Monitors.DeviceClass enum('Camera','Speaker'). This is what the device
is, as distinct from Type, which selects the capture backend - an IP speaker
still captures over Ffmpeg like any other RTSP device, so Type could not
carry the distinction.

Add the MonitorActions table: MonitorId is the monitor that triggers,
TargetMonitorId the device acted on, and the two are frequently different.
TriggerOn covers EventStart, EventEnd, Alarm and Manual.

Which actions a device is offered is decided by its Controls row - CanLight,
CanIndicatorLight, CanAudioPlay - so a device can only be asked to do what it
has been measured to do. The editor filters on this and the save path
re-checks it, because the request is not to be trusted.

Execution goes straight to the target's zmcontrol socket rather than forking
zmcontrol.pl per action: the daemon already accepts a line of JSON there, and
it is the same path the control panel uses. ActionCommandName maps the DB
enum onto a method name as a whitelist, so nothing out of the database
reaches the control daemon uninspected. Actions are fire-and-forget - a
speaker that is offline is logged and skipped, never allowed to hold up event
handling.

Alarm actions fire only on the genuine entry into alarm, not on the
ALERT->ALARM re-entry, which would re-sound a speaker within one incident.
EventEnd runs on the calling thread before the event is handed to the closing
thread, which does not capture `this`.

Manual actions appear as buttons on the watch page, and are the reason the
control panel is now shown for a monitor that has actions but no control of
its own. Firing one sends only the action id; the command and target are
rebuilt server side, and Control rights are required on the target device and
not merely on the monitor the action hangs off.

Also add --file to zmcontrol.pl, without which audioPlay could not be driven
from the command line. The web path was unaffected as it bypasses GetOptions.

Tests: tests/zm_monitor_action.cpp covers the command whitelist, the message
format (including that file id 0 is a real id and that a stale AudioFile is
never passed to a command that takes none), trigger names, and that every
value of the ActionType enum maps to a command.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UvTCzCbvGt8xKQRNCSA7o8
(cherry picked from commit b561341e988af69c3a46db854da410309f7cfa82)
2026-09-11 20:13:02 -05:00
Isaac ConnorandClaude Opus 5 3d8fdf8677 fix: stop the monitor edit form from undeleting a deleted monitor
Saving a deleted monitor undeleted it even though the undelete checkbox was
never ticked.

Two causes. The $types map, whose entries exist so that unticked checkboxes
(which submit nothing) get forced to a default, listed Deleted => 0. But the
Deleted checkbox is not a state toggle, it is an undelete action: it is only
rendered for a deleted monitor and only ever submits 0. Forcing it to 0 when
absent put Deleted => 0 into every $changes set. Remove it from $types so an
absent Deleted simply is not a change.

Second, the guard meant to catch the "new monitor reusing a deleted record's
Id" case tested isset($_REQUEST['newMonitor[Deleted]']), a literal-bracket key
that never exists, so $monitor->Deleted(false) ran on every save of a deleted
record. Since save() writes every field, that persisted. Test empty(mid)
instead, which is exactly the case the comment describes: mid is absent when
adding a new monitor whose requested Id collides with a deleted row. Editing an
existing monitor by mid now only undeletes via the checkbox, which arrives as
newMonitor[Deleted]=0 and shows up in $changes on its own.

Manual test on a deleted monitor: editing and saving with the checkbox
unticked keeps Deleted set and persists the other edits; ticking the checkbox
and saving clears Deleted as before. The restart block was already guarded by
!$monitor->Deleted(), so no capture daemon is started for a monitor that stays
deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDhTBPj9xEaT52pRmAufaP
2026-08-05 22:52:36 -04:00
Isaac ConnorandClaude Opus 4.7 f26dee598e fix: surface DB error when new monitor insert fails fixes #4944
The insert branch only logged via ZM\Error and returned, leaving
$error_message empty. The dispatcher then re-rendered views/monitor.php
with no banner and the user saw an apparent success that silently
discarded their new monitor.

Append $monitor->get_last_error() to $error_message so the existing
<div id="error"> in getBodyTopHTML() shows the actual DB message, and
keep the early return so the user stays on the edit view with the form
values preserved by views/monitor.php's $_REQUEST['newMonitor'] handling.
Matches the pattern already used on the update path at line 273.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-27 12:07:22 -04:00
IgorA100 fa744743e5 Canonicalize the storage path (monitor.php) 2026-06-13 17:23:02 +03:00
IgorA100 c2820d1317 - Don't create symlinks if the monitor name is equal to its ID. This was previously broken in https://github.com/IgorA100/zoneminder/commit/9561ed894738a707814f0b25a11420241b89b29d
- Moved $oldLinkPathFound analysis outside the storages loop
- More correct check for an empty $_REQUEST['monitorName'] value
2026-05-27 12:12:38 +03:00
IgorA100andCopilot Autofix powered by AI d4bbf23c98 Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-27 09:57:42 +03:00
IgorA100 37ed304994 To find old symlinks, compare $oldLinkPath with $linkPath, not $absolutePath. 2026-05-26 16:35:56 +03:00
IgorA100 3de3dc7d21 - Corrected English and Russian translations
- Check opendir($storagePath) before reading files.
- Use strict validation when creating symlinks
2026-05-25 00:35:40 +03:00
IgorA100 a95efdbd42 When changing the monitor name, delete all symlinks in all storages (monitor.php)
Now we scan directories in all storages for any symbolic links to the events folder of the renamed monitor and delete all found symbolic links in all storages. This is necessary to remove any possible symbolic link garbage from the previous operating algorithm or after other failures.

After deleting all old symbolic links, we create a new one with the new monitor name.
2026-05-14 16:49:18 +03:00
IgorA100 9561ed8947 If the monitor name has changed, you must change the symbolic links in all repositories that contain folders for recording events for this monitor. (monitor.php) 2026-05-14 14:13:22 +03:00
IgorA100 4453d15f82 Update monitor.php 2026-05-11 22:09:20 +03:00
IgorA100 dcbb7b65da - When changing the monitor name, also change the symlink, instead of simply deleting the old one.
- Condition adjustment. Changed from:
if (($saferName != $newMonitor['Name']) and !@symlink($mid, $link_path)) {
to:
if (($saferName != $newMonitor['Id']) and !@symlink($mid, $link_path)) {
This condition was changed in https://github.com/ZoneMinder/zoneminder/commit/203418d45ed6fb2a96a77202a7b20c79bc373ef4
But there was probably an error in the condition, since comparing one variable to itself is pointless, unless we're looking for prohibited characters in the name.
2026-05-03 00:58:00 +03:00
Isaac ConnorandClaude Opus 4.6 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 ConnorandClaude Opus 4.6 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
Isaac ConnorandClaude Opus 4.6 e6ace6fcf4 feat: add AUDIT logging level for tracking administrative changes
Add a new AUDIT logging level (-5) between PANIC (-4) and NOLOG (shifted
to -6) across C++, PHP, and Perl loggers. AUDIT entries use code 'AUD'
and syslog priority LOG_NOTICE. They record who changed what, from where,
for monitors, filters, users, config, roles, groups, zones, states,
servers, storage, events, snapshots, control caps, and login/logout.

AUDIT entries have their own retention period (ZM_LOG_AUDIT_DATABASE_LIMIT,
default 1 year) separate from regular log pruning. The log pruning in
zmstats.pl and zmaudit.pl now excludes AUDIT rows from regular pruning
and prunes them independently.

Critical safety: the C++ termination logic is changed from
'if (level <= FATAL)' to 'if (level == FATAL || level == PANIC)' to
prevent AUDIT-level log calls from killing the process.

Includes db migration zm_update-1.39.1.sql to shift any stored NOLOG
config values from -5 to -6.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 18:19:20 -05:00
Isaac ConnorandClaude Opus 4.6 c0016fa00b feat: store zone coordinates as percentages for resolution independence
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>
2026-02-23 18:19:20 -05:00
Isaac ConnorandClaude Opus 4.5 d89f2e59db refactor: rename Janus_Use_RTSP_Restream to Restream
Rename Janus-specific restream fields to be more generic since they are
now used by Go2RTC and RTSP2Web as well:
- Janus_Use_RTSP_Restream → Restream
- Janus_RTSP_User → RTSP_User

Update visibility logic so the Restream checkbox appears when RTSPServer
is enabled AND any streaming service (Janus, Go2RTC, or RTSP2Web) is
selected, rather than only when Janus is enabled.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 18:47:37 -05:00
Steve GilvarryandClaude Sonnet 4.5 c4385768ad Fix default zone creation to account for monitor rotation
When creating a new monitor with Orientation set to ROTATE_90 or
ROTATE_270, the default "All" zone dimensions are now correctly swapped
to match the rotated image dimensions. This prevents zm_zone.cpp from
reporting that zones extend outside of image dimensions and having to
fix them at runtime.

Fixes issue where monitors created with Rotate Right or Rotate Left
would generate warnings like:
"Zone 1/All for monitor X extends outside of image dimensions,
(0,0), (3839,2159) != (2160,3840), fixing"

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-08 22:58:59 +11:00
Isaac Connor 203418d45e Only make symlink if name is different than id 2025-09-06 11:37:07 -04:00
Isaac Connor e42d31fe3b Fix not being able to turn off Go2RTCEnabled 2025-06-26 16:28:16 -04:00
Isaac Connor edb0836fe1 Fix not being able to undelete a monitor 2024-12-09 12:27:42 -05:00
Isaac Connor caeb65233e Fix not being able to unset use WallClockTimestamps 2024-07-08 08:36:55 -04:00
Isaac Connor c2b6f6ba17 Detect when a new monitor uses a specified Id and the record exists in a deleted monitor. Automatically Undelete it. 2024-03-28 12:48:48 -04:00
Isaac Connor b4bbff6353 Use per-monitor permissions 2024-03-05 14:00:40 -05:00
Server 0ff48f631e Implements an option to disable soap_wsa code that doesn't work
with some chinese cameras, more common to Latam zone
2024-02-12 19:22:01 -06:00
Isaac Connor 2d49e93405 Introduce a Create permission for Monitors, so that a given user may Edit existing monitors, but not create new ones. 2024-01-02 12:18:09 -05:00
Isaac Connor aafd4067e0 Dont' restart a monitor if it has been deleted 2023-12-04 14:50:37 -05:00
Isaac Connor 889c809e0b Add RTSP2WebEnabled to defaults 2023-08-09 18:55:36 -04:00
Isaac Connor e13a42aa46 Fix not saving Manufacturer and Model. Have to unset the text input if the ddm is populated 2023-07-12 14:30:49 -04:00
Isaac Connor 969baa3891 Convert user from an array to a User object 2023-04-23 12:57:29 -04:00
Isaac Connor ce0a83f904 Reduce minimum to .05% as that is what seems to be necessary these days 2023-04-05 17:47:03 -04:00
Isaac Connor 21f6bdae35 Change minimum percent to 50% instead of 3% 2023-04-05 14:46:13 -04:00
Isaac Connor d170112ef7 Change minimum percent to 50% instead of 3% 2023-04-05 14:45:30 -04:00
Isaac Connor b1df318491 Use ORM Zone Object for Zone insertion on new monitor 2023-04-05 14:43:50 -04:00
baudneo aab62c5a7e Add Janus RTSP session timeout to PHP (front end) ⚗️
Signed-off-by: baudneo <86508179+baudneo@users.noreply.github.com>
2023-01-09 17:57:26 -07:00
Isaac Connor f06e11a097 Add SectionLengthWarn, use it to disable warnings in logs about section length being exceeded. 2023-01-07 12:33:42 -05:00
Isaac Connor 25100cd592 Fix missed references to oldW and oldH 2022-10-31 09:27:53 -04:00
Isaac Connor 2fb631fa6e Clean up debug. Clone the entire monitor object into oldMonitor instead of cherry-picking properties. Fixes use of new name when deleting old symlink. Always check for existence of monitor dir and symlink after done saving. 2022-10-31 09:27:53 -04:00
Isaac Connor 33455cbad2 Remove left over references to REQUEST['newMonitor']. Add error reporting to creating monitor directory and symlinking when adding a new monitor 2022-10-31 09:27:53 -04:00
Isaac Connor 6b3768fdf6 When saving monitor, only turn off error reporting for symlink. Don't delete old symlink unless the name has changed. 2022-10-31 09:27:53 -04:00
Isaac Connor 1f20b18100 Merge branch 'master' of github.com:ZoneMinder/zoneminder 2022-09-06 11:22:28 -04:00
Isaac Connor d76c425057 Fix saving monitor due to MQTT being empty 2022-09-06 11:22:04 -04:00
Isaac Connor 683acbbc1d Clear Manufacturer and Model so that they don't list as changes 2022-08-31 10:05:52 -04:00
Jonathan Bennett d41792ae00 Adds Janus options Profile-ID Override and Use RTSP Restream to work around camera issues 2022-08-09 00:54:41 -05:00
Isaac Connor cf3c194e21 Handle when there are no servers and hence no defined ServerId in newMonitor 2022-08-03 11:55:44 -04:00
Isaac Connor 10e39b3b13 :wqMerge branch 'master' into replace_function_concept 2022-02-02 17:08:01 -05:00
Jonathan Bennett fb832e7d1b Adds janus_enable_audio, a switch to try to enable audio in live stream viewing. 2022-01-19 00:14:52 -06:00
Isaac Connor d00aaa11e9 default JanusEnabled to 0 so that we can turn it off 2022-01-18 23:02:26 -05:00
Isaac Connor 3241fa59c5 Don't redirect if there was an error so that we display it 2022-01-18 23:02:26 -05:00
Isaac Connor b1d881b118 Report more saving errors to ui 2021-11-17 18:57:36 -05:00