diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 000000000..4a55614da --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,83 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +name: "CodeQL" + +on: + push: + branches: [master] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + schedule: + - cron: '0 3 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: ['cpp', 'javascript'] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + - name: Clean install dependencies and build + run: | + git submodule init + git submodule update --init --recursive + sudo apt-get update + sudo apt-get install libx264-dev libmp4v2-dev libavdevice-dev libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev + sudo apt-get install libbz2-dev libgcrypt20-dev libcurl4-gnutls-dev libjpeg-turbo8-dev libturbojpeg0-dev + sudo apt-get install default-libmysqlclient-dev libpcre3-dev libpolkit-gobject-1-dev libv4l-dev libvlc-dev + sudo apt-get install libdate-manip-perl libdbd-mysql-perl libphp-serialization-perl libsys-mmap-perl + sudo apt-get install libwww-perl libdata-uuid-perl libssl-dev libcrypt-eksblowfish-perl libdata-entropy-perl + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl- + + + # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 98457b7c3..310d194d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -806,7 +806,7 @@ find_package( Getopt::Long Time::HiRes Date::Manip LWP::UserAgent ExtUtils::MakeMaker ${ZM_MMAP_PERLPACKAGE}) if(NOT PERLMODULES_FOUND) - message(FATAL_ERROR + message(WARNING "Not all required perl modules were found on your system") endif(NOT PERLMODULES_FOUND) @@ -842,7 +842,7 @@ if(WITH_SYSTEMD) # Check for polkit find_package(Polkit) if(NOT POLKIT_FOUND) - message(FATAL_ERROR + message(WARNING "Running ZoneMinder requires polkit. Building ZoneMinder requires the polkit development package.") endif(NOT POLKIT_FOUND) endif(WITH_SYSTEMD) diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index cd4622df1..71d07ad07 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -285,6 +285,7 @@ CREATE TABLE `Filters` ( `UserId` int(10) unsigned, `Query_json` text NOT NULL, `AutoArchive` tinyint(3) unsigned NOT NULL default '0', + `AutoUnarchive` tinyint(3) unsigned NOT NULL default '0', `AutoVideo` tinyint(3) unsigned NOT NULL default '0', `AutoUpload` tinyint(3) unsigned NOT NULL default '0', `AutoEmail` tinyint(3) unsigned NOT NULL default '0', @@ -551,7 +552,7 @@ CREATE TABLE `Monitor_Status` ( `AnalysisFPS` DECIMAL(5,2) NOT NULL default 0, `CaptureBandwidth` INT NOT NULL default 0, PRIMARY KEY (`MonitorId`) -) ENGINE=MEMORY; +) ENGINE=@ZM_MYSQL_ENGINE@; -- -- Table structure for table `States` -- PP - Added IsActive to track custom run states @@ -1024,6 +1025,13 @@ INSERT INTO MontageLayouts (`Name`,`Positions`) VALUES ('3 Wide', '{ "default":{ INSERT INTO MontageLayouts (`Name`,`Positions`) VALUES ('4 Wide', '{ "default":{"float":"left", "width":"24.5%","left":"0px","right":"0px","top":"0px","bottom":"0px"} }' ); INSERT INTO MontageLayouts (`Name`,`Positions`) VALUES ('5 Wide', '{ "default":{"float":"left", "width":"19%","left":"0px","right":"0px","top":"0px","bottom":"0px"} }' ); +CREATE TABLE Sessions ( + id char(32) not null, + access INT(10) UNSIGNED DEFAULT NULL, + data text, + PRIMARY KEY(id) +) ENGINE=InnoDB; + -- We generally don't alter triggers, we drop and re-create them, so let's keep them in a separate file that we can just source in update scripts. source @PKGDATADIR@/db/triggers.sql -- diff --git a/db/zm_update-1.35.10.sql b/db/zm_update-1.35.10.sql new file mode 100644 index 000000000..81c768b57 --- /dev/null +++ b/db/zm_update-1.35.10.sql @@ -0,0 +1,15 @@ +-- +-- Add AutoUnarchive action to Filters +-- + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Filters' + AND column_name = 'AutoUnarchive' + ) > 0, +"SELECT 'Column AutoUunarchive already exists in Filters'", +"ALTER TABLE Filters ADD `AutoUnarchive` tinyint(3) unsigned NOT NULL default '0' AFTER `AutoArchive`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; diff --git a/db/zm_update-1.35.8.sql b/db/zm_update-1.35.8.sql new file mode 100644 index 000000000..1549903ab --- /dev/null +++ b/db/zm_update-1.35.8.sql @@ -0,0 +1,12 @@ +/* The MEMORY TABLE TYPE IS BAD! Switch to regular InnoDB */ + +DROP TABLE IF EXISTS `Monitor_Status`; +CREATE TABLE `Monitor_Status` ( + `MonitorId` int(10) unsigned NOT NULL, + `Status` enum('Unknown','NotRunning','Running','Connected','Signal') NOT NULL default 'Unknown', + `CaptureFPS` DECIMAL(10,2) NOT NULL default 0, + `AnalysisFPS` DECIMAL(5,2) NOT NULL default 0, + `CaptureBandwidth` INT NOT NULL default 0, + PRIMARY KEY (`MonitorId`) +) ENGINE=InnoDB; + diff --git a/db/zm_update-1.35.9.sql b/db/zm_update-1.35.9.sql new file mode 100644 index 000000000..b5f807225 --- /dev/null +++ b/db/zm_update-1.35.9.sql @@ -0,0 +1,21 @@ +-- +-- This adds Sessions Table +-- + +SET @s = (SELECT IF( + (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.TABLES + WHERE table_name = 'Sessions' + AND table_schema = DATABASE() + ) > 0, + "SELECT 'Sessions table exists'", + "CREATE TABLE Sessions ( + id char(32) not null, + access INT(10) UNSIGNED DEFAULT NULL, + data text, + PRIMARY KEY(id) +) ENGINE=InnoDB;" + )); + +PREPARE stmt FROM @s; +EXECUTE stmt; diff --git a/distros/redhat/zoneminder.spec b/distros/redhat/zoneminder.spec index b594c2de2..5c19dd0c0 100644 --- a/distros/redhat/zoneminder.spec +++ b/distros/redhat/zoneminder.spec @@ -28,7 +28,7 @@ %global _hardened_build 1 Name: zoneminder -Version: 1.35.7 +Version: 1.35.10 Release: 1%{?dist} Summary: A camera monitoring and analysis tool Group: System Environment/Daemons diff --git a/distros/ubuntu2004/control b/distros/ubuntu2004/control index 0a1f9e0de..f0cf23a27 100644 --- a/distros/ubuntu2004/control +++ b/distros/ubuntu2004/control @@ -17,6 +17,7 @@ Build-Depends: debhelper, dh-systemd, sphinx-doc, python3-sphinx, dh-linktree, d ,libbz2-dev ,libgcrypt20-dev ,libcurl4-gnutls-dev + ,libjpeg-turbo8-dev | libjpeg62-turbo-dev | libjpeg8-dev | libjpeg9-dev ,libturbojpeg0-dev ,default-libmysqlclient-dev | libmysqlclient-dev | libmariadbclient-dev-compat ,libpcre3-dev @@ -32,6 +33,7 @@ Build-Depends: debhelper, dh-systemd, sphinx-doc, python3-sphinx, dh-linktree, d ,libssl-dev ,libcrypt-eksblowfish-perl ,libdata-entropy-perl + ,libvncserver-dev # Unbundled (dh_linktree): ,libjs-jquery ,libjs-mootools @@ -80,6 +82,7 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} ,libpcre3 ,libcrypt-eksblowfish-perl ,libdata-entropy-perl + ,libvncclient1|libvncclient0 Recommends: ${misc:Recommends} ,libapache2-mod-php | php-fpm ,mysql-server | mariadb-server | virtual-mysql-server diff --git a/distros/ubuntu2004/zoneminder.postinst b/distros/ubuntu2004/zoneminder.postinst index 39a2fa1b0..a8b8eaf51 100644 --- a/distros/ubuntu2004/zoneminder.postinst +++ b/distros/ubuntu2004/zoneminder.postinst @@ -16,17 +16,20 @@ create_db () { else echo "Db exists." fi +} + +create_update_user () { USER_EXISTS="$(mysql --defaults-file=/etc/mysql/debian.cnf -sse "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '$ZM_DB_USER')")" if [ $USER_EXISTS -ne 1 ]; then echo "Creating zm user $ZM_DB_USER" # This creates the user. echo "CREATE USER '${ZM_DB_USER}'@${ZM_DB_HOST} IDENTIFIED BY '${ZM_DB_PASS}';" | mysql --defaults-file=/etc/mysql/debian.cnf mysql fi + echo "Updating permissions" + echo "GRANT LOCK tables,alter,drop,select,insert,update,delete,create,index,alter routine,create routine,trigger,execute ON ${ZM_DB_NAME}.* TO '${ZM_DB_USER}'@${ZM_DB_HOST};" | mysql --defaults-file=/etc/mysql/debian.cnf mysql } update_db () { - echo "Updating permissions" - echo "GRANT LOCK tables,alter,drop,select,insert,update,delete,create,index,alter routine,create routine,trigger,execute ON ${ZM_DB_NAME}.* TO '${ZM_DB_USER}'@${ZM_DB_HOST};" | mysql --defaults-file=/etc/mysql/debian.cnf mysql zmupdate.pl --nointeractive zmupdate.pl --nointeractive -f @@ -94,6 +97,7 @@ if [ "$1" = "configure" ]; then # Make sure systemctl status exit code is 0; i.e. the DB is running if systemctl is-active --quiet "$DBSERVICE"; then create_db + create_update_user update_db else echo 'NOTE: MySQL/MariaDB not running; please start mysql and run dpkg-reconfigure zoneminder when it is running.' @@ -108,6 +112,7 @@ if [ "$1" = "configure" ]; then fi if $(/etc/init.d/mysql status >/dev/null 2>&1); then create_db + create_update_user update_db else echo 'NOTE: MySQL/MariaDB not running; please start mysql and run dpkg-reconfigure zoneminder when it is running.' diff --git a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in index 43fbcb457..98ed7fa69 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in +++ b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in @@ -2648,7 +2648,7 @@ our @options = ( }, { name => 'ZM_WEB_EVENT_SORT_FIELD', - default => 'StartDateTime', + default => 'StartTime', description => 'Default field the event lists are sorted by', help => q` Events in lists can be initially ordered in any way you want. @@ -2660,7 +2660,7 @@ our @options = ( `, type => { db_type =>'string', - hint =>'Id|Name|Cause|DiskSpace|MonitorName|StartDateTime|Length|Frames|AlarmFrames|TotScore|AvgScore|MaxScore', + hint =>'Id|Name|Cause|DiskSpace|MonitorName|StartTime|Length|Frames|AlarmFrames|TotScore|AvgScore|MaxScore', pattern =>qr|.|, format =>q( $1 ) }, diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm b/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm index b48946d1f..9bb13132c 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm @@ -58,6 +58,7 @@ Id Name Query_json AutoArchive +AutoUnarchive AutoVideo AutoUpload AutoEmail @@ -201,131 +202,130 @@ sub Sql { $self->{Sql} .= 'extract( hour_second from E.EndTime )'; } elsif ( $term->{attr} eq 'EndWeekday' ) { $self->{Sql} .= "weekday( E.EndTime )"; - -# } elsif ( $term->{attr} eq 'ExistsInFileSystem' ) { push @{$self->{PostSQLConditions}}, $term; - } elsif ( $term->{attr} eq 'DiskSpace' ) { - $self->{Sql} .= 'E.DiskSpace'; + $self->{Sql} .= 'TRUE /* ExistsInFileSystem */'; } elsif ( $term->{attr} eq 'DiskPercent' ) { $self->{Sql} .= 'zmDiskPercent'; $self->{HasDiskPercent} = !undef; - $self->{HasPreCondition} = !undef; } elsif ( $term->{attr} eq 'DiskBlocks' ) { $self->{Sql} .= 'zmDiskBlocks'; $self->{HasDiskBlocks} = !undef; - $self->{HasPreCondition} = !undef; } elsif ( $term->{attr} eq 'SystemLoad' ) { $self->{Sql} .= 'zmSystemLoad'; $self->{HasSystemLoad} = !undef; - $self->{HasPreCondition} = !undef; } else { $self->{Sql} .= 'E.'.$term->{attr}; } - ( my $stripped_value = $value ) =~ s/^["\']+?(.+)["\']+?$/$1/; - foreach my $temp_value ( split( /["'\s]*?,["'\s]*?/, $stripped_value ) ) { - - if ( $term->{attr} eq 'AlarmedZoneId' ) { - $value = '(SELECT * FROM Stats WHERE EventId=E.Id AND ZoneId='.$value.')'; - } elsif ( $term->{attr} =~ /^MonitorName/ ) { - $value = "'$temp_value'"; - } elsif ( $term->{attr} =~ /ServerId/) { - Debug("ServerId, temp_value is ($temp_value) ($ZoneMinder::Config::Config{ZM_SERVER_ID})"); - if ( $temp_value eq 'ZM_SERVER_ID' ) { - $value = "'$ZoneMinder::Config::Config{ZM_SERVER_ID}'"; - # This gets used later, I forget for what - $$self{Server} = new ZoneMinder::Server($ZoneMinder::Config::Config{ZM_SERVER_ID}); - } elsif ( $temp_value eq 'NULL' ) { - $value = $temp_value; - } else { + if ( $term->{attr} eq 'ExistsInFileSystem' ) { + # PostCondition, so no further SQL + } else { + ( my $stripped_value = $value ) =~ s/^["\']+?(.+)["\']+?$/$1/; + foreach my $temp_value ( split( /["'\s]*?,["'\s]*?/, $stripped_value ) ) { + + if ( $term->{attr} eq 'AlarmedZoneId' ) { + $value = '(SELECT * FROM Stats WHERE EventId=E.Id AND ZoneId='.$value.')'; + } elsif ( $term->{attr} =~ /^MonitorName/ ) { $value = "'$temp_value'"; - # This gets used later, I forget for what - $$self{Server} = new ZoneMinder::Server($temp_value); - } - } elsif ( $term->{attr} eq 'StorageId' ) { - $value = "'$temp_value'"; - $$self{Storage} = new ZoneMinder::Storage($temp_value); - } elsif ( $term->{attr} eq 'Name' + } elsif ( $term->{attr} =~ /ServerId/) { + Debug("ServerId, temp_value is ($temp_value) ($ZoneMinder::Config::Config{ZM_SERVER_ID})"); + if ( $temp_value eq 'ZM_SERVER_ID' ) { + $value = "'$ZoneMinder::Config::Config{ZM_SERVER_ID}'"; + # This gets used later, I forget for what + $$self{Server} = new ZoneMinder::Server($ZoneMinder::Config::Config{ZM_SERVER_ID}); + } elsif ( $temp_value eq 'NULL' ) { + $value = $temp_value; + } else { + $value = "'$temp_value'"; + # This gets used later, I forget for what + $$self{Server} = new ZoneMinder::Server($temp_value); + } + } elsif ( $term->{attr} eq 'StorageId' ) { + $value = "'$temp_value'"; + $$self{Storage} = new ZoneMinder::Storage($temp_value); + } elsif ( $term->{attr} eq 'Name' || $term->{attr} eq 'Cause' || $term->{attr} eq 'Notes' - ) { + ) { if ( $term->{op} eq 'LIKE' - || $term->{op} eq 'NOT LIKE' + || $term->{op} eq 'NOT LIKE' ) { - $temp_value = '%'.$temp_value.'%' if $temp_value !~ /%/; + $temp_value = '%'.$temp_value.'%' if $temp_value !~ /%/; + } + $value = "'$temp_value'"; + } elsif ( $term->{attr} eq 'DateTime' or $term->{attr} eq 'StartDateTime' or $term->{attr} eq 'EndDateTime' ) { + if ( $temp_value eq 'NULL' ) { + $value = $temp_value; + } else { + $value = DateTimeToSQL($temp_value); + if ( !$value ) { + Error("Error parsing date/time '$temp_value', skipping filter '$self->{Name}'"); + return; + } + $value = "'$value'"; + } + } elsif ( $term->{attr} eq 'Date' or $term->{attr} eq 'StartDate' or $term->{attr} eq 'EndDate' ) { + if ( $temp_value eq 'NULL' ) { + $value = $temp_value; + } elsif ( $temp_value eq 'CURDATE()' or $temp_value eq 'NOW()' ) { + $value = 'to_days('.$temp_value.')'; + } else { + $value = DateTimeToSQL($temp_value); + if ( !$value ) { + Error("Error parsing date/time '$temp_value', skipping filter '$self->{Name}'"); + return; + } + $value = "to_days( '$value' )"; + } + } elsif ( $term->{attr} eq 'Time' or $term->{attr} eq 'StartTime' or $term->{attr} eq 'EndTime' ) { + if ( $temp_value eq 'NULL' ) { + $value = $temp_value; + } else { + $value = DateTimeToSQL($temp_value); + if ( !$value ) { + Error("Error parsing date/time '$temp_value', skipping filter '$self->{Name}'"); + return; + } + $value = "extract( hour_second from '$value' )"; } - $value = "'$temp_value'"; - } elsif ( $term->{attr} eq 'DateTime' or $term->{attr} eq 'StartDateTime' or $term->{attr} eq 'EndDateTime' ) { - if ( $temp_value eq 'NULL' ) { - $value = $temp_value; } else { - $value = DateTimeToSQL($temp_value); - if ( !$value ) { - Error("Error parsing date/time '$temp_value', skipping filter '$self->{Name}'"); - return; - } - $value = "'$value'"; - } - } elsif ( $term->{attr} eq 'Date' or $term->{attr} eq 'StartDate' or $term->{attr} eq 'EndDate' ) { - if ( $temp_value eq 'NULL' ) { $value = $temp_value; - } elsif ( $temp_value eq 'CURDATE()' or $temp_value eq 'NOW()' ) { - $value = 'to_days('.$temp_value.')'; - } else { - $value = DateTimeToSQL($temp_value); - if ( !$value ) { - Error("Error parsing date/time '$temp_value', skipping filter '$self->{Name}'"); - return; - } - $value = "to_days( '$value' )"; } - } elsif ( $term->{attr} eq 'Time' or $term->{attr} eq 'StartTime' or $term->{attr} eq 'EndTime' ) { - if ( $temp_value eq 'NULL' ) { - $value = $temp_value; + push @value_list, $value; + } # end foreach temp_value + } # end if has an attr + + if ( $term->{op} ) { + if ( $term->{op} eq '=~' ) { + $self->{Sql} .= ' REGEXP '.$value; + } elsif ( $term->{op} eq '!~' ) { + $self->{Sql} .= ' NOT REGEXP '.$value; + } elsif ( $term->{op} eq 'IS' ) { + if ( $value eq 'Odd' ) { + $self->{Sql} .= ' % 2 = 1'; + } elsif ( $value eq 'Even' ) { + $self->{Sql} .= ' % 2 = 0'; } else { - $value = DateTimeToSQL($temp_value); - if ( !$value ) { - Error("Error parsing date/time '$temp_value', skipping filter '$self->{Name}'"); - return; - } - $value = "extract( hour_second from '$value' )"; + $self->{Sql} .= " IS $value"; } + } elsif ( $term->{op} eq 'EXISTS' ) { + $self->{Sql} .= ' EXISTS '.$value; + } elsif ( $term->{op} eq 'IS NOT' ) { + $self->{Sql} .= ' IS NOT '.$value; + } elsif ( $term->{op} eq '=[]' or $term->{op} eq 'IN' ) { + $self->{Sql} .= ' IN ('.join(',', @value_list).')'; + } elsif ( $term->{op} eq '![]' ) { + $self->{Sql} .= ' NOT IN ('.join(',', @value_list).')'; + } elsif ( $term->{op} eq 'LIKE' ) { + $self->{Sql} .= ' LIKE '.$value; + } elsif ( $term->{op} eq 'NOT LIKE' ) { + $self->{Sql} .= ' NOT LIKE '.$value; } else { - $value = $temp_value; + $self->{Sql} .= ' '.$term->{op}.' '.$value; } - push @value_list, $value; - } # end foreach temp_value - } # end if has an attr - if ( $term->{op} ) { - if ( $term->{op} eq '=~' ) { - $self->{Sql} .= " regexp $value"; - } elsif ( $term->{op} eq '!~' ) { - $self->{Sql} .= " not regexp $value"; - } elsif ( $term->{op} eq 'IS' ) { - if ( $value eq 'Odd' ) { - $self->{Sql} .= ' % 2 = 1'; - } elsif ( $value eq 'Even' ) { - $self->{Sql} .= ' % 2 = 0'; - } else { - $self->{Sql} .= " IS $value"; - } - } elsif ( $term->{op} eq 'EXISTS' ) { - $self->{Sql} .= " EXISTS $value"; - } elsif ( $term->{op} eq 'IS NOT' ) { - $self->{Sql} .= " IS NOT $value"; - } elsif ( $term->{op} eq '=[]' ) { - $self->{Sql} .= ' IN ('.join(',', @value_list).')'; - } elsif ( $term->{op} eq '!~' ) { - $self->{Sql} .= ' NOT IN ('.join(',', @value_list).')'; - } elsif ( $term->{op} eq 'LIKE' ) { - $self->{Sql} .= " LIKE $value"; - } elsif ( $term->{op} eq 'NOT LIKE' ) { - $self->{Sql} .= " NOT LIKE $value"; - } else { - $self->{Sql} .= ' '.$term->{op}.' '.$value; - } - } # end if has an operator + } # end if has an operator + } # end if Pre/Post or SQL if ( exists($term->{cbr}) ) { $self->{Sql} .= ' '.str_repeat(')', $term->{cbr}).' '; } @@ -341,6 +341,9 @@ sub Sql { if ( $self->{AutoArchive} ) { push @auto_terms, 'E.Archived = 0'; } + if ( $self->{AutoUnarchive} ) { + push @auto_terms, 'E.Archived = 1'; + } # Don't do this, it prevents re-generation and concatenation. # If the file already exists, then the video won't be re-recreated if ( $self->{AutoVideo} ) { @@ -394,7 +397,7 @@ sub Sql { $sort_column = 'E.StartTime'; } my $sort_order = $filter_expr->{sort_asc} ? 'ASC' : 'DESC'; - $sql .= ' ORDER BY '.$sort_column." ".$sort_order; + $sql .= ' ORDER BY '.$sort_column.' '.$sort_order; if ( $filter_expr->{limit} ) { $sql .= ' LIMIT 0,'.$filter_expr->{limit}; } diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm b/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm index 963f75638..55e46e114 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm @@ -121,6 +121,24 @@ $serial = $primary_key = 'Id'; WebColour Exif Sequence + TotalEvents + TotalEventDiskSpace + HourEvents + HourEventDiskSpace + DayEvents + DayEventDiskSpace + WeekEvents + WeekEventDiskSpace + MonthEvents + MonthEventDiskSpace + ArchivedEvents + ArchivedEventDiskSpace + ZoneCount + Refresh + DefaultCodec + GroupIds + Latitude + Longitude ); %defaults = ( @@ -201,6 +219,23 @@ $serial = $primary_key = 'Id'; WebColour => '#ff0000', Exif => 0, Sequence => undef, + TotalEvents => undef, + TotalEventDiskSpace => undef, + HourEvents => undef, + HourEventDiskSpace => undef, + DayEvents => undef, + DayEventDiskSpace => undef, + WeekEvents => undef, + WeekEventDiskSpace => undef, + MonthEvents => undef, + MonthEventDiskSpace => undef, + ArchivedEvents => undef, + ArchivedEventDiskSpace => undef, + ZoneCount => 0, + Refresh => undef, + DefaultCodec => 'auto', + Latitude => undef, + Longitude => undef, ); sub Server { @@ -211,6 +246,30 @@ sub Storage { return new ZoneMinder::Storage( $_[0]{StorageId} ); } # end sub Storage +sub control { + my $monitor = shift; + my $command = shift; + my $process = shift; + + if ( $command eq 'stop' or $command eq 'restart' ) { + if ( $process ) { + `/usr/bin/zmdc.pl stop $process -m $$monitor{Id}`; + } else { + `/usr/bin/zmdc.pl stop zma -m $$monitor{Id}`; + `/usr/bin/zmdc.pl stop zmc -m $$monitor{Id}`; + } + } + if ( $command eq 'start' or $command eq 'restart' ) { + if ( $process ) { + `/usr/bin/zmdc.pl start $process -m $$monitor{Id}`; + } else { + `/usr/bin/zmdc.pl start zmc -m $$monitor{Id}`; + `/usr/bin/zmdc.pl start zma -m $$monitor{Id}`; + } # end if + } +} # end sub control + + 1; __END__ diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Zone.pm b/scripts/ZoneMinder/lib/ZoneMinder/Zone.pm new file mode 100644 index 000000000..986c5a4dc --- /dev/null +++ b/scripts/ZoneMinder/lib/ZoneMinder/Zone.pm @@ -0,0 +1,105 @@ +# ========================================================================== +# +# ZoneMinder Zone Module +# Copyright (C) 2020 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. +# +# ========================================================================== + +package ZoneMinder::Zone; + +use 5.006; +use strict; +use warnings; + +require ZoneMinder::Base; +require ZoneMinder::Object; + +#our @ISA = qw(Exporter ZoneMinder::Base); +use parent qw(ZoneMinder::Object); + +use vars qw/ $table $primary_key %fields $serial %defaults $debug/; +$table = 'Zones'; +$serial = $primary_key = 'Id'; +%fields = map { $_ => $_ } qw( + Id + Name + MonitorId + Type + Units + CheckMethod + MinPixelThreshold + MaxPixelThreshold + MinAlarmPixels + MaxAlarmPixels + FilterX + FilterY + MinFilterPixels + MaxFilterPixels + MinBlobPixels + MaxBlobPixels + MinBlobs + MaxBlobs + OverloadFrames + ExtendAlarmFrames + ); + +%defaults = ( + Name => '', + Type => 'Active', + Units => 'Pixels', + CheckMethod => 'Blobs', + MinPixelThreshold => undef, + MaxPixelThreshold => undef, + MinAlarmPixels => undef, + MaxAlarmPixels => undef, + FilterX => undef, + FilterY => undef, + MinFilterPixels => undef, + MaxFilterPixels => undef, + MinBlobPixels => undef, + MaxBlobPixels => undef, + MinBlobs => undef, + MaxBlobs => undef, + OverloadFrames => 0, + ExtendAlarmFrames => 0, +); + +1; +__END__ + +=head1 NAME + +ZoneMinder::Zone - Perl Class for Zones + +=head1 SYNOPSIS + +use ZoneMinder::Zone; + +=head1 AUTHOR + +Isaac Connor, Eisaac@zoneminder.comE + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2001-2017 ZoneMinder LLC + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself, either Perl version 5.8.3 or, +at your option, any later version of Perl 5 you may have available. + + +=cut diff --git a/scripts/zmfilter.pl.in b/scripts/zmfilter.pl.in index 5ff316078..9a93c8953 100644 --- a/scripts/zmfilter.pl.in +++ b/scripts/zmfilter.pl.in @@ -98,6 +98,7 @@ use constant EVENT_PATH => ($Config{ZM_DIR_EVENTS}=~m|/|) ; logInit($filter_id?(id=>'zmfilter_'.$filter_id):()); + sub HupHandler { # This idea at this time is to just exit, freeing up the memory. # zmfilter.pl will be respawned by zmdc. @@ -236,6 +237,7 @@ sub getFilters { $sql .= ' `Background` = 1 AND'; } $sql .= '( `AutoArchive` = 1 + or `AutoUnarchive` = 1 or `AutoVideo` = 1 or `AutoUpload` = 1 or `AutoEmail` = 1 @@ -282,6 +284,7 @@ sub checkFilter { join(', ', ($filter->{AutoDelete}?'delete':()), ($filter->{AutoArchive}?'archive':()), + ($filter->{AutoUnarchive}?'unarchive':()), ($filter->{AutoVideo}?'video':()), ($filter->{AutoUpload}?'upload':()), ($filter->{AutoEmail}?'email':()), @@ -299,7 +302,7 @@ sub checkFilter { last if $zm_terminate; my $Event = new ZoneMinder::Event($$event{Id}, $event); - Debug("Checking event $Event->{Id}"); + Debug('Checking event '.$Event->{Id}); my $delete_ok = !undef; $dbh->ping(); if ( $filter->{AutoArchive} ) { @@ -311,6 +314,15 @@ sub checkFilter { my $res = $sth->execute($Event->{Id}) or Error("Unable to execute '$sql': ".$dbh->errstr()); } + if ( $filter->{AutoUnarchive} ) { + Info("Unarchiving event $Event->{Id}"); + # Do it individually to avoid locking up the table for new events + my $sql = 'UPDATE `Events` SET `Archived` = 0 WHERE `Id` = ?'; + my $sth = $dbh->prepare_cached($sql) + or Fatal("Unable to prepare '$sql': ".$dbh->errstr()); + my $res = $sth->execute($Event->{Id}) + or Error("Unable to execute '$sql': ".$dbh->errstr()); + } if ( $Config{ZM_OPT_FFMPEG} && $filter->{AutoVideo} ) { if ( !$Event->{Videoed} ) { $delete_ok = undef if !generateVideo($filter, $Event); diff --git a/src/zm_event.cpp b/src/zm_event.cpp index 6ab53cf54..b201f5c86 100644 --- a/src/zm_event.cpp +++ b/src/zm_event.cpp @@ -39,6 +39,7 @@ //#define USE_PREPARED_SQL 1 const char * Event::frame_type_names[3] = { "Normal", "Bulk", "Alarm" }; +char frame_insert_sql[ZM_SQL_LGE_BUFSIZ] = "INSERT INTO `Frames` (`EventId`, `FrameId`, `Type`, `TimeStamp`, `Delta`, `Score`) VALUES "; int Event::pre_alarm_count = 0; @@ -104,9 +105,11 @@ Event::Event( ); db_mutex.lock(); if ( mysql_query(&dbconn, sql) ) { - Error("Can't insert event: %s. sql was (%s)", mysql_error(&dbconn), sql); db_mutex.unlock(); + Error("Can't insert event: %s. sql was (%s)", mysql_error(&dbconn), sql); return; + } else { + Debug(2, "Created new event with %s", sql); } id = mysql_insert_id(&dbconn); @@ -202,8 +205,8 @@ Event::Event( video_name = stringtf("%" PRIu64 "-%s", id, "video.mp4"); snprintf(sql, sizeof(sql), "UPDATE Events SET DefaultVideo = '%s' WHERE Id=%" PRIu64, video_name.c_str(), id); if ( mysql_query(&dbconn, sql) ) { - Error("Can't update event: %s. sql was (%s)", mysql_error(&dbconn), sql); db_mutex.unlock(); + Error("Can't update event: %s. sql was (%s)", mysql_error(&dbconn), sql); return; } video_file = path + "/" + video_name; @@ -501,8 +504,9 @@ void Event::AddFrames(int n_frames, Image **images, struct timeval **timestamps) } void Event::AddFramesInternal(int n_frames, int start_frame, Image **images, struct timeval **timestamps) { - static char sql[ZM_SQL_LGE_BUFSIZ]; - strncpy(sql, "INSERT INTO `Frames` (`EventId`, `FrameId`, `TimeStamp`, `Delta`) VALUES ", sizeof(sql)); + char *frame_insert_values = (char *)&frame_insert_sql + 90; // 90 == strlen(frame_insert_sql); + //static char sql[ZM_SQL_LGE_BUFSIZ]; + //strncpy(sql, "INSERT INTO `Frames` (`EventId`, `FrameId`, `TimeStamp`, `Delta`) VALUES ", sizeof(sql)); int frameCount = 0; for ( int i = start_frame; i < n_frames && i - start_frame < ZM_SQL_BATCH_SIZE; i++ ) { if ( timestamps[i]->tv_sec <= 0 ) { @@ -541,21 +545,24 @@ void Event::AddFramesInternal(int n_frames, int start_frame, Image **images, str delta_time.sec = 0; } - int sql_len = strlen(sql); - snprintf(sql+sql_len, sizeof(sql)-sql_len, "( %" PRIu64 ", %d, from_unixtime(%ld), %s%ld.%02ld ), ", + frame_insert_values += snprintf(frame_insert_values, + sizeof(frame_insert_sql)-(frame_insert_values-(char *)&frame_insert_sql), + "\n( %" PRIu64 ", %d, 'Normal', from_unixtime(%ld), %s%ld.%02ld, 0 ),", id, frames, timestamps[i]->tv_sec, delta_time.positive?"":"-", delta_time.sec, delta_time.fsec); frameCount++; } // end foreach frame if ( frameCount ) { - Debug(1, "Adding %d/%d frames to DB", frameCount, n_frames); - *(sql+strlen(sql)-2) = '\0'; + *(frame_insert_values-1) = '\0'; db_mutex.lock(); - if ( mysql_query(&dbconn, sql) ) { - Error("Can't insert frames: %s, sql was (%s)", mysql_error(&dbconn), sql); - } + int rc = mysql_query(&dbconn, frame_insert_sql); db_mutex.unlock(); + if ( rc ) { + Error("Can't insert frames: %s, sql was (%s)", mysql_error(&dbconn), frame_insert_sql); + } else { + Debug(1, "INSERT %d/%d frames sql %s", frameCount, n_frames, frame_insert_sql); + } last_db_frame = frames; } else { Debug(1, "No valid pre-capture frames to add"); @@ -563,16 +570,15 @@ void Event::AddFramesInternal(int n_frames, int start_frame, Image **images, str } // void Event::AddFramesInternal(int n_frames, int start_frame, Image **images, struct timeval **timestamps) void Event::WriteDbFrames() { - static char sql[ZM_SQL_LGE_BUFSIZ]; - char * sql_ptr = (char *)&sql; - sql_ptr += snprintf(sql, sizeof(sql), - "INSERT INTO Frames ( EventId, FrameId, Type, TimeStamp, Delta, Score ) VALUES " - ); + char *frame_insert_values_ptr = (char *)&frame_insert_sql + 90; // 90 == strlen(frame_insert_sql); while ( frame_data.size() ) { Frame *frame = frame_data.front(); frame_data.pop(); - sql_ptr += snprintf(sql_ptr, sizeof(sql)-(sql_ptr-(char *)&sql), "( %" PRIu64 ", %d, '%s', from_unixtime( %ld ), %s%ld.%02ld, %d ), ", - id, frame->frame_id, frame_type_names[frame->type], + frame_insert_values_ptr += snprintf(frame_insert_values_ptr, + sizeof(frame_insert_sql)-(frame_insert_values_ptr-(char *)&frame_insert_sql), + "\n( %" PRIu64 ", %d, '%s', from_unixtime( %ld ), %s%ld.%02ld, %d ),", + id, frame->frame_id, + frame_type_names[frame->type], frame->timestamp.tv_sec, frame->delta.positive?"":"-", frame->delta.sec, @@ -580,14 +586,17 @@ void Event::WriteDbFrames() { frame->score); delete frame; } - *(sql_ptr-2) = '\0'; + *(frame_insert_values_ptr-1) = '\0'; // The -2 is for the extra , added for values above db_mutex.lock(); - if ( mysql_query(&dbconn, sql) ) { - db_mutex.unlock(); - Error("Can't insert frames: %s, sql was %s", mysql_error(&dbconn), sql); - return; - } + int rc = mysql_query(&dbconn, frame_insert_sql); db_mutex.unlock(); + + if ( rc ) { + Error("Can't insert frames: %s, sql was %s", mysql_error(&dbconn), frame_insert_sql); + return; + } else { + Debug(1, "INSERT FRAMES: sql was %s", frame_insert_sql); + } } // end void Event::WriteDbFrames() // Subtract an offset time from frames deltas to match with video start time @@ -668,6 +677,8 @@ void Event::AddFrame(Image *image, struct timeval timestamp, int score, Image *a struct DeltaTimeval delta_time; DELTA_TIMEVAL(delta_time, timestamp, start_time, DT_PREC_2); + Debug(1, "Frame delta is %d.%d - %d.%d = %d.%d", + start_time.tv_sec, start_time.tv_usec, timestamp.tv_sec, timestamp.tv_usec, delta_time.sec, delta_time.fsec); bool db_frame = ( frame_type != BULK ) || (frames==1) || ((frames%config.bulk_frame_interval)==0) ; if ( db_frame ) { @@ -675,16 +686,12 @@ void Event::AddFrame(Image *image, struct timeval timestamp, int score, Image *a // The idea is to write out 1/sec frame_data.push(new Frame(id, frames, frame_type, timestamp, delta_time, score)); - if ( write_to_db || ( monitor->get_fps() && (frame_data.size() > monitor->get_fps())) ) { - Debug(1, "Adding %d frames to DB because write_to_db:%d or frames > analysis fps %f", + if ( write_to_db or ( monitor->get_fps() and (frame_data.size() > monitor->get_fps())) or frame_type==BULK ) { + Debug(1, "Adding %d frames to DB because write_to_db:%d or frames > analysis fps %f or BULK", frame_data.size(), write_to_db, monitor->get_fps()); WriteDbFrames(); last_db_frame = frames; - Debug(1, "Adding %d frames to DB, done", frame_data.size()); - } - // We are writing a Bulk frame - if ( frame_type == BULK ) { snprintf(sql, sizeof(sql), "UPDATE Events SET Length = %s%ld.%02ld, Frames = %d, AlarmFrames = %d, TotScore = %d, AvgScore = %d, MaxScore = %d WHERE Id = %" PRIu64, ( delta_time.positive?"":"-" ), diff --git a/src/zm_eventstream.cpp b/src/zm_eventstream.cpp index b9abf5e76..b8de60e46 100644 --- a/src/zm_eventstream.cpp +++ b/src/zm_eventstream.cpp @@ -117,6 +117,7 @@ bool EventStream::loadEventData(uint64_t event_id) { snprintf(sql, sizeof(sql), "SELECT `MonitorId`, `StorageId`, `Frames`, unix_timestamp( `StartTime` ) AS StartTimestamp, " + "unix_timestamp( `EndTime` ) AS EndTimestamp, " "(SELECT max(`Delta`)-min(`Delta`) FROM `Frames` WHERE `EventId`=`Events`.`Id`) AS Duration, " "`DefaultVideo`, `Scheme`, `SaveJPEGs`, `Orientation`+0 FROM `Events` WHERE `Id` = %" PRIu64, event_id); @@ -150,9 +151,10 @@ bool EventStream::loadEventData(uint64_t event_id) { event_data->storage_id = dbrow[1] ? atoi(dbrow[1]) : 0; event_data->frame_count = dbrow[2] == nullptr ? 0 : atoi(dbrow[2]); event_data->start_time = atoi(dbrow[3]); - event_data->duration = dbrow[4] ? atof(dbrow[4]) : 0.0; - strncpy(event_data->video_file, dbrow[5], sizeof(event_data->video_file)-1); - std::string scheme_str = std::string(dbrow[6]); + event_data->end_time = dbrow[4] ? atoi(dbrow[4]) : 0; + event_data->duration = dbrow[5] ? atof(dbrow[5]) : 0.0; + strncpy(event_data->video_file, dbrow[6], sizeof(event_data->video_file)-1); + std::string scheme_str = std::string(dbrow[7]); if ( scheme_str == "Deep" ) { event_data->scheme = Storage::DEEP; } else if ( scheme_str == "Medium" ) { @@ -160,8 +162,8 @@ bool EventStream::loadEventData(uint64_t event_id) { } else { event_data->scheme = Storage::SHALLOW; } - event_data->SaveJPEGs = dbrow[7] == nullptr ? 0 : atoi(dbrow[7]); - event_data->Orientation = (Monitor::Orientation)(dbrow[8] == nullptr ? 0 : atoi(dbrow[8])); + event_data->SaveJPEGs = dbrow[8] == nullptr ? 0 : atoi(dbrow[8]); + event_data->Orientation = (Monitor::Orientation)(dbrow[9] == nullptr ? 0 : atoi(dbrow[9])); mysql_free_result(result); if ( !monitor ) { @@ -223,8 +225,6 @@ bool EventStream::loadEventData(uint64_t event_id) { } updateFrameRate((double)event_data->frame_count/event_data->duration); - Debug(3, "fps set by frame_count(%d)/duration(%f)", - event_data->frame_count, event_data->duration); snprintf(sql, sizeof(sql), "SELECT `FrameId`, unix_timestamp(`TimeStamp`), `Delta` " "FROM `Frames` WHERE `EventId` = %" PRIu64 " ORDER BY `FrameId` ASC", event_id); @@ -284,11 +284,13 @@ bool EventStream::loadEventData(uint64_t event_id) { event_data->frames[id-1].in_db ); } + // Incomplete events might not have any frame data + event_data->last_frame_id = last_id; + if ( mysql_errno(&dbconn) ) { Error("Can't fetch row: %s", mysql_error(&dbconn)); exit(mysql_errno(&dbconn)); } - mysql_free_result(result); if ( event_data->video_file[0] || (monitor->GetOptVideoWriter() > 0) ) { @@ -296,9 +298,10 @@ bool EventStream::loadEventData(uint64_t event_id) { snprintf(event_data->video_file, sizeof(event_data->video_file), "%" PRIu64 "-%s", event_data->event_id, "video.mp4"); } std::string filepath = std::string(event_data->path) + "/" + std::string(event_data->video_file); - //char filepath[PATH_MAX]; - //snprintf(filepath, sizeof(filepath), "%s/%s", event_data->path, event_data->video_file); Debug(1, "Loading video file from %s", filepath.c_str()); + if ( ffmpeg_input ) + delete ffmpeg_input; + ffmpeg_input = new FFmpeg_Input(); if ( 0 > ffmpeg_input->Open(filepath.c_str()) ) { Warning("Unable to open ffmpeg_input %s", filepath.c_str()); @@ -307,14 +310,15 @@ bool EventStream::loadEventData(uint64_t event_id) { } } + // Not sure about this if ( forceEventChange || mode == MODE_ALL_GAPLESS ) { if ( replay_rate > 0 ) curr_stream_time = event_data->frames[0].timestamp; else - curr_stream_time = event_data->frames[event_data->frame_count-1].timestamp; + curr_stream_time = event_data->frames[event_data->last_frame_id-1].timestamp; } - Debug(2, "Event:%" PRIu64 ", Frames:%ld, Duration: %.2f", - event_data->event_id, event_data->frame_count, event_data->duration); + Debug(2, "Event:%" PRIu64 ", Frames:%ld, Last Frame ID(%ld, Duration: %.2f", + event_data->event_id, event_data->frame_count, event_data->last_frame_id, event_data->duration); return true; } // bool EventStream::loadEventData( int event_id ) @@ -341,12 +345,12 @@ void EventStream::processCommand(const CmdMsg *msg) { if ( (mode == MODE_SINGLE || mode == MODE_NONE) && - ((unsigned int)curr_frame_id == event_data->frame_count) + ((unsigned int)curr_frame_id == event_data->last_frame_id) ) { Debug(1, "Was in single or no replay mode, and at last frame, so jumping to 1st frame"); curr_frame_id = 1; } else { - Debug(1, "mode is %s, current frame is %d, frame count is %d", + Debug(1, "mode is %s, current frame is %ld, frame count is %ld, last frame id is %ld", (mode == MODE_SINGLE ? "single" : "not single"), curr_frame_id, event_data->frame_count ); } @@ -359,6 +363,13 @@ void EventStream::processCommand(const CmdMsg *msg) { paused = false; } replay_rate = ntohs(((unsigned char)msg->msg_data[2]<<8)|(unsigned char)msg->msg_data[1])-32768; + if ( replay_rate > 50 * ZM_RATE_BASE ) { + Warning("requested replay rate (%d) is too high. We only support up to 50x", replay_rate); + replay_rate = 50 * ZM_RATE_BASE; + } else if ( replay_rate < -50*ZM_RATE_BASE ) { + Warning("requested replay rate (%d) is too low. We only support up to -50x", replay_rate); + replay_rate = -50 * ZM_RATE_BASE; + } break; case CMD_STOP : Debug(1, "Got STOP command"); @@ -394,7 +405,7 @@ void EventStream::processCommand(const CmdMsg *msg) { paused = true; replay_rate = ZM_RATE_BASE; step = 1; - if ( (unsigned int)curr_frame_id < event_data->frame_count ) + if ( (unsigned int)curr_frame_id < event_data->last_frame_id ) curr_frame_id += 1; Debug(1, "Got SLOWFWD command new frame id %d", curr_frame_id); break; @@ -411,6 +422,8 @@ void EventStream::processCommand(const CmdMsg *msg) { paused = false; // Set play rate switch ( replay_rate ) { + case -1 * ZM_RATE_BASE : + replay_rate = -2 * ZM_RATE_BASE; case -2 * ZM_RATE_BASE : replay_rate = -5 * ZM_RATE_BASE; break; @@ -425,7 +438,7 @@ void EventStream::processCommand(const CmdMsg *msg) { replay_rate = -50 * ZM_RATE_BASE; break; default : - replay_rate = -2 * ZM_RATE_BASE; + replay_rate = -1 * ZM_RATE_BASE; break; } break; @@ -489,14 +502,14 @@ void EventStream::processCommand(const CmdMsg *msg) { if ( replay_rate >= 0 ) curr_frame_id = 0; else - curr_frame_id = event_data->frame_count+1; + curr_frame_id = event_data->last_frame_id+1; paused = false; forceEventChange = true; break; case CMD_NEXT : Debug(1, "Got NEXT command"); if ( replay_rate >= 0 ) - curr_frame_id = event_data->frame_count+1; + curr_frame_id = event_data->last_frame_id+1; else curr_frame_id = 0; paused = false; @@ -505,9 +518,33 @@ void EventStream::processCommand(const CmdMsg *msg) { case CMD_SEEK : { // offset is in seconds - int offset = ((unsigned char)msg->msg_data[1]<<24)|((unsigned char)msg->msg_data[2]<<16)|((unsigned char)msg->msg_data[3]<<8)|(unsigned char)msg->msg_data[4]; - curr_frame_id = (int)(event_data->frame_count*offset/event_data->duration); - Debug(1, "Got SEEK command, to %d (new cfid: %d)", offset, curr_frame_id); + + int int_part = ((unsigned char)msg->msg_data[1]<<24)|((unsigned char)msg->msg_data[2]<<16)|((unsigned char)msg->msg_data[3]<<8)|(unsigned char)msg->msg_data[4]; + int dec_part = ((unsigned char)msg->msg_data[5]<<24)|((unsigned char)msg->msg_data[6]<<16)|((unsigned char)msg->msg_data[7]<<8)|(unsigned char)msg->msg_data[8]; + + double offset = (double)int_part + (double)(dec_part / (double)1000000); + if ( offset < 0.0 ) { + Warning("Invalid offset, not seeking"); + break; + } + // This should get us close, but not all frames will have the same duration + curr_frame_id = (int)(event_data->frame_count*offset/event_data->duration)+1; + if ( event_data->frames[curr_frame_id-1].offset > offset ) { + while ( (curr_frame_id --) && ( event_data->frames[curr_frame_id-1].offset > offset ) ) { + } + } else if ( event_data->frames[curr_frame_id-1].offset < offset ) { + while ( (curr_frame_id ++) && ( event_data->frames[curr_frame_id-1].offset > offset ) ) { + } + } + if ( curr_frame_id < 1 ) { + curr_frame_id = 1; + } else if ( curr_frame_id > event_data->last_frame_id ) { + curr_frame_id = event_data->last_frame_id; + } + + curr_stream_time = event_data->frames[curr_frame_id-1].timestamp; + Debug(1, "Got SEEK command, to %f (new current frame id: %d offset %f)", + offset, curr_frame_id, event_data->frames[curr_frame_id-1].offset); send_frame = true; break; } @@ -524,19 +561,22 @@ void EventStream::processCommand(const CmdMsg *msg) { struct { uint64_t event_id; - int progress; + double duration; + double progress; int rate; int zoom; bool paused; } status_data; status_data.event_id = event_data->event_id; - status_data.progress = (int)event_data->frames[curr_frame_id-1].offset; + status_data.duration = event_data->duration; + status_data.progress = event_data->frames[curr_frame_id-1].offset; status_data.rate = replay_rate; status_data.zoom = zoom; status_data.paused = paused; - Debug(2, "Event:%" PRIu64 ", Paused:%d, progress:%d Rate:%d, Zoom:%d", + Debug(2, "Event:%" PRIu64 ", Duration %f, Paused:%d, progress:%f Rate:%d, Zoom:%d", status_data.event_id, + status_data.duration, status_data.paused, status_data.progress, status_data.rate, @@ -568,7 +608,14 @@ bool EventStream::checkEventLoaded() { snprintf(sql, sizeof(sql), "SELECT `Id` FROM `Events` WHERE `MonitorId` = %d AND `Id` < %" PRIu64 " ORDER BY `Id` DESC LIMIT 1", event_data->monitor_id, event_data->event_id); - } else if ( (unsigned int)curr_frame_id > event_data->frame_count ) { + } else if ( (unsigned int)curr_frame_id > event_data->last_frame_id ) { + if ( !event_data->end_time ) { + // We are viewing an in-process event, so just reload it. + loadEventData(event_data->event_id); + if ( (unsigned int)curr_frame_id > event_data->last_frame_id ) + curr_frame_id = event_data->last_frame_id; + return false; + } snprintf(sql, sizeof(sql), "SELECT `Id` FROM `Events` WHERE `MonitorId` = %d AND `Id` > %" PRIu64 " ORDER BY `Id` ASC LIMIT 1", event_data->monitor_id, event_data->event_id); @@ -581,6 +628,7 @@ bool EventStream::checkEventLoaded() { // Event change required. if ( forceEventChange || ( (mode != MODE_SINGLE) && (mode != MODE_NONE) ) ) { + Debug(1, "Checking for next event %s", sql); if ( mysql_query(&dbconn, sql) ) { Error("Can't run query: %s", mysql_error(&dbconn)); exit(mysql_errno(&dbconn)); @@ -591,6 +639,9 @@ bool EventStream::checkEventLoaded() { Error("Can't use query result: %s", mysql_error(&dbconn)); exit(mysql_errno(&dbconn)); } + if ( mysql_num_rows(result) != 1 ) { + Debug(1, "No rows returned for %s", sql); + } MYSQL_ROW dbrow = mysql_fetch_row(result); if ( mysql_errno(&dbconn)) { @@ -605,7 +656,7 @@ bool EventStream::checkEventLoaded() { loadEventData(event_id); if ( replay_rate < 0 ) // rewind - curr_frame_id = event_data->frame_count; + curr_frame_id = event_data->last_frame_id; else curr_frame_id = 1; Debug(2, "New frame id = %d", curr_frame_id); @@ -626,7 +677,7 @@ bool EventStream::checkEventLoaded() { if ( curr_frame_id <= 0 ) curr_frame_id = 1; else - curr_frame_id = event_data->frame_count; + curr_frame_id = event_data->last_frame_id; paused = true; } return false; @@ -683,9 +734,6 @@ bool EventStream::sendFrame(int delta_us) { #endif // HAVE_LIBAVCODEC { - static unsigned char temp_img_buffer[ZM_MAX_IMAGE_SIZE]; - int img_buffer_size = 0; - uint8_t *img_buffer = temp_img_buffer; bool send_raw = (type == STREAM_JPEG) && ((scale>=ZM_SCALE_BASE)&&(zoom==ZM_SCALE_BASE)) && filepath[0]; @@ -748,6 +796,9 @@ bool EventStream::sendFrame(int delta_us) { } Image *send_image = prepareImage(image); + static unsigned char temp_img_buffer[ZM_MAX_IMAGE_SIZE]; + int img_buffer_size = 0; + uint8_t *img_buffer = temp_img_buffer; switch ( type ) { case STREAM_JPEG : @@ -776,7 +827,7 @@ bool EventStream::sendFrame(int delta_us) { } // end if stream MPEG or other - fputs("\r\n\r\n", stdout); + fputs("\r\n", stdout); fflush(stdout); last_frame_sent = TV_2_FLOAT(now); return true; @@ -795,13 +846,11 @@ void EventStream::runStream() { exit(0); } - Debug(3, "frame rate is: (%f)", (double)event_data->frame_count/event_data->duration); updateFrameRate((double)event_data->frame_count/event_data->duration); gettimeofday(&start, nullptr); uint64_t start_usec = start.tv_sec * 1000000 + start.tv_usec; uint64_t last_frame_offset = 0; - bool in_event = true; double time_to_event = 0; while ( !zm_terminate ) { @@ -843,8 +892,8 @@ void EventStream::runStream() { send_frame = true; } else if ( !send_frame ) { // We are paused, not stepping and doing nothing, meaning that comms didn't set send_frame to true - double actual_delta_time = TV_2_FLOAT(now) - last_frame_sent; - if ( actual_delta_time > MAX_STREAM_DELAY ) { + double time_since_last_send = TV_2_FLOAT(now) - last_frame_sent; + if ( time_since_last_send > MAX_STREAM_DELAY ) { // Send keepalive Debug(2, "Sending keepalive frame"); send_frame = true; @@ -852,21 +901,19 @@ void EventStream::runStream() { } // end if streaming stepping or doing nothing // time_to_event > 0 means that we are not in the event - if ( time_to_event > 0 ) { - double actual_delta_time = TV_2_FLOAT(now) - last_frame_sent; - Debug(1, "Actual delta time = %f = %f - %f", actual_delta_time, TV_2_FLOAT(now), last_frame_sent); - // > 1 second - if ( actual_delta_time > 1 ) { - Debug(1, "Sending time to next event frame"); + if ( ( time_to_event > 0 ) and ( mode == MODE_ALL ) ) { + double time_since_last_send = TV_2_FLOAT(now) - last_frame_sent; + Debug(1, "Time since last send = %f = %f - %f", time_since_last_send, TV_2_FLOAT(now), last_frame_sent); + if ( time_since_last_send > 1 /* second */ ) { static char frame_text[64]; - snprintf(frame_text, sizeof(frame_text), "Time to next event = %d seconds", (int)time_to_event); + + snprintf(frame_text, sizeof(frame_text), "Time to %s event = %d seconds", + (replay_rate > 0 ? "next" : "previous" ), (int)time_to_event); if ( !sendTextFrame(frame_text) ) zm_terminate = true; - } else { - Debug(1, "Not Sending time to next event frame because actual delta time is %f", actual_delta_time); + send_frame = false; // In case keepalive was set } - //else - //{ + // FIXME ICON But we are not paused. We are somehow still in the event? double sleep_time = (replay_rate>0?1:-1) * ((1.0L * replay_rate * STREAM_PAUSE_WAIT)/(ZM_RATE_BASE * 1000000)); //double sleep_time = (replay_rate * STREAM_PAUSE_WAIT)/(ZM_RATE_BASE * 1000000); @@ -976,29 +1023,28 @@ void EventStream::runStream() { //if ( step != 0 )// Adding 0 is cheaper than an if 0 // curr_frame_id starts at 1 though, so we might skip the first frame? curr_frame_id += step; - - // Detects when we hit end of event and will load the next event or previous event - if ( checkEventLoaded() ) { - // Have change of event - - // This next bit is to determine if we are in the current event time wise - // and whether to show an image saying how long until the next event. - if ( replay_rate > 0 ) { - // This doesn't make sense unless we have hit the end of the event. - time_to_event = event_data->frames[0].timestamp - curr_stream_time; - Debug(1, "replay rate(%d) time_to_event(%f)=frame timestamp:%f - curr_stream_time(%f)", - replay_rate, time_to_event, - event_data->frames[0].timestamp, - curr_stream_time); - - } else if ( replay_rate < 0 ) { - time_to_event = curr_stream_time - event_data->frames[event_data->frame_count-1].timestamp; - Debug(1, "replay rate(%d) time_to_event(%f)=curr_stream_time(%f)-frame timestamp:%f", - replay_rate, time_to_event, curr_stream_time, event_data->frames[event_data->frame_count-1].timestamp); - } // end if forward or reverse - - } // end if checkEventLoaded } // end if !paused + + // Detects when we hit end of event and will load the next event or previous event + if ( checkEventLoaded() ) { + // Have change of event + + // This next bit is to determine if we are in the current event time wise + // and whether to show an image saying how long until the next event. + if ( replay_rate > 0 ) { + // This doesn't make sense unless we have hit the end of the event. + time_to_event = event_data->frames[0].timestamp - curr_stream_time; + Debug(1, "replay rate(%d) time_to_event(%f)=frame timestamp:%f - curr_stream_time(%f)", + replay_rate, time_to_event, + event_data->frames[0].timestamp, + curr_stream_time); + + } else if ( replay_rate < 0 ) { + time_to_event = curr_stream_time - event_data->frames[event_data->frame_count-1].timestamp; + Debug(1, "replay rate(%d) time_to_event(%f)=curr_stream_time(%f)-frame timestamp:%f", + replay_rate, time_to_event, curr_stream_time, event_data->frames[event_data->frame_count-1].timestamp); + } // end if forward or reverse + } // end if checkEventLoaded } // end while ! zm_terminate #if HAVE_LIBAVCODEC if ( type == STREAM_MPEG ) @@ -1010,6 +1056,7 @@ void EventStream::runStream() { bool EventStream::send_file(const char * filepath) { static unsigned char temp_img_buffer[ZM_MAX_IMAGE_SIZE]; + int rc; int img_buffer_size = 0; uint8_t *img_buffer = temp_img_buffer; @@ -1020,47 +1067,50 @@ bool EventStream::send_file(const char * filepath) { Error("Can't open %s: %s", filepath, strerror(errno)); return false; } - bool size_sent = false; - #if HAVE_SENDFILE static struct stat filestat; if ( fstat(fileno(fdj), &filestat) < 0 ) { + fclose(fdj); /* Close the file handle */ Error("Failed getting information about file %s: %s", filepath, strerror(errno)); return false; } + if ( !filestat.st_size ) { + fclose(fdj); /* Close the file handle */ + Info("File size is zero. Unable to send raw frame %u: %s", curr_frame_id); + return false; + } if ( 0 > fprintf(stdout, "Content-Length: %d\r\n\r\n", (int)filestat.st_size) ) { fclose(fdj); /* Close the file handle */ Info("Unable to send raw frame %u: %s", curr_frame_id, strerror(errno)); return false; } - size_sent = true; - - if ( zm_sendfile(fileno(stdout), fileno(fdj), 0, (int)filestat.st_size) != (int)filestat.st_size ) { + rc = zm_sendfile(fileno(stdout), fileno(fdj), 0, (int)filestat.st_size); + if ( rc == (int)filestat.st_size ) { + // Success fclose(fdj); /* Close the file handle */ return true; } + Warning("Unable to send raw frame %u: %s rc %d", curr_frame_id, strerror(errno), rc); #endif img_buffer_size = fread(img_buffer, 1, sizeof(temp_img_buffer), fdj); - if ( !size_sent ) { - if ( 0 > fprintf(stdout, "Content-Length: %d\r\n\r\n", img_buffer_size) ) { - fclose(fdj); /* Close the file handle */ - Info("Unable to send raw frame %u: %s", curr_frame_id, strerror(errno)); - return false; - } - } - if ( fwrite(img_buffer, img_buffer_size, 1, stdout) != 1 ) { - Error("Unable to send raw frame %u: %s", curr_frame_id, strerror(errno)); + fclose(fdj); /* Close the file handle */ + if ( !img_buffer_size ) { + Info("Unable to read raw frame %u: %s", curr_frame_id, strerror(errno)); return false; } - fclose(fdj); /* Close the file handle */ - return true; + return send_buffer(img_buffer, img_buffer_size); } // end bool EventStream::send_file(const char * filepath) bool EventStream::send_buffer(uint8_t* buffer, int size) { - fprintf(stdout, "Content-Length: %d\r\n\r\n", size); - if ( fwrite(buffer, size, 1, stdout) != 1 ) { - Error("Unable to send raw frame %u: %s", curr_frame_id, strerror(errno)); + if ( 0 > fprintf(stdout, "Content-Length: %d\r\n\r\n", size) ) { + Info("Unable to send raw frame %u: %s", curr_frame_id, strerror(errno)); + return false; + } + int rc = fwrite(buffer, size, 1, stdout); + + if ( 1 != rc ) { + Error("Unable to send raw frame %u: %s %d", curr_frame_id, strerror(errno), rc); return false; } return true; diff --git a/src/zm_eventstream.h b/src/zm_eventstream.h index 023dd7a99..5013e5487 100644 --- a/src/zm_eventstream.h +++ b/src/zm_eventstream.h @@ -54,11 +54,13 @@ class EventStream : public StreamBase { uint64_t event_id; unsigned int monitor_id; unsigned long storage_id; - unsigned long frame_count; + unsigned long frame_count; // Value of Frames column in Event + unsigned long last_frame_id; // Highest frame id known about. Can be < frame_count in incomplete events time_t start_time; + time_t end_time; double duration; char path[PATH_MAX]; - int n_frames; + int n_frames; // # of frame rows returned from database FrameData *frames; char video_file[PATH_MAX]; Storage::Schemes scheme; @@ -74,7 +76,7 @@ class EventStream : public StreamBase { StreamMode mode; bool forceEventChange; - int curr_frame_id; + unsigned long curr_frame_id; double curr_stream_time; bool send_frame; struct timeval start; // clock time when started the event diff --git a/src/zm_ffmpeg_camera.cpp b/src/zm_ffmpeg_camera.cpp index 6224c1f4f..e04f5e8ca 100644 --- a/src/zm_ffmpeg_camera.cpp +++ b/src/zm_ffmpeg_camera.cpp @@ -770,6 +770,7 @@ int FfmpegCamera::CaptureAndRecord( return -1; } +#if 0 if ( (packet.pts != AV_NOPTS_VALUE) && (packet.pts < -100000) ) { // Ignore packets that have crazy negative pts. // They aren't supposed to happen. @@ -788,6 +789,7 @@ int FfmpegCamera::CaptureAndRecord( } // If we get a good frame, decrease the error count.. We could zero it... if ( error_count > 0 ) error_count -= 1; +#endif int keyframe = packet.flags & AV_PKT_FLAG_KEY; bytes += packet.size; diff --git a/src/zm_ffmpeg_input.cpp b/src/zm_ffmpeg_input.cpp index 0ae6e634b..613131d12 100644 --- a/src/zm_ffmpeg_input.cpp +++ b/src/zm_ffmpeg_input.cpp @@ -17,7 +17,7 @@ FFmpeg_Input::~FFmpeg_Input() { if ( streams ) { for ( unsigned int i = 0; i < input_format_context->nb_streams; i += 1 ) { avcodec_close(streams[i].context); - streams[i].context = nullptr; + avcodec_free_context(&streams[i].context); } delete[] streams; streams = nullptr; @@ -206,7 +206,7 @@ AVFrame *FFmpeg_Input::get_frame(int stream_id, double at) { if ( (last_seek_request >= 0) && - (last_seek_request > seek_target ) + (last_seek_request > seek_target) && (frame->pts > seek_target) ) { @@ -221,6 +221,9 @@ AVFrame *FFmpeg_Input::get_frame(int stream_id, double at) { // Have to grab a frame to update our current frame to know where we are get_frame(stream_id); zm_dump_frame(frame, "frame->pts > seek_target, got"); + } else if ( last_seek_request == seek_target ) { + // paused case, sending keepalives + return frame; } // end if frame->pts > seek_target last_seek_request = seek_target; diff --git a/src/zm_image.cpp b/src/zm_image.cpp index 0a4cca08f..b2707dd52 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -1850,10 +1850,10 @@ void Image::Delta( const Image &image, Image* targetimage) const { #endif } -const Coord Image::centreCoord( const char *text ) const { +const Coord Image::centreCoord( const char *text, int size=1 ) const { int index = 0; int line_no = 0; - int text_len = strlen( text ); + int text_len = strlen(text); int line_len = 0; int max_line_len = 0; const char *line = text; @@ -1869,8 +1869,8 @@ const Coord Image::centreCoord( const char *text ) const { line = text+index; line_no++; } - int x = (width - (max_line_len * ZM_CHAR_WIDTH) ) / 2; - int y = (height - (line_no * LINE_HEIGHT) ) / 2; + int x = (width - (max_line_len * ZM_CHAR_WIDTH * size) ) / 2; + int y = (height - (line_no * LINE_HEIGHT * size) ) / 2; return Coord(x, y); } diff --git a/src/zm_image.h b/src/zm_image.h index 8f86c3e30..3e07e41eb 100644 --- a/src/zm_image.h +++ b/src/zm_image.h @@ -263,7 +263,7 @@ public: //Image *Delta( const Image &image ) const; void Delta( const Image &image, Image* targetimage) const; - const Coord centreCoord( const char *text ) const; + const Coord centreCoord( const char *text, const int size ) const; void MaskPrivacy( const unsigned char *p_bitmask, const Rgb pixel_colour=0x00222222 ); void Annotate( const char *p_text, const Coord &coord, const unsigned int size=1, const Rgb fg_colour=RGB_WHITE, const Rgb bg_colour=RGB_BLACK ); Image *HighlightEdges( Rgb colour, unsigned int p_colours, unsigned int p_subpixelorder, const Box *limits=0 ); diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index de245f854..7a1677d7b 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -497,6 +497,8 @@ Monitor::Monitor( shared_data->last_read_time = 0; shared_data->alarm_x = -1; shared_data->alarm_y = -1; + } else { + shared_data = nullptr; } start_time = last_fps_time = time( 0 ); @@ -1448,18 +1450,19 @@ bool Monitor::Analyse() { int new_motion_score = DetectMotion(*snap_image, zoneSet); Debug(3, - "After motion detection, last_motion_score(%d), new motion score(%d)", - last_motion_score, new_motion_score + "After motion detection, score(%d), last_motion_score(%d), new motion score(%d)", + score, last_motion_score, new_motion_score ); last_motion_score = new_motion_score; } if ( last_motion_score ) { score += last_motion_score; - if ( !event ) { + // cause is calculated every frame, + //if ( !event ) { if ( cause.length() ) cause += ", "; cause += MOTION_CAUSE; - } + //} noteSetMap[MOTION_CAUSE] = zoneSet; } // end if motion_score //shared_data->active = signal; // unneccessary active gets set on signal change @@ -1559,7 +1562,7 @@ bool Monitor::Analyse() { alarm_cause = alarm_cause + "," + std::string(zones[i]->Label()); } } - if ( !alarm_cause.empty() ) alarm_cause[0] = ' '; + if ( !alarm_cause.empty() ) alarm_cause[0] = ' '; // replace leading , with a space alarm_cause = cause + alarm_cause; strncpy(shared_data->alarm_cause, alarm_cause.c_str(), sizeof(shared_data->alarm_cause)-1); Info("%s: %03d - Gone into alarm state PreAlarmCount: %u > AlarmFrameCount:%u Cause:%s", @@ -1631,6 +1634,13 @@ bool Monitor::Analyse() { } } event->AddFrames(pre_event_images, images, timestamps); + } else if ( alarm_frame_count > 1 ) { + int temp_alarm_frame_count = alarm_frame_count; + while ( --temp_alarm_frame_count ) { + Debug(1, "Adding previous frame due to alarm_frame_count %d", pre_index); + event->AddFrame(image_buffer[pre_index].image, *image_buffer[pre_index].timestamp, 0, nullptr); + pre_index = (pre_index + 1)%image_buffer_count; + } } // end if pre_event_images if ( ( alarm_frame_count > 1 ) && Event::PreAlarmCount() ) { @@ -1668,6 +1678,8 @@ bool Monitor::Analyse() { } else { shared_data->state = state = TAPE; } + } else { + Debug(1, "Not leaving ALERT beacuse image_count(%d)-last_alarm_count(%d) > post_event_count(%d) and timestamp.tv_sec(%d) - recording.tv_src(%d) >= min_section_length(%d)", image_count, last_alarm_count, post_event_count, timestamp->tv_sec, video_store_data->recording.tv_sec, min_section_length); } } // end if ALARM or ALERT @@ -2108,7 +2120,6 @@ Monitor *Monitor::Load(MYSQL_ROW dbrow, bool load_zones, Purpose purpose) { } else { v4l_captures_per_frame = config.captures_per_frame; } - Debug(1, "Got %d for v4l_captures_per_frame", v4l_captures_per_frame); col++; std::string protocol = dbrow[col] ? dbrow[col] : ""; col++; @@ -2398,10 +2409,13 @@ Monitor *Monitor::Load(MYSQL_ROW dbrow, bool load_zones, Purpose purpose) { 0 ); camera->setMonitor(monitor); - Zone **zones = 0; - int n_zones = Zone::Load(monitor, zones); - monitor->AddZones(n_zones, zones); - monitor->AddPrivacyBitmask(zones); + int n_zones = 0; + if ( load_zones ) { + Zone **zones = 0; + n_zones = Zone::Load(monitor, zones); + monitor->AddZones(n_zones, zones); + monitor->AddPrivacyBitmask(zones); + } Debug(1, "Loaded monitor %d(%s), %d zones", id, name, n_zones); return monitor; } // end Monitor *Monitor::Load(MYSQL_ROW dbrow, bool load_zones, Purpose purpose) diff --git a/src/zm_monitor.h b/src/zm_monitor.h index f1db76195..4b6ec5088 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -505,6 +505,8 @@ public: inline time_t getStartupTime() const { return shared_data->startup_time; } inline void setStartupTime( time_t p_time ) { shared_data->startup_time = p_time; } + int LabelSize() { return label_size; } + void actionReload(); void actionEnable(); void actionDisable(); diff --git a/src/zm_stream.cpp b/src/zm_stream.cpp index 05236c3ba..e73813b4b 100644 --- a/src/zm_stream.cpp +++ b/src/zm_stream.cpp @@ -39,7 +39,7 @@ StreamBase::~StreamBase() { closeComms(); if ( monitor ) { delete monitor; - monitor = NULL; + monitor = nullptr; } } @@ -252,7 +252,8 @@ bool StreamBase::sendTextFrame(const char *frame_text) { monitor->Width(), monitor->Height(), scale, frame_text); Image image(monitor->Width(), monitor->Height(), monitor->Colours(), monitor->SubpixelOrder()); - image.Annotate(frame_text, image.centreCoord(frame_text)); + image.Clear(); + image.Annotate(frame_text, image.centreCoord(frame_text, monitor->LabelSize()), monitor->LabelSize()); if ( scale != 100 ) { image.Scale(scale); diff --git a/src/zm_stream.h b/src/zm_stream.h index d906b2de6..aa9120809 100644 --- a/src/zm_stream.h +++ b/src/zm_stream.h @@ -116,7 +116,7 @@ protected: public: StreamBase(): monitor_id(0), - monitor(0), + monitor(nullptr), type(DEFAULT_TYPE), format(""), replay_rate(DEFAULT_RATE), diff --git a/src/zm_utils.cpp b/src/zm_utils.cpp index 0f7a9bca9..45a41e543 100644 --- a/src/zm_utils.cpp +++ b/src/zm_utils.cpp @@ -229,63 +229,31 @@ void hwcaps_detect() { neonversion = 0; sse_version = 0; #if (defined(__i386__) || defined(__x86_64__)) - /* x86 or x86-64 processor */ - uint32_t r_edx, r_ecx, r_ebx; + __builtin_cpu_init(); -#ifdef __x86_64__ - __asm__ __volatile__( - "push %%rbx\n\t" - "mov $0x0,%%ecx\n\t" - "mov $0x7,%%eax\n\t" - "cpuid\n\t" - "push %%rbx\n\t" - "mov $0x1,%%eax\n\t" - "cpuid\n\t" - "pop %%rax\n\t" - "pop %%rbx\n\t" - : "=d" (r_edx), "=c" (r_ecx), "=a" (r_ebx) - : - : - ); -#else - __asm__ __volatile__( - "push %%ebx\n\t" - "mov $0x0,%%ecx\n\t" - "mov $0x7,%%eax\n\t" - "cpuid\n\t" - "push %%ebx\n\t" - "mov $0x1,%%eax\n\t" - "cpuid\n\t" - "pop %%eax\n\t" - "pop %%ebx\n\t" - : "=d" (r_edx), "=c" (r_ecx), "=a" (r_ebx) - : - : - ); -#endif - if ( r_ebx & 0x00000020 ) { + if ( __builtin_cpu_supports("avx2") ) { sse_version = 52; /* AVX2 */ Debug(1, "Detected a x86\\x86-64 processor with AVX2"); - } else if ( r_ecx & 0x10000000 ) { + } else if ( __builtin_cpu_supports("avx") ) { sse_version = 51; /* AVX */ Debug(1, "Detected a x86\\x86-64 processor with AVX"); - } else if ( r_ecx & 0x00100000 ) { + } else if ( __builtin_cpu_supports("sse4.2") ) { sse_version = 42; /* SSE4.2 */ Debug(1, "Detected a x86\\x86-64 processor with SSE4.2"); - } else if ( r_ecx & 0x00080000 ) { + } else if ( __builtin_cpu_supports("sse4.1") ) { sse_version = 41; /* SSE4.1 */ Debug(1, "Detected a x86\\x86-64 processor with SSE4.1"); - } else if ( r_ecx & 0x00000200 ) { + } else if ( __builtin_cpu_supports("ssse3") ) { sse_version = 35; /* SSSE3 */ Debug(1,"Detected a x86\\x86-64 processor with SSSE3"); - } else if ( r_ecx & 0x00000001 ) { + } else if ( __builtin_cpu_supports("sse3") ) { sse_version = 30; /* SSE3 */ Debug(1, "Detected a x86\\x86-64 processor with SSE3"); - } else if ( r_edx & 0x04000000 ) { + } else if ( __builtin_cpu_supports("sse2") ) { sse_version = 20; /* SSE2 */ Debug(1, "Detected a x86\\x86-64 processor with SSE2"); - } else if ( r_edx & 0x02000000 ) { + } else if ( __builtin_cpu_supports("sse") ) { sse_version = 10; /* SSE */ Debug(1, "Detected a x86\\x86-64 processor with SSE"); } else { @@ -320,7 +288,7 @@ __attribute__((noinline,__target__("sse2"))) #endif void* sse2_aligned_memcpy(void* dest, const void* src, size_t bytes) { #if ((defined(__i386__) || defined(__x86_64__) || defined(ZM_KEEP_SSE)) && !defined(ZM_STRIP_SSE)) - if ( bytes > 128 ) { + if(bytes > 128) { unsigned int remainder = bytes % 128; const uint8_t* lastsrc = (uint8_t*)src + (bytes - remainder); @@ -362,7 +330,7 @@ void* sse2_aligned_memcpy(void* dest, const void* src, size_t bytes) { } #else /* Non x86\x86-64 platform, use memcpy */ - memcpy(dest, src, bytes); + memcpy(dest,src,bytes); #endif return dest; } diff --git a/src/zm_zone.cpp b/src/zm_zone.cpp index 974409ff3..8fc7f88c9 100644 --- a/src/zm_zone.cpp +++ b/src/zm_zone.cpp @@ -126,7 +126,8 @@ void Zone::Setup( Zone::~Zone() { delete[] label; - delete image; + if ( image ) + delete image; delete pg_image; delete[] ranges; } @@ -158,7 +159,8 @@ void Zone::SetScore(unsigned int nScore) { } // end void Zone::SetScore(unsigned int nScore) void Zone::SetAlarmImage(const Image* srcImage) { - delete image; + if ( image ) + delete image; image = new Image(*srcImage); } // end void Zone::SetAlarmImage( const Image* srcImage ) @@ -205,7 +207,8 @@ bool Zone::CheckAlarms(const Image *delta_image) { return false; } - delete image; + if ( image ) + delete image; // Get the difference image Image *diff_image = image = new Image(*delta_image); int diff_width = diff_image->Width(); @@ -734,14 +737,12 @@ bool Zone::CheckAlarms(const Image *delta_image) { // Only need to delete this when 'image' becomes detached and points somewhere else delete diff_image; - } else { - delete image; - image = 0; - } + diff_image = nullptr; + } // end if ( (type < PRECLUSIVE) && (check_method >= BLOBS) && (monitor->GetOptSaveJPEGs() > 1) Debug(1, "%s: Pixel Diff: %d, Alarm Pixels: %d, Filter Pixels: %d, Blob Pixels: %d, Blobs: %d, Score: %d", Label(), pixel_diff, alarm_pixels, alarm_filter_pixels, alarm_blob_pixels, alarm_blobs, score); - } + } // end if score return true; } diff --git a/version b/version index 96eced874..a0b71b46f 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.35.7 +1.35.10 diff --git a/web/ajax/add_monitors.php b/web/ajax/add_monitors.php index e7048bb4a..952304775 100644 --- a/web/ajax/add_monitors.php +++ b/web/ajax/add_monitors.php @@ -19,11 +19,11 @@ function probe( &$url_bits ) { $cam_list_html = file_get_contents('http://'.$url_bits['host'].':5000/monitoring/'); if ( $cam_list_html ) { - ZM\Logger::Debug("Have content at port 5000/monitoring"); + ZM\Debug("Have content at port 5000/monitoring"); $matches_count = preg_match_all( '/([^<]+)<\/a>/', $cam_list_html, $cam_list ); - ZM\Logger::Debug(print_r($cam_list,true)); + ZM\Debug(print_r($cam_list,true)); } if ( $matches_count ) { for( $index = 0; $index < $matches_count; $index ++ ) { @@ -33,7 +33,7 @@ function probe( &$url_bits ) { if ( ! isset($new_stream['scheme'] ) ) $new_stream['scheme'] = 'http'; $available_streams[] = $new_stream; -ZM\Logger::Debug("Have new stream " . print_r($new_stream,true) ); +ZM\Debug("Have new stream " . print_r($new_stream,true) ); } } else { ZM\Info('No matches'); diff --git a/web/ajax/device.php b/web/ajax/device.php new file mode 100644 index 000000000..71e76d8f7 --- /dev/null +++ b/web/ajax/device.php @@ -0,0 +1,47 @@ + diff --git a/web/ajax/devices.php b/web/ajax/devices.php new file mode 100644 index 000000000..25d17af49 --- /dev/null +++ b/web/ajax/devices.php @@ -0,0 +1,38 @@ + diff --git a/web/ajax/events.php b/web/ajax/events.php index 16c777ebf..f9a4fc14b 100644 --- a/web/ajax/events.php +++ b/web/ajax/events.php @@ -1,41 +1,214 @@ "search text" pairs +// Bootstrap table sends json_ecoded array, which we must decode +$advsearch = isset($_REQUEST['filter']) ? json_decode($_REQUEST['filter'], JSON_OBJECT_AS_ARRAY) : array(); + +// Sort specifies the name of the column to sort on +$sort = 'StartTime'; +if ( isset($_REQUEST['sort']) ) { + if ( !in_array($_REQUEST['sort'], array_merge($columns, $col_alt)) ) { + ZM\Error('Invalid sort field: ' . $_REQUEST['sort']); + } else { + $sort = $_REQUEST['sort']; + //if ( $sort == 'DateTime' ) $sort = 'TimeKey'; + } +} + +// Offset specifies the starting row to return, used for pagination +$offset = 0; +if ( isset($_REQUEST['offset']) ) { + if ( ( !is_int($_REQUEST['offset']) and !ctype_digit($_REQUEST['offset']) ) ) { + ZM\Error('Invalid value for offset: ' . $_REQUEST['offset']); + } else { + $offset = $_REQUEST['offset']; + } +} + +// Order specifies the sort direction, either asc or desc +$order = (isset($_REQUEST['order']) and (strtolower($_REQUEST['order']) == 'asc')) ? 'ASC' : 'DESC'; + +// Limit specifies the number of rows to return +$limit = 100; +if ( isset($_REQUEST['limit']) ) { + if ( ( !is_int($_REQUEST['limit']) and !ctype_digit($_REQUEST['limit']) ) ) { + ZM\Error('Invalid value for limit: ' . $_REQUEST['limit']); + } else { + $limit = $_REQUEST['limit']; + } +} + +// +// MAIN LOOP +// + +switch ( $task ) { + case 'archive' : + case 'unarchive' : + foreach ( $eids as $eid ) archiveRequest($task, $eid); + break; + case 'delete' : + foreach ( $eids as $eid ) $data[] = deleteRequest($eid); + break; + case 'query' : + $data = queryRequest($search, $advsearch, $sort, $offset, $order, $limit); + break; + default : + ZM\Fatal("Unrecognised task '$task'"); +} // end switch task + +ajaxResponse($data); + +// +// FUNCTION DEFINITIONS +// + +function archiveRequest($task, $eid) { + $archiveVal = ($task == 'archive') ? 1 : 0; + dbQuery( + 'UPDATE Events SET Archived = ? WHERE Id = ?', + array($archiveVal, $eid) + ); +} + +function deleteRequest($eid) { $message = array(); + $event = new ZM\Event($eid); + if ( !$event->Id() ) { + $message[] = array($eid=>'Event not found.'); + } else if ( $event->Archived() ) { + $message[] = array($eid=>'Event is archived, cannot delete it.'); + } else { + $event->delete(); + } + + return $message; +} - foreach ( $_REQUEST['eids'] as $eid ) { +function queryRequest($search, $advsearch, $sort, $offset, $order, $limit) { + // Put server pagination code here + // The table we want our data from + $table = 'Events'; - switch ( $_REQUEST['action'] ) { - case 'archive' : - case 'unarchive' : - $archiveVal = ($_REQUEST['action'] == 'archive') ? 1 : 0; - dbQuery( - 'UPDATE Events SET Archived = ? WHERE Id = ?', - array($archiveVal, $eid) - ); - break; - case 'delete' : - $event = new ZM\Event($eid); - if ( !$event->Id() ) { - $message[] = array($eid=>'Event not found.'); - } else if ( $event->Archived() ) { - $message[] = array($eid=>'Event is archived, cannot delete it.'); - } else { - $event->delete(); + // The names of the dB columns in the log table we are interested in + $columns = array('Id', 'MonitorId', 'StorageId', 'Name', 'Cause', 'StartTime', 'EndTime', 'Length', 'Frames', 'AlarmFrames', 'TotScore', 'AvgScore', 'MaxScore', 'Archived', 'Emailed', 'Notes', 'DiskSpace'); + + // The names of columns shown in the log view that are NOT dB columns in the database + $col_alt = array('Monitor', 'Storage'); + + $col_str = implode(', ', $columns); + $data = array(); + $query = array(); + $query['values'] = array(); + $likes = array(); + $where = ''; + // There are two search bars in the log view, normal and advanced + // Making an exuctive decision to ignore the normal search, when advanced search is in use + // Alternatively we could try to do both + if ( count($advsearch) ) { + + foreach ( $advsearch as $col=>$text ) { + if ( !in_array($col, array_merge($columns, $col_alt)) ) { + ZM\Error("'$col' is not a sortable column name"); + continue; } - break; - } // end switch action - } // end foreach - ajaxResponse($message); -} // end if canEdit('Events') + $text = '%' .$text. '%'; + array_push($likes, $col.' LIKE ?'); + array_push($query['values'], $text); + } + $wherevalues = $query['values']; + $where = ' WHERE (' .implode(' OR ', $likes). ')'; -ajaxError('Unrecognised action '.$_REQUEST['action'].' or insufficient permissions for user '.$user['Username']); + } else if ( $search != '' ) { + + $search = '%' .$search. '%'; + foreach ( $columns as $col ) { + array_push($likes, $col.' LIKE ?'); + array_push($query['values'], $search); + } + $wherevalues = $query['values']; + $where = ' WHERE (' .implode(' OR ', $likes). ')'; + } + + $query['sql'] = 'SELECT ' .$col_str. ' FROM `' .$table. '` ' .$where. ' ORDER BY ' .$sort. ' ' .$order. ' LIMIT ?, ?'; + array_push($query['values'], $offset, $limit); + + //ZM\Warning('Calling the following sql query: ' .$query['sql']); + + $data['totalNotFiltered'] = dbFetchOne('SELECT count(*) AS Total FROM ' .$table, 'Total'); + if ( $search != '' || count($advsearch) ) { + $data['total'] = dbFetchOne('SELECT count(*) AS Total FROM ' .$table.$where , 'Total', $wherevalues); + } else { + $data['total'] = $data['totalNotFiltered']; + } + + $storage_areas = ZM\Storage::find(); + $StorageById = array(); + foreach ( $storage_areas as $S ) { + $StorageById[$S->Id()] = $S; + } + + $monitor_names = ZM\Monitor::find(); + $MonitorById = array(); + foreach ( $monitor_names as $S ) { + $MonitorById[$S->Id()] = $S; + } + + $rows = array(); + foreach ( dbFetchAll($query['sql'], NULL, $query['values']) as $row ) { + $event = new ZM\Event($row['Id']); + $scale = intval(5*100*ZM_WEB_LIST_THUMB_WIDTH / $event->Width()); + $imgSrc = $event->getThumbnailSrc(array(),'&'); + $streamSrc = $event->getStreamSrc(array( + 'mode'=>'jpeg', 'scale'=>$scale, 'maxfps'=>ZM_WEB_VIDEO_MAXFPS, 'replay'=>'single', 'rate'=>'400'), '&'); + + // Modify the row data as needed + $row['imgHtml'] = '' .validHtmlStr('Event ' .$event->Id()). ''; + $row['Name'] = validHtmlStr($row['Name']); + $row['Archived'] = $row['Archived'] ? translate('Yes') : translate('No'); + $row['Emailed'] = $row['Emailed'] ? translate('Yes') : translate('No'); + $row['Monitor'] = ( $row['MonitorId'] and isset($MonitorById[$row['MonitorId']]) ) ? $MonitorById[$row['MonitorId']]->Name() : ''; + $row['Cause'] = validHtmlStr($row['Cause']); + $row['StartTime'] = strftime(STRF_FMT_DATETIME_SHORTER, strtotime($row['StartTime'])); + $row['EndTime'] = strftime(STRF_FMT_DATETIME_SHORTER, strtotime($row['StartTime'])); + $row['Length'] = gmdate('H:i:s', $row['Length'] ); + $row['Storage'] = ( $row['StorageId'] and isset($StorageById[$row['StorageId']]) ) ? $StorageById[$row['StorageId']]->Name() : 'Default'; + $row['Notes'] = htmlspecialchars($row['Notes']); + $row['DiskSpace'] = human_filesize($row['DiskSpace']); + $rows[] = $row; + } + $data['rows'] = $rows; + $data['updated'] = preg_match('/%/', DATE_FMT_CONSOLE_LONG) ? strftime(DATE_FMT_CONSOLE_LONG) : date(DATE_FMT_CONSOLE_LONG); + + return $data; +} ?> diff --git a/web/ajax/log.php b/web/ajax/log.php index 16f7ac573..9e6bd9585 100644 --- a/web/ajax/log.php +++ b/web/ajax/log.php @@ -1,466 +1,132 @@ "search text" pairs +// Bootstrap table sends json_ecoded array, which we must decode +$advsearch = isset($_REQUEST['filter']) ? json_decode($_REQUEST['filter'], JSON_OBJECT_AS_ARRAY) : array(); + +// Sort specifies the name of the column to sort on +$sort = 'TimeKey'; +if ( isset($_REQUEST['sort']) ) { + if ( !in_array($_REQUEST['sort'], array_merge($columns, $col_alt)) ) { + ZM\Error('Invalid sort field: ' . $_REQUEST['sort']); + } else { + $sort = $_REQUEST['sort']; + if ( $sort == 'DateTime' ) $sort = 'TimeKey'; } - $sortField = 'TimeKey'; - if ( isset($_REQUEST['sortField']) ) { - if ( !in_array($_REQUEST['sortField'], $filterFields) and ( $_REQUEST['sortField'] != 'TimeKey' ) ) { - ZM\Error('Invalid sort field ' . $_REQUEST['sortField']); - } else { - $sortField = $_REQUEST['sortField']; - } - } - $sortOrder = (isset($_REQUEST['sortOrder']) and ($_REQUEST['sortOrder'] == 'asc')) ? 'asc' : 'desc'; - $filter = isset($_REQUEST['filter']) ? $_REQUEST['filter'] : array(); +} - $sql = $action.' FROM Logs'; - $where = array(); - $values = array(); - if ( $minTime ) { - $where[] = 'TimeKey > ?'; - $values[] = $minTime; - } elseif ( $maxTime ) { - $where[] = 'TimeKey < ?'; - $values[] = $maxTime; +// Offset specifies the starting row to return, used for pagination +$offset = 0; +if ( isset($_REQUEST['offset']) ) { + if ( ( !is_int($_REQUEST['offset']) and !ctype_digit($_REQUEST['offset']) ) ) { + ZM\Error('Invalid value for offset: ' . $_REQUEST['offset']); + } else { + $offset = $_REQUEST['offset']; } +} - foreach ( $filter as $field=>$value ) { - if ( !in_array($field, $filterFields) ) { - ZM\Error("'$field' is not in valid filter fields " . print_r($filterField, true)); +// Order specifies the sort direction, either asc or desc +$order = (isset($_REQUEST['order']) and (strtolower($_REQUEST['order']) == 'asc')) ? 'ASC' : 'DESC'; + +// Limit specifies the number of rows to return +$limit = 100; +if ( isset($_REQUEST['limit']) ) { + if ( ( !is_int($_REQUEST['limit']) and !ctype_digit($_REQUEST['limit']) ) ) { + ZM\Error('Invalid value for limit: ' . $_REQUEST['limit']); + } else { + $limit = $_REQUEST['limit']; + } +} + +$col_str = implode(', ', $columns); +$data = array(); +$query = array(); +$query['values'] = array(); +$likes = array(); +$where = ''; +// There are two search bars in the log view, normal and advanced +// Making an exuctive decision to ignore the normal search, when advanced search is in use +// Alternatively we could try to do both +if ( count($advsearch) ) { + + foreach ( $advsearch as $col=>$text ) { + if ( !in_array($col, array_merge($columns, $col_alt)) ) { + ZM\Error("'$col' is not a sortable column name"); continue; } - if ( $field == 'Level' ) { - $where[] = $field.' <= ?'; - $values[] = $value; - } else { - $where[] = $field.' = ?'; - $values[] = $value; - } + $text = '%' .$text. '%'; + array_push($likes, $col.' LIKE ?'); + array_push($query['values'], $text); } - if ( count($where) ) - $sql.= ' WHERE '.join(' AND ', $where); - $sql .= ' ORDER BY '.$sortField.' '.$sortOrder.' LIMIT '.$limit; + $wherevalues = $query['values']; + $where = ' WHERE (' .implode(' OR ', $likes). ')'; - return array('sql'=>$sql, 'values'=>$values); -} # function buildLogQuery($action) +} else if ( $search != '' ) { -switch ( $_REQUEST['task'] ) { - case 'create' : - { - // Silently ignore bogus requests - if ( !empty($_POST['level']) && !empty($_POST['message']) ) { - ZM\logInit(array('id'=>'web_js')); - - $string = $_POST['message']; - - $file = !empty($_POST['file']) ? preg_replace('/\w+:\/\/[\w.:]+\//', '', $_POST['file']) : ''; - if ( !empty($_POST['line']) ) { - $line = validInt($_POST['line']); - } else { - $line = NULL; - } - - $levels = array_flip(ZM\Logger::$codes); - if ( !isset($levels[$_POST['level']]) ) { - ZM\Panic('Unexpected logger level '.$_POST['level']); - } - $level = $levels[$_POST['level']]; - ZM\Logger::fetch()->logPrint($level, $string, $file, $line); - } else { - ZM\Error('Invalid log create: '.print_r($_POST, true)); - } - ajaxResponse(); - break; + $search = '%' .$search. '%'; + foreach ( $columns as $col ) { + array_push($likes, $col.' LIKE ?'); + array_push($query['values'], $search); } - case 'delete' : - { - if ( !canEdit('System') ) - ajaxError('Insufficient permissions to delete log entries'); + $wherevalues = $query['values']; + $where = ' WHERE (' .implode(' OR ', $likes). ')'; +} - $query = buildLogQuery('DELETE'); - $result = dbQuery($query['sql'], $query['values']); - ajaxResponse(array('result'=>'Ok', 'deleted'=>$result->rowCount())); - } - case 'query' : - { - if ( !canView('System') ) - ajaxError('Insufficient permissions to view log entries'); - $total = dbFetchOne('SELECT count(*) AS Total FROM Logs', 'Total'); - $query = buildLogQuery('SELECT *'); +$query['sql'] = 'SELECT ' .$col_str. ' FROM `' .$table. '` ' .$where. ' ORDER BY ' .$sort. ' ' .$order. ' LIMIT ?, ?'; +array_push($query['values'], $offset, $limit); - global $Servers; - if ( !$Servers ) - $Servers = ZM\Server::find(); - $servers_by_Id = array(); - # There is probably a better way to do this. - foreach ( $Servers as $server ) { - $servers_by_Id[$server->Id()] = $server; - } +//ZM\Warning('Calling the following sql query: ' .$query['sql']); - $logs = array(); - $options = array(); +$data['totalNotFiltered'] = dbFetchOne('SELECT count(*) AS Total FROM ' .$table, 'Total'); +if ( $search != '' || count($advsearch) ) { + $data['total'] = dbFetchOne('SELECT count(*) AS Total FROM ' .$table.$where , 'Total', $wherevalues); +} else { + $data['total'] = $data['totalNotFiltered']; +} - foreach ( dbFetchAll($query['sql'], NULL, $query['values']) as $log ) { +if ( !$Servers ) + $Servers = ZM\Server::find(); +$servers_by_Id = array(); +# There is probably a better way to do this. +foreach ( $Servers as $server ) { + $servers_by_Id[$server->Id()] = $server; +} - $log['DateTime'] = strftime('%Y-%m-%d %H:%M:%S', intval($log['TimeKey'])); - $log['Server'] = ( $log['ServerId'] and isset($servers_by_Id[$log['ServerId']]) ) ? $servers_by_Id[$log['ServerId']]->Name() : ''; - $log['Message'] = preg_replace('/[\x00-\x1F\x7F-\xFF]/', '', $log['Message']); - foreach ( $filterFields as $field ) { - if ( !isset($options[$field]) ) - $options[$field] = array(); - $value = $log[$field]; +$rows = array(); +foreach ( dbFetchAll($query['sql'], NULL, $query['values']) as $row ) { + $row['DateTime'] = strftime('%Y-%m-%d %H:%M:%S', intval($row['TimeKey'])); + $row['Server'] = ( $row['ServerId'] and isset($servers_by_Id[$row['ServerId']]) ) ? $servers_by_Id[$row['ServerId']]->Name() : ''; + // First strip out any html tags + // Second strip out all characters that are not ASCII 32-126 (yes, 126) + $row['Message'] = preg_replace('/[^\x20-\x7E]/', '', strip_tags($row['Message'])); + $rows[] = $row; +} +$data['rows'] = $rows; +$data['logstate'] = logState(); +$data['updated'] = preg_match('/%/', DATE_FMT_CONSOLE_LONG) ? strftime(DATE_FMT_CONSOLE_LONG) : date(DATE_FMT_CONSOLE_LONG); - if ( $field == 'Level' ) { - if ( $value <= ZM\Logger::INFO ) - $options[$field][$value] = ZM\Logger::$codes[$value]; - else - $options[$field][$value] = 'DB'.$value; - } else if ( $field == 'ServerId' ) { - $options['ServerId'][$value] = ( $value and isset($servers_by_Id[$value]) ) ? $servers_by_Id[$value]->Name() : ''; - } else if ( isset($log[$field]) ) { - $options[$field][$log[$field]] = $value; - } - } - $logs[] = $log; - } # end foreach log db row +ajaxResponse($data); - foreach ( $options as $field => $values ) { - asort($options[$field]); - } - - $available = count($logs); - ajaxResponse(array( - 'updated' => preg_match('/%/', DATE_FMT_CONSOLE_LONG) ? strftime(DATE_FMT_CONSOLE_LONG) : date(DATE_FMT_CONSOLE_LONG), - 'total' => $total, - 'available' => isset($available) ? $available : $total, - 'logs' => $logs, - 'state' => logState(), - 'options' => $options, - )); - break; - } - case 'export' : - { - if ( !canView('System') ) - ajaxError('Insufficient permissions to export logs'); - - $minTime = isset($_POST['minTime']) ? $_POST['minTime'] : NULL; - $maxTime = isset($_POST['maxTime']) ? $_POST['maxTime'] : NULL; - if ( !is_null($minTime) && !is_null($maxTime) && ($minTime > $maxTime) ) { - $tempTime = $minTime; - $minTime = $maxTime; - $maxTime = $tempTime; - } - //$limit = isset($_POST['limit'])?$_POST['limit']:1000; - $filter = isset($_POST['filter'])?$_POST['filter']:array(); - $sortField = 'TimeKey'; - if ( isset($_POST['sortField']) ) { - if ( !in_array($_POST['sortField'], $filterFields) and ($_POST['sortField'] != 'TimeKey') ) { - ZM\Error('Invalid sort field '.$_POST['sortField']); - } else { - $sortField = $_POST['sortField']; - } - } - $sortOrder = (isset($_POST['sortOrder']) and $_POST['sortOrder']) == 'asc' ? 'asc' : 'desc'; - - global $Servers; - if ( !$Servers ) - $Servers = ZM\Server::find(); - $servers_by_Id = array(); - # There is probably a better way to do this. - foreach ( $Servers as $server ) { - $servers_by_Id[$server->Id()] = $server; - } - - $sql = 'SELECT * FROM Logs'; - $where = array(); - $values = array(); - if ( $minTime ) { - if ( preg_match('/(.+)(\.\d+)/', $minTime, $matches) ) { - # This handles sub second precision - $minTime = strtotime($matches[1]).$matches[2]; - } else { - $minTime = strtotime($minTime); - } - $where[] = 'TimeKey >= ?'; - $values[] = $minTime; - } - if ( $maxTime ) { - if ( preg_match('/(.+)(\.\d+)/', $maxTime, $matches) ) { - $maxTime = strtotime($matches[1]).$matches[2]; - } else { - $maxTime = strtotime($maxTime); - } - $where[] = 'TimeKey <= ?'; - $values[] = $maxTime; - } - foreach ( $filter as $field=>$value ) { - if ( $value != '' ) { - if ( $field == 'Level' ) { - $where[] = $field.' <= ?'; - $values[] = $value; - } else { - $where[] = $field.' = ?'; - $values[] = $value; - } - } - } - if ( count($where) ) - $sql.= ' WHERE '.join(' AND ', $where); - $sql .= ' ORDER BY '.$sortField.' '.$sortOrder; - //$sql .= " limit ".dbEscape($limit); - $format = isset($_POST['format']) ? $_POST['format'] : 'text'; - switch ( $format ) { - case 'text' : - $exportExt = 'txt'; - break; - case 'tsv' : - $exportExt = 'tsv'; - break; - case 'html' : - $exportExt = 'html'; - break; - case 'xml' : - $exportExt = 'xml'; - break; - default : - ZM\Fatal("Unrecognised log export format '$format'"); - } - $exportKey = substr(md5(rand()), 0, 8); - $exportFile = 'zm-log.'.$exportExt; - - // mkdir will generate a warning if it exists, but that is ok - error_reporting(0); - if ( ! ( mkdir(ZM_DIR_EXPORTS) || file_exists(ZM_DIR_EXPORTS) ) ) { - ZM\Fatal('Can\'t create exports dir at \''.ZM_DIR_EXPORTS.'\''); - } - $exportPath = ZM_DIR_EXPORTS.'/zm-log-'.$exportKey.$exportExt; - ZM\Logger::Debug("Exporting to $exportPath"); - if ( !($exportFP = fopen($exportPath, 'w')) ) - ZM\Fatal("Unable to open log export file $exportPath"); - $logs = array(); - foreach ( dbFetchAll($sql, NULL, $values) as $log ) { - $log['DateTime'] = preg_replace('/^\d+/', strftime('%Y-%m-%d %H:%M:%S', intval($log['TimeKey'])), $log['TimeKey']); - $log['Server'] = ( $log['ServerId'] and isset($servers_by_Id[$log['ServerId']]) ) ? $servers_by_Id[$log['ServerId']]->Name() : ''; - $logs[] = $log; - } - ZM\Logger::Debug(count($logs).' lines being exported by '.$sql.implode(',', $values)); - - switch( $format ) { - case 'text' : - { - foreach ( $logs as $log ) { - if ( $log['Line'] ) - fprintf($exportFP, "%s %s[%d].%s-%s/%d [%s]\n", - $log['DateTime'], $log['Component'], $log['Pid'], $log['Code'], $log['File'], $log['Line'], $log['Message']); - else - fprintf($exportFP, "%s %s[%d].%s-%s [%s]\n", - $log['DateTime'], $log['Component'], $log['Pid'], $log['Code'], $log['File'], $log['Message']); - } - break; - } - case 'tsv' : - { - # This line doesn't need fprintf, it could use fwrite - fprintf($exportFP, join("\t", - translate('DateTime'), - translate('Component'), - translate('Server'), - translate('Pid'), - translate('Level'), - translate('Message'), - translate('File'), - translate('Line') - )."\n"); - foreach ( $logs as $log ) { - fprintf($exportFP, "%s\t%s\t%s\t%d\t%s\t%s\t%s\t%s\n", - $log['DateTime'], $log['Component'], $log['Server'], $log['Pid'], $log['Code'], $log['Message'], $log['File'], $log['Line']); - } - break; - } - case 'html' : - { - fwrite($exportFP, -' - - - '.translate('ZoneMinderLog').' - - - -

'.translate('ZoneMinderLog').'

-

'.htmlspecialchars(preg_match('/%/', DATE_FMT_CONSOLE_LONG) ? strftime(DATE_FMT_CONSOLE_LONG) : date(DATE_FMT_CONSOLE_LONG)).'

-

'.count($logs).' '.translate('Logs').'

- - - - '); - foreach ( $logs as $log ) { - $classLevel = $log['Level']; - if ( $classLevel < ZM\Logger::FATAL ) { - $classLevel = ZM\Logger::FATAL; - } else if ( $classLevel > ZM\Logger::DEBUG ) { - $classLevel = ZM\Logger::DEBUG; - } - $logClass = 'log-'.strtolower(ZM\Logger::$codes[$classLevel]); - fprintf($exportFP, ' - ', $logClass, $log['DateTime'], $log['Component'], $log['Server'], $log['Pid'], $log['Code'], $log['Message'], $log['File'], $log['Line']); - } - fwrite($exportFP, - ' -
'.translate('DateTime').''.translate('Component').''.translate('Server').''.translate('Pid').''.translate('Level').''.translate('Message').''.translate('File').''.translate('Line').'
%s%s%s%d%s%s%s%s
- - '); - break; - } - case 'xml' : - { - fwrite($exportFP, - ' - - '.$_POST['selector'].''); - foreach ( $filter as $field=>$value ) - if ( $value != '' ) - fwrite( $exportFP, - ' - <'.strtolower($field).'>'.htmlspecialchars($value).' - ' ); - fwrite( $exportFP, - ' - - '.translate('DateTime').' - '.translate('Component').' - '.translate('Server').' - '.translate('Pid').' - '.translate('Level').' - '.translate('Message').' - '.translate('File').' - '.translate('Line').' - - - ' ); - foreach ( $logs as $log ) { - fprintf( $exportFP, - ' - %s - %s - %s - %d - %s - - %s - %d - -', $log['DateTime'], $log['Component'], $log['Server'], $log['Pid'], $log['Code'], utf8_decode( $log['Message'] ), $log['File'], $log['Line'] ); - } - fwrite( $exportFP, - ' - ' ); - break; - } - $exportExt = 'xml'; - break; - } - fclose( $exportFP ); - ajaxResponse( array( - 'key' => $exportKey, - 'format' => $format, - ) ); - break; - } - case 'download' : - { - if ( !canView('System') ) - ajaxError('Insufficient permissions to download logs'); - - if ( empty($_REQUEST['key']) ) - ZM\Fatal('No log export key given'); - $exportKey = $_REQUEST['key']; - if ( empty($_REQUEST['format']) ) - ZM\Fatal('No log export format given'); - $format = $_REQUEST['format']; - - switch ( $format ) { - case 'text' : - $exportExt = 'txt'; - break; - case 'tsv' : - $exportExt = 'tsv'; - break; - case 'html' : - $exportExt = 'html'; - break; - case 'xml' : - $exportExt = 'xml'; - break; - default : - ZM\Fatal("Unrecognised log export format '$format'"); - } - - $exportFile = 'zm-log.'.$exportExt; - $exportPath = ZM_DIR_EXPORTS.'/zm-log-'.$exportKey.$exportExt; - - header('Pragma: public'); - header('Expires: 0'); - header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); - header('Cache-Control: private', false); // required by certain browsers - header('Content-Description: File Transfer'); - header('Content-Disposition: attachment; filename="'.$exportFile.'"'); - header('Content-Transfer-Encoding: binary'); - header('Content-Type: application/force-download'); - header('Content-Length: '.filesize($exportPath)); - readfile($exportPath); - exit(0); - break; - } -} // end switch ( $_REQUEST['task'] ) -ajaxError('Unrecognised action or insufficient permissions'); -?> diff --git a/web/ajax/modal.php b/web/ajax/modal.php index bd96b137f..2794fb0af 100644 --- a/web/ajax/modal.php +++ b/web/ajax/modal.php @@ -8,7 +8,7 @@ if ( empty($_REQUEST['modal']) ) { $modal = validJsStr($_REQUEST['modal']); $data = array(); -ZM\Logger::Debug("Including modals/$modal.php"); +ZM\Debug("Including modals/$modal.php"); # Shouldn't be necessary but at the moment we have last .conf file contents ob_start(); @$result = include('modals/'.$modal.'.php'); diff --git a/web/ajax/modals/controlpreset.php b/web/ajax/modals/controlpreset.php new file mode 100644 index 000000000..124859507 --- /dev/null +++ b/web/ajax/modals/controlpreset.php @@ -0,0 +1,55 @@ + + diff --git a/web/ajax/modals/device.php b/web/ajax/modals/device.php new file mode 100644 index 000000000..5a29785ef --- /dev/null +++ b/web/ajax/modals/device.php @@ -0,0 +1,54 @@ + "", + "Name" => "New Device", + "KeyString" => "" + ); +} + +?> + diff --git a/web/ajax/modals/download.php b/web/ajax/modals/download.php new file mode 100644 index 000000000..7009f8738 --- /dev/null +++ b/web/ajax/modals/download.php @@ -0,0 +1,119 @@ +'.PHP_EOL; + + $Event = new ZM\Event($eid); + if ( !$Event->Id() ) { + ZM\Error('Invalid event id'); + $result .= '
Invalid event id
'.PHP_EOL; + } else { + $result .= 'Downloading event ' . $Event->Id . '. Resulting file should be approximately ' . human_filesize( $Event->DiskSpace() ).PHP_EOL; + } + } else if ( !empty($eids) ) { + $total_size = 0; + foreach ( $eids as $eid ) { + if ( !validInt($eid) ) { + ZM\Warning("Invalid event id in eids[] $eid"); + continue; + } + $Event = new ZM\Event($eid); + $total_size += $Event->DiskSpace(); + $result .= ''.PHP_EOL; + } + unset($eid); + $result .= 'Downloading ' . count($eids) . ' events. Resulting file should be approximately ' . human_filesize($total_size).PHP_EOL; + } else { + $result .= '
There are no events found. Resulting download will be empty.
'; + } + + return $result; +} + +if ( !canView('Events') ) { + $view = 'error'; + return; +} + +$eid = isset($_REQUEST['eid']) ? $_REQUEST['eid'] : ''; +$eids = isset($_REQUEST['eids']) ? $_REQUEST['eids'] : array(); +$generated = isset($_REQUEST['generated']) ? $_REQUEST['generated'] : ''; + +$total_size = 0; +if ( isset($_SESSION['montageReviewFilter']) and !$eids ) { + # Handles montageReview filter + $eventsSql = 'SELECT E.Id, E.DiskSpace FROM Events AS E WHERE 1'; + $eventsSql .= $_SESSION['montageReviewFilter']['sql']; + $results = dbQuery($eventsSql); + while ( $event_row = dbFetchNext( $results ) ) { + array_push($eids, $event_row['Id']); + $total_size += $event_row['DiskSpace']; + } + if ( ! count($eids) ) { + ZM\Error("No events found for download using $eventsSql"); + } + #session_start(); + #unset($_SESSION['montageReviewFilter']); + #session_write_close(); +#} else { +#Debug("NO montageReviewFilter"); +} + +$exportFormat = ''; +if ( isset($_REQUEST['exportFormat']) ) { + if ( !in_array($_REQUEST['exportFormat'], array('zip', 'tar')) ) { + ZM\Error('Invalid exportFormat: '.$_REQUEST['exportFormat']); + } else { + $exportFormat = $_REQUEST['exportFormat']; + } +} + +$focusWindow = true; +$connkey = isset($_REQUEST['connkey']) ? validInt($_REQUEST['connkey']) : generateConnKey(); + +?> +
diff --git a/web/ajax/modals/logout.php b/web/ajax/modals/logout.php index 565e80cc2..a0648a70c 100644 --- a/web/ajax/modals/logout.php +++ b/web/ajax/modals/logout.php @@ -30,14 +30,73 @@ global $CLANG;