Files
zoneminder/web/ajax/add_monitors.php
Isaac ConnorandClaude Opus 5 696b479a88 fix: qualify the remaining logger calls in the web ui, and test for them
Looking for other instances of the CronTab.php fatal in the previous commit
found eleven, in ten files, dating from 2019 to 2023. Each is a call to a
logger function from the global namespace without the \ZM\ prefix, so each is
a fatal the moment its branch runs:

  web/ajax/console.php, events.php, reports.php, add_monitors.php, status.php
  web/includes/actions/snapshot.php (two), donate.php
  web/includes/functions.php, session.php
  web/views/image.php

They have survived this long because of where they sit. Every one is on an
error or diagnostic path -- an invalid sort order, a zero byte download, a
failed read lock -- so the call is only reached when something has already
gone wrong, and then it turns a handled problem into a blank 500. That is the
shape of the bug on the monitor save: the write had worked, and the thing that
broke was the attempt to log.

Verified before changing any of them that each file is in the global
namespace, does not define the function itself and does not import it, so the
call really does not resolve.

Tests: tests/php/test_logger_calls_qualified.php, 16 checks. It tokenizes
every php file under web/ and reports unqualified calls, with the function
names read out of logger.php rather than listed, so a new one is covered the
day it is added. web/api is skipped as cakephp with its own logging, and
assets as third party.

Php's own tokenizer rather than a regex, because the distinctions matter: a
method call on an object that happens to have an Error() method is fine, as
is a definition, a mention, a qualified call, the same call from inside
namespace ZM, and the name appearing in a comment or a string. There is a
check for each of those, since a scan that quietly matched nothing would pass
the tree-wide check on its own.

Verified the scan fails when the Debug() in CronTab.php is put back.

Ran: tests/php/test_logger_calls_qualified.php (16 passed), the rest of
tests/php and tests/js, npx eslint . (clean), utils/check-homoglyphs.py
(clean). test_bandwidth_clamp.php fails on this box for an unrelated reason:
it cannot read /etc/zm/conf.d/03custom.conf and the permission warning lands
in its captured output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpiSWBmtQkR5bcgpHWY4ME
2026-10-28 18:01:26 -05:00

231 lines
7.7 KiB
PHP

<?php
require_once('includes/monitor_probe.php');
$defaultMonitor = new ZM\Monitor();
function probe(&$url_bits) {
$cameras = probeNetwork();
return $cameras;
}
function tprobe( &$url_bits ) {
error_reporting(0);
global $defaultMonitor;
$available_streams = array();
if ( ! isset($url_bits['port']) ) {
$cam_list_html = file_get_contents('http://'.$url_bits['host'].':5000/monitoring/');
if ( $cam_list_html ) {
ZM\Debug("Have content at port 5000/monitoring");
$matches_count = preg_match_all(
'/<a href="http:\/\/([.[:digit:]]+):([[:digit:]]+)\/\?action=stream" target="_blank">([^<]+)<\/a>/',
$cam_list_html, $cam_list );
ZM\Debug(print_r($cam_list,true));
}
if ( $matches_count ) {
for( $index = 0; $index < $matches_count; $index ++ ) {
$new_stream = $url_bits; // make a copy
$new_stream['port'] = $cam_list[2][$index];
$new_stream['Name'] = trim($cam_list[3][$index]);
if ( ! isset($new_stream['scheme'] ) )
$new_stream['scheme'] = 'http';
$available_streams[] = $new_stream;
ZM\Debug("Have new stream " . print_r($new_stream,true) );
}
} else {
ZM\Info('No matches');
}
if ( 0 ) {
// No port given, do a port scan
foreach ( range( 2000, 2007 ) as $port ) {
$socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
socket_set_option( $socket,
SOL_SOCKET, // socket level
SO_SNDTIMEO, // timeout option
array(
'sec'=>0, // Timeout in seconds
'usec'=>500 // I assume timeout in microseconds
)
);
$new_stream = null;
\ZM\Info('Testing connection to '.$url_bits['host'].':'.$port);
if ( socket_connect( $socket, $url_bits['host'], $port ) ) {
$new_stream = $url_bits; // make a copy
$new_stream['port'] = $port;
} else {
socket_close($socket);
ZM\Info('No connection to '.$url_bits['host'].' on port '.$port);
continue;
}
if ( $new_stream ) {
if ( ! isset($new_stream['scheme'] ) )
$new_stream['scheme'] = 'http';
$url = unparse_url($new_stream, array('path'=>'/', 'query'=>'action=snapshot'));
list($width, $height, $type, $attr) = getimagesize( $url );
ZM\Info("Got $width x $height from $url");
$new_stream['Width'] = $width;
$new_stream['Height'] = $height;
//try {
//if ( $response = do_request( 'GET', $url ) ) {
//$new_stream['path'] = '/';
//$new_stream['query'] = '?action=stream';
//$image = imagecreatefromstring($response);
////$size = getimagesize( $image );
//
//} else {
//Info("No response from $url");
//}
//} catch ( EXception $e ) {
//Info("No response from $url");
//}
$available_streams[] = $new_stream;
} // end if new_Stream
} // end foreach port to scan
} # end if 0
} else {
// A port was specified, so don't need to port scan.
$available_streams[] = $url_bits;
}
foreach ( $available_streams as &$stream ) {
# check for existence in db.
$stream['url'] = unparse_url($stream, array('path'=>'/','query'=>'action=stream'));
$monitors = ZM\Monitor::find(array('Path'=>$stream['url']));
if ( count($monitors) ) {
ZM\Info('Found monitors matching ' . $stream['url'] );
$stream['Monitor'] = $monitors[0];
if ( isset( $stream['Width'] ) and ( $stream['Monitor']->Width() != $stream['Width'] ) ) {
$stream['Warning'] .= 'Monitor width ('.$stream['Monitor']->Width().') and stream width ('.$stream['Width'].") do not match!\n";
}
if ( isset( $stream['Height'] ) and ( $stream['Monitor']->Height() != $stream['Height'] ) ) {
$stream['Warning'] .= 'Monitor height ('.$stream['Monitor']->Height().') and stream width ('.$stream['Height'].") do not match!\n";
}
} else {
$stream['Monitor'] = clone $defaultMonitor;
if ( isset($stream['Width']) ) {
$stream['Monitor']->Width($stream['Width']);
$stream['Monitor']->Height($stream['Height']);
}
if ( isset($stream['Name']) ) {
$stream['Monitor']->Name($stream['Name']);
}
} // Monitor found or not
} // end foreach Stream
#$macCommandString = 'arp ' . $url_bits['host'] . " | awk 'BEGIN{ i=1; } { i++; if(i==3) print $3 }'";
#$mac = exec($macCommandString);
#Info("Mac $mac");
return $available_streams;
} // end function probe
if (canEdit('Monitors')) {
switch ($_REQUEST['action']) {
case 'probe' :
{
$available_streams = array();
$url = isset($_REQUEST['url']) ? $_REQUEST['url'] : '';
$url_bits = null;
if ( preg_match('/(\d+)\.(\d+)\.(\d+)\.(\d+)/', $url) ) {
$url_bits = array('host'=>$url);
} else {
$url_bits = parse_url($url);
}
if (!$url_bits) {
ajaxError('The given URL was too malformed to parse.');
return;
}
$available_streams = probe($url_bits);
#ZM\Debug("available_streams".print_r($available_streams, true));
ajaxResponse(array('Streams'=>$available_streams));
return;
} // end case url_probe
case 'import':
{
if ( !isset($_FILES['import_file']) ) {
ajaxError('No file was uploaded.');
return;
}
$file = $_FILES['import_file'];
if ( $file['error'] > 0 ) {
ajaxError('File upload failed with error code '.$file['error']);
return;
}
if ( !is_uploaded_file($file['tmp_name']) ) {
ajaxError('Uploaded file does not exist');
return;
}
if ( ($handle = fopen($file['tmp_name'], 'r')) === FALSE ) {
ajaxError('Unable to open uploaded file');
return;
}
$available_streams = array();
$row = 0;
while ( ($data = fgetcsv($handle, 1000, ',')) !== FALSE ) {
$row ++;
if ( count($data) < 2 ) {
ZM\Info("Skipping line $row, expected at least Name,URL columns");
continue;
}
$name = trim($data[0]);
$url = trim($data[1]);
$group = isset($data[2]) ? trim($data[2]) : '';
// Skip an optional header row (Name,URL,Group)
if ( $row == 1 and !strcasecmp($name, 'Name') and !strcasecmp($url, 'URL') ) {
ZM\Debug('Skipping header row');
continue;
}
if ( $url === '' ) {
ZM\Info("Skipping line $row, no URL");
continue;
}
ZM\Info("Importing line $row: $name $url $group");
$url_bits = parse_url($url);
$host = ($url_bits and isset($url_bits['host'])) ? $url_bits['host'] : '';
# If a monitor already exists for this url, offer to edit it instead of adding a duplicate.
$monitor = null;
$existing = ZM\Monitor::find(array('Path'=>$url));
if ( count($existing) ) $monitor = $existing[0];
$available_streams[] = array(
'mac' => '',
'description' => $name.' - '.$url,
'url' => $url,
'IP' => $host,
'Group' => $group,
'camera' => array(
'Name' => $name,
'ip' => $host,
'Manufacturer' => '',
'Model' => '',
'monitor' => array(
'Type' => 'Ffmpeg',
'Path' => $url,
'Name' => $name,
),
),
'Monitor' => $monitor,
);
} // end while rows
fclose($handle);
ajaxResponse(array('Streams'=>$available_streams));
break;
} // end case import
default:
ZM\Warning('unknown action '.$_REQUEST['action']);
} // end switch action
} else {
ZM\Warning('Cannot edit monitors');
}
ajaxError('Unrecognised action '.validHtmlStr($_REQUEST['action']).' or insufficient permissions for user '.validHtmlStr($user->Username()));
?>