mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-10-12 23:03:52 -04:00
zmc runs a monitor's EventStartCommand and EventEndCommand at the start and end of each event. Both the classic save and bulk edit actions and the API monitor add and edit authorized only on the monitor, so a user with edit on one monitor and no System permission could set a command for zmc to run as the ZoneMinder service user. Changing either field now needs System=Edit, as Filter::canEdit() already requires for AutoExecuteCmd. Monitor::commandFieldChangesNotAllowed() reports the command fields a change would alter; submitting the current value is not a change, so other users can still save the rest of the monitor. The monitor form shows the fields read-only to them. Also close the EventEndCommand list item. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
505 lines
17 KiB
PHP
505 lines
17 KiB
PHP
<?php
|
|
App::uses('AppController', 'Controller');
|
|
/**
|
|
* Monitors Controller
|
|
*
|
|
* @property Monitor $Monitor
|
|
* @property PaginatorComponent $Paginator
|
|
*/
|
|
class MonitorsController extends AppController {
|
|
|
|
/**
|
|
* Components
|
|
*
|
|
* @var array
|
|
*/
|
|
public $components = array('Paginator', 'RequestHandler');
|
|
|
|
public function beforeRender() {
|
|
$this->set($this->Monitor->enumValues());
|
|
}
|
|
|
|
public function beforeFilter() {
|
|
parent::beforeFilter();
|
|
}
|
|
|
|
/**
|
|
* index method
|
|
*
|
|
* @return void
|
|
*/
|
|
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']);
|
|
} else {
|
|
$conditions = array();
|
|
}
|
|
|
|
// Only join Groups_Monitors when the request actually filters by group, so
|
|
// a bare GroupId named param can resolve against that table. Joining it
|
|
// unconditionally multiplied rows for monitors in multiple groups, which we
|
|
// used to collapse with GROUP BY `Monitor`.`Id`. That GROUP BY fails under
|
|
// ONLY_FULL_GROUP_BY on engines without functional-dependency detection
|
|
// (e.g. MariaDB), which rejects the non-grouped SELECT columns. Refs #3633.
|
|
$group_filter = false;
|
|
foreach ($conditions as $key => $value) {
|
|
if (strpos((string)$key, 'GroupId') !== false) { $group_filter = true; break; }
|
|
if (is_array($value)) {
|
|
foreach ($value as $sub_key => $sub_value) {
|
|
if (strpos((string)$sub_key, 'GroupId') !== false) { $group_filter = true; break 2; }
|
|
}
|
|
}
|
|
}
|
|
|
|
$find_array = array(
|
|
'conditions' => &$conditions,
|
|
'contain' => array('Group'),
|
|
);
|
|
if ($group_filter) {
|
|
$find_array['joins'] = array(
|
|
array(
|
|
'table' => 'Groups_Monitors',
|
|
'type' => 'left',
|
|
'conditions' => array(
|
|
'Groups_Monitors.MonitorId = Monitor.Id',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
$monitors = $this->Monitor->find('all', $find_array);
|
|
$allowed_monitors = [];
|
|
$seen_monitor_ids = [];
|
|
require_once __DIR__ .'/../../../includes/Monitor.php';
|
|
foreach ($monitors as $m) {
|
|
// A monitor matching multiple GroupId values appears once per match;
|
|
// collapse those here rather than with a GROUP BY (see note above).
|
|
$monitor_id = $m['Monitor']['Id'];
|
|
if (isset($seen_monitor_ids[$monitor_id])) continue;
|
|
$seen_monitor_ids[$monitor_id] = true;
|
|
$monitor = new ZM\Monitor($m['Monitor']);
|
|
if (!$monitor->canView()) continue;
|
|
array_push($allowed_monitors, $m);
|
|
}
|
|
$this->set(array(
|
|
'monitors' => $allowed_monitors,
|
|
'_serialize' => array('monitors')
|
|
));
|
|
}
|
|
|
|
/**
|
|
* view method
|
|
*
|
|
* @throws NotFoundException
|
|
* @param string $id
|
|
* @return void
|
|
*/
|
|
public function view($id = null) {
|
|
$this->Monitor->recursive = 0;
|
|
if (!$this->Monitor->exists($id)) {
|
|
throw new NotFoundException(__('Invalid monitor'));
|
|
}
|
|
|
|
require_once __DIR__ .'/../../../includes/Monitor.php';
|
|
$options = array('conditions' => array( array('Monitor.'.$this->Monitor->primaryKey => $id)));
|
|
$monitor = $this->Monitor->find('first', $options);
|
|
$zm_monitor = new ZM\Monitor($monitor['Monitor']);
|
|
if (!$zm_monitor->canView()) {
|
|
throw new UnauthorizedException(__('Insufficient Privileges'));
|
|
return;
|
|
}
|
|
|
|
if ($zm_monitor->JanusEnabled()) {
|
|
$monitor['Monitor']['Janus_Pin'] = $zm_monitor->Janus_Pin();
|
|
}
|
|
$this->set(array(
|
|
'monitor' => $monitor,
|
|
'_serialize' => array('monitor')
|
|
));
|
|
}
|
|
|
|
/**
|
|
* add method
|
|
*
|
|
* @return void
|
|
*/
|
|
// The EventStartCommand / EventEndCommand values the request supplies, if any.
|
|
private function requestCommandFields() {
|
|
require_once __DIR__ .'/../../../includes/Monitor.php';
|
|
$fields = array();
|
|
foreach (ZM\Monitor::$commandFields as $field) {
|
|
$value = $this->requestField('Monitor', $field);
|
|
if ($value !== null) $fields[$field] = $value;
|
|
}
|
|
return $fields;
|
|
}
|
|
|
|
public function add() {
|
|
if ( $this->request->is('post') ) {
|
|
|
|
global $user;
|
|
$canAdd = (!$user) || ($user->System() == 'Edit' || $user->Monitors() == 'Create' );
|
|
if ( !$canAdd ) {
|
|
throw new UnauthorizedException(__('Insufficient privileges'));
|
|
return;
|
|
}
|
|
// A new monitor's command fields start empty, so any value is a change.
|
|
require_once __DIR__ .'/../../../includes/Monitor.php';
|
|
$new_monitor = new ZM\Monitor();
|
|
if (count($new_monitor->commandFieldChangesNotAllowed($this->requestCommandFields()))) {
|
|
throw new UnauthorizedException(__('Setting the event commands requires System edit permission'));
|
|
}
|
|
$this->pinRequestId($this->Monitor, null);
|
|
$this->Monitor->create();
|
|
if ($this->Monitor->save($this->request->data) ) {
|
|
$this->runDaemonControl($this->Monitor->id, 'start');
|
|
//return $this->flash(__('The monitor has been saved.'), array('action' => 'index'));
|
|
$message = 'Saved';
|
|
} else {
|
|
$message = 'Error';
|
|
// if there is a validation message, use it
|
|
if (!$this->Monitor->validates()) {
|
|
$message = $this->Monitor->validationErrors;
|
|
}
|
|
}
|
|
$this->set(array(
|
|
'message' => $message,
|
|
'_serialize' => array('message')
|
|
));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* edit method
|
|
*
|
|
* @throws NotFoundException
|
|
* @param string $id
|
|
* @return void
|
|
*/
|
|
public function edit($id = null) {
|
|
$this->Monitor->id = $id;
|
|
|
|
if ( !$this->Monitor->exists($id) ) {
|
|
throw new NotFoundException(__('Invalid monitor'));
|
|
}
|
|
|
|
$monitor = $this->Monitor->find('first', array(
|
|
'conditions' => array('Id' => $id)
|
|
))['Monitor'];
|
|
require_once __DIR__ .'/../../../includes/Monitor.php';
|
|
$zm_monitor = new ZM\Monitor($monitor);
|
|
if (!$zm_monitor->canEdit()) {
|
|
throw new UnauthorizedException(__('Insufficient Privileges'));
|
|
return;
|
|
}
|
|
if (count($zm_monitor->commandFieldChangesNotAllowed($this->requestCommandFields()))) {
|
|
throw new UnauthorizedException(__('Changing the event commands requires System edit permission'));
|
|
}
|
|
|
|
$this->pinRequestId($this->Monitor, $id);
|
|
$message = '';
|
|
if ($this->Monitor->save($this->request->data)) {
|
|
$message = 'Saved';
|
|
|
|
// Stop the monitor. Should happen before saving
|
|
$this->Monitor->daemonControl($monitor, 'stop');
|
|
$monitor = $this->Monitor->find('first', array(
|
|
'conditions' => array('Id' => $id)
|
|
))['Monitor'];
|
|
|
|
if ($monitor['Capturing'] != 'None')
|
|
$this->Monitor->daemonControl($monitor, 'start');
|
|
} else {
|
|
$message = 'Error ' . print_r($this->Monitor->invalidFields(), true);
|
|
}
|
|
|
|
$this->set(array(
|
|
'message' => $message,
|
|
'_serialize' => array('message')
|
|
));
|
|
|
|
} // end function edit
|
|
|
|
/**
|
|
* delete method
|
|
*
|
|
* @throws NotFoundException
|
|
* @param string $id
|
|
* @return void
|
|
*/
|
|
public function delete($id = null) {
|
|
$this->Monitor->id = $id;
|
|
if ( !$this->Monitor->exists() ) {
|
|
throw new NotFoundException(__('Invalid monitor'));
|
|
}
|
|
global $user;
|
|
$canEdit = (!$user) || ($user->System() == 'Edit');
|
|
if ( !$canEdit ) {
|
|
throw new UnauthorizedException(__('Insufficient privileges'));
|
|
return;
|
|
}
|
|
$this->request->allowMethod('post', 'delete');
|
|
|
|
// Delete the way the console does: ZM\Monitor::delete() stops zmc and
|
|
// zmcontrol and marks the monitor Deleted. A hard delete of the row left
|
|
// its events pointing at a monitor that no longer exists.
|
|
require_once __DIR__ .'/../../../includes/Monitor.php';
|
|
$monitor = new ZM\Monitor($id);
|
|
if ( $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;');
|
|
|
|
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')
|
|
));
|
|
}
|
|
|
|
// 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|disable
|
|
public function alarm() {
|
|
$id = $this->request->params['named']['id'];
|
|
if ( !$this->Monitor->exists($id) ) {
|
|
throw new NotFoundException(__('Invalid monitor'));
|
|
}
|
|
|
|
$cmd = strtolower($this->request->params['named']['command']);
|
|
switch ($cmd) {
|
|
case 'on':
|
|
$q = '-a';
|
|
$verbose = '-v';
|
|
break;
|
|
case 'off':
|
|
$q = '-c';
|
|
$verbose = '-v';
|
|
break;
|
|
case 'disable':
|
|
$q = '-n';
|
|
$verbose = '-v';
|
|
break;
|
|
case 'status':
|
|
$verbose = ''; // zmu has a bug - gives incorrect verbose output in this case
|
|
$q = '-s';
|
|
break;
|
|
default :
|
|
throw new BadRequestException(__('Invalid command'));
|
|
}
|
|
|
|
// zmu only checks that the user can see the monitor, so a monitor the user may only
|
|
// view could have its alarms forced or disabled. Changing alarm state needs edit.
|
|
require_once __DIR__ .'/../../../includes/Monitor.php';
|
|
$zm_monitor = new ZM\Monitor($id);
|
|
if (!(($cmd == 'status') ? $zm_monitor->canView() : $zm_monitor->canEdit())) {
|
|
throw new UnauthorizedException(__('Insufficient Privileges'));
|
|
}
|
|
|
|
// form auth key based on auth credentials
|
|
$auth = '';
|
|
|
|
if (ZM_OPT_USE_AUTH) {
|
|
global $user;
|
|
$mToken = $this->request->query('token') ? $this->request->query('token') : $this->request->data('token');;
|
|
if ($mToken) {
|
|
$auth = ' -T '.escapeshellarg($mToken);
|
|
} else if (ZM_AUTH_RELAY == 'hashed') {
|
|
$auth = ' -A '.calculateAuthHash(''); # Can't do REMOTE_IP because zmu doesn't normally have access to it.
|
|
} else if (ZM_AUTH_RELAY == 'plain') {
|
|
# Plain requires the plain text password which must either be in request or stored in session
|
|
$password = $this->request->query('pass') ? $this->request->query('pass') : $this->request->data('pass');;
|
|
if (!$password)
|
|
$password = $this->request->query('password') ? $this->request->query('password') : $this->request->data('password');
|
|
|
|
if (!$password) {
|
|
# during auth the session will have been populated with the plaintext password
|
|
$stateful = $this->request->query('stateful') ? $this->request->query('stateful') : $this->request->data('stateful');
|
|
if ($stateful) {
|
|
$password = $_SESSION['password'];
|
|
}
|
|
} else if ($_COOKIE['ZMSESSID']) {
|
|
$password = $_SESSION['password'];
|
|
}
|
|
|
|
$auth = ' -U '.escapeshellarg($user->Username()).' -P '.escapeshellarg($password);
|
|
} else if (ZM_AUTH_RELAY == 'none') {
|
|
$auth = ' -U '.escapeshellarg($user->Username());
|
|
}
|
|
}
|
|
|
|
$shellcmd = ZM_PATH_BIN.'/zmu'
|
|
.($verbose ? " $verbose" : '')
|
|
.' -m'.escapeshellarg($id)
|
|
." $q"
|
|
.$auth;
|
|
$status = exec($shellcmd, $output, $rc);
|
|
ZM\Debug("Command: $shellcmd output: ".implode(PHP_EOL, $output)." rc: $rc");
|
|
if ($rc) {
|
|
$this->set(array(
|
|
'status'=>'false',
|
|
'code' => $rc,
|
|
'error'=> implode(PHP_EOL, $output),
|
|
'_serialize' => array('status','code','error'),
|
|
));
|
|
} else if ($cmd == 'status') {
|
|
// In 1.36.16 the values got shifted up so that we could index into an array of strings.
|
|
// So do a hack to restore the previous behavour
|
|
$this->set(array(
|
|
'status' => intval($status)-1,
|
|
'output' => intval($output[0])-1,
|
|
'_serialize' => array('status','output'),
|
|
));
|
|
} else {
|
|
$this->set(array(
|
|
'status' => $status,
|
|
'output' => implode(PHP_EOL, $output),
|
|
'_serialize' => array('status','output'),
|
|
));
|
|
}
|
|
}
|
|
|
|
// Check if a daemon is running for the monitor id
|
|
/**
|
|
* Load a monitor the caller is allowed to act on.
|
|
*
|
|
* daemonStatus() and daemonControl() checked nothing at all, so any enabled,
|
|
* API-enabled account could read the state of, and start or stop, the daemons
|
|
* of a monitor it is not permitted even to see. index() and view() have always
|
|
* filtered on ZM\Monitor::canView(), so the two daemon actions were the odd
|
|
* ones out.
|
|
*
|
|
* @param int $id monitor id
|
|
* @param bool $for_edit whether the caller intends to change the monitor's state
|
|
* @throws NotFoundException|UnauthorizedException
|
|
* @return array the Monitor row
|
|
*/
|
|
private function monitorForDaemonAction($id, $for_edit) {
|
|
if (!$this->Monitor->exists($id)) {
|
|
throw new NotFoundException(__('Invalid monitor'));
|
|
}
|
|
require_once __DIR__ .'/../../../includes/Monitor.php';
|
|
$monitor = $this->Monitor->find('first', array(
|
|
'conditions' => array('Monitor.'.$this->Monitor->primaryKey => $id)
|
|
));
|
|
$zm_monitor = new ZM\Monitor($monitor['Monitor']);
|
|
# Reporting state needs view; starting or stopping capture is a change to
|
|
# the monitor, so it needs edit, the same as any other mutation here.
|
|
if ($for_edit ? !$zm_monitor->canEdit() : !$zm_monitor->canView()) {
|
|
throw new UnauthorizedException(__('Insufficient Privileges'));
|
|
}
|
|
return $monitor['Monitor'];
|
|
}
|
|
|
|
/**
|
|
* zmdc.pl takes the daemon and command as separate arguments, and they are
|
|
* interpolated into the command line, so both are restricted to known values
|
|
* rather than merely escaped.
|
|
*/
|
|
private function assertDaemonAndCommand($daemon, $command) {
|
|
$daemons = array('zmc', 'zma', 'zmfilter.pl', 'zmaudit.pl', 'zmtrigger.pl',
|
|
'zmwatch.pl', 'zmupdate.pl', 'zmtrack.pl', 'zmcontrol.pl', 'zmstats.pl', 'zmeventnotification.pl');
|
|
$commands = array('start', 'stop', 'restart', 'reload', 'status', 'check', 'logrot');
|
|
if (($daemon !== null) and !in_array($daemon, $daemons, true)) {
|
|
throw new BadRequestException(__('Invalid daemon'));
|
|
}
|
|
if (!in_array($command, $commands, true)) {
|
|
throw new BadRequestException(__('Invalid command'));
|
|
}
|
|
}
|
|
|
|
public function daemonStatus() {
|
|
$id = $this->request->params['named']['id'];
|
|
$daemon = $this->request->params['named']['daemon'];
|
|
|
|
$this->monitorForDaemonAction($id, false);
|
|
|
|
if (preg_match('/^[a-z]+$/i', $daemon) !== 1) {
|
|
throw new BadRequestException(__('Invalid command'));
|
|
}
|
|
|
|
$monitor = $this->Monitor->find('first', array(
|
|
'fields' => array('Id', 'Type', 'Device', 'Capturing'),
|
|
'conditions' => array('Id' => $id)
|
|
));
|
|
|
|
// Clean up the returned array
|
|
$monitor = Set::extract('/Monitor/.', $monitor);
|
|
if ($monitor[0]['Capturing'] == 'None') {
|
|
$this->set(array(
|
|
'status' => false,
|
|
'statustext' => 'Monitor capturing is set to None',
|
|
'_serialize' => array('status','statustext'),
|
|
));
|
|
return;
|
|
}
|
|
|
|
// Pass -d for local, otherwise -m
|
|
if ( $monitor[0]['Type'] == 'Local' ) {
|
|
$args = '-d '. escapeshellarg($monitor[0]['Device']);
|
|
} else {
|
|
$args = '-m '. escapeshellarg($monitor[0]['Id']);
|
|
}
|
|
|
|
// Build the command, and execute it
|
|
$command = escapeshellcmd(ZM_PATH_BIN."/zmdc.pl status $daemon $args");
|
|
$status = exec($command);
|
|
ZM\Debug("Command: $command output: $status");
|
|
|
|
// 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'),
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Stop or start a monitor's daemons, with no permission check of its own.
|
|
*
|
|
* Kept separate from the daemonControl() route so that delete(), which has
|
|
* already required System Edit, is not additionally required to pass the
|
|
* per-monitor edit check that the route applies. A System Edit account need not
|
|
* hold Monitors Edit, and re-gating here would have stopped it deleting.
|
|
*/
|
|
private function runDaemonControl($id, $command, $daemon=null) {
|
|
// Need to see if it is local or remote
|
|
$monitor = $this->Monitor->find('first', array(
|
|
'fields' => array('Id', 'Type', 'Capturing', 'Device', 'ServerId'),
|
|
'conditions' => array('Id' => $id)
|
|
));
|
|
return $this->Monitor->daemonControl($monitor['Monitor'], $command, $daemon);
|
|
}
|
|
|
|
public function daemonControl($id, $command, $daemon=null) {
|
|
$this->assertDaemonAndCommand($daemon, $command);
|
|
$this->monitorForDaemonAction($id, true);
|
|
$status_text = $this->runDaemonControl($id, $command, $daemon);
|
|
|
|
$this->set(array(
|
|
'status' => 'ok',
|
|
'statustext' => $status_text,
|
|
'_serialize' => array('status','statustext'),
|
|
));
|
|
} // end function daemonControl
|
|
} // end class MonitorsController
|