Commit Graph
211 Commits
Author SHA1 Message Date
Isaac Connor 84967e9b52 fix: treat event DefaultVideo and Name as filenames in the perl scripts
GenerateVideo used DefaultVideo as the ffmpeg input relative to the event
directory and built the output file from Name with only whitespace
replaced, so an Events=Edit user could make zmvideo read or write any
path the account can reach. zmfilter's generateImage passed
event_path/DefaultVideo to ffmpeg -i, and the %EV% email tag attached
event_path/DefaultVideo, which could mail out any readable file.

Strip everything up to the last path separator from DefaultVideo in all
three places, and reduce Name to [-A-Za-z0-9_.] with no leading dot
before using it as the video filename.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5904023f13448e557dc205edb9440972e4a6482d)
2026-09-24 23:16:11 -04:00
Isaac Connor 2cd0863922 fix: keep filter AutoExecute tag values away from shell parsing
executeCommand substituted %TAG% placeholders in AutoExecuteCmd with raw
event and monitor fields and ran the result with qx(), so the shell parsed
those values. Several are writable by non-admin users: event Name, Cause
and Notes (Events=Edit via ajax/event.php and the API), event tags
(Events=Edit), monitor Name (Monitors=Edit), and Cause/Notes are also set
by zmtrigger clients on TCP 6802. An event named $(cmd) therefore ran cmd
as the web/zm user whenever an admin's AutoExecute command used %EN%,
%EC%, %ED%, %ETAGS%, %MN% and similar tags.

Each tag is now substituted on its own. Values made only of word
characters, spaces and /.,:+=@%- are inserted as before, so commands using
%EID%, %MID%, %EPATH% and similar keep working unchanged. Any other value
is exported as an environment variable ZM_TAG_n and referenced in a form
the shell expands without re-parsing, chosen from the quoting context the
admin placed the tag in: "${ZM_TAG_n}" when bare, ${ZM_TAG_n} inside
double quotes and '"${ZM_TAG_n}"' inside single quotes. Such a value
now always reaches the command literally; a bare one is no longer split
into several words.

substituteTags itself is unchanged, so email and message bodies behave
as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit b9891a4a443f1f5e0d8faa7026518a7463ec9440)
2026-09-24 23:16:11 -04:00
Isaac Connor 093b24a530 fix: extract filter email frames from the event video without a shell
GHSA-pxq8-5c8j-xf3r reports the event Name reaching a shell through
Event::GenerateVideo. That sink is already closed by "fix: run the event
video encoder without a shell" (GHSA-pfph-4j9j-7cv7), which runs ffmpeg as a
list. Reviewing the rest of the Perl video path turned up one more place an
event field reaches a shell: zmfilter's generateImage, used to attach frames
to filter emails and messages when no capture jpeg exists, built

  ffmpeg -nostdin -ss <Delta> -i '<event path>/<DefaultVideo>' ...

and ran it with qx(). DefaultVideo is settable by an operator with
Events=Edit, so a single quote in it ends the quoted argument. The -r check
before it means the named file has to exist, but GenerateVideo itself will
create one whose name is the event Name, so the two fields together are
enough. Driving generateImage directly with DefaultVideo set to
"a';touch${IFS}<marker>;'.mp4" (and that file present) created the marker
before this change.

ffmpeg is now run through a list-form piped open, so there is no shell and
the path is one argument whatever it holds. stdout is still collected for the
debug log as qx() did; if ffmpeg cannot be started, status is reported as
127, as the shell did for a missing command. With the change the same input
passes the literal path to ffmpeg and creates no marker.

See GHSA-pxq8-5c8j-xf3r.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 0f700d30e8b5e67705243029b57005236ccbc3ff)
2026-09-24 19:48:40 -04:00
Steve GilvarryandClaude Opus 5 ba49bbb33c fix: make the Perl scripts' taint-safe PATH configurable
The scripts run under -T, so they cannot trust the caller's PATH and set
their own. Sixteen of them hardcoded /bin:/usr/bin:/usr/local/bin, which
assumes everything they shell out to lives under /usr or /usr/local.

ZoneMinder::General::findDbCommand looks for a database client on that
PATH, and zmupdate.pl and zmcamtool.pl run what it finds. So an install
whose client sits anywhere else cannot apply schema changes:

  sh: mysql: command not found
  Command 'mysql -u'zmuser' ... ' exited with status: 127

even with the client on the caller's PATH. Homebrew on Apple Silicon is
the case that surfaced it - the client is in /opt/homebrew/bin - but a
--prefix=/opt install on Linux has the same shape, as does anything that
keeps its database client outside the FHS locations.

Replaced the literal with @ZM_SCRIPT_PATH@, defaulting to the same three
directories plus wherever cmake actually found a client, and overridable
for packagers who want to pin it. Warn at configure time when no client
is found at all, since that failure otherwise appears much later and
says something unrelated.

Nothing changes for an install whose client is already under /usr/bin:
the directory is only appended when it is not in the list, so the
default stays exactly as it was.

Memory.pm is deliberately left alone. It has a narrower PATH of
/bin:/usr/bin, and the only command it runs is uname through an absolute
path from ZM_PATH_UNAME, so it does not need widening.

Verified on macOS across the three cases: with the client in
/opt/homebrew/bin the directory is appended; -DZM_SCRIPT_PATH= is
respected verbatim; and pointing detection at /usr/bin/mariadb leaves
the default untouched. Build clean, suite 146 cases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5KL9Xbi7K5aGsauLtd8tG
2026-09-13 13:04:59 +10:00
Isaac ConnorandClaude Opus 5 f796053c2a fix: reconnect to db instead of bare ping in zmupdate and zmfilter loops
Both loops called $dbh->ping() and discarded the result. ping() only tests
the connection; DBD::mysql does not reconnect on its own since
mysql_auto_reconnect is off. zmupdate's update-check loop sleeps 3600s
between iterations, so on a server with wait_timeout below an hour the
handle is already closed on wake-up and the following zmDbDo fails with
"The client was disconnected by the server because of inactivity".

zmDbConnect() already pings and reconnects when needed, and assigns the
package global $dbh that zmDbDo uses, so call that instead. zmupdate keeps
passing mysql_multi_statements so a reconnect matches the original handle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 10:27:36 -04:00
Isaac ConnorandClaude Opus 5 0c6cf155ef fix: apply zmfilter's start delay to the daemon it was meant for
The guard skipped the delay when a filter was named. zmpkg starts this as
"zmdc.pl start zmfilter.pl --filter_id=N --daemon", so it always names one --
the boot path never waited, and the only invocation that did was a hand-run
scan of every filter. Exactly backwards from what the delay is for.

--daemon is what marks the unattended case, so gate on that instead, still
skipping when there is a terminal on stdin.

Measured with Filter::Execute stubbed to return no events, so nothing was
deleted or emailed, timing the first call to it:

  invocation                     before   after
  --filter_id=1 --daemon (zmpkg)   0.1s    5.1s
  --filter_id=1 (hand-run)         0.1s    0.1s
  no args, scan all filters        5.1s    0.1s

So filters now start 5 seconds later at boot, which is the point of the
constant, and a hand-run returns immediately.

Also sets zmstats' START_DELAY to 5. The previous commit's message said it
did this and it did not: the edit was written against "=> 5" when the file
said "=> 30", so it silently matched nothing while the interactive skip
beside it landed. Verified this time by running the generated script both
ways -- non-interactive logs "starting in 5 seconds", interactive logs
nothing and proceeds.

That run also closes the gap the previous commit noted: startedInteractively
resolves at runtime in the generated zmstats.pl. It had only been checked
with perl -Tc, which does not catch an unimported sub called with parens --
an earlier run against a stale module path failed with exactly that error and
sent me looking.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
2026-09-05 15:44:49 -04:00
Isaac ConnorandClaude Opus 5 6cfe9d7ab0 fix: replace zmwatch's fixed start delay with a per-monitor startup grace
zmwatch slept 30 seconds before its first pass. The reason is real: when it
starts, zmc has not necessarily created its shared memory or written a
heartbeat, and neither state is distinguishable from a camera that has died,
so the first pass would restart every monitor.

A fixed sleep is the wrong shape for that. It is not derived from anything --
ZM_WATCH_CHECK_INTERVAL is 10 and ZM_WATCH_MAX_DELAY is 45, so the delay was
three check intervals and shorter than the staleness threshold it was
standing in for. On a busy host with many cameras, or one camera slow to
answer, 30 seconds is not enough and the fleet gets restarted anyway. It also
blocks every other check, and makes running zmwatch by hand a 30 second wait.

Each monitor now gets until ZM_WATCH_MAX_DELAY after zmwatch started -- the
same threshold used to judge a heartbeat stale -- to appear, and only if we
have never yet seen it healthy. Once seen healthy it is judged immediately, so
a camera that dies later is caught exactly as fast as before, and every check
other than the restart runs from the first pass.

Verified against the two monitors on a machine with no daemons running, which
is the boot state, with Monitor::control stubbed so nothing was actually
started. Before: both monitors restarted on the first pass after the sleep,
and again every 10 seconds. After: no restart for 45 seconds, then both
restarted on the first pass past the grace, at 50 seconds. So a monitor that
never comes up is still caught, ~20 seconds later than before at boot and at
the same speed as before thereafter.

Also adds ZoneMinder::General::startedInteractively and uses it to skip the
start delay in zmstats.pl and zmfilter.pl when a person ran them. zmdc.pl
reopens STDIN on /dev/null for everything it starts, so a terminal on STDIN
reliably means a hand-run; zmtelemetry.pl already relies on this. A run from
cron or a unit file has no terminal either and still waits, which is the right
way round. zmstats keeps a delay because it is polite while zmc and zma are
competing for the machine, but nothing in it races with startup -- it connects
to the database first and has its own reconnect loop -- so the value is now 5
rather than 30.

Note zmfilter's existing guard is inverted with respect to that intent:
zmpkg starts it as "--filter_id=N --daemon", and the guard skips the delay
when a filter is named, so the daemon path never waited and only a manual
scan-all did. Left as it is here beyond adding the interactive case; worth a
separate look.

The zmstats and zmfilter changes are not runtime tested. Running either
against a live install prunes and rewrites rows, and there was no throwaway
install to point them at; startedInteractively is verified directly in both
directions, and all three generated scripts pass perl -Tc.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Y6FieTwEXuLhhR4e2yiax
2026-09-05 14:31:57 -04:00
Isaac Connor 831c4ce6b6 fix: lock filter events one at a time instead of locking the whole batch
A filter with LockRows set selected its entire result set FOR UPDATE inside one
transaction, so every lock the per-event work went on to take was held until
the run committed:

  Events[Id] -> Events_Hour/Day/Week/Month[EventId] -> Event_Summaries[MonitorId] -> Storage[Id]

Because the locks accumulated across events, two filters deadlocked: one held
Event_Summaries for a monitor while waiting on a Storage row, the other held
that Storage row while waiting on Event_Summaries for its next event. No
per-event lock ordering can fix that while both rows stay locked for the length
of the batch.

Holding Event_Summaries for the whole run also blocked zmc from opening a new
event on any monitor the filter had touched, since creating an event updates
that row. The transaction spanned ffmpeg encodes, uploads and executed commands
as well, so it could be held open for minutes.

zmfilter now claims one event at a time in Events_Lock and releases it when it
is done with that event, so no InnoDB lock is held across the work. The
per-event body moves into checkFilterEvent.

skip_locked now adds NOT EXISTS over Events_Lock to the filter query rather
than SKIP LOCKED. The exclusion has to happen in the query: a filter whose
whole result set was held elsewhere would otherwise fill its LIMIT with events
it could only skip, and make no progress. It no longer depends on MariaDB 10.6
/ MySQL 8.0.1, so the UI no longer disables the option on older servers.

Also drops the two dbh->commit() calls in the AutoCopy branch. With no
transaction open they would warn, and before this they were silently ending the
batch transaction mid-loop, so AutoCopy filters never had the guarantee
LockRows was supposed to give them.

filterdebug.php was appending a bare ' SKIP LOCKED' after the LIMIT, which is
not valid SQL; it now renders the real clause in the right position.

Adds t/event_lock.t and t/filter_sql.t.
2026-08-19 22:06:02 -05:00
Isaac Connor 0506103aaa fix: QP-encode plain-text email body so URLs survive transit
The non-HTML branch of sendTheEmail() declared Content-Transfer-Encoding
as quoted-printable but passed the body to MIME::Lite unencoded. Mail
clients then QP-decoded literal '=NN' digit pairs in URLs, eating
characters from substitution tags like %EP%, %EPS%, %EPI%. For example
&eid=1947908 was decoded as &eid<0x19>47908 and rendered as eid47908.

Match the HTML branch's pattern by encoding the body via
MIME::QuotedPrint::encode_qp before attaching.

fixes #4822
2026-05-12 08:49:01 -04:00
Isaac Connor 252ac03b77 Move the delay calculation code to after we run the filter so that the time taken running the filter is included 2026-04-06 09:39:01 -04:00
Isaac Connor d8f30811cf Rename last_action to last_reload as that is what it actually is 2026-04-06 09:38:52 -04:00
Isaac Connor f112ead9dd Include ExecuteInterval in warning 2026-04-06 09:38:44 -04:00
Isaac Connor 82261c047e Use now instead of 0 when no last_ran so that elapsesd=0 on initial run, and we don't log a warning. 2026-04-03 11:54:03 -04:00
Isaac Connor b141c9e18a Warn() -> Warning(). Fixes #4724 2026-04-03 11:00:01 -04:00
Isaac ConnorandClaude Opus 4.6 04d28258db fix: use minimum per-filter delay instead of last filter's delay in zmfilter
The sleep delay was overwritten by each filter in the loop, so only the
last filter's timing controlled the sleep. Now tracks the minimum delay
across all filters so no filter oversleeps its ExecuteInterval.

Also removes the unused ZM_FILTER_EXECUTE_INTERVAL reference since each
filter has its own ExecuteInterval, and fixes the overdue warning to
check the unclamped filter_delay and include the filter name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:17:35 -04:00
Isaac Connor a7e25ad18b Only require mail sending modules if we are actually sending mail 2024-08-18 12:18:44 -04:00
Isaac Connor a24fbe4574 Add EmailServer to filter to override server set in options 2024-07-10 17:14:36 -04:00
Isaac Connor 84e1869fb8 Add ELOC substitution to get Latitude and Longitude of event 2024-07-05 17:28:59 -04:00
Isaac Connor 56dec01eef Fix summary email inline images not working. 2024-06-27 08:11:33 -04:00
Isaac Connor efe61f3a83 Remove debug line causing crashes 2024-02-07 16:50:18 -05:00
Nickolai Golubev fa9ddac61d fixed bug in zmfilter.pl.in on line #632, per https://github.com/ZoneMinder/zoneminder/issues/3811 2024-02-04 01:43:33 -08:00
Charlie Root dbb8a942d9 Only require URI::Encode if having attachments 2024-01-24 14:27:38 -08:00
Isaac Connor 0a6139b57d Rename variable from html to text because it is the text case. 2024-01-17 17:08:35 -05:00
Isaac Connor f45c787d07 Instead of replacing with empty string, replace with content_id which is the filename and is the behaviour specified in PR #3769 2024-01-17 17:08:35 -05:00
Isaac Connor 8618900339 If we are not doing an html email, then don't replace with img tags referencing the attachment. Just do the attachment. Introduce %FID% to sub with the id of the filter. 2024-01-15 16:58:54 -05:00
Isaac Connor 878b521a08 Add replacements for EI1, EIM, EIA1, EIAM when there are no alarm frames. 2024-01-12 13:34:36 -05:00
Isaac Connor 9e2a4335d1 Even objects can have cached properties, so add an undef in calls to Storage and Path so that they get recalculated. This should fix invalid errors about Event already being moved, when in fact it hasn't. 2024-01-05 10:43:33 -05:00
Isaac Connor 6f40293e93 Fix incorrect use of zmDbExecute causing emails to not be sent. Fixes #3800 2024-01-04 10:16:38 -05:00
Isaac Connor 112eb6b9ad Add support for EISNAP and EPISNAP for snapshot image 2023-12-19 12:27:02 -05:00
Isaac Connor 4eaf274c67 Use new zmDbExecute helper function, when there are no alarm frames, still do the substitutions so that we don't leave the tags. EIMOD and EIMODG don't depend on frames, so move them out. 2023-12-19 12:16:19 -05:00
Isaac Connor 66e1f968c0 Add a warning when a filter takes longere than it's execution period 2023-12-18 11:40:20 -05:00
Isaac Connor 651fe2596f Remove debug, add ETAGS subsitution. Fix not doing substitutions on the head of a summary email 2023-10-04 17:08:13 -04:00
Isaac Connor bf34db7da2 Log a warning when failed to send an email 2023-09-09 12:04:57 -04:00
Alejandro Criado-Pérez 871bf1772f fix: messages 2023-07-05 01:03:48 +02:00
Isaac Connor 2f827a5178 Fix EPATH substitution 2023-05-30 12:01:14 -04:00
Isaac Connor e51ddc2492 Don't require loading the monitor object for MonitorId 2023-05-29 20:43:49 -04:00
Isaac Connor e8cd673569 Add MID and EID substitutions 2023-05-29 16:23:15 -04:00
Isaac Connor e0d0d17f5e Fix not attaching body when not doing an html body. Fixes #3708 2023-05-16 09:56:01 -04:00
Isaac Connor becabbcd1a Make objdetect attachments have unique filename 2023-04-26 14:54:02 -04:00
Isaac Connor 1d7fe3f8ff objdetect images not existing should not be a warning 2023-04-25 09:59:40 -04:00
Isaac Connor 3fbf16cf74 Only send summary email if there are events 2023-04-24 20:15:11 -04:00
Isaac Connor d2fb365fa9 Add the ability to send a single summary email instead of individual emails per event 2023-04-24 17:53:16 -04:00
Isaac Connor ffb4e05089 Missing objdetect.gif should be a debug, not a warning 2023-04-24 08:28:54 -04:00
Isaac Connor 4030da1caf Fix cutnpaste typo 2023-03-26 10:09:25 -04:00
Isaac Connor 3e34b36ec0 Fix email structure for ios 2023-02-16 17:24:23 -05:00
Isaac Connor 4b6f47eebd Fixup email structure so that inline images properly display while also having a text version. 2023-02-15 17:41:19 -05:00
Isaac Connor c6e93036f3 Include command line in debug output when generating images 2023-02-13 14:46:33 -05:00
Isaac Connor aba6281296 Fix typo 2023-02-12 16:49:46 -05:00
Isaac Connor 6893a3d84d Prevent duplicate attachments. Use eri_encode to escape content-id 2023-02-11 15:35:10 -05:00
Isaac Connor 44f75508c8 Attach a text version of body 2023-02-11 12:36:51 -05:00