Files
zoneminder/web/api/app/Model/Event.php
T
Isaac ConnorandClaude Opus 5.5 da0ba713c1 fix: use only the filename part of DefaultVideo in the API event fileSize()
fileSize() joined the stored DefaultVideo onto the event directory as is,
so an older row, or one written before the model validation was added,
holding ../ could stat a file outside it. fileExists() above already
applies basename(); do the same here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:16:53 -04:00

202 lines
5.7 KiB
PHP

<?php
require_once __DIR__ .'/../../../includes/Event.php';
App::uses('AppModel', 'Model');
/**
* Event Model
*
* @property Monitor $Monitor
* @property Frame $Frame
*/
class Event extends AppModel {
/**
* Use table
*
* @var mixed False or table name
*/
public $useTable = 'Events';
/**
* Primary key field
*
* @var string
*/
public $primaryKey = 'Id';
/**
* Display field
*
* @var string
*/
public $displayField = 'Name';
// DefaultVideo is joined onto the event directory by readers, so it must be
// a bare filename: no path separators, NUL, or . / ..
public $validate = array(
'DefaultVideo' => array(
'rule' => array('custom', '/^(?!\.\.?$)[^\/\\\\\x00]+$/D'),
'allowEmpty' => true,
'message' => 'DefaultVideo must be a filename without a path',
),
);
// For events that never wrote EndDateTime (zmc killed/crashed mid-event),
// fall back to StartDateTime + Length (Length is flushed to the DB every few
// seconds during recording, so it reflects the actual recorded duration).
// Only fall back to NOW() if Length is also 0 (event has no recorded data
// yet, e.g. just started). This prevents montagereview and other consumers
// from painting an event bar across hours/days of no real recording.
public $virtualFields = array(
'StartTimeSecs' => 'UNIX_TIMESTAMP(StartDateTime)',
'EndTimeSecs' => '(CASE WHEN Event.EndDateTime IS NOT NULL THEN UNIX_TIMESTAMP(Event.EndDateTime) WHEN Event.Length > 0 THEN UNIX_TIMESTAMP(Event.StartDateTime) + Event.Length ELSE UNIX_TIMESTAMP(Event.StartDateTime) END)',
'StartTime' => 'StartDateTime',
'EndTime' => '(CASE WHEN Event.EndDateTime IS NOT NULL THEN Event.EndDateTime WHEN Event.Length > 0 THEN DATE_ADD(Event.StartDateTime, INTERVAL FLOOR(Event.Length) SECOND) ELSE NOW() END)'
);
//The Associations below have been created with all possible keys, those that are not needed can be removed
/**
* belongsTo associations
*
* @var array
*/
public $belongsTo = array(
'Monitor' => array(
'className' => 'Monitor',
'foreignKey' => 'MonitorId',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Storage' => array(
'className' => 'Storage',
'joinTable' => 'Storage',
'foreignKey' => 'StorageId',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
/**
* hasMany associations
*
* @var array
*/
public $hasMany = array(
'Frame' => array(
'className' => 'Frame',
'foreignKey' => 'EventId',
'dependent' => true,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'exclusive' => 'true',
'finderQuery' => '',
'counterQuery' => ''
)
);
/**
* * * hasMany associations
* * *
* * * @var array
* * */
public $hasAndBelongsToMany = array(
'Group' => array(
'className' => 'Group',
'joinTable' => 'Groups_Monitors',
'foreignKey' => 'MonitorId',
'associationForeignKey' => 'MonitorId',
'unique' => true,
'dependent' => false,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'exclusive' => '',
'finderQuery' => '',
'counterQuery' => ''
),
'Tag' => array(
'className' => 'Tag',
'joinTable' => 'Events_Tags',
'foreignKey' => 'EventId',
'associationForeignKey' => 'TagId',
'unique' => true,
'dependent' => false,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'exclusive' => '',
'finderQuery' => '',
'counterQuery' => ''
),
);
public $actsAs = array(
'Containable',
'CakePHP-Enum-Behavior.Enum' => array(
'Orientation' => array('ROTATE_0','ROTATE_90','ROTATE_180','ROTATE_270','FLIP_HORI','FLIP_VERT'),
'Scheme' => array('Deep','Medium','Shallow')
)
);
public function Relative_Path() {
$Event = ZM\Event::find_one(['Id'=>$this->id]);
return $Event ? $Event->Relative_Path() : '';
} // end function Relative_Path()
public function Path() {
$Event = ZM\Event::find_one(['Id'=>$this->id]);
return $Event ? $Event->Path() : '';
}
public function Link_Path() {
$Event = ZM\Event::find_one(['Id'=>$this->id]);
return $Event ? $Event->Link_Path() : '';
}
public function fileExists($event) {
if ($event['DefaultVideo']) {
if (file_exists($this->Path().'/'.basename($event['DefaultVideo']))) {
return 1;
}
// While an event is recording its DefaultVideo is incomplete.mp4. When
// the event closes the file is renamed to <Id>-video.* and the DB row is
// updated. If we still see incomplete.mp4 the model is likely stale, so
// reload the event from the database and re-check the new DefaultVideo.
if (preg_match('/^incomplete\./', basename($event['DefaultVideo']))) {
ZM\Event::clear_cache();
$Event = ZM\Event::find_one(array('Id'=>$this->id));
if ($Event and $Event->DefaultVideo() and $Event->DefaultVideo() != $event['DefaultVideo']
and file_exists($Event->Path().'/'.$Event->DefaultVideo())) {
return 1;
}
}
ZM\Warning('File does not exist at ' . $this->Path().'/'.$event['DefaultVideo'] );
} else {
return 0;
}
} // end function fileExists($event)
public function fileSize($event) {
return filesize($this->Path().'/'.basename($event['DefaultVideo']));
}
public function beforeDelete($cascade=true) {
$Event = ZM\Event::find_one(['Id'=>$this->id]);
if ($Event) $Event->delete();
// Event->delete() will do it all, so cake doesn't have to do anything.
return false;
} // end function afterDelete
}