Files
zoneminder/scripts/zmvideo.pl.in
T
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

272 lines
8.6 KiB
Plaintext

#!@PERL_EXECUTABLE@ -wT
#
# ==========================================================================
#
# ZoneMinder Video Creation Script, $Date$, $Revision$
# Copyright (C) 2001-2008 Philip Coombes
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# ==========================================================================
=head1 NAME
zmvideo.pl - ZoneMinder Video Creation Script
=head1 SYNOPSIS
zmvideo.pl [ -e <event_id>,--event=<event_id> | --filter_name=<filter name> | --filter_id=<filter id> ]
[--concat=filename]
[--format <format>]
[--rate=<rate>]
[--scale=<scale>]
[--fps=<fps>]
[--size=<size>]
[--overwrite]
=head1 DESCRIPTION
This script is used to create MPEG videos of events for the web pages
or as email attachments.
=head1 OPTIONS
-c[=filename], --concat[=filename] - When creating videos for multiple events, create a concatenated video as well.
- If not specified, filename is taken from filter name.
-e<event_id>, --event=<event_id> - What event to create the video for
--filter_name=<filter name> - The name of a saved filter to generate a video for all events returned by it.
--filter_id=<filter id> - The id of a saved filter to generate a video for all events returned by it.
-f<format>, --format=<format> - What format to create the video in, default is mpg. For ffmpeg only.
-r<rate>, --rate=<rate> - Relative rate, 1 = realtime, 2 = double speed, 0.5 = half speed etc.
-s<scale>, --scale=<scale> - Scale, 1 = normal, 2 = double size, 0.5 = half size etc.
-t<transforms>, --transform=<transforms> - Transform video. Applies -vf option to ffmpeg.
-F<fps>, --fps=<fps> - Absolute frame rate, in frames per second
-S<size>, --size=<size> - Absolute video size, WxH or other specification supported by ffmpeg
-o, --overwrite - Whether to overwrite an existing file, off by default.
-v, --version - Outputs the currently installed version of ZoneMinder
=cut
use strict;
use bytes;
# ==========================================================================
#
# You shouldn't need to change anything from here downwards
#
# ==========================================================================
@EXTRA_PERL_LIB@
use ZoneMinder;
require ZoneMinder::Filter;
require ZoneMinder::Event;
use DBI;
use autouse 'Data::Dumper'=>qw(Dumper);
use POSIX qw(strftime);
use Getopt::Long qw(:config no_ignore_case );
use Cwd;
use File::Temp qw(tempfile);
use autouse 'Pod::Usage'=>qw(pod2usage);
$| = 1;
$ENV{PATH} = '@ZM_SCRIPT_PATH@';
$ENV{SHELL} = '/bin/sh' if exists $ENV{SHELL};
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
logInit();
my $event_id;
my $concat_name;
my $filter_name;
my $filter_id;
my $format = 'mpg';
my $rate = '';
my $scale = '';
my $fps = '';
my $size = '';
my $transforms = '';
my $overwrite = 0;
my $version = 0;
GetOptions(
'concat|c:s' =>\$concat_name,
'event|e=i' =>\$event_id,
'filter_name=s' =>\$filter_name,
'filter_id=i' =>\$filter_id,
'format|f=s' =>\$format,
'rate|r=f' =>\$rate,
'scale|s=f' =>\$scale,
'fps|F=f' =>\$fps,
'size|S=s' =>\$size,
'transforms|t=s'=>\$transforms,
'overwrite' =>\$overwrite,
'version' =>\$version
) or pod2usage(-exitstatus => -1);
if ( $version ) {
print ZoneMinder::Base::ZM_VERSION . "\n";
exit(0);
}
if ( !( $filter_id or $filter_name or $event_id ) || ($event_id and ( $event_id < 0 ) ) ) {
print( STDERR "Please give a valid event id or filter name\n" );
pod2usage(-exitstatus => -1);
}
if ( ! $Config{ZM_OPT_FFMPEG} ) {
print( STDERR "Mpeg encoding is not currently enabled\n" );
exit(-1);
}
my @formats = split( /\s+/, $Config{ZM_FFMPEG_FORMATS} );
for ( my $i = 0; $i < @formats; $i++ ) {
if ( $i =~ /^(.+)\*$/ ) {
$format = $formats[$i] = $1;
}
}
if ( !$rate && !$fps ) {
$rate = 1;
}
if ( !$scale && !$size ) {
$scale = 1;
}
if ( $rate && ($rate < 0.25 || $rate > 100) ) {
print( STDERR "Rate is out of range, 0.25 >= rate <= 100\n" );
pod2usage(-exitstatus => -1);
}
if ( $scale && ($scale < 0.25 || $scale > 4) ) {
print( STDERR "Scale is out of range, 0.25 >= scale <= 4\n" );
pod2usage(-exitstatus => -1);
}
if ( $fps && ($fps > 30) ) {
print( STDERR "FPS is out of range, <= 30\n" );
pod2usage(-exitstatus => -1);
}
my ( $detaint_format ) = $format =~ /^(\w+)$/;
my ( $detaint_rate ) = $rate =~ /^(-?\d+(?:\.\d+)?)$/;
my ( $detaint_scale ) = $scale =~ /^(-?\d+(?:\.\d+)?)$/;
my ( $detaint_fps ) = $fps =~ /^(-?\d+(?:\.\d+)?)$/;
my ( $detaint_size ) = $size =~ /^(\w+)$/;
$format = $detaint_format;
$rate = $detaint_rate;
$scale = $detaint_scale;
$fps = $detaint_fps;
$size = $detaint_size;
($transforms) = $transforms =~ /^([,\w=]+)$/;
my $dbh = zmDbConnect();
my $cwd = getcwd;
my $video_name;
my @event_ids;
# Fail if the path to a valid ffmpeg binary is not set
if ( ! -x $Config{ZM_PATH_FFMPEG} ) {
Fatal("Ffmpeg binary not found or not executable. Verify ZM_PATH_FFMPEG points to ffmpeg, avconv, or a compatible binary.");
}
if ( $event_id ) {
@event_ids = ( $event_id );
} elsif ( $filter_name or $filter_id ) {
my $Filter = ZoneMinder::Filter->find_one(
($filter_name ? ( Name => $filter_name ) : () ),
($filter_id ? ( Id => $filter_name ) : () ),
);
if ( ! $Filter ) {
Fatal("Filter $filter_name $filter_id not found.");
}
@event_ids = map { $_->{Id} } $Filter->Execute();
if (!@event_ids) {
Fatal("No events found for $filter_name")
} else {
Debug(@event_ids . " events found for $filter_name");
}
$concat_name = $filter_name if $concat_name eq '';
}
my $sql = " SELECT (SELECT max(Delta) FROM Frames WHERE EventId=Events.Id)-(SELECT min(Delta) FROM Frames WHERE EventId=Events.Id) as FullLength,
Events.*,
unix_timestamp(Events.StartDateTime) as Time,
M.Name as MonitorName,
M.Palette
FROM Events
INNER JOIN Monitors as M on Events.MonitorId = M.Id
WHERE Events.Id = ?
";
my $sth = $dbh->prepare_cached( $sql ) or Fatal( "Can't prepare '$sql': ".$dbh->errstr() );
Debug($sql);
my @video_files;
foreach my $event_id ( @event_ids ) {
my $res = $sth->execute( $event_id )
or Fatal( "Can't execute: ".$sth->errstr() );
my $event = $sth->fetchrow_hashref();
my $Event = new ZoneMinder::Event( $$event{Id}, $event );
my $video_file = $Event->GenerateVideo( $rate, $fps, $scale, $size, $overwrite, $format, $transforms );
if ( $video_file ) {
push @video_files, $video_file;
print( STDOUT $video_file."\n" );
} else {
Warning("No video file generated for event $event_id");
}
} # end foreach event_id
if ( $concat_name ) {
($cwd) = $cwd =~ /(.*)/; # detaint
chdir( $cwd );
($concat_name ) = $concat_name =~ /([\-A-Za-z0-9_\.]*)/;
my $video_file = $concat_name . '.'. $detaint_format;
# Create the ffmpeg concat list with a randomized name (atomic O_EXCL) inside
# ZoneMinder's own temp dir, rather than a predictable /tmp/<name>.concat.lst
# in world-writable /tmp, which was open to a symlink/race and leaked
# monitor/event names. The list entries are absolute paths, so its location
# does not affect ffmpeg's resolution.
my ($fd, $concat_list_file) = tempfile('zmconcat-XXXXXX', SUFFIX => '.concat.lst', DIR => '@ZM_TMPDIR@');
foreach ( @video_files ) {
print $fd "file '$_'\n";
}
close $fd;
my $command = $Config{ZM_PATH_FFMPEG}
. " -f concat -safe 0 -i $concat_list_file -c copy "
.$Config{ZM_FFMPEG_OUTPUT_OPTIONS}
." '$video_file' > $Config{ZM_PATH_LOGS}/ffmpeg_${concat_name}.log 2>&1"
;
Debug( $command."\n" );
my $output = qx($command);
my $status = $? >> 8;
unlink $concat_list_file;
if ( $status ) {
Error( "Unable to generate video, check $Config{ZM_PATH_LOGS}/ffmpeg_${concat_name}.log for details");
exit(-1);
}
print( STDOUT $video_file."\n" );
}
exit( 0 );
__END__