Files
zoneminder/web/ajax/console.php
Isaac Connor 19cc1b89b6 fix: only show 'Use ONVIF' badge on console when listener is enabled
web/ajax/console.php was returning ONVIF_Alarm_Text under the
ONVIF_Event_Listener key whenever the column existed (always), so
console.js displayed "Use ONVIF 'MotionAlarm'" for every monitor
regardless of whether the listener was actually enabled.

Gate the alarm text on the actual ONVIF_Event_Listener boolean,
returning 0 otherwise so the JS falsy check works as intended.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:30:26 -04:00

696 lines
28 KiB
PHP

<?php
$message = '';
$data = array();
// Handle query task for bootstrap-table AJAX requests
if (!empty($_REQUEST['task'])) {
$task = $_REQUEST['task'];
if ($task == 'query') {
$data = queryRequest();
ajaxResponse($data);
return;
}
if ($task == 'export') {
exportMonitorsJSON();
return;
}
}
// Handle legacy action-based requests
if ( canEdit('Monitors') ) {
switch ( $_REQUEST['action'] ) {
case 'sort' :
{
$monitor_ids = $_POST['monitor_ids'];
# Two concurrent sorts could generate odd sorting... so lock the table.
global $dbConn;
$dbConn->beginTransaction();
$dbConn->exec('LOCK TABLES Monitors WRITE');
for ( $i = 0; $i < count($monitor_ids); $i += 1 ) {
$monitor_id = $monitor_ids[$i];
$monitor_id = preg_replace('/^monitor_id-/', '', $monitor_id);
if ( ( !$monitor_id ) or ! ( is_integer($monitor_id) or ctype_digit($monitor_id) ) ) {
Warning('Got '.$monitor_id.' from '.$monitor_ids[$i]);
continue;
}
dbQuery('UPDATE Monitors SET Sequence=? WHERE Id=?', array($i, $monitor_id));
} // end for each monitor_id
$dbConn->commit();
$dbConn->exec('UNLOCK TABLES');
return;
} // end case sort
default:
ZM\Warning('unknown action '.$_REQUEST['action']);
}
} else {
ZM\Warning('Cannot edit monitors');
}
ajaxError('Unrecognised action '.$_REQUEST['action'].' or insufficient permissions for user ' . $user->Username());
//
// FUNCTION DEFINITIONS
//
function queryRequest() {
global $user, $Servers;
require_once('includes/Monitor.php');
require_once('includes/Group.php');
require_once('includes/Group_Monitor.php');
require_once getSkinFile('views/_monitor_filters.php');
$data = array(
'total' => 0,
'totalNotFiltered' => 0,
'rows' => array()
);
// Get pagination parameters
$offset = 0;
if (isset($_REQUEST['offset']) and ($_REQUEST['offset'] != 'NaN')) {
if ((!is_int($_REQUEST['offset']) and !ctype_digit($_REQUEST['offset']))) {
ZM\Error('Invalid value for offset: ' . $_REQUEST['offset']);
} else {
$offset = $_REQUEST['offset'];
}
}
$limit = 0;
if (isset($_REQUEST['limit']) and ($_REQUEST['limit'] != 'NaN')) {
if ((!is_int($_REQUEST['limit']) and !ctype_digit($_REQUEST['limit']))) {
ZM\Error('Invalid value for limit: ' . $_REQUEST['limit']);
} else {
$limit = $_REQUEST['limit'];
}
}
// Get search parameter
$search = isset($_REQUEST['search']) ? $_REQUEST['search'] : '';
// Get sort parameters
$sort = isset($_REQUEST['sort']) ? $_REQUEST['sort'] : 'Sequence';
$order = isset($_REQUEST['order']) ? strtoupper($_REQUEST['order']) : 'ASC';
// Build monitor query with filters from request parameters, falling back to cookies
$conditions = array();
$values = array();
// Get filter values from request, falling back to cookies for persistence after page refresh.
// getFilterSelection() reads $_REQUEST first, then the zmFilter_* cookie.
$request_filters = array(
'GroupId' => getFilterSelection('GroupId'),
'ServerId' => getFilterSelection('ServerId'),
'StorageId' => getFilterSelection('StorageId'),
'Capturing' => getFilterSelection('Capturing'),
'Analysing' => getFilterSelection('Analysing'),
'Recording' => getFilterSelection('Recording'),
'Status' => getFilterSelection('Status'),
'MonitorId' => getFilterSelection('MonitorId'),
'MonitorName' => getFilterSelection('MonitorName'),
'Source' => getFilterSelection('Source')
);
// Text filters must be strings; guard against a cookie value that happens to be valid JSON.
if (is_array($request_filters['MonitorName'])) $request_filters['MonitorName'] = '';
if (is_array($request_filters['Source'])) $request_filters['Source'] = '';
// Apply GroupId filter using get_group_sql() to include child groups.
// Use validCardinal() to sanitize ID values before use.
if ($request_filters['GroupId']) {
$groupIds = is_array($request_filters['GroupId']) ? $request_filters['GroupId'] : array($request_filters['GroupId']);
$groupIds = array_values(array_filter(array_map('validCardinal', $groupIds)));
if (count($groupIds)) {
$groupSql = ZM\Group::get_group_sql($groupIds);
if ($groupSql) {
$conditions[] = $groupSql;
}
}
}
foreach (array('ServerId','StorageId') as $filter) {
if ($request_filters[$filter]) {
$filter_values = is_array($request_filters[$filter]) ? $request_filters[$filter] : array($request_filters[$filter]);
// Use validCardinal() to sanitize ID values
$filter_values = array_values(array_filter(array_map('validCardinal', $filter_values)));
if (count($filter_values)) {
$conditions[] = 'M.'.$filter.' IN (' . implode(',', array_fill(0, count($filter_values), '?')) . ')';
$values = array_merge($values, $filter_values);
}
}
}
foreach (array('Capturing','Analysing','Recording') as $filter) {
if ($request_filters[$filter]) {
$filter_values = is_array($request_filters[$filter]) ? $request_filters[$filter] : array($request_filters[$filter]);
if (count($filter_values)) {
$conditions[] = 'M.'.$filter.' IN (' . implode(',', array_fill(0, count($filter_values), '?')) . ')';
$values = array_merge($values, $filter_values);
}
}
}
if ($request_filters['Status']) {
$status_values = is_array($request_filters['Status']) ? $request_filters['Status'] : array($request_filters['Status']);
if (count($status_values)) {
$conditions[] = 'COALESCE(S.Status, IF(M.Type="WebSite","Running","NotRunning")) IN (' . implode(',', array_fill(0, count($status_values), '?')) . ')';
$values = array_merge($values, $status_values);
}
}
// Build SQL query
$sql = 'SELECT M.*, S.*, E.*, (SELECT Name FROM Manufacturers WHERE Manufacturers.Id=M.ManufacturerId) AS Manufacturer, (SELECT Name FROM Models where Models.Id=M.ModelId) AS Model
FROM Monitors AS M
LEFT JOIN Monitor_Status AS S ON S.MonitorId=M.Id
LEFT JOIN Event_Summaries AS E ON E.MonitorId=M.Id
WHERE M.`Deleted`=false';
if (count($conditions)) {
$sql .= ' AND ' . implode(' AND ', $conditions);
}
// Get total count before filtering
$monitors = dbFetchAll($sql, null, $values);
$unfiltered_monitors = array();
foreach ($monitors as $monitor) {
if (visibleMonitor($monitor['Id'])) {
$unfiltered_monitors[] = $monitor;
}
}
$data['totalNotFiltered'] = count($unfiltered_monitors);
// Apply search filter
$filtered_monitors = $unfiltered_monitors;
if ($search != '') {
$search_lower = strtolower($search);
$filtered_monitors = array_filter($unfiltered_monitors, function($monitor) use ($search_lower) {
// Search across common fields without creating Monitor object
return (
stripos($monitor['Name'], $search_lower) !== false ||
stripos($monitor['Function'], $search_lower) !== false ||
stripos($monitor['Path'], $search_lower) !== false ||
stripos($monitor['Device'], $search_lower) !== false ||
stripos($monitor['Host'], $search_lower) !== false ||
stripos($monitor['Id'], $search_lower) !== false ||
(isset($monitor['Status']) && stripos($monitor['Status'], $search_lower) !== false)
);
});
}
// Apply MonitorName and Source request filters
if ($request_filters['MonitorName']) {
$regexp = $request_filters['MonitorName'];
if (!strpos($regexp, '/')) $regexp = '/'.$regexp.'/i';
$filtered_monitors = array_filter($filtered_monitors, function($monitor) use ($regexp) {
return @preg_match($regexp, $monitor['Name']);
});
}
if ($request_filters['Source']) {
$regexp = $request_filters['Source'];
if (!preg_match("/^\/.+\/[a-z]*$/i", $regexp))
$regexp = '/'.$regexp.'/i';
$filtered_monitors = array_filter($filtered_monitors, function($monitor) use ($regexp) {
// Match against Path field directly instead of creating Monitor object
return (preg_match($regexp, $monitor['Path']) || preg_match($regexp, $monitor['Device']) || preg_match($regexp, $monitor['Host']));
});
}
// Apply MonitorId filter (use validCardinal() to sanitize ID values)
if ($request_filters['MonitorId']) {
$monitor_ids = is_array($request_filters['MonitorId']) ? $request_filters['MonitorId'] : array($request_filters['MonitorId']);
$monitor_ids = array_values(array_filter(array_map('validCardinal', $monitor_ids)));
if (count($monitor_ids)) {
$filtered_monitors = array_filter($filtered_monitors, function($monitor) use ($monitor_ids) {
return in_array($monitor['Id'], $monitor_ids);
});
}
}
$data['total'] = count($filtered_monitors);
// Sort monitors
usort($filtered_monitors, function($a, $b) use ($sort, $order) {
$aVal = isset($a[$sort]) ? $a[$sort] : '';
$bVal = isset($b[$sort]) ? $b[$sort] : '';
if (is_numeric($aVal) && is_numeric($bVal)) {
$result = $aVal - $bVal;
} else {
$result = strcasecmp($aVal, $bVal);
}
return $order == 'ASC' ? $result : -$result;
});
// Apply pagination
if ($limit > 0) {
$filtered_monitors = array_slice($filtered_monitors, $offset, $limit);
} else {
$filtered_monitors = array_slice($filtered_monitors, $offset);
}
// Get storage areas and servers
$storage_areas = ZM\Storage::find();
$StorageById = array();
foreach ($storage_areas as $S) {
$StorageById[$S->Id()] = $S;
}
$ServersById = array();
foreach ($Servers as $s) {
$ServersById[$s->Id()] = $s;
}
// Get group IDs for each monitor
$monitor_ids = array_map(function($m) { return $m['Id']; }, $filtered_monitors);
$group_ids_by_monitor_id = array();
if (count($monitor_ids)) {
foreach (ZM\Group_Monitor::find(array('MonitorId'=>$monitor_ids)) as $GM) {
if (!isset($group_ids_by_monitor_id[$GM->MonitorId()]))
$group_ids_by_monitor_id[$GM->MonitorId()] = array();
$group_ids_by_monitor_id[$GM->MonitorId()][] = $GM->GroupId();
}
}
// Process each monitor and build row data
$footer_totals = array(
'monitor_count' => count($filtered_monitors),
'total_bandwidth' => 0,
'total_fps' => 0,
'total_analysis_fps' => 0,
'total_zones' => 0,
'event_totals' => array(
'Total' => array('events' => 0, 'diskspace' => 0),
'Hour' => array('events' => 0, 'diskspace' => 0),
'Day' => array('events' => 0, 'diskspace' => 0),
'Week' => array('events' => 0, 'diskspace' => 0),
'Month' => array('events' => 0, 'diskspace' => 0),
'Archived' => array('events' => 0, 'diskspace' => 0)
)
);
foreach ($filtered_monitors as $monitor) {
$Monitor = new ZM\Monitor($monitor);
$Monitor->GroupIds(isset($group_ids_by_monitor_id[$Monitor->Id()]) ? $group_ids_by_monitor_id[$Monitor->Id()] : array());
// Accumulate footer totals
$footer_totals['total_bandwidth'] += isset($monitor['CaptureBandwidth']) ? $monitor['CaptureBandwidth'] : 0;
$footer_totals['total_fps'] += isset($monitor['CaptureFPS']) ? floatval($monitor['CaptureFPS']) : 0;
$footer_totals['total_analysis_fps'] += isset($monitor['AnalysisFPS']) ? floatval($monitor['AnalysisFPS']) : 0;
$footer_totals['total_zones'] += isset($monitor['ZoneCount']) ? intval($monitor['ZoneCount']) : 0;
foreach (array('Total', 'Hour', 'Day', 'Week', 'Month', 'Archived') as $period) {
$footer_totals['event_totals'][$period]['events'] += isset($monitor[$period.'Events']) ? intval($monitor[$period.'Events']) : 0;
$footer_totals['event_totals'][$period]['diskspace'] += isset($monitor[$period.'EventDiskSpace']) ? intval($monitor[$period.'EventDiskSpace']) : 0;
}
$row = array();
$row['Id'] = $monitor['Id'];
$row['Name'] = validHtmlStr($monitor['Name']);
$row['Function'] = $monitor['Function'];
$row['Enabled'] = $monitor['Enabled'];
$row['Manufacturer'] = $monitor['Manufacturer'];
$row['Model'] = $monitor['Model'];
$row['Sequence'] = isset($monitor['Sequence']) ? $monitor['Sequence'] : 0;
// Status
if (!$monitor['Status']) {
if ($monitor['Type'] == 'WebSite')
$monitor['Status'] = 'Running';
else
$monitor['Status'] = 'NotRunning';
}
$row['Status'] = $monitor['Status'];
// Server
if (count($Servers)) {
$Server = isset($ServersById[$monitor['ServerId']]) ? $ServersById[$monitor['ServerId']] : new ZM\Server($monitor['ServerId']);
$row['Server'] = validHtmlStr($Server->Name());
$row['ServerId'] = $monitor['ServerId'];
}
// Source
$row['Source'] = validHtmlStr($Monitor->Source());
$row['Width'] = $Monitor->Width();
$row['Height'] = $Monitor->Height();
// Storage
if (isset($StorageById[$monitor['StorageId']])) {
$row['Storage'] = validHtmlStr($StorageById[$monitor['StorageId']]->Name());
} else if ($monitor['StorageId']) {
$row['Storage'] = '<span class="error">Deleted '.$monitor['StorageId'].'</span>';
} else {
$row['Storage'] = '';
}
// Event counts with filter querystrings
$eventCounts = array(
'Total' => array(
'filter' => array('Query' => array('terms' => array()))
),
'Hour' => array(
'filter' => array('Query' => array('terms' => array(
array('cnj' => 'and', 'attr' => 'StartDateTime', 'op' => '>=', 'val' => '-1 hour')
)))
),
'Day' => array(
'filter' => array('Query' => array('terms' => array(
array('cnj' => 'and', 'attr' => 'StartDateTime', 'op' => '>=', 'val' => '-1 day')
)))
),
'Week' => array(
'filter' => array('Query' => array('terms' => array(
array('cnj' => 'and', 'attr' => 'StartDateTime', 'op' => '>=', 'val' => '-7 day')
)))
),
'Month' => array(
'filter' => array('Query' => array('terms' => array(
array('cnj' => 'and', 'attr' => 'StartDateTime', 'op' => '>=', 'val' => '-1 month')
)))
),
'Archived' => array(
'filter' => array('Query' => array('terms' => array(
array('cnj' => 'and', 'attr' => 'Archived', 'op' => '=', 'val' => '1')
)))
)
);
foreach ($eventCounts as $period => $eventCount) {
$row[$period.'Events'] = (int)$monitor[$period.'Events'];
$row[$period.'EventDiskSpace'] = human_filesize($monitor[$period.'EventDiskSpace']);
// Generate filter querystring for this period and monitor
$filter = addFilterTerm(
$eventCount['filter'],
count($eventCount['filter']['Query']['terms']),
array('cnj' => 'and', 'attr' => 'Monitor', 'op' => '=', 'val' => $monitor['Id'])
);
parseFilter($filter);
$row[$period.'FilterQuery'] = $filter['querystring'];
}
// Zone count
$row['ZoneCount'] = $monitor['ZoneCount'];
// FPS and bandwidth
$row['CaptureFPS'] = isset($monitor['CaptureFPS']) ? $monitor['CaptureFPS'] : '0.00';
$row['AnalysisFPS'] = isset($monitor['AnalysisFPS']) ? $monitor['AnalysisFPS'] : '0.00';
// Format bandwidth with units (bytes per second) - use human_filesize for consistency
$bandwidth = isset($monitor['CaptureBandwidth']) ? $monitor['CaptureBandwidth'] : 0;
if ($bandwidth > 0) {
$row['CaptureBandwidth'] = human_filesize($bandwidth).'/s';
} else {
$row['CaptureBandwidth'] = '';
}
$row['Analysing'] = isset($monitor['Analysing']) ? $monitor['Analysing'] : 'None';
$row['Recording'] = isset($monitor['Recording']) ? $monitor['Recording'] : 'None';
// console.js treats this as both an enable flag AND the text to display:
// if (row.ONVIF_Event_Listener) html += "Use ONVIF '" + row.ONVIF_Event_Listener + "'"
// So send the alarm text only when the listener is actually enabled, else 0.
$row['ONVIF_Event_Listener'] = !empty($monitor['ONVIF_Event_Listener']) ? $monitor['ONVIF_Alarm_Text'] : 0;
$row['UpdatedOn'] = isset($monitor['UpdatedOn']) ? $monitor['UpdatedOn'] : '';
$row['Type'] = $monitor['Type'];
$row['Capturing'] = isset($monitor['Capturing']) ? $monitor['Capturing'] : 'None';
// Groups
if (canView('Groups')) {
$groups_html = implode('<br/>',
array_map(function($group_id) {
$Group = ZM\Group::find_one(array('Id'=>$group_id));
if ($Group) {
$Groups = $Group->Parents();
array_push($Groups, $Group);
} else {
$Groups = array();
}
return implode(' &gt; ', array_map(function($Group) {
if (canView('Stream')) {
return '<a href="?view=montagereview&amp;GroupId='.$Group->Id().'">'.validHtmlStr($Group->Name()).'</a>';
} else {
return validHtmlStr($Group->Name());
}
}, $Groups));
}, $Monitor->GroupIds())
);
$row['Groups'] = $groups_html;
} else {
$row['Groups'] = '';
}
// Thumbnail
$row['Thumbnail'] = '';
if (ZM_WEB_LIST_THUMBS && ($monitor['Capturing'] != 'None') && canView('Stream')) {
$ratio_factor = $Monitor->ViewWidth() ? $Monitor->ViewHeight() / $Monitor->ViewWidth() : 1;
$options = array(
'width' => ZM_WEB_LIST_THUMB_WIDTH,
'height' => ZM_WEB_LIST_THUMB_HEIGHT ? ZM_WEB_LIST_THUMB_HEIGHT : ZM_WEB_LIST_THUMB_WIDTH * $ratio_factor,
'scale' => $Monitor->ViewWidth() ? intval(100 * ZM_WEB_LIST_THUMB_WIDTH / $Monitor->ViewWidth()) : 100,
'mode' => 'jpeg',
'frames' => 1,
);
$stillSrc = $Monitor->getStreamSrc($options);
// Calculate optimal scale for the popup stream based on browser width
// The JS overlay uses 60% of window.innerWidth (see calculateOverlayDimensions in skin.js)
$browser_width = 1920; // Default fallback
if (isset($_COOKIE['zmBrowserSizes'])) {
$zmBrowserSizes = jsonDecode($_COOKIE['zmBrowserSizes']);
if (!empty($zmBrowserSizes['innerWidth'])) {
$browser_width = validInt($zmBrowserSizes['innerWidth']);
}
}
$target_width = $browser_width * 0.6; // Match JS overlay sizing (60% of viewport)
$options['scale'] = $Monitor->ViewWidth() ? intval(100 * $target_width / $Monitor->ViewWidth()) : 100;
if ($options['scale'] > 100) $options['scale'] = 100;
else if ($options['scale'] < 10) $options['scale'] = 10;
unset($options['frames']);
$streamSrc = $Monitor->getStreamSrc($options);
$videoAttr = '';
$go2rtcAttr = '';
$liveStreamAttr = '';
// Debug info for troubleshooting
$debugInfo = 'Analysing='.$monitor['Analysing'].',Decoding='.$monitor['Decoding'].
',Go2RTC='.$Monitor->Go2RTCEnabled().
',ZM_GO2RTC_PATH='.(defined('ZM_GO2RTC_PATH') ? ZM_GO2RTC_PATH : 'undefined');
$debugAttr = ' data-debug="'.htmlspecialchars($debugInfo).'"';
// STILL THUMBNAIL: When not analysing/decoding, use the most recent event thumbnail
// (ZM's nph-zms can't produce frames when not decoding)
if ($monitor['Analysing'] == 'None' && $monitor['Decoding'] == 'None') {
$debugAttr .= ' data-debug-still="event"';
$event = ZM\Event::find_one(
array(
'MonitorId' => $monitor['Id'],
'EndDateTime' => 'NOT NULL'
),
array('order' => 'Id DESC')
);
if ($event) {
$stillSrc = $event->getThumbnailSrc(array(), '&amp;');
// Default stream/video sources from event (may be overridden by live streaming below)
// Use browser width for optimal scale (same as live stream calculation above)
$event_scale = $event->Width() ? intval(100 * $target_width / $event->Width()) : 100;
if ($event_scale > 100) $event_scale = 100;
else if ($event_scale < 10) $event_scale = 10;
$streamSrc = $event->getStreamSrc(array(
'mode' => 'jpeg', 'scale' => $event_scale, 'maxfps' => ZM_WEB_VIDEO_MAXFPS,
'replay' => 'single', 'rate' => '400'), '&amp;');
if ($event->DefaultVideo()) {
$videoSrc = $event->getStreamSrc(array('mode' => 'mp4'), '&amp;');
$videoAttr = ' video_src="'.$videoSrc.'" data-event-start="'.htmlspecialchars($event->StartDateTime()).'"';
}
}
} else {
$debugAttr .= ' data-debug-still="monitor"';
}
// HOVER OVERLAY: Check for external streaming methods (these work independently of ZM's decode/analyze)
// Priority: go2rtc > rtsp2web > janus > event replay > ZM native stream
if ($Monitor->Go2RTCEnabled() && defined('ZM_GO2RTC_PATH') && ZM_GO2RTC_PATH) {
$liveStreamAttr = ' data-stream-type="go2rtc"'.
' data-go2rtc-src="'.htmlspecialchars(ZM_GO2RTC_PATH).'"'.
' data-monitor-id="'.$monitor['Id'].'"';
$go2rtcAttr = ' go2rtc_src="'.htmlspecialchars(ZM_GO2RTC_PATH).'" go2rtc_mid="'.$monitor['Id'].'"';
$debugAttr .= ' data-debug-overlay="go2rtc"';
} else if ($Monitor->RTSP2WebEnabled() && defined('ZM_RTSP2WEB_PATH') && ZM_RTSP2WEB_PATH) {
$liveStreamAttr = ' data-stream-type="rtsp2web"'.
' data-rtsp2web-src="'.htmlspecialchars(ZM_RTSP2WEB_PATH).'"'.
' data-stream-channel="'.($Monitor->StreamChannel() ?: 'Restream').'"'.
' data-monitor-id="'.$monitor['Id'].'"';
$debugAttr .= ' data-debug-overlay="rtsp2web"';
} else if ($Monitor->JanusEnabled()) {
$janusPath = defined('ZM_JANUS_PATH') && ZM_JANUS_PATH ? ZM_JANUS_PATH : '';
$liveStreamAttr = ' data-stream-type="janus"'.
' data-janus-src="'.htmlspecialchars($janusPath).'"'.
' data-monitor-id="'.$monitor['Id'].'"';
$debugAttr .= ' data-debug-overlay="janus"';
} else if ($monitor['Analysing'] == 'None' && $monitor['Decoding'] == 'None') {
// No external streaming and ZM not running - overlay will use event replay (already set above)
$debugAttr .= ' data-debug-overlay="event-replay"';
} else {
// Use native ZM streaming (zms) - Analysing or Decoding is running
$debugAttr .= ' data-debug-overlay="zm-native"';
}
$thmbWidth = ($options['width']) ? 'width:'.$options['width'].'px;' : '';
$thmbHeight = ($options['height']) ? 'height:'.$options['height'].'px;' : '';
$row['Thumbnail'] = '<div class="colThumbnail" style="'.$thmbHeight.'"><a href="?view=watch&amp;mid='.$monitor['Id'].'">'.
'<img id="thumbnail'.$Monitor->Id().'" src="'.$stillSrc.'" style="'.$thmbWidth.$thmbHeight.
'" stream_src="'.$streamSrc.'" still_src="'.$stillSrc.'"'.$videoAttr.$go2rtcAttr.$liveStreamAttr.$debugAttr.
' data-monitor-width="'.$Monitor->ViewWidth().'" data-monitor-height="'.$Monitor->ViewHeight().'"'.
($options['width'] ? ' width="'.$options['width'].'"' : '').
($options['height'] ? ' height="'.$options['height'].'"' : '').
' loading="lazy" /></a></div>';
}
$data['rows'][] = $row;
}
// Add footer totals to response
$data['footer'] = array(
'monitor_count' => $footer_totals['monitor_count'],
'bandwidth_fps' => human_filesize($footer_totals['total_bandwidth']).'/s '.
round($footer_totals['total_fps'], 2).' fps / '.
round($footer_totals['total_analysis_fps'], 2).' fps',
'total_zones' => $footer_totals['total_zones']
);
// Build filter querystrings for footer event totals (matching header behavior)
$eventCountsFooter = array(
'Total' => array(
'filter' => array('Query' => array('terms' => array()))
),
'Hour' => array(
'filter' => array('Query' => array('terms' => array(
array('cnj' => 'and', 'attr' => 'StartDateTime', 'op' => '>=', 'val' => '-1 hour')
)))
),
'Day' => array(
'filter' => array('Query' => array('terms' => array(
array('cnj' => 'and', 'attr' => 'StartDateTime', 'op' => '>=', 'val' => '-1 day')
)))
),
'Week' => array(
'filter' => array('Query' => array('terms' => array(
array('cnj' => 'and', 'attr' => 'StartDateTime', 'op' => '>=', 'val' => '-7 day')
)))
),
'Month' => array(
'filter' => array('Query' => array('terms' => array(
array('cnj' => 'and', 'attr' => 'StartDateTime', 'op' => '>=', 'val' => '-1 month')
)))
),
'Archived' => array(
'filter' => array('Query' => array('terms' => array(
array('cnj' => 'and', 'attr' => 'Archived', 'op' => '=', 'val' => '1')
)))
)
);
// Add formatted event totals to footer with filter querystrings
foreach (array('Total', 'Hour', 'Day', 'Week', 'Month', 'Archived') as $period) {
$data['footer'][$period.'Events'] = $footer_totals['event_totals'][$period]['events'];
$data['footer'][$period.'EventDiskSpace'] = human_filesize($footer_totals['event_totals'][$period]['diskspace']);
// Generate filter querystring for footer (include monitor filter if monitors are filtered)
$filter = $eventCountsFooter[$period]['filter'];
if (count($monitor_ids) > 0) {
$filter = addFilterTerm(
$filter,
count($filter['Query']['terms']),
array('cnj' => 'and', 'attr' => 'Monitor', 'op' => 'IN', 'val' => implode(',', $monitor_ids))
);
}
parseFilter($filter);
$data['footer'][$period.'FilterQuery'] = $filter['querystring'];
}
return $data;
}
function exportMonitorsJSON() {
require_once('includes/Monitor.php');
require_once('includes/Zone.php');
require_once('includes/Group.php');
require_once('includes/Group_Monitor.php');
if (!canView('Monitors')) {
ajaxError('Insufficient permissions');
return;
}
// Fields to exclude from export (runtime/instance-specific, not useful for import)
$exclude_fields = array('Id', 'Deleted', 'ServerId', 'StorageId', 'Sequence', 'ZoneCount');
$zone_exclude_fields = array('Id', 'MonitorId');
// Get all non-deleted visible monitors
$sql = 'SELECT M.* FROM Monitors AS M WHERE M.Deleted=false ORDER BY M.Sequence ASC';
$db_monitors = dbFetchAll($sql);
// Build group name lookup: MonitorId => array of group names
$group_names_by_monitor = array();
$all_groups = ZM\Group::find();
$groups_by_id = array();
foreach ($all_groups as $G) {
$groups_by_id[$G->Id()] = $G;
}
foreach (ZM\Group_Monitor::find() as $GM) {
$mid = $GM->MonitorId();
$gid = $GM->GroupId();
if (isset($groups_by_id[$gid])) {
if (!isset($group_names_by_monitor[$mid])) {
$group_names_by_monitor[$mid] = array();
}
$group_names_by_monitor[$mid][] = $groups_by_id[$gid]->Name();
}
}
$export = array(
'version' => ZM_VERSION,
'exported' => date('c'),
'monitors' => array()
);
foreach ($db_monitors as $db_row) {
if (!visibleMonitor($db_row['Id'])) continue;
$Monitor = new ZM\Monitor($db_row);
// Serialize all monitor fields via to_json, then filter
$monitor_data = json_decode($Monitor->to_json(), true);
foreach ($exclude_fields as $field) {
unset($monitor_data[$field]);
}
// Add group names
$monitor_data['Groups'] = isset($group_names_by_monitor[$db_row['Id']])
? $group_names_by_monitor[$db_row['Id']]
: array();
// Add zones
$zones = ZM\Zone::find(array('MonitorId' => $db_row['Id']));
$monitor_data['Zones'] = array();
foreach ($zones as $zone) {
$zone_data = json_decode($zone->to_json(), true);
foreach ($zone_exclude_fields as $field) {
unset($zone_data[$field]);
}
$monitor_data['Zones'][] = $zone_data;
}
$export['monitors'][] = $monitor_data;
}
header('Content-Type: application/json; charset=utf-8');
header('Content-Disposition: attachment; filename="zm_monitors_export.json"');
echo json_encode($export, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
exit();
}
?>