mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-10-06 09:22:04 -04:00
Every query on Frames filters on EventId and orders or ranges by FrameId, and nothing needs a frame by Id. The Id primary key was a clustered index nothing read, and EventId_FrameId_idx was a secondary index every query used. On a copy of a local table that secondary index was half the table's size. With (EventId, FrameId) as the primary key the index is gone, and an event's rows are stored together, so deleting an event is a range delete. zm_update-1.39.36.sql: - converts AI_Detections.FrameId from Frames.Id to the per-event frame number, drops its foreign key to Frames and indexes (EventId, FrameId). A composite foreign key cannot replace it: ON DELETE SET NULL would have to null the NOT NULL EventId, and Frames rows are written in batches, so a detection can be recorded before its frame row. - removes duplicate (EventId, FrameId) rows, keeping the earliest. - rebuilds Frames with the new primary key. - removes ON UPDATE CURRENT_TIMESTAMP from Frames.TimeStamp. Any UPDATE of a frame row was overwriting its capture time. Each step checks the current schema first, so the migration can be re-run. REST API: view, edit and delete take /frames/<action>/<EventId>/<FrameId>.json. The old single-Id URLs return 404. CakePHP 2 has no composite keys, so the model's primaryKey is EventId. That keeps Event's dependent cascade delete limited to the event's own frames. The controller writes with explicit (EventId, FrameId) conditions instead of save(), which would match rows on EventId alone. Edit no longer changes EventId or FrameId. view=image with fid but no eid used to look up Frames.Id. It now returns 404. The Perl Frame class is identified by (EventId, FrameId). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
228 lines
7.1 KiB
PHP
228 lines
7.1 KiB
PHP
<?php
|
|
App::uses('AppController', 'Controller');
|
|
require_once __DIR__ .'/../../../includes/Event.php';
|
|
/**
|
|
* Frames Controller
|
|
*
|
|
* @property Frame $Frame
|
|
*/
|
|
class FramesController extends AppController {
|
|
|
|
/**
|
|
* Components
|
|
*
|
|
* @var array
|
|
*/
|
|
public $components = array('RequestHandler');
|
|
|
|
|
|
public function beforeFilter() {
|
|
parent::beforeFilter();
|
|
global $user;
|
|
# We already tested for auth in appController, so we just need to test for specific permission
|
|
$canView = (!$user) || ($user->Events() != 'None');
|
|
if (!$canView) {
|
|
throw new UnauthorizedException(__('Insufficient Privileges'));
|
|
return;
|
|
}
|
|
}
|
|
|
|
# A single frame is addressed by (EventId, FrameId), its primary key.
|
|
private function findFrame($eventId, $frameId) {
|
|
$this->Frame->recursive = -1;
|
|
$frame = $this->Frame->find('first', array(
|
|
'conditions' => array('Frame.EventId' => $eventId, 'Frame.FrameId' => $frameId)
|
|
));
|
|
if (!$frame) {
|
|
throw new NotFoundException(__('Invalid frame'));
|
|
}
|
|
return $frame;
|
|
}
|
|
|
|
# Frames carry no MonitorId, so the parent Event's per-monitor ACL has to be
|
|
# resolved explicitly.
|
|
private function eventForFrame($eventId, $frameId) {
|
|
$frame = $this->findFrame($eventId, $frameId);
|
|
$this->loadModel('Event');
|
|
$this->Event->recursive = -1;
|
|
$event = $this->Event->find('first', array(
|
|
'conditions' => array('Event.Id' => $frame['Frame']['EventId'])
|
|
));
|
|
if (!$event) {
|
|
throw new NotFoundException(__('Invalid event'));
|
|
}
|
|
return new ZM\Event($event['Event']);
|
|
}
|
|
|
|
# A frame being added names its event in the request data. Require edit on
|
|
# that event too, or a user could attach frames to a denied monitor's event.
|
|
private function requireRequestEventEdit() {
|
|
$eventId = $this->requestField('Frame', 'EventId');
|
|
if ($eventId === null) {
|
|
throw new BadRequestException(__('EventId is required'));
|
|
}
|
|
$this->loadModel('Event');
|
|
$this->Event->recursive = -1;
|
|
$event = $this->Event->find('first', array('conditions' => array('Event.Id' => $eventId)));
|
|
if (!$event) {
|
|
throw new NotFoundException(__('Invalid event'));
|
|
}
|
|
$event = new ZM\Event($event['Event']);
|
|
if (!$event->canEdit()) {
|
|
throw new UnauthorizedException(__('Insufficient Privileges'));
|
|
}
|
|
}
|
|
|
|
# Frame mutation is an Event mutation, so require Events=Edit as well as the
|
|
# per-monitor ACL. beforeFilter() only guarantees Events != None.
|
|
private function requireFrameEdit($eventId, $frameId) {
|
|
global $user;
|
|
if ($user and ($user->Events() != 'Edit')) {
|
|
throw new UnauthorizedException(__('Insufficient Privileges'));
|
|
}
|
|
if (!$this->eventForFrame($eventId, $frameId)->canEdit()) {
|
|
throw new UnauthorizedException(__('Insufficient Privileges'));
|
|
}
|
|
}
|
|
|
|
# The request's Frame fields that are real columns. Model save() cannot be
|
|
# used to write them: it would match rows on the single-column primaryKey.
|
|
private function requestColumns($exclude = array()) {
|
|
$data = $this->request->data;
|
|
if (isset($data['Frame']) and is_array($data['Frame'])) $data = $data['Frame'];
|
|
$columns = array();
|
|
foreach (array_keys($this->Frame->schema()) as $field) {
|
|
if (in_array($field, $exclude) or !array_key_exists($field, $data)) continue;
|
|
$columns[$field] = $data[$field];
|
|
}
|
|
return $columns;
|
|
}
|
|
|
|
/**
|
|
* index method
|
|
* @return void
|
|
*/
|
|
public function index() {
|
|
$this->Frame->recursive = -1;
|
|
|
|
global $user;
|
|
$monitorCondition = $this->viewableMonitorCondition('Event.MonitorId');
|
|
|
|
$named_params = $this->request->params['named'];
|
|
if ( $named_params ) {
|
|
$this->FilterComponent = $this->Components->load('Filter');
|
|
$conditions = $this->FilterComponent->buildFilter($named_params);
|
|
} else {
|
|
$conditions = array();
|
|
}
|
|
|
|
$findOptions = array('conditions' => $conditions);
|
|
if ( count($monitorCondition) ) {
|
|
// Frame has no MonitorId of its own, and recursive=-1 above means the
|
|
// Event association isn't auto-joined, so the per-monitor ACL has to
|
|
// join through to the owning Event explicitly.
|
|
$findOptions['joins'] = array(array(
|
|
'table' => 'Events',
|
|
'alias' => 'Event',
|
|
'type' => 'inner',
|
|
'conditions' => array('Event.Id = Frame.EventId'),
|
|
));
|
|
$findOptions['conditions'][] = $monitorCondition;
|
|
}
|
|
|
|
$frames = $this->Frame->find('all', $findOptions);
|
|
$this->set(array(
|
|
'frames' => $frames,
|
|
'_serialize' => array('frames')
|
|
));
|
|
}
|
|
|
|
/**
|
|
* view method
|
|
*
|
|
* @throws NotFoundException
|
|
* @param string $eventId
|
|
* @param string $frameId
|
|
* @return void
|
|
*/
|
|
public function view($eventId = null, $frameId = null) {
|
|
if (!$this->eventForFrame($eventId, $frameId)->canView()) {
|
|
throw new UnauthorizedException(__('Insufficient Privileges'));
|
|
}
|
|
$this->set(array(
|
|
'frame' => $this->findFrame($eventId, $frameId),
|
|
'_serialize' => array('frame')
|
|
));
|
|
}
|
|
|
|
/**
|
|
* add method
|
|
*
|
|
* @return void
|
|
*/
|
|
public function add() {
|
|
if ($this->request->is('post')) {
|
|
global $user;
|
|
if ($user and ($user->Events() != 'Edit')) {
|
|
throw new UnauthorizedException(__('Insufficient Privileges'));
|
|
}
|
|
$this->requireRequestEventEdit();
|
|
$columns = $this->requestColumns();
|
|
$this->Frame->set($columns);
|
|
if ($this->Frame->validates() and
|
|
$this->Frame->getDataSource()->create($this->Frame, array_keys($columns), array_values($columns))) {
|
|
return $this->flash(__('The frame has been saved.'), array('action' => 'index'));
|
|
}
|
|
}
|
|
$events = $this->Frame->Event->find('list');
|
|
$this->set(compact('events'));
|
|
}
|
|
|
|
/**
|
|
* edit method
|
|
*
|
|
* EventId and FrameId are the key and cannot be changed.
|
|
*
|
|
* @throws NotFoundException
|
|
* @param string $eventId
|
|
* @param string $frameId
|
|
* @return void
|
|
*/
|
|
public function edit($eventId = null, $frameId = null) {
|
|
$this->requireFrameEdit($eventId, $frameId);
|
|
if ($this->request->is(array('post', 'put'))) {
|
|
# updateAll() takes SQL expressions, so the values must be quoted here.
|
|
$db = $this->Frame->getDataSource();
|
|
$columns = array();
|
|
foreach ($this->requestColumns(array('EventId', 'FrameId')) as $field => $value) {
|
|
$columns[$field] = $db->value($value, $this->Frame->getColumnType($field));
|
|
}
|
|
if (count($columns) and $this->Frame->updateAll($columns,
|
|
array('Frame.EventId' => $eventId, 'Frame.FrameId' => $frameId))) {
|
|
return $this->flash(__('The frame has been saved.'), array('action' => 'index'));
|
|
}
|
|
} else {
|
|
$this->request->data = $this->findFrame($eventId, $frameId);
|
|
}
|
|
$events = $this->Frame->Event->find('list');
|
|
$this->set(compact('events'));
|
|
}
|
|
|
|
/**
|
|
* delete method
|
|
*
|
|
* @throws NotFoundException
|
|
* @param string $eventId
|
|
* @param string $frameId
|
|
* @return void
|
|
*/
|
|
public function delete($eventId = null, $frameId = null) {
|
|
$this->request->allowMethod('post', 'delete');
|
|
$this->requireFrameEdit($eventId, $frameId);
|
|
if ($this->Frame->deleteAll(array('Frame.EventId' => $eventId, 'Frame.FrameId' => $frameId), false)) {
|
|
return $this->flash(__('The frame has been deleted.'), array('action' => 'index'));
|
|
} else {
|
|
return $this->flash(__('The frame could not be deleted. Please, try again.'), array('action' => 'index'));
|
|
}
|
|
}}
|