Commit Graph
13910 Commits
Author SHA1 Message Date
IgorA100 8d1a9054ec - On the Zone page, when changePlayer() is called, execute monitorStream.start() with a 200ms delay; otherwise, the preceding stop() call might not have enough time to complete properly.
- When calling `monitorStream.manageAvailablePlayers()`, now pass the candidate player as an argument instead of determining the player from stored cookies inside `manageAvailablePlayers()`. This is necessary because cookies could belong to either the Watch page or the Zone page.
- On the Zone page, load `hls.min.js` if Go2RTC or RTSP2Web is being used for the monitor.
- `monitorStream.setPlayer()` is now executed within `monitorStream.manageAvailablePlayers()`. Previously, `manageAvailablePlayers()` only handled the assignment of `monitorStream.player`.
- Added a new function, `this.setRTSP2WebType()`, to avoid code duplication. This ensures that `RTSP2WebType` is checked and set even if `setPlayer()` is not called on the page.
- Minor optimization regarding variable usage.
- If `hls.min.js` is not loaded, attempting to play via HLS will now trigger a "The HLS library is not loaded" warning in the browser console and initiate a stream restart. Previously, this would result in an error.
2026-10-17 20:54:57 +03:00
IgorA100 75fa7e2df0 Update Janus condition to include 'zone' view 2026-10-16 17:07:41 +03:00
IgorA100 e66029d73e Manage available players in MonitorStream 2026-10-16 17:04:09 +03:00
Isaac Connor b8f9e00249 Merge pull request #5291 from IgorA100/patch-331473
Fix: Refactor Janus handling in MonitorStream.js
2026-10-16 09:22:38 -04:00
IgorA100 9996e8967f Fix syntax error in MonitorStream.js 2026-10-16 13:09:18 +03:00
IgorA100 a266a82998 Refactor Janus handling in MonitorStream.js
- The `janus` variable is no longer global; it now belongs to `MonitorStream`.
- In the `Janus.init` callback, do not perform any actions if the monitor is in a stopping state or is already stopped.
- Check for the existence of the `destroy` method before calling `janus.destroy()`.
- Allow the Janus player to be selected in `this.selectPlayer` regardless of the `streamChannel` on the Montage page. This will not cause visual confusion, as the Montage page does not allow selecting a `streamChannel` (it uses the default one). 
However, if the default `streamChannel` is a secondary one and the default player is set to Janus, the Janus player would never be selected on the Montage page. I believe this is incorrect, as we should allow for more flexibility.
2026-10-16 13:05:33 +03:00
Isaac ConnorandClaude Opus 5.5 7603a7458f fix: read yes/no, true/false and on/off in config files as booleans in PHP and Perl
A boolean option set in zm.conf or conf.d wins over the Config table, and a
person editing a file writes yes or off rather than 1 or 0. The C++ daemons
now read those words (ConfigFileValue, 4d7609d36), but PHP and Perl took the
value as written, and a non-empty string is true in both. So
ZM_STRICT_VIDEO_CONFIG=off in conf.d left the option on in the web UI and the
Perl scripts while zmc turned it off.

Read a file value for a boolean option the same way in all three: yes, true
and on are 1; no, false and off are 0; anything else stays as written. Only
booleans are read this way, so a string option such as a title keeps "Yes".

PHP defines the file values as constants before it reads the Config table,
since it needs the database settings to connect, and only the table says
which options are booleans. So a word now waits until the table has been
read, apart from ZM_DB_* settings, whose words database.php reads itself.
Perl reads the type with each row.

Tested on 1.38's zm138 database with a conf.d file setting
ZM_OPT_FAST_DELETE=yes (0 in the table), ZM_STRICT_VIDEO_CONFIG=off (1)
and ZM_WEB_TITLE=Yes. The installed PHP and Perl gave 'yes', 'off' and
'Yes'; these give '1', '0' and 'Yes'. tests/php/test_config_file_booleans.php
checks the rule.

refs #5289

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 22:37:07 -04:00
Isaac ConnorandClaude Opus 5.5 68298351ab feat: report which streaming services each monitor uses in telemetry
Add JanusEnabled, Go2RTCEnabled, RTSP2WebEnabled and RTSPServer to the
per-monitor telemetry, so we can tell whether Janus and RTSP2Web are still
in use before deciding whether to deprecate them. Nothing in the existing
payload says.

Bring the privacy statement's list of collected monitor fields up to date
too. It listed Function, which no longer exists, and left out Capturing,
Analysing and Recording, and the source path, which is sent with its user
name, password and host replaced. Only en_gb is updated; the other
translations keep their own lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:27:13 -04:00
IgorA100 c398c6356e Avoiding errors when stopping a Janus stream (MonitorStream.js) 2026-10-15 23:22:24 +03:00
Isaac Connor 5871d50383 Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-10-15 09:00:59 -04:00
Isaac ConnorandClaude Opus 5.5 0960b2064b fix: log the database error when dbQuery's prepare or query returns false
dbQuery()'s two error paths for a prepare() or query() that returns false
logged $pdo->errorInfo. $pdo is not defined there, so PHP warned twice and
the message ended without the error. They also read dbLastError() from the
connection only after ZM\Error() had logged; with a database log target that
inserts into Logs through the same connection, which replaces its errorInfo,
so dbLastError() came back as "00000" instead of the error.

Read the error once, before logging, and use it for both. The query() path
also said "Error preparing" for a statement it ran directly.

These paths run when PDO's error mode doesn't throw; dbConnect() sets
ERRMODE_EXCEPTION, so normally the catch below handles the failure.

tests/php/test_database_error_paths.php drives both paths with
ERRMODE_SILENT and checks for no PHP warning and the real error text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:58:26 -04:00
Nic Boet 35a8daf99e perf: emulate prepares for ZoneMinder's own queries on PHP 8.1+
dbConnect() turns emulated prepares off, so every query of the web UI, the
ajax calls and the includes the API runs (config, servers, users,
permissions) is a server-side prepare: prepare, execute and close, three
round trips to the database where one would do. From PHP 8.1, pdo_mysql
returns integers and floats as PHP ints and floats with emulated prepares
too, so values keep the types they have now. Emulate prepares there and keep
server-side prepares on older PHP, where every value would become a string.

Emulated prepares quote parameters bound as strings, which breaks LIMIT ?.
dbQuery() now binds integer values as integers (everything else as strings,
as execute() did), and the log and watch ajax calls cast their validated
offset and limit to int. With emulation, PDO would also run several
statements given in one string; MYSQL_ATTR_MULTI_STATEMENTS is turned off so
that is refused as before. CakePHP's own connection is not changed: it
already uses PDO's default, emulated prepares.

tests/php/test_database_emulated_prepares.php compares every column of every
table read through both kinds of prepare (types and values), checks
aggregates, LIMIT with positional and named integers, string and null
parameters, and that two statements in one string are refused.
2026-10-14 22:41:42 -05:00
Isaac Connor 24c1c6ab98 Merge pull request #5243 from nabbi/fix/b16-user-self-edit-api
fix-medium: honour ZM_USER_SELF_EDIT in the users API
2026-10-13 18:15:54 -04:00
Isaac Connor 000692daa4 Merge pull request #5245 from nabbi/fix/b19-intelgpu-inline-script
fix-low: drop the Intel GPU view's inline script that CSP blocks
2026-10-13 18:10:31 -04:00
nicandClaude Opus 5.5 cc46523a0b fix: show "map unavailable" when Leaflet is missing; recommend libjs-leaflet on Debian/Ubuntu (#5261)
* fix: show "map unavailable" on the map view when Leaflet is missing

Leaflet is an optional install: map.php loads js/leaflet/leaflet.js only
if it exists, and nothing in the build or the packages puts it there.
Without it views/js/map.js still ran new ZMMap(), which calls L.map(),
so the view stayed blank with "L is not defined" in the console.

Check for window.L first, as monitor.js and event.js already do for
their location maps, and show a translated warning in the map area
instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: recommend libjs-leaflet in the Debian/Ubuntu package and link it

The map view and the location maps of the monitor and event views load
Leaflet from js/leaflet/ in the web root, which nothing installs. Debian
and Ubuntu ship it as libjs-leaflet under /usr/share/javascript/leaflet.

Recommend libjs-leaflet and link /usr/share/zoneminder/www/js/leaflet to
it. Without the package the link dangles and the views behave as now
(map.php and output_script_if_exists() check that the file exists).

fixes #5260

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:23:09 -04:00
Nic BoetandClaude Opus 5.5 b2c915e7aa fix: drop the Intel GPU view's inline script that CSP blocks
intelgpu.php ended with an inline <script> without the CSP nonce, so the
browser refused to run it and logged a Content-Security-Policy violation
each time the view opened. The script bound the Back and Refresh buttons,
which views/js/intelgpu.js already does (loaded by xhtmlFooter()), the
same way the other views do it. Remove the inline copy rather than give it
a nonce: with a nonce both handlers would run on each click, and its
"enable Back when history.length > 1" would fight intelgpu.js, which
disables Back when there is no referrer.

No change in what the buttons do: the inline script never ran.

fixes #5244

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:32:45 -05:00
Nic BoetandClaude Opus 5.5 110d599f75 fix: honour ZM_USER_SELF_EDIT in the users API
UsersController::view() and edit() assigned their permission check with
`$canX = A or B;`. `=` binds tighter than `or`, so only A was stored and the
self check was dropped: with ZM_USER_SELF_EDIT on, a user without System=Edit
got 401 on PUT users/<own id>.json. beforeFilter() also refused every user
with System=None before view() or edit() ran.

Let beforeFilter() through view and edit of the user's own record while
ZM_USER_SELF_EDIT is on, use || in both checks, and limit what such a user
saves to the fields the web UI allows for self edit
(includes/actions/user.php): Password, Language and HomeView. The request
data is rebuilt from those three fields, so Id, System, permissions and the
rest are never saved; a Language without a lang file is refused as in the UI.
Users with System=Edit save as before.

Self view follows the web UI too: a user without System permission sees their
own record only while ZM_USER_SELF_EDIT is on.

fixes #5242

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:28:40 -05:00
Isaac ConnorandClaude Opus 5.5 c3d2e542bc fix: never build an object from an id the request sent as an array
ZM_Object's constructor treats an array as the object's row, and validInt()
and validCardinal() returned an array as they got it (preg_replace() does).
So wherever a request id reached `new ZM\X(...)`, raw or through those
validators, a crafted array built an object whose Id, UserId or MonitorId
the sender chose. The permission check then looked at those, and the delete
or update that followed used the Id. A user with Events=View deleted another
user's filter by posting Id[Id]=<filter>&Id[UserId]=<self> to the filter
delete action; the same shape reached events (MonitorId), users, reports,
snapshots and storage.

validInt() and validCardinal() now give '' for anything that isn't a single
value. The places that built an object from a raw $_REQUEST value (the
control and event ajax handlers, the event, filter, report, snapshot and
storage actions, view_hls, and the download and event detail modals) take
the id through validCardinal().

tests/php/test_request_ids_scalar.php covers the validators with arrays and
fails if web code builds an object straight from $_REQUEST, $_GET or $_POST.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:07:08 -04:00
Isaac Connor b973991159 Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-10-11 21:04:38 -04:00
nicandClaude Opus 5.5 25dba32671 fix: restart zmcontrol.pl when a monitor is edited through the API (#5205)
MonitorsController::edit() restarted zmc through the CakePHP model but
left the monitor's control server running. zmcontrol.pl reads
ControlAddress, ControlDevice and the Control when it starts, so after
an API edit PTZ kept using the old values: a camera password changed
through the API kept failing PTZ with 401 until zmcontrol.pl was
restarted by hand.

Restart the daemons the way the console's save does
(includes/actions/monitor.php), through ZM\Monitor: stop zmc and, for a
controllable monitor, zmcontrol.pl; start them again from the saved row
unless the monitor does not capture, is a WebSite or is deleted.
ZM\Monitor also sends the commands to the monitor's own server in a
multi-server setup, where the model's daemonControl() logged an error and
did nothing.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:23:46 -04:00
Isaac ConnorandClaude Opus 5.5 13138a0afc fix: check each event's monitor when editing event details
actions/eventdetail.php checked only the global Events=Edit permission and
then updated the Name, Cause and Notes of every id in markEids, so a user
denied some monitors could rewrite those monitors' events. The event detail
modal likewise showed the name, cause and notes of any event id it was given.

Load each event and require $event->canEdit(), which adds the per-monitor
view check, as archiving and unarchiving in actions/events.php already do.
The action skips events that fail it; the modal leaves them out, and shows
nothing for a single event that fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:21:55 -04:00
Isaac ConnorandClaude Opus 5.5 7eb7dc4bff fix: only redirect back to the Referer from control and event detail actions when it is on this server
actions/control.php and actions/eventdetail.php redirected to
$_SERVER['HTTP_REFERER'] as is. That is the Referer of the POST itself, which
normally comes from a ZoneMinder page, and the CSRF token keeps other sites
from making the POST, so it isn't the open redirect the zone save had. But it
is the same pattern, and a missing Referer warned about an undefined index and
rendered the action's view instead of redirecting.

Use localRedirectUrl() as the zone save does, falling back to the monitor's
control view and to the events list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:19:01 -04:00
Isaac ConnorandClaude Opus 5.5 16817e4ffc fix: only redirect back to a zone's Referer when it is on this server
After saving a zone, actions/zone.php redirected to the REFERER the zone form
posted, which the form fills from the Referer its page was loaded with. A
zone editor who reached the zone page through a link on another site was sent
to that site after saving: an open redirect, usable to lead someone from a
trusted ZoneMinder page to a look-alike login. Forging the POST itself is
blocked by the CSRF token, so it needs an editor who followed such a link.

localRedirectUrl() returns the target only if it is a query-only URL or an
absolute http(s) URL on the host and port of the current request, and the
fallback otherwise: another host, //host, backslashes, user@host, other
schemes and control characters all give the fallback. The zone save falls
back to the zones list, as it already does with no Referer (#5207).

tests/php/test_local_redirect_url.php covers 21 cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:16:14 -04:00
nicandClaude Opus 5.5 6a35e89c14 fix: go to the zones list after saving a zone opened without a Referer (#5207)
The zone form posts its Referer back as REFERER, and posts it empty when
the form was opened from a typed URL or a bookmark. The action used it
whenever it was set, so $redirect was empty, index.php rendered the zone
view with the action's $zone (a database row, or array() for a new zone,
neither with Points) and ksort() at views/zone.php:105 failed with
'Argument #1 must be of type array, null given'. The zone was saved but
the browser got a 500.

Use REFERER only when it is not empty and fall back to the monitor's
zones list.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:15:08 -04:00
nicandClaude Opus 5.5 f4b46657b3 fix: stop fixAmps() recursing forever on a navbar link with an entity (#5210)
* fix: stop fixAmps() recursing forever on a navbar link with an entity

fixAmps() is declared with one parameter but called itself with a
position, which PHP drops, so every call searched from the first '&'
again. Once ZM_WEB_NAVBAR_LINKS held an entity (&amp;, &#39;, ...) or a
bare '&' followed later by a ';', rendering the left navbar recursed until
PHP ran out of memory, and every page answered 500 for users who use the
left navbar.

Escape the bare ampersands in one preg_replace() instead. Character
references are matched at the '&' itself, so a bare '&' is no longer
taken as valid because an entity appears later in the string, and
hexadecimal references (&#x27;) are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: read the view of a navbar link only when its href has one

getAdditionalLinksHTML() parsed every left navbar link's href as if it
carried a query string with a view parameter. A link to another site
(https://host/docs) logged 'Undefined array key "query"', a parse_str()
deprecation and 'Undefined array key "view"' on every page view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:04:47 -04:00
Isaac ConnorandClaude Opus 5.5 53662582f0 fix: stop the event view resizing and wiping on every gapless event
Gapless playback loads a new event page for each event, and each load moved
the video three times and ended in a page-wide wipe:

- video.js sets the player up on DOMContentLoaded without a size, while the
  first changeScale() ran from initPage() on window load. The browser could
  paint a collapsed video with the controls drawn up under it, and
  changeScale() measured that collapsed layout (an empty inline-block still
  leaves a line box), which made the video too small by about 17px.
- skin.js's ResizeObserver on the navbar statistics called endOfResize() for
  the size it reports when it starts observing, and for every width change of
  the figures, so changeScale() ran again 250ms later and corrected the size.
- With the stats panel turned off, initPage() hid it and widened the video's
  column only after load.
- The page-leave roll-up in skin.js animated the content to nothing on every
  reload, a wipe from the bottom up between events.

Size the video on DOMContentLoaded, after video.js has the player, and
remember the size per monitor (zmEventVideoSize<Id>) so event.php opens the
next page at it; changeScale() still corrects it if the window or layout
changed. Render the stats panel and the video's column width from the
zmEventStats cookie in event.php. Rescale for the navbar only when its height
changes. Move between events through goToEvent(), which sets
skipPageLeaveAnimation so the roll-up is skipped for that reload.

Measured in Chrome at 1920x915 on a 1280x720 event, each frame's video size
and controls position: before, every page went 1x0, 1144x644, 1174x660 with
the controls starting near the top of the video, and the content shrank from
851px to 363px before the reload. After, the page reached by gapless playback
paints at 1174x660 with the controls in place and does not move, and there is
no roll-up. The first event of a session still sizes once, as no size has
been remembered yet. Window resize and the stats button still rescale, for
both video.js and zms events.

tests/js/event-gapless-layout.test.js covers goToEvent() and the
DOMContentLoaded sizing; two of its three tests fail without this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:28:01 -04:00
Isaac ConnorandClaude Opus 5.5 821030e17e fix: read API request fields the way Model::set() saves them refs GHSA-fp33-8fx5-4j6c
requestField() read Model[field] whenever the request's Model entry was an
array, empty or not. CakePHP's Model::set() treats the bare top-level fields
as the model's data when that entry is empty (_setAliasData), so
{"Monitor":[],"EventEndCommand":"x"} passed the System-edit check on the
monitor command fields, which found no value, while save() stored x. A
Monitors=Edit user without System=Edit could set EventStartCommand or
EventEndCommand, which zmc runs through /bin/sh.

Every other check built on requestField() had the same gap: the target
MonitorId when adding or editing an event, the EventId when adding a frame,
and the ParentId when editing a group.

Read Model[field] only when the Model entry is non-empty, as set() does, and
otherwise the bare field; a non-empty entry that is not an array yields
nothing, since set() skips it.

tests/php/test_api_request_field.php runs the real method against each
request shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:36:01 -04:00
Isaac Connor 5c6b122814 Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-10-10 11:50:48 -04:00
Isaac ConnorandClaude Opus 5 6c0f976c22 refactor: move the presentation settings to the Display tab
The skin and css, the language and locale, the date, time and datetime
formats, the default bandwidth and the snapshots feature switch all
decide how the interface looks or what it offers, which is what that tab
is for, but they sat under System among the shutdown command, the shm
key and the audit intervals. System drops from 42 entries to 33 and
Display goes from 32 to 41.

The snapshots switch is read only by the skin: it gates a button in
montage and montage review, a menu item, and the snapshot views and
their permission checkboxes. Nothing in the daemons or the perl scripts
looks at it.

The category key is still 'web'. That is the tab now labelled Display;
the tab whose key is 'display' holds the per-session cookie settings and
stores no configuration at all, so a stored default does not belong
there. The hint setup for the moved entries -- the installed skin list,
the css list for the current skin, the bandwidth options, and the
language and locale lists -- moves to the same branch.

ZM_TIMEZONE stays under System. It reads like a presentation setting,
but Filter.pm uses it for date arithmetic when evaluating filters, so it
changes behaviour and not just what is displayed.

A migration, 1.39.40, is needed as well as the ConfigData change, because a
version upgrade does not rewrite Config: zmupdate only reloads it from
ConfigData under --freshen, which is mutually exclusive with an upgrade
run. Fresh installs take the new category from the generated
zm_create.sql.

Verified both paths converge on 33 system and 41 display entries with
identical membership: a fresh install of this branch, and a copy of a
real pre-change Config table with the migration applied. On the running
install the bandwidth, language and locale entries still render as their
pickers on the new tab, css as its list of choices, and the timezone
picker is still on System with its full list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
2026-10-10 09:32:17 -04:00
Isaac ConnorandClaude Opus 5 7f7c9cc81b refactor: name the options tabs after what they hold
The Display tab holds skin, css and navbar choices kept in a cookie for
the session looking at the page. They are not stored configuration and
do not affect anyone else, but the tab was named as though they were the
system's display settings. The settings that genuinely are the system's
display configuration -- the navbar, titles, thumbnails, event sorting,
animations, alarm popups -- sit on a tab labelled Web, which says where
they apply rather than what they do.

So the labels swap to match: the cookie tab becomes This Session, and
the Web tab becomes Display. A short note at the top of This Session
says the settings are kept in a cookie rather than saved on the server,
since a name alone will not carry that. The note goes inside the form
rather than beside it, because #optionsContainer is a flex row and two
siblings there split the width between them.

Labels only. The category keys are untouched, so ?tab=display and
?tab=web keep working, no configuration entry moves, and no migration is
needed. The one-off version/Versions case that was inline in the menu
builder becomes a small map beside the other two, applied at both places
that build the item list.

Only en_gb gains the new strings. Language files are loaded with en_gb
as the base and the chosen language layered over it, so the other 27
show the English until somebody translates them. Display and Web are
already translated in 22 of them, and those translations stay correct:
the word Display now labels the tab that actually holds display
settings.

Verified on a running install: the sidebar renders This Session for
tab=display and Display for tab=web, the note spans the full width above
the fields, all three tabs return 200 with their entry counts unchanged
and no php diagnostics.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
2026-10-10 09:32:17 -04:00
Isaac ConnorandClaude Opus 5 1043273495 refactor: drop the unused Type column from Controls
Controls.Type looks like the monitor type a control applies to, and has
been kept in step with the Monitors enum since 1.27, but nothing has
ever read it. The control dropdown on the monitor page lists every row
by Name, no query filters on it, and no code compares it to the
monitor's own Type. It was a field the edit form asked for and the
control list displayed back.

The column appears to be MonitorPresets.Type copied by analogy, where
the idea is real: that one decides what a preset creates. That also
explains why every migration altering one altered the other.

Removes the column from the schema, adds the 1.39.39 migration, the edit form row
and its type list, the control list column, the defaults in Control.php
and Control.pm, and the value from all 53 shipped rows in controls.sql.
This supersedes the 1.39.38 retyping of three libvlc controls to Ffmpeg:
the column they were being retyped into is gone.

zmcamtool needs no change -- its import regex rewrites the Id and
captures the Name, so it is not positional past that, and its export is
a mysqldump that follows the live schema.

Dropping Type left the two shipped Pelco rows byte-identical to their
twins, which existed only as a Local and an Ffmpeg copy of the same
control, so the redundant copies are removed: 53 shipped rows become
51, with both Pelco-D and Pelco-P still present once.

Existing installs are deliberately left with their duplicates. The same
pair collapses there too, but deleting rows a monitor's ControlId may
point at, to tidy a repeated name in a dropdown, is not a trade a
migration should make silently.

Verified by loading the old and rewritten controls.sql into scratch
databases and diffing every remaining column across all 53 rows: no
value shifted, which is the failure mode when a positional INSERT loses
a field. The file mixes positional and explicit-column INSERTs, so both
forms were handled. Fresh-install and migrated Controls schemas compare
equal, the shipped data loads into both, and Monitors and MonitorPresets
keep their Type. Build clean, 147 test cases pass, php -l and perl -c
clean on the edited files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
2026-10-10 09:32:17 -04:00
Isaac ConnorandClaude Opus 5 bf8a73e9fb feat: remove Libvlc from the web ui, api and config help
Follows the daemons: no monitor can be type Libvlc any more, so the ui
should not offer it or carry text describing it.

Drops the source-type option and its deprecation notice, the Libvlc arm
of the Source Path and console source-column branches, the entry in the
control-capability type list, and the value from the api's Type
validation list. The OPTIONS_LIBVLC help block goes from all 25 language
files: monitor.php builds that key as OPTIONS_ plus the monitor type, so
it became unreachable the moment the type did. The Libvlc source-type
label goes from the 22 files that carried it.

SourceTypeDeprecated and Deprecated lose their last caller and are
removed. RemoteRtspDeprecated and Removed stay -- the rtsp protocol
option is still shown to monitors already set to it.

The WEB_FILTER_SOURCE help listed the affected source types; corrected
in ConfigData.pm.in and in the one translation that had localised it.

All 25 language files and the four edited php files parse clean under
php -l, and the config module parses under perl -c. No javascript
changed, so eslint has nothing to say here -- it cannot run in this
worktree anyway, which has no node_modules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
2026-10-10 09:32:17 -04:00
Isaac ConnorandClaude Opus 5 fd0450c160 feat: remove the NVSocket capture method
NVSocket spoke a bespoke socket protocol to one vendor's nvr. It has a
single known user, no way for anyone else to test it, and nothing it
reaches that ffmpeg cannot reach over rtsp or http, so it is removed
rather than carried further.

Drops RemoteCameraNVSocket, the NVSOCKET camera type and its parse and
construction arms, the source-type option and settings panel in the web
ui, and the stray ja_jp translation key. The preceding migration moves
any existing rows off the type first, so Monitor::Load cannot meet a
type it no longer recognises -- that path is Fatal and would stop zmc
rather than skip the monitor.

Built clean; unit suite passes (147 cases, 12531 assertions, run from
tests/ where the font fixtures' relative paths resolve). eslint clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
2026-10-10 09:32:17 -04:00
Isaac ConnorandClaude Opus 5 ce16d3eb08 feat: deprecate the Libvlc and NVSocket capture methods
Both reach cameras that ffmpeg reaches. Libvlc does it through a second
streaming library with its own build dependency, threading and bugs; NVSocket
speaks a bespoke socket protocol for one vendor's nvr that we have no way to
test. Neither earns the maintenance, and the same monitors work as Type Ffmpeg
with the camera's rtsp or http url.

Deprecated, not removed. A monitor using either still captures; it logs a
warning at startup naming what to change it to, and the form marks the type and
explains. That follows what Remote/RTSP did before it was removed, which is the
shape that gives people a release to move in.

The warnings were verified firing rather than assumed: switching a monitor to
each type in turn produces the expected message naming that monitor.

php -l and eslint clean; the three pre-existing duplicate lang keys are
unchanged. Suite 147 cases / 12531 assertions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
2026-10-10 09:32:17 -04:00
Isaac ConnorandClaude Opus 5 af0d6ddc32 refactor: remove RTSPDescribe, which lost its reader with the rtsp client
RTSPDescribe chose whether to take the media url from the DESCRIBE
response rather than the configured path. That was a quirk of
ZoneMinder's own rtsp implementation; ffmpeg decides it itself. Removing
that client left the setting with nothing behind it -- the monitor form
still drew the checkbox and the save path still wrote the column, but no
code read it, so the only thing it could do was mislead whoever ticked
it.

Drops the form row and the Remote-only block that existed to hold it,
the javascript that showed and hid it as the protocol changed, the field
in the php and perl monitor models, the default in the save action, the
column from the schema, and the label and help text from all the
language files carrying them.

The 1.39.37 migration drops the column behind an information_schema
guard scoped to DATABASE(), so the file stays re-runnable.

Verified by building a pre-upgrade database from upstream/master's own
schema and applying the migration: the column goes, and Monitors matches a fresh install of this
branch column for column. Migration applied three times with no error.
php -l clean on the four edited files and all 28 language files, perl -c
clean, build clean, 154 test cases pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
2026-10-10 09:32:17 -04:00
Isaac ConnorandClaude Opus 5 a88c6859ca fix: stop the web offering an rtsp protocol that no longer captures
With the rtsp client gone, Type=Remote with Protocol=rtsp cannot capture, so
the monitor form no longer offers it and the camera probes no longer suggest
it.

A monitor already saved as rtsp still sees the option, because dropping it
outright would mean opening such a monitor to read the warning silently
re-saved it as http -- swapping one broken configuration for another. For those
the option is labelled removed and the warning now says the monitor will not
capture until it is changed, rather than that it will stop working in a future
release.

The three probes that suggested Remote/rtsp -- ACTI in both probe
implementations, and Vivotek -- now suggest Type Ffmpeg with the equivalent
rtsp:// Source Path, which is the same stream by a maintained route.

Type=Remote keeps http. mjpeg and snapshot-jpeg cameras are what it is for.

php -l clean on all four files; eslint clean; the three pre-existing duplicate
lang keys are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
2026-10-10 09:32:17 -04:00
Isaac Connor f449b92a3d Only show options sidebar if it isn't in the left menu 2026-10-10 09:06:44 -04:00
Isaac Connor 793684d6b9 SIDEBAR_FOOTER is not part of the actual menu 2026-10-10 09:06:44 -04:00
Isaac ConnorandClaude Opus 5.5 0e9f82513c fix: bind the FOSCAMR2C preset and require Control permission for PTZ refs GHSA-qcm7-vq92-f86f
FOSCAMR2C presetGoto and presetSet interpolated the preset into
"SELECT Label FROM ControlPresets WHERE Preset = $preset" and sent the selected
Label to the camera as name=. For presetGoto the preset came straight from the
web request, so any user who could see the monitor could run a SELECT against
the ZoneMinder database and have the value (the auth hash secret, password
hashes) delivered to the camera link, or use it as a boolean oracle.

- FOSCAMR2C: bind the preset in every statement, refuse a preset that is not a
  number, and look ControlPresets up by MonitorId as Dahua and PSIA do.
- buildControlCommand(): refuse a non-numeric presetGoto preset, and a control
  name that is not a plain method name. zmcontrol calls the name as a method and
  the options are split back out of the command string on spaces.
- zmcontrol.pl: refuse a command that is not a plain method name, so a qualified
  Package::sub name can't call a sub outside the control module.
- canView()/canEdit() with a monitor id returned only the monitor check and
  ignored the area, so canView('Control', $mid) passed for anyone who could see
  the monitor. Require the area permission as well; Monitors stays decided by
  visibleMonitor()/editableMonitor().

tests/php/test_control_command.php covers buildControlCommand() and the
monitor-scoped canView()/canEdit(); tests/perl/test_foscamr2c_preset_sql.pl
checks the statements FOSCAMR2C prepares and what it sends to the camera.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:21:32 -04:00
Isaac ConnorandClaude Opus 5.5 1b2bc60936 fix: give ZM\User real canView/canEdit/canCreate area checks refs GHSA-ff93-w6fm-vqxr
Filter::canView(), canEdit() and canDelete() ask the user object
$u->canEdit('System'), $u->canView('Events') and so on, but ZM\User had no such
methods. The calls fell through to ZM_Object::__call(), which stored the argument
as a property and returned it, so every check returned a truthy area name. Any
account with Events=View passed as a System editor and could save and run a
filter with AutoExecute, an OS command run by zmfilter.pl. The same fall-through
made the filter ownership and filterdebug checks pass for everyone.

Add canView(), canEdit() and canCreate() to ZM\User. They take the level from the
user's own field and from their role, the way the global functions in auth.php do
for the logged-in user, and refuse anything that is not a permission area.
Monitor::commandFieldChangesNotAllowed() now uses canEdit('System') so a role's
System=Edit counts there too.

In the filter action, require edit on a stored filter before applying the
submitted fields when saving over it, since the fields can name a new owner;
stop after a refused save instead of starting the filter in the background; and
require edit on the filter for start/stop/restart, which had no check.

tests/php/test_user_permissions.php drives the real ZM\User, ZM\User_Role and
ZM\Filter classes. The older filter tests stubbed the user with its own
canView/canEdit, which is why they passed while the real class had neither.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:14:17 -04:00
Isaac ConnorandClaude Opus 5.5 c8eb03b4f2 fix: encode values written into page scripts, montage review dates and colours refs GHSA-qm44-862r-fmq3 GHSA-8xqq-932v-9h3j
Montage review wrote two values into single-quoted JavaScript strings
unencoded, inside the page's nonce-bearing script:

- minTime/maxTime, which can come from the date terms of a filter in
  the request, so a crafted link ran script for the Events viewer who
  opened it (GHSA-qm44-862r-fmq3). Stored cookies fed them the same way.
- Monitor.WebColour, which any editor of that monitor can set through
  the API, so it ran script for whoever viewed montage review
  (GHSA-8xqq-932v-9h3j).

Keep only range values that parse as a time, in canonical form, and
write minTime, maxTime and monitorColour with json_encode(). The canvas
style attribute took WebColour raw as well; escape it for HTML.

The same unencoded pattern ran through the other page-script templates,
where earlier fixes had covered named fields one at a time. Pass every
value echoed directly into a quoted string in web/skins/classic/views/js
through validJsStr(), 68 in all, including the timeline view's own
minTime/maxTime and request-derived values such as rate, scale, mode and
streamMode. Numeric and date values are unchanged by it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:20:45 -04:00
Simpler1 c9338125b9 Keep preview from bleeding through the tags dropdown (#5193)
Removed z-index property from #frames img.

I'm not sure if this will have any other adverse effects, but I haven't found any.  

When there are many tags in the dropdown list, the previews bleed through the list and make the tags unselectable where the previews are.  This change allows the tags dropdown to display over the previews as they should.
2026-10-07 18:45:18 -04:00
Isaac Connor a4e803a79f Merge branch '5191-storage-diskspace-null' 2026-10-07 08:51:20 -04:00
Isaac ConnorandClaude Opus 5.5 1779e7acae fix: store the computed event disk space when Storage.DiskSpace is NULL refs #5191
Storage.DiskSpace defaults to NULL on fresh installs, and zmc's
DiskSpace = DiskSpace + N leaves it NULL. Storage::event_disk_space()
treated NULL as uncached and ran SUM(DiskSpace) over Events on every
navbar render and status refresh, taking seconds per page on large
Events tables.

Write the computed sum back to the Storage row (only while it is still
NULL, so a value set by zmaudit is not overwritten). Later page loads use
the stored value and zmc keeps it current. COALESCE the sum so a storage
area with no events stores 0 instead of NULL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:50:55 -04:00
Isaac ConnorandClaude Opus 5.5 1677470af1 fix: require System edit to change a monitor's event commands refs GHSA-vcrj-35c6-g97c
zmc runs a monitor's EventStartCommand and EventEndCommand at the start
and end of each event. Both the classic save and bulk edit actions and
the API monitor add and edit authorized only on the monitor, so a user
with edit on one monitor and no System permission could set a command
for zmc to run as the ZoneMinder service user.

Changing either field now needs System=Edit, as Filter::canEdit()
already requires for AutoExecuteCmd. Monitor::commandFieldChangesNotAllowed()
reports the command fields a change would alter; submitting the current
value is not a change, so other users can still save the rest of the
monitor. The monitor form shows the fields read-only to them. Also
close the EventEndCommand list item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:09:33 -04:00
Isaac ConnorandClaude Opus 5.5 47ec378403 fix: do not show Private option values to System viewers refs GHSA-7g3f-93q8-fhwj
The Options page admits System=View and wrote every Config value into
its inputs, only disabling them for users who cannot edit. The Private
flag was never consulted, so a read-only administrator received
ZM_AUTH_HASH_SECRET, which is enough to sign a token for any user, and
the other Private credentials on their tabs.

For a Private option, show its value only to System editors; others see
that it is hidden. The note about a value overridden in zm.conf, which
printed the overriding value, is skipped for those options too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:09:33 -04:00
Isaac ConnorandClaude Opus 5.5 22f9413333 fix: keep zm.conf values out of the browser refs GHSA-jv9c-pg2m-5m6m
Since b2861d5cf loadConfig() returns the Config table merged with the
zm.conf and conf.d values that have no Config row. Those carry no
Private flag, and skin.js.php writes every config entry not marked
Private into each page as a JavaScript const, for any logged-in user
whatever their permissions. ZM_DB_USER and ZM_DB_PASS were in every
page, the permission error page included. The API's Configs listing
redacts by the same flag.

Mark values that exist only in the config files Private, apart from
ZM_DIR_SOUNDS, the one the browser uses (for the alarm sound).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:09:33 -04:00
ad33e3007e Fix: JPEG snapshot generation when requested frame is unavailable in event video (#5184)
* Fix: JPEG snapshot generation when requested frame is unavailable in event video

* Improve frame extraction fallback for unavailable timestamps

- Limit ffprobe search to a small time range around the requested position
- Validate the generated JPEG before considering extraction successful
- Fix timestamp formatting to be locale-independent
- Remove obsolete command/output handling from callers
-Handle non-monotonic packet PTS order
-- Do not assume ffprobe returns packets sorted by PTS
-- Scan all packets in the selected time range
-- Select the closest packet before and the closest packet after the requested timestamp
-- Avoid selecting an incorrect fallback frame when PTS values are reordered

* Use temporary files for frame extraction

- Write FFmpeg output to a unique temporary file for each extraction attempt
- Atomically rename the validated frame to the target path on success
- Prevent failed concurrent requests from deleting a frame created by another request

* Fix video frame extraction near event end

* Remove frame_time condition from lookback loop

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Isaac Connor <isaac@zoneminder.com>
Co-authored-by: Isaac Connor <iconnor@connortechnology.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-10-06 17:41:30 -04:00
IgorA100 c6b77d44d7 Preserve non-ASCII characters in log messages (#5188) 2026-10-06 08:56:35 -04:00
Isaac ConnorandClaude Opus 5.5 575e5484b3 fix: skip an unconfigured go2rtc under the Auto player
With its player left on Auto, a monitor with go2rtc enabled had
selectNextPlayer() pick go2rtc_webrtc first, and with ZM_GO2RTC_PATH
empty select_go2rtc() then alerted "ZM_GO2RTC_PATH is empty", on the
montage for every such monitor, although nobody had chosen go2rtc.

The fallback chain already skipped go2rtc for a monitor without it
enabled; skip it when ZM_GO2RTC_PATH is empty too. A monitor or browser
that chose go2rtc reaches select_go2rtc() directly and still gets the
alert.

A node test drives selectPlayer() through the restart chain for Auto
without a path, Auto with one, and go2rtc chosen explicitly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 18:36:33 -04:00