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 2079296f6..ce371586a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -808,7 +808,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) @@ -844,7 +844,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 092fce14f..401dac87c 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', @@ -537,6 +538,8 @@ CREATE TABLE `Monitors` ( `ArchivedEventDiskSpace` bigint default NULL, `ZoneCount` TINYINT NOT NULL DEFAULT 0, `Refresh` int(10) unsigned default NULL, + `Latitude` DECIMAL(10,8), + `Longitude` DECIMAL(10,8), PRIMARY KEY (`Id`) ) ENGINE=@ZM_MYSQL_ENGINE@; @@ -550,7 +553,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 @@ -1023,6 +1026,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.7.sql b/db/zm_update-1.35.7.sql new file mode 100644 index 000000000..84c3dbb1e --- /dev/null +++ b/db/zm_update-1.35.7.sql @@ -0,0 +1,23 @@ +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Monitors' + AND column_name = 'Latitude' + ) > 0, +"SELECT 'Column Latitude already exists in Monitors'", +"ALTER TABLE `Monitors` ADD `Latitude` DECIMAL(10,8) AFTER `Refresh`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Monitors' + AND column_name = 'Longitude' + ) > 0, +"SELECT 'Column Longitude already exists in Monitors'", +"ALTER TABLE `Monitors` ADD `Longitude` DECIMAL(10,8) AFTER `Latitude`" +)); + +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 8a03de245..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.6 +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 cf2b59c85..43fbcb457 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in +++ b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in @@ -479,6 +479,33 @@ our @options = ( type => $types{string}, category => 'system', }, + { + name => 'ZM_OPT_USE_GEOLOCATION', + description => 'Add geolocation features to ZoneMinder.', + help => 'Whether or not to enable Latitude/Longitude settings on Monitors and enable mapping options.', + type => $types{boolean}, + category => 'system', + }, + { + name => 'ZM_OPT_GEOLOCATION_TILE_PROVIDER', + description => 'Tile provider to use for maps.', + help => 'OpenStreetMaps does not itself provide the images to use in the map. There are many to choose from. Mapbox.com is one example that offers free tiles and has been tested during development of this feature.', + requires => [ + {name=>'ZM_OPT_USE_GEOLOCATION', value=>'yes'} + ], + type => $types{string}, + category => 'system', + }, + { + name => 'ZM_OPT_GEOLOCATION_ACCESS_TOKEN', + description => 'Access Token for the tile provider used for maps.', + help => 'OpenStreetMaps does not itself provide the images to use in the map. There are many to choose from. Mapbox.com is one example that offers free tiles and has been tested during development of this feature. You must go to mapbox.com and sign up and get an access token and cutnpaste it here.', + requires => [ + {name=>'ZM_OPT_USE_GEOLOCATION', value=>'yes'} + ], + type => $types{string}, + category => 'system', + }, { name => 'ZM_SYSTEM_SHUTDOWN', default => 'true', 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 f69723bb6..373fa0dff 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; @@ -109,9 +110,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); @@ -219,8 +222,8 @@ Event::Event( video_name = stringtf("%" PRIu64 "-%s.%s", id, "video", container.c_str()); 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; @@ -479,8 +482,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 ) { @@ -520,21 +524,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"); @@ -559,16 +566,15 @@ void Event::AddPacket(ZMPacket *packet, int score, Image *alarm_image) { } 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, @@ -576,14 +582,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 @@ -666,20 +675,18 @@ void Event::AddFrame(Image *image, struct timeval timestamp, int score, Image *a static char sql[ZM_SQL_MED_BUFSIZ]; 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); // The idea is to write out 1/sec frame_data.push(new Frame(id, frames, frame_type, timestamp, delta_time, score)); double fps = monitor->get_capture_fps(); - if ( write_to_db || ( fps && (frame_data.size() > fps) ) ) { - Debug(1, "Adding %d frames to DB because write_to_db:%d or frames > 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 > capture fps %f or BULK", frame_data.size(), write_to_db, 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?"":"-" ), @@ -703,5 +710,4 @@ void Event::AddFrame(Image *image, struct timeval timestamp, int score, Image *a } // end if db_frame end_time = timestamp; - } // end void Event::AddFrame(Image *image, struct timeval timestamp, int score, Image *alarm_image) 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_input.cpp b/src/zm_ffmpeg_input.cpp index bcf612a7b..92410fc63 100644 --- a/src/zm_ffmpeg_input.cpp +++ b/src/zm_ffmpeg_input.cpp @@ -20,7 +20,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; @@ -247,7 +247,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) ) { @@ -262,6 +262,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 4e4ef62f1..9636facd4 100644 --- a/src/zm_image.cpp +++ b/src/zm_image.cpp @@ -1859,10 +1859,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; @@ -1878,8 +1878,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 7095a1b5a..77cf80f3d 100644 --- a/src/zm_image.h +++ b/src/zm_image.h @@ -285,7 +285,7 @@ public: //Image *Delta( const Image &image ) const; void Delta( const Image &image, Image* targetimage) const; - const Coord centreCoord(const char *p_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 13a27600f..db23cfa48 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -280,7 +280,7 @@ Monitor::Monitor() //user //pass //path - //device + //device palette(0), channel(0), format(0), @@ -1766,9 +1766,7 @@ bool Monitor::Analyse() { int motion_score = last_motion_score; if ( !(analysis_image_count % (motion_frame_skip+1) ) ) { if ( snap->image ) { - // Get new score. - Debug(3, "before DetectMotion packet index is %d", snap->image_index); motion_score = DetectMotion(*snap_image, zoneSet); Debug(3, "After motion detection, last_motion_score(%d), new motion score(%d)", last_motion_score, motion_score); @@ -1780,11 +1778,9 @@ bool Monitor::Analyse() { } if ( motion_score ) { score += motion_score; - if ( !event ) { - if ( cause.length() ) - cause += ", "; - cause += MOTION_CAUSE; - } + 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 @@ -1887,7 +1883,6 @@ bool Monitor::Analyse() { ( timestamp->tv_sec - video_store_data->recording.tv_sec ), min_section_length ); } - if ( (!pre_event_count) || (Event::PreAlarmCount() >= alarm_frame_count-1) ) { // lets construct alarm cause. It will contain cause + names of zones alarmed std::string alarm_cause = ""; @@ -1914,7 +1909,7 @@ bool Monitor::Analyse() { Info("%s: %03d - Opening new event %" PRIu64 ", alarm start", name, image_count, event->Id()); } if ( alarm_frame_count ) { -Debug(1, "alarm frame count so SavePreAlarmFrames"); + Debug(1, "alarm frame count so SavePreAlarmFrames"); event->SavePreAlarmFrames(); } } else if ( state != PREALARM ) { @@ -1947,6 +1942,8 @@ Debug(1, "alarm frame count so SavePreAlarmFrames"); } else if ( state == PREALARM ) { // Back to IDLE shared_data->state = state = function != MOCORD ? IDLE : 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); } if ( Event::PreAlarmCount() ) Event::EmptyPreAlarmFrames(); diff --git a/src/zm_monitor.h b/src/zm_monitor.h index 2ca6bba42..792bf7c55 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -502,6 +502,8 @@ public: inline void setStartupTime( time_t p_time ) { shared_data->startup_time = p_time; } void get_ref_image(); + 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 67fcc0669..253e6a297 100644 --- a/src/zm_utils.cpp +++ b/src/zm_utils.cpp @@ -239,63 +239,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 { @@ -330,7 +298,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); @@ -372,7 +340,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 de2964283..b4add4cf7 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 21cbe96c2..a0b71b46f 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.35.6 +1.35.10 diff --git a/web/ajax/add_monitors.php b/web/ajax/add_monitors.php index aaa86790e..4a6809cf9 100644 --- a/web/ajax/add_monitors.php +++ b/web/ajax/add_monitors.php @@ -27,11 +27,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 ++ ) { @@ -41,7 +41,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/controlcaps.php b/web/ajax/controlcaps.php new file mode 100644 index 000000000..9e655c128 --- /dev/null +++ b/web/ajax/controlcaps.php @@ -0,0 +1,33 @@ + 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/delconfirm.php b/web/ajax/modals/delconfirm.php index 9aec739f4..02f742cff 100644 --- a/web/ajax/modals/delconfirm.php +++ b/web/ajax/modals/delconfirm.php @@ -1,18 +1,20 @@