Merge branch 'master' into feat/notification-profile
commit
0206fcf963
|
|
@ -887,14 +887,14 @@ CREATE TABLE `ZonePresets` (
|
|||
`CheckMethod` enum('AlarmedPixels','FilteredPixels','Blobs') NOT NULL default 'Blobs',
|
||||
`MinPixelThreshold` smallint(5) unsigned default NULL,
|
||||
`MaxPixelThreshold` smallint(5) unsigned default NULL,
|
||||
`MinAlarmPixels` int(10) unsigned default NULL,
|
||||
`MaxAlarmPixels` int(10) unsigned default NULL,
|
||||
`MinAlarmPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
`MaxAlarmPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
`FilterX` tinyint(3) unsigned default NULL,
|
||||
`FilterY` tinyint(3) unsigned default NULL,
|
||||
`MinFilterPixels` int(10) unsigned default NULL,
|
||||
`MaxFilterPixels` int(10) unsigned default NULL,
|
||||
`MinBlobPixels` int(10) unsigned default NULL,
|
||||
`MaxBlobPixels` int(10) unsigned default NULL,
|
||||
`MinFilterPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
`MaxFilterPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
`MinBlobPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
`MaxBlobPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
`MinBlobs` smallint(5) unsigned default NULL,
|
||||
`MaxBlobs` smallint(5) unsigned default NULL,
|
||||
`OverloadFrames` smallint(5) unsigned NOT NULL default '0',
|
||||
|
|
@ -921,14 +921,14 @@ CREATE TABLE `Zones` (
|
|||
`CheckMethod` enum('AlarmedPixels','FilteredPixels','Blobs') NOT NULL default 'Blobs',
|
||||
`MinPixelThreshold` smallint(5) unsigned default NULL,
|
||||
`MaxPixelThreshold` smallint(5) unsigned default NULL,
|
||||
`MinAlarmPixels` int(10) unsigned default NULL,
|
||||
`MaxAlarmPixels` int(10) unsigned default NULL,
|
||||
`MinAlarmPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
`MaxAlarmPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
`FilterX` tinyint(3) unsigned default NULL,
|
||||
`FilterY` tinyint(3) unsigned default NULL,
|
||||
`MinFilterPixels` int(10) unsigned default NULL,
|
||||
`MaxFilterPixels` int(10) unsigned default NULL,
|
||||
`MinBlobPixels` int(10) unsigned default NULL,
|
||||
`MaxBlobPixels` int(10) unsigned default NULL,
|
||||
`MinFilterPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
`MaxFilterPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
`MinBlobPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
`MaxBlobPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
`MinBlobs` smallint(5) unsigned default NULL,
|
||||
`MaxBlobs` smallint(5) unsigned default NULL,
|
||||
`OverloadFrames` smallint(5) unsigned NOT NULL default '0',
|
||||
|
|
@ -1349,6 +1349,39 @@ CREATE TABLE `Notifications` (
|
|||
CONSTRAINT `Notifications_ibfk_1` FOREIGN KEY (`UserId`) REFERENCES `Users` (`Id`) ON DELETE CASCADE
|
||||
) ENGINE=@ZM_MYSQL_ENGINE@;
|
||||
|
||||
--
|
||||
-- Table structure for table `Menu_Items`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `Menu_Items`;
|
||||
CREATE TABLE `Menu_Items` (
|
||||
`Id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`MenuKey` varchar(32) NOT NULL,
|
||||
`Enabled` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`Label` varchar(64) DEFAULT NULL,
|
||||
`SortOrder` smallint NOT NULL DEFAULT 0,
|
||||
`Icon` varchar(128) DEFAULT NULL,
|
||||
`IconType` enum('material','fontawesome','image','none') NOT NULL DEFAULT 'material',
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `Menu_Items_MenuKey_idx` (`MenuKey`)
|
||||
) ENGINE=@ZM_MYSQL_ENGINE@;
|
||||
|
||||
INSERT INTO `Menu_Items` (`MenuKey`, `Enabled`, `SortOrder`) VALUES
|
||||
('Console', 1, 10),
|
||||
('Montage', 1, 20),
|
||||
('MontageReview', 1, 30),
|
||||
('Events', 1, 40),
|
||||
('Options', 1, 50),
|
||||
('Log', 1, 60),
|
||||
('Devices', 1, 70),
|
||||
('IntelGpu', 1, 80),
|
||||
('Groups', 1, 90),
|
||||
('Filters', 1, 100),
|
||||
('Snapshots', 1, 110),
|
||||
('Reports', 1, 120),
|
||||
('ReportEventAudit', 1, 130),
|
||||
('Map', 1, 140);
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,110 +0,0 @@
|
|||
--
|
||||
-- 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.
|
||||
--
|
||||
|
||||
DELIMITER //
|
||||
|
||||
DROP PROCEDURE IF EXISTS `zm_update_zone_coords_to_percent` //
|
||||
|
||||
CREATE PROCEDURE `zm_update_zone_coords_to_percent`()
|
||||
BEGIN
|
||||
DECLARE done INT DEFAULT FALSE;
|
||||
DECLARE v_zone_id INT;
|
||||
DECLARE v_coords TINYTEXT;
|
||||
DECLARE v_mon_width INT;
|
||||
DECLARE v_mon_height INT;
|
||||
DECLARE v_new_coords TEXT DEFAULT '';
|
||||
DECLARE v_pair TEXT;
|
||||
DECLARE v_x_str TEXT;
|
||||
DECLARE v_y_str TEXT;
|
||||
DECLARE v_x_pct TEXT;
|
||||
DECLARE v_y_pct TEXT;
|
||||
DECLARE v_remaining TEXT;
|
||||
DECLARE v_space_pos INT;
|
||||
|
||||
DECLARE cur CURSOR FOR
|
||||
SELECT z.Id, z.Coords, m.Width, m.Height
|
||||
FROM Zones z
|
||||
JOIN Monitors m ON z.MonitorId = m.Id
|
||||
WHERE m.Width > 0 AND m.Height > 0;
|
||||
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
|
||||
|
||||
OPEN cur;
|
||||
|
||||
read_loop: LOOP
|
||||
FETCH cur INTO v_zone_id, v_coords, v_mon_width, v_mon_height;
|
||||
IF done THEN
|
||||
LEAVE read_loop;
|
||||
END IF;
|
||||
|
||||
-- Skip if coords already look like percentages (contain a decimal point)
|
||||
IF v_coords LIKE '%.%' THEN
|
||||
ITERATE read_loop;
|
||||
END IF;
|
||||
|
||||
SET v_new_coords = '';
|
||||
SET v_remaining = TRIM(v_coords);
|
||||
|
||||
-- Parse each space-separated x,y pair
|
||||
coord_loop: LOOP
|
||||
IF v_remaining = '' OR v_remaining IS NULL THEN
|
||||
LEAVE coord_loop;
|
||||
END IF;
|
||||
|
||||
SET v_space_pos = LOCATE(' ', v_remaining);
|
||||
IF v_space_pos > 0 THEN
|
||||
SET v_pair = LEFT(v_remaining, v_space_pos - 1);
|
||||
SET v_remaining = TRIM(SUBSTRING(v_remaining, v_space_pos + 1));
|
||||
ELSE
|
||||
SET v_pair = v_remaining;
|
||||
SET v_remaining = '';
|
||||
END IF;
|
||||
|
||||
-- Skip empty pairs (from double spaces, trailing commas etc)
|
||||
IF v_pair = '' OR v_pair = ',' THEN
|
||||
ITERATE coord_loop;
|
||||
END IF;
|
||||
|
||||
-- Split on comma
|
||||
SET v_x_str = SUBSTRING_INDEX(v_pair, ',', 1);
|
||||
SET v_y_str = SUBSTRING_INDEX(v_pair, ',', -1);
|
||||
|
||||
-- Convert to percentage with 2 decimal places
|
||||
-- Use CAST to DECIMAL which always uses '.' as decimal separator (locale-independent)
|
||||
SET v_x_pct = CAST(ROUND(CAST(v_x_str AS DECIMAL(10,2)) / v_mon_width * 100, 2) AS DECIMAL(10,2));
|
||||
SET v_y_pct = CAST(ROUND(CAST(v_y_str AS DECIMAL(10,2)) / v_mon_height * 100, 2) AS DECIMAL(10,2));
|
||||
|
||||
IF v_new_coords != '' THEN
|
||||
SET v_new_coords = CONCAT(v_new_coords, ' ');
|
||||
END IF;
|
||||
SET v_new_coords = CONCAT(v_new_coords, v_x_pct, ',', v_y_pct);
|
||||
|
||||
END LOOP coord_loop;
|
||||
|
||||
IF v_new_coords != '' THEN
|
||||
UPDATE Zones SET Coords = v_new_coords WHERE Id = v_zone_id;
|
||||
END IF;
|
||||
|
||||
END LOOP read_loop;
|
||||
|
||||
CLOSE cur;
|
||||
END //
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
CALL zm_update_zone_coords_to_percent();
|
||||
DROP PROCEDURE IF EXISTS `zm_update_zone_coords_to_percent`;
|
||||
|
||||
-- Recalculate Area from pixel-space to percentage-space (100x100 = 10000 for full frame)
|
||||
UPDATE Zones z
|
||||
JOIN Monitors m ON z.MonitorId = m.Id
|
||||
SET z.Area = ROUND(z.Area * 10000.0 / (m.Width * m.Height))
|
||||
WHERE m.Width > 0 AND m.Height > 0 AND z.Area > 0;
|
||||
|
||||
-- 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';
|
||||
|
|
@ -1,3 +1,112 @@
|
|||
--
|
||||
-- Convert Zone Coords from pixel values to percentage values (0.00-100.00)
|
||||
-- so that zones are resolution-independent.
|
||||
--
|
||||
|
||||
DELIMITER //
|
||||
|
||||
DROP PROCEDURE IF EXISTS `zm_update_zone_coords_to_percent` //
|
||||
|
||||
CREATE PROCEDURE `zm_update_zone_coords_to_percent`()
|
||||
BEGIN
|
||||
DECLARE done INT DEFAULT FALSE;
|
||||
DECLARE v_zone_id INT;
|
||||
DECLARE v_coords TINYTEXT;
|
||||
DECLARE v_mon_width INT;
|
||||
DECLARE v_mon_height INT;
|
||||
DECLARE v_new_coords TEXT DEFAULT '';
|
||||
DECLARE v_pair TEXT;
|
||||
DECLARE v_x_str TEXT;
|
||||
DECLARE v_y_str TEXT;
|
||||
DECLARE v_x_pct TEXT;
|
||||
DECLARE v_y_pct TEXT;
|
||||
DECLARE v_remaining TEXT;
|
||||
DECLARE v_space_pos INT;
|
||||
|
||||
DECLARE cur CURSOR FOR
|
||||
SELECT z.Id, z.Coords, m.Width, m.Height
|
||||
FROM Zones z
|
||||
JOIN Monitors m ON z.MonitorId = m.Id
|
||||
WHERE m.Width > 0 AND m.Height > 0;
|
||||
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
|
||||
|
||||
OPEN cur;
|
||||
|
||||
read_loop: LOOP
|
||||
FETCH cur INTO v_zone_id, v_coords, v_mon_width, v_mon_height;
|
||||
IF done THEN
|
||||
LEAVE read_loop;
|
||||
END IF;
|
||||
|
||||
-- Skip if coords already look like percentages (contain a decimal point)
|
||||
IF v_coords LIKE '%.%' THEN
|
||||
ITERATE read_loop;
|
||||
END IF;
|
||||
|
||||
SET v_new_coords = '';
|
||||
SET v_remaining = TRIM(v_coords);
|
||||
|
||||
-- Parse each space-separated x,y pair
|
||||
coord_loop: LOOP
|
||||
IF v_remaining = '' OR v_remaining IS NULL THEN
|
||||
LEAVE coord_loop;
|
||||
END IF;
|
||||
|
||||
SET v_space_pos = LOCATE(' ', v_remaining);
|
||||
IF v_space_pos > 0 THEN
|
||||
SET v_pair = LEFT(v_remaining, v_space_pos - 1);
|
||||
SET v_remaining = TRIM(SUBSTRING(v_remaining, v_space_pos + 1));
|
||||
ELSE
|
||||
SET v_pair = v_remaining;
|
||||
SET v_remaining = '';
|
||||
END IF;
|
||||
|
||||
-- Skip empty pairs (from double spaces, trailing commas etc)
|
||||
IF v_pair = '' OR v_pair = ',' THEN
|
||||
ITERATE coord_loop;
|
||||
END IF;
|
||||
|
||||
-- Split on comma
|
||||
SET v_x_str = SUBSTRING_INDEX(v_pair, ',', 1);
|
||||
SET v_y_str = SUBSTRING_INDEX(v_pair, ',', -1);
|
||||
|
||||
-- Convert to percentage with 2 decimal places
|
||||
-- Use CAST to DECIMAL which always uses '.' as decimal separator (locale-independent)
|
||||
SET v_x_pct = CAST(ROUND(CAST(v_x_str AS DECIMAL(10,2)) / v_mon_width * 100, 2) AS DECIMAL(10,2));
|
||||
SET v_y_pct = CAST(ROUND(CAST(v_y_str AS DECIMAL(10,2)) / v_mon_height * 100, 2) AS DECIMAL(10,2));
|
||||
|
||||
IF v_new_coords != '' THEN
|
||||
SET v_new_coords = CONCAT(v_new_coords, ' ');
|
||||
END IF;
|
||||
SET v_new_coords = CONCAT(v_new_coords, v_x_pct, ',', v_y_pct);
|
||||
|
||||
END LOOP coord_loop;
|
||||
|
||||
IF v_new_coords != '' THEN
|
||||
UPDATE Zones SET Coords = v_new_coords WHERE Id = v_zone_id;
|
||||
END IF;
|
||||
|
||||
END LOOP read_loop;
|
||||
|
||||
CLOSE cur;
|
||||
END //
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
CALL zm_update_zone_coords_to_percent();
|
||||
DROP PROCEDURE IF EXISTS `zm_update_zone_coords_to_percent`;
|
||||
|
||||
-- Recalculate Area from pixel-space to percentage-space (100x100 = 10000 for full frame)
|
||||
UPDATE Zones z
|
||||
JOIN Monitors m ON z.MonitorId = m.Id
|
||||
SET z.Area = ROUND(z.Area * 10000.0 / (m.Width * m.Height))
|
||||
WHERE m.Width > 0 AND m.Height > 0 AND z.Area > 0;
|
||||
|
||||
-- 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
|
||||
--
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
--
|
||||
-- Add Profile column to Notifications table
|
||||
--
|
||||
|
|
@ -14,3 +15,187 @@ SET @s = (SELECT IF(
|
|||
PREPARE stmt FROM @s;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Convert zone threshold fields (MinAlarmPixels, etc.) from pixel counts
|
||||
-- to percentages of zone area, matching the coordinate percentage migration
|
||||
-- done in zm_update-1.39.2.sql.
|
||||
--
|
||||
|
||||
-- First convert existing pixel count values to percentages WHILE columns
|
||||
-- are still INT (values 0-100 fit in INT; ALTER to DECIMAL would fail on
|
||||
-- large pixel counts that exceed DECIMAL(10,2) max of 99999.99).
|
||||
--
|
||||
-- Zone pixel area = (Zones.Area * Monitors.Width * Monitors.Height) / 10000
|
||||
-- where Zones.Area is in percentage-space (0-10000) from zm_update-1.39.2.sql.
|
||||
-- new_percent = old_pixel_count * 100 / zone_pixel_area
|
||||
-- = old_pixel_count * 1000000 / (Zones.Area * Monitors.Width * Monitors.Height)
|
||||
--
|
||||
-- Only convert zones with percentage coordinates (contain '.') that still have
|
||||
-- pixel-scale threshold values (> 100 means it can't be a percentage).
|
||||
|
||||
UPDATE Zones z
|
||||
JOIN Monitors m ON z.MonitorId = m.Id
|
||||
SET
|
||||
z.MinAlarmPixels = CASE
|
||||
WHEN z.MinAlarmPixels IS NULL THEN NULL
|
||||
WHEN z.MinAlarmPixels = 0 THEN 0
|
||||
WHEN z.Area > 0 AND m.Width > 0 AND m.Height > 0
|
||||
THEN LEAST(ROUND(z.MinAlarmPixels * 1000000.0 / (z.Area * m.Width * m.Height)), 100)
|
||||
ELSE z.MinAlarmPixels END,
|
||||
z.MaxAlarmPixels = CASE
|
||||
WHEN z.MaxAlarmPixels IS NULL THEN NULL
|
||||
WHEN z.MaxAlarmPixels = 0 THEN 0
|
||||
WHEN z.Area > 0 AND m.Width > 0 AND m.Height > 0
|
||||
THEN LEAST(ROUND(z.MaxAlarmPixels * 1000000.0 / (z.Area * m.Width * m.Height)), 100)
|
||||
ELSE z.MaxAlarmPixels END,
|
||||
z.MinFilterPixels = CASE
|
||||
WHEN z.MinFilterPixels IS NULL THEN NULL
|
||||
WHEN z.MinFilterPixels = 0 THEN 0
|
||||
WHEN z.Area > 0 AND m.Width > 0 AND m.Height > 0
|
||||
THEN LEAST(ROUND(z.MinFilterPixels * 1000000.0 / (z.Area * m.Width * m.Height)), 100)
|
||||
ELSE z.MinFilterPixels END,
|
||||
z.MaxFilterPixels = CASE
|
||||
WHEN z.MaxFilterPixels IS NULL THEN NULL
|
||||
WHEN z.MaxFilterPixels = 0 THEN 0
|
||||
WHEN z.Area > 0 AND m.Width > 0 AND m.Height > 0
|
||||
THEN LEAST(ROUND(z.MaxFilterPixels * 1000000.0 / (z.Area * m.Width * m.Height)), 100)
|
||||
ELSE z.MaxFilterPixels END,
|
||||
z.MinBlobPixels = CASE
|
||||
WHEN z.MinBlobPixels IS NULL THEN NULL
|
||||
WHEN z.MinBlobPixels = 0 THEN 0
|
||||
WHEN z.Area > 0 AND m.Width > 0 AND m.Height > 0
|
||||
THEN LEAST(ROUND(z.MinBlobPixels * 1000000.0 / (z.Area * m.Width * m.Height)), 100)
|
||||
ELSE z.MinBlobPixels END,
|
||||
z.MaxBlobPixels = CASE
|
||||
WHEN z.MaxBlobPixels IS NULL THEN NULL
|
||||
WHEN z.MaxBlobPixels = 0 THEN 0
|
||||
WHEN z.Area > 0 AND m.Width > 0 AND m.Height > 0
|
||||
THEN LEAST(ROUND(z.MaxBlobPixels * 1000000.0 / (z.Area * m.Width * m.Height)), 100)
|
||||
ELSE z.MaxBlobPixels END
|
||||
WHERE z.Coords LIKE '%.%'
|
||||
AND (z.MinAlarmPixels > 100 OR z.MaxAlarmPixels > 100
|
||||
OR z.MinFilterPixels > 100 OR z.MaxFilterPixels > 100
|
||||
OR z.MinBlobPixels > 100 OR z.MaxBlobPixels > 100);
|
||||
|
||||
-- Now change threshold columns from int to DECIMAL(10,2) to store percentages
|
||||
-- with 2 decimal places (e.g. 25.50 = 25.50% of zone area).
|
||||
-- Values are now 0-100 from the UPDATE above, so they fit in DECIMAL(10,2).
|
||||
|
||||
SET @s = (SELECT IF(
|
||||
(SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE()
|
||||
AND table_name = 'Zones' AND column_name = 'MinAlarmPixels'
|
||||
) = 'decimal',
|
||||
"SELECT 'Zones threshold columns already DECIMAL'",
|
||||
"ALTER TABLE `Zones`
|
||||
MODIFY `MinAlarmPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
MODIFY `MaxAlarmPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
MODIFY `MinFilterPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
MODIFY `MaxFilterPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
MODIFY `MinBlobPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
MODIFY `MaxBlobPixels` DECIMAL(10,2) unsigned default NULL"
|
||||
));
|
||||
|
||||
PREPARE stmt FROM @s;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Also update ZonePresets table column types for consistency
|
||||
SET @s = (SELECT IF(
|
||||
(SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE()
|
||||
AND table_name = 'ZonePresets' AND column_name = 'MinAlarmPixels'
|
||||
) = 'decimal',
|
||||
"SELECT 'ZonePresets threshold columns already DECIMAL'",
|
||||
"ALTER TABLE `ZonePresets`
|
||||
MODIFY `MinAlarmPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
MODIFY `MaxAlarmPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
MODIFY `MinFilterPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
MODIFY `MaxFilterPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
MODIFY `MinBlobPixels` DECIMAL(10,2) unsigned default NULL,
|
||||
MODIFY `MaxBlobPixels` DECIMAL(10,2) unsigned default NULL"
|
||||
));
|
||||
|
||||
PREPARE stmt FROM @s;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
--
|
||||
-- Add Menu_Items table for customizable navbar/sidebar menu
|
||||
--
|
||||
|
||||
SET @s = (SELECT IF(
|
||||
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = DATABASE()
|
||||
AND table_name = 'Menu_Items'
|
||||
) > 0,
|
||||
"SELECT 'Table Menu_Items already exists'",
|
||||
"CREATE TABLE `Menu_Items` (
|
||||
`Id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`MenuKey` varchar(32) NOT NULL,
|
||||
`Enabled` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`Label` varchar(64) DEFAULT NULL,
|
||||
`SortOrder` smallint NOT NULL DEFAULT 0,
|
||||
`Icon` varchar(128) DEFAULT NULL,
|
||||
`IconType` enum('material','fontawesome','image','none') NOT NULL DEFAULT 'material',
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `Menu_Items_MenuKey_idx` (`MenuKey`)
|
||||
) ENGINE=InnoDB"
|
||||
));
|
||||
|
||||
PREPARE stmt FROM @s;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
--
|
||||
-- Seed default menu items if table is empty
|
||||
--
|
||||
|
||||
SET @s = (SELECT IF(
|
||||
(SELECT COUNT(*) FROM `Menu_Items`) > 0,
|
||||
"SELECT 'Menu_Items already has data'",
|
||||
"INSERT INTO `Menu_Items` (`MenuKey`, `Enabled`, `SortOrder`) VALUES
|
||||
('Console', 1, 10),
|
||||
('Montage', 1, 20),
|
||||
('MontageReview', 1, 30),
|
||||
('Events', 1, 40),
|
||||
('Options', 1, 50),
|
||||
('Log', 1, 60),
|
||||
('Devices', 1, 70),
|
||||
('IntelGpu', 1, 80),
|
||||
('Groups', 1, 90),
|
||||
('Filters', 1, 100),
|
||||
('Snapshots', 1, 110),
|
||||
('Reports', 1, 120),
|
||||
('ReportEventAudit', 1, 130),
|
||||
('Map', 1, 140)"
|
||||
));
|
||||
|
||||
PREPARE stmt FROM @s;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
--
|
||||
-- Add Icon and IconType columns if they don't exist
|
||||
--
|
||||
|
||||
SET @s = (SELECT IF(
|
||||
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE()
|
||||
AND table_name = 'Menu_Items' AND column_name = 'Icon'
|
||||
) > 0,
|
||||
"SELECT 'Column Icon already exists'",
|
||||
"ALTER TABLE `Menu_Items` ADD `Icon` varchar(128) DEFAULT NULL AFTER `SortOrder`"
|
||||
));
|
||||
|
||||
PREPARE stmt FROM @s;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @s = (SELECT IF(
|
||||
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE()
|
||||
AND table_name = 'Menu_Items' AND column_name = 'IconType'
|
||||
) > 0,
|
||||
"SELECT 'Column IconType already exists'",
|
||||
"ALTER TABLE `Menu_Items` ADD `IconType` enum('material','fontawesome','image','none') NOT NULL DEFAULT 'material' AFTER `Icon`"
|
||||
));
|
||||
|
||||
PREPARE stmt FROM @s;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
|
|
|||
|
|
@ -7,5 +7,7 @@ add_subdirectory(RtspServer)
|
|||
add_subdirectory(span-lite)
|
||||
set(ENABLE_INSTALL_SAVED "${ENABLE_INSTALL}")
|
||||
set(ENABLE_INSTALL OFF)
|
||||
set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
|
||||
add_subdirectory(CxxUrl)
|
||||
unset(CMAKE_POLICY_DEFAULT_CMP0077)
|
||||
set(ENABLE_INSTALL "${ENABLE_INSTALL_SAVED}")
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 24e6b7153aa561ecc4123cc7c8fc1b530cde0bc9
|
||||
Subproject commit a071599575f60a6f1f424e1f9b408dad7da734a8
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
%global zmtargetdistro %{?rhel:el%{rhel}}%{!?rhel:fc%{fedora}}
|
||||
|
||||
Name: zoneminder
|
||||
Version: 1.39.0
|
||||
Version: 1.39.3
|
||||
Release: 1%{?dist}
|
||||
Summary: A camera monitoring and analysis tool
|
||||
Group: System Environment/Daemons
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ module.exports = defineConfig([{
|
|||
"web/js/fontfaceobserver.standalone.js",
|
||||
"web/skins/classic/js/bootstrap-4.5.0.js",
|
||||
"web/skins/classic/js/bootstrap.bundle.min.js",
|
||||
"web/skins/classic/js/bootstrap-table-1.23.5",
|
||||
"web/skins/classic/assets",
|
||||
"web/skins/classic/js/chosen",
|
||||
"web/skins/classic/js/dateTimePicker",
|
||||
"web/skins/classic/js/jquery-*.js",
|
||||
|
|
|
|||
|
|
@ -535,6 +535,22 @@ our @options = (
|
|||
type => $types{boolean},
|
||||
category => 'system',
|
||||
},
|
||||
{
|
||||
name => 'ZM_OPT_USE_REMEMBER_ME',
|
||||
default => 'no',
|
||||
description => 'Show a "Remember Me" option on the login page',
|
||||
help => q`
|
||||
When enabled, a "Remember Me" checkbox will appear on the login
|
||||
page. If the user does not check "Remember Me", the session
|
||||
cookie will expire when the browser is closed. If checked, the
|
||||
session will persist for the duration configured by
|
||||
ZM_COOKIE_LIFETIME. When this option is disabled, sessions
|
||||
always persist for ZM_COOKIE_LIFETIME as before.
|
||||
`,
|
||||
requires => [ { name=>'ZM_OPT_USE_AUTH', value=>'yes' } ],
|
||||
type => $types{boolean},
|
||||
category => 'auth',
|
||||
},
|
||||
# Google reCaptcha settings
|
||||
{
|
||||
name => 'ZM_OPT_USE_GOOG_RECAPTCHA',
|
||||
|
|
@ -2974,6 +2990,30 @@ our @options = (
|
|||
},
|
||||
category => 'web',
|
||||
},
|
||||
{
|
||||
name => 'ZM_WEB_FILTER_SETTINGS_POSITION',
|
||||
default => 'sidebar',
|
||||
description => 'Where to position the monitor filter settings panel on views.',
|
||||
help => q`
|
||||
Controls how the monitor filter settings panel is displayed on
|
||||
views that support it (Console, Watch, Montage, Montage Review).
|
||||
When set to 'Embedded in Left Sidebar' (default), the filter
|
||||
panel is moved into the left sidebar extruder. This option only
|
||||
takes effect when ZM_WEB_NAVBAR_TYPE is set to 'left'. When
|
||||
the navbar is not on the left, filters use the standard
|
||||
toggle icon regardless of this setting.
|
||||
Setting this to 'inline' makes the filter panel always visible
|
||||
at the top of the page.
|
||||
`,
|
||||
type => {
|
||||
db_type => 'string',
|
||||
hint => 'Embedded in Left Sidebar|inline',
|
||||
pattern => qr|^([EiI])|i,
|
||||
format => q( ($1 =~ /^i/i) ? 'inline' : 'sidebar' )
|
||||
},
|
||||
requires => [ { name=>'ZM_WEB_NAVBAR_TYPE', value=>'left' } ],
|
||||
category => 'web',
|
||||
},
|
||||
{
|
||||
name => 'ZM_WEB_H_REFRESH_MAIN',
|
||||
default => '240',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -347,12 +347,20 @@ sub control {
|
|||
my $command = shift;
|
||||
my $process = shift;
|
||||
|
||||
my $valid_device = (defined $monitor->{Device} and $monitor->{Device} =~ /^\/dev\/[\w\/.\-]+$/);
|
||||
if ($monitor->{Type} eq 'Local' and !$valid_device) {
|
||||
Error("Invalid device path rejected: $monitor->{Device}");
|
||||
return;
|
||||
} elsif (!$valid_device and defined $monitor->{Device} and length($monitor->{Device})) {
|
||||
Warning("Monitor $$monitor{Id} has invalid device path: $monitor->{Device}");
|
||||
}
|
||||
|
||||
if ($command eq 'stop') {
|
||||
if ($process) {
|
||||
ZoneMinder::General::runCommand("zmdc.pl stop $process -m $$monitor{Id}");
|
||||
} else {
|
||||
if ($monitor->{Type} eq 'Local') {
|
||||
ZoneMinder::General::runCommand('zmdc.pl stop zmc -d '.$monitor->{Device});
|
||||
ZoneMinder::General::runCommand("zmdc.pl stop zmc -d '$monitor->{Device}'");
|
||||
} else {
|
||||
ZoneMinder::General::runCommand('zmdc.pl stop zmc -m '.$monitor->{Id});
|
||||
}
|
||||
|
|
@ -362,7 +370,7 @@ sub control {
|
|||
ZoneMinder::General::runCommand("zmdc.pl start $process -m $$monitor{Id}");
|
||||
} else {
|
||||
if ($monitor->{Type} eq 'Local') {
|
||||
ZoneMinder::General::runCommand('zmdc.pl start zmc -d '.$monitor->{Device});
|
||||
ZoneMinder::General::runCommand("zmdc.pl start zmc -d '$monitor->{Device}'");
|
||||
} else {
|
||||
ZoneMinder::General::runCommand('zmdc.pl start zmc -m '.$monitor->{Id});
|
||||
}
|
||||
|
|
@ -372,7 +380,7 @@ sub control {
|
|||
ZoneMinder::General::runCommand("zmdc.pl restart $process -m $$monitor{Id}");
|
||||
} else {
|
||||
if ($monitor->{Type} eq 'Local') {
|
||||
ZoneMinder::General::runCommand('zmdc.pl restart zmc -d '.$monitor->{Device});
|
||||
ZoneMinder::General::runCommand("zmdc.pl restart zmc -d '$monitor->{Device}'");
|
||||
} else {
|
||||
ZoneMinder::General::runCommand('zmdc.pl restart zmc -m '.$monitor->{Id});
|
||||
}
|
||||
|
|
@ -498,7 +506,11 @@ sub zmcControl {
|
|||
|
||||
if ((!$ZoneMinder::Config{ZM_SERVER_ID}) or ( $$self{ServerId} and ($ZoneMinder::Config{ZM_SERVER_ID}==$$self{ServerId}) )) {
|
||||
if ($$self{Type} eq 'Local') {
|
||||
$zmcArgs .= '-d '.$self->{Device};
|
||||
if (!defined $$self{Device} or $$self{Device} !~ /^\/dev\/[\w\/.\-]+$/) {
|
||||
Error("Invalid device path rejected: $$self{Device}");
|
||||
return;
|
||||
}
|
||||
$zmcArgs .= "-d '$$self{Device}'";
|
||||
} else {
|
||||
$zmcArgs .= '-m '.$self->{Id};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -211,7 +211,11 @@ if ( $command =~ /^(?:start|restart)$/ ) {
|
|||
foreach my $monitor (@monitors) {
|
||||
if (($monitor->{Capturing} ne 'None') and ($monitor->{Type} ne 'WebSite')) {
|
||||
if ( $monitor->{Type} eq 'Local' ) {
|
||||
runCommand("zmdc.pl start zmc -d $monitor->{Device}");
|
||||
if ($monitor->{Device} !~ /^\/dev\/[\w\/.\-]+$/) {
|
||||
Error("Invalid device path rejected for monitor $monitor->{Id}: $monitor->{Device}");
|
||||
next;
|
||||
}
|
||||
runCommand("zmdc.pl start zmc -d '$monitor->{Device}'");
|
||||
} else {
|
||||
runCommand("zmdc.pl start zmc -m $monitor->{Id}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,8 +44,13 @@ int FFmpeg_Input::Open(const char *filepath) {
|
|||
/** Open the input file to read from it. */
|
||||
error = avformat_open_input(&input_format_context, filepath, nullptr, nullptr);
|
||||
if ( error < 0 ) {
|
||||
Error("Could not open input file '%s' (error '%s')",
|
||||
filepath, av_make_error_string(error).c_str());
|
||||
if (std::string(filepath).find("incomplete") != std::string::npos) {
|
||||
Warning("Could not open input file '%s' (error '%s')",
|
||||
filepath, av_make_error_string(error).c_str());
|
||||
} else {
|
||||
Error("Could not open input file '%s' (error '%s')",
|
||||
filepath, av_make_error_string(error).c_str());
|
||||
}
|
||||
input_format_context = nullptr;
|
||||
return error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,22 @@ std::vector<Group_Permission> Group_Permission::find(int p_user_id) {
|
|||
return results;
|
||||
}
|
||||
|
||||
std::vector<Group_Permission> Group_Permission::findByRole(int role_id) {
|
||||
std::vector<Group_Permission> results;
|
||||
std::string sql = stringtf("SELECT `Id`,`RoleId`,`GroupId`,`Permission`+0 FROM Role_Groups_Permissions WHERE `RoleId`='%d'", role_id);
|
||||
|
||||
MYSQL_RES *result = zmDbFetch(sql.c_str());
|
||||
|
||||
if (result) {
|
||||
results.reserve(mysql_num_rows(result));
|
||||
while (MYSQL_ROW dbrow = mysql_fetch_row(result)) {
|
||||
results.push_back(Group_Permission(dbrow));
|
||||
}
|
||||
mysql_free_result(result);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
void Group_Permission::loadMonitorIds() {
|
||||
Group group(group_id);
|
||||
monitor_ids = group.MonitorIds();
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ class Group_Permission {
|
|||
void loadMonitorIds();
|
||||
|
||||
static std::vector<Group_Permission> find(int p_user_id);
|
||||
static std::vector<Group_Permission> findByRole(int role_id);
|
||||
};
|
||||
|
||||
#endif // ZM_GROUP_PERMISSION_H
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -60,3 +60,19 @@ std::vector<Monitor_Permission> Monitor_Permission::find(int p_user_id) {
|
|||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
std::vector<Monitor_Permission> Monitor_Permission::findByRole(int role_id) {
|
||||
std::vector<Monitor_Permission> results;
|
||||
std::string sql = stringtf("SELECT `Id`,`RoleId`,`MonitorId`,`Permission`+0 FROM Role_Monitors_Permissions WHERE `RoleId`='%d'", role_id);
|
||||
|
||||
MYSQL_RES *result = zmDbFetch(sql.c_str());
|
||||
|
||||
if (result) {
|
||||
results.reserve(mysql_num_rows(result));
|
||||
while (MYSQL_ROW dbrow = mysql_fetch_row(result)) {
|
||||
results.push_back(Monitor_Permission(dbrow));
|
||||
}
|
||||
mysql_free_result(result);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ class Monitor_Permission {
|
|||
Permission getPermission() const { return permission; }
|
||||
|
||||
static std::vector<Monitor_Permission> find(int p_user_id);
|
||||
static std::vector<Monitor_Permission> findByRole(int role_id);
|
||||
};
|
||||
|
||||
#endif // ZM_MOnitor_PERMISSION_H
|
||||
|
|
|
|||
|
|
@ -255,6 +255,7 @@ void MonitorStream::processCommand(const CmdMsg *msg) {
|
|||
bool forced;
|
||||
int score;
|
||||
int analysing;
|
||||
bool analysis_image;
|
||||
} status_data;
|
||||
|
||||
status_data.id = monitor->Id();
|
||||
|
|
@ -302,7 +303,10 @@ void MonitorStream::processCommand(const CmdMsg *msg) {
|
|||
status_data.delay = FPSeconds(now - last_frame_sent).count();
|
||||
status_data.zoom = zoom;
|
||||
status_data.scale = scale;
|
||||
Debug(2, "viewing fps: %.2f capture_fps: %.2f analysis_fps: %.2f Buffer Level:%d, Delayed:%d, Paused:%d, Rate:%d, delay:%.3f, Zoom:%d, Enabled:%d Forced:%d score: %d",
|
||||
status_data.analysis_image = (frame_type == FRAME_ANALYSIS) &&
|
||||
monitor->ShmValid() &&
|
||||
(monitor->Analysing() != Monitor::ANALYSING_NONE);
|
||||
Debug(2, "viewing fps: %.2f capture_fps: %.2f analysis_fps: %.2f Buffer Level:%d, Delayed:%d, Paused:%d, Rate:%d, delay:%.3f, Zoom:%d, Enabled:%d Forced:%d score: %d analysis_image: %d",
|
||||
status_data.fps,
|
||||
status_data.capture_fps,
|
||||
status_data.analysis_fps,
|
||||
|
|
@ -314,7 +318,8 @@ void MonitorStream::processCommand(const CmdMsg *msg) {
|
|||
status_data.zoom,
|
||||
status_data.enabled,
|
||||
status_data.forced,
|
||||
status_data.score
|
||||
status_data.score,
|
||||
status_data.analysis_image
|
||||
);
|
||||
|
||||
DataMsg status_msg;
|
||||
|
|
|
|||
118
src/zm_user.cpp
118
src/zm_user.cpp
|
|
@ -25,7 +25,7 @@
|
|||
#include "zm_utils.h"
|
||||
#include <cstring>
|
||||
|
||||
User::User() : id(0), enabled(false) {
|
||||
User::User() : id(0), enabled(false), role_id(0) {
|
||||
username[0] = password[0] = 0;
|
||||
stream = events = control = monitors = system = PERM_NONE;
|
||||
}
|
||||
|
|
@ -41,8 +41,15 @@ User::User(const MYSQL_ROW &dbrow) {
|
|||
control = (Permission)atoi(dbrow[index++]);
|
||||
monitors = (Permission)atoi(dbrow[index++]);
|
||||
system = (Permission)atoi(dbrow[index++]);
|
||||
role_id = atoi(dbrow[index++]);
|
||||
monitor_permissions_loaded = false;
|
||||
group_permissions_loaded = false;
|
||||
role_monitor_permissions_loaded = false;
|
||||
role_group_permissions_loaded = false;
|
||||
|
||||
if (role_id > 0) {
|
||||
loadRoleBasePermissions();
|
||||
}
|
||||
}
|
||||
|
||||
User::~User() {
|
||||
|
|
@ -58,10 +65,15 @@ void User::Copy(const User &u) {
|
|||
control = u.control;
|
||||
monitors = u.monitors;
|
||||
system = u.system;
|
||||
role_id = u.role_id;
|
||||
monitor_permissions_loaded = u.monitor_permissions_loaded;
|
||||
monitor_permissions = u.monitor_permissions;
|
||||
group_permissions_loaded = u.group_permissions_loaded;
|
||||
group_permissions = u.group_permissions;
|
||||
role_monitor_permissions_loaded = u.role_monitor_permissions_loaded;
|
||||
role_monitor_permissions = u.role_monitor_permissions;
|
||||
role_group_permissions_loaded = u.role_group_permissions_loaded;
|
||||
role_group_permissions = u.role_group_permissions;
|
||||
}
|
||||
|
||||
void User::loadMonitorPermissions() {
|
||||
|
|
@ -76,6 +88,48 @@ void User::loadGroupPermissions() {
|
|||
Debug(1, "# of Group_Permissions %zu", group_permissions.size());
|
||||
}
|
||||
|
||||
void User::loadRoleBasePermissions() {
|
||||
std::string sql = stringtf("SELECT `Stream`+0, `Events`+0, `Control`+0, `Monitors`+0, `System`+0"
|
||||
" FROM `User_Roles` WHERE `Id` = %d", role_id);
|
||||
MYSQL_RES *result = zmDbFetch(sql);
|
||||
if (!result) return;
|
||||
if (mysql_num_rows(result) != 1) {
|
||||
mysql_free_result(result);
|
||||
return;
|
||||
}
|
||||
MYSQL_ROW dbrow = mysql_fetch_row(result);
|
||||
Permission role_stream = (Permission)atoi(dbrow[0]);
|
||||
Permission role_events = (Permission)atoi(dbrow[1]);
|
||||
Permission role_control = (Permission)atoi(dbrow[2]);
|
||||
Permission role_monitors = (Permission)atoi(dbrow[3]);
|
||||
Permission role_system = (Permission)atoi(dbrow[4]);
|
||||
mysql_free_result(result);
|
||||
|
||||
// Use role permission as fallback when user permission is PERM_NONE
|
||||
if (stream == PERM_NONE && role_stream > PERM_NONE) stream = role_stream;
|
||||
if (events == PERM_NONE && role_events > PERM_NONE) events = role_events;
|
||||
if (control == PERM_NONE && role_control > PERM_NONE) control = role_control;
|
||||
if (monitors == PERM_NONE && role_monitors > PERM_NONE) monitors = role_monitors;
|
||||
if (system == PERM_NONE && role_system > PERM_NONE) system = role_system;
|
||||
|
||||
Debug(1, "Merged role %d base permissions: stream=%d events=%d control=%d monitors=%d system=%d",
|
||||
role_id, stream, events, control, monitors, system);
|
||||
}
|
||||
|
||||
void User::loadRoleMonitorPermissions() {
|
||||
for (const Monitor_Permission &p : Monitor_Permission::findByRole(role_id)) {
|
||||
role_monitor_permissions[p.MonitorId()] = p;
|
||||
}
|
||||
role_monitor_permissions_loaded = true;
|
||||
Debug(1, "# of Role_Monitor_Permissions %zu", role_monitor_permissions.size());
|
||||
}
|
||||
|
||||
void User::loadRoleGroupPermissions() {
|
||||
role_group_permissions = Group_Permission::findByRole(role_id);
|
||||
role_group_permissions_loaded = true;
|
||||
Debug(1, "# of Role_Group_Permissions %zu", role_group_permissions.size());
|
||||
}
|
||||
|
||||
bool User::canAccess(int monitor_id) {
|
||||
if (!monitor_permissions_loaded) loadMonitorPermissions();
|
||||
auto it = monitor_permissions.find(monitor_id);
|
||||
|
|
@ -124,6 +178,56 @@ bool User::canAccess(int monitor_id) {
|
|||
}
|
||||
} // end foreach Group_Permission
|
||||
|
||||
// Check role permissions if user has a role
|
||||
if (role_id > 0) {
|
||||
if (!role_monitor_permissions_loaded) loadRoleMonitorPermissions();
|
||||
auto rit = role_monitor_permissions.find(monitor_id);
|
||||
|
||||
if (rit != role_monitor_permissions.end()) {
|
||||
auto permission = rit->second.getPermission();
|
||||
switch (permission) {
|
||||
case Monitor_Permission::PERM_NONE :
|
||||
Debug(1, "Returning None from role_monitor_permission");
|
||||
return false;
|
||||
case Monitor_Permission::PERM_VIEW :
|
||||
Debug(1, "Returning true because VIEW from role_monitor_permission");
|
||||
return true;
|
||||
case Monitor_Permission::PERM_EDIT :
|
||||
Debug(1, "Returning true because EDIT from role_monitor_permission");
|
||||
return true;
|
||||
case Monitor_Permission::PERM_INHERIT :
|
||||
Debug(1, "INHERIT from role_monitor_permission");
|
||||
break;
|
||||
default:
|
||||
Warning("UNKNOWN permission %d from role_monitor_permission", permission);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!role_group_permissions_loaded) loadRoleGroupPermissions();
|
||||
|
||||
for (Group_Permission &gp : role_group_permissions) {
|
||||
auto permission = gp.getPermission(monitor_id);
|
||||
switch (permission) {
|
||||
case Group_Permission::PERM_NONE :
|
||||
Debug(1, "Returning None from role_group_permission");
|
||||
return false;
|
||||
case Group_Permission::PERM_VIEW :
|
||||
Debug(1, "Returning true because VIEW from role_group_permission");
|
||||
return true;
|
||||
case Group_Permission::PERM_EDIT :
|
||||
Debug(1, "Returning true because EDIT from role_group_permission");
|
||||
return true;
|
||||
case Group_Permission::PERM_INHERIT :
|
||||
Debug(1, "INHERIT from role_group_permission %d", gp.GroupId());
|
||||
break;
|
||||
default :
|
||||
Warning("UNKNOWN permission %d from role_group_permission %d", permission, gp.GroupId());
|
||||
break;
|
||||
}
|
||||
} // end foreach role Group_Permission
|
||||
} // end if role_id
|
||||
|
||||
return (monitors != PERM_NONE);
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +235,8 @@ User *User::find(const std::string &username) {
|
|||
std::string escaped_username = zmDbEscapeString(username);
|
||||
|
||||
std::string sql = stringtf("SELECT `Id`, `Username`, `Password`, `Enabled`,"
|
||||
" `Stream`+0, `Events`+0, `Control`+0, `Monitors`+0, `System`+0"
|
||||
" `Stream`+0, `Events`+0, `Control`+0, `Monitors`+0, `System`+0,"
|
||||
" COALESCE(`RoleId`, 0)"
|
||||
" FROM `Users` WHERE `Username` = '%s' AND `Enabled` = 1",
|
||||
escaped_username.c_str());
|
||||
MYSQL_RES *result = zmDbFetch(sql);
|
||||
|
|
@ -149,7 +254,8 @@ User *User::find(const std::string &username) {
|
|||
|
||||
User *User::find(int id) {
|
||||
std::string sql = stringtf("SELECT `Id`, `Username`, `Password`, `Enabled`,"
|
||||
" `Stream`+0, `Events`+0, `Control`+0, `Monitors`+0, `System`+0"
|
||||
" `Stream`+0, `Events`+0, `Control`+0, `Monitors`+0, `System`+0,"
|
||||
" COALESCE(`RoleId`, 0)"
|
||||
" FROM `Users` WHERE `Id` = %d AND `Enabled` = 1",
|
||||
id);
|
||||
MYSQL_RES *result = zmDbFetch(sql);
|
||||
|
|
@ -229,7 +335,8 @@ User *zmLoadTokenUser(const std::string &jwt_token_str, bool use_remote_addr) {
|
|||
}
|
||||
|
||||
std::string sql = stringtf("SELECT `Id`, `Username`, `Password`, `Enabled`, `Stream`+0, `Events`+0,"
|
||||
" `Control`+0, `Monitors`+0, `System`+0, `TokenMinExpiry`"
|
||||
" `Control`+0, `Monitors`+0, `System`+0, COALESCE(`RoleId`, 0),"
|
||||
" `TokenMinExpiry`"
|
||||
" FROM `Users` WHERE `Username` = '%s' AND `Enabled` = 1", username.c_str());
|
||||
|
||||
MYSQL_RES *result = zmDbFetch(sql);
|
||||
|
|
@ -274,7 +381,8 @@ User *zmLoadAuthUser(const std::string &auth, const std::string &username, bool
|
|||
Debug(1, "Attempting to authenticate user %s from auth string '%s', remote addr(%s)",
|
||||
username.c_str(), auth.c_str(), remote_addr);
|
||||
std::string sql = "SELECT `Id`, `Username`, `Password`, `Enabled`,"
|
||||
" `Stream`+0, `Events`+0, `Control`+0, `Monitors`+0, `System`+0"
|
||||
" `Stream`+0, `Events`+0, `Control`+0, `Monitors`+0, `System`+0,"
|
||||
" COALESCE(`RoleId`, 0)"
|
||||
" FROM `Users` WHERE `Enabled` = 1";
|
||||
if (!username.empty()) {
|
||||
sql += " AND `Username`='"+zmDbEscapeString(username)+"'";
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class User {
|
|||
Permission control;
|
||||
Permission monitors;
|
||||
Permission system;
|
||||
int role_id;
|
||||
|
||||
bool group_permissions_loaded;
|
||||
std::vector<Group_Permission> group_permissions;
|
||||
|
|
@ -51,6 +52,12 @@ class User {
|
|||
bool monitor_permissions_loaded;
|
||||
std::map<int, Monitor_Permission> monitor_permissions;
|
||||
|
||||
bool role_group_permissions_loaded;
|
||||
std::vector<Group_Permission> role_group_permissions;
|
||||
|
||||
bool role_monitor_permissions_loaded;
|
||||
std::map<int, Monitor_Permission> role_monitor_permissions;
|
||||
|
||||
public:
|
||||
User();
|
||||
explicit User(const MYSQL_ROW &dbrow);
|
||||
|
|
@ -78,6 +85,9 @@ class User {
|
|||
|
||||
void loadMonitorPermissions();
|
||||
void loadGroupPermissions();
|
||||
void loadRoleBasePermissions();
|
||||
void loadRoleMonitorPermissions();
|
||||
void loadRoleGroupPermissions();
|
||||
};
|
||||
|
||||
User *zmLoadUser(const std::string&username, const std::string &password="");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -956,21 +956,21 @@ std::vector<Zone> Zone::Load(const std::shared_ptr<Monitor> &monitor) {
|
|||
col++;
|
||||
int MaxPixelThreshold = dbrow[col]?atoi(dbrow[col]):0;
|
||||
col++;
|
||||
int MinAlarmPixels = dbrow[col]?atoi(dbrow[col]):0;
|
||||
double MinAlarmPixels_pct = dbrow[col] ? atof(dbrow[col]) : 0;
|
||||
col++;
|
||||
int MaxAlarmPixels = dbrow[col]?atoi(dbrow[col]):0;
|
||||
double MaxAlarmPixels_pct = dbrow[col] ? atof(dbrow[col]) : 0;
|
||||
col++;
|
||||
int FilterX = dbrow[col]?atoi(dbrow[col]):0;
|
||||
col++;
|
||||
int FilterY = dbrow[col]?atoi(dbrow[col]):0;
|
||||
col++;
|
||||
int MinFilterPixels = dbrow[col]?atoi(dbrow[col]):0;
|
||||
double MinFilterPixels_pct = dbrow[col] ? atof(dbrow[col]) : 0;
|
||||
col++;
|
||||
int MaxFilterPixels = dbrow[col]?atoi(dbrow[col]):0;
|
||||
double MaxFilterPixels_pct = dbrow[col] ? atof(dbrow[col]) : 0;
|
||||
col++;
|
||||
int MinBlobPixels = dbrow[col]?atoi(dbrow[col]):0;
|
||||
double MinBlobPixels_pct = dbrow[col] ? atof(dbrow[col]) : 0;
|
||||
col++;
|
||||
int MaxBlobPixels = dbrow[col]?atoi(dbrow[col]):0;
|
||||
double MaxBlobPixels_pct = dbrow[col] ? atof(dbrow[col]) : 0;
|
||||
col++;
|
||||
int MinBlobs = dbrow[col]?atoi(dbrow[col]):0;
|
||||
col++;
|
||||
|
|
@ -984,60 +984,47 @@ std::vector<Zone> Zone::Load(const std::shared_ptr<Monitor> &monitor) {
|
|||
/* HTML colour code is actually BGR in memory, we want RGB */
|
||||
AlarmRGB = rgb_convert(AlarmRGB, ZM_SUBPIX_ORDER_BGR);
|
||||
|
||||
// Auto-detect coordinate format: decimal points mean percentages,
|
||||
// integer-only means legacy pixel values. Units field is not trusted.
|
||||
Debug(5, "Parsing polygon %s (Units=%s)", Coords, Units);
|
||||
Polygon polygon;
|
||||
if (!strcmp(Units, "Pixels")) {
|
||||
// Legacy pixel-based coordinates: parse as integer pixel values
|
||||
if (!ParsePolygonString(Coords, polygon)) {
|
||||
if (strchr(Coords, '.')) {
|
||||
// Decimal values present — treat as percentages regardless of Units field
|
||||
if (!ParsePercentagePolygon(Coords, monitor->Width(), monitor->Height(), polygon)) {
|
||||
Error("Unable to parse polygon string '%s' for zone %d/%s for monitor %s, ignoring",
|
||||
Coords, Id, Name, monitor->Name());
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Percentage-based coordinates (default): convert to pixels using monitor dimensions.
|
||||
// However, if any coordinate value exceeds 100, these are actually pixel values
|
||||
// stored with incorrect Units — fall back to pixel parsing with a warning.
|
||||
bool has_pixel_values = false;
|
||||
{
|
||||
const char *s = Coords;
|
||||
while (*s != '\0') {
|
||||
double val = strtod(s, nullptr);
|
||||
if (val > 100.0) {
|
||||
has_pixel_values = true;
|
||||
break;
|
||||
}
|
||||
// Skip to next number: find comma then space (x,y pairs separated by spaces)
|
||||
const char *comma = strchr(s, ',');
|
||||
if (!comma) break;
|
||||
val = strtod(comma + 1, nullptr);
|
||||
if (val > 100.0) {
|
||||
has_pixel_values = true;
|
||||
break;
|
||||
}
|
||||
const char *space = strchr(comma + 1, ' ');
|
||||
if (space) {
|
||||
s = space + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (has_pixel_values) {
|
||||
Debug(1, "Zone %d/%s has Units=Percent but Coords contain pixel values (>100), "
|
||||
"parsing as pixels instead", Id, Name);
|
||||
if (!ParsePolygonString(Coords, polygon)) {
|
||||
Error("Unable to parse polygon string '%s' for zone %d/%s for monitor %s, ignoring",
|
||||
Coords, Id, Name, monitor->Name());
|
||||
continue;
|
||||
}
|
||||
} else if (!ParsePercentagePolygon(Coords, monitor->Width(), monitor->Height(), polygon)) {
|
||||
// Integer-only coordinates — treat as pixel values
|
||||
if (!ParsePolygonString(Coords, polygon)) {
|
||||
Error("Unable to parse polygon string '%s' for zone %d/%s for monitor %s, ignoring",
|
||||
Coords, Id, Name, monitor->Name());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert threshold values from DB format to pixel counts for runtime use.
|
||||
// Percentage coordinates: thresholds are stored as % of zone area, convert to pixels.
|
||||
// Legacy pixel coordinates: thresholds are already pixel counts.
|
||||
int MinAlarmPixels, MaxAlarmPixels, MinFilterPixels, MaxFilterPixels, MinBlobPixels, MaxBlobPixels;
|
||||
if (strchr(Coords, '.') && polygon.Area() > 0) {
|
||||
int zpa = polygon.Area();
|
||||
MinAlarmPixels = MinAlarmPixels_pct > 0 ? static_cast<int>(MinAlarmPixels_pct * zpa / 100.0 + 0.5) : 0;
|
||||
MaxAlarmPixels = MaxAlarmPixels_pct > 0 ? static_cast<int>(MaxAlarmPixels_pct * zpa / 100.0 + 0.5) : 0;
|
||||
MinFilterPixels = MinFilterPixels_pct > 0 ? static_cast<int>(MinFilterPixels_pct * zpa / 100.0 + 0.5) : 0;
|
||||
MaxFilterPixels = MaxFilterPixels_pct > 0 ? static_cast<int>(MaxFilterPixels_pct * zpa / 100.0 + 0.5) : 0;
|
||||
MinBlobPixels = MinBlobPixels_pct > 0 ? static_cast<int>(MinBlobPixels_pct * zpa / 100.0 + 0.5) : 0;
|
||||
MaxBlobPixels = MaxBlobPixels_pct > 0 ? static_cast<int>(MaxBlobPixels_pct * zpa / 100.0 + 0.5) : 0;
|
||||
} else {
|
||||
MinAlarmPixels = static_cast<int>(MinAlarmPixels_pct);
|
||||
MaxAlarmPixels = static_cast<int>(MaxAlarmPixels_pct);
|
||||
MinFilterPixels = static_cast<int>(MinFilterPixels_pct);
|
||||
MaxFilterPixels = static_cast<int>(MaxFilterPixels_pct);
|
||||
MinBlobPixels = static_cast<int>(MinBlobPixels_pct);
|
||||
MaxBlobPixels = static_cast<int>(MaxBlobPixels_pct);
|
||||
}
|
||||
|
||||
if (atoi(dbrow[2]) == Zone::INACTIVE) {
|
||||
zones.emplace_back(monitor, Id, Name, polygon);
|
||||
} else if (atoi(dbrow[2]) == Zone::PRIVACY) {
|
||||
|
|
|
|||
|
|
@ -220,3 +220,106 @@ TEST_CASE("Zone: pixel values through ParsePercentagePolygon produce wrong resul
|
|||
REQUIRE(verts[1] == Vector2(static_cast<int>(width), 0));
|
||||
REQUIRE(verts[2] == Vector2(static_cast<int>(width), static_cast<int>(height)));
|
||||
}
|
||||
|
||||
// --- Auto-detect format tests ---
|
||||
// The zone loader uses strchr(Coords, '.') to decide format:
|
||||
// decimal point present -> ParsePercentagePolygon
|
||||
// no decimal point -> ParsePolygonString (legacy pixels)
|
||||
// These tests verify both parsers handle the inputs they'll receive
|
||||
// under auto-detection, and document the broken case that auto-detection prevents.
|
||||
|
||||
TEST_CASE("Zone: ParsePolygonString truncates decimal coords via atoi", "[Zone]") {
|
||||
// This is the bug that broke motion detection: percentage coords like
|
||||
// "0.00,0.00 99.96,0.00 99.96,99.93 0.00,99.93" parsed by
|
||||
// ParsePolygonString (which uses atoi) get truncated to a 99x99 pixel zone
|
||||
Polygon polygon;
|
||||
bool ok = Zone::ParsePolygonString("0.00,0.00 99.96,0.00 99.96,99.93 0.00,99.93", polygon);
|
||||
REQUIRE(ok);
|
||||
|
||||
auto const &verts = polygon.GetVertices();
|
||||
REQUIRE(verts.size() == 4);
|
||||
// atoi("99.96") = 99, atoi("99.93") = 99
|
||||
// On a 2560x1440 monitor this would be a 99x99 pixel zone — essentially no coverage
|
||||
REQUIRE(verts[1] == Vector2(99, 0));
|
||||
REQUIRE(verts[2] == Vector2(99, 99));
|
||||
}
|
||||
|
||||
TEST_CASE("Zone: decimal coords through ParsePercentagePolygon give correct pixels", "[Zone]") {
|
||||
// Same coords as above, but correctly routed to ParsePercentagePolygon
|
||||
// by the auto-detect logic (decimal point present)
|
||||
Polygon polygon;
|
||||
unsigned int width = 2560;
|
||||
unsigned int height = 1440;
|
||||
|
||||
bool ok = Zone::ParsePercentagePolygon(
|
||||
"0.00,0.00 99.96,0.00 99.96,99.93 0.00,99.93", width, height, polygon);
|
||||
REQUIRE(ok);
|
||||
|
||||
auto const &verts = polygon.GetVertices();
|
||||
REQUIRE(verts.size() == 4);
|
||||
// 99.96% of 2560 = 2558.976 -> 2559
|
||||
REQUIRE(verts[1].x_ == 2559);
|
||||
REQUIRE(verts[1].y_ == 0);
|
||||
// 99.93% of 1440 = 1438.992 -> 1439
|
||||
REQUIRE(verts[2].x_ == 2559);
|
||||
REQUIRE(verts[2].y_ == 1439);
|
||||
}
|
||||
|
||||
TEST_CASE("Zone: integer pixel coords stay as pixels", "[Zone]") {
|
||||
// Legacy integer-only coords should be parsed as raw pixel values
|
||||
// Auto-detect: no decimal point -> ParsePolygonString
|
||||
Polygon polygon;
|
||||
bool ok = Zone::ParsePolygonString("0,0 2559,0 2559,1439 0,1439", polygon);
|
||||
REQUIRE(ok);
|
||||
|
||||
auto const &verts = polygon.GetVertices();
|
||||
REQUIRE(verts.size() == 4);
|
||||
REQUIRE(verts[0] == Vector2(0, 0));
|
||||
REQUIRE(verts[1] == Vector2(2559, 0));
|
||||
REQUIRE(verts[2] == Vector2(2559, 1439));
|
||||
REQUIRE(verts[3] == Vector2(0, 1439));
|
||||
}
|
||||
|
||||
TEST_CASE("Zone: auto-detect heuristic — strchr for decimal point", "[Zone]") {
|
||||
// Verify the heuristic used by the zone loader:
|
||||
// strchr(coords, '.') distinguishes percentage from pixel coords
|
||||
|
||||
// Percentage coords always have decimal points from round(..., 2)
|
||||
const char *pct_coords = "0.00,0.00 99.96,0.00 99.96,99.93 0.00,99.93";
|
||||
REQUIRE(strchr(pct_coords, '.') != nullptr);
|
||||
|
||||
// Legacy pixel coords are always integers
|
||||
const char *px_coords = "0,0 2559,0 2559,1439 0,1439";
|
||||
REQUIRE(strchr(px_coords, '.') == nullptr);
|
||||
|
||||
// Edge case: small pixel zone that looks like it could be percentages
|
||||
// but has no decimal points — correctly detected as pixels
|
||||
const char *small_px = "0,0 50,0 50,50 0,50";
|
||||
REQUIRE(strchr(small_px, '.') == nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE("Zone: percentage coords at various resolutions", "[Zone]") {
|
||||
Polygon polygon;
|
||||
|
||||
// The same percentage zone should produce proportional pixel coords
|
||||
// regardless of monitor resolution
|
||||
const char *coords = "10.00,20.00 90.00,20.00 90.00,80.00 10.00,80.00";
|
||||
|
||||
SECTION("640x480") {
|
||||
bool ok = Zone::ParsePercentagePolygon(coords, 640, 480, polygon);
|
||||
REQUIRE(ok);
|
||||
auto const &v = polygon.GetVertices();
|
||||
REQUIRE(v[0] == Vector2(64, 96)); // 10% of 640, 20% of 480
|
||||
REQUIRE(v[1] == Vector2(576, 96)); // 90% of 640
|
||||
REQUIRE(v[2] == Vector2(576, 384)); // 80% of 480
|
||||
}
|
||||
|
||||
SECTION("3840x2160 (4K)") {
|
||||
bool ok = Zone::ParsePercentagePolygon(coords, 3840, 2160, polygon);
|
||||
REQUIRE(ok);
|
||||
auto const &v = polygon.GetVertices();
|
||||
REQUIRE(v[0] == Vector2(384, 432)); // 10% of 3840, 20% of 2160
|
||||
REQUIRE(v[1] == Vector2(3456, 432)); // 90% of 3840
|
||||
REQUIRE(v[2] == Vector2(3456, 1728)); // 80% of 2160
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ EOF
|
|||
sudo apt-get install devscripts equivs
|
||||
sudo mk-build-deps -ir $DIRECTORY.orig/debian/control
|
||||
echo "Status: $?"
|
||||
DEBUILD=debuild
|
||||
DEBUILD=debuild -b -uc -us
|
||||
else
|
||||
if [ $TYPE == "local" ]; then
|
||||
# Auto-install all ZoneMinder's dependencies using the Debian control file
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1.39.1
|
||||
1.39.3
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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">×</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>
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
if ($fid) {
|
||||
$filter = new ZM\Filter($fid);
|
||||
if (!$filter->Id()) {
|
||||
echo '<div class="error">Filter not found for id '.$_REQUEST['fid'].'</div>';
|
||||
echo '<div class="error">Filter not found for id '.$fid.'</div>';
|
||||
}
|
||||
} else {
|
||||
$filter = new ZM\Filter();
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ default :
|
|||
$data = unpack('ltype', $msg);
|
||||
switch ( $data['type'] ) {
|
||||
case MSG_DATA_WATCH :
|
||||
$data = unpack('ltype/imonitor/istate/dfps/dcapturefps/danalysisfps/ilevel/irate/ddelay/izoom/iscale/Cdelayed/Cpaused/Cenabled/Cforced/iscore/ianalysing', $msg);
|
||||
$data = unpack('ltype/imonitor/istate/dfps/dcapturefps/danalysisfps/ilevel/irate/ddelay/izoom/iscale/Cdelayed/Cpaused/Cenabled/Cforced/iscore/ianalysing/Canalysisimage', $msg);
|
||||
$data['fps'] = round( $data['fps'], 2 );
|
||||
$data['capturefps'] = round( $data['capturefps'], 2 );
|
||||
$data['analysisfps'] = round( $data['analysisfps'], 2 );
|
||||
|
|
|
|||
|
|
@ -396,7 +396,7 @@ class EventsController extends AppController {
|
|||
}
|
||||
$matches = NULL;
|
||||
$value = preg_replace('/^\s?interval\s?/i', '', $value);
|
||||
if (preg_match('/^(?P<expr>[ -.:0-9\']+)\s+(?P<unit>[_a-z]+)$/i', trim($value), $matches) !== 1) {
|
||||
if (preg_match('/^(?P<expr>[ \-.:0-9]+)\s+(?P<unit>[_a-z]+)$/i', trim($value), $matches) !== 1) {
|
||||
throw new Exception('Invalid interval: ' . $value);
|
||||
}
|
||||
$expr = trim($matches['expr']);
|
||||
|
|
@ -429,7 +429,7 @@ class EventsController extends AppController {
|
|||
$matches = NULL;
|
||||
// https://dev.mysql.com/doc/refman/5.5/en/expressions.html#temporal-intervals
|
||||
// Examples: `'1-1' YEAR_MONTH`, `'-1 10' DAY_HOUR`, `'1.999999' SECOND_MICROSECOND`
|
||||
if (preg_match('/^(?P<expr>[ -.:0-9\']+)\s+(?P<unit>[_a-z]+)$/i', trim($interval), $matches) !== 1) {
|
||||
if (preg_match('/^(?P<expr>[ \-.:0-9]+)\s+(?P<unit>[_a-z]+)$/i', trim($interval), $matches) !== 1) {
|
||||
throw new Exception('Invalid interval: ' . $interval);
|
||||
}
|
||||
$expr = trim($matches['expr']);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ class HostController extends AppController {
|
|||
return;
|
||||
}
|
||||
# To try to prevent abuse here, we are only going to allow certain characters in the daemon and args.
|
||||
$safe_daemon = preg_replace('/[^A-Za-z0-9\- \.]/', '', $daemon, -1, $count);
|
||||
$safe_daemon = preg_replace('/[^A-Za-z0-9\-\.]/', '', $daemon, -1, $count);
|
||||
if ($count) Error("Invalid characters found in daemon string ($daemon). Potential attack?");
|
||||
$safe_command = preg_replace('/[^a-z]/', '', $command, -1, $count);
|
||||
if ($count) Error("Invalid characters found in command string ($command). Potential attack?");
|
||||
|
|
@ -240,8 +240,8 @@ class HostController extends AppController {
|
|||
|
||||
if ( $mid ) {
|
||||
// Get disk usage for $mid
|
||||
ZM\Debug("Executing du -s0 $zm_dir_events/$mid | awk '{print $1}'");
|
||||
$usage = shell_exec("du -s0 $zm_dir_events/$mid | awk '{print $1}'");
|
||||
ZM\Debug("Executing du -s0 $zm_dir_events/$mid | awk '{print \$1}'");
|
||||
$usage = shell_exec("du -s0 ".escapeshellarg($zm_dir_events.'/'.$mid)." | awk '{print \$1}'");
|
||||
} else {
|
||||
$monitors = $this->Monitor->find('all', array(
|
||||
'fields' => array('Id', 'Name', 'WebColour')
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ class MonitorsController extends AppController {
|
|||
global $user;
|
||||
$mToken = $this->request->query('token') ? $this->request->query('token') : $this->request->data('token');;
|
||||
if ($mToken) {
|
||||
$auth = ' -T '.$mToken;
|
||||
$auth = ' -T '.escapeshellarg($mToken);
|
||||
} else if (ZM_AUTH_RELAY == 'hashed') {
|
||||
$auth = ' -A '.calculateAuthHash(''); # Can't do REMOTE_IP because zmu doesn't normally have access to it.
|
||||
} else if (ZM_AUTH_RELAY == 'plain') {
|
||||
|
|
@ -278,13 +278,17 @@ class MonitorsController extends AppController {
|
|||
$password = $_SESSION['password'];
|
||||
}
|
||||
|
||||
$auth = ' -U ' .$user->Username().' -P '.$password;
|
||||
$auth = ' -U '.escapeshellarg($user->Username()).' -P '.escapeshellarg($password);
|
||||
} else if (ZM_AUTH_RELAY == 'none') {
|
||||
$auth = ' -U ' .$user->Username();
|
||||
$auth = ' -U '.escapeshellarg($user->Username());
|
||||
}
|
||||
}
|
||||
|
||||
$shellcmd = escapeshellcmd(ZM_PATH_BIN."/zmu $verbose -m$id $q $auth");
|
||||
|
||||
$shellcmd = ZM_PATH_BIN.'/zmu'
|
||||
.($verbose ? " $verbose" : '')
|
||||
.' -m'.escapeshellarg($id)
|
||||
." $q"
|
||||
.$auth;
|
||||
$status = exec($shellcmd, $output, $rc);
|
||||
ZM\Debug("Command: $shellcmd output: ".implode(PHP_EOL, $output)." rc: $rc");
|
||||
if ($rc) {
|
||||
|
|
@ -342,9 +346,9 @@ class MonitorsController extends AppController {
|
|||
|
||||
// Pass -d for local, otherwise -m
|
||||
if ( $monitor[0]['Type'] == 'Local' ) {
|
||||
$args = '-d '. $monitor[0]['Device'];
|
||||
$args = '-d '. escapeshellarg($monitor[0]['Device']);
|
||||
} else {
|
||||
$args = '-m '. $monitor[0]['Id'];
|
||||
$args = '-m '. escapeshellarg($monitor[0]['Id']);
|
||||
}
|
||||
|
||||
// Build the command, and execute it
|
||||
|
|
|
|||
|
|
@ -60,6 +60,14 @@ class Monitor extends AppModel {
|
|||
'OutputCodec' => array (
|
||||
'rule' => array('inList', array (0,27,173,167,226)),
|
||||
'message'=>'Invalid value. Should be one of these integer values: 0(auto), 27(h264), 173(h265/hvec), 167(vp9), 226(av1)'
|
||||
),
|
||||
'Device' => array(
|
||||
'validPath' => array(
|
||||
'rule' => array('custom', '#^(/dev/[\w/.\-]+)?$#'),
|
||||
'message' => 'Invalid device path. Must be a valid /dev/ path (e.g. /dev/video0).',
|
||||
'allowEmpty' => true,
|
||||
'required' => false,
|
||||
),
|
||||
)
|
||||
|
||||
);
|
||||
|
|
@ -183,11 +191,11 @@ class Monitor extends AppModel {
|
|||
foreach ($daemons as $daemon) {
|
||||
$args = '';
|
||||
if ($daemon == 'zmc' and $monitor['Type'] == 'Local') {
|
||||
$args = '-d ' . $monitor['Device'];
|
||||
$args = '-d ' . escapeshellarg($monitor['Device']);
|
||||
} else if ($daemon == 'zmcontrol.pl') {
|
||||
$args = '--id '.$monitor['Id'];
|
||||
$args = '--id '.escapeshellarg($monitor['Id']);
|
||||
} else {
|
||||
$args = '-m ' . $monitor['Id'];
|
||||
$args = '-m ' . escapeshellarg($monitor['Id']);
|
||||
}
|
||||
|
||||
$shellcmd = escapeshellcmd(ZM_PATH_BIN.'/zmdc.pl '.$command.' '.$daemon.' '.$args);
|
||||
|
|
|
|||
|
|
@ -700,22 +700,20 @@ class Event extends ZM_Object {
|
|||
}
|
||||
|
||||
function createVideo($format, $rate, $scale, $transform, $overwrite=false) {
|
||||
$command = ZM_PATH_BIN.'/zmvideo.pl -e '.$this->{'Id'}.' -f '.$format.' -r '.sprintf('%.2F', ($rate/RATE_BASE));
|
||||
if (preg_match('/\d+x\d+/', $scale)) {
|
||||
$command .= ' -S '.$scale;
|
||||
$command = ZM_PATH_BIN.'/zmvideo.pl -e '.escapeshellarg($this->{'Id'})
|
||||
.' -f '.escapeshellarg(preg_replace('/[^\w]/', '', $format))
|
||||
.' -r '.escapeshellarg(sprintf('%.2F', ($rate/RATE_BASE)));
|
||||
if (preg_match('/^\d+x\d+$/', $scale)) {
|
||||
$command .= ' -S '.escapeshellarg($scale);
|
||||
} else {
|
||||
if ( version_compare(phpversion(), '4.3.10', '>=') )
|
||||
$command .= ' -s '.sprintf('%.2F', ($scale/SCALE_BASE));
|
||||
else
|
||||
$command .= ' -s '.sprintf('%.2f', ($scale/SCALE_BASE));
|
||||
$command .= ' -s '.escapeshellarg(sprintf('%.2F', ($scale/SCALE_BASE)));
|
||||
}
|
||||
if ($transform != '') {
|
||||
$transform = preg_replace('/[^\w=]/', '', $transform);
|
||||
$command .= ' -t '.$transform;
|
||||
$command .= ' -t '.escapeshellarg($transform);
|
||||
}
|
||||
if ($overwrite)
|
||||
$command .= ' -o';
|
||||
$command = escapeshellcmd($command);
|
||||
$result = exec($command, $output, $status);
|
||||
Debug("generating Video $command: result($result outptu:(".implode("\n", $output )." status($status");
|
||||
return $status ? '' : rtrim($result);
|
||||
|
|
|
|||
|
|
@ -40,6 +40,12 @@ class FilterTerm {
|
|||
$this->attr = preg_replace('/[^A-Za-z0-9\.]/', '', $this->attr, -1, $count);
|
||||
if ($count) Error("Invalid characters removed from filter attr {$term['attr']}, possible hacking attempt.");
|
||||
$this->op = isset($term['op']) ? $term['op'] : '=';
|
||||
$valid_ops = array('=', '!=', '>=', '<=', '>', '<', 'LIKE', 'NOT LIKE', '=~', '!~',
|
||||
'=[]', '![]', 'IN', 'NOT IN', 'EXISTS', 'IS', 'IS NOT');
|
||||
if (!in_array($this->op, $valid_ops)) {
|
||||
Warning('Invalid operator in filter term: ' . $this->op);
|
||||
$this->op = '=';
|
||||
}
|
||||
$this->val = isset($term['val']) ? $term['val'] : '';
|
||||
if (is_array($this->val)) $this->val = implode(',', $this->val);
|
||||
if ( isset($term['cnj']) ) {
|
||||
|
|
@ -71,7 +77,7 @@ class FilterTerm {
|
|||
}
|
||||
$this->cookie = isset($term['cookie']) ? $term['cookie'] : '';
|
||||
$this->placeholder = isset($term['placeholder']) ? $term['placeholder'] : null;
|
||||
$this->collate = isset($term['collate']) ? $term['collate'] : '';
|
||||
$this->collate = isset($term['collate']) ? preg_replace('/[^a-zA-Z0-9_]/', '', $term['collate']) : '';
|
||||
$this->multiple = isset($term['multiple']) ? $term['multiple'] : '';
|
||||
$this->chosen = isset($term['chosen']) ? $term['chosen'] : '';
|
||||
|
||||
|
|
@ -80,6 +86,21 @@ class FilterTerm {
|
|||
}
|
||||
} # end function __construct
|
||||
|
||||
private function compare($left, $op, $right) {
|
||||
$right = floatval($right);
|
||||
switch ($op) {
|
||||
case '=': return $left == $right;
|
||||
case '!=': return $left != $right;
|
||||
case '>': return $left > $right;
|
||||
case '>=': return $left >= $right;
|
||||
case '<': return $left < $right;
|
||||
case '<=': return $left <= $right;
|
||||
default:
|
||||
Warning("Invalid operator '$op' in compare");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
# Returns an array of values. AS term->value can be a list, we will break it apart, remove quotes etc
|
||||
public function sql_values() {
|
||||
$values = array();
|
||||
|
|
@ -97,7 +118,7 @@ class FilterTerm {
|
|||
$value = $group->MonitorIds();
|
||||
break;
|
||||
case 'AlarmedZoneId':
|
||||
$value = '(SELECT * FROM Stats WHERE EventId=E.Id AND ZoneId='.$value.' AND Score > 0 LIMIT 1)';
|
||||
$value = '(SELECT * FROM Stats WHERE EventId=E.Id AND ZoneId='.intval($value).' AND Score > 0 LIMIT 1)';
|
||||
break;
|
||||
case 'ExistsInFileSystem':
|
||||
$value = '';
|
||||
|
|
@ -433,16 +454,9 @@ class FilterTerm {
|
|||
}
|
||||
} # end foreach Storage Area
|
||||
} else if ( $this->attr == 'SystemLoad' ) {
|
||||
$string_to_eval = 'return getLoad() '.$this->op.' '.$this->val.';';
|
||||
try {
|
||||
$ret = eval($string_to_eval);
|
||||
Debug("Evaled $string_to_eval = $ret");
|
||||
if ( $ret )
|
||||
return true;
|
||||
} catch ( Throwable $t ) {
|
||||
Error('Failed evaluating '.$string_to_eval);
|
||||
return false;
|
||||
}
|
||||
$ret = $this->compare(getLoad(), $this->op, $this->val);
|
||||
Debug("SystemLoad compare: getLoad() {$this->op} {$this->val} = " . ($ret ? 'true' : 'false'));
|
||||
if ($ret) return true;
|
||||
} else {
|
||||
Error('testing unsupported pre term ' . $this->attr);
|
||||
}
|
||||
|
|
@ -459,27 +473,13 @@ class FilterTerm {
|
|||
return !file_exists($event->Path());
|
||||
}
|
||||
} else if ( $this->attr == 'DiskPercent' ) {
|
||||
$string_to_eval = 'return $event->Storage()->disk_usage_percent() '.$this->op.' '.$this->val.';';
|
||||
try {
|
||||
$ret = eval($string_to_eval);
|
||||
Debug("Evalled $string_to_eval = $ret");
|
||||
if ( $ret )
|
||||
return true;
|
||||
} catch ( Throwable $t ) {
|
||||
Error('Failed evaluating '.$string_to_eval);
|
||||
return false;
|
||||
}
|
||||
$ret = $this->compare($event->Storage()->disk_usage_percent(), $this->op, $this->val);
|
||||
Debug("DiskPercent compare: " . ($ret ? 'true' : 'false'));
|
||||
if ($ret) return true;
|
||||
} else if ( $this->attr == 'DiskBlocks' ) {
|
||||
$string_to_eval = 'return $event->Storage()->disk_usage_blocks() '.$this->op.' '.$this->val.';';
|
||||
try {
|
||||
$ret = eval($string_to_eval);
|
||||
Debug("Evalled $string_to_eval = $ret");
|
||||
if ( $ret )
|
||||
return true;
|
||||
} catch ( Throwable $t ) {
|
||||
Error('Failed evaluating '.$string_to_eval);
|
||||
return false;
|
||||
}
|
||||
$ret = $this->compare($event->Storage()->disk_usage_blocks(), $this->op, $this->val);
|
||||
Debug("DiskBlocks compare: " . ($ret ? 'true' : 'false'));
|
||||
if ($ret) return true;
|
||||
} else if ( $this->attr == 'Tags' ) {
|
||||
// Debug('TODO: Complete this post_sql_condition for Tags val: ' . $this->val . ' op: ' . $this->op . ' id: ' . $this->id);
|
||||
// Debug(print_r($this, true));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
namespace ZM;
|
||||
require_once('database.php');
|
||||
require_once('Object.php');
|
||||
|
||||
class MenuItem extends ZM_Object {
|
||||
protected static $table = 'Menu_Items';
|
||||
|
||||
protected $defaults = array(
|
||||
'Id' => null,
|
||||
'MenuKey' => '',
|
||||
'Enabled' => 1,
|
||||
'Label' => null,
|
||||
'SortOrder' => 0,
|
||||
'Icon' => null,
|
||||
'IconType' => 'material',
|
||||
);
|
||||
|
||||
// Default material icons for each menu key
|
||||
public static $defaultIcons = array(
|
||||
'Console' => 'dashboard',
|
||||
'Montage' => 'live_tv',
|
||||
'MontageReview' => 'movie',
|
||||
'Events' => 'event',
|
||||
'Options' => 'settings',
|
||||
'Log' => 'notification_important',
|
||||
'Devices' => 'devices_other',
|
||||
'IntelGpu' => 'memory',
|
||||
'Groups' => 'group',
|
||||
'Filters' => 'filter_alt',
|
||||
'Snapshots' => 'preview',
|
||||
'Reports' => 'report',
|
||||
'ReportEventAudit' => 'shield',
|
||||
'Map' => 'language',
|
||||
);
|
||||
|
||||
public function effectiveIcon() {
|
||||
if ($this->{'Icon'} !== null && $this->{'Icon'} !== '') {
|
||||
return $this->{'Icon'};
|
||||
}
|
||||
return isset(self::$defaultIcons[$this->{'MenuKey'}]) ? self::$defaultIcons[$this->{'MenuKey'}] : 'menu';
|
||||
}
|
||||
|
||||
public function effectiveIconType() {
|
||||
if ($this->{'IconType'} == 'none') {
|
||||
return 'none';
|
||||
}
|
||||
if ($this->{'Icon'} !== null && $this->{'Icon'} !== '') {
|
||||
return $this->{'IconType'};
|
||||
}
|
||||
return 'material';
|
||||
}
|
||||
|
||||
public static function find($parameters = array(), $options = array()) {
|
||||
return ZM_Object::_find(self::class, $parameters, $options);
|
||||
}
|
||||
|
||||
public static function find_one($parameters = array(), $options = array()) {
|
||||
return ZM_Object::_find_one(self::class, $parameters, $options);
|
||||
}
|
||||
|
||||
public function displayLabel() {
|
||||
if ($this->{'Label'} !== null && $this->{'Label'} !== '') {
|
||||
return $this->{'Label'};
|
||||
}
|
||||
return translate($this->{'MenuKey'});
|
||||
}
|
||||
}
|
||||
|
|
@ -646,9 +646,9 @@ class Monitor extends ZM_Object {
|
|||
}
|
||||
if ((!defined('ZM_SERVER_ID')) or ( property_exists($this, 'ServerId') and (ZM_SERVER_ID==$this->{'ServerId'}) )) {
|
||||
if ($this->Type() == 'Local') {
|
||||
$zmcArgs = '-d '.$this->{'Device'};
|
||||
$zmcArgs = '-d '.escapeshellarg($this->{'Device'});
|
||||
} else {
|
||||
$zmcArgs = '-m '.$this->{'Id'};
|
||||
$zmcArgs = '-m '.escapeshellarg($this->{'Id'});
|
||||
}
|
||||
|
||||
if ($mode == 'stop') {
|
||||
|
|
@ -939,7 +939,40 @@ class Monitor extends ZM_Object {
|
|||
$group_permission_value = $value;
|
||||
}
|
||||
}
|
||||
if ($group_permission_value != 'Inherit') return true;
|
||||
if ($group_permission_value != 'Inherit') return true;
|
||||
|
||||
# Check role permissions if user has a role
|
||||
$role = $u->Role();
|
||||
if ($role) {
|
||||
$role_monitor_permissions = $role->Monitor_Permissions();
|
||||
foreach ($role_monitor_permissions as $rmp) {
|
||||
if ($rmp->MonitorId() == $this->Id()) {
|
||||
$permission = $rmp->Permission();
|
||||
if ($permission != 'Inherit') {
|
||||
return ($permission != 'None');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$role_group_permissions = $role->Group_Permissions();
|
||||
$role_group_permission_value = 'Inherit';
|
||||
foreach ($role_group_permissions as $permission) {
|
||||
$value = $permission->MonitorPermission($this->Id());
|
||||
if ($value == 'None') {
|
||||
Debug('Can\'t view monitor '.$this->{'Id'}.' because of role group '.$permission->Group()->Name().' '.$permission->Permission());
|
||||
return false;
|
||||
}
|
||||
if ($value == 'Edit' or $value == 'View') {
|
||||
$role_group_permission_value = $value;
|
||||
}
|
||||
}
|
||||
if ($role_group_permission_value != 'Inherit') return true;
|
||||
|
||||
if ($u->Monitors() == 'None' and $role->Monitors() != 'None') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return ($u->Monitors() != 'None');
|
||||
} # end function canView
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,16 @@ if ( ('login' == $action) && isset($_REQUEST['username']) && ( ZM_AUTH_TYPE == '
|
|||
|
||||
// if captcha existed, it was passed
|
||||
|
||||
if (defined('ZM_OPT_USE_REMEMBER_ME') && ZM_OPT_USE_REMEMBER_ME) {
|
||||
if (!empty($_REQUEST['remember_me'])) {
|
||||
zm_setcookie('ZM_REMEMBER_ME', '1', array('expires' => time() + ZM_COOKIE_LIFETIME));
|
||||
$_COOKIE['ZM_REMEMBER_ME'] = '1';
|
||||
} else {
|
||||
zm_setcookie('ZM_REMEMBER_ME', '', array('expires' => time() - 31536000));
|
||||
unset($_COOKIE['ZM_REMEMBER_ME']);
|
||||
}
|
||||
}
|
||||
|
||||
zm_session_start();
|
||||
if (!isset($user) ) {
|
||||
$_SESSION['loginFailed'] = true;
|
||||
|
|
|
|||
|
|
@ -58,6 +58,15 @@ if ($action == 'save') {
|
|||
# For convenience
|
||||
$newMonitor = $_REQUEST['newMonitor'];
|
||||
|
||||
# Validate Device path to prevent command injection (CVE-worthy)
|
||||
if (!empty($newMonitor['Device'])) {
|
||||
$newMonitor['Device'] = validDevicePath($newMonitor['Device']);
|
||||
if ($newMonitor['Device'] === '') {
|
||||
$error_message .= 'Invalid device path. Must be a valid /dev/ path (e.g. /dev/video0).</br>';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$newMonitor['ManufacturerId'] and ($newMonitor['Manufacturer'] != '')) {
|
||||
# Need to add a new Manufacturer entry
|
||||
$newManufacturer = ZM\Manufacturer::find_one(array('Name'=>$newMonitor['Manufacturer']));
|
||||
|
|
|
|||
|
|
@ -189,5 +189,95 @@ if ( $action == 'delete' ) {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if ($action == 'menuitems') {
|
||||
if (!canEdit('System')) {
|
||||
ZM\Warning('Need System permission to edit menu items');
|
||||
} else if (isset($_REQUEST['items'])) {
|
||||
require_once('includes/MenuItem.php');
|
||||
$allItems = ZM\MenuItem::find();
|
||||
foreach ($allItems as $item) {
|
||||
$id = $item->Id();
|
||||
$enabled = isset($_REQUEST['items'][$id]['Enabled']) ? 1 : 0;
|
||||
$label = isset($_REQUEST['items'][$id]['Label']) ? trim($_REQUEST['items'][$id]['Label']) : null;
|
||||
$sortOrder = isset($_REQUEST['items'][$id]['SortOrder']) ? intval($_REQUEST['items'][$id]['SortOrder']) : $item->SortOrder();
|
||||
if ($label === '') $label = null;
|
||||
|
||||
$iconType = isset($_REQUEST['items'][$id]['IconType']) ? $_REQUEST['items'][$id]['IconType'] : $item->IconType();
|
||||
if (!in_array($iconType, ['material', 'fontawesome', 'image', 'none'])) $iconType = 'material';
|
||||
$icon = isset($_REQUEST['items'][$id]['Icon']) ? trim($_REQUEST['items'][$id]['Icon']) : $item->Icon();
|
||||
if ($icon === '') $icon = null;
|
||||
|
||||
// Handle image upload
|
||||
if (isset($_FILES['items']['name'][$id]['IconFile'])
|
||||
&& $_FILES['items']['error'][$id]['IconFile'] == UPLOAD_ERR_OK) {
|
||||
$uploadDir = ZM_PATH_WEB.'/graphics/menu/';
|
||||
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
|
||||
|
||||
$tmpName = $_FILES['items']['tmp_name'][$id]['IconFile'];
|
||||
$origName = basename($_FILES['items']['name'][$id]['IconFile']);
|
||||
$ext = strtolower(pathinfo($origName, PATHINFO_EXTENSION));
|
||||
$allowedExts = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico'];
|
||||
if (in_array($ext, $allowedExts)) {
|
||||
// Validate it's actually an image (except SVG/ICO)
|
||||
if ($ext == 'svg' || $ext == 'ico' || getimagesize($tmpName) !== false) {
|
||||
$safeName = 'menu_'.$id.'_'.time().'.'.$ext;
|
||||
$destPath = $uploadDir.$safeName;
|
||||
if (move_uploaded_file($tmpName, $destPath)) {
|
||||
// Remove old uploaded icon if it exists
|
||||
if ($item->IconType() == 'image' && $item->Icon() && file_exists(ZM_PATH_WEB.'/'.$item->Icon())) {
|
||||
unlink(ZM_PATH_WEB.'/'.$item->Icon());
|
||||
}
|
||||
$icon = 'graphics/menu/'.$safeName;
|
||||
$iconType = 'image';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If user cleared icon, reset to default
|
||||
if ($iconType != 'image' && ($icon === null || $icon === '')) {
|
||||
$icon = null;
|
||||
}
|
||||
|
||||
$item->save([
|
||||
'Enabled' => $enabled,
|
||||
'Label' => $label,
|
||||
'SortOrder' => $sortOrder,
|
||||
'Icon' => $icon,
|
||||
'IconType' => $iconType,
|
||||
]);
|
||||
}
|
||||
}
|
||||
$redirect = '?view=options&tab=menu';
|
||||
} else if ($action == 'resetmenu') {
|
||||
if (!canEdit('System')) {
|
||||
ZM\Warning('Need System permission to reset menu items');
|
||||
} else {
|
||||
// Clean up any uploaded icon files
|
||||
require_once('includes/MenuItem.php');
|
||||
$oldItems = ZM\MenuItem::find();
|
||||
foreach ($oldItems as $item) {
|
||||
if ($item->IconType() == 'image' && $item->Icon() && file_exists(ZM_PATH_WEB.'/'.$item->Icon())) {
|
||||
unlink(ZM_PATH_WEB.'/'.$item->Icon());
|
||||
}
|
||||
}
|
||||
dbQuery('DELETE FROM Menu_Items');
|
||||
dbQuery("INSERT INTO `Menu_Items` (`MenuKey`, `Enabled`, `SortOrder`) VALUES
|
||||
('Console', 1, 10),
|
||||
('Montage', 1, 20),
|
||||
('MontageReview', 1, 30),
|
||||
('Events', 1, 40),
|
||||
('Options', 1, 50),
|
||||
('Log', 1, 60),
|
||||
('Devices', 1, 70),
|
||||
('IntelGpu', 1, 80),
|
||||
('Groups', 1, 90),
|
||||
('Filters', 1, 100),
|
||||
('Snapshots', 1, 110),
|
||||
('Reports', 1, 120),
|
||||
('ReportEventAudit', 1, 130),
|
||||
('Map', 1, 140)");
|
||||
}
|
||||
$redirect = '?view=options&tab=menu';
|
||||
} // end if object vs action
|
||||
?>
|
||||
|
|
|
|||
|
|
@ -31,18 +31,9 @@ if ( !empty($_REQUEST['mid']) && canEdit('Monitors', $_REQUEST['mid']) ) {
|
|||
$zone = array();
|
||||
}
|
||||
|
||||
if ( $_REQUEST['newZone']['Units'] == 'Percent' ) {
|
||||
// Convert percentage thresholds to pixel counts using actual monitor pixel area
|
||||
$pixelArea = $monitor->ViewWidth() * $monitor->ViewHeight();
|
||||
foreach (array(
|
||||
'MinAlarmPixels','MaxAlarmPixels',
|
||||
'MinFilterPixels','MaxFilterPixels',
|
||||
'MinBlobPixels','MaxBlobPixels'
|
||||
) as $field ) {
|
||||
if ( isset($_REQUEST['newZone'][$field]) and $_REQUEST['newZone'][$field] )
|
||||
$_REQUEST['newZone'][$field] = intval(($_REQUEST['newZone'][$field]*$pixelArea)/100);
|
||||
}
|
||||
}
|
||||
// Threshold fields (MinAlarmPixels, etc.) are always submitted as percentages
|
||||
// of zone area by the JavaScript submitForm() function. If displaying in Pixels
|
||||
// mode, submitForm() converts back to percentages before submitting.
|
||||
|
||||
unset($_REQUEST['newZone']['Points']);
|
||||
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ function userLogout() {
|
|||
global $user;
|
||||
ZM\Info('User "'.($user?$user->Username():'no one').'" logged out');
|
||||
$user = null;// unset only clears the local variable
|
||||
zm_setcookie('ZM_REMEMBER_ME', '', array('expires' => time() - 31536000));
|
||||
zm_session_clear();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ function downloadEvents(
|
|||
} else {
|
||||
ZM\Error("Can't open event images export file 'event_files.txt'");
|
||||
}
|
||||
$cmd = ZM_PATH_FFMPEG.' -f concat -safe 0 -i event_files.txt -c copy \''.$export_dir.'/'.$mergedFileName. '\' 2>&1';
|
||||
$cmd = ZM_PATH_FFMPEG.' -f concat -safe 0 -i event_files.txt -c copy '.escapeshellarg($export_dir.'/'.$mergedFileName). ' 2>&1';
|
||||
exec($cmd, $output, $return);
|
||||
ZM\Debug($cmd.' return code: '.$return.' output: '.print_r($output,true));
|
||||
$exportFileList[] = $mergedFileName;
|
||||
|
|
@ -147,7 +147,7 @@ function downloadEvents(
|
|||
}
|
||||
|
||||
if ($command) {
|
||||
$command .= ' \''.$mergedFileName.'\''; # Name of the file to be added
|
||||
$command .= ' -- '.escapeshellarg($mergedFileName); # Name of the file to be added
|
||||
if (executeShelCommand($command, $deleteFile = $mergedFileName) === false) return false;
|
||||
}
|
||||
} # end foreach monitor
|
||||
|
|
@ -160,7 +160,7 @@ function downloadEvents(
|
|||
# Create an archive if necessary
|
||||
//$exportCompressed = true; // For debugging
|
||||
if ($exportCompressed) {
|
||||
$command = 'gzip '.escapeshellarg($archive_path); # Name of the file to be archived
|
||||
$command = 'gzip -- '.escapeshellarg($archive_path); # Name of the file to be archived
|
||||
if (executeShelCommand($command) === false) return false;
|
||||
$archiveFileName .= '.gz';
|
||||
}
|
||||
|
|
@ -208,7 +208,7 @@ function generateFileList ($exportFormat, $exportStructure, $archive_path, $expo
|
|||
$command = 'zip -j '.escapeshellarg($archive_path);
|
||||
$command .= $exportCompressed ? ' -9' : ' -0';
|
||||
}
|
||||
$command .= ' \''.$export_listFile.'\''; # Name of the file to be added
|
||||
$command .= ' '.escapeshellarg($export_listFile); # Name of the file to be added
|
||||
if (executeShelCommand($command, $deleteFile = $export_listFile) === false) return false;
|
||||
|
||||
# Let's delete the directory, it should already be empty.
|
||||
|
|
|
|||
|
|
@ -321,8 +321,8 @@ function deletePath( $path ) {
|
|||
if (is_link($path)) {
|
||||
if (!unlink($path)) ZM\Debug("Failed to unlink $path");
|
||||
} else if (is_dir($path)) {
|
||||
if (false === ($output = system('rm -rf "'.escapeshellcmd($path).'"'))) {
|
||||
ZM\Warning('Failed doing rm -rf "'.escapeshellcmd($path).'"');
|
||||
if (false === ($output = system('rm -rf '.escapeshellarg($path)))) {
|
||||
ZM\Warning('Failed doing rm -rf '.escapeshellarg($path));
|
||||
}
|
||||
} else if (file_exists($path)) {
|
||||
if (!unlink($path)) ZM\Debug("Failed to delete $path");
|
||||
|
|
@ -501,6 +501,12 @@ function getFormChanges($values, $newValues, $types=false, $columns=false) {
|
|||
if ( $columns && !isset($columns[$key]) )
|
||||
continue;
|
||||
|
||||
# Validate column name to prevent SQL injection via array keys
|
||||
if ( !preg_match('/^[a-zA-Z0-9_]+$/', $key) ) {
|
||||
ZM\Warning("Invalid column name rejected in getFormChanges: $key");
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( !isset($types[$key]) )
|
||||
$types[$key] = false;
|
||||
|
||||
|
|
@ -517,10 +523,14 @@ function getFormChanges($values, $newValues, $types=false, $columns=false) {
|
|||
case 'image' :
|
||||
if ( is_array( $newValues[$key] ) ) {
|
||||
$imageData = getimagesize( $newValues[$key]['tmp_name'] );
|
||||
$changes[$key.'Width'] = $key.'Width = '.$imageData[0];
|
||||
$changes[$key.'Height'] = $key.'Height = '.$imageData[1];
|
||||
$changes[$key.'Type'] = $key.'Type = \''.$newValues[$key]['type'].'\'';
|
||||
$changes[$key.'Size'] = $key.'Size = '.$newValues[$key]['size'];
|
||||
if ( !is_array($imageData) ) {
|
||||
ZM\Warning("getimagesize failed for uploaded field '$key'; skipping width/height update.");
|
||||
} else {
|
||||
$changes[$key.'Width'] = $key.'Width = '.intval($imageData[0]);
|
||||
$changes[$key.'Height'] = $key.'Height = '.intval($imageData[1]);
|
||||
}
|
||||
$changes[$key.'Type'] = $key.'Type = '.dbEscape($newValues[$key]['type']);
|
||||
$changes[$key.'Size'] = $key.'Size = '.intval($newValues[$key]['size']);
|
||||
ob_start();
|
||||
readfile( $newValues[$key]['tmp_name'] );
|
||||
$changes[$key] = $key." = ".dbEscape( ob_get_contents() );
|
||||
|
|
@ -531,9 +541,8 @@ function getFormChanges($values, $newValues, $types=false, $columns=false) {
|
|||
break;
|
||||
case 'document' :
|
||||
if ( is_array( $newValues[$key] ) ) {
|
||||
$imageData = getimagesize( $newValues[$key]['tmp_name'] );
|
||||
$changes[$key.'Type'] = $key.'Type = \''.$newValues[$key]['type'].'\'';
|
||||
$changes[$key.'Size'] = $key.'Size = '.$newValues[$key]['size'];
|
||||
$changes[$key.'Type'] = $key.'Type = '.dbEscape($newValues[$key]['type']);
|
||||
$changes[$key.'Size'] = $key.'Size = '.intval($newValues[$key]['size']);
|
||||
ob_start();
|
||||
readfile( $newValues[$key]['tmp_name'] );
|
||||
$changes[$key] = $key.' = '.dbEscape( ob_get_contents() );
|
||||
|
|
@ -755,9 +764,9 @@ function daemonStatus($daemon, $args=false) {
|
|||
|
||||
function zmcStatus($monitor) {
|
||||
if ( $monitor['Type'] == 'Local' ) {
|
||||
$zmcArgs = '-d '.$monitor['Device'];
|
||||
$zmcArgs = '-d '.escapeshellarg($monitor['Device']);
|
||||
} else {
|
||||
$zmcArgs = '-m '.$monitor['Id'];
|
||||
$zmcArgs = '-m '.escapeshellarg($monitor['Id']);
|
||||
}
|
||||
return daemonStatus('zmc', $zmcArgs);
|
||||
}
|
||||
|
|
@ -776,9 +785,9 @@ function daemonCheck($daemon=false, $args=false) {
|
|||
|
||||
function zmcCheck($monitor) {
|
||||
if ( $monitor['Type'] == 'Local' ) {
|
||||
$zmcArgs = '-d '.$monitor['Device'];
|
||||
$zmcArgs = '-d '.escapeshellarg($monitor['Device']);
|
||||
} else {
|
||||
$zmcArgs = '-m '.$monitor['Id'];
|
||||
$zmcArgs = '-m '.escapeshellarg($monitor['Id']);
|
||||
}
|
||||
return daemonCheck('zmc', $zmcArgs);
|
||||
}
|
||||
|
|
@ -1467,7 +1476,7 @@ function limitPoints(&$points, $min_x, $min_y, $max_x, $max_y) {
|
|||
} // end function limitPoints( $points, $min_x, $min_y, $max_x, $max_y )
|
||||
|
||||
function convertPixelPointsToPercent(&$points, $width, $height) {
|
||||
if (!$width || !$height) return;
|
||||
if (!$width || !$height) return false;
|
||||
$isPixel = false;
|
||||
foreach ($points as $point) {
|
||||
if ($point['x'] > 100 || $point['y'] > 100) {
|
||||
|
|
@ -1482,6 +1491,7 @@ function convertPixelPointsToPercent(&$points, $width, $height) {
|
|||
}
|
||||
unset($point);
|
||||
}
|
||||
return $isPixel;
|
||||
}
|
||||
|
||||
function scalePoints(&$points, $scale) {
|
||||
|
|
@ -1880,6 +1890,18 @@ function validNum( $input ) {
|
|||
return preg_replace('/[^\d.-]/', '', $input);
|
||||
}
|
||||
|
||||
// For device path strings - must be a valid Unix device path
|
||||
function validDevicePath($input) {
|
||||
if (is_null($input) || $input === '') return '';
|
||||
// Only allow typical device paths: /dev/video0, /dev/v4l/by-id/..., etc.
|
||||
// Reject any shell metacharacters
|
||||
if (!preg_match('#^/dev/[\w/.\-]+$#', $input)) {
|
||||
ZM\Warning("Invalid device path rejected: ".validHtmlStr($input));
|
||||
return '';
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
// For general strings
|
||||
function validStr($input) {
|
||||
if (is_null($input)) return '';
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@ function probeAmcrest($ip, $username='', $password='') {
|
|||
}
|
||||
|
||||
function wget($method, $url, $username, $password) {
|
||||
exec("wget --keep-session-cookies -O - $url", $output, $result_code);
|
||||
exec('wget --keep-session-cookies -O - '.escapeshellarg($url), $output, $result_code);
|
||||
return implode("\n", $output);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,12 @@ function zm_session_start() {
|
|||
// use_strict_mode is mandatory for security reasons.
|
||||
ini_set('session.use_strict_mode', 1);
|
||||
|
||||
$currentCookieParams = session_get_cookie_params();
|
||||
$currentCookieParams['lifetime'] = ZM_COOKIE_LIFETIME;
|
||||
$currentCookieParams = session_get_cookie_params();
|
||||
if (defined('ZM_OPT_USE_REMEMBER_ME') && ZM_OPT_USE_REMEMBER_ME && empty($_COOKIE['ZM_REMEMBER_ME'])) {
|
||||
$currentCookieParams['lifetime'] = 0;
|
||||
} else {
|
||||
$currentCookieParams['lifetime'] = ZM_COOKIE_LIFETIME;
|
||||
}
|
||||
$currentCookieParams['httponly'] = true;
|
||||
if ( version_compare(phpversion(), '7.3.0', '<') ) {
|
||||
session_set_cookie_params(
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ function MonitorStream(monitorData) {
|
|||
this.wsMSE = null;
|
||||
this.streamStartTime = 0; // Initial point of flow start time. Used for flow lag time analysis.
|
||||
this.waitingStart;
|
||||
this.handlerEventListener = {};
|
||||
this.mseListenerSourceopenBind = null;
|
||||
this.streamListenerBind = null;
|
||||
this.mseSourceBufferListenerUpdateendBind = null;
|
||||
|
|
@ -441,7 +442,7 @@ function MonitorStream(monitorData) {
|
|||
clearInterval(this.statusCmdTimer); // Fix for issues in Chromium when quickly hiding/showing a page. Doesn't clear statusCmdTimer when minimizing a page https://stackoverflow.com/questions/9501813/clearinterval-not-working
|
||||
this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout);
|
||||
this.started = true;
|
||||
this.streamListenerBind();
|
||||
this.handlerEventListener['killStream'] = this.streamListenerBind();
|
||||
|
||||
if (typeof observerMontage !== 'undefined') observerMontage.observe(stream);
|
||||
this.activePlayer = 'go2rtc';
|
||||
|
|
@ -482,7 +483,7 @@ function MonitorStream(monitorData) {
|
|||
attachVideo(this);
|
||||
this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout);
|
||||
this.started = true;
|
||||
this.streamListenerBind();
|
||||
this.handlerEventListener['killStream'] = this.streamListenerBind();
|
||||
this.activePlayer = 'janus';
|
||||
this.updateStreamInfo('Janus', 'loading');
|
||||
return;
|
||||
|
|
@ -554,7 +555,7 @@ function MonitorStream(monitorData) {
|
|||
clearInterval(this.statusCmdTimer); // Fix for issues in Chromium when quickly hiding/showing a page. Doesn't clear statusCmdTimer when minimizing a page https://stackoverflow.com/questions/9501813/clearinterval-not-working
|
||||
this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout);
|
||||
this.started = true;
|
||||
this.streamListenerBind();
|
||||
this.handlerEventListener['killStream'] = this.streamListenerBind();
|
||||
this.updateStreamInfo((typeof players !== "undefined" && players) ? players[this.activePlayer] : 'RTSP2Web ' + this.RTSP2WebType, 'loading');
|
||||
return;
|
||||
} else {
|
||||
|
|
@ -618,12 +619,14 @@ function MonitorStream(monitorData) {
|
|||
}
|
||||
} // end if paused or not
|
||||
this.started = true;
|
||||
this.streamListenerBind();
|
||||
this.handlerEventListener['killStream'] = this.streamListenerBind();
|
||||
this.activePlayer = 'zms';
|
||||
this.updateStreamInfo('ZMS MJPEG');
|
||||
}; // this.start
|
||||
|
||||
this.stop = function() {
|
||||
manageEventListener.removeEventListener(this.handlerEventListener['killStream']);
|
||||
|
||||
/* Stop should stop the stream (killing zms) but NOT set src=''; This leaves the last jpeg up on screen instead of a broken image */
|
||||
const stream = this.getElement();
|
||||
if (!stream) {
|
||||
|
|
@ -1334,6 +1337,30 @@ function MonitorStream(monitorData) {
|
|||
}
|
||||
} // end if canEdit.Monitors
|
||||
|
||||
// Update analyse_frames and button to reflect what zms is actually sending
|
||||
if (streamStatus.analysisimage !== undefined) {
|
||||
const got_analysis = !!streamStatus.analysisimage;
|
||||
if (this.analyse_frames != got_analysis) {
|
||||
console.log('Analysis image state changed: requested=' + this.analyse_frames + ' actual=' + got_analysis);
|
||||
this.analyse_frames = got_analysis;
|
||||
if ('analyseBtn' in this.buttons) {
|
||||
if (got_analysis) {
|
||||
this.buttons.analyseBtn.addClass('btn-primary');
|
||||
this.buttons.analyseBtn.removeClass('btn-secondary');
|
||||
if (typeof translate !== 'undefined') {
|
||||
this.buttons.analyseBtn.prop('title', translate['Showing Analysis']);
|
||||
}
|
||||
} else {
|
||||
this.buttons.analyseBtn.removeClass('btn-primary');
|
||||
this.buttons.analyseBtn.addClass('btn-secondary');
|
||||
if (typeof translate !== 'undefined') {
|
||||
this.buttons.analyseBtn.prop('title', translate['Not Showing Analysis']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.status.auth) {
|
||||
if (this.status.auth != auth_hash) {
|
||||
// Don't reload the stream because it causes annoying flickering. Wait until the stream breaks.
|
||||
|
|
@ -1779,17 +1806,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);
|
||||
}
|
||||
|
|
@ -1976,10 +2003,10 @@ function startRTSP2WebPlay(videoEl, url, stream) {
|
|||
}
|
||||
|
||||
function streamListener(stream) {
|
||||
window.addEventListener('beforeunload', function(event) {
|
||||
return manageEventListener.addEventListener(window, 'beforeunload', function() {
|
||||
console.log('streamListener');
|
||||
stream.kill();
|
||||
});
|
||||
}, {capture: false});
|
||||
}
|
||||
|
||||
function mseListenerSourceopen(context, videoEl, url) {
|
||||
|
|
|
|||
|
|
@ -31,21 +31,21 @@ var Server = function() {
|
|||
value: function url() {
|
||||
const port = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
|
||||
|
||||
return location.protocol + '//' + this.Hostname + (port ? ':' + port : '') + (this.PathPrefix && this.PathPrefix != 'null' ? this.PathPrefix : '');
|
||||
return location.protocol + '//' + this.Hostname + (port ? ':' + port : (this.Port ? ':' + this.Port : (location.port ? ':' + location.port : ''))) + (this.PathPrefix && this.PathPrefix != 'null' ? this.PathPrefix : '');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'urlToZMS',
|
||||
value: function urlToZMS() {
|
||||
const port = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
|
||||
return this.Protocol + '://' + this.Hostname + (port ? ':' + port : '') + (this.PathToZMS && this.PathToZMS != 'null' ? this.PathToZMS : '');
|
||||
return this.Protocol + '://' + this.Hostname + (port ? ':' + port : (this.Port ? ':' + this.Port : (location.port ? ':' + location.port : ''))) + (this.PathToZMS && this.PathToZMS != 'null' ? this.PathToZMS : '');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'urlToApi',
|
||||
value: function urlToApi() {
|
||||
const port = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
|
||||
return (location.protocol=='https:'? 'https:' : this.Protocol+':') + '//' + this.Hostname + (port ? ':' + port : (this.Port ? ':' + this.Port : '')) + ((this.PathToApi && (this.PathToApi != 'null')) ? this.PathToApi : '');
|
||||
return (location.protocol=='https:'? 'https:' : this.Protocol+':') + '//' + this.Hostname + (port ? ':' + port : (this.Port ? ':' + this.Port : (location.port ? ':' + location.port : ''))) + ((this.PathToApi && (this.PathToApi != 'null')) ? this.PathToApi : '');
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -59,7 +59,7 @@ var Server = function() {
|
|||
key: 'urlToJanus',
|
||||
value: function urlToJanus() {
|
||||
const port = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
|
||||
return (location.protocol=='https:'? 'https:' : this.Protocol+':') + '//' + this.Hostname + (port ? ':' + port : '') + '/janus';
|
||||
return (location.protocol=='https:'? 'https:' : this.Protocol+':') + '//' + this.Hostname + (port ? ':' + port : (this.Port ? ':' + this.Port : (location.port ? ':' + location.port : ''))) + '/janus';
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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?',
|
||||
|
|
@ -564,6 +567,7 @@ $SLANG = array(
|
|||
'RecaptchaWarning' => 'Your reCaptcha secret key is invalid. Please correct it, or reCaptcha will not work', // added Sep 24 2015 - PP
|
||||
'RecordAudio' => 'Whether to store the audio stream when saving an event.',
|
||||
'RefImageBlendPct' => 'Reference Image Blend %ge',
|
||||
'RememberMe' => 'Remember Me',
|
||||
'RemoteHostName' => 'Host Name',
|
||||
'RemoteHostPath' => 'Path',
|
||||
'RemoteHostSubPath' => 'SubPath',
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
bootstrap-table-1.27.0/
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
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
Loading…
Reference in New Issue