Files
zoneminder/web/includes/actions/monitor.php
IgorA100 dcbb7b65da - When changing the monitor name, also change the symlink, instead of simply deleting the old one.
- Condition adjustment. Changed from:
if (($saferName != $newMonitor['Name']) and !@symlink($mid, $link_path)) {
to:
if (($saferName != $newMonitor['Id']) and !@symlink($mid, $link_path)) {
This condition was changed in 203418d45e
But there was probably an error in the condition, since comparing one variable to itself is pointless, unless we're looking for prohibited characters in the name.
2026-05-03 00:58:00 +03:00

359 lines
14 KiB
PHP

<?php
//
// ZoneMinder web action file
// Copyright (C) 2019 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.
//
// Monitor edit actions, monitor id derived, require edit permissions for that monitor
require_once('includes/Monitor.php');
require_once('includes/Zone.php');
global $error_message;
if ($action == 'save') {
$mid = 0;
if (!empty($_REQUEST['mid'])) {
$mid = validInt($_REQUEST['mid']);
if (!canEdit('Monitors', $mid)) {
ZM\Warning('You do not have permission to edit this monitor');
return;
}
if (ZM_OPT_X10) {
$x10Monitor = dbFetchOne('SELECT * FROM TriggersX10 WHERE MonitorId=?', NULL, array($mid));
if (!$x10Monitor) $x10Monitor = array();
}
} else {
if (!canCreate('Monitors')) {
ZM\Warning('Monitor actions require Monitors Create Permissions');
return;
}
if ($user->unviewableMonitorIds()) {
ZM\Warning('You are restricted to certain monitors so cannot add a new one.');
return;
}
if (ZM_OPT_X10) {
$x10Monitor = array();
}
if (!empty($_REQUEST['newMonitor']['Id'])) {
// Reuse existing but deleted monitor
$mid = validCardinal($_REQUEST['newMonitor']['Id']);
}
}
# For convenience
$newMonitor = $_REQUEST['newMonitor'];
# Validate Device path to prevent command injection (CVE-worthy).
# Only Local monitors pass Device to a shell; for other Types the field
# is unused and may legitimately hold legacy values (e.g. an RTSP URL).
if (!empty($newMonitor['Device']) and isset($newMonitor['Type']) and $newMonitor['Type'] == 'Local') {
$newMonitor['Device'] = validDevicePath($newMonitor['Device']);
if ($newMonitor['Device'] === '') {
$error_message .= 'Invalid device path. Must be a valid /dev/ path (e.g. /dev/video0).</br>';
return;
}
}
if (!$newMonitor['ManufacturerId'] and ($newMonitor['Manufacturer'] != '')) {
# Need to add a new Manufacturer entry
$newManufacturer = ZM\Manufacturer::find_one(array('Name'=>$newMonitor['Manufacturer']));
if (!$newManufacturer) {
$newManufacturer = new ZM\Manufacturer();
if (!$newManufacturer->save(array('Name'=>$newMonitor['Manufacturer']))) {
$error_message .= "Error saving new Manufacturer: " . $newManufacturer->get_last_error().'</br>';
}
}
$newMonitor['ManufacturerId'] = $newManufacturer->Id();
unset($newMonitor['Manufacturer']);
}
if (!$newMonitor['ModelId'] and ($newMonitor['Model'] != '')) {
# Need to add a new Model entry
$newModel = ZM\Model::find_one(array('Name'=>$newMonitor['Model']));
if (!$newModel) {
$newModel = new ZM\Model();
if (!$newModel->save(array(
'Name'=>$newMonitor['Model'],
'ManufacturerId'=>$newMonitor['ManufacturerId']
))) {
$error_message .= "Error saving new Model: " . $newModel->get_last_error().'</br>';
}
}
$newMonitor['ModelId'] = $newModel->Id();
unset($newMonitor['Model']);
}
$monitor = new ZM\Monitor($mid);
// Define a field type for anything that's not simple text equivalent
$types = array(
'Triggers' => array(),
'Controllable' => 0,
'TrackMotion' => 0,
'ModectDuringPTZ' => 0,
'Enabled' => 0,
'Deleted' => 0,
'DecodingEnabled' => 0,
'RTSP2WebEnabled' => 0,
'Go2RTCEnabled' => 0,
'JanusEnabled' => 0,
'JanusAudioEnabled' => 0,
'Restream' => 0,
// 'Janus_RTSP_Session_Timeout' => 0,
'Exif' => 0,
'RTSPDescribe' => 0,
'V4LMultiBuffer' => '',
'WallClockTimestamps' => '',
'RecordAudio' => 0,
'Method' => 'raw',
'GroupIds' => array(),
'LinkedMonitors' => array(),
'MQTT_Enabled' => 0,
'RTSPServer' => 0,
'SectionLengthWarn' => 0,
'SOAP_wsa_compl' => 0
);
# Checkboxes don't return an element in the POST data, so won't be present in newMonitor.
# So force a value for these fields
foreach ($types as $field => $value) {
if (!isset($newMonitor[$field])) {
$newMonitor[$field] = $value;
}
} # end foreach type
if (isset($newMonitor['ServerId']) and ($newMonitor['ServerId'] == 'auto')) {
$newMonitor['ServerId'] = dbFetchOne(
'SELECT Id FROM Servers WHERE Status=\'Running\' ORDER BY FreeMem DESC, CpuLoad ASC LIMIT 1', 'Id');
ZM\Debug('Auto selecting server: Got ' . $newMonitor['ServerId']);
if ((!$newMonitor['ServerId']) and defined('ZM_SERVER_ID')) {
$newMonitor['ServerId'] = ZM_SERVER_ID;
ZM\Debug('Auto selecting server to '.ZM_SERVER_ID);
}
}
if (!empty($newMonitor['ManufacturerId']) and empty($newMonitor['Manufacturer']))
unset($newMonitor['Manufacturer']);
if (!empty($newMonitor['ModelId']) and empty($newMonitor['Model']))
unset($newMonitor['Model']);
$changes = $monitor->changes($newMonitor);
ZM\Debug('Changes: '. print_r($changes, true));
$restart = false;
if (count($changes)) {
// monitor->Id() has a value when the db record exists
if ($monitor->Id()) {
if ($monitor->Deleted() and ! isset($_REQUEST['newMonitor[Deleted]'])) {
# We are saving a new monitor with a specified Id and the Id is used in a deleted record.
# Undelete it so that it is visible.
$monitor->Deleted(false);
}
# If we change anything that changes the shared mem size, zma can complain. So let's stop first.
if ($monitor->Type() != 'WebSite') {
$monitor->zmcControl('stop');
if ($monitor->Controllable()) {
$monitor->sendControlCommand('stop');
}
}
$oldMonitor = clone $monitor;
if ($monitor->save($changes)) {
# Leave old symlinks on old storage areas, as old events will still be there. Only delete the link if the name has changed
if (isset($changes['Name'])) {
$link_path = $oldMonitor->Storage()->Path().'/'.basename($oldMonitor->Name());
if (file_exists($link_path)) {
ZM\Debug("Deleting old link ".$link_path);
unlink($link_path);
} else {
ZM\Debug("Old link didn't exist at ".$link_path);
}
$saferName = basename($newMonitor['Name']);
$link_path = $monitor->Storage()->Path().'/'.$saferName;
if (($saferName != $newMonitor['Id']) and !@symlink($mid, $link_path)) {
if (!(file_exists($link_path) and is_link($link_path))) {
ZM\Warning('Unable to symlink ' . $monitor->Storage()->Path().'/'.$mid . ' to ' . $link_path);
}
}
}
if (isset($changes['Width']) || isset($changes['Height'])) {
$newW = $newMonitor['Width'];
$newH = $newMonitor['Height'];
$zones = dbFetchAll('SELECT * FROM Zones WHERE MonitorId=?', NULL, array($mid));
// Zone coords are stored as percentages, so they don't need rescaling
// on resolution change. Only handle rotation (swap x/y) and rescale
// threshold pixel counts.
$newA = $newW * $newH;
$oldA = $oldMonitor->Width() * $oldMonitor->Height();
if ( ($newW == $oldMonitor->Height()) and ($newH == $oldMonitor->Width()) ) {
// Rotation: swap x,y percentage coords
foreach ( $zones as $zone ) {
$newZone = $zone;
$points = coordsToPoints($zone['Coords']);
for ( $i = 0; $i < count($points); $i++ ) {
$x = $points[$i]['x'];
$points[$i]['x'] = $points[$i]['y'];
$points[$i]['y'] = $x;
}
$newZone['Coords'] = pointsToCoords($points);
$changes = getFormChanges($zone, $newZone, $types);
if ( count($changes) ) {
dbQuery('UPDATE Zones SET '.implode(', ', $changes).' WHERE MonitorId=? AND Id=?',
array($mid, $zone['Id']));
}
} # end foreach zone
} else if ($oldA > 0 && $newA != $oldA) {
// Non-rotation resize: coords stay the same (percentages),
// but rescale threshold pixel counts by area ratio
foreach ( $zones as $zone ) {
$newZone = $zone;
$newZone['MinAlarmPixels'] = intval(round(($zone['MinAlarmPixels']*$newA)/$oldA));
$newZone['MaxAlarmPixels'] = intval(round(($zone['MaxAlarmPixels']*$newA)/$oldA));
$newZone['MinFilterPixels'] = intval(round(($zone['MinFilterPixels']*$newA)/$oldA));
$newZone['MaxFilterPixels'] = intval(round(($zone['MaxFilterPixels']*$newA)/$oldA));
$newZone['MinBlobPixels'] = intval(round(($zone['MinBlobPixels']*$newA)/$oldA));
$newZone['MaxBlobPixels'] = intval(round(($zone['MaxBlobPixels']*$newA)/$oldA));
$changes = getFormChanges($zone, $newZone, $types);
if ( count($changes) ) {
dbQuery('UPDATE Zones SET '.implode(', ', $changes).' WHERE MonitorId=? AND Id=?',
array($mid, $zone['Id']));
}
} // end foreach zone
} // end if rotation or just size change
} // end if changes in width or height
ZM\AuditAction('update', 'monitor', $mid, 'Changed: '.implode(', ', array_keys($changes)));
} else {
$error_message .= $monitor->get_last_error();
} // end if successful save
$restart = true;
} else { // new monitor
// Can only create new monitors if we are not restricted to specific monitors
# FIXME This is actually a race condition. Should lock the table.
$maxSeq = dbFetchOne('SELECT MAX(Sequence) AS MaxSequence FROM Monitors', 'MaxSequence');
$changes['Sequence'] = $maxSeq+1;
if ( $mid ) $changes['Id'] = $mid; # mid specified in request, doesn't exist in db, will re-use slot
if ( $monitor->insert($changes) ) {
$mid = $monitor->Id();
// Zone coords are now stored as percentages (0-100)
$zoneWidth = $newMonitor['Width'];
$zoneHeight = $newMonitor['Height'];
if (isset($newMonitor['Orientation']) &&
($newMonitor['Orientation'] == 'ROTATE_90' || $newMonitor['Orientation'] == 'ROTATE_270')) {
$zoneWidth = $newMonitor['Height'];
$zoneHeight = $newMonitor['Width'];
}
$zoneArea = $zoneWidth * $zoneHeight;
$zone = new ZM\Zone();
if (!$zone->save(['MonitorId'=>$monitor->Id(), 'Name'=>'All',
'Units'=>'Percent',
'Coords'=>'0.00,0.00 100.00,0.00 100.00,100.00 0.00,100.00',
'Area'=>10000,
'MinAlarmPixels'=>intval(($zoneArea*.05)/100),
'MaxAlarmPixels'=>intval(($zoneArea*75)/100),
'MinFilterPixels'=>intval(($zoneArea*.05)/100),
'MaxFilterPixels'=>intval(($zoneArea*75)/100),
'MinBlobPixels'=>intval(($zoneArea*.05)/100)
])) {
$error_message .= $zone->get_last_error();
ZM\Error('Error adding zone:' . $error_message);
}
ZM\AuditAction('create', 'monitor', $mid, 'Name: '.($newMonitor['Name'] ?? ''));
} else {
ZM\Error('Error saving new Monitor.');
return;
}
}
$Storage = $monitor->Storage();
$mid_dir = $Storage->Path().'/'.$mid;
if (!file_exists($mid_dir)) {
if (!@mkdir($mid_dir, 0755)) {
ZM\Error('Unable to mkdir '.$Storage->Path().'/'.$mid);
}
}
$saferName = basename($newMonitor['Name']);
$link_path = $Storage->Path().'/'.$saferName;
if (($saferName != $newMonitor['Id']) and !@symlink($mid, $link_path)) {
if (!(file_exists($link_path) and is_link($link_path))) {
ZM\Warning('Unable to symlink ' . $Storage->Path().'/'.$mid . ' to ' . $link_path);
}
}
if ( isset($changes['GroupIds']) ) {
dbQuery('DELETE FROM Groups_Monitors WHERE MonitorId=?', array($mid));
foreach ( $changes['GroupIds'] as $group_id ) {
dbQuery('INSERT INTO Groups_Monitors (GroupId, MonitorId) VALUES (?,?)', array($group_id, $mid));
}
} // end if there has been a change of groups
$restart = true;
} else {
ZM\Debug('No action due to no changes to Monitor');
} # end if count(changes)
if ( !$mid ) {
ZM\Error("We should have a mid by now. Something went wrong.");
return;
}
if ( ZM_OPT_X10 ) {
$x10Changes = getFormChanges($x10Monitor, $_REQUEST['newX10Monitor']);
if ( count($x10Changes) ) {
if ( $x10Monitor && isset($_REQUEST['newX10Monitor']) ) {
dbQuery('UPDATE TriggersX10 SET '.implode(', ', $x10Changes).' WHERE MonitorId=?', array($mid));
} else if ( !$user->unviewableMonitorIds() ) {
if ( !$x10Monitor ) {
dbQuery('INSERT INTO TriggersX10 SET MonitorId = ?, '.implode(', ', $x10Changes), array($mid));
} else {
dbQuery('DELETE FROM TriggersX10 WHERE MonitorId = ?', array($mid));
}
}
$restart = true;
} # end if has x10Changes
} # end if ZM_OPT_X10
if ( $restart ) {
if ( $monitor->Capturing() != 'None' and $monitor->Type() != 'WebSite' and !$monitor->Deleted()) {
$monitor->zmcControl('start');
if ( $monitor->Controllable() ) {
$monitor->sendControlCommand('start');
}
}
// really should thump zmwatch and maybe zmtrigger too.
//daemonControl( 'restart', 'zmwatch.pl' );
} // end if restart
if (!$error_message)
$redirect = '?view=console';
} else {
ZM\Warning("Unknown action $action in Monitor");
} // end if action == Delete
?>