Files
zoneminder/web/ajax/watch.php
Nic Boet 35a8daf99e perf: emulate prepares for ZoneMinder's own queries on PHP 8.1+
dbConnect() turns emulated prepares off, so every query of the web UI, the
ajax calls and the includes the API runs (config, servers, users,
permissions) is a server-side prepare: prepare, execute and close, three
round trips to the database where one would do. From PHP 8.1, pdo_mysql
returns integers and floats as PHP ints and floats with emulated prepares
too, so values keep the types they have now. Emulate prepares there and keep
server-side prepares on older PHP, where every value would become a string.

Emulated prepares quote parameters bound as strings, which breaks LIMIT ?.
dbQuery() now binds integer values as integers (everything else as strings,
as execute() did), and the log and watch ajax calls cast their validated
offset and limit to int. With emulation, PDO would also run several
statements given in one string; MYSQL_ATTR_MULTI_STATEMENTS is turned off so
that is refused as before. CakePHP's own connection is not changed: it
already uses PDO's default, emulated prepares.

tests/php/test_database_emulated_prepares.php compares every column of every
table read through both kinds of prepare (types and values), checks
aggregates, LIMIT with positional and named integers, string and null
parameters, and that two statements in one string are refused.
2026-10-14 22:41:42 -05:00

110 lines
3.7 KiB
PHP

<?php
//
// This is simplified version of the logic from ajax/events.php
// For efficiency, sorting and and pagination functionality have been excluded
//
$message = '';
$data = array();
//
// INITIALIZE AND CHECK SANITY
//
if ( !canView('Events') ) $message = 'Insufficient permissions for user '.$user->Username();
// Mid specifies the monitor id we are searching on
if ( empty($_REQUEST['mid']) ) {
$message = 'Must specify a monitor Id';
} else {
$mid = validCardinal($_REQUEST['mid']);
}
// Limit specifies the number of rows to return
if ( ( !is_int($_REQUEST['limit']) and !ctype_digit($_REQUEST['limit']) ) ) {
$message = 'Invalid value for limit: '.$_REQUEST['limit'];
} else {
$limit = (int)$_REQUEST['limit'];
}
if ($message) {
ZM\Error($message);
ajaxError($message);
return;
}
// Sort specifies the name of the column to sort on
$sort = 'Id';
if (isset($_REQUEST['sort'])) {
$sort = $_REQUEST['sort'];
}
// Order specifies the sort direction, either asc or desc
$order = (isset($_REQUEST['order']) and (strtolower($_REQUEST['order']) == 'asc')) ? 'ASC' : 'DESC';
//
// MAIN LOOP
//
// The names of the dB columns in the events table we are interested in
$columns = array('Id', 'MonitorId', 'StorageId', 'Name', 'Cause', 'StartDateTime', 'EndDateTime', 'Length', 'Frames', 'AlarmFrames', 'TotScore', 'AvgScore', 'MaxScore', 'Archived', 'Emailed', 'Notes', 'DiskSpace');
if ( $sort != 'Id' ) {
if (!in_array($sort, $columns)) {
ZM\Error('Invalid sort field: ' . $sort);
$sort = 'Id';
} else if ($sort == 'EndDateTime') {
if ($order == 'ASC') {
$sort = 'E.EndDateTime IS NULL, E.EndDateTime';
} else {
$sort = 'E.EndDateTime IS NOT NULL, E.EndDateTime';
}
} else {
$sort = 'E.'.$sort;
}
}
$where = 'WHERE MonitorId = '.$mid;
$col_str = ' E.*, GROUP_CONCAT(T.Name SEPARATOR ", ") AS Tags ';
$sql = 'SELECT ' .$col_str. ' FROM `Events` AS E
LEFT JOIN Events_Tags AS ET ON E.Id = ET.EventId
LEFT JOIN Tags AS T ON T.Id = ET.TagId
'.$where.' GROUP BY E.Id ORDER BY '.$sort.' '.$order.' LIMIT ?';
$rows = dbQuery($sql, array($limit));
$returned_rows = array();
if ($rows) {
foreach ( $rows as $row ) {
$event = new ZM\Event($row);
$event->remove_from_cache();
if (!$event->canView()) continue;
if ($event->Monitor()->Deleted()) continue;
$scale = intval(5*100*ZM_WEB_LIST_THUMB_WIDTH / $event->Width());
$imgSrc = $event->getThumbnailSrc(array(), '&amp;');
$streamSrc = $event->getStreamSrc(array(
'mode'=>'jpeg', 'scale'=>$scale, 'maxfps'=>ZM_WEB_VIDEO_MAXFPS, 'replay'=>'single', 'rate'=>'400'), '&amp;');
$videoAttr = '';
if ($event->DefaultVideo()) {
$videoSrc = $event->getStreamSrc(array('mode'=>'mp4'), '&amp;');
$videoHlsSrc = $event->getStreamSrc(array('mode'=>'mp4hls'), '&amp;');
$videoDuration = htmlspecialchars((string)$event->Length());
$videoAttr = ' video_src="' .$videoSrc. '" data-video-hls-src="' .$videoHlsSrc. '" data-video-duration-secs="' .$videoDuration. '" data-event-start="'.htmlspecialchars($event->StartDateTime()).'"';
}
// Modify the row data as needed
$row['imgHtml'] = '<img id="thumbnail' .$event->Id(). '" src="' .$imgSrc. '" alt="Event '.$event->Id().'" width="' .validInt($event->ThumbnailWidth()). '" height="' .validInt($event->ThumbnailHeight()).'" stream_src="' .$streamSrc. '" still_src="' .$imgSrc. '"' .$videoAttr. ' data-monitor-width="'.$event->Width().'" data-monitor-height="'.$event->Height().'" loading="lazy" />';
$row['Name'] = validHtmlStr($row['Name']);
$row['Length'] = gmdate('H:i:s', intval($row['Length']));
$returned_rows[] = $row;
} # end foreach row matching search
}
$data['rows'] = &$returned_rows;
ajaxResponse($data);
?>