mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-10-03 07:55:22 -04:00
DefaultVideo can be set by any user with Events=Edit through the API (EventsController add/edit), and readers join it onto the event path: view_video.php streams it, image.php extracts frames from it, findVideoEventFile, Event::getStreamSrc/FileSize and the API fileExists check all use Path().'/'.DefaultVideo. A value such as ../../x pointed those reads at arbitrary files readable by the web account. Reject DefaultVideo values on save in the API model unless they are a bare filename (no / or \, no NUL, not . or ..). Make the PHP Event DefaultVideo() accessor return basename() so every web reader only ever looks inside the event directory, even for rows written before this check, and apply basename() to the raw array read in the API model's fileExists. Event::GenerateVideo built its output filename from Name with only whitespace replaced; Name is editable through the event rename and eventdetail actions. Replace anything outside [-A-Za-z0-9_.] and a leading dot so the output stays inside the event directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit f3f679b644db28b4b1bff79d5dd98eca24395fc4)
202 lines
5.7 KiB
PHP
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().'/'.$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
|
|
}
|