Files
zoneminder/scripts/ZoneMinder/lib/ZoneMinder/Object.pm
T
Isaac ConnorandClaude Opus 5 ddb5c93211 fix: drive the database with DBD::MariaDB when it is installed fixes #5008
DBD::mysql 5 refuses to connect to a MariaDB server and upstream does not
consider that a bug, so a MariaDB system needs DBD::MariaDB. Prefer it when
present; it drives both servers. ZM_DB_TYPE cannot select it, which is why
setting it to MariaDB does not work: the same value builds the PHP PDO dsn,
where the only valid driver is mysql whichever server is in use.

Swapping the dsn scheme is not enough on its own. DBD::MariaDB spells
DBD::mysql's mysql_* parameters mariadb_* and ignores parameters it does not
recognise, so a dsn built with the wrong prefix does not fail: it drops the
socket path and the TLS settings and connects over plain TCP. Build the dsn in
one place from the chosen driver, renaming caller supplied options too, since
zmupdate.pl asks for mysql_multi_statements.

Read the id of an inserted row through DBI's last_insert_id rather than the
mysql_insertid handle attribute, which DBD::MariaDB does not have and would
answer undef for, leaving events and objects with no Id.

DBD::MariaDB is always utf8mb4, so it takes no mysql_enable_utf8mb4 attribute.

Let cmake accept either driver instead of requiring DBD::mysql, checking in the
same order, and warn when neither is installed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019URmtYqza6Rzi6F7cmabSm
2026-08-20 08:12:25 -04:00

1000 lines
35 KiB
Perl

# ==========================================================================
#
# ZoneMinder Object Module, $Date$, $Revision$
# Copyright (C) 2001-2017 ZoneMinder LLC
#
# 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.
#
# ==========================================================================
#
# This module contains the common definitions and functions used by the rest
# of the ZoneMinder scripts
#
package ZoneMinder::Object;
use 5.006;
use strict;
use warnings;
use Time::HiRes qw{ gettimeofday tv_interval };
use Carp qw( cluck );
require ZoneMinder::Base;
require ZoneMinder::Object_Type;
our @ISA = qw(ZoneMinder::Base);
# ==========================================================================
#
# General Utility Functions
#
# ==========================================================================
sub def_or_undef {
return defined($_[0]) ? $_[0] : 'undef';
}
use ZoneMinder::Config qw(:all);
use ZoneMinder::Logger qw(:all);
use ZoneMinder::Database qw(:all);
use vars qw/ $AUTOLOAD $log $dbh %cache $no_cache/;
*log = \$ZoneMinder::Logger::logger;
*dbh = \$ZoneMinder::Database::dbh;
my $debug = 0;
$no_cache = 0;
use constant DEBUG_ALL=>0;
sub init_cache {
$no_cache = 0;
%cache = ();
} # end sub init_cache
sub new {
my ( $parent, $id, $data ) = @_;
my $ref = ref $id;
$cache{$parent} = {} if ! $cache{$parent};
my $sub_cache = $cache{$parent};
my $self = {};
bless $self, $parent;
if (!$ref) {
# single primary key
no strict 'refs';
my $primary_key = ${$parent.'::primary_key'};
if ( ! $primary_key ) {
my ( $caller, undef, $line ) = caller;
Error( 'NO primary_key for type ' . $parent . ' called from '.$caller.$line);
return;
} # end if
if ( $id and (!$no_cache) and $$sub_cache{$id} ) {
if ( $data ) {
# The reason to use load is if we have overridden it in the object,
$$sub_cache{$id} = $self;
$$sub_cache{$id}->load( $data );
}
return $$sub_cache{$id};
}
if ( ( $$self{$primary_key} = $id ) or $data ) {
#$log->debug("loading $parent $id") if $debug or DEBUG_ALL;
$self->load( $data );
if ( !$no_cache ) {
$$sub_cache{$id} = $self;
} # end if
} # end if
} elsif ($ref eq 'ARRAY') {
@$self{@$id} = @$data{@$id} if @$id;
$self->load( $data );
#$log->debug( $parent . ': ' .$self->to_string() );
return $self;
} # end if single or multiple primary key
return $self;
} # end sub new
sub load {
my ( $self, $data ) = @_;
my $type = ref $self;
if ( !$data ) {
no strict 'refs';
my $table = ${$type.'::table'};
if ( ! $table ) {
Error('No table for type '.$type);
return;
} # end if
my $primary_key = ${$type.'::primary_key'};
if ( !$primary_key ) {
Error('No primary_key for type '.$type);
return;
} # end if
if ( ! $$self{$primary_key} ) {
my ( $caller, undef, $line ) = caller;
Error("$type ::load called without $primary_key from $caller:$line");
} else {
Debug("Loading $type from $table WHERE $primary_key = $$self{$primary_key}");
$data = $ZoneMinder::Database::dbh->selectrow_hashref("SELECT * FROM `$table` WHERE `$primary_key`=?", {}, $$self{$primary_key});
if ( !$data ) {
if ( $ZoneMinder::Database::dbh->errstr ) {
Error( "Failure to load Object record for $type id:$$self{$primary_key}: Reason: " . $ZoneMinder::Database::dbh->errstr );
} else {
Debug("No Results Loading $type from $table WHERE $primary_key = $$self{$primary_key}");
} # end if
delete $$self{$primary_key};
} # end if
} # end if
} # end if ! $data
if ( $data and %$data ) {
@$self{keys %$data} = values %$data;
} # end if
return $data;
} # end sub load
sub lock_and_load {
my ( $self ) = @_;
my $type = ref $self;
no strict 'refs';
my $table = ${$type.'::table'};
if ( ! $table ) {
Error('NO table for type '.$type);
return;
} # end if
my $primary_key = ${$type.'::primary_key'};
if ( !$primary_key ) {
Error('No primary_key for type ' . $type);
return;
} # end if
if ( !$$self{$primary_key} ) {
my ( $caller, undef, $line ) = caller;
Error("$type ::lock_and_load called without $primary_key from $caller:$line");
return;
}
Debug("Lock and Load $type from $table WHERE $primary_key = $$self{$primary_key}");
my $data = $ZoneMinder::Database::dbh->selectrow_hashref("SELECT * FROM `$table` WHERE `$primary_key`=? FOR UPDATE", {}, $$self{$primary_key});
if ( ! $data ) {
if ( $ZoneMinder::Database::dbh->errstr ) {
Error("Failure to load Object record for $type id:$$self{$primary_key}: Reason: ".$ZoneMinder::Database::dbh->errstr);
} else {
Debug("No Results Lock and Loading $type from $table WHERE $primary_key = $$self{$primary_key}");
} # end if
} # end if
if ( $data and %$data ) {
@$self{keys %$data} = values %$data;
} else {
Debug("No values Lock and Loading $type from $table WHERE $primary_key = $$self{$primary_key}");
} # end if
} # end sub lock_and_load
sub save {
my ( $self, $data, $force_insert ) = @_;
my $type = ref $self;
if ( ! $type ) {
my ( $caller, undef, $line ) = caller;
$log->error("No type in Object::save. self:$self from $caller:$line");
}
my $local_dbh = eval '$'.$type.'::dbh';
if ( ! $local_dbh ) {
$local_dbh = $ZoneMinder::Database::dbh;
if ( $debug or DEBUG_ALL ) {
$log->debug("Using global dbh");
}
}
$self->set( $data ? $data : {} );
if ( $debug or DEBUG_ALL ) {
if ( $data ) {
foreach my $k ( keys %$data ) {
$log->debug("Object::save after set $k => $$data{$k} $$self{$k}");
}
}
}
my $table = eval '$'.$type.'::table';
my $fields = eval '\%'.$type.'::fields';
my $debug = eval '$'.$type.'::debug';
#$debug = DEBUG_ALL if ! $debug;
my %sql;
foreach my $k ( keys %$fields ) {
$sql{$$fields{$k}} = $$self{$k} if defined $$fields{$k};
} # end foreach
if ( ! $force_insert ) {
$sql{$$fields{updated_on}} = 'NOW()' if exists $$fields{updated_on};
} # end if
my $serial = eval '$'.$type.'::serial';
my @identified_by = eval '@'.$type.'::identified_by';
my $ac = ZoneMinder::Database::start_transaction( $local_dbh ) if $local_dbh->{AutoCommit};
if ( ! $serial ) {
my $insert = $force_insert;
my %serial = eval '%'.$type.'::serial';
if ( ! %serial ) {
$log->debug("No serial") if $debug;
# No serial columns defined, which means that we will do saving by delete/insert instead of insert/update
if ( @identified_by ) {
my $where = join(' AND ', map { '`'.$$fields{$_}.'`=?' } @identified_by );
if ( $debug ) {
$log->debug("DELETE FROM `$table` WHERE $where");
} # end if
if ( ! ( ( $_ = $local_dbh->prepare("DELETE FROM `$table` WHERE $where") ) and $_->execute( @$self{@identified_by} ) ) ) {
$where =~ s/\?/\%s/g;
$log->error("Error deleting: DELETE FROM $table WHERE " . sprintf($where, map { defined $_ ? $_ : 'undef' } ( @$self{@identified_by}) ).'):' . $local_dbh->errstr);
$local_dbh->rollback() if $ac;
ZoneMinder::Database::end_transaction( $local_dbh, $ac ) if $ac;
return $local_dbh->errstr;
} elsif ( $debug ) {
$log->debug("SQL successful DELETE FROM $table WHERE $where");
} # end if
} # end if
$insert = 1;
} else {
foreach my $id ( @identified_by ) {
if ( ! $serial{$id} ) {
my ( $caller, undef, $line ) = caller;
$log->error("$id nor in serial for $type from $caller:$line") if $debug;
next;
}
if ( ! $$self{$id} ) {
#($$self{$id}) = ($sql{$$fields{$id}}) = $local_dbh->selectrow_array( q{SELECT nextval('} . $serial{$id} . q{')} );
#$log->debug("SQL statement execution SELECT $s returned $$self{$id}") if $debug or DEBUG_ALL;
$insert = 1;
} # end if
} # end foreach
} # end if ! %serial
if ( $insert ) {
my @keys = keys %sql;
my $command = "INSERT INTO `$table` (" . join(',', @keys ) . ') VALUES (' . join(',', map { '?' } @sql{@keys} ) . ')';
if ( ! ( ( $_ = $local_dbh->prepare($command) ) and $_->execute( @sql{@keys} ) ) ) {
my $error = $local_dbh->errstr;
$command =~ s/\?/\%s/g;
$log->error('SQL statement execution failed: ('.sprintf($command, , map { defined $_ ? $_ : 'undef' } ( @sql{@keys}) ).'):' . $local_dbh->errstr);
$local_dbh->rollback() if $ac;
ZoneMinder::Database::end_transaction( $local_dbh, $ac ) if $ac;
return $error;
} # end if
if ( $debug or DEBUG_ALL ) {
$command =~ s/\?/\%s/g;
$log->debug('SQL statement execution: ('.sprintf($command, , map { defined $_ ? $_ : 'undef' } ( @sql{@keys} ) ).'):' );
} # end if
} else {
my @keys = keys %sql;
my $command = "UPDATE `$table` SET " . join(',', map { '`'.$_ . '` = ?' } @keys ) . ' WHERE ' . join(' AND ', map { '`'.$_ . '` = ?' } @$fields{@identified_by} );
if ( ! ( $_ = $local_dbh->prepare($command) and $_->execute( @sql{@keys,@$fields{@identified_by}} ) ) ) {
my $error = $local_dbh->errstr;
$command =~ s/\?/\%s/g;
$log->error('SQL failed: ('.sprintf($command, , map { defined $_ ? $_ : 'undef' } ( @sql{@keys, @$fields{@identified_by}}) ).'):' . $local_dbh->errstr);
$local_dbh->rollback() if $ac;
ZoneMinder::Database::end_transaction( $local_dbh, $ac ) if $ac;
return $error;
} # end if
if ( $debug or DEBUG_ALL ) {
$command =~ s/\?/\%s/g;
$log->debug('SQL DEBUG: ('.sprintf($command, map { defined $_ ? $_ : 'undef' } ( @sql{@keys,@$fields{@identified_by}} ) ).'):' );
} # end if
} # end if
} else { # not identified_by
@identified_by = ('Id') if ! @identified_by;
# If the size of the arrays are not equal which means one or more are missing
my @identified_by_without_values = map { $$self{$_} ? () : $_ } @identified_by;
my $need_serial = @identified_by_without_values > 0;
if ( $force_insert or $need_serial ) {
my @keys = keys %sql;
my $command = "INSERT INTO `$table` (" . join(',', map { '`'.$_.'`' } @keys ) . ') VALUES (' . join(',', map { '?' } @sql{@keys} ) . ')';
if ( ! ( $_ = $local_dbh->prepare($command) and $_->execute( @sql{@keys} ) ) ) {
$command =~ s/\?/\%s/g;
my $error = $local_dbh->errstr;
$log->error('SQL failed: ('.sprintf($command, map { defined $_ ? $_ : 'undef' } ( @sql{@keys}) ).'):' . $error);
$local_dbh->rollback() if $ac;
ZoneMinder::Database::end_transaction( $local_dbh, $ac ) if $ac;
return $error;
} # end if
if ( $debug or DEBUG_ALL ) {
$command =~ s/\?/\%s/g;
$log->debug('SQL DEBUG: ('.sprintf($command, map { defined $_ ? $_ : 'undef' } ( @sql{@keys} ) ).'):' );
} # end if
# DBI's portable accessor: DBD::MariaDB has no mysql_insertid, and
# would hand back undef, leaving the new row's Id unset.
$$self{Id} = $local_dbh->last_insert_id(undef, undef, undef, undef) if !$$self{Id};
} else {
delete $sql{created_on};
my @keys = keys %sql;
my %identified_by = map { $_, $_ } @identified_by;
@keys = map { $identified_by{$_} ? () : $$fields{$_} } @keys;
my $command = "UPDATE `$table` SET " . join(',', map { '`'.$_ . '` = ?' } @keys ) . ' WHERE ' . join(' AND ', map { '`'.$$fields{$_} .'`= ?' } @identified_by );
if ( ! ( $_ = $local_dbh->prepare($command) and $_->execute( @sql{@keys}, @sql{@$fields{@identified_by}} ) ) ) {
my $error = $local_dbh->errstr;
$command =~ s/\?/\%s/g;
$log->error('SQL failed: ('.sprintf($command, map { defined $_ ? $_ : 'undef' } ( @sql{@keys}, @sql{@$fields{@identified_by}} ) ).'):' . $error) if $log;
$local_dbh->rollback() if $ac;
ZoneMinder::Database::end_transaction( $local_dbh, $ac ) if $ac;
return $error;
} # end if
if ( $debug or DEBUG_ALL ) {
$command =~ s/\?/\%s/g;
$log->debug('SQL DEBUG: ('.sprintf($command, map { defined $_ ? ( ref $_ eq 'ARRAY' ? join(',',@{$_}) : $_ ) : 'undef' } ( @sql{@keys}, @$self{@identified_by} ) ).'):' );
} # end if
} # end if
} # end if
ZoneMinder::Database::end_transaction( $local_dbh, $ac ) if $ac;
return '';
} # end sub save
sub set {
my ( $self, $params ) = @_;
my @set_fields = ();
my $type = ref $self;
my %fields = eval ('%'.$type.'::fields');
if ( ! %fields ) {
$log->warn("ZoneMinder::Object::set called on an object ($type) with no fields".$@);
} # end if
my %defaults = eval('%'.$type.'::defaults');
if ( ref $params ne 'HASH' ) {
my ( $caller, undef, $line ) = caller;
$log->error("$type -> set called with non-hash params from $caller $line");
}
if ( $params ) {
foreach my $field ( keys %{$params} ) {
$log->debug("field: $field, ".def_or_undef($$self{$field}).' =? param: '.def_or_undef($$params{$field})) if $debug or DEBUG_ALL;
if ( ( ! defined $$self{$field} ) or ($$self{$field} ne $params->{$field}) ) {
# Only make changes to fields that have changed
if ( defined $fields{$field} ) {
$$self{$field} = $$params{$field};
push @set_fields, $fields{$field}, $$params{$field}; #mark for sql updating
} # end if has a column
$log->debug("Running $field with $$params{$field}") if $debug;
if ( my $func = $self->can( $field ) ) {
$func->( $self, $$params{$field} );
} # end if has function
} # end if has change
} # end foreach field
} # end if $params
foreach my $field ( keys %defaults ) {
if ( ( ! exists $$self{$field} ) or (!defined $$self{$field}) or ( $$self{$field} eq '' ) ) {
$log->debug("Setting default ($field) (".def_or_undef($$self{$field}).') ('.def_or_undef($defaults{$field}).') ') if $debug;
if ( defined $defaults{$field} ) {
if ( $defaults{$field} eq '' or $defaults{$field} eq 'NOW()' ) {
$$self{$field} = $defaults{$field};
} else {
$$self{$field} = eval($defaults{$field});
$log->error( "Eval error of object default $field default ($defaults{$field}) Reason: " . $@ ) if $@;
} # end if
} else {
$$self{$field} = $defaults{$field};
} # end if
$log->debug("Setting default for ($field) using (".def_or_undef($defaults{$field}).') to ('.def_or_undef($$self{$field}).') ') if $debug;
} elsif ( defined $fields{$field} and $$self{$field} ) {
$$self{$field} = transform( $type, $field, $$self{$field} );
} # end if
} # end foreach default
return @set_fields;
} # end sub set
sub transform {
my $type = ref $_[0];
$type = $_[0] if ! $type;
my $fields = eval '\%'.$type.'::fields';
my $value = $_[2];
if ( defined $$fields{$_[1]} ) {
my @transforms = eval('@{$'.$type.'::transforms{$_[1]}}');
$log->debug("Transforms for $_[1] before $_[2]: @transforms") if $debug;
if ( @transforms ) {
foreach my $transform ( @transforms ) {
if ( $transform =~ /^s\// or $transform =~ /^tr\// ) {
eval '$value =~ ' . $transform;
} elsif ( $transform =~ /^<(\d+)/ ) {
if ( $value > $1 ) {
$value = undef;
} # end if
} else {
$log->debug("evalling $value ".$transform . " Now value is $value" );
eval '$value '.$transform;
$log->error("Eval error $@") if $@;
}
$log->debug("After $transform: $value") if $debug;
} # end foreach
} # end if
} else {
$log->error("Object::transform ($_[1]) not in fields for $type");
} # end if
return $value;
} # end sub transform
sub to_string {
my $type = ref($_[0]);
my $fields = eval '\%'.$type.'::fields';
if ( $fields and %{$fields} ) {
return $type . ': '. join(' ', map { $_[0]{$_} ? "$_ => $_[0]{$_}" : () } sort { $a cmp $b } keys %$fields );
}
return $type . ': '. join(' ', map { $_ .' => '.(defined $_[0]{$_} ? $_[0]{$_} : 'undef') } sort { $a cmp $b } keys %{$_[0]} );
}
# We make this a separate function so that we can use it to generate the sql statements for each value in an OR
sub find_operators {
my ( $field, $type, $operator, $value ) = @_;
$log->debug("find_operators: field($field) type($type) op($operator) value($value)") if DEBUG_ALL;
my $add_placeholder = ( ! ( $field =~ /\?/ ) ) ? 1 : 0;
if ( $operator eq '='
or $operator eq '!='
or $operator eq '<'
or $operator eq '>'
or $operator eq '<='
or $operator eq '>='
or $operator eq '<<=' ) {
return ( $field.$type.' ' . $operator . ( $add_placeholder ? ' ?' : '' ), $value );
} elsif ( $operator eq 'not' ) {
return ( '( NOT ' . $field.$type.')', $value );
} elsif ( $operator eq '&&' or $operator eq '<@' or $operator eq '@>' ) {
if ( ref $value eq 'ARRAY' ) {
if ( $field =~ /^\(/ ) {
return ( 'ARRAY('.$field.$type.') ' . $operator . ' ?', $value );
} else {
return ( $field.$type.' ' . $operator . ' ?', $value );
} # emd of
} else {
return ( $field.$type.' ' . $operator . ' ?', [ $value ] );
} # end if
} elsif ( $operator eq 'exists' ) {
return ( $value ? '' : 'NOT ' ) . 'EXISTS ' . $field.$type;
} elsif ( $operator eq 'in' or $operator eq 'not in' ) {
if ( ref $value eq 'ARRAY' ) {
return ( $field.$type.' ' . $operator . ' ('. join(',', map { '?' } @{$value} ) . ')', @{$value} );
} else {
return ( $field.$type.' ' . $operator . ' (?)', $value );
} # end if
} elsif ( $operator eq 'contains' ) {
return ( '? IN '.$field.$type, $value );
} elsif ( $operator eq 'does not contain' ) {
return ( '? NOT IN '.$field.$type, $value );
} elsif ( $operator eq 'like' or $operator eq 'ilike' ) {
return $field.'::text ' . $operator . ' ?', $value;
} elsif ( $operator eq 'null_or_<=' ) {
return '('.$field.$type.' IS NULL OR '.$field.$type.' <= ?)', $value;
} elsif ( $operator eq 'is null or <=' ) {
return '('.$field.$type.' IS NULL OR '.$field.$type.' <= ?)', $value;
} elsif ( $operator eq 'null_or_>=' ) {
return '('.$field.$type.' IS NULL OR '.$field.$type.' >= ?)', $value;
} elsif ( $operator eq 'is null or >=' ) {
return '('.$field.$type.' IS NULL OR '.$field.$type.' >= ?)', $value;
} elsif ( $operator eq 'null_or_>' or $operator eq 'is null or >' ) {
return '('.$field.$type.' IS NULL OR '.$field.$type.' > ?)', $value;
} elsif ( $operator eq 'null_or_<' or $operator eq 'is null or <' ) {
return '('.$field.$type.' IS NULL OR '.$field.$type.' < ?)', $value;
} elsif ( $operator eq 'null_or_=' or $operator eq 'is null or =' ) {
return '('.$field.$type.' IS NULL OR '.$field.$type.' = ?)', $value;
} elsif ( $operator eq 'null or in' or $operator eq 'is null or in' ) {
return '('.$field.$type.' IS NULL OR '.$field.$type.' IN ('.join(',', map { '?' } @{$value} ) . '))', @{$value};
} elsif ( $operator eq 'null or not in' ) {
return '('.$field.$type.' IS NULL OR '.$field.$type.' NOT IN ('.join(',', map { '?' } @{$value} ) . '))', @{$value};
} elsif ( $operator eq 'exists' ) {
return ( $value ? ' EXISTS ' : 'NOT EXISTS ' ).$field;
} elsif ( $operator eq 'lc' ) {
return 'lower('.$field.$type.') = ?', $value;
} elsif ( $operator eq 'uc' ) {
return 'upper('.$field.$type.') = ?', $value;
} elsif ( $operator eq 'trunc' ) {
return 'trunc('.$field.$type.') = ?', $value;
} elsif ( $operator eq 'any' ) {
if ( ref $value eq 'ARRAY' ) {
return '(' . join(',', map { '?' } @{$value} ).") = ANY($field)", @{$value};
} else {
return "? = ANY($field)", $value;
} # end if
} elsif ( $operator eq 'not any' ) {
if ( ref $value eq 'ARRAY' ) {
return '(' . join(',', map { '?' } @{$value} ).") != ANY($field)", @{$value};
} else {
return "? != ANY($field)", $value;
} # end if
} elsif ( $operator eq 'is null' ) {
if ( $value ) {
return $field.$type. ' is null';
} else {
return $field.$type. ' is not null';
} # end if
} elsif ( $operator eq 'is not null' ) {
if ( $value ) {
return $field.$type. ' is not null';
} else {
return $field.$type. ' is null';
} # end if
} else {
$log->warn("find_operators: op not found field($field) type($type) op($operator) value($value)");
} # end if
return;
} # end sub find_operators
sub get_fields_values {
my ( $object_type, $search, $param_keys ) = @_;
my @used_fields;
my @where;
my @values;
no strict 'refs';
foreach my $k ( @$param_keys ) {
if ( $k eq 'or' ) {
my $or_ref = ref $$search{or};
if ( $or_ref eq 'HASH' ) {
my @keys = keys %{$$search{or}};
if ( @keys ) {
my ( $where, $values, $used_fields ) = get_fields_values( $object_type, $$search{or}, \@keys );
push @where, '('.join(' OR ', @{$where} ).')';
push @values, @{$values};
} else {
$log->error("No keys in or");
}
} elsif ( $or_ref eq 'ARRAY' ) {
my %s = @{$$search{or}};
my ( $where, $values, $used_fields ) = get_fields_values( $object_type, \%s, [ keys %s ] );
push @where, '('.join(' OR ', @{$where} ).')';
push @values, @{$values};
} else {
$log->error("Deprecated use of or $or_ref for $$search{or}");
} # end if
push @used_fields, $k;
next;
} elsif ( $k eq 'and' ) {
my $and_ref = ref $$search{and};
if ( $and_ref eq 'HASH' ) {
my @keys = keys %{$$search{and}};
if ( @keys ) {
my ( $where, $values, $used_fields ) = get_fields_values( $object_type, $$search{and}, \@keys );
push @where, '('.join(' AND ', @{$where} ).')';
push @values, @{$values};
} else {
$log->error("No keys in and");
}
} elsif ( $and_ref eq 'ARRAY' and @{$$search{and}} ) {
my @sub_where;
for( my $p_index = 0; $p_index < @{$$search{and}}; $p_index += 2 ) {
my %p = ( $$search{and}[$p_index], $$search{and}[$p_index+1] );
my ( $where, $values, $used_fields ) = get_fields_values( $object_type, \%p, [ keys %p ] );
push @sub_where, @{$where};
push @values, @{$values};
}
push @where, '('.join(' AND ', @sub_where ).')';
} else {
$log->error("incorrect ref of and $and_ref");
}
push @used_fields, $k;
next;
}
my ( $field, $type, $function ) = $k =~ /^([_\+\w\-]+)(::\w+\[?\]?)?[\s_]*(.*)?$/;
$type = '' if ! defined $type;
$log->debug("$object_type param $field($type) func($function) " . ( ref $$search{$k} eq 'ARRAY' ? join(',',@{$$search{$k}}) : $$search{$k} ) ) if DEBUG_ALL;
foreach ( 'find_fields', 'fields' ) {
my $fields = \%{$object_type.'::'.$_};
if ( ! $fields ) {
$log->debug("No $fields in $object_type") if DEBUG_ALL;
next;
} # end if
if ( ! $$fields{$field} ) {
#$log->debug("No $field in $_ for $object_type") if DEBUG_ALL;
next;
} # end if
# This allows mainly for find_fields to reference multiple values, opinion in Project, value
foreach my $db_field ( ref $$fields{$field} eq 'ARRAY' ? @{$$fields{$field}} : $$fields{$field} ) {
if ( ! $function ) {
$db_field .= $type;
if ( ref $$search{$k} eq 'ARRAY' ) {
$log->debug("Have array for $k $$search{$k}") if DEBUG_ALL;
if ( ! ( $db_field =~ /\?/ ) ) {
if ( @{$$search{$k}} != 1 ) {
push @where, '`'.$db_field .'` IN ('.join(',', map {'?'} @{$$search{$k}} ) . ')';
} else {
push @where, '`'.$db_field.'`=?';
} # end if
} else {
$log->debug("Have question ? for $k $$search{$k} $db_field") if DEBUG_ALL;
$db_field =~ s/=/IN/g;
my $question_replacement = '('.join(',', map {'?'} @{$$search{$k}} ) . ')';
$db_field =~ s/\?/$question_replacement/;
push @where, $db_field;
}
push @values, @{$$search{$k}};
} elsif ( ref $$search{$k} eq 'HASH' ) {
foreach my $p_k ( keys %{$$search{$k}} ) {
my $v = $$search{$k}{$p_k};
if ( ref $v eq 'ARRAY' ) {
push @where, '`'.$db_field.'` IN ('.join(',', map {'?'} @{$v} ) . ')';
push @values, $p_k, @{$v};
} else {
push @where, '`'.$db_field.'`=?';
push @values, $p_k, $v;
} # end if
} # end foreach p_k
} elsif ( ! defined $$search{$k} ) {
push @where, $db_field.' IS NULL';
} else {
if ( ! ( $db_field =~ /\?/ ) ) {
push @where, '`'.$db_field .'`=?';
} else {
push @where, $db_field;
}
push @values, $$search{$k};
} # end if
push @used_fields, $k;
} else {
#my @w =
#ref $search{$k} eq 'ARRAY' ?
#map { find_operators( $field, $type, $function, $_ ); } @{$search{$k}} :
my ( $w, @v ) = find_operators( $db_field, $type, $function, $$search{$k} );
if ( $w ) {
#push @where, '(' . join(' OR ', @w ) . ')';
push @where, $w;
push @values, @v if @v;
push @used_fields, $k;
} # end if @w
} # end if has function or not
} # end foreach db_field
} # end foreach find_field
} # end foreach k
return ( \@where, \@values, \@used_fields );
}
sub find {
no strict 'refs';
my $object_type = shift;
my $debug = ${$object_type.'::debug'};
$debug = DEBUG_ALL if ! $debug;
my $starttime = [gettimeofday] if $debug;
my $params;
if ( @_ == 1 ) {
$params = $_[0];
if ( ref $params ne 'HASH' ) {
$log->error("params $params was not a has");
} # end if
} else {
$params = { @_ };
} # end if
my $local_dbh = ${$object_type.'::dbh'};
if ($$params{dbh}) {
$local_dbh = $$params{dbh};
delete $$params{dbh};
} elsif (!$local_dbh) {
$local_dbh = $dbh;
} # end if
my $sql = find_sql($object_type, $params);
my $do_cache = $$sql{columns} ne '*' ? 0 : 1;
#$log->debug( 'find prepare: ' . sprintf('%.4f', tv_interval($starttime)*1000) ." useconds") if $debug;
my $data = $local_dbh->selectall_arrayref($$sql{sql}, { Slice => {} }, @{$$sql{values}});
if ( ! $data ) {
$log->error('Error ' . $local_dbh->errstr() . " loading $object_type ($$sql{sql}) (". join(',', map { ref $_ eq 'ARRAY' ? 'ARRAY('.join(',',@$_).')' : $_ } @{$$sql{values}} ) . ')' );
return ();
#} elsif ( ( ! @$data ) and $debug ) {
#$log->debug("No $type ($sql) (@values) " );
} elsif ( $debug ) {
$log->debug("Loading Debug:$debug $object_type ($$sql{sql}) (".join(',', map { ref $_ eq 'ARRAY' ? join(',', @{$_}) : $_ } @{$$sql{values}}).') # of results:' . @$data . ' in ' . sprintf('%.4f', tv_interval($starttime)*1000) .' useconds' );
} # end if
my $fields = \%{$object_type.'::fields'};
my $primary_key = ${$object_type.'::primary_key'};
if ($primary_key) {
if ( ! ($fields and keys %{$fields}) ) {
return map { new($object_type, $$_{$primary_key}, $_ ) } @$data;
} elsif ( $$fields{$primary_key} ) {
return map { new($object_type, $_->{$$fields{$primary_key}}, $_) } @$data;
}
} else {
my @identified_by = eval '@'.$object_type.'::identified_by';
if (@identified_by) {
return map { new($object_type, \@identified_by, $_, !$do_cache) } @$data;
} else {
my ( $caller, undef, $line ) = caller;
Error('NO primary_key for type ' . $object_type . ' called from '.$caller.$line);
return;
} # end if
} # end if
} # end sub find
sub find_one {
my $object_type = shift;
my $params;
if ( @_ == 1 ) {
$params = $_[0];
} else {
%{$params} = @_;
} # end if
$$params{limit}=1;
my @Results = $object_type->find(%$params);
my ( $caller, undef, $line ) = caller;
$log->debug("returning to $caller:$line from find_one") if DEBUG_ALL;
return $Results[0] if @Results;
} # end sub find_one
sub find_sql {
no strict 'refs';
my $object_type = shift;
my $debug = ${$object_type.'::debug'};
$debug = DEBUG_ALL if ! $debug;
my $params;
if ( @_ == 1 ) {
$params = $_[0];
if ( ref $params ne 'HASH' ) {
$log->error("params $params was not a has");
} # end if
} else {
$params = { @_ };
} # end if
my %sql = (
( distinct => ( exists $$params{distinct} ? 1:0 ) ),
( columns => ( exists $$params{columns} ? $$params{columns} : '*' ) ),
( table => ( exists $$params{table} ? $$params{table} : ${$object_type.'::table'} )),
'group by'=> $$params{'group by'},
limit => $$params{limit},
offset => $$params{offset},
);
if ( exists $$params{order} ) {
$sql{order} = $$params{order};
} else {
my $order = eval '$'.$object_type.'::default_sort';
#$log->debug("default sort: $object_type :: default_sort = $order") if DEBUG_ALL;
$sql{order} = $order if $order;
} # end if
delete @$params{'distinct','columns','table','group by','limit','offset','order'};
my @where;
my @values;
if ( exists $$params{custom} ) {
push @where, '(' . (shift @{$$params{custom}}) . ')';
push @values, @{$$params{custom}};
delete $$params{custom};
} # end if
my @param_keys = keys %$params;
# no operators, just which fields are being searched on. Mostly just useful for detection of the deleted field.
my %used_fields;
# We use this search hash so that we can mash it up and leave the params hash alone
my %search;
@search{@param_keys} = @$params{@param_keys};
my ( $where, $values, $used_fields ) = get_fields_values( $object_type, \%search, \@param_keys );
delete @search{@{$used_fields}};
@used_fields{ @{$used_fields} } = @{$used_fields};
push @where, @{$where};
push @values, @{$values};
my $fields = \%{$object_type.'::fields'};
#optimise this
if ( $$fields{deleted} and ! $used_fields{deleted} ) {
push @where, 'deleted=?';
push @values, 0;
} # end if
$sql{where} = \@where;
$sql{values} = \@values;
$sql{used_fields} = \%used_fields;
foreach my $k ( keys %search ) {
$log->error("Extra parameters in $object_type ::find $k => $search{$k}");
Carp::cluck("Extra parameters in $object_type ::find $k => $search{$k}");
} # end foreach
$sql{sql} = join( ' ',
( 'SELECT', ( $sql{distinct} ? ('DISTINCT') : () ) ),
( $sql{columns}, 'FROM', $sql{table} ),
( @{$sql{where}} ? ('WHERE', join(' AND ', @{$sql{where}})) : () ),
( $sql{order} ? ( 'ORDER BY', $sql{order} ) : () ),
( $sql{'group by'} ? ( 'GROUP BY', $sql{'group by'} ) : () ),
( $sql{limit} ? ( 'LIMIT', $sql{limit}) : () ),
( $sql{offset} ? ( 'OFFSET', $sql{offset} ) : () ),
);
#$log->debug("Loading Debug:$debug $object_type ($sql) (".join(',', map { ref $_ eq 'ARRAY' ? join(',', @{$_}) : $_ } @values).')' ) if $debug;
return \%sql;
} # end sub find_sql
sub array_diff(\@\@) {
my %e = map { $_ => undef } @{$_[1]};
return @{[ ( grep { (exists $e{$_}) ? ( delete $e{$_} ) : ( 1 ) } @{ $_[0] } ), keys %e ] };
}
sub changes {
my ( $self, $params ) = @_;
my $type = ref $self;
if ( ! $type ) {
my ( $caller, undef, $line ) = caller;
$log->error("No type in Object::changes. self:$self from $caller:$line");
}
my $fields = eval ('\%'.$type.'::fields');
if (!$fields) {
$log->warn('Object::changes called on an object with no fields');
return;
} # end if
my @results;
foreach my $field (sort keys %$fields) {
next if ! exists $$params{$field};
if ( ref $$self{$field} eq 'ARRAY' ) {
my @second = ref $$params{$field} eq 'ARRAY' ? @{$$params{$field}} : ($$params{$field});
if (array_diff(@{$$self{$field}}, @second)) {
push @results, [ $field, $$self{$field}, $$params{$field} ];
}
} elsif (
(!defined($$self{$field}) and defined($$params{$field}))
or
(defined($$self{$field}) and !defined($$params{$field}))
) {
push @results, $field;
} elsif ( defined($$self{$field}) and defined($$params{$field}) and ($$self{$field} ne $$params{$field}) ) {
push @results, $field;
}
}
return @results;
}
sub clone {
my $new = {};
bless $new, ref $_[0];
@$new{keys %{$_[0]}} = values %{$_[0]};
return $new;
} # end sub clone
sub Object_Type {
if ( $_[0]{object_type_id} ) {
$_[0]{Object_Type} = new ZoneMinder::Object_Type( $_[0]{object_type_id} );
} else {
$_[0]{Object_Type} = ZoneMinder::Object_Type->find_one( name=>ref $_[0] );
$_[0]{Object_Type} = new ZoneMinder::Object_Type() if ! $_[0]{Object_Type};
} # end if
return $_[0]{Object_Type};
} # end sub Object_Type
sub object_type {
if ( @_ > 1 ) {
my $Type = ZoneMinder::Object_Type->find_one( name => $_[1] );
if ( ! $Type ) {
$Type = new ZoneMinder::Object_Type();
$Type->save({ Name=>$_[1], Human=>$_[1] });
} # end if
$_[0]{object_type} = $Type->Name();
$_[0]{object_type_id} = $Type->Id();
} # end if
if ( ! $_[0]{object_type} ) {
$_[0]{object_type} = new ZoneMinder::Object_Type( $_[0]{object_type_id} )->Name();
} # end if
return $_[0]{object_type};
} # end sub object_type
sub Object {
my $self = shift;
if ( @_ ) {
$self->object_type( ref $_[0] );
$$self{object_id} = $_[0]{id};
$$self{Object} = $_[0];
} # end if
my $type = $self->object_type();
if ( !$type ) {
Error('No type in Object::Object'. $self->to_string()) if ref $self ne 'ZoneMinder::Log';
return undef;
} # end if
my ( $module ) = $type =~ /ZoneMinder::(.*)/;
if ( $module ) {
eval {
require "ZoneMinder/$module.pm";
};
if ( ! $$self{Object} ) {
$_ = $type->new($$self{object_id});
Debug( 'Returning object of type ' . ref $_ ) if $debug;
$$self{Object} = $_;
}
return $$self{Object};
} else {
Error("Unvalid object $type");
return new ZoneMinder::Object();
}
} # end sub Object
sub AUTOLOAD {
my $type = ref($_[0]);
Carp::cluck("No type in autoload") if ! $type;
if ( DEBUG_ALL ) {
Carp::cluck("Using AUTOLOAD $AUTOLOAD");
}
my $name = $AUTOLOAD;
$name =~ s/.*://;
if ( @_ > 1 ) {
return $_[0]{$name} = $_[1];
}
return $_[0]{$name};
}
sub DESTROY {
}
1;
__END__
# Below is stub documentation for your module. You'd better edit it!
=head1 NAME
ZoneMinder::Object
=head1 SYNOPSIS
use parent ZoneMinder::Object;
This package should likely not be used directly, as it is meant mainly to be a parent for all other ZoneMinder classes.
=head1 DESCRIPTION
A base Object to act as parent for other ZoneMinder Objects.
=head2 EXPORT
None by default.
=head1 AUTHOR
Isaac Connor, E<lt>isaac@zoneminder.comE<gt>
=head1 COPYRIGHT AND LICENSE
Copyright (C) 2001-2017 ZoneMinder LLC
Licensed under the GNU General Public License v2 or later; see the COPYING
file distributed with ZoneMinder for the full text.
=cut