Merge branch 'master' into add_export_to_filter

This commit is contained in:
Isaac Connor 2018-07-25 17:06:49 -04:00
commit 6c1371fac7
13 changed files with 416 additions and 360 deletions

View File

@ -777,7 +777,7 @@ INSERT INTO `Controls` VALUES (NULL,'Reolink RLC-423','Ffmpeg','Reolink',0,0,1,1
INSERT INTO `Controls` VALUES (NULL,'Reolink RLC-411','Ffmpeg','Reolink',0,0,1,1,0,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'Reolink RLC-411','Ffmpeg','Reolink',0,0,1,1,0,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO `Controls` VALUES (NULL,'Reolink RLC-420','Ffmpeg','Reolink',0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'Reolink RLC-420','Ffmpeg','Reolink',0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO `Controls` VALUES (NULL,'D-LINK DCS-3415','Remote','DCS3415',0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'D-LINK DCS-3415','Remote','DCS3415',0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO `Controls` VALUES (NULL,'IOS Camera','Ffmpeg','IPCAMIOS',0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0);
-- --
-- Add some monitor preset values -- Add some monitor preset values
-- --

View File

@ -13,8 +13,8 @@ The API is built in CakePHP and lives under the ``/api`` directory. It
provides a RESTful service and supports CRUD (create, retrieve, update, delete) provides a RESTful service and supports CRUD (create, retrieve, update, delete)
functions for Monitors, Events, Frames, Zones and Config. functions for Monitors, Events, Frames, Zones and Config.
Security Login, Logout & API Security
^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The APIs tie into ZoneMinder's existing security model. This means if you have The APIs tie into ZoneMinder's existing security model. This means if you have
OPT_AUTH enabled, you need to log into ZoneMinder using the same browser you plan to OPT_AUTH enabled, you need to log into ZoneMinder using the same browser you plan to
use the APIs from. If you are developing an app that relies on the API, you need use the APIs from. If you are developing an app that relies on the API, you need
@ -23,12 +23,33 @@ to do a POST login from the app into ZoneMinder before you can access the API.
Then, you need to re-use the authentication information of the login (returned as cookie states) Then, you need to re-use the authentication information of the login (returned as cookie states)
with subsequent APIs for the authentication information to flow through to the APIs. with subsequent APIs for the authentication information to flow through to the APIs.
This means if you plan to use cuRL to experiment with these APIs, you first need to do This means if you plan to use cuRL to experiment with these APIs, you first need to login:
**Login process for ZoneMinder v1.32.0 and above**
::
curl -XPOST -d "user=XXXX&pass=YYYY" -c cookies.txt http://yourzmip/zm/api/login.json
Staring ZM 1.32.0, you also have a `logout` API that basically clears your session. It looks like this:
::
curl -b cookies.txt http://yourzmip/zm/api/logout.json
**Login process for older versions of ZoneMinder**
:: ::
curl -d "username=XXXX&password=YYYY&action=login&view=console" -c cookies.txt http://yourzmip/zm/index.php curl -d "username=XXXX&password=YYYY&action=login&view=console" -c cookies.txt http://yourzmip/zm/index.php
The equivalent logout process for older versions of ZoneMinder is:
::
curl -XPOST -d "username=XXXX&password=YYYY&action=logout&view=console" -b cookies.txt http://yourzmip/zm/index.php
replacing *XXXX* and *YYYY* with your username and password, respectively. replacing *XXXX* and *YYYY* with your username and password, respectively.
Please make sure you do this in a directory where you have write permissions, otherwise cookies.txt will not be created Please make sure you do this in a directory where you have write permissions, otherwise cookies.txt will not be created
@ -45,16 +66,46 @@ using CuRL like so:
This would return a list of monitors and pass on the authentication information to the ZM API layer. This would return a list of monitors and pass on the authentication information to the ZM API layer.
So remember, if you are using authentication, please add a ``-b cookies.txt`` to each of the commands below if you are using A deeper dive into the login process
CuRL. If you are not using CuRL and writing your own app, you need to make sure you pass on cookies to subsequent requests ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
in your app.
As you might have seen above, there are two ways to login, one that uses the `login.json` API and the other that logs in using the ZM portal. If you are running ZoneMinder 1.32.0 and above, it is *strongly* recommended you use the `login.json` approach. The "old" approach will still work but is not as powerful as the API based login. Here are the reasons why:
* The "old" approach basically uses the same login webpage (`index.php`) that a user would log into when viewing the ZM console. This is not really using an API and more importantly, if you have additional components like reCAPTCHA enabled, this will not work. Using the API approach is much cleaner and will work irrespective of reCAPTCHA
* The new login API returns important information that you can use to stream videos as well, right after login. Consider for example, a typical response to the login API (`/login.json`):
::
{
"credentials": "auth=f5b9cf48693fe8552503c8ABCD5",
"append_password": 0,
"version": "1.31.44",
"apiversion": "1.0"
}
In this example I have `OPT_AUTH` enabled in ZoneMinder and it returns my credential key. You can then use this key to stream images like so:
::
<img src="https://server/zm/cgi-bin/nph-zms?monitor=1&auth=<authval>" />
Where `authval` is the credentials returned to start streaming videos.
The `append_password` field will contain 1 when it is necessary for you to append your ZM password. This is the case when you set `AUTH_RELAY` in ZM options to "plain", for example. In that case, the `credentials` field may contain something like `&user=admin&pass=` and you have to add your password to that string.
.. NOTE:: It is recommended you invoke the `login` API once every 60 minutes to make sure the session stays alive. The same is true if you use the old login method too.
Examples (please read security notice above) Examples (please read security notice above)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
You will see each URL ending in either ``.xml`` or ``.json``. This is the Please remember, if you are using authentication, please add a ``-b cookies.txt`` to each of the commands below if you are using
format of the request, and it determines the format that any data returned to CuRL. If you are not using CuRL and writing your own app, you need to make sure you pass on cookies to subsequent requests
you will be in. I like json, however you can use xml if you'd like. in your app.
(In all examples, replace 'server' with IP or hostname & port where ZoneMinder is running) (In all examples, replace 'server' with IP or hostname & port where ZoneMinder is running)

View File

@ -72,10 +72,11 @@ class AppController extends Controller {
} }
// We need to reject methods that are not authenticated // We need to reject methods that are not authenticated
// besides login and logout // besides login and logout
if (strcasecmp($this->params->controller, "host") && if (
strcasecmp($this->params->action, "login") && strcasecmp($this->params->action, 'login')
strcasecmp($this->params->action,"logout")) { &&
strcasecmp($this->params->action,"logout")
) {
if ( !$this->Session->read('user.Username') ) { if ( !$this->Session->read('user.Username') ) {
throw new UnauthorizedException(__('Not Authenticated')); throw new UnauthorizedException(__('Not Authenticated'));
return; return;
@ -83,8 +84,6 @@ class AppController extends Controller {
throw new UnauthorizedException(__('User is not enabled')); throw new UnauthorizedException(__('User is not enabled'));
return; return;
} }
} }
} # end function beforeFilter() } # end function beforeFilter()
} }

View File

@ -228,7 +228,7 @@ class EventsController extends AppController {
foreach ($this->params['named'] as $param_name => $value) { foreach ($this->params['named'] as $param_name => $value) {
// Transform params into mysql // Transform params into mysql
if (preg_match("/interval/i", $value, $matches)) { if ( preg_match('/interval/i', $value, $matches) ) {
$condition = array("$param_name >= (date_sub(now(), $value))"); $condition = array("$param_name >= (date_sub(now(), $value))");
} else { } else {
$condition = array($param_name => $value); $condition = array($param_name => $value);
@ -254,9 +254,9 @@ class EventsController extends AppController {
$this->Event->recursive = -1; $this->Event->recursive = -1;
$results = array(); $results = array();
$moreconditions =""; $moreconditions = '';
foreach ($this->request->params['named'] as $name => $param) { foreach ($this->request->params['named'] as $name => $param) {
$moreconditions = $moreconditions . " AND ".$name.$param; $moreconditions = $moreconditions . ' AND '.$name.$param;
} }
$query = $this->Event->query("select MonitorId, COUNT(*) AS Count from Events WHERE (StartTime >= (DATE_SUB(NOW(), interval $interval)) $moreconditions) GROUP BY MonitorId;"); $query = $this->Event->query("select MonitorId, COUNT(*) AS Count from Events WHERE (StartTime >= (DATE_SUB(NOW(), interval $interval)) $moreconditions) GROUP BY MonitorId;");
@ -285,13 +285,13 @@ class EventsController extends AppController {
// Find the max Frame for this Event. Error out otherwise. // Find the max Frame for this Event. Error out otherwise.
$this->loadModel('Frame'); $this->loadModel('Frame');
if (! $frame = $this->Frame->find('first', array( if ( !( $frame = $this->Frame->find('first', array(
'conditions' => array( 'conditions' => array(
'EventId' => $event['Event']['Id'], 'EventId' => $event['Event']['Id'],
'Score' => $event['Event']['MaxScore'] 'Score' => $event['Event']['MaxScore']
) )
))) { ))) ) {
throw new NotFoundException(__("Can not find Frame for Event " . $event['Event']['Id'])); throw new NotFoundException(__('Can not find Frame for Event ' . $event['Event']['Id']));
} }
$this->loadModel('Config'); $this->loadModel('Config');
@ -304,7 +304,8 @@ class EventsController extends AppController {
$config = $this->Config->find('list', array( $config = $this->Config->find('list', array(
'conditions' => array('OR' => array( 'conditions' => array('OR' => array(
'Name' => array('ZM_WEB_LIST_THUMB_WIDTH', 'Name' => array(
'ZM_WEB_LIST_THUMB_WIDTH',
'ZM_WEB_LIST_THUMB_HEIGHT', 'ZM_WEB_LIST_THUMB_HEIGHT',
'ZM_EVENT_IMAGE_DIGITS', 'ZM_EVENT_IMAGE_DIGITS',
'ZM_DIR_IMAGES', 'ZM_DIR_IMAGES',
@ -382,7 +383,7 @@ class EventsController extends AppController {
'Score' => $event['Event']['MaxScore'] 'Score' => $event['Event']['MaxScore']
) )
))) { ))) {
throw new NotFoundException(__("Can not find Frame for Event " . $event['Event']['Id'])); throw new NotFoundException(__('Can not find Frame for Event ' . $event['Event']['Id']));
} }
return $frame['Frame']['Id']; return $frame['Frame']['Id'];
} }

View File

@ -39,8 +39,8 @@ class HostController extends AppController {
$zmOptAuth = $config['Config']['Value']; $zmOptAuth = $config['Config']['Value'];
if ( $zmOptAuth == '1' ) { if ( $zmOptAuth == '1' ) {
require_once "../../../includes/auth.php";
require_once "../../../includes/auth.php";
global $user; global $user;
$user = $this->Session->read('user'); $user = $this->Session->read('user');
@ -152,6 +152,8 @@ class HostController extends AppController {
$isZmAuth = $this->Config->find('first',array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_OPT_USE_AUTH')))['Config']['Value']; $isZmAuth = $this->Config->find('first',array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_OPT_USE_AUTH')))['Config']['Value'];
if ( $isZmAuth ) { if ( $isZmAuth ) {
require_once "../../../includes/auth.php"; # in the event we directly call getCredentials.json
$this->Session->read('user'); # this is needed for command line/curl to recognize a session
$zmAuthRelay = $this->Config->find('first',array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_AUTH_RELAY')))['Config']['Value']; $zmAuthRelay = $this->Config->find('first',array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_AUTH_RELAY')))['Config']['Value'];
if ( $zmAuthRelay == 'hashed' ) { if ( $zmAuthRelay == 'hashed' ) {
$zmAuthHashIps= $this->Config->find('first',array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_AUTH_HASH_IPS')))['Config']['Value']; $zmAuthHashIps= $this->Config->find('first',array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_AUTH_HASH_IPS')))['Config']['Value'];

View File

@ -18,6 +18,7 @@ class MonitorsController extends AppController {
public function beforeRender() { public function beforeRender() {
$this->set($this->Monitor->enumValues()); $this->set($this->Monitor->enumValues());
} }
public function beforeFilter() { public function beforeFilter() {
parent::beforeFilter(); parent::beforeFilter();
$canView = $this->Session->Read('monitorPermission'); $canView = $this->Session->Read('monitorPermission');
@ -164,7 +165,15 @@ class MonitorsController extends AppController {
$func = $Monitor['Function']; $func = $Monitor['Function'];
// We don't pass the request data as the monitor object because it may be a subset of the full monitor array // We don't pass the request data as the monitor object because it may be a subset of the full monitor array
$this->daemonControl($this->Monitor->id, 'stop'); $this->daemonControl($this->Monitor->id, 'stop');
if ( ( $func != 'None' ) and ( (!defined('ZM_SERVER_ID')) or ($Monitor['ServerId']==ZM_SERVER_ID) ) ) { if (
( $func != 'None' )
and
(
(!defined('ZM_SERVER_ID'))
or
($Monitor['ServerId']==ZM_SERVER_ID)
)
) {
$this->daemonControl($this->Monitor->id, 'start'); $this->daemonControl($this->Monitor->id, 'start');
} }
} else { } else {

View File

@ -96,4 +96,5 @@ class ZonePresetsController extends AppController {
} else { } else {
return $this->flash(__('The zone preset could not be deleted. Please, try again.'), array('action' => 'index')); return $this->flash(__('The zone preset could not be deleted. Please, try again.'), array('action' => 'index'));
} }
}} }
} // end class ZonePresetsController

View File

@ -17,12 +17,10 @@ public $components = array('RequestHandler');
public function beforeFilter() { public function beforeFilter() {
parent::beforeFilter(); parent::beforeFilter();
$canView = $this->Session->Read('monitorPermission'); $canView = $this->Session->Read('monitorPermission');
if ($canView =='None') if ( $canView =='None' ) {
{
throw new UnauthorizedException(__('Insufficient Privileges')); throw new UnauthorizedException(__('Insufficient Privileges'));
return; return;
} }
} }
// Find all zones which belong to a MonitorId // Find all zones which belong to a MonitorId
@ -40,16 +38,14 @@ public function forMonitor($id = null) {
'_serialize' => array('zones') '_serialize' => array('zones')
)); ));
} }
public function index() { public function index() {
$this->Zone->recursive = -1; $this->Zone->recursive = -1;
$allowedMonitors = preg_split('@,@', $this->Session->Read('allowedMonitors'), NULL, PREG_SPLIT_NO_EMPTY); $allowedMonitors = preg_split('@,@', $this->Session->Read('allowedMonitors'), NULL, PREG_SPLIT_NO_EMPTY);
if (!empty($allowedMonitors)) if ( !empty($allowedMonitors) ) {
{
$mon_options = array('Zones.MonitorId' => $allowedMonitors); $mon_options = array('Zones.MonitorId' => $allowedMonitors);
} } else {
else
{
$mon_options = ''; $mon_options = '';
} }
$zones = $this->Zone->find('all',$mon_options); $zones = $this->Zone->find('all',$mon_options);
@ -58,6 +54,7 @@ public function index() {
'_serialize' => array('zones') '_serialize' => array('zones')
)); ));
} }
/** /**
* add method * add method
* *
@ -119,8 +116,6 @@ public function index() {
} }
} }
public function createZoneImage($id = null) { public function createZoneImage($id = null) {
$this->loadModel('Monitor'); $this->loadModel('Monitor');
$this->Monitor->id = $id; $this->Monitor->id = $id;
@ -128,7 +123,6 @@ public function index() {
throw new NotFoundException(__('Invalid zone')); throw new NotFoundException(__('Invalid zone'));
} }
$this->loadModel('Config'); $this->loadModel('Config');
$zm_dir_images = $this->Config->find('list', array( $zm_dir_images = $this->Config->find('list', array(
'conditions' => array('Name' => 'ZM_DIR_IMAGES'), 'conditions' => array('Name' => 'ZM_DIR_IMAGES'),
@ -149,6 +143,5 @@ public function index() {
'status' => $status, 'status' => $status,
'_serialize' => array('status') '_serialize' => array('status')
)); ));
} }
} }

View File

@ -41,7 +41,7 @@ var popupSizes = {
'filter': { 'width': 900, 'height': 700 }, 'filter': { 'width': 900, 'height': 700 },
'frame': { 'addWidth': 32, 'minWidth': 384, 'addHeight': 200 }, 'frame': { 'addWidth': 32, 'minWidth': 384, 'addHeight': 200 },
'frames': { 'width': 600, 'height': 600 }, 'frames': { 'width': 600, 'height': 600 },
'function': { 'width': 350, 'height': 160 }, 'function': { 'width': 350, 'height': 260 },
'group': { 'width': 760, 'height': 600 }, 'group': { 'width': 760, 'height': 600 },
'groups': { 'width': 540, 'height': 420 }, 'groups': { 'width': 540, 'height': 420 },
'image': { 'addWidth': 48, 'addHeight': 80 }, 'image': { 'addWidth': 48, 'addHeight': 80 },
@ -54,7 +54,7 @@ var popupSizes = {
'monitorselect':{ 'width': 160, 'height': 200 }, 'monitorselect':{ 'width': 160, 'height': 200 },
'montage': { 'width': -1, 'height': -1 }, 'montage': { 'width': -1, 'height': -1 },
'onvifprobe': { 'width': 700, 'height': 550 }, 'onvifprobe': { 'width': 700, 'height': 550 },
'optionhelp': { 'width': 400, 'height': 320 }, 'optionhelp': { 'width': 400, 'height': 400 },
'options': { 'width': 1000, 'height': 660 }, 'options': { 'width': 1000, 'height': 660 },
'preset': { 'width': 300, 'height': 220 }, 'preset': { 'width': 300, 'height': 220 },
'server': { 'width': 600, 'height': 405 }, 'server': { 'width': 600, 'height': 405 },

View File

@ -260,8 +260,8 @@ foreach( array_map( 'basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI
<thead class="thead-highlight"> <thead class="thead-highlight">
<tr> <tr>
<th class="colId"><?php echo translate('Id') ?></th> <th class="colId"><?php echo translate('Id') ?></th>
<th class="colName"><?php echo translate('name') ?></th> <th class="colName"><?php echo translate('Name') ?></th>
<th class="colPath"><?php echo translate('path') ?></th> <th class="colPath"><?php echo translate('Path') ?></th>
<th class="colType"><?php echo translate('Type') ?></th> <th class="colType"><?php echo translate('Type') ?></th>
<th class="colScheme"><?php echo translate('StorageScheme') ?></th> <th class="colScheme"><?php echo translate('StorageScheme') ?></th>
<th class="colServer"><?php echo translate('Server') ?></th> <th class="colServer"><?php echo translate('Server') ?></th>