diff --git a/.gitmodules b/.gitmodules index 0fa8df21a..fc8ea09d6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = web/api/app/Plugin/Crud url = https://github.com/FriendsOfCake/crud.git branch = 3.0 +[submodule "web/api/app/Plugin/CakePHP-Enum-Behavior"] + path = web/api/app/Plugin/CakePHP-Enum-Behavior + url = https://github.com/asper/CakePHP-Enum-Behavior.git diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index 4314f87be..f9ac3b6d2 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -213,7 +213,8 @@ CREATE TABLE `Events` ( KEY `MonitorId` (`MonitorId`), KEY `StartTime` (`StartTime`), KEY `Frames` (`Frames`), - KEY `Archived` (`Archived`) + KEY `Archived` (`Archived`), + KEY `EndTime_DiskSpace` (`EndTime`,`DiskSpace`) ) ENGINE=@ZM_MYSQL_ENGINE@; -- @@ -277,7 +278,7 @@ CREATE TABLE `Groups` ( --Table structure for table `Groups_Monitors` -- -DROP TABLE IF EXISTS `Groups_Monitors` +DROP TABLE IF EXISTS `Groups_Monitors`; CREATE TABLE `Groups_Monitors` ( `Id` INT(10) unsigned NOT NULL auto_increment, `GroupId` int(10) unsigned NOT NULL, @@ -640,6 +641,7 @@ CREATE TABLE `Storage` ( `Type` enum('local','s3fs') NOT NULL default 'local', `DiskSpace` bigint unsigned default NULL, `Scheme enum('Deep','Medium','Shallow') NOT NULL default 'Medium', + `ServerId` int(10) unsigned, PRIMARY KEY (`Id`) ) ENGINE=@ZM_MYSQL_ENGINE@; @@ -668,7 +670,25 @@ insert into Users VALUES (NULL,'admin',password('admin'),'',1,'View','Edit','Edi -- -- Add a sample filter to purge the oldest 100 events when the disk is 95% full -- -insert into Filters values (NULL,'PurgeWhenFull','{"sort_field":"Id","terms":[{"val":0,"attr":"Archived","op":"="},{"cnj":"and","val":95,"attr":"DiskPercent","op":">="}],"limit":100,"sort_asc":1}',0,0,0,0,0,0,'',1,0,1,0); + `Id` int(10) unsigned NOT NULL auto_increment, + `Name` varchar(64) NOT NULL default '', + `Query` text NOT NULL, + `AutoArchive` 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', + `AutoMessage` tinyint(3) unsigned NOT NULL default '0', + `AutoExecute` tinyint(3) unsigned NOT NULL default '0', + `AutoExecuteCmd` tinytext, + `AutoDelete` tinyint(3) unsigned NOT NULL default '0', + `AutoMove` tinyint(3) unsigned NOT NULL default '0', + `AutoMoveTo` smallint(5) unsigned NOT NULL default 0, + `UpdateDiskSpace` tinyint(3) unsigned NOT NULL default '0', + `Background` tinyint(1) unsigned NOT NULL default '0', + `Concurrent` tinyint(1) unsigned NOT NULL default '0', + +insert into Filters values (NULL,'PurgeWhenFull','{"sort_field":"Id","terms":[{"val":0,"attr":"Archived","op":"="},{"cnj":"and","val":95,"attr":"DiskPercent","op":">="}],"limit":100,"sort_asc":1}',0/*AutoArchive*/,0/*AutoVideo*/,0/*AutoUpload*/,0/*AutoEmail*/,0/*AutoMessage*/,0/*AutoExecute*/,'',1/*AutoDelete*/,0/*AutoMove*/,0/*MoveTo*/,0/*UpdateDiskSpace*/,1/*Background*/,0/*Concurrent*/); +insert into Filters values (NULL,'Update DiskSpace','{"terms":[{"attr":"DiskSpace","op":"IS","val":"NULL"}]}',0,0,0,0,0,0,'',0,0,0,1,1,0); -- -- Add in some sample control protocol definitions diff --git a/db/zm_update-1.31.18.sql b/db/zm_update-1.31.18.sql new file mode 100644 index 000000000..5ba3b6caa --- /dev/null +++ b/db/zm_update-1.31.18.sql @@ -0,0 +1,23 @@ + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Storage' + AND column_name = 'ServerId' + ) > 0, + "SELECT 'Column ServerId already exists in Storage'", + "ALTER TABLE Storage ADD `ServerId` int(10) unsigned AFTER `Scheme`" + )); + +PREPARE stmt FROM @s; +EXECUTE stmt; + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM Filters WHERE Name = 'Update DiskSpace' + AND Query = '{"terms":[{"attr":"DiskSpace","op":"IS","val":"NULL"}]}' + ) > 0, + "SELECT 'Update Disk Space Filter already exists.'", + "INSERT INTO Filters (Name,Query,UpdateDiskSpace,Background) values ('Update DiskSpace','{\"terms\":[{\"attr\":\"DiskSpace\",\"op\":\"IS\",\"val\":\"NULL\"}]}',1,1)" + )); + +PREPARE stmt FROM @s; +EXECUTE stmt; diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Event.pm b/scripts/ZoneMinder/lib/ZoneMinder/Event.pm index e7fa9e296..1194ea56c 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Event.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Event.pm @@ -154,11 +154,21 @@ sub Path { if ( ! $$event{Path} ) { my $Storage = $event->Storage(); - + $$event{Path} = join('/', $Storage->Path(), $event->RelativePath() ); + } + return $$event{Path}; +} + +sub RelativePath { + my $event = shift; + if ( @_ ) { + $$event{RelativePath} = $_[0]; + } + + if ( ! $$event{RelativePath} ) { if ( $$event{Scheme} eq 'Deep' ) { if ( $event->Time() ) { - $$event{Path} = join('/', - $Storage->Path(), + $$event{RelativePath} = join('/', $event->{MonitorId}, strftime( '%y/%m/%d/%H/%M/%S', localtime($event->Time()) @@ -166,30 +176,28 @@ sub Path { ); } else { Error("Event $$event{Id} has no value for Time(), unable to determine path"); - $$event{Path} = ''; + $$event{RelativePath} = ''; } } elsif ( $$event{Scheme} eq 'Medium' ) { if ( $event->Time() ) { - $$event{Path} = join('/', - $Storage->Path(), + $$event{RelativePath} = join('/', $event->{MonitorId}, strftime( '%Y-%m-%d', localtime($event->Time())), $event->{Id}, ); } else { Error("Event $$event{Id} has no value for Time(), unable to determine path"); - $$event{Path} = ''; + $$event{RelativePath} = ''; } } else { # Shallow - $$event{Path} = join('/', - $Storage->Path(), + $$event{RelativePath} = join('/', $event->{MonitorId}, $event->{Id}, ); } # end if Scheme } # end if ! Path - return $$event{Path}; + return $$event{RelativePath}; } sub GenerateVideo { @@ -337,7 +345,7 @@ sub delete_files { return; } Debug("Deleting files for Event $$event{Id} from $storage_path."); - my $link_path = $$event{MonitorId}."/*/*/*/.".$$event{Id}; + my $link_path = $$event{MonitorId}.'/*/*/*/.'.$$event{Id}; #Debug( "LP1:$link_path" ); my @links = glob($link_path); #Debug( "L:".$links[0].": $!" ); @@ -371,7 +379,7 @@ sub delete_files { } } # end if links } else { - my $command = "/bin/rm -rf ". $event->Path(); + my $command = '/bin/rm -rf '. $storage_path . '/'. $event->RelativePath(); ZoneMinder::General::executeShellCommand( $command ); } } # end sub delete_files diff --git a/scripts/zmaudit.pl.in b/scripts/zmaudit.pl.in index 2de067c3e..e1c97e448 100644 --- a/scripts/zmaudit.pl.in +++ b/scripts/zmaudit.pl.in @@ -238,8 +238,13 @@ MAIN: while( $loop ) { } elsif ( $$Storage{Scheme} eq 'Medium' ) { foreach my $event_dir ( glob("$monitor_dir/*/*") ) { next if ! -d $event_dir; - my $Event = $fs_events->{$event} = new ZoneMinder::Event(); - $$Event{Id} = $event; + my ( $date, $event_id ) = $event_dir =~ /^$monitor_dir\/(\d{4}\-\d{2}\-\d{2})\/(\d\+)$/; + if ( ! $event_id ) { + Debug("Unable to parse date/event_id from $event_dir"); + next; + } + my $Event = $fs_events->{$event_id} = new ZoneMinder::Event(); + $$Event{Id} = $event_id; $$Event{Path} = $event_dir; $Event->MonitorId( $monitor_dir ); $Event->StorageId( $Storage->Id() ); diff --git a/version b/version index 874058d96..d1be1a9fa 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.31.17 +1.31.18 diff --git a/web/api/app/Config/bootstrap.php.in b/web/api/app/Config/bootstrap.php.in index cc096f233..4e2d99c63 100644 --- a/web/api/app/Config/bootstrap.php.in +++ b/web/api/app/Config/bootstrap.php.in @@ -70,6 +70,7 @@ Cache::config('default', array('engine' => 'Apc')); * */ CakePlugin::load('Crud'); +CakePlugin::load('CakePHP-Enum-Behavior'); /** * You can attach event listeners to the request lifecycle as Dispatcher Filter. By default CakePHP bundles two filters: @@ -142,6 +143,27 @@ foreach( $configvals as $key => $value) { Configure::write( $key, $value ); } +// For Human-readability, use ZM_SERVER_HOST or ZM_SERVER_NAME in zm.conf, and convert it here to a ZM_SERVER_ID +if ( ! defined('ZM_SERVER_ID') ) { + App::uses('ClassRegistry', 'Utility'); + $ServerModel = ClassRegistry::init('Server'); + if ( defined('ZM_SERVER_NAME') and ZM_SERVER_NAME ) { + $Server = $ServerModel->find( 'first', array( 'conditions'=>array('Name'=>ZM_SERVER_NAME) ) ); + if ( ! $Server ) { + Error('Invalid Multi-Server configration detected. ZM_SERVER_NAME set to ' . ZM_SERVER_NAME . ' in zm.conf, but no corresponding entry found in Servers table.'); + } else { + define( 'ZM_SERVER_ID', $Server['Server']['Id'] ); + } + } else if ( defined('ZM_SERVER_HOST') and ZM_SERVER_HOST ) { + $Server = $ServerModel->find( 'first', array( 'conditions'=>array('Name'=>ZM_SERVER_HOST) ) ); + if ( ! $Server ) { + Error('Invalid Multi-Server configration detected. ZM_SERVER_HOST set to ' . ZM_SERVER_HOST . ' in zm.conf, but no corresponding entry found in Servers table.'); + } else { + define( 'ZM_SERVER_ID', $Server['Server']['Id'] ); + } + } +} + function process_configfile($configFile) { if ( is_readable( $configFile ) ) { $configvals = array(); diff --git a/web/api/app/Controller/MonitorsController.php b/web/api/app/Controller/MonitorsController.php index af9cc8488..0880cc68e 100644 --- a/web/api/app/Controller/MonitorsController.php +++ b/web/api/app/Controller/MonitorsController.php @@ -13,12 +13,15 @@ class MonitorsController extends AppController { * * @var array */ - public $components = array('Paginator', 'RequestHandler'); + public $components = array('Paginator', 'RequestHandler'); + public function beforeRender() { + $this->set($this->Monitor->enumValues()); + } public function beforeFilter() { parent::beforeFilter(); $canView = $this->Session->Read('monitorPermission'); - if ($canView =='None') { + if ($canView == 'None') { throw new UnauthorizedException(__('Insufficient Privileges')); return; } @@ -29,21 +32,21 @@ class MonitorsController extends AppController { * * @return void */ - public function index() { + public function index() { $this->Monitor->recursive = 0; - if ($this->request->params['named']) { - $this->FilterComponent = $this->Components->load('Filter'); - //$conditions = $this->FilterComponent->buildFilter($this->request->params['named']); - $conditions = $this->request->params['named']; - } else { - $conditions = array(); - } + if ($this->request->params['named']) { + $this->FilterComponent = $this->Components->load('Filter'); + //$conditions = $this->FilterComponent->buildFilter($this->request->params['named']); + $conditions = $this->request->params['named']; + } else { + $conditions = array(); + } - $allowedMonitors=preg_split ('@,@', $this->Session->Read('allowedMonitors'),NULL, PREG_SPLIT_NO_EMPTY); - if (!empty($allowedMonitors)) { + $allowedMonitors=preg_split ('@,@', $this->Session->Read('allowedMonitors'),NULL, PREG_SPLIT_NO_EMPTY); + if (!empty($allowedMonitors)) { $conditions['Monitor.Id' ] = $allowedMonitors; - } + } $find_array = array('conditions'=>$conditions,'contain'=>array('Group')); if ( isset( $conditions['GroupId'] ) ) { @@ -70,7 +73,7 @@ class MonitorsController extends AppController { 'monitors' => $monitors, '_serialize' => array('monitors') )); - } + } /** * view method @@ -79,51 +82,57 @@ class MonitorsController extends AppController { * @param string $id * @return void */ - public function view($id = null) { - $this->Monitor->recursive = 0; - if (!$this->Monitor->exists($id)) { - throw new NotFoundException(__('Invalid monitor')); - } - $allowedMonitors=preg_split ('@,@', $this->Session->Read('allowedMonitors'),NULL, PREG_SPLIT_NO_EMPTY); - if (!empty($allowedMonitors)) { - $restricted = array('Monitor.' . $this->Monitor->primaryKey => $allowedMonitors); - } else { - $restricted = ''; - } - - $options = array('conditions' => array( - array('Monitor.' . $this->Monitor->primaryKey => $id), - $restricted - ) - ); - $monitor = $this->Monitor->find('first', $options); - $this->set(array( - 'monitor' => $monitor, - '_serialize' => array('monitor') - )); - } + public function view($id = null) { + $this->Monitor->recursive = 0; + if (!$this->Monitor->exists($id)) { + throw new NotFoundException(__('Invalid monitor')); + } + $allowedMonitors=preg_split ('@,@', $this->Session->Read('allowedMonitors'),NULL, PREG_SPLIT_NO_EMPTY); + if (!empty($allowedMonitors)) { + $restricted = array('Monitor.' . $this->Monitor->primaryKey => $allowedMonitors); + } else { + $restricted = ''; + } + + $options = array('conditions' => array( + array('Monitor.' . $this->Monitor->primaryKey => $id), + $restricted + ) + ); + $monitor = $this->Monitor->find('first', $options); + $this->set(array( + 'monitor' => $monitor, + '_serialize' => array('monitor') + )); + } /** * add method * * @return void */ - public function add() { - if ($this->request->is('post')) { + public function add() { + if ( $this->request->is('post') ) { - if ($this->Session->Read('systemPermission') != 'Edit') - { - throw new UnauthorizedException(__('Insufficient privileges')); - return; - } + if ( $this->Session->Read('systemPermission') != 'Edit' ) { + throw new UnauthorizedException(__('Insufficient privileges')); + return; + } - $this->Monitor->create(); - if ($this->Monitor->save($this->request->data)) { - $this->daemonControl($this->Monitor->id, 'start'); - return $this->flash(__('The monitor has been saved.'), array('action' => 'index')); - } - } - } + $this->Monitor->create(); + if ($this->Monitor->save($this->request->data)) { + $this->daemonControl($this->Monitor->id, 'start'); + //return $this->flash(__('The monitor has been saved.'), array('action' => 'index')); + $message = 'Saved'; + } else { + $message = 'Error'; + } + $this->set(array( + 'message' => $message, + '_serialize' => array('message') + )); + } + } /** * edit method @@ -132,40 +141,38 @@ class MonitorsController extends AppController { * @param string $id * @return void */ - public function edit($id = null) { - $this->Monitor->id = $id; + public function edit($id = null) { + $this->Monitor->id = $id; - if (!$this->Monitor->exists($id)) { - throw new NotFoundException(__('Invalid monitor')); - } - if ($this->Session->Read('monitorPermission') != 'Edit') - { - throw new UnauthorizedException(__('Insufficient privileges')); - return; - } - if ($this->Monitor->save($this->request->data)) { - $message = 'Saved'; - } else { - $message = 'Error'; - } + if (!$this->Monitor->exists($id)) { + throw new NotFoundException(__('Invalid monitor')); + } + if ($this->Session->Read('monitorPermission') != 'Edit') { + throw new UnauthorizedException(__('Insufficient privileges')); + return; + } + if ($this->Monitor->save($this->request->data)) { + $message = 'Saved'; + } else { + $message = 'Error'; + } - $this->set(array( - 'message' => $message, - '_serialize' => array('message') - )); + $this->set(array( + 'message' => $message, + '_serialize' => array('message') + )); - // - restart or stop this monitor after change + // - restart or stop this monitor after change $func = $this->Monitor->find('first', array( 'fields' => array('Function'), 'conditions' => array('Id' => $id) ))['Monitor']['Function']; // We don't pass the request data as the monitor object because it may be a subset of the full monitor array - if ( $func == 'None' ) { - $this->daemonControl( $this->Monitor->id, 'stop' ); - } else { - $this->daemonControl( $this->Monitor->id, 'restart' ); + $this->daemonControl( $this->Monitor->id, 'stop' ); + if ( ( $func != 'None' ) and ( $this->Monitor->ServerId == ZM_SERVER_ID ) ) { + $this->daemonControl( $this->Monitor->id, 'start' ); } - } + } // end function edit /** * delete method @@ -174,196 +181,183 @@ class MonitorsController extends AppController { * @param string $id * @return void */ - public function delete($id = null) { - $this->Monitor->id = $id; - if (!$this->Monitor->exists()) { - throw new NotFoundException(__('Invalid monitor')); - } - if ($this->Session->Read('systemPermission') != 'Edit') - { - throw new UnauthorizedException(__('Insufficient privileges')); - return; - } - $this->request->allowMethod('post', 'delete'); + public function delete($id = null) { + $this->Monitor->id = $id; + if (!$this->Monitor->exists()) { + throw new NotFoundException(__('Invalid monitor')); + } + if ($this->Session->Read('systemPermission') != 'Edit') { + throw new UnauthorizedException(__('Insufficient privileges')); + return; + } + $this->request->allowMethod('post', 'delete'); - $this->daemonControl($this->Monitor->id, 'stop'); + $this->daemonControl($this->Monitor->id, 'stop'); - if ($this->Monitor->delete()) { - return $this->flash(__('The monitor has been deleted.'), array('action' => 'index')); - } else { - return $this->flash(__('The monitor could not be deleted. Please, try again.'), array('action' => 'index')); - } - } + if ($this->Monitor->delete()) { + return $this->flash(__('The monitor has been deleted.'), array('action' => 'index')); + } else { + return $this->flash(__('The monitor could not be deleted. Please, try again.'), array('action' => 'index')); + } + } - public function sourceTypes() { - $sourceTypes = $this->Monitor->query("describe Monitors Type;"); + public function sourceTypes() { + $sourceTypes = $this->Monitor->query("describe Monitors Type;"); - preg_match('/^enum\((.*)\)$/', $sourceTypes[0]['COLUMNS']['Type'], $matches); - foreach( explode(',', $matches[1]) as $value ) { - $enum[] = trim( $value, "'" ); - } + preg_match('/^enum\((.*)\)$/', $sourceTypes[0]['COLUMNS']['Type'], $matches); + foreach( explode(',', $matches[1]) as $value ) { + $enum[] = trim( $value, "'" ); + } - $this->set(array( - 'sourceTypes' => $enum, - '_serialize' => array('sourceTypes') - )); - } + $this->set(array( + 'sourceTypes' => $enum, + '_serialize' => array('sourceTypes') + )); + } - // arm/disarm alarms - // expected format: http(s):/portal-api-url/monitors/alarm/id:M/command:C.json - // where M=monitorId - // where C=on|off|status - public function alarm() - { - $id = $this->request->params['named']['id']; - $cmd = strtolower($this->request->params['named']['command']); - if (!$this->Monitor->exists($id)) { - throw new NotFoundException(__('Invalid monitor')); - } - if ( $cmd != 'on' && $cmd != 'off' && $cmd != 'status') - { - throw new BadRequestException(__('Invalid command')); - } - $zm_path_bin = Configure::read('ZM_PATH_BIN'); + // arm/disarm alarms + // expected format: http(s):/portal-api-url/monitors/alarm/id:M/command:C.json + // where M=monitorId + // where C=on|off|status + public function alarm() { + $id = $this->request->params['named']['id']; + $cmd = strtolower($this->request->params['named']['command']); + if (!$this->Monitor->exists($id)) { + throw new NotFoundException(__('Invalid monitor')); + } + if ( $cmd != 'on' && $cmd != 'off' && $cmd != 'status' ) { + throw new BadRequestException(__('Invalid command')); + } + $zm_path_bin = Configure::read('ZM_PATH_BIN'); - switch ($cmd) - { - case "on": - $q = '-a'; - $verbose = "-v"; - break; - case "off": - $q = "-c"; - $verbose = "-v"; - break; - case "status": - $verbose = ""; // zmu has a bug - gives incorrect verbose output in this case - $q = "-s"; - break; - } + switch ($cmd) { + case 'on': + $q = '-a'; + $verbose = '-v'; + break; + case 'off': + $q = '-c'; + $verbose = '-v'; + break; + case 'status': + $verbose = ''; // zmu has a bug - gives incorrect verbose output in this case + $q = '-s'; + break; + } - // form auth key based on auth credentials - $this->loadModel('Config'); - $options = array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_OPT_USE_AUTH')); + // form auth key based on auth credentials + $this->loadModel('Config'); + $options = array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_OPT_USE_AUTH')); $config = $this->Config->find('first', $options); - $zmOptAuth = $config['Config']['Value']; + $zmOptAuth = $config['Config']['Value']; - $options = array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_AUTH_RELAY')); + $options = array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_AUTH_RELAY')); $config = $this->Config->find('first', $options); - $zmAuthRelay = $config['Config']['Value']; - - $auth=""; - if ($zmOptAuth) - { - if ($zmAuthRelay == 'hashed') - { - $options = array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_AUTH_HASH_SECRET')); - $config = $this->Config->find('first', $options); - $zmAuthHashSecret = $config['Config']['Value']; + $zmAuthRelay = $config['Config']['Value']; + + $auth=''; + if ( $zmOptAuth ) { + if ( $zmAuthRelay == 'hashed' ) { + $options = array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_AUTH_HASH_SECRET')); + $config = $this->Config->find('first', $options); + $zmAuthHashSecret = $config['Config']['Value']; - $time = localtime(); - $ak = $zmAuthHashSecret.$this->Session->Read('username').$this->Session->Read('passwordHash').$time[2].$time[3].$time[4].$time[5]; - $ak = md5($ak); - $auth = " -A ".$ak; - } - elseif ($zmAuthRelay == 'plain') - { - $auth = " -U " .$this->Session->Read('username')." -P ".$this->Session->Read('password'); - - } - elseif ($zmAuthRelay == 'none') - { - $auth = " -U " .$this->Session->Read('username'); - } - } - - $shellcmd = escapeshellcmd("$zm_path_bin/zmu $verbose -m$id $q $auth"); - $status = exec ($shellcmd); + $time = localtime(); + $ak = $zmAuthHashSecret.$this->Session->Read('username').$this->Session->Read('passwordHash').$time[2].$time[3].$time[4].$time[5]; + $ak = md5($ak); + $auth = ' -A '.$ak; + } else if ( $zmAuthRelay == 'plain' ) { + $auth = ' -U ' .$this->Session->Read('username').' -P '.$this->Session->Read('password'); + + } else if ( $zmAuthRelay == 'none' ) { + $auth = ' -U ' .$this->Session->Read('username'); + } + } + + $shellcmd = escapeshellcmd("$zm_path_bin/zmu $verbose -m$id $q $auth"); + $status = exec ($shellcmd); - $this->set(array( - 'status' => $status, - '_serialize' => array('status'), - )); + $this->set(array( + 'status' => $status, + '_serialize' => array('status'), + )); + } - - } + // Check if a daemon is running for the monitor id + public function daemonStatus() { + $id = $this->request->params['named']['id']; + $daemon = $this->request->params['named']['daemon']; - // Check if a daemon is running for the monitor id - public function daemonStatus() { - $id = $this->request->params['named']['id']; - $daemon = $this->request->params['named']['daemon']; + if (!$this->Monitor->exists($id)) { + throw new NotFoundException(__('Invalid monitor')); + } - if (!$this->Monitor->exists($id)) { - throw new NotFoundException(__('Invalid monitor')); - } + $monitor = $this->Monitor->find('first', array( + 'fields' => array('Id', 'Type', 'Device'), + 'conditions' => array('Id' => $id) + )); - $monitor = $this->Monitor->find('first', array( - 'fields' => array('Id', 'Type', 'Device'), - 'conditions' => array('Id' => $id) - )); + // Clean up the returned array + $monitor = Set::extract('/Monitor/.', $monitor); - // Clean up the returned array - $monitor = Set::extract('/Monitor/.', $monitor); + // Pass -d for local, otherwise -m + if ($monitor[0]['Type'] == 'Local') { + $args = '-d '. $monitor[0]['Device']; + } else { + $args = '-m '. $monitor[0]['Id']; + } - // Pass -d for local, otherwise -m - if ($monitor[0]['Type'] == 'Local') { - $args = "-d ". $monitor[0]['Device']; - } else { - $args = "-m ". $monitor[0]['Id']; - } + // Build the command, and execute it + $zm_path_bin = Configure::read('ZM_PATH_BIN'); + $command = escapeshellcmd("$zm_path_bin/zmdc.pl status $daemon $args"); + $status = exec( $command ); - // Build the command, and execute it - $zm_path_bin = Configure::read('ZM_PATH_BIN'); - $command = escapeshellcmd("$zm_path_bin/zmdc.pl status $daemon $args"); - $status = exec( $command ); + // If 'not' is present, the daemon is not running, so return false + // https://github.com/ZoneMinder/ZoneMinder/issues/799#issuecomment-108996075 + // Also sending back the status text so we can check if the monitor is in pending + // state which means there may be an error + $statustext = $status; + $status = (strpos($status, 'not')) ? false : true; - // If 'not' is present, the daemon is not running, so return false - // https://github.com/ZoneMinder/ZoneMinder/issues/799#issuecomment-108996075 - // Also sending back the status text so we can check if the monitor is in pending - // state which means there may be an error - $statustext = $status; - $status = (strpos($status, 'not')) ? false : true; + $this->set(array( + 'status' => $status, + 'statustext' => $statustext, + '_serialize' => array('status','statustext'), + )); + } - $this->set(array( - 'status' => $status, - 'statustext' => $statustext, - '_serialize' => array('status','statustext'), - )); - } + public function daemonControl($id, $command, $monitor=null, $daemon=null) { + $args = ''; + $daemons = array(); - public function daemonControl($id, $command, $monitor=null, $daemon=null) { - $args = ''; - $daemons = array(); + if (!$monitor) { + // Need to see if it is local or remote + $monitor = $this->Monitor->find('first', array( + 'fields' => array('Type', 'Function'), + 'conditions' => array('Id' => $id) + )); + $monitor = $monitor['Monitor']; + } - if (!$monitor) { - // Need to see if it is local or remote - $monitor = $this->Monitor->find('first', array( - 'fields' => array('Type', 'Function'), - 'conditions' => array('Id' => $id) - )); - $monitor = $monitor['Monitor']; - } + if ($monitor['Type'] == 'Local') { + $args = '-d ' . $monitor['Device']; + } else { + $args = '-m ' . $id; + } - if ($monitor['Type'] == 'Local') { - $args = "-d " . $monitor['Device']; - } else { - $args = "-m " . $id; - } + if ($monitor['Function'] == 'Monitor') { + array_push($daemons, 'zmc'); + } else { + array_push($daemons, 'zmc', 'zma'); + } + + $zm_path_bin = Configure::read('ZM_PATH_BIN'); - if ($monitor['Function'] == 'Monitor') { - array_push($daemons, 'zmc'); - } else { - array_push($daemons, 'zmc', 'zma'); - } - - $zm_path_bin = Configure::read('ZM_PATH_BIN'); - - foreach ($daemons as $daemon) { - $shellcmd = escapeshellcmd("$zm_path_bin/zmdc.pl $command $daemon $args"); - $status = exec( $shellcmd ); - } - } - -} + foreach ($daemons as $daemon) { + $shellcmd = escapeshellcmd("$zm_path_bin/zmdc.pl $command $daemon $args"); + $status = exec( $shellcmd ); + } + } +} // end class MonitorsController diff --git a/web/api/app/Model/Monitor.php b/web/api/app/Model/Monitor.php index 0504dd22a..c9697b694 100644 --- a/web/api/app/Model/Monitor.php +++ b/web/api/app/Model/Monitor.php @@ -86,10 +86,10 @@ class Monitor extends AppModel { ); /** - * * hasMany associations - * * - * * @var array - * */ + * hasMany associations + * + * @var array + */ public $hasAndBelongsToMany = array( 'Group' => array( 'className' => 'Group', @@ -108,5 +108,16 @@ class Monitor extends AppModel { 'counterQuery' => '' ), ); + public $actsAs = array( + 'CakePHP-Enum-Behavior.Enum' => array( + 'Type' => array('Local','Remote','File','Ffmpeg','Libvlc','cURL'), + 'Function' => array('None','Monitor','Modect','Record','Mocord','Nodect'), + 'Orientation' => array('0','90','180','270','hori','vert'), + 'OutputCodec' => array('h264','mjpeg','mpeg1','mpeg2'), + 'OutputContainer' => array('auto','mp4','mkv'), + 'DefaultView' => array('Events','Control'), + 'Status' => array('Unknown','NotRunning','Running','NoSignal','Signal'), + ) + ); } diff --git a/web/api/app/Plugin/CakePHP-Enum-Behavior b/web/api/app/Plugin/CakePHP-Enum-Behavior new file mode 160000 index 000000000..7108489f2 --- /dev/null +++ b/web/api/app/Plugin/CakePHP-Enum-Behavior @@ -0,0 +1 @@ +Subproject commit 7108489f218c54d36d235d3af91d6da2f8311237 diff --git a/web/api/app/View/Monitors/json/add.ctp b/web/api/app/View/Monitors/json/add.ctp new file mode 100644 index 000000000..77d2dd08b --- /dev/null +++ b/web/api/app/View/Monitors/json/add.ctp @@ -0,0 +1,2 @@ +echo json_encode($message); +echo json_encode($monitor); diff --git a/web/api/app/View/Monitors/xml/add.ctp b/web/api/app/View/Monitors/xml/add.ctp new file mode 100644 index 000000000..09fb8979a --- /dev/null +++ b/web/api/app/View/Monitors/xml/add.ctp @@ -0,0 +1,2 @@ +$xml = Xml::fromArray(array('response' => $message)); +echo $xml->asXML(); diff --git a/web/includes/Event.php b/web/includes/Event.php index ca05390bd..d55152b51 100644 --- a/web/includes/Event.php +++ b/web/includes/Event.php @@ -160,7 +160,16 @@ class Event { public function getStreamSrc( $args=array(), $querySep='&' ) { if ( $this->{'DefaultVideo'} and $args['mode'] != 'jpeg' ) { - return ( ZM_BASE_PATH != '/' ? ZM_BASE_PATH : '' ).'/index.php?view=view_video&eid='.$this->{'Id'}; + $streamSrc = ZM_BASE_PROTOCOL.'://'; + $Monitor = $this->Monitor(); + if ( $Monitor->ServerId() ) { + $Server = $Monitor->Server(); + $streamSrc .= $Server->Hostname(); + } else { + $streamSrc .= $_SERVER['HTTP_HOST']; + } + $streamSrc .= ( ZM_BASE_PATH != '/' ? ZM_BASE_PATH : '' ).'/index.php?view=view_video&eid='.$this->{'Id'}; + return $streamSrc; } $streamSrc = ZM_BASE_URL.ZM_PATH_ZMS; diff --git a/web/includes/Monitor.php b/web/includes/Monitor.php index 26b5518a4..9e226aa89 100644 --- a/web/includes/Monitor.php +++ b/web/includes/Monitor.php @@ -295,8 +295,8 @@ private $control_fields = array( } return $filters; } - public function save( $new_values = null ) { + public function save( $new_values = null ) { if ( $new_values ) { foreach ( $new_values as $k=>$v ) { @@ -304,8 +304,10 @@ private $control_fields = array( } } - $sql = 'UPDATE Monitors SET '.implode(', ', array_map( function($field) {return $field.'=?';}, array_keys( $this->defaults ) ) ) . ' WHERE Id=?'; - $values = array_map( function($field){return $this->{$field};}, $this->fields ); + $fields = array_keys( $this->defaults ); + + $sql = 'UPDATE Monitors SET '.implode(', ', array_map( function($field) {return $field.'=?';}, $fields ) ) . ' WHERE Id=?'; + $values = array_map( function($field){return $this->{$field};}, $fields ); $values[] = $this->{'Id'}; dbQuery( $sql, $values ); } // end function save diff --git a/web/includes/Storage.php b/web/includes/Storage.php index 448507379..945b8a464 100644 --- a/web/includes/Storage.php +++ b/web/includes/Storage.php @@ -70,15 +70,38 @@ class Storage { Warning( "Unknown function call Storage->$fn from $file:$line" ); } } - public static function find_all() { - $storage_areas = array(); - $result = dbQuery( 'SELECT * FROM Storage ORDER BY Name'); - $results = $result->fetchALL(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, 'Storage' ); - foreach ( $results as $row => $obj ) { - $storage_areas[] = $obj; - $storage_cache[$obj->Id()] = $obj; +public static function find_all( $parameters = null, $options = null ) { + $filters = array(); + $sql = 'SELECT * FROM Storage '; + $values = array(); + + if ( $parameters ) { + $fields = array(); + $sql .= 'WHERE '; + foreach ( $parameters as $field => $value ) { + if ( $value == null ) { + $fields[] = $field.' IS NULL'; + } else if ( is_array( $value ) ) { + $func = function(){return '?';}; + $fields[] = $field.' IN ('.implode(',', array_map( $func, $value ) ). ')'; + $values += $value; + + } else { + $fields[] = $field.'=?'; + $values[] = $value; + } + } + $sql .= implode(' AND ', $fields ); } - return $storage_areas; + if ( $options and isset($options['order']) ) { + $sql .= ' ORDER BY ' . $options['order']; + } + $result = dbQuery($sql, $values); + $results = $result->fetchALL(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, 'Storage'); + foreach ( $results as $row => $obj ) { + $filters[] = $obj; + } + return $filters; } public function disk_usage_percent() { $path = $this->Path(); @@ -95,11 +118,8 @@ class Storage { Error("disk_total_space returned false for " . $path ); return 0; } - $free = disk_free_space( $path ); - if ( ! $free ) { - Error("disk_free_space returned false for " . $path ); - } - $usage = round(($total - $free) / $total * 100); + $used = $this->disk_used_space(); + $usage = round( ($used / $total) * 100); return $usage; } public function disk_total_space() { diff --git a/web/includes/functions.php b/web/includes/functions.php index c908a054c..97c0f1919 100644 --- a/web/includes/functions.php +++ b/web/includes/functions.php @@ -1280,6 +1280,7 @@ function parseFilter( &$filter, $saveToSession=false, $querySep='&' ) { $filter['sql'] .= ' not regexp '.$value; break; case '=[]' : + case 'IN' : $filter['sql'] .= ' in ('.join( ',', $valueList ).')'; break; case '![]' : diff --git a/web/skins/classic/includes/functions.php b/web/skins/classic/includes/functions.php index 4a2304965..e6a0e1fe4 100644 --- a/web/skins/classic/includes/functions.php +++ b/web/skins/classic/includes/functions.php @@ -320,7 +320,10 @@ if ($reload == 'reload') ob_start(); } $func = function($S){ return ''.$S->Name() . ': ' . $S->disk_usage_percent().'%' . ''; }; #$func = function($S){ return ''.$S->Name() . ': ' . $S->disk_usage_percent().'%' . ''; }; - echo implode( ', ', array_map ( $func, $storage_areas ) ); + if ( count($storage_areas) >= 4 ) + $storage_areas = Storage::find_all( array('ServerId'=>null) ); + if ( count($storage_areas) < 4 ) + echo implode( ', ', array_map ( $func, $storage_areas ) ); echo ' ' . ZM_PATH_MAP .': '. getDiskPercent(ZM_PATH_MAP).'%'; ?> diff --git a/web/skins/classic/views/console.php b/web/skins/classic/views/console.php index 23d5549bd..8042d2aae 100644 --- a/web/skins/classic/views/console.php +++ b/web/skins/classic/views/console.php @@ -37,7 +37,7 @@ $eventCounts = array( 'filter' => array( 'Query' => array( 'terms' => array( - array( 'attr' => 'DateTime', 'op' => '>=', 'val' => '-1 hour' ), + array( 'attr' => 'StartDateTime', 'op' => '>=', 'val' => '-1 hour' ), ) ) ), diff --git a/web/skins/classic/views/js/montagereview.js b/web/skins/classic/views/js/montagereview.js index 0c0237f85..e1f854f66 100644 --- a/web/skins/classic/views/js/montagereview.js +++ b/web/skins/classic/views/js/montagereview.js @@ -55,7 +55,17 @@ function SetImageSource( monId, time ) { if ( eMonId[i] == monId && time >= eStartSecs[i] && time <= eEndSecs[i] ) { var duration = eEndSecs[i]-eStartSecs[i]; var frame = parseInt((time - eStartSecs[i])/(duration)*eventFrames[i])+1; -console.log("SetImageSource: " + time + " duration: " + duration + " frame: " + frame ); + var storage = Storage[eStorageId[i]]; + if ( storage.ServerId ) { + var server = Servers[storage.ServerId]; + if ( server ) { +console.log( server.Hostname + " for event " + eId[i] ); + return location.protocol + '//' + server.Hostname + '/index.php?view=image&eid=' + eId[i] + '&fid='+frame + "&width=" + monitorCanvasObj[monId].width + "&height=" + monitorCanvasObj[monId].height; + } else { + console.log("No server found for " + storage.ServerId ); + } + } + console.log("No storage found for " + eStorageId[i] ); return "index.php?view=image&eid=" + eId[i] + '&fid='+frame + "&width=" + monitorCanvasObj[monId].width + "&height=" + monitorCanvasObj[monId].height; } } // end for @@ -465,7 +475,6 @@ function setSpeed( speed_index ) { currentSpeed = parseFloat(speeds[speed_index]); speedIndex = speed_index; playSecsperInterval = Math.floor( 1000 * currentSpeed * currentDisplayInterval ) / 1000000; -console.log("playSecsPerInterval: " + playSecsperInterval + " = currentspeed:" + currentSpeed + " * " + currentDisplayInterval + " /1000"); showSpeed(speed_index); if ( timerInterval != currentDisplayInterval || currentSpeed == 0 ) timerFire(); // if the timer isn't firing we need to trigger it to update } diff --git a/web/skins/classic/views/js/montagereview.js.php b/web/skins/classic/views/js/montagereview.js.php index b1988ed86..19089a315 100644 --- a/web/skins/classic/views/js/montagereview.js.php +++ b/web/skins/classic/views/js/montagereview.js.php @@ -23,6 +23,7 @@ var imageLoadTimesNeeded=15; // and how many we need var timeLabelsFractOfRow = 0.9; var eMonId = []; var eId = []; +var eStorageId = []; var eStartSecs = []; var eEndSecs = []; var eventFrames = []; // this is going to presume all frames equal durationlength @@ -43,7 +44,6 @@ $index = 0; $anyAlarms = false; if ( ! $initialModeIsLive ) { -Warning($eventsSql); $result = dbQuery( $eventsSql ); if ( ! $result ) { Fatal('SQL-ERR'); @@ -56,6 +56,7 @@ Warning($eventsSql); if ( $maxTimeSecs < $event['CalcEndTimeSecs'] ) $maxTimeSecs = $event['CalcEndTimeSecs']; echo " eMonId[$index]=" . $event['MonitorId'] . "; +eStorageId[$index]=".$event['StorageId'] . "; eId[$index]=" . $event['Id'] . "; eStartSecs[$index]=" . $event['StartTimeSecs'] . "; eEndSecs[$index]=" . $event['CalcEndTimeSecs'] . "; @@ -146,7 +147,17 @@ if ( $mId > 0 ) { echo "var maxScore=$maxScore;\n"; // used to skip frame load if we find no alarms. } // end if initialmodeislive -echo "var monitorName = [];\n"; + +echo "var Storage = []\n"; +foreach ( Storage::find_all() as $Storage ) { +echo 'Storage[' . $Storage->Id() . '] = ' . json_encode($Storage). ";\n"; +} +echo "var Servers = []\n"; +foreach ( Server::find_all() as $Server ) { +echo 'Servers[' . $Server->Id() . '] = ' . json_encode($Server). ";\n"; +} +echo " +var monitorName = [];\n"; echo "var monitorLoading = [];\n"; echo "var monitorImageObject = [];\n"; echo "var monitorImageURL = [];\n"; diff --git a/web/skins/classic/views/montagereview.php b/web/skins/classic/views/montagereview.php index 3452b2f5f..56dadabd1 100644 --- a/web/skins/classic/views/montagereview.php +++ b/web/skins/classic/views/montagereview.php @@ -94,7 +94,7 @@ if (isset($_REQUEST['minTime']) && isset($_REQUEST['maxTime']) && count($display // Note we round up just a bit on the end time as otherwise you get gaps, like 59.78 to 00 in the next second, which can give blank frames when moved through slowly. $eventsSql = ' - SELECT E.Id,E.Name,UNIX_TIMESTAMP(E.StartTime) AS StartTimeSecs, + SELECT E.Id,E.Name,E.StorageId,UNIX_TIMESTAMP(E.StartTime) AS StartTimeSecs, CASE WHEN E.EndTime IS NULL THEN (SELECT UNIX_TIMESTAMP(DATE_ADD(E.StartTime, Interval max(Delta)+0.5 Second)) FROM Frames F WHERE F.EventId=E.Id) ELSE UNIX_TIMESTAMP(E.EndTime) END AS CalcEndTimeSecs, E.Length, diff --git a/web/skins/classic/views/options.php b/web/skins/classic/views/options.php index 443fe4337..2ca14f71d 100644 --- a/web/skins/classic/views/options.php +++ b/web/skins/classic/views/options.php @@ -194,7 +194,7 @@ foreach( array_map( 'basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI +} else if ( $tab == 'servers' ) { ?>