Files
zoneminder/web/ajax/status.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

622 lines
22 KiB
PHP

<?php
if (!isset($_REQUEST['entity'])) {
\ZM\Error("No entity pass to status request.");
http_response_code(404);
return;
} else {
}
if ($_REQUEST['entity'] == 'navBar') {
global $bandwidth_options, $user;
$data = array();
if ( ZM_OPT_USE_AUTH && (ZM_AUTH_RELAY == 'hashed') ) {
$auth_hash = generateAuthHash(ZM_AUTH_HASH_IPS);
$data['auth'] = $auth_hash;
$data['auth_relay'] = get_auth_relay();
}
// Each widget on the navbar has its own function
// Call the functions we want to dynamically update
$data['getBandwidthHTML'] = getBandwidthHTML($bandwidth_options, $user);
$data['getSysLoadHTML'] = getSysLoadHTML();
$data['getCpuUsageHTML'] = getCpuUsageHTML();
$data['getDbConHTML'] = getDbConHTML();
$data['getStorageHTML'] = getStorageHTML();
//$data['getShmHTML'] = getShmHTML();
$data['getRamHTML'] = getRamHTML();
$data['getLogStatusHTML'] = logState();
ajaxResponse($data);
return;
}
$statusData = array(
'system' => array(
'permission' => 'System',
'table' => 'Monitors',
'limit' => 1,
'elements' => array(
'MonitorCount' => array( 'sql' => 'count(*)' ),
'ActiveMonitorCount' => array( 'sql' => 'count(if(`Capturing` != \'None\',1,NULL))' ),
'State' => array( 'func' => function() { return daemonCheck() ? translate('Running') : translate('Stopped'); } ),
'Load' => array( 'func' => function() { return getLoad(); } ),
'Disk' => array( 'func' => function() { return getDiskPercent(); } ),
),
),
'monitor' => array(
#'permission' => 'Monitors',
'object' => 'Monitor',
'table' => 'Monitors',
'limit' => 1,
'selector' => 'Monitors.Id',
'elements' => array(
'Id' => array( 'sql' => 'Monitors.Id' ),
'Name' => array( 'sql' => 'Monitors.Name' ),
'Type' => true,
'Capturing' => true,
'Analysing' => true,
'Recording' => true,
'Enabled' => true,
'LinkedMonitors' => true,
'Triggers' => true,
'Device' => true,
'Channel' => true,
'Format' => true,
'Host' => true,
'Port' => true,
'Path' => true,
'Width' => array( 'sql' => 'Monitors.Width' ),
'Height' => array( 'sql' => 'Monitors.Height' ),
'Palette' => true,
'Orientation' => true,
'Brightness' => true,
'Contrast' => true,
'Hue' => true,
'Colour' => true,
'EventPrefix' => true,
'LabelFormat' => true,
'LabelX' => true,
'LabelY' => true,
'LabelSize' => true,
'ImageBufferCount' => true,
'WarmupCount' => true,
'PreEventCount' => true,
'PostEventCount' => true,
'AlarmFrameCount' => true,
'SectionLength' => true,
'MotionFrameSkip' => true,
'MaxFPS' => true,
'AlarmMaxFPS' => true,
'FPSReportInterval' => true,
'RefBlendPerc' => true,
'Controllable' => true,
'ControlId' => true,
'ControlDevice' => true,
'ControlAddress' => true,
'AutoStopTimeout' => true,
'TrackMotion' => true,
'TrackDelay' => true,
'ReturnLocation' => true,
'ReturnDelay' => true,
'DefaultView' => true,
'DefaultRate' => true,
'DefaultScale' => true,
'WebColour' => true,
'Sequence' => true,
'MinEventId' => array( 'sql' => '(SELECT min(Events.Id) FROM Events WHERE Events.MonitorId = Monitors.Id)' ),
'MaxEventId' => array( 'sql' => '(SELECT max(Events.Id) FROM Events WHERE Events.MonitorId = Monitors.Id)' ),
'TotalEvents' => array( 'sql' => '(SELECT count(Events.Id) FROM Events WHERE Events.MonitorId = Monitors.Id)' ),
'Status' => (isset($_REQUEST['id'])?array( 'zmu' => '-m '.escapeshellarg($_REQUEST['id']).' -s' ):null),
'FrameRate' => (isset($_REQUEST['id'])?array( 'zmu' => '-m '.escapeshellarg($_REQUEST['id']).' -f' ):null),
'CaptureFPS' => [ 'sql'=>'(SELECT `CaptureFPS` FROM Monitor_Status WHERE MonitorId=Monitors.Id)' ],
'AnalysisFPS' => [ 'sql'=>'(SELECT `AnalysisFPS` FROM Monitor_Status WHERE MonitorId=Monitors.Id)' ],
'CaptureBandwidth' => [ 'sql'=>'(SELECT `CaptureBandwidth` FROM Monitor_Status WHERE MonitorId=Monitors.Id)' ],
),
),
'events' => array(
'permission' => 'Events',
'table' => 'Events',
'selector' => 'Events.MonitorId',
'elements' => array(
'Id' => true,
'MonitorId' => true,
'Name' => true,
'Cause' => true,
'Notes' => true,
'StartDateTime' => true,
'StartDateTimeFormatted' => array('postFunction'=>function($row){
global $dateTimeFormatter;
return $dateTimeFormatter->format(strtotime($row['StartDateTime']));
}),
# Left for backwards compatibility. Remove in 1.37
'EndDateTime' => true,
'EndDateTimeFormatted' => array('postFunction'=>function($row){
global $dateTimeFormatter;
return $dateTimeFormatter->format(strtotime($row['EndDateTime']));
}),
'Width' => true,
'Height' => true,
'Length' => true,
'Frames' => true,
'AlarmFrames' => true,
'TotScore' => true,
'AvgScore' => true,
'MaxScore' => true,
),
),
'event' => array(
'permission' => 'Events',
'table' => 'Events',
'limit' => 1,
'selector' => 'Events.Id',
'elements' => array(
'Id' => array( 'sql' => 'Events.Id' ),
'MonitorId' => true,
'MonitorName' => array('sql' => '(SELECT Monitors.Name FROM Monitors WHERE Monitors.Id = Events.MonitorId)'),
'Name' => true,
'Cause' => true,
'DiskSpace' => true,
'Storage' => array('sql' => '(SELECT Storage.Name FROM Storage WHERE Storage.Id=Events.StorageId)'),
'StartDateTime' => true,
'StartDateTimeFormatted' => array('postFunction'=>function($row){
global $dateTimeFormatter;
return $dateTimeFormatter->format(strtotime($row['StartDateTime']));
}),
# Left for backwards compatibility. Remove in 1.37
'EndDateTime' => true,
'EndDateTimeFormatted' => array('postFunction'=>function($row){
global $dateTimeFormatter;
return $dateTimeFormatter->format(strtotime($row['EndDateTime']));
}),
'Width' => true,
'Height' => true,
'Length' => true,
'Frames' => true,
'DefaultVideo' => true,
'AlarmFrames' => true,
'TotScore' => true,
'AvgScore' => true,
'MaxScore' => true,
'Archived' => true,
'Videoed' => true,
'Uploaded' => true,
'Emailed' => true,
'Messaged' => true,
'Executed' => true,
'Notes' => true,
'MinFrameId' => array( 'sql' => '(SELECT min(Frames.FrameId) FROM Frames WHERE EventId=Events.Id)' ),
'MaxFrameId' => array( 'sql' => '(SELECT max(Frames.FrameId) FROM Frames WHERE Events.Id = Frames.EventId)' ),
'MinFrameDelta' => array( 'sql' => '(SELECT min(Frames.Delta) FROM Frames WHERE Events.Id = Frames.EventId)' ),
'MaxFrameDelta' => array( 'sql' => '(SELECT max(Frames.Delta) FROM Frames WHERE Events.Id = Frames.EventId)' ),
),
),
'frames' => array(
'permission' => 'Events',
'table' => 'Frames',
'selector' => 'EventId',
'elements' => array(
'EventId' => true,
'FrameId' => true,
'Type' => true,
'Delta' => true,
// elements is a whitelist, so the event view's cue graph was reading an
// undefined Score off every frame until these were added.
'Score' => true,
'AudioLevel' => true,
),
),
'frame' => array(
'permission' => 'Events',
'table' => 'Frames',
'limit' => 1,
'selector' => array( array( 'table' => 'Events', 'join' => 'Events.Id = Frames.EventId', 'selector'=>'Events.Id' ), 'Frames.FrameId' ),
'elements' => array(
//'Id' => array( 'sql' => 'Frames.FrameId' ),
'FrameId' => true,
'EventId' => true,
'Type' => true,
'TimeStamp' => true,
'TimeStampShort' => array( 'sql' => 'date_format( StartDateTime, \''.MYSQL_FMT_DATETIME_SHORT.'\' )' ),
'Delta' => true,
'Score' => true,
//'Image' => array( 'postFunc' => 'getFrameImage' ),
),
),
'frameimage' => array(
'permission' => 'Events',
'func' => 'getFrameImage'
),
'nearframe' => array(
'permission' => 'Events',
'func' => 'getNearFrame'
),
'nearevents' => array(
'permission' => 'Events',
'func' => 'getNearEvents'
)
);
function collectData() {
global $statusData;
$entity = strtolower(validJsStr($_REQUEST['entity']));
$entitySpec = &$statusData[$entity];
#print_r( $entitySpec );
if (isset($entitySpec['permission'])) {
if (!canView($entitySpec['permission'])) {
ajaxError('Unrecognised action or insufficient permissions for '.$entity.' permission: '.$entitySpec['permission']);
return;
}
}
if ( !empty($entitySpec['func']) ) {
return call_user_func($entitySpec['func']);
}
$data = array();
$postFuncs = array();
$postFunctions = array();
$fieldSql = array();
$joinSql = array();
$groupSql = array();
$values = array();
$elements = &$entitySpec['elements'];
$lc_elements = array_change_key_case($elements);
$id = false;
if ( isset($_REQUEST['id']) )
if ( !is_array($_REQUEST['id']) )
$id = array( validJsStr($_REQUEST['id']) );
else
$id = array_values($_REQUEST['id']);
# Early per-object permission check before processing elements.
# This must happen before the element loop because func/zmu elements
# execute immediately and would bypass the post-SQL canView() check.
if (isset($entitySpec['object']) && $id) {
$object_name = 'ZM\\'.$entitySpec['object'];
$object = new $object_name($id[0]);
if (!$object->Id()) {
ajaxError('Not found: '.$entity.' id '.$id[0]);
return;
}
if (!$object->canView()) {
ajaxError('Insufficient permissions for '.$entity.' id '.$id[0]);
return;
}
}
if ( !isset($_REQUEST['element']) )
$_REQUEST['element'] = array_keys($elements);
else if ( !is_array($_REQUEST['element']) )
$_REQUEST['element'] = array( validJsStr($_REQUEST['element']) );
if ( isset($entitySpec['selector']) ) {
if ( !is_array($entitySpec['selector']) )
$entitySpec['selector'] = array( $entitySpec['selector'] );
foreach( $entitySpec['selector'] as $selector )
if ( is_array( $selector ) && isset($selector['table']) && isset($selector['join']) )
$joinSql[] = 'left join '.$selector['table'].' on '.$selector['join'];
}
foreach ( $_REQUEST['element'] as $element ) {
if ( !($elementData = $lc_elements[strtolower($element)]) ) {
ajaxError('Bad '.validJsStr($_REQUEST['entity']).' element '.$element);
continue;
}
if (isset($elementData['func'])) {
$data[$element] = call_user_func($elementData['func']);
} else if ( isset($elementData['postFunc']) ) {
$postFuncs[$element] = $elementData['postFunc'];
} else if ( isset($elementData['postFunction']) ) {
$postFunctions[$element] = $elementData['postFunction'];
} else if ( isset($elementData['zmu']) ) {
$command = escapeshellcmd(getZmuCommand(' '.$elementData['zmu']));
$data[$element] = exec($command);
} else {
if ( isset($elementData['sql']) )
$fieldSql[] = $elementData['sql'].' as '.$element;
else
$fieldSql[] = '`'.$element.'`';
if ( isset($elementData['table']) && isset($elementData['join']) ) {
$joinSql[] = 'left join '.$elementData['table'].' on '.$elementData['join'];
}
if ( isset($elementData['group']) ) {
$groupSql[] = $elementData['group'];
}
}
} # end foreach element
if (isset($entitySpec['object'])) {
$fieldSql[] = 'Id';
}
if ( count($fieldSql) ) {
$sql = 'SELECT '.join(', ', $fieldSql).' FROM '.$entitySpec['table'];
if ( $joinSql )
$sql .= ' '.join(' ', array_unique($joinSql));
if ( $id && !empty($entitySpec['selector']) ) {
$index = 0;
$where = array();
foreach ( $entitySpec['selector'] as $selIndex => $selector ) {
$selectorParamName = ':selector' . $selIndex;
if ( is_array($selector) ) {
$where[] = $selector['selector'].' = '.$selectorParamName;
$values[$selectorParamName] = validInt($id[$index]);
} else {
$where[] = $selector.' = '.$selectorParamName;
$values[$selectorParamName] = validInt($id[$index]);
}
$index++;
}
$sql .= ' WHERE '.join(' AND ', $where);
}
if ( $groupSql )
$sql .= ' GROUP BY '.join(',', array_unique($groupSql));
if ( !empty($_REQUEST['sort']) ) {
$sort_fields = explode(',', $_REQUEST['sort']);
$order_clauses = array();
foreach ( $sort_fields as $sort_field ) {
if (!preg_match('/^`?(\w+)`?\s*(ASC|DESC)?( NULLS FIRST)?$/i', $sort_field, $matches)) {
ZM\Error('Sort field didn\'t match regexp '.$sort_field);
continue;
}
// Check that the field name is one we are actually selecting.
// $fieldSql entries may be bare (`Name`), backtick-wrapped (`Name`),
// or aliased (expression as Name), so match against all forms.
$field = $matches[1];
$found = false;
foreach ($fieldSql as $f) {
if ($f === $field || $f === '`'.$field.'`' || preg_match('/\bas\s+`?'.$field.'`?$/i', $f)) {
$found = true;
break;
}
}
if (!$found) {
ZM\Error('Sort field '.$field.' from '.$sort_field.' not in SQL Fields: '.join(',', $fieldSql));
continue;
}
$clause = '`'.$field.'`';
if (!empty($matches[2])) {
$clause .= ' '.strtoupper($matches[2]);
}
if (!empty($matches[3])) {
$clause .= ' '.strtoupper(trim($matches[3]));
}
$order_clauses[] = $clause;
} # end foreach sort field
if ($order_clauses) {
$sql .= ' ORDER BY '.join(', ', $order_clauses);
}
} # end if has sort
if ( !empty($entitySpec['limit']) )
$limit = $entitySpec['limit'];
elseif ( !empty($_REQUEST['count']) )
$limit = validInt($_REQUEST['count']);
$limit_offset = '';
if ( !empty($_REQUEST['offset']) )
$limit_offset = validInt($_REQUEST['offset']) . ', ';
if ( !empty($limit) )
$sql .= ' limit '.$limit_offset.$limit;
if ( isset($limit) && ($limit == 1) ) {
if ( $sqlData = dbFetchOne($sql, NULL, $values) ) {
if (isset($entitySpec['object'])) {
ZM\Debug("Have object".$entitySpec['object']);
$object_name = 'ZM\\'.$entitySpec['object'];
$object = new $object_name($sqlData);
ZM\Debug("Canview:".$object->canView());
if (!$object->canView()) {
ajaxError('Insufficient permissions for '.$entity.' id '.$sqlData['Id']);
return;
}
}
foreach ( $postFuncs as $element=>$func )
$sqlData[$element] = call_user_func($func, $sqlData);
foreach ( $postFunctions as $element=>$function )
$sqlData[$element] = $function($sqlData);
$data = array_merge($data, $sqlData);
}
} else {
$count = 0;
foreach ( dbFetchAll($sql, NULL, $values) as $sqlData ) {
if (isset($entitySpec['object'])) {
ZM\Debug("Have object".$entitySpec['object']);
$object_name = 'ZM\\'.$entitySpec['object'];
$object = new $object_name($sqlData);
ZM\Debug("Canview:".$object->canView());
if (!$object->canView()) continue;
}
foreach ( $postFuncs as $element=>$func )
$sqlData[$element] = call_user_func($func, $sqlData);
foreach ( $postFunctions as $element=>$function )
$sqlData[$element] = $function($sqlData);
$data[] = $sqlData;
if ( isset($limit) && ++$count >= $limit )
break;
} # end foreach
} # end if have limit == 1
} else {
ZM\Debug("No fieldSQL");
}
//ZM\Debug(print_r($data, true));
return $data;
}
function formatDateTime($dt) {
return $dateTimeFormatter->format(strtotime($dt));
}
$data = collectData();
if ( !isset($_REQUEST['layout']) ) {
$_REQUEST['layout'] = 'json';
}
switch ( $_REQUEST['layout'] ) {
case 'xml NOT CURRENTLY SUPPORTED' :
header('Content-type: application/xml');
echo('<?xml version="1.0" encoding="iso-8859-1"?>
');
$entity = strtolower($_REQUEST['entity']);
$entity = preg_replace('/[^A-Za-z0-9]/', '', $entity);
echo '<'.$entity.'>
';
foreach ( $data as $key=>$value ) {
$key = strtolower($key);
echo "<$key>".htmlentities($value)."</$key>\n";
}
echo '</'.$entity.">\n";
break;
case 'json' :
{
$response = array( strtolower(validJsStr($_REQUEST['entity'])) => $data );
if ( ZM_OPT_USE_AUTH && (ZM_AUTH_RELAY == 'hashed') ) {
$auth_hash = generateAuthHash(ZM_AUTH_HASH_IPS);
$response['auth'] = $auth_hash;
$response['auth_relay'] = get_auth_relay();
}
if ( isset($_REQUEST['loopback']) )
$response['loopback'] = validJsStr($_REQUEST['loopback']);
#ZM\Warning(print_r($response, true));
ajaxResponse($response);
break;
}
case 'text' :
header('Content-type: text/plain');
echo join(' ', array_values($data));
break;
default:
ZM\Error('Unsupported layout: '.$_REQUEST['layout']);
}
function getFrameImage() {
$eventId = validCardinal($_REQUEST['eid']);
$frameId = validCardinal($_REQUEST['fid']);
$sql = 'SELECT * FROM Frames WHERE EventId = ? AND FrameId = ?';
if ( !($frame = dbFetchOne($sql, NULL, array($eventId, $frameId))) ) {
ZM\Debug("Frame not found for event $eventId frame $frameId");
$frame = array();
$frame['EventId'] = $eventId;
$frame['FrameId'] = $frameId;
$frame['Type'] = 'Virtual';
}
$event = new ZM\Event($frame['EventId']);
$frame['Image'] = $event->getImageSrc($frame, SCALE_BASE);
return $frame;
}
function getNearFrame() {
$eventId = validCardinal($_REQUEST['id'][0]);
$frameId = validCardinal($_REQUEST['id'][1]);
$sql = 'SELECT FrameId FROM Frames WHERE EventId = ? AND FrameId <= ? ORDER BY FrameId DESC LIMIT 1';
if ( !$nearFrameId = dbFetchOne($sql, 'FrameId', array($eventId, $frameId)) ) {
$sql = 'SELECT * FROM Frames WHERE EventId = ? AND FrameId > ? ORDER BY FrameId ASC LIMIT 1';
if ( !$nearFrameId = dbFetchOne($sql, 'FrameId', array($eventId, $frameId)) ) {
return( array() );
}
}
$_REQUEST['entity'] = 'frame';
$_REQUEST['id'][1] = $nearFrameId;
return collectData();
}
function getNearEvents() {
global $user, $sortColumn, $sortOrder;
$eventId = validCardinal($_REQUEST['id']);
$NearEvents = array('EventId'=>$eventId);
$event = dbFetchOne('SELECT * FROM Events WHERE Id=?', NULL, array($eventId));
if ( !$event ) return $NearEvents;
$filter = ZM\Filter::parse($_REQUEST['filter']);
parseSort();
if ( count($user->unviewableMonitorIds()) ) {
$filter = $filter->addTerm(array('cnj'=>'and', 'attr'=>'MonitorId', 'op'=>'IN', 'val'=>$user->viewableMonitorIds()));
}
$filter_sql = $filter->sql();
# When listing, it may make sense to list them in descending order.
# But when viewing Prev should be timewise earlier and Next should be after.
if ( $sortColumn == 'E.Id' or $sortColumn == 'E.StartDateTime' ) {
$sortOrder = 'ASC';
}
# The sort value of the current event. Read through the same join as the searches so that
# sort columns from Monitors (M.Name) resolve too.
$sortValue = dbFetchOne('SELECT '.$sortColumn.' AS SortValue FROM Events AS E
INNER JOIN Monitors AS M ON E.MonitorId = M.Id WHERE E.Id=?', 'SortValue', array($eventId));
# Events are ordered by ($sortColumn $sortOrder, E.Id ASC). Many events can share a sort value,
# e.g. several monitors starting events in the same second, so Prev/Next must compare the full
# (sort value, Id) tuple. Comparing the sort value alone with >= / <= lets the event after a tie
# pick the one before it as its Next, and playback loops between them forever.
#
# That tuple is the whole ordering: do not also constrain E.StartDateTime. When the sort is not
# StartDateTime (e.g. MonitorName, grouping cameras, or Id) a time constraint drops events that
# are correctly next in the chosen order but started earlier or later than the current one -- the
# next monitor's first event, or a concurrently recorded event with a higher Id, was skipped
# (refs #4186).
$after = ($sortOrder == 'ASC') ? '>' : '<';
$before = ($sortOrder == 'ASC') ? '<' : '>';
$reverseOrder = ($sortOrder == 'ASC') ? 'DESC' : 'ASC';
$nearEvent = function($cmp, $idCmp, $order, $idOrder) use ($eventId, $filter, $sortColumn, $sortValue) {
$sql = '
SELECT E.Id AS Id, E.StartDateTime AS StartDateTime
FROM Events AS E
INNER JOIN Monitors AS M ON E.MonitorId = M.Id
LEFT JOIN Events_Tags AS ET ON E.Id = ET.EventId
LEFT JOIN Tags AS T ON T.Id = ET.TagId
WHERE ';
if ($sortColumn == 'E.Id') {
$sql .= 'E.Id '.$idCmp.' ?';
$values = [$eventId];
} else {
$sql .= '('.$sortColumn.' '.$cmp.' ? OR ('.$sortColumn.' = ? AND E.Id '.$idCmp.' ?))';
$values = [$sortValue, $sortValue, $eventId];
}
if ($filter->sql()) {
$sql .= ' AND ('.$filter->sql().')';
}
$sql .= ' ORDER BY '.$sortColumn.' '.$order;
if ($sortColumn != 'E.Id') {
$sql .= ', E.Id '.$idOrder;
}
$sql .= ' LIMIT 1';
$result = dbQuery($sql, $values);
if (!$result) {
ZM\Error('Failed to load near event using '.$sql);
return false;
}
return dbFetchNext($result);
};
$prevEvent = $nearEvent($before, '<', $reverseOrder, 'DESC');
$nextEvent = $nearEvent($after, '>', $sortOrder, 'ASC');
if ( $prevEvent ) {
$NearEvents['PrevEventId'] = $prevEvent['Id'];
$NearEvents['PrevEventStartTime'] = $prevEvent['StartDateTime'];
$NearEvents['PrevEventDefVideoPath'] = getEventDefaultVideoPath($prevEvent['Id']);
} else {
$NearEvents['PrevEventId'] = $NearEvents['PrevEventStartTime'] = $NearEvents['PrevEventDefVideoPath'] = 0;
}
if ( $nextEvent ) {
$NearEvents['NextEventId'] = $nextEvent['Id'];
$NearEvents['NextEventStartTime'] = $nextEvent['StartDateTime'];
$NearEvents['NextEventDefVideoPath'] = getEventDefaultVideoPath($nextEvent['Id']);
} else {
$NearEvents['NextEventId'] = $NearEvents['NextEventStartTime'] = $NearEvents['NextEventDefVideoPath'] = 0;
}
return $NearEvents;
} # end function getNearEvents()
?>