Commit Graph

2632 Commits

Author SHA1 Message Date
Isaac Connor
bac9b8af01 fix: stop IP-less auth hash from poisoning the IP-bound cache slot refs #4921
generateAuthHash() cached every hash under the IP-keyed slot
'AuthHash'.$remoteAddr, but the value was IP-bound only when $useRemoteAddr was
set. getZmuCommand() calls generateAuthHash(false, true), which wrote an
IP-less hash into the IP-bound slot and reset AuthHashGeneratedAt. Because the
status poll runs getZmuCommand (web/ajax/status.php) right before emitting the
auth hash, the poll then served the IP-less hash to the browser; the next
IP-bound request was rejected by the validator, redirecting the user to login
roughly every poll. This happens with a completely stable client IP, so it is
distinct from the IP-rotation case.

Key the cache slot by the address actually baked into the value: only use the
session address when this caller asked for it (and AUTH_HASH_IPS is on). IP-less
and IP-bound hashes now occupy separate slots and can no longer clobber each
other.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:07:44 -04:00
Isaac Connor
97f2fee109 fix: validate FilterTerm tablename against allowlist to prevent SQL injection
The tablename field of a filter term was copied verbatim into SQL by
sql_attr(), while attr, op, val and collate were all sanitized. An
authenticated user with Events View permission could inject SQL via the
filter[Query][terms][N][tablename] request parameter, enabling blind
read access to the whole database (password hashes, camera credentials).

Restrict tablename to the table aliases actually used by the filter
queries (E, M, S, F, T, ET, Snapshots); reject anything else, log it,
and fall back to 'E'.

Refs GHSA-q2w3-h644-f8xq

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:21:50 -04:00
Isaac Connor
fefc1471e6 Merge pull request #4785 from IgorA100/patch-401263
Feat: Allow national Unicode characters in monitor name
2026-06-14 10:10:55 -04:00
Isaac Connor
6d939bf0af Merge pull request #4923 from SteveGilvarry/4223-filter-is-operator
fix: convert filter IS/IS NOT to =/!= for non-NULL values
2026-06-14 09:40:02 -04:00
Isaac Connor
55bbec4c55 Merge pull request #4924 from SteveGilvarry/3816-db-ssl-verify-server-cert
feat: add ZM_DB_SSL_VERIFY_SERVER_CERT option (portable across MySQL/MariaDB)
2026-06-14 09:39:36 -04:00
IgorA100
3983de5d4b Merge branch 'master' into patch-401263 2026-06-14 10:18:33 +03: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
19ac9c97ae fix: negate modulo for IS NOT Odd/Even; clarify IS comment
Addresses review feedback on #4223:
- The IS NOT Odd/Even branch emitted '% 2 = ' (identical to IS), so
  'MaxScore IS NOT Odd' matched odd values instead of even. Emit '% 2 != '
  so the negation is correct.
- Reword the comment to reflect that IS is only preserved for NULL here;
  any other value (including TRUE/FALSE) compares for equality.

refs #4223
2026-06-14 15:43:30 +10:00
SteveGilvarry
fa79d193c8 fix: convert filter IS/IS NOT to =/!= for non-NULL values refs #4223
A filter term such as 'Score IS 2' or 'Score IS NOT 2' emitted literal
SQL 'Score IS 2', which is invalid - IS / IS NOT only accept
NULL/TRUE/FALSE in MySQL. Keep IS/IS NOT only when the value is NULL
(and the existing Odd/Even modulo handling); for any other value emit
= / != so the term is valid and matches as the user intended.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:05:39 +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
Isaac Connor
f53b94b72a Merge pull request #4902 from SteveGilvarry/fix/login-single-session-cookie
fix: emit a single session cookie on login
2026-06-13 11:15:27 -04:00
Isaac Connor
84f9505b4c Merge pull request #4907 from SteveGilvarry/monitor-janus-pin-validcardinal
fix: pass monitor Id through validCardinal in Janus_Pin shell command
2026-06-13 11:14:09 -04:00
IgorA100
3175719860 Merge branch 'master' into patch-401263 2026-06-13 17:24:39 +03:00
IgorA100
fa744743e5 Canonicalize the storage path (monitor.php) 2026-06-13 17:23:02 +03:00
Isaac Connor
6be787be3e Merge pull request #4916 from SteveGilvarry/3443-thumbnail-aspect-ratio
fix: correct thumbnail aspect ratio for high-resolution monitors (refs #3443)
2026-06-13 10:08:05 -04:00
IgorA100
c114625c53 Allow diacritics and non-ASCII digits (Monitor.php) 2026-06-13 16:59:42 +03:00
SteveGilvarry
cd6aeb2c74 fix: correct thumbnail aspect ratio for high-resolution monitors
Event::ThumbnailWidth()/ThumbnailHeight() derived the secondary dimension
through an integer SCALE_BASE scale:

  $scale = intval((SCALE_BASE * THUMB_WIDTH) / Width);
  ThumbnailHeight = reScale(Height, $scale);

For a high-resolution monitor the scale factor is small and intval()
truncates it. e.g. a 2688x1520 monitor with WEB_LIST_THUMB_WIDTH=48 gives
scale = intval(100*48/2688) = intval(1.78) = 1, so the height becomes 15
instead of 27 — a 3.2:1 thumbnail instead of 16:9. The events-list hover
overlay sizes its container from that thumbnail and object-fit:cover-crops
the real (correct-aspect) video into the squashed box, which shows as a
"long narrow" popout.

Compute the secondary dimension directly from the aspect ratio
(round(Height * THUMB_WIDTH / Width)) so no precision is lost.

refs #3443

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:47:13 +10:00
IgorA100
82d45575e6 Merge branch 'master' into patch-401263 2026-06-13 15:32:51 +03:00
SteveGilvarry
81227683b2 fix: pass monitor Id through validCardinal in Janus_Pin shell command
Janus_Pin() interpolated $this->Id() directly into the zmu command
string passed to shell_exec(), while the sibling AlarmCommand() path
already routes the same value through validCardinal() before building
its command. Bring the two into line so every Id reaching a shell sink
is validated as a cardinal number.

Id is the monitor's integer primary key and is not user-controlled, so
this is a consistency and defense-in-depth change rather than a fix for
an exploitable issue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 11:47:23 +10:00
Isaac Connor
a2d6319b0b Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-12 19:46:57 -04:00
SteveGilvarry
5ae582b1a1 fix: emit a single session cookie on login refs #2471
On successful login auth.php called zm_session_clear() followed by
zm_session_regenerate_id(), which together emitted three Set-Cookie
ZMSESSID headers: a deletion, a throwaway intermediate id, and the final
authenticated id. This is the multiple-cookie behaviour reported in #2471.

Add zm_session_regenerate_id_login(), which clears the pre-auth session
data and calls session_regenerate_id(true) to issue one new id while
deleting the old session server-side. Same anti-session-fixation
guarantee in a single Set-Cookie. Logout (zm_session_clear) and the
periodic mid-session regeneration are left unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 20:25:49 +10:00
Isaac Connor
b23aac4665 Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-06-11 14:21:54 -04:00
SteveGilvarry
a918735e9c fix: correct stale SharedData and TriggerData offsets in PHP shm map
The PHP shared memory map in web/includes/Monitor.php was not updated
when last_analysis_viewed_time was added to SharedData (zm_monitor.h,
offset +152), so every field from control_state onward was read 8 bytes
early: control_state, alarm_cause, video_fifo, audio_fifo and janus_pin.
The TriggerData base offset (864) had been updated, so only the
SharedData tail was skewed. The error goes undetected because connect()
only checks the 'valid' byte at +88, which sits in the unaffected
region, and never compares the 'size' field.

Also corrects the TriggerData showtext offset: 'text' is 256 bytes at
912, so 'showtext' starts at 1168, not 1268.

The Perl map (Memory.pm.in) computes offsets from the field sequence
and is unaffected.

Tests: php -l passes; offsets verified field-by-field against the
SharedData struct in src/zm_monitor.h on master.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 15:06:38 +10:00
Isaac Connor
6f8c1a4eca feat: add Filter::buildSortSql for directional event sort specs with integration test 2026-06-09 17:35:39 -04:00
Isaac Connor
5f623078cd feat: parse comma-separated event sort expressions with NULLs-last idiom 2026-06-09 13:10:13 -04:00
IgorA100
245111c956 Merge branch 'master' into patch-401263 2026-06-06 23:24:08 +03:00
Isaac Connor
20f95ccde9 Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-06-04 09:46:11 -04:00
Isaac Connor
f02bc29c0d feat: add Indicator LED control capability with session keepalive refs #4875
Add a CanIndicatorLight capability and status-aware Indicator toggle button. The indicator LED on the ASH21-B, ADC2W and ASH42-B is controlled via the LightGlobal config (configManager get/setConfig); add indicatorLightOn/Off/Status to Dahua_RPC and a model-specific 'Amcrest ASH42-B RPC' Controls row, with the capability also enabled on the ASH21-B/ADC2W/generic rows. Migration zm_update-1.39.13.sql adds the column.

Add Dahua_RPC keepAlive (global.keepAlive) wired into a 30s zmcontrol idle tick, plus session-expiry re-login retry in set_config and the status queries, so the long-running control daemon does not silently fail after the ~60s session timeout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:00:00 -04:00
Isaac Connor
fd83672a11 feat: add status-aware Light control capability and control-command response path refs #4869
Add a CanLight control capability rendering a single status-aware Light toggle button. The ADC2W white light is driven via CoaxialControlIO.control (Type 1, numeric IO); the button queries live state and reflects it (amber when on).

To get device state to the browser, add an opt-in two-way response path to the control protocol: zmcontrol writes a JSON result back only when a request sets wants_response (fire-and-forget commands unchanged, SIGPIPE-safe); Monitor::sendControlCommandWithResponse and ajax/control.php return it.

Also adds get_config/set_config/probe to Dahua_RPC for characterising cameras, the CanLight column (migration zm_update-1.39.12.sql), edit-UI checkbox, and a config unit test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 19:00:00 -04:00
IgorA100
55276e73fe Merge branch 'master' into patch-180448 2026-06-05 12:11:00 +03:00
IgorA100
671e2bc58a Restored the missing <canvas> for audioVisualization (Monitor.php)
Although audioMotion-analyzer.js can create its own <canvas>, we use our own <canvas> because it speeds up performance and avoids some issues.
2026-06-04 20:27:42 +03:00
IgorA100
f3ac936605 Deleted the unnecessary line (Event.php) 2026-06-04 14:47:51 +03:00
IgorA100
2bbb80b056 Merge branch 'master' into patch-180448 2026-06-04 10:19:52 +03:00
IgorA100
cb242008b4 Merge branch 'master' into patch-180448 2026-06-03 23:37:58 +03:00
Steve Gilvarry
0538bd48fc fix: apply filter result limit when an empty Tags term is present
The event filter "Limit to first N" stopped being honored in 1.38. Using
"List Matches" on a filter that should return N events returned all
matching events instead. This was a regression from 1.36, triggered by
the Tags feature.

Root cause chain:

1. The events view adds a default, empty `Tags` term to unsaved filters.
   That term is invalid (empty value) so Filter::sql() correctly omits it
   from the WHERE clause, but pre_sql_conditions()/post_sql_conditions()
   counted it regardless of validity.

2. Tags was classified as a post-sql condition, even though it is filtered
   in SQL (sql_attr() returns T.Id, with EXISTS/NOT EXISTS for any/no tag)
   and its post-sql test() was a no-op stub. So the empty Tags term made
   has_post_sql_conditions true.

3. has_post_sql_conditions being true disabled the SQL LIMIT fast-path in
   ajax/events.php, leaving the PHP-side trim as the only limit enforcement
   -- and that trim used an inverted comparison (limit > row count), so it
   never trimmed when there were more rows than the limit.

Fixes:

- Filter::pre_sql_conditions()/post_sql_conditions() now require
  term->valid(), so empty default terms no longer count as conditions and
  the SQL LIMIT fast-path stays enabled.
- FilterTerm::is_post_sql() no longer returns true for Tags, since Tags is
  handled in SQL.
- ajax/events.php filter-limit trim comparison corrected to '<' so the
  limit is applied when there are more matches than the limit (matches the
  pagination trim below it). This covers genuine post-sql filters such as
  ExistsInFileSystem.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 00:08:34 +10:00
Isaac Connor
2bfab23096 Merge branch 'master' of github.com:ZoneMinder/zoneminder 2026-06-01 17:05:03 -04:00
IgorA100
60a8a59585 Merge branch 'master' into patch-401263 2026-06-01 12:49:50 +03:00
Isaac Connor
97755a969f Merge pull request #4848 from IgorA100/patch-36225
Optimized the drop-down list of found monitors on Onvifprobe page
2026-05-31 12:38:31 -04:00
IgorA100
8ecad0d28e If the requested Object isn't found in the database, generate a Warning instead of an Error. (Object.php) 2026-05-31 17:46:02 +03:00
IgorA100
a233fc1224 - Correct HLS playback on the Watch page
- When executing Event->Length() , get the duration of the recorded event file if the duration in the database table is 0
- If native HLS playback fails in Safari, we first try playing MP4, and only if MP4 playback fails do we switch to MJPEG playback
2026-05-31 16:25:51 +03:00
IgorA100
70cfbbf61b Merge branch 'master' into patch-180448 2026-05-31 14:44:48 +03:00
IgorA100
ea604ab3a7 Merge branch 'master' into patch-36225 2026-05-30 11:08:27 +03:00
IgorA100
9053998247 Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-30 11:08:01 +03:00
Isaac Connor
f7a3263c52 perf: emit sargable range queries for Date/StartDate/EndDate filter terms
zmfilter and the web filter UI generated SQL like
  to_days(E.StartDateTime) = to_days('2026-05-06 09:42:56')
which prevents MySQL from using the StartDateTime index, forcing a
full table scan. With many filter daemons against a large Events
table this saturates mysqld and makes the system unresponsive.

Rewrite the SQL generation in ZoneMinder::Filter (Perl) and
ZM\FilterTerm (PHP) so Date/StartDate/EndDate attrs emit range
expressions against the underlying datetime column:
  E.StartDateTime >= '2026-05-06 00:00:00'
    AND E.StartDateTime < '2026-05-07 00:00:00'
Covers =, !=, >, >=, <, <=, IS, IS NOT, IN, NOT IN, and the
CURDATE()/NOW() values (which use INTERVAL 1 DAY for the upper
bound). EXPLAIN now reports type=range on Events_StartDateTime_idx
where it previously reported type=ALL.

CurrentDate (the constant left-hand expression to_days(NOW()))
keeps its existing form since it does not touch the indexed column.

Add Perl and PHP unit tests under tests/perl/ and tests/php/
exercising the generated SQL across operators.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 22:13:22 -04:00
Isaac Connor
20b15036b5 Merge pull request #4853 from IgorA100/patch-298172
The "Id" in the "Monitor" object is now public instead of protected. This allows us to retrieve Monitor.Id.
2026-05-29 21:39:16 -04:00
IgorA100
97bfdb082e Now we don't need to return "$monitor->Id()" because we can get Monitor.Id after Id has become public. (monitor_probe.php) 2026-05-29 16:27:15 +03:00
IgorA100
1acc859f9a "Id" is now public, not protected. (Monitor.php) 2026-05-29 16:14:04 +03:00
IgorA100
4f9c905f64 The added class must be escaped using htmlspecialchars() (functions.php) 2026-05-29 12:10:39 +03:00
IgorA100
212b743a14 Removed extra parentheses (monitor_probe.php) 2026-05-29 11:46:15 +03:00
IgorA100
a075e55006 Added the "controlHeader" class to the filter block on the Events page (Filter.php) 2026-05-29 00:33:02 +03:00