Merge branch 'master' of github.com:ZoneMinder/zoneminder

pull/4693/head
Isaac Connor 2026-03-07 17:30:39 -05:00
commit 3f911d5397
328 changed files with 39912 additions and 21069 deletions

View File

@ -1329,6 +1329,25 @@ CREATE TABLE `Events_Tags` (
CONSTRAINT `Events_Tags_ibfk_2` FOREIGN KEY (`EventId`) REFERENCES `Events` (`Id`) ON DELETE CASCADE
) ENGINE=@ZM_MYSQL_ENGINE@;
CREATE TABLE `Notifications` (
`Id` int unsigned NOT NULL AUTO_INCREMENT,
`UserId` int unsigned DEFAULT NULL,
`Token` varchar(512) NOT NULL,
`Platform` enum('android','ios','web') NOT NULL,
`MonitorList` text DEFAULT NULL,
`Interval` int unsigned NOT NULL DEFAULT 0,
`PushState` enum('enabled','disabled') NOT NULL DEFAULT 'enabled',
`AppVersion` varchar(32) DEFAULT NULL,
`BadgeCount` int NOT NULL DEFAULT 0,
`LastNotifiedAt` datetime DEFAULT NULL,
`CreatedOn` datetime DEFAULT NULL,
`UpdatedOn` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`Id`),
UNIQUE KEY `Notifications_Token_idx` (`Token`),
KEY `Notifications_UserId_idx` (`UserId`),
CONSTRAINT `Notifications_ibfk_1` FOREIGN KEY (`UserId`) REFERENCES `Users` (`Id`) ON DELETE CASCADE
) ENGINE=@ZM_MYSQL_ENGINE@;
source @PKGDATADIR@/db/Object_Types.sql
-- We generally don't alter triggers, we drop and re-create them, so let's keep them in a separate file that we can just source in update scripts.
source @PKGDATADIR@/db/triggers.sql

View File

@ -1,6 +1,4 @@
--
-- This updates a 1.37.80 database to 1.37.81
--
-- Convert Zone Coords from pixel values to percentage values (0.00-100.00)
-- so that zones are resolution-independent.
--
@ -108,3 +106,35 @@ UPDATE Zones z
-- Update Units to Percent for all zones, and set as new default
UPDATE Zones SET Units = 'Percent' WHERE Units = 'Pixels';
ALTER TABLE Zones ALTER Units SET DEFAULT 'Percent';
--
-- Add Notifications table for FCM push token registration
--
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE table_name = 'Notifications'
AND table_schema = DATABASE()) > 0,
"SELECT 'Notifications table already exists'",
"CREATE TABLE `Notifications` (
`Id` int unsigned NOT NULL AUTO_INCREMENT,
`UserId` int unsigned DEFAULT NULL,
`Token` varchar(512) NOT NULL,
`Platform` enum('android','ios','web') NOT NULL,
`MonitorList` text DEFAULT NULL,
`Interval` int unsigned NOT NULL DEFAULT 0,
`PushState` enum('enabled','disabled') NOT NULL DEFAULT 'enabled',
`AppVersion` varchar(32) DEFAULT NULL,
`BadgeCount` int NOT NULL DEFAULT 0,
`LastNotifiedAt` datetime DEFAULT NULL,
`CreatedOn` datetime DEFAULT NULL,
`UpdatedOn` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`Id`),
UNIQUE KEY `Notifications_Token_idx` (`Token`),
KEY `Notifications_UserId_idx` (`UserId`),
CONSTRAINT `Notifications_ibfk_1` FOREIGN KEY (`UserId`) REFERENCES `Users` (`Id`) ON DELETE CASCADE
) ENGINE=InnoDB"
));
PREPARE stmt FROM @s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@ -232,6 +232,19 @@ sub open {
}
}
# --- Credential fallback: ONVIF_Username/Password, then User/Pass -----
if (!$$self{username}) {
if ($self->{Monitor}->{ONVIF_Username}) {
$$self{username} = $self->{Monitor}->{ONVIF_Username};
$$self{password} = $self->{Monitor}->{ONVIF_Password} if $self->{Monitor}->{ONVIF_Password};
Debug('Using ONVIF_Username/ONVIF_Password from Monitor');
} elsif ($self->{Monitor}->{User}) {
$$self{username} = $self->{Monitor}->{User};
$$self{password} = $self->{Monitor}->{Pass} if $self->{Monitor}->{Pass};
Debug('Using User/Pass from Monitor');
}
}
# --- Connectivity check (non-fatal) ------------------------------------
if ($$self{BaseURL}) {
my $res = $self->sendCmd('/onvif/device_service',

View File

@ -741,8 +741,10 @@ void Monitor::Load(MYSQL_ROW dbrow, bool load_zones=true, Purpose p = QUERY) {
startup_delay = dbrow[col] ? atoi(dbrow[col]) : 0;
col++;
// How many frames we need to have before we start analysing
ready_count = std::max(warmup_count, pre_event_count);
// How many frames we need to have before we start analysing.
// Must account for alarm_frame_count because openEvent walks back
// max(pre_event_count, alarm_frame_count) frames from the analysis point.
ready_count = std::max({warmup_count, pre_event_count, alarm_frame_count});
//shared_data->image_count = 0;
last_alarm_count = 0;

View File

@ -1497,7 +1497,7 @@ int VideoStore::write_packet(AVPacket *pkt, AVStream *stream) {
Debug(1, "non increasing dts, fixing. our dts %" PRId64 " stream %d last_dts %" PRId64 " stream %d. reorder_queue_size=%zu",
pkt->dts, stream->index, last_dts[stream->index], stream->index, reorder_queue_size);
// dts MUST monotonically increase, so add 1 which should be a small enough time difference to not matter.
pkt->dts = last_dts[stream->index]+last_duration[stream->index];
pkt->dts = last_dts[stream->index]+1;
if (pkt->dts > pkt->pts) pkt->pts = pkt->dts; // Do it here to avoid warning below
}
}

View File

@ -22,6 +22,17 @@ if (!isset($_REQUEST['task'])) {
} else {
createRequest();
}
} else if ($_REQUEST['task'] == 'delete') {
global $user;
if (!canEdit('System')) {
$message = 'Insufficient permissions to delete log entries for user '.$user->Username();
} else {
if (!empty($_REQUEST['ids'])) {
$ids = array_map('intval', (array)$_REQUEST['ids']);
$placeholders = implode(',', array_fill(0, count($ids), '?'));
dbQuery('DELETE FROM Logs WHERE Id IN (' . $placeholders . ')', $ids);
}
}
} else {
// Only the query and create tasks are supported at the moment
$message = 'Unrecognised task '.$_REQUEST['task'];
@ -80,8 +91,7 @@ function queryRequest() {
$table = 'Logs';
// The names of the dB columns in the log table we are interested in
$columns = array('TimeKey', 'Component', 'ServerId', 'Pid', 'Code', 'Message', 'File', 'Line');
$columns = array('Id', 'TimeKey', 'Component', 'ServerId', 'Pid', 'Code', 'Message', 'File', 'Line');
// The names of columns shown in the log view that are NOT dB columns in the database
$col_alt = array('DateTime', 'Server');

View File

@ -0,0 +1,23 @@
<?php
// Clear Logs confirmation modal
?>
<div id="clearLogsConfirm" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><?php echo translate('ConfirmClearLogsTitle') ?></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p><?php echo translate('ConfirmClearLogs') ?></p>
</div>
<div id="clearLogsProgressTicker"></div>
<div class="modal-footer">
<button id="clearLogsCancelBtn" type="button" class="btn btn-secondary" data-dismiss="modal"><?php echo translate('Cancel') ?></button>
<button id="clearLogsConfirmBtn" type="button" class="btn btn-danger"><?php echo translate('ClearLogs') ?></button>
</div>
</div>
</div>
</div>

View File

@ -45,6 +45,7 @@
Router::mapResources('user_preference');
Router::mapResources('zonepresets');
Router::mapResources('zones');
Router::mapResources('notifications');
Router::parseExtensions();

View File

@ -0,0 +1,183 @@
<?php
App::uses('AppController', 'Controller');
class NotificationsController extends AppController {
public $components = array('RequestHandler');
private function _isAdmin() {
global $user;
return (!$user) || ($user->System() == 'Edit');
}
private function _userId() {
global $user;
return $user ? $user->Id() : null;
}
public function beforeFilter() {
parent::beforeFilter();
// Any authenticated user can manage their own notifications.
// Per-row ownership checks are enforced in each action.
// When auth is disabled ($user is null), allow all access.
}
public function index() {
$conditions = array();
if (!$this->_isAdmin()) {
$conditions['Notification.UserId'] = $this->_userId();
}
$notifications = $this->Notification->find('all', array(
'conditions' => $conditions,
'recursive' => -1,
));
$this->set(array(
'notifications' => $notifications,
'_serialize' => array('notifications'),
));
}
public function view($id = null) {
$this->Notification->id = $id;
if (!$this->Notification->exists()) {
throw new NotFoundException(__('Invalid notification'));
}
$notification = $this->Notification->find('first', array(
'conditions' => array('Notification.Id' => $id),
'recursive' => -1,
));
if (!$this->_isAdmin() && $notification['Notification']['UserId'] != $this->_userId()) {
throw new UnauthorizedException(__('Insufficient Privileges'));
}
$this->set(array(
'notification' => $notification,
'_serialize' => array('notification'),
));
}
public function add() {
if (!$this->request->is('post')) {
throw new BadRequestException(__('POST required'));
}
$data = $this->request->data;
if (isset($data['Notification'])) {
$data = $data['Notification'];
}
if (!$this->_isAdmin() || !isset($data['UserId'])) {
$data['UserId'] = $this->_userId();
}
if (!isset($data['CreatedOn'])) {
$data['CreatedOn'] = date('Y-m-d H:i:s');
}
if (isset($data['Token'])) {
$existing = $this->Notification->find('first', array(
'conditions' => array('Notification.Token' => $data['Token']),
'recursive' => -1,
));
if ($existing) {
if (!$this->_isAdmin() && $existing['Notification']['UserId'] != $this->_userId()) {
throw new UnauthorizedException(__('Token belongs to another user'));
}
$this->Notification->id = $existing['Notification']['Id'];
unset($data['CreatedOn']);
} else {
$this->Notification->create();
}
} else {
$this->Notification->create();
}
if ($this->Notification->save(array('Notification' => $data))) {
$notification = $this->Notification->find('first', array(
'conditions' => array('Notification.Id' => $this->Notification->id),
'recursive' => -1,
));
$this->set(array(
'notification' => $notification,
'_serialize' => array('notification'),
));
} else {
$this->response->statusCode(400);
$this->set(array(
'message' => __('Could not save notification'),
'errors' => $this->Notification->validationErrors,
'_serialize' => array('message', 'errors'),
));
}
}
public function edit($id = null) {
$this->Notification->id = $id;
if (!$this->Notification->exists()) {
throw new NotFoundException(__('Invalid notification'));
}
$this->request->allowMethod('post', 'put');
$existing = $this->Notification->find('first', array(
'conditions' => array('Notification.Id' => $id),
'recursive' => -1,
));
if (!$this->_isAdmin() && $existing['Notification']['UserId'] != $this->_userId()) {
throw new UnauthorizedException(__('Insufficient Privileges'));
}
$data = $this->request->data;
if (isset($data['Notification'])) {
$data = $data['Notification'];
}
if (!$this->_isAdmin()) {
unset($data['UserId']);
}
if ($this->Notification->save(array('Notification' => $data))) {
$notification = $this->Notification->find('first', array(
'conditions' => array('Notification.Id' => $id),
'recursive' => -1,
));
$this->set(array(
'notification' => $notification,
'_serialize' => array('notification'),
));
} else {
$this->response->statusCode(400);
$this->set(array(
'message' => __('Could not save notification'),
'errors' => $this->Notification->validationErrors,
'_serialize' => array('message', 'errors'),
));
}
}
public function delete($id = null) {
$this->Notification->id = $id;
if (!$this->Notification->exists()) {
throw new NotFoundException(__('Invalid notification'));
}
$this->request->allowMethod('post', 'delete');
$existing = $this->Notification->find('first', array(
'conditions' => array('Notification.Id' => $id),
'recursive' => -1,
));
if (!$this->_isAdmin() && $existing['Notification']['UserId'] != $this->_userId()) {
throw new UnauthorizedException(__('Insufficient Privileges'));
}
if ($this->Notification->delete()) {
$this->set(array(
'message' => __('Notification deleted'),
'_serialize' => array('message'),
));
} else {
$this->response->statusCode(400);
$this->set(array(
'message' => __('Could not delete notification'),
'_serialize' => array('message'),
));
}
}
}

View File

@ -0,0 +1,39 @@
<?php
App::uses('AppModel', 'Model');
class Notification extends AppModel {
public $useTable = 'Notifications';
public $primaryKey = 'Id';
public $displayField = 'Token';
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'UserId',
),
);
public $validate = array(
'Token' => array(
'notBlank' => array(
'rule' => array('notBlank'),
'message' => 'Token is required',
),
),
'Platform' => array(
'inList' => array(
'rule' => array('inList', array('android', 'ios', 'web')),
'message' => 'Platform must be android, ios, or web',
),
),
'PushState' => array(
'inList' => array(
'rule' => array('inList', array('enabled', 'disabled')),
'message' => 'PushState must be enabled or disabled',
'required' => false,
),
),
);
}

View File

@ -1779,17 +1779,17 @@ async function attachVideo(monitorStream) {
Janus.debug(" ::: Got a remote track :::");
Janus.debug(track);
if (track.kind ==="audio") {
stream = new MediaStream();
const stream = new MediaStream();
stream.addTrack(track.clone());
if (document.getElementById("liveAudio" + id) == null) {
audioElement = document.createElement('audio');
const audioElement = document.createElement('audio');
audioElement.setAttribute("id", "liveAudio" + id);
audioElement.controls = true;
document.getElementById("imageFeed" + id).append(audioElement);
}
Janus.attachMediaStream(document.getElementById("liveAudio" + id), stream);
} else {
stream = new MediaStream();
const stream = new MediaStream();
stream.addTrack(track.clone());
Janus.attachMediaStream(document.getElementById("liveStream" + id), stream);
}

View File

@ -218,6 +218,7 @@ $SLANG = array(
'ChooseLogSelection' => 'Choose a log selection',
'ChoosePreset' => 'Choose Preset',
'ClassLabel' => 'Label',
'ClearLogs' => 'Clear Logs',
'CloneMonitor' => 'Clone',
'ConcurrentFilter' => 'Run filter concurrently',
'ConfigOptions' => 'ConfigOptions',
@ -225,6 +226,8 @@ $SLANG = array(
'ConfiguredFor' => 'Configured for',
'ConfigURL' => 'Config Base URL',
'ConfirmAction' => 'Action Confirmation',
'ConfirmClearLogs' => 'Are you sure you wish to delete the selected log entries?',
'ConfirmClearLogsTitle' => 'Clear Logs Confirmation',
'ConfirmDeleteControl' => 'Warning, deleting a control will reset all monitors that use it to be uncontrollable.<br><br>Are you sure you wish to delete?',
'ConfirmDeleteDevices' => 'Are you sure you wish to delete the selected devices?',
'ConfirmDeleteEvents' => 'Are you sure you wish to delete the selected events?',

View File

@ -0,0 +1 @@
bootstrap-table-1.27.0/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More