diff --git a/.github/support.yml b/.github/support.yml new file mode 100644 index 000000000..fc61e151e --- /dev/null +++ b/.github/support.yml @@ -0,0 +1,27 @@ +# Configuration for support-requests - https://github.com/dessant/support-requests + +# Label used to mark issues as support requests +supportLabel: support + +# Comment to post on issues marked as support requests, `{issue-author}` is an +# optional placeholder. Set to `false` to disable +supportComment: > + :wave: @{issue-author}, we use the issue tracker exclusively for bug reports. + However, this issue appears to be a support request, a feature request, or + attempts to ask a question. + + Please use our support channels to get help with or discuss this project: + - The [ZoneMinder-Chat Slack channel](https://zoneminder-chat.herokuapp.com/) + - The [ZoneMinder Forum](https://forums.zoneminder.com/) + +# Close issues marked as support requests +close: true + +# Lock issues marked as support requests +lock: true + +# Assign `off-topic` as the reason for locking. Set to `false` to disable +setLockReason: true + +# Repository to extend settings from +# _extends: repo diff --git a/CMakeLists.txt b/CMakeLists.txt index 5529f8daf..1c72660d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,7 +129,8 @@ mark_as_advanced( ZM_PATH_ARP ZM_CONFIG_DIR ZM_CONFIG_SUBDIR - ZM_SYSTEMD) + ZM_SYSTEMD + ZM_MANPAGE_DEST_PREFIX) set(ZM_RUNDIR "/var/run/zm" CACHE PATH "Location of transient process files, default: /var/run/zm") @@ -212,6 +213,10 @@ set(ZM_TARGET_DISTRO "" CACHE STRING "Build ZoneMinder for a specific distribution. Currently, valid names are: fc27, fc26, el7, OS13, FreeBSD") set(ZM_SYSTEMD "OFF" CACHE BOOL "Set to ON to force building ZM with systemd support. default: OFF") +set(ZM_MANPAGE_DEST_PREFIX "share/man" CACHE PATH + "Relative path used to install ZoneMinder's Man pages into a + non-standard folder. Most Linux users will not need to change this. + BSD users may need to set this.") # Reassign some variables if a target distro has been specified if((ZM_TARGET_DISTRO MATCHES "^el") OR (ZM_TARGET_DISTRO MATCHES "^fc")) @@ -604,22 +609,40 @@ if(NOT ZM_NO_FFMPEG) set(optlibsnotfound "${optlibsnotfound} SWScale") endif(SWSCALE_LIBRARIES) - # rescale (using find_library and find_path) - find_library(AVRESAMPLE_LIBRARIES avresample) - if(AVRESAMPLE_LIBRARIES) - set(HAVE_LIBAVRESAMPLE 1) - list(APPEND ZM_BIN_LIBS "${AVRESAMPLE_LIBRARIES}") - find_path(AVRESAMPLE_INCLUDE_DIR "libavresample/avresample.h" /usr/include/ffmpeg) - if(AVRESAMPLE_INCLUDE_DIR) - include_directories("${AVRESAMPLE_INCLUDE_DIR}") - set(CMAKE_REQUIRED_INCLUDES "${AVRESAMPLE_INCLUDE_DIR}") - endif(AVRESAMPLE_INCLUDE_DIR) - mark_as_advanced(FORCE AVRESAMPLE_LIBRARIES AVRESAMPLE_INCLUDE_DIR) - check_include_file("libavresample/avresample.h" HAVE_LIBAVRESAMPLE_AVRESAMPLE_H) - set(optlibsfound "${optlibsfound} AVResample") - else(AVRESAMPLE_LIBRARIES) - set(optlibsnotfound "${optlibsnotfound} AVResample") - endif(AVRESAMPLE_LIBRARIES) + # SWresample (using find_library and find_path) + find_library(SWRESAMPLE_LIBRARIES swresample) + if(SWRESAMPLE_LIBRARIES) + set(HAVE_LIBSWRESAMPLE 1) + list(APPEND ZM_BIN_LIBS "${SWRESAMPLE_LIBRARIES}") + find_path(SWRESAMPLE_INCLUDE_DIR "libswresample/swresample.h" /usr/include/ffmpeg) + if(SWRESAMPLE_INCLUDE_DIR) + include_directories("${SWRESAMPLE_INCLUDE_DIR}") + set(CMAKE_REQUIRED_INCLUDES "${SWRESAMPLE_INCLUDE_DIR}") + endif(SWRESAMPLE_INCLUDE_DIR) + mark_as_advanced(FORCE SWRESAMPLE_LIBRARIES SWRESAMPLE_INCLUDE_DIR) + check_include_file("libswresample/swresample.h" HAVE_LIBSWRESAMPLE_SWRESAMPLE_H) + set(optlibsfound "${optlibsfound} SWResample") + else(SWRESAMPLE_LIBRARIES) + set(optlibsnotfound "${optlibsnotfound} SWResample") + + # AVresample (using find_library and find_path) + find_library(AVRESAMPLE_LIBRARIES avresample) + if(AVRESAMPLE_LIBRARIES) + set(HAVE_LIBAVRESAMPLE 1) + list(APPEND ZM_BIN_LIBS "${AVRESAMPLE_LIBRARIES}") + find_path(AVRESAMPLE_INCLUDE_DIR "libavresample/avresample.h" /usr/include/ffmpeg) + if(AVRESAMPLE_INCLUDE_DIR) + include_directories("${AVRESAMPLE_INCLUDE_DIR}") + set(CMAKE_REQUIRED_INCLUDES "${AVRESAMPLE_INCLUDE_DIR}") + endif(AVRESAMPLE_INCLUDE_DIR) + mark_as_advanced(FORCE AVRESAMPLE_LIBRARIES AVRESAMPLE_INCLUDE_DIR) + check_include_file("libavresample/avresample.h" HAVE_LIBAVRESAMPLE_AVRESAMPLE_H) + set(optlibsfound "${optlibsfound} AVResample") + else(AVRESAMPLE_LIBRARIES) + set(optlibsnotfound "${optlibsnotfound} AVResample") + endif(AVRESAMPLE_LIBRARIES) + + endif(SWRESAMPLE_LIBRARIES) # Find the path to the ffmpeg executable find_program(FFMPEG_EXECUTABLE diff --git a/cmake/Modules/Pod2Man.cmake b/cmake/Modules/Pod2Man.cmake index 6804fab53..734be239b 100644 --- a/cmake/Modules/Pod2Man.cmake +++ b/cmake/Modules/Pod2Man.cmake @@ -21,7 +21,7 @@ # To use it, include this file in CMakeLists.txt and # invoke POD2MAN(
) -MACRO(POD2MAN PODFILE MANFILE SECTION) +MACRO(POD2MAN PODFILE MANFILE SECTION MANPAGE_DEST_PREFIX) FIND_PROGRAM(POD2MAN pod2man) FIND_PROGRAM(GZIP gzip) @@ -58,9 +58,9 @@ MACRO(POD2MAN PODFILE MANFILE SECTION) INSTALL( FILES ${CMAKE_CURRENT_BINARY_DIR}/${MANFILE}.${SECTION}.gz - DESTINATION share/man/man${SECTION} + DESTINATION ${MANPAGE_DEST_PREFIX}/man${SECTION} ) -ENDMACRO(POD2MAN PODFILE MANFILE SECTION) +ENDMACRO(POD2MAN PODFILE MANFILE SECTION MANPAGE_DEST_PREFIX) MACRO(ADD_MANPAGE_TARGET) # It is not possible add a dependency to target 'install' diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index 38c1be52c..63bc4bbe8 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -497,7 +497,6 @@ CREATE TABLE `Monitors` ( `TrackDelay` smallint(5) unsigned, `ReturnLocation` tinyint(3) NOT NULL default '-1', `ReturnDelay` smallint(5) unsigned, - `DefaultView` enum('Events','Control') NOT NULL default 'Events', `DefaultRate` smallint(5) unsigned NOT NULL default '100', `DefaultScale` smallint(5) unsigned NOT NULL default '100', `DefaultCodec` enum('auto','H264','H265','MJPEG') NOT NULL default 'auto', @@ -558,7 +557,12 @@ INSERT INTO States (Name,Definition,IsActive) VALUES ('default','','1'); DROP TABLE IF EXISTS `Servers`; CREATE TABLE `Servers` ( `Id` int(10) unsigned NOT NULL auto_increment, + `Protocol` TEXT, `Hostname` TEXT, + `Port` INTEGER UNSIGNED, + `PathToIndex` TEXT, + `PathToZMS` TEXT, + `PathToApi` TEXT, `Name` varchar(64) NOT NULL default '', `State_Id` int(10) unsigned, `Status` enum('Unknown','NotRunning','Running') NOT NULL default 'Unknown', diff --git a/db/zm_update-1.31.47.sql b/db/zm_update-1.31.47.sql index 155fb29cc..714afd133 100644 --- a/db/zm_update-1.31.47.sql +++ b/db/zm_update-1.31.47.sql @@ -1,2 +1 @@ ALTER TABLE Frames MODIFY COLUMN EventId bigint unsigned NOT NULL; - diff --git a/db/zm_update-1.32.3.sql b/db/zm_update-1.32.3.sql index fe38baede..6c3e2c47f 100644 --- a/db/zm_update-1.32.3.sql +++ b/db/zm_update-1.32.3.sql @@ -2,8 +2,361 @@ -- This updates a 1.32.2 database to 1.32.3 -- +delimiter // +DROP TRIGGER IF EXISTS Events_Hour_delete_trigger// +CREATE TRIGGER Events_Hour_delete_trigger BEFORE DELETE ON Events_Hour +FOR EACH ROW BEGIN + UPDATE Monitors SET + HourEvents = COALESCE(HourEvents,1)-1, + HourEventDiskSpace=COALESCE(HourEventDiskSpace,0)-COALESCE(OLD.DiskSpace,0) + WHERE Id=OLD.MonitorId; +END; +// + +DROP TRIGGER IF EXISTS Events_Hour_update_trigger// + +CREATE TRIGGER Events_Hour_update_trigger AFTER UPDATE ON Events_Hour +FOR EACH ROW + BEGIN + declare diff BIGINT default 0; + + set diff = COALESCE(NEW.DiskSpace,0) - COALESCE(OLD.DiskSpace,0); + IF ( diff ) THEN + IF ( NEW.MonitorID != OLD.MonitorID ) THEN + UPDATE Monitors SET HourEventDiskSpace=COALESCE(HourEventDiskSpace,0)-COALESCE(OLD.DiskSpace,0) WHERE Monitors.Id=OLD.MonitorId; + UPDATE Monitors SET HourEventDiskSpace=COALESCE(HourEventDiskSpace,0)-COALESCE(NEW.DiskSpace,0) WHERE Monitors.Id=NEW.MonitorId; + ELSE + UPDATE Monitors SET HourEventDiskSpace=COALESCE(HourEventDiskSpace,0)+diff WHERE Monitors.Id=NEW.MonitorId; + END IF; + END IF; + END; +// +DELIMITER ; + +delimiter // +DROP TRIGGER IF EXISTS Events_Day_delete_trigger// +CREATE TRIGGER Events_Day_delete_trigger BEFORE DELETE ON Events_Day +FOR EACH ROW BEGIN + UPDATE Monitors SET + DayEvents = COALESCE(DayEvents,1)-1, + DayEventDiskSpace=COALESCE(DayEventDiskSpace,0)-COALESCE(OLD.DiskSpace,0) + WHERE Id=OLD.MonitorId; +END; +// + +DROP TRIGGER IF EXISTS Events_Day_update_trigger; +CREATE TRIGGER Events_Day_update_trigger AFTER UPDATE ON Events_Day +FOR EACH ROW + BEGIN + declare diff BIGINT default 0; + + set diff = COALESCE(NEW.DiskSpace,0) - COALESCE(OLD.DiskSpace,0); + IF ( diff ) THEN + IF ( NEW.MonitorID != OLD.MonitorID ) THEN + UPDATE Monitors SET DayEventDiskSpace=COALESCE(DayEventDiskSpace,0)-COALESCE(OLD.DiskSpace,0) WHERE Monitors.Id=OLD.MonitorId; + UPDATE Monitors SET DayEventDiskSpace=COALESCE(DayEventDiskSpace,0)+COALESCE(NEW.DiskSpace,0) WHERE Monitors.Id=NEW.MonitorId; + ELSE + UPDATE Monitors SET DayEventDiskSpace=COALESCE(DayEventDiskSpace,0)+diff WHERE Monitors.Id=NEW.MonitorId; + END IF; + END IF; + END; + // + + +DROP TRIGGER IF EXISTS Events_Week_delete_trigger// +CREATE TRIGGER Events_Week_delete_trigger BEFORE DELETE ON Events_Week +FOR EACH ROW BEGIN + UPDATE Monitors SET + WeekEvents = COALESCE(WeekEvents,1)-1, + WeekEventDiskSpace=COALESCE(WeekEventDiskSpace,0)-COALESCE(OLD.DiskSpace,0) + WHERE Id=OLD.MonitorId; +END; +// + +DROP TRIGGER IF EXISTS Events_Week_update_trigger; +CREATE TRIGGER Events_Week_update_trigger AFTER UPDATE ON Events_Week +FOR EACH ROW + BEGIN + declare diff BIGINT default 0; + + set diff = COALESCE(NEW.DiskSpace,0) - COALESCE(OLD.DiskSpace,0); + IF ( diff ) THEN + IF ( NEW.MonitorID != OLD.MonitorID ) THEN + UPDATE Monitors SET WeekEventDiskSpace=COALESCE(WeekEventDiskSpace,0)-COALESCE(OLD.DiskSpace,0) WHERE Monitors.Id=OLD.MonitorId; + UPDATE Monitors SET WeekEventDiskSpace=COALESCE(WeekEventDiskSpace,0)+COALESCE(NEW.DiskSpace,0) WHERE Monitors.Id=NEW.MonitorId; + ELSE + UPDATE Monitors SET WeekEventDiskSpace=COALESCE(WeekEventDiskSpace,0)+diff WHERE Monitors.Id=NEW.MonitorId; + END IF; + END IF; + END; + // + +DROP TRIGGER IF EXISTS Events_Month_delete_trigger// +CREATE TRIGGER Events_Month_delete_trigger BEFORE DELETE ON Events_Month +FOR EACH ROW BEGIN + UPDATE Monitors SET + MonthEvents = COALESCE(MonthEvents,1)-1, + MonthEventDiskSpace=COALESCE(MonthEventDiskSpace,0)-COALESCE(OLD.DiskSpace,0) + WHERE Id=OLD.MonitorId; +END; +// + +DROP TRIGGER IF EXISTS Events_Month_update_trigger; +CREATE TRIGGER Events_Month_update_trigger AFTER UPDATE ON Events_Month +FOR EACH ROW + BEGIN + declare diff BIGINT default 0; + + set diff = COALESCE(NEW.DiskSpace,0) - COALESCE(OLD.DiskSpace,0); + IF ( diff ) THEN + IF ( NEW.MonitorID != OLD.MonitorID ) THEN + UPDATE Monitors SET MonthEventDiskSpace=COALESCE(MonthEventDiskSpace,0)-COALESCE(OLD.DiskSpace) WHERE Monitors.Id=OLD.MonitorId; + UPDATE Monitors SET MonthEventDiskSpace=COALESCE(MonthEventDiskSpace,0)+COALESCE(NEW.DiskSpace) WHERE Monitors.Id=NEW.MonitorId; + ELSE + UPDATE Monitors SET MonthEventDiskSpace=COALESCE(MonthEventDiskSpace,0)+diff WHERE Monitors.Id=NEW.MonitorId; + END IF; + END IF; + END; + // + +drop procedure if exists update_storage_stats// + +drop trigger if exists event_update_trigger// + +CREATE TRIGGER event_update_trigger AFTER UPDATE ON Events +FOR EACH ROW +BEGIN + declare diff BIGINT default 0; + + set diff = COALESCE(NEW.DiskSpace,0) - COALESCE(OLD.DiskSpace,0); + IF ( NEW.StorageId = OLD.StorageID ) THEN + IF ( diff ) THEN + UPDATE Storage SET DiskSpace = COALESCE(DiskSpace,0) + diff WHERE Id = OLD.StorageId; + END IF; + ELSE + IF ( NEW.DiskSpace ) THEN + UPDATE Storage SET DiskSpace = COALESCE(DiskSpace,0) + NEW.DiskSpace WHERE Id = NEW.StorageId; + END IF; + IF ( OLD.DiskSpace ) THEN + UPDATE Storage SET DiskSpace = COALESCE(DiskSpace,0) - OLD.DiskSpace WHERE Id = OLD.StorageId; + END IF; + END IF; + + UPDATE Events_Hour SET DiskSpace=NEW.DiskSpace WHERE EventId=NEW.Id; + UPDATE Events_Day SET DiskSpace=NEW.DiskSpace WHERE EventId=NEW.Id; + UPDATE Events_Week SET DiskSpace=NEW.DiskSpace WHERE EventId=NEW.Id; + UPDATE Events_Month SET DiskSpace=NEW.DiskSpace WHERE EventId=NEW.Id; + + IF ( NEW.Archived != OLD.Archived ) THEN + IF ( NEW.Archived ) THEN + INSERT INTO Events_Archived (EventId,MonitorId,DiskSpace) VALUES (NEW.Id,NEW.MonitorId,NEW.DiskSpace); + UPDATE Monitors SET ArchivedEvents = COALESCE(ArchivedEvents,0)+1, ArchivedEventDiskSpace = COALESCE(ArchivedEventDiskSpace,0) + COALESCE(NEW.DiskSpace,0) WHERE Id=NEW.MonitorId; + ELSEIF ( OLD.Archived ) THEN + DELETE FROM Events_Archived WHERE EventId=OLD.Id; + UPDATE Monitors SET ArchivedEvents = COALESCE(ArchivedEvents,0)-1, ArchivedEventDiskSpace = COALESCE(ArchivedEventDiskSpace,0) - COALESCE(OLD.DiskSpace,0) WHERE Id=OLD.MonitorId; + ELSE + IF ( OLD.DiskSpace != NEW.DiskSpace ) THEN + UPDATE Events_Archived SET DiskSpace=NEW.DiskSpace WHERE EventId=NEW.Id; + UPDATE Monitors SET + ArchivedEventDiskSpace = COALESCE(ArchivedEventDiskSpace,0) - COALESCE(OLD.DiskSpace,0) + COALESCE(NEW.DiskSpace,0) + WHERE Id=OLD.MonitorId; + END IF; + END IF; + ELSEIF ( NEW.Archived AND diff ) THEN + UPDATE Events_Archived SET DiskSpace=NEW.DiskSpace WHERE EventId=NEW.Id; + END IF; + + IF ( diff ) THEN + UPDATE Monitors SET TotalEventDiskSpace = COALESCE(TotalEventDiskSpace,0) - COALESCE(OLD.DiskSpace,0) + COALESCE(NEW.DiskSpace,0) WHERE Id=OLD.MonitorId; + END IF; + +END; + +// + +delimiter ; + +DROP TRIGGER IF EXISTS event_insert_trigger; + +delimiter // +/* The assumption is that when an Event is inserted, it has no size yet, so don't bother updating the DiskSpace, just the count. + * The DiskSpace will get update in the Event Update Trigger + */ +CREATE TRIGGER event_insert_trigger AFTER INSERT ON Events +FOR EACH ROW + BEGIN + + INSERT INTO Events_Hour (EventId,MonitorId,StartTime,DiskSpace) VALUES (NEW.Id,NEW.MonitorId,NEW.StartTime,0); + INSERT INTO Events_Day (EventId,MonitorId,StartTime,DiskSpace) VALUES (NEW.Id,NEW.MonitorId,NEW.StartTime,0); + INSERT INTO Events_Week (EventId,MonitorId,StartTime,DiskSpace) VALUES (NEW.Id,NEW.MonitorId,NEW.StartTime,0); + INSERT INTO Events_Month (EventId,MonitorId,StartTime,DiskSpace) VALUES (NEW.Id,NEW.MonitorId,NEW.StartTime,0); + UPDATE Monitors SET + HourEvents = COALESCE(HourEvents,0)+1, + DayEvents = COALESCE(DayEvents,0)+1, + WeekEvents = COALESCE(WeekEvents,0)+1, + MonthEvents = COALESCE(MonthEvents,0)+1, + TotalEvents = COALESCE(TotalEvents,0)+1 + WHERE Id=NEW.MonitorId; +END; +// + +DROP TRIGGER IF EXISTS event_delete_trigger// + +CREATE TRIGGER event_delete_trigger BEFORE DELETE ON Events +FOR EACH ROW +BEGIN + IF ( OLD.DiskSpace ) THEN + UPDATE Storage SET DiskSpace = COALESCE(DiskSpace,0) - CAST(OLD.DiskSpace AS SIGNED) WHERE Id = OLD.StorageId; + END IF; + DELETE FROM Events_Hour WHERE EventId=OLD.Id; + DELETE FROM Events_Day WHERE EventId=OLD.Id; + DELETE FROM Events_Week WHERE EventId=OLD.Id; + DELETE FROM Events_Month WHERE EventId=OLD.Id; + IF ( OLD.Archived ) THEN + DELETE FROM Events_Archived WHERE EventId=OLD.Id; + UPDATE Monitors SET + ArchivedEvents = COALESCE(ArchivedEvents,1) - 1, + ArchivedEventDiskSpace = COALESCE(ArchivedEventDiskSpace,0) - COALESCE(OLD.DiskSpace,0), + TotalEvents = COALESCE(TotalEvents,1) - 1, + TotalEventDiskSpace = COALESCE(TotalEventDiskSpace,0) - COALESCE(OLD.DiskSpace,0) + WHERE Id=OLD.MonitorId; + ELSE + UPDATE Monitors SET + TotalEvents = COALESCE(TotalEvents,1)-1, + TotalEventDiskSpace=COALESCE(TotalEventDiskSpace,0)-COALESCE(OLD.DiskSpace,0) + WHERE Id=OLD.MonitorId; + END IF; +END; + +// + +DROP TRIGGER IF EXISTS Zone_Insert_Trigger// +CREATE TRIGGER Zone_Insert_Trigger AFTER INSERT ON Zones +FOR EACH ROW + BEGIN + UPDATE Monitors SET ZoneCount=(SELECT COUNT(*) FROM Zones WHERE MonitorId=NEW.MonitorId) WHERE Id=NEW.MonitorID; + END +// +DROP TRIGGER IF EXISTS Zone_Delete_Trigger// +CREATE TRIGGER Zone_Delete_Trigger AFTER DELETE ON Zones +FOR EACH ROW + BEGIN + UPDATE Monitors SET ZoneCount=(SELECT COUNT(*) FROM Zones WHERE MonitorId=OLD.MonitorId) WHERE Id=OLD.MonitorID; + END +// + +DELIMITER ; + +REPLACE INTO Events_Day SELECT Id,MonitorId,StartTime,DiskSpace FROM Events WHERE StartTime > DATE_SUB(NOW(), INTERVAL 1 day); +REPLACE INTO Events_Week SELECT Id,MonitorId,StartTime,DiskSpace FROM Events WHERE StartTime > DATE_SUB(NOW(), INTERVAL 1 week); +REPLACE INTO Events_Month SELECT Id,MonitorId,StartTime,DiskSpace FROM Events WHERE StartTime > DATE_SUB(NOW(), INTERVAL 1 month); +REPLACE INTO Events_Archived SELECT Id,MonitorId,DiskSpace FROM Events WHERE Archived=1; + +UPDATE Monitors INNER JOIN ( + SELECT MonitorId, + COUNT(Id) AS TotalEvents, + SUM(DiskSpace) AS TotalEventDiskSpace, + SUM(IF(Archived,1,0)) AS ArchivedEvents, + SUM(IF(Archived,DiskSpace,0)) AS ArchivedEventDiskSpace, + SUM(IF(StartTime > DATE_SUB(NOW(), INTERVAL 1 hour),1,0)) AS HourEvents, + SUM(IF(StartTime > DATE_SUB(NOW(), INTERVAL 1 hour),DiskSpace,0)) AS HourEventDiskSpace, + SUM(IF(StartTime > DATE_SUB(NOW(), INTERVAL 1 day),1,0)) AS DayEvents, + SUM(IF(StartTime > DATE_SUB(NOW(), INTERVAL 1 day),DiskSpace,0)) AS DayEventDiskSpace, + SUM(IF(StartTime > DATE_SUB(NOW(), INTERVAL 1 week),1,0)) AS WeekEvents, + SUM(IF(StartTime > DATE_SUB(NOW(), INTERVAL 1 week),DiskSpace,0)) AS WeekEventDiskSpace, + SUM(IF(StartTime > DATE_SUB(NOW(), INTERVAL 1 month),1,0)) AS MonthEvents, + SUM(IF(StartTime > DATE_SUB(NOW(), INTERVAL 1 month),DiskSpace,0)) AS MonthEventDiskSpace + FROM Events GROUP BY MonitorId + ) AS E ON E.MonitorId=Monitors.Id SET + Monitors.TotalEvents = E.TotalEvents, + Monitors.TotalEventDiskSpace = E.TotalEventDiskSpace, + Monitors.ArchivedEvents = E.ArchivedEvents, + Monitors.ArchivedEventDiskSpace = E.ArchivedEventDiskSpace, + Monitors.HourEvents = E.HourEvents, + Monitors.HourEventDiskSpace = E.HourEventDiskSpace, + Monitors.DayEvents = E.DayEvents, + Monitors.DayEventDiskSpace = E.DayEventDiskSpace, + Monitors.WeekEvents = E.WeekEvents, + Monitors.WeekEventDiskSpace = E.WeekEventDiskSpace, + Monitors.MonthEvents = E.MonthEvents, + Monitors.MonthEventDiskSpace = E.MonthEventDiskSpace; + -- --- Add some additional monitor preset values +-- Add Protocol column to Storage -- -INSERT INTO MonitorPresets VALUES (NULL,'D-link DCS-930L, 640x480, mjpeg','Remote','http',0,0,'http','simple','',80,'/mjpeg.cgi',NULL,640,480,3,NULL,0,NULL,NULL,NULL,100,100); +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Servers' + AND column_name = 'Protocol' + ) > 0, +"SELECT 'Column Protocol already exists in Servers'", +"ALTER TABLE Servers ADD `Protocol` TEXT AFTER `Id`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + +-- +-- Add PathToIndex column to Storage +-- + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Servers' + AND column_name = 'PathToIndex' + ) > 0, +"SELECT 'Column PathToIndex already exists in Servers'", +"ALTER TABLE Servers ADD `PathToIndex` TEXT AFTER `Hostname`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + +-- +-- Add PathToZMS column to Storage +-- + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Servers' + AND column_name = 'PathToZMS' + ) > 0, +"SELECT 'Column PathToZMS already exists in Servers'", +"ALTER TABLE Servers ADD `PathToZMS` TEXT AFTER `PathToIndex`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + +-- +-- Add PathToApi column to Storage +-- + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Servers' + AND column_name = 'PathToApi' + ) > 0, +"SELECT 'Column PathToApi already exists in Servers'", +"ALTER TABLE Servers ADD `PathToApi` TEXT AFTER `PathToZMS`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + +-- +-- Add Port column to Storage +-- + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Servers' + AND column_name = 'Port' + ) > 0, +"SELECT 'Column Port already exists in Servers'", +"ALTER TABLE Servers ADD `Port` INTEGER UNSIGNED AFTER `Hostname`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; diff --git a/db/zm_update-1.33.0.sql b/db/zm_update-1.33.0.sql new file mode 100644 index 000000000..cc7a257c7 --- /dev/null +++ b/db/zm_update-1.33.0.sql @@ -0,0 +1,21 @@ +-- +-- This updates a 1.32.3 database to 1.33.0 +-- +-- +-- Remove DefaultView from Monitors table. +-- + +SET @s = (SELECT IF( + (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'Monitors' + AND table_schema = DATABASE() + AND column_name = 'DefaultView' + ) > 0, +"ALTER TABLE Monitors DROP COLUMN DefaultView", +"SELECT 'Column DefaultView no longer exists in Monitors'" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; + diff --git a/distros/redhat/CMakeLists.txt b/distros/redhat/CMakeLists.txt index 37643cd89..06b0e8cbf 100644 --- a/distros/redhat/CMakeLists.txt +++ b/distros/redhat/CMakeLists.txt @@ -1,5 +1,10 @@ # CMakeLists.txt for the Redhat Target Distros. +# +# General strategy is to configure and install all files specific to Apache and Nginx +# Then let the rpm specfile sort them into the appropriate sub-package +# + # Display a message to show the RHEL build options are being processed. if(ZM_TARGET_DISTRO MATCHES "^el") message([STATUS] "Starting RHEL Build Options" ...) @@ -9,33 +14,39 @@ else(ZM_TARGET_DISTRO MATCHES "^el") message([WARNING] "Unknown Build Option Detected" ...) endif(ZM_TARGET_DISTRO MATCHES "^el") -if((NOT ZM_TARGET_DISTRO MATCHES "^fc") AND (ZM_WEB_USER STREQUAL "nginx")) - message([FATAL_ERROR] "Experimental Nginx support is currently only supported on Fedora" ...) -endif((NOT ZM_TARGET_DISTRO MATCHES "^fc") AND (ZM_WEB_USER STREQUAL "nginx")) +# +# CONFIGURE STAGE +# -# Configure the zoneminder service files -configure_file(systemd/zoneminder.logrotate.in ${CMAKE_CURRENT_SOURCE_DIR}/zoneminder.logrotate @ONLY) -configure_file(nginx/zoneminder.php-fpm.conf.in ${CMAKE_CURRENT_SOURCE_DIR}/zoneminder.php-fpm.conf @ONLY) -configure_file(nginx/zoneminder.conf.in ${CMAKE_CURRENT_SOURCE_DIR}/zoneminder.nginx.conf @ONLY) -if(ZM_WEB_USER STREQUAL "nginx") - configure_file(nginx/zoneminder.service.in ${CMAKE_CURRENT_SOURCE_DIR}/zoneminder.service @ONLY) - configure_file(nginx/zoneminder.tmpfiles.in ${CMAKE_CURRENT_SOURCE_DIR}/zoneminder.tmpfiles @ONLY) - configure_file(nginx/README.Fedora ${CMAKE_CURRENT_SOURCE_DIR}/readme/README COPYONLY) -else(ZM_WEB_USER STREQUAL "nginx") - configure_file(systemd/zoneminder.service.in ${CMAKE_CURRENT_SOURCE_DIR}/zoneminder.service @ONLY) - configure_file(apache/zoneminder.conf.in ${CMAKE_CURRENT_SOURCE_DIR}/zoneminder.conf @ONLY) - configure_file(systemd/zoneminder.tmpfiles.in ${CMAKE_CURRENT_SOURCE_DIR}/zoneminder.tmpfiles @ONLY) - if( ZM_TARGET_DISTRO MATCHES "^fc") - configure_file(readme/README.Fedora ${CMAKE_CURRENT_SOURCE_DIR}/readme/README COPYONLY) - else( ZM_TARGET_DISTRO MATCHES "^fc") - configure_file(readme/README.Redhat7 ${CMAKE_CURRENT_SOURCE_DIR}/readme/README COPYONLY) - endif( ZM_TARGET_DISTRO MATCHES "^fc") -endif(ZM_WEB_USER STREQUAL "nginx") - -# Create several empty folders +# Configure the common zoneminder files +configure_file(common/zoneminder.logrotate.in ${CMAKE_CURRENT_SOURCE_DIR}/zoneminder.logrotate @ONLY) +configure_file(common/zoneminder.service.in ${CMAKE_CURRENT_SOURCE_DIR}/zoneminder.service @ONLY) file(MAKE_DIRECTORY sock swap zoneminder zoneminder-upload events images temp) -# Install the empty folders +# Configure the Apache zoneminder files +configure_file(httpd/zm-httpd.conf.in ${CMAKE_CURRENT_SOURCE_DIR}/zm-httpd.conf @ONLY) +configure_file(httpd/zoneminder.httpd.conf.in ${CMAKE_CURRENT_SOURCE_DIR}/zoneminder.httpd.conf @ONLY) +configure_file(httpd/zoneminder.httpd.tmpfiles.in ${CMAKE_CURRENT_SOURCE_DIR}/zoneminder.httpd.tmpfiles.conf @ONLY) +configure_file(httpd/com.zoneminder.systemctl.rules.httpd.in ${CMAKE_CURRENT_SOURCE_DIR}/com.zoneminder.systemctl.rules.httpd @ONLY) + +# Configure the Nginx zoneminder files +configure_file(nginx/zm-nginx.conf ${CMAKE_CURRENT_SOURCE_DIR}/zm-nginx.conf COPYONLY) +configure_file(nginx/zoneminder.nginx.conf.in ${CMAKE_CURRENT_SOURCE_DIR}/zoneminder.nginx.conf @ONLY) +configure_file(nginx/redirect.nginx.conf ${CMAKE_CURRENT_SOURCE_DIR}/redirect.nginx.conf COPYONLY) +configure_file(nginx/zoneminder.nginx.tmpfiles.in ${CMAKE_CURRENT_SOURCE_DIR}/zoneminder.nginx.tmpfiles.conf @ONLY) +configure_file(nginx/zm-web-user.conf ${CMAKE_CURRENT_SOURCE_DIR}/zm-web-user.conf COPYONLY) +configure_file(nginx/zoneminder.php-fpm.conf ${CMAKE_CURRENT_SOURCE_DIR}/zoneminder.php-fpm.conf COPYONLY) +configure_file(nginx/com.zoneminder.systemctl.rules.nginx ${CMAKE_CURRENT_SOURCE_DIR}/com.zoneminder.systemctl.rules.nginx COPYONLY) + +# +# INSTALLATION STAGE +# + + +# Install the common zoneminder files +install(FILES zoneminder.logrotate DESTINATION /etc/logrotate.d RENAME zoneminder PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) +install(FILES zoneminder.service DESTINATION /usr/lib/systemd/system PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) + install(DIRECTORY sock swap DESTINATION /var/lib/zoneminder DIRECTORY_PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) install(DIRECTORY zoneminder DESTINATION /var/log DIRECTORY_PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) install(DIRECTORY zoneminder DESTINATION /var/run DIRECTORY_PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) @@ -43,6 +54,23 @@ install(DIRECTORY zoneminder DESTINATION /var/cache DIRECTORY_PERMISSIONS OWNER_ install(DIRECTORY zoneminder-upload DESTINATION /var/spool DIRECTORY_PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) install(DIRECTORY events images temp DESTINATION /var/lib/zoneminder DIRECTORY_PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) +# Install the Apache zoneminder files +install(FILES zm-httpd.conf DESTINATION /usr/lib/systemd/system/zoneminder.service.d PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) +install(FILES zoneminder.httpd.conf DESTINATION /etc/zm/www PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) +install(FILES zoneminder.httpd.tmpfiles.conf DESTINATION /usr/lib/tmpfiles.d PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) +install(FILES com.zoneminder.systemctl.rules.httpd DESTINATION /etc/zm/www PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) + +# Install the Nginx zoneminder files +install(FILES zm-nginx.conf DESTINATION /usr/lib/systemd/system/zoneminder.service.d PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) +install(FILES zoneminder.nginx.conf DESTINATION /etc/zm/www PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) +install(FILES redirect.nginx.conf DESTINATION /etc/zm/www PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) +install(FILES zoneminder.nginx.tmpfiles.conf DESTINATION /usr/lib/tmpfiles.d PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) +install(FILES com.zoneminder.systemctl.rules.nginx DESTINATION /etc/zm/www PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) +install(FILES zm-web-user.conf DESTINATION /etc/zm/conf.d PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) +install(FILES zoneminder.php-fpm.conf DESTINATION /etc/php-fpm.d PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) + +# Miscellaneous + # Symlink the cake php temp folder to the ZoneMinder temp folder install(CODE "execute_process(COMMAND ln -sf ../../../../../../var/lib/zoneminder/temp \"\$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/zoneminder/www/api/app/tmp\")") @@ -50,14 +78,5 @@ install(CODE "execute_process(COMMAND ln -sf ../../../../../../var/lib/zoneminde install(CODE "execute_process(COMMAND ln -sf ../../java/cambozola.jar \"\$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/zoneminder/www/cambozola.jar\")") # Install auxiliary files -install(FILES misc/redalert.wav DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/zoneminder/www/sounds PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) - -# Install zoneminder service files -install(FILES zoneminder.logrotate DESTINATION /etc/logrotate.d RENAME zoneminder PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) -install(FILES zoneminder.conf DESTINATION /etc/zm/www PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) -install(FILES zoneminder.php-fpm.conf DESTINATION /etc/zm/www PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) -install(FILES zoneminder.nginx.conf DESTINATION /etc/zm/www PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) - -install(FILES zoneminder.service DESTINATION /usr/lib/systemd/system PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) -install(FILES zoneminder.tmpfiles DESTINATION /usr/lib/tmpfiles.d RENAME zoneminder.conf PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ) +install(FILES common/redalert.wav DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/zoneminder/www/sounds PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) diff --git a/distros/redhat/misc/redalert.wav b/distros/redhat/common/redalert.wav similarity index 100% rename from distros/redhat/misc/redalert.wav rename to distros/redhat/common/redalert.wav diff --git a/distros/redhat/systemd/zoneminder.logrotate.in b/distros/redhat/common/zoneminder.logrotate.in similarity index 100% rename from distros/redhat/systemd/zoneminder.logrotate.in rename to distros/redhat/common/zoneminder.logrotate.in diff --git a/distros/redhat/systemd/zoneminder.service.in b/distros/redhat/common/zoneminder.service.in similarity index 76% rename from distros/redhat/systemd/zoneminder.service.in rename to distros/redhat/common/zoneminder.service.in index 68918ab9e..8551a60e2 100644 --- a/distros/redhat/systemd/zoneminder.service.in +++ b/distros/redhat/common/zoneminder.service.in @@ -1,13 +1,12 @@ # ZoneMinder systemd unit file for RedHat distros and clones +# See drop-in folder for additional config directives [Unit] Description=ZoneMinder CCTV recording and security system -After=network.target mariadb.service httpd.service -Requires=mariadb.service httpd.service +After=network.target mariadb.service +Requires=mariadb.service [Service] -User=@WEB_USER@ -Group=@WEB_GROUP@ Type=forking ExecStart=@BINDIR@/zmpkg.pl start ExecReload=@BINDIR@/zmpkg.pl restart diff --git a/distros/redhat/httpd/com.zoneminder.systemctl.rules.httpd.in b/distros/redhat/httpd/com.zoneminder.systemctl.rules.httpd.in new file mode 100644 index 000000000..d101dad69 --- /dev/null +++ b/distros/redhat/httpd/com.zoneminder.systemctl.rules.httpd.in @@ -0,0 +1,7 @@ +polkit.addRule(function(action, subject) { + if (action.id == "com.zoneminder.policykit.pkexec.run-zmsystemctl" && + subject.user != "@WEB_USER@") { + return polkit.Result.NO; + } + +}); diff --git a/distros/redhat/httpd/zm-httpd.conf.in b/distros/redhat/httpd/zm-httpd.conf.in new file mode 100644 index 000000000..1a00d8d9b --- /dev/null +++ b/distros/redhat/httpd/zm-httpd.conf.in @@ -0,0 +1,8 @@ +# Additional config directives for ZoneMinder with Apache web server + +[Unit] +After=httpd.service + +[Service] +User=@WEB_USER@ +Group=@WEB_GROUP@ diff --git a/distros/redhat/apache/zoneminder.conf.in b/distros/redhat/httpd/zoneminder.httpd.conf.in similarity index 100% rename from distros/redhat/apache/zoneminder.conf.in rename to distros/redhat/httpd/zoneminder.httpd.conf.in diff --git a/distros/redhat/systemd/zoneminder.tmpfiles.in b/distros/redhat/httpd/zoneminder.httpd.tmpfiles.in similarity index 99% rename from distros/redhat/systemd/zoneminder.tmpfiles.in rename to distros/redhat/httpd/zoneminder.httpd.tmpfiles.in index 21e6119fe..f24babdc6 100644 --- a/distros/redhat/systemd/zoneminder.tmpfiles.in +++ b/distros/redhat/httpd/zoneminder.httpd.tmpfiles.in @@ -3,3 +3,4 @@ D @ZM_SOCKDIR@ 0755 @WEB_USER@ @WEB_GROUP@ D @ZM_CACHEDIR@ 0755 @WEB_USER@ @WEB_GROUP@ d @ZM_DIR_EVENTS@ 0755 @WEB_USER@ @WEB_GROUP@ D @ZM_DIR_IMAGES@ 0755 @WEB_USER@ @WEB_GROUP@ + diff --git a/distros/redhat/misc/local_zoneminder.te b/distros/redhat/misc/local_zoneminder.te deleted file mode 100644 index c49505785..000000000 --- a/distros/redhat/misc/local_zoneminder.te +++ /dev/null @@ -1,125 +0,0 @@ -module local_zoneminder 1.2; - -require { - type afs_ka_port_t; - type netsupport_port_t; - type port_t; - type presence_port_t; - type postfix_master_t; - type postfix_qmgr_t; - type postfix_pickup_t; - type httpd_t; - type var_lib_t; - type ionixnetmon_port_t; - type glance_port_t; - type mmcc_port_t; - type postfix_master_t; - type commplex_port_t; - type syslogd_port_t; - type dcc_port_t; - type sip_port_t; - type amqp_port_t; - type condor_port_t; - type afs_fs_port_t; - type nodejs_debug_port_t; - type httpd_var_lib_t; - type websm_port_t; - type afs_pt_port_t; - type postfix_qmgr_t; - type git_port_t; - type ipp_port_t; - type aol_port_t; - type unconfined_t; - type kernel_t; - type init_t; - type auditd_t; - type mysqld_t; - type httpd_log_t; - type syslogd_t; - type httpd_t; - type initrc_state_t; - type initrc_t; - type var_lib_t; - type udev_t; - type mysqld_safe_t; - type sshd_t; - type crond_t; - type getty_t; - type httpd_var_lib_t; - type initrc_var_run_t; - type tmpfs_t; - type dhcpc_t; - type v4l_device_t; - type file_t; - class sock_file { write create unlink }; - class unix_stream_socket { read connectto }; - class lnk_file { write create getattr read lock unlink }; - class dir {search getattr }; - class udp_socket name_bind; - class file { write getattr read lock unlink open }; - class shm { unix_read unix_write associate read write getattr }; - class chr_file getattr; -} - -#============= httpd_t ============== -allow httpd_t auditd_t:dir { search getattr }; -allow httpd_t auditd_t:file { read getattr open }; -allow httpd_t crond_t:dir { search getattr }; -allow httpd_t crond_t:file { read getattr open }; -allow httpd_t dhcpc_t:dir { search getattr }; -allow httpd_t dhcpc_t:file { read getattr open }; -allow httpd_t getty_t:dir { search getattr }; -allow httpd_t getty_t:file { read getattr open }; -allow httpd_t httpd_log_t:file write; -allow httpd_t httpd_var_lib_t:lnk_file { write getattr read lock unlink }; -allow httpd_t init_t:dir { search getattr }; -allow httpd_t init_t:file { read getattr open }; -#!!!! The source type 'httpd_t' can write to a 'file' of the following types: -#squirrelmail_spool_t, mirrormanager_var_run_t, dirsrvadmin_config_t, httpd_lock_t, httpd_tmp_t, dirsrv_config_t, dirsrvadmin_tmp_t, httpd_cache_t, httpd_tmpfs_t, httpd_squirrelmail_t, dirsrv_var_run_t, dirsrv_var_log_t, httpd_var_lib_t, httpd_var_run_t, zarafa_var_lib_t, httpd_prewikka_rw_content_t, httpd_mediawiki_rw_content_t, httpd_squid_rw_content_t, passenger_var_run_t, httpd_smokeping_cgi_rw_content_t, httpd_openshift_rw_content_t, httpd_dirsrvadmin_rw_content_t, httpd_w3c_validator_rw_content_t, httpd_collectd_rw_content_t, cluster_var_lib_t, cluster_var_run_t, httpd_user_rw_content_t, httpd_awstats_rw_content_t, httpdcontent, root_t, httpd_cobbler_rw_content_t, httpd_munin_rw_content_t, cluster_conf_t, httpd_bugzilla_rw_content_t, passenger_tmp_t, httpd_cvs_rw_content_t, httpd_git_rw_content_t, httpd_sys_rw_content_t, httpd_sys_rw_content_t, httpd_nagios_rw_content_t, httpd_apcupsd_cgi_rw_content_t, httpd_nutups_cgi_rw_content_t, httpd_dspam_rw_content_t - -allow httpd_t initrc_state_t:file { read write getattr unlink open }; -allow httpd_t initrc_t:unix_stream_socket connectto; -allow httpd_t initrc_t:shm { unix_read unix_write associate read write getattr }; -allow httpd_t initrc_var_run_t:file { write read lock open }; -allow httpd_t kernel_t:dir { search getattr }; -allow httpd_t kernel_t:file { read getattr open }; -allow httpd_t mysqld_safe_t:dir { search getattr }; -allow httpd_t mysqld_safe_t:file { read getattr open }; -allow httpd_t mysqld_t:dir { search getattr }; -allow httpd_t mysqld_t:file { read getattr open }; -allow httpd_t sshd_t:dir { search getattr }; -allow httpd_t sshd_t:file { read getattr open }; -allow httpd_t syslogd_t:dir { search getattr }; -allow httpd_t syslogd_t:file { read getattr open }; -allow httpd_t tmpfs_t:sock_file write; -allow httpd_t udev_t:dir { search getattr }; -allow httpd_t udev_t:file { read getattr open }; -allow httpd_t unconfined_t:dir { search getattr }; -allow httpd_t unconfined_t:file { read getattr open }; -allow httpd_t var_lib_t:lnk_file { write getattr read lock unlink }; -allow httpd_t var_lib_t:sock_file { write unlink }; -allow httpd_t v4l_device_t:chr_file getattr; -allow httpd_t afs_fs_port_t:udp_socket name_bind; -allow httpd_t afs_ka_port_t:udp_socket name_bind; -allow httpd_t afs_pt_port_t:udp_socket name_bind; -allow httpd_t amqp_port_t:udp_socket name_bind; -allow httpd_t aol_port_t:udp_socket name_bind; -allow httpd_t commplex_port_t:udp_socket name_bind; -allow httpd_t condor_port_t:udp_socket name_bind; -allow httpd_t dcc_port_t:udp_socket name_bind; -allow httpd_t git_port_t:udp_socket name_bind; -allow httpd_t glance_port_t:udp_socket name_bind; -allow httpd_t httpd_var_lib_t:lnk_file create; -allow httpd_t ionixnetmon_port_t:udp_socket name_bind; -allow httpd_t ipp_port_t:udp_socket name_bind; -allow httpd_t mmcc_port_t:udp_socket name_bind; -allow httpd_t netsupport_port_t:udp_socket name_bind; -allow httpd_t nodejs_debug_port_t:udp_socket name_bind; -allow httpd_t port_t:udp_socket name_bind; -allow httpd_t postfix_master_t:dir { search getattr }; -allow httpd_t postfix_master_t:file { read getattr open }; -allow httpd_t postfix_pickup_t:dir { search getattr }; -allow httpd_t postfix_pickup_t:file { read getattr open }; -allow httpd_t postfix_qmgr_t:dir { search getattr }; -allow httpd_t postfix_qmgr_t:file { read getattr open }; -allow httpd_t presence_port_t:udp_socket name_bind; diff --git a/distros/redhat/nginx/com.zoneminder.systemctl.rules.nginx b/distros/redhat/nginx/com.zoneminder.systemctl.rules.nginx new file mode 100644 index 000000000..8eaa78d5c --- /dev/null +++ b/distros/redhat/nginx/com.zoneminder.systemctl.rules.nginx @@ -0,0 +1,7 @@ +polkit.addRule(function(action, subject) { + if (action.id == "com.zoneminder.policykit.pkexec.run-zmsystemctl" && + subject.user != "nginx") { + return polkit.Result.NO; + } + +}); diff --git a/distros/redhat/nginx/redirect.nginx.conf b/distros/redhat/nginx/redirect.nginx.conf new file mode 100644 index 000000000..67ef08124 --- /dev/null +++ b/distros/redhat/nginx/redirect.nginx.conf @@ -0,0 +1,2 @@ +# Auto redirect to https +return 301 https://$host$request_uri; diff --git a/distros/redhat/nginx/zm-nginx.conf b/distros/redhat/nginx/zm-nginx.conf new file mode 100644 index 000000000..5f0319b17 --- /dev/null +++ b/distros/redhat/nginx/zm-nginx.conf @@ -0,0 +1,9 @@ +# Additional config directives for ZoneMinder with Nginx web server + +[Unit] +After=nginx.service php-fpm.service fcgiwrap.service +Requires=php-fpm.service fcgiwrap@nginx.service + +[Service] +User=nginx +Group=nginx diff --git a/distros/redhat/nginx/zm-web-user.conf b/distros/redhat/nginx/zm-web-user.conf new file mode 100644 index 000000000..3146679fd --- /dev/null +++ b/distros/redhat/nginx/zm-web-user.conf @@ -0,0 +1,3 @@ +ZM_WEB_USER=nginx +ZM_WEB_GROUP=nginx + diff --git a/distros/redhat/nginx/zoneminder.conf.in b/distros/redhat/nginx/zoneminder.conf.in deleted file mode 100644 index cca9af54f..000000000 --- a/distros/redhat/nginx/zoneminder.conf.in +++ /dev/null @@ -1,53 +0,0 @@ -listen 443 ssl; -listen [::]:443 ssl; - -ssl_certificate "/etc/pki/tls/certs/localhost.crt"; -ssl_certificate_key "/etc/pki/tls/private/localhost.key"; -ssl_session_cache shared:SSL:1m; -ssl_session_timeout 10m; -ssl_ciphers PROFILE=SYSTEM; -ssl_prefer_server_ciphers on; - -# Auto-redirect HTTP requests to HTTPS -if ($scheme != "https") { - rewrite ^/?(zm)(.*)$ https://$host/$1$2 permanent; -} - -location /cgi-bin-zm { - gzip off; - alias "@ZM_CGIDIR@"; - - include /etc/nginx/fastcgi_params; - fastcgi_param SCRIPT_FILENAME $request_filename; - fastcgi_pass unix:/run/fcgiwrap.sock; -} - -location /zm/cache { - alias "@ZM_CACHEDIR@"; -} - -location /zm { - gzip off; - alias "@ZM_WEBDIR@"; - index index.php; - - location ~ \.php$ { - if (!-f $request_filename) { return 404; } - expires epoch; - include /etc/nginx/fastcgi_params; - fastcgi_param SCRIPT_FILENAME $request_filename; - fastcgi_index index.php; - fastcgi_pass unix:/run/php-fpm/www.sock; - } - - location ~ \.(jpg|jpeg|gif|png|ico)$ { - access_log off; - expires 33d; - } - - location /zm/api/ { - alias "@ZM_WEBDIR@"; - rewrite ^/zm/api(.+)$ /zm/api/index.php?p=$1 last; - } -} - diff --git a/distros/redhat/nginx/zoneminder.nginx.conf.in b/distros/redhat/nginx/zoneminder.nginx.conf.in new file mode 100644 index 000000000..2976a83b2 --- /dev/null +++ b/distros/redhat/nginx/zoneminder.nginx.conf.in @@ -0,0 +1,57 @@ +server { + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + server_name = localhost $hostname; + + ssl_certificate "/etc/pki/tls/certs/localhost.crt"; + ssl_certificate_key "/etc/pki/tls/private/localhost.key"; + ssl_session_cache shared:SSL:1m; + ssl_session_timeout 10m; + ssl_ciphers PROFILE=SYSTEM; + ssl_prefer_server_ciphers on; + + # Auto redirect to server/zm when no url suffix was given + location = / { + return 301 zm; + } + + location /cgi-bin-zm { + gzip off; + alias "@ZM_CGIDIR@"; + + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $request_filename; + fastcgi_pass unix:/run/fcgiwrap/fcgiwrap-nginx.sock; + } + + location /zm/cache { + alias "@ZM_CACHEDIR@"; + } + + location /zm { + gzip off; + alias "@ZM_WEBDIR@"; + index index.php; + + location ~ \.php$ { + try_files $uri =404; + expires epoch; + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $request_filename; + fastcgi_index index.php; + fastcgi_pass unix:/run/php-fpm/www.sock; + } + + location ~ \.(jpg|jpeg|gif|png|ico)$ { + access_log off; + expires 33d; + } + + location /zm/api/ { + alias "@ZM_WEBDIR@"; + rewrite ^/zm/api(.+)$ /zm/api/app/webroot/index.php?p=$1 last; + } + } + +} + diff --git a/distros/redhat/nginx/zoneminder.nginx.tmpfiles.in b/distros/redhat/nginx/zoneminder.nginx.tmpfiles.in new file mode 100644 index 000000000..1c581fcb9 --- /dev/null +++ b/distros/redhat/nginx/zoneminder.nginx.tmpfiles.in @@ -0,0 +1,6 @@ +D @ZM_TMPDIR@ 0755 nginx nginx +D @ZM_SOCKDIR@ 0755 nginx nginx +D @ZM_CACHEDIR@ 0755 nginx nginx +d @ZM_DIR_EVENTS@ 0755 nginx nginx +D @ZM_DIR_IMAGES@ 0755 nginx nginx + diff --git a/distros/redhat/nginx/zoneminder.php-fpm.conf.in b/distros/redhat/nginx/zoneminder.php-fpm.conf similarity index 91% rename from distros/redhat/nginx/zoneminder.php-fpm.conf.in rename to distros/redhat/nginx/zoneminder.php-fpm.conf index a4537b236..cd60a5120 100644 --- a/distros/redhat/nginx/zoneminder.php-fpm.conf.in +++ b/distros/redhat/nginx/zoneminder.php-fpm.conf @@ -3,8 +3,8 @@ ; Change the user and group of the default pool to the web server account [www] -user = @WEB_USER@ -group = @WEB_GROUP@ +user = nginx +group = nginx ; These parameters are typically a tradoff between performance and memory ; consumption. See the contents of www.conf for details. diff --git a/distros/redhat/nginx/zoneminder.service.in b/distros/redhat/nginx/zoneminder.service.in deleted file mode 100644 index 7e2e36585..000000000 --- a/distros/redhat/nginx/zoneminder.service.in +++ /dev/null @@ -1,22 +0,0 @@ -# ZoneMinder systemd unit file for Fedora -# Replace mariadb with community-mysql if using mysql service instead of mariadb - -[Unit] -Description=ZoneMinder CCTV recording and security system -After=network.target mariadb.service nginx.service php-fpm.service fcgiwrap.service -Requires=mariadb.service nginx.service php-fpm.service fcgiwrap.service - -[Service] -User=@WEB_USER@ -Group=@WEB_GROUP@ -Type=forking -ExecStart=@BINDIR@/zmpkg.pl start -ExecReload=@BINDIR@/zmpkg.pl restart -ExecStop=@BINDIR@/zmpkg.pl stop -PIDFile=@ZM_RUNDIR@/zm.pid -Environment=TZ=/etc/localtime -RuntimeDirectory=zoneminder -RuntimeDirectoryMode=0755 - -[Install] -WantedBy=multi-user.target diff --git a/distros/redhat/nginx/zoneminder.tmpfiles.in b/distros/redhat/nginx/zoneminder.tmpfiles.in deleted file mode 100644 index 07bae0900..000000000 --- a/distros/redhat/nginx/zoneminder.tmpfiles.in +++ /dev/null @@ -1,8 +0,0 @@ -D @ZM_TMPDIR@ 0755 @WEB_USER@ @WEB_GROUP@ -D @ZM_SOCKDIR@ 0755 @WEB_USER@ @WEB_GROUP@ -D @ZM_CACHEDIR@ 0755 @WEB_USER@ @WEB_GROUP@ -d @ZM_DIR_EVENTS@ 0755 @WEB_USER@ @WEB_GROUP@ -D @ZM_DIR_IMAGES@ 0755 @WEB_USER@ @WEB_GROUP@ -D /var/lib/php/session 770 root @WEB_GROUP@ -D /var/lib/php/wsdlcache 770 root @WEB_GROUP@ - diff --git a/distros/redhat/readme/README b/distros/redhat/readme/README new file mode 100644 index 000000000..0b3e4bb9c --- /dev/null +++ b/distros/redhat/readme/README @@ -0,0 +1,36 @@ +What's New +========== + +1. See the ZoneMinder release notes for a list of new features: + https://github.com/ZoneMinder/zoneminder/releases + +2. The contents of the ZoneMinder Apache config file have changed. In + addition, this ZoneMinder package now requires you to manually symlink the + ZoneMinder Apache config file. See new install step 6 and upgrade step 3 + in the appropriate README for details. + +3. This package has been split into sub-packages to allow compatibility with + other web servers. Here is a breakdown of the available packages: + + zoneminder - Meta-package installs zoneminder-common and zoneminder-httpd + This exists soley for backwards compatibility. + zoneminder-common - Common files that do not differ based on the web server + zoneminder-httpd - Files needed for compatibility with the Apache web server + zoneminder-nginx - Files needed for compatibility with the Nginx web server + + You can switch between different subpackages with dnf/yum. Be advised that, + switching between httpd <-> nginx requires manaully changing ownership of + all event folders and the php session folder after the change. + +4. If you have installed ZoneMinder from the FedBerry repositories, this build + of ZoneMinder has support for Raspberry Pi hardware acceleration when using + ffmpeg. Unforunately, there is a problem with the same hardware acceleration + when using libvlc. Consequently, libvlc support in this build of ZoneMinder + has been disabled until the problem is resolved. See the following bug + report for details: https://trac.videolan.org/vlc/ticket/18594 + +5. Continue on to the next README that corresponds to the chosen webserver: + + README.httpd - Follow these steps when using Apache + README.nginx - Follow these steps when using Nginx + diff --git a/distros/redhat/readme/README.Redhat7 b/distros/redhat/readme/README.Redhat7 deleted file mode 100644 index c78d02042..000000000 --- a/distros/redhat/readme/README.Redhat7 +++ /dev/null @@ -1,175 +0,0 @@ -What's New -========== - -1. See the ZoneMinder release notes for a list of new features: - https://github.com/ZoneMinder/zoneminder/releases - -2. The contents of the ZoneMinder Apache config file have changed. In - addition, this ZoneMinder package now requires you to manually symlink the - ZoneMinder Apache config file. See new install step 6 and upgrade step 3 - below for details. - -New installs -============ - -1. Unless you are already using MariaDB server, you need to ensure that the - server is configured to start during boot and properly secured by running: - - sudo yum install mariadb-server - sudo systemctl enable mariadb - sudo systemctl start mariadb.service - mysql_secure_installation - -2. Using the password for the root account set during the previous step, you - will need to create the ZoneMinder database and configure a database - account for ZoneMinder to use: - - mysql -uroot -p < /usr/share/zoneminder/db/zm_create.sql - mysql -uroot -p -e "grant all on zm.* to \ - 'zmuser'@localhost identified by 'zmpass';" - mysqladmin -uroot -p reload - - The database account credentials, zmuser/zmpass, are arbitrary. Set them to - anything that suits your environment. - -3. If you have chosen to change the zoneminder database account credentials to - something other than zmuser/zmpass, you must now create a config file under - /etc/zm/conf.d and set your credentials there. For example, create the file - /etc/zm/conf.d/zm-db-user.conf and add the following content to it: - - ZM_DB_USER = {username of the sql account you want to use} - ZM_DB_PASS = {password of the sql account you want to use} - - Once the file has been saved, set proper file & ownership permissions on it: - - sudo chown root:apache *.conf - sudo chmod 640 *.conf - -4. Edit /etc/php.ini, uncomment the date.timezone line, and add your local - timezone. PHP will complain loudly if this is not set, or if it is set - incorrectly, and these complaints will show up in the zoneminder logging - system as errors. - - If you are not sure of the proper timezone specification to use, look at - http://php.net/date.timezone - -5. Disable SELinux - - We currently do not have the resources to create and maintain an accurate - SELinux policy for ZoneMinder on CentOS 7. We will gladly accept pull - reqeusts from anyone who wishes to do the work. In the meantime, SELinux - will need to be disabled or put into permissive mode. - - To immediately disbale SELinux for the current seesion, issue the following - from the command line: - - sudo setenforce 0 - - To permanently disable SELinux, edit /etc/selinux/config and change the - SELINUX line from "enforcing" to "disabled". This change will take - effect after a reboot. - -6. Configure the web server - - This package uses the HTTPS protocol by default to access the web portal, - using the default self signed certificate on your system. Requests using - HTTP will auto-redirect to HTTPS. - - Inspect the web server configuration file and verify it meets your needs: - - /etc/zm/www/zoneminder.conf - - If you are running other web enabled services then you may need to edit - this file to suite. See README.https to learn about other alternatives. - - When in doubt, proceed with the default: - - sudo ln -s /etc/zm/www/zoneminder.conf /etc/httpd/conf.d/ - sudo yum install mod_ssl - -7. Now start the web server: - - sudo systemctl enable httpd - sudo systemctl start httpd - -8. Now start zoneminder: - - sudo systemctl enable zoneminder - sudo systemctl start zoneminder - -9. Optionally configure the firewall - - All Redhat distros ship with the firewall enabled. That means you will not - be able to access the ZoneMinder web console from a remote machine until - changes are made to the firewall. - - What follows are a set of minimal commands to allow remote access to the - ZoneMinder web console and also allow ZoneMinder's ONVIF discovery to - work. The following commands do not put any restrictions on which remote - machine(s) have access to the listed ports or services. - - sudo firewall-cmd --permanent --zone=public --add-service=http - sudo firewall-cmd --permanent --zone=public --add-service=https - sudo firewall-cmd --permanent --zone=public --add-port=3702/udp - sudo firewall-cmd --reload - - Additional changes to the firewall may be required, depending on your - security requirements and how you use the system. It is up to you to verify - these commands are sufficient. - -10. Access the ZoneMinder web console - - You may now access the ZoneMinder web console from your web browser using - an appropriate url. Here are some examples: - - http://localhost/zm (works from the local machine only) - http://{machine name}/zm (works only if dns is configured for your network) - http://{ip address}/zm - -Upgrades -======== - -1. Conf.d folder support has been added to ZoneMinder. Any custom - changes previously made to zm.conf must now be made in one or more custom - config files, created under the conf.d folder. Do this now. See - /etc/zm/conf.d/README for details. Once you recreate any custom config changes - under the conf.d folder, they will remain in place indefinitely. - -2. Verify permissions of the zmuser account. - - Over time, the database account permissions required for normal operation - have increased. Verify the zmuser database account has been granted all - permission to the ZoneMinder database: - - mysql -uroot -p -e "show grants for zmuser@localhost;" - - See step 2 of the Installation section to add missing permissions. - -3. Verify the ZoneMinder Apache configuration file in the folder - /etc/zm/www. You will have a file called "zoneminder.conf" and there - may also be a file called "zoneminder.conf.rpmnew". If an rpmnew file - exists, inspect it and merge anything new in that file with zoneminder.conf. - Verify the SSL REquirements meet your needs. Read README.https if necessary. - - The contents of this file must be merged into your Apache configuration. - See step 6 of the installation section if you have not already done this - during a previous upgrade. - -4. Upgrade the database before starting ZoneMinder. - - Most upgrades can be performed by executing the following command: - - sudo zmupdate.pl - - Recent versions of ZoneMinder don't require any parameters added to the - zmupdate command. However, if ZoneMinder complains, you may need to call - zmupdate in the following manner: - - sudo zmupdate.pl --user=root --pass= --version= - -5. Now restart the web server then start zoneminder: - - sudo systemctl restart httpd - sudo systemctl start zoneminder - - diff --git a/distros/redhat/readme/README.Fedora b/distros/redhat/readme/README.httpd similarity index 89% rename from distros/redhat/readme/README.Fedora rename to distros/redhat/readme/README.httpd index 9c5061e28..5301850df 100644 --- a/distros/redhat/readme/README.Fedora +++ b/distros/redhat/readme/README.httpd @@ -1,17 +1,8 @@ -What's New -========== - -1. See the ZoneMinder release notes for a list of new features: - https://github.com/ZoneMinder/zoneminder/releases - -2. The contents of the ZoneMinder Apache config file have changed. In - addition, this ZoneMinder package now requires you to manually symlink the - ZoneMinder Apache config file. See new install step 6 and upgrade step 3 - below for details. - New installs ============ +NOTE: EL7 users should replace "dnf" with "yum" in the instructions below. + 1. Unless you are already using MariaDB server, you need to ensure that the server is configured to start during boot and properly secured by running: @@ -77,14 +68,14 @@ New installs Inspect the web server configuration file and verify it meets your needs: - /etc/zm/www/zoneminder.conf + /etc/zm/www/zoneminder.httpd.conf If you are running other web enabled services then you may need to edit this file to suite. See README.https to learn about other alternatives. When in doubt, proceed with the default: - sudo ln -s /etc/zm/www/zoneminder.conf /etc/httpd/conf.d/ + sudo ln -sf /etc/zm/www/zoneminder.httpd.conf /etc/httpd/conf.d/ sudo dnf install mod_ssl 7. Now start the web server: @@ -146,15 +137,18 @@ Upgrades See step 2 of the Installation section to add missing permissions. 3. Verify the ZoneMinder Apache configuration file in the folder - /etc/zm/www. You will have a file called "zoneminder.conf" and there - may also be a file called "zoneminder.conf.rpmnew". If the rpmnew file + /etc/zm/www. You will have a file called "zoneminder.httpd.conf" and there + may also be one or more files with "rpmnew" extenstion. If the rpmnew file exists, inspect it and merge anything new in that file with zoneminder.conf. - Verify the SSL REquirements meet your needs. Read README.https if necessary. + Verify the SSL Requirements meet your needs. Read README.https if necessary. The contents of this file must be merged into your Apache configuration. See step 6 of the installation section if you have not already done this during a previous upgrade. + IMPORTANT: Failure to complete this step properly will result in a mostly + empty or significantly corrupted web console post-upgrade. + 4. Upgrade the database before starting ZoneMinder. Most upgrades can be performed by executing the following command: diff --git a/distros/redhat/readme/README.https b/distros/redhat/readme/README.https index 4b02aaa0d..620588bf9 100644 --- a/distros/redhat/readme/README.https +++ b/distros/redhat/readme/README.https @@ -20,7 +20,8 @@ experience. to do this: https://wiki.centos.org/HowTos/Https . Additionally, Googling "centos certificate" reveals many articles on the subject. -3. You can turn off HTTPS entirely by simply commenting out the SSLRequireSSL - directives found in /etc/httpd/conf.d/zoneminder.conf. You should also - comment out the HTTP -> HTTPS Rewrite rule. +3. When using Apache, you can turn off HTTPS entirely by simply commenting + out the SSLRequireSSL directives found in + /etc/zm/www/zoneminder.apache.conf. You should also comment out the + HTTP -> HTTPS Rewrite rule. diff --git a/distros/redhat/nginx/README.Fedora b/distros/redhat/readme/README.nginx similarity index 78% rename from distros/redhat/nginx/README.Fedora rename to distros/redhat/readme/README.nginx index 90d760952..cca4e72c2 100644 --- a/distros/redhat/nginx/README.Fedora +++ b/distros/redhat/readme/README.nginx @@ -1,24 +1,3 @@ -What's New -========== - -1. See the ZoneMinder release notes for a list of new features: - https://github.com/ZoneMinder/zoneminder/releases - -2. The contents of the ZoneMinder Apache config file have changed. In - addition, this ZoneMinder package now requires you to manually symlink the - ZoneMinder Apache config file. See new install step 6 and upgrade step 3 - below for details. - -3. This is an experimental build of ZoneMinder supporting nginx, rather than - apache web server. - -4. If you have installed ZoneMinder from the FedBerry repositories, this build - of ZoneMinder has support for Raspberry Pi hardware acceleration when using - ffmpeg. Unforunately, there is a problem with the same hardware acceleration - when using libvlc. Consequently, libvlc support in this build of ZoneMinder - has been disabled until the problem is resolved. See the following bug - report for details: https://trac.videolan.org/vlc/ticket/18594 - New installs ============ @@ -52,7 +31,7 @@ New installs Once the file has been saved, set proper file & ownership permissions on it: - sudo chown root:apache *.conf + sudo chown root:nginx *.conf sudo chmod 640 *.conf 4. Edit /etc/php.ini, uncomment the date.timezone line, and add your local @@ -82,8 +61,7 @@ New installs 6. Configure the web server This package uses the HTTPS protocol by default to access the web portal, - using the default self signed certificate on your system. Requests using - HTTP will auto-redirect to HTTPS. + using the default self signed certificate on your system. Inspect the web server configuration file and verify it meets your needs: @@ -92,23 +70,15 @@ New installs If you are running other web enabled services then you may need to edit this file to suite. See README.https to learn about other alternatives. + If you wish http requests to auto-redirect to https requests, then link or + copy /etc/zm/www/redirect.nginx.conf into /etc/nginx/default.d folder. + When in doubt, proceed with the default: - sudo ln -s /etc/zm/www/zoneminder.conf /etc/nginx/default.d/ + sudo ln -sf /etc/zm/www/zoneminder.nginx.conf /etc/nginx/conf.d/ + sudo ln -sf /etc/zm/www/redirect.nginx.conf /etc/nginx/default.d/ -7. Fcgiwrap is required when using ZoneMinder with Nginx. At the time of this - writing, fcgiwrap is not yet available in the Fedora repos. Until it - becomes available, you may install it from my Copr repository: - - https://copr.fedorainfracloud.org/coprs/kni/fcgiwrap/ - - Follow the intructions on that site to enable the repo. Once enabled, - install fcgiwrap: - - sudo dnf install fcgiwrap - - After fcgiwrap is installed, it must be configured. Edit - /etc/sysconfig/fcgiwrap and set DAEMON_PROCS to the maximum number of +7. Edit /etc/sysconfig/fcgiwrap and set DAEMON_PROCS to the maximum number of simulatneous streams the server should support. Generally, a good minimum value for this equals the total number of cameras you expect to view at the same time. @@ -193,7 +163,7 @@ Upgrades sudo zmupdate.pl --user=root --pass= --version= -5. Now restart nginx and php-fpm then start and zoneminder: +5. Now restart nginx and php-fpm then start zoneminder: sudo systemctl restart nginx sudo systemctl restart php-fpm diff --git a/distros/redhat/zoneminder.spec b/distros/redhat/zoneminder.spec index 27cc59ba9..cbda079ca 100644 --- a/distros/redhat/zoneminder.spec +++ b/distros/redhat/zoneminder.spec @@ -1,3 +1,4 @@ +# Leaving this to allow one to build zoneminder-http subpackage using arbitrary user account %global zmuid_final apache %global zmgid_final apache @@ -7,10 +8,6 @@ # CakePHP-Enum-Behavior is configured as a git submodule %global ceb_version 1.0-zm -%if "%{zmuid_final}" == "nginx" -%global with_nginx 1 -%endif - %global sslcert %{_sysconfdir}/pki/tls/certs/localhost.crt %global sslkey %{_sysconfdir}/pki/tls/private/localhost.key @@ -22,10 +19,11 @@ %global with_apcu_bc 1 %endif +# The default for everything but el7 these days %global _hardened_build 1 Name: zoneminder -Version: 1.32.2 +Version: 1.33.0 Release: 1%{?dist} Summary: A camera monitoring and analysis tool Group: System Environment/Daemons @@ -44,7 +42,7 @@ BuildRequires: systemd-devel BuildRequires: mariadb-devel BuildRequires: perl-podlators BuildRequires: polkit-devel -BuildRequires: cmake >= 2.8.7 +BuildRequires: cmake3 BuildRequires: gnutls-devel BuildRequires: bzip2-devel BuildRequires: pcre-devel @@ -74,6 +72,7 @@ BuildRequires: vlc-devel BuildRequires: libcurl-devel BuildRequires: libv4l-devel BuildRequires: desktop-file-utils +BuildRequires: gzip # ZoneMinder looks for and records the location of the ffmpeg binary during build BuildRequires: ffmpeg @@ -83,10 +82,25 @@ BuildRequires: ffmpeg-devel BuildRequires: libmp4v2-devel BuildRequires: x264-devel -%{?with_nginx:Requires: nginx} -%{?with_nginx:Requires: php-fpm} -%{!?with_nginx:Requires: httpd} -%{!?with_nginx:Requires: php} +# Allow existing user base to seamlessly transition to sub-packages +Requires: %{name}-common%{?_isa} = %{version}-%{release} +Requires: %{name}-httpd%{?_isa} = %{version}-%{release} + +%description +ZoneMinder is a set of applications which is intended to provide a complete +solution allowing you to capture, analyze, record and monitor any cameras you +have attached to a Linux based machine. It is designed to run on kernels which +support the Video For Linux (V4L) interface and has been tested with cameras +attached to BTTV cards, various USB cameras and IP network cameras. It is +designed to support as many cameras as you can attach to your computer without +too much degradation of performance. + +This is a meta package for backwards compatibility with the existing +ZoneMinder user base. + +%package common +Summary: Common files for ZoneMinder, not tied to a specific web server + Requires: php-mysqli Requires: php-common Requires: php-gd @@ -111,16 +125,12 @@ Requires: perl(Net::FTP) Requires: perl(LWP::Protocol::https) Requires: ca-certificates Requires: zip - -Requires(post): systemd -Requires(post): systemd-sysv -Requires(preun): systemd -Requires(postun): systemd +%{?systemd_requires} Requires(post): %{_bindir}/gpasswd -Requires(post): %{_bindir}/less +Requires(post): %{_bindir}/chown -%description +%description common ZoneMinder is a set of applications which is intended to provide a complete solution allowing you to capture, analyze, record and monitor any cameras you have attached to a Linux based machine. It is designed to run on kernels which @@ -129,15 +139,57 @@ attached to BTTV cards, various USB cameras and IP network cameras. It is designed to support as many cameras as you can attach to your computer without too much degradation of performance. +This is a meta-package that exists solely to allow the existing user base to +seamlessly transition to sub-packages. + +%package httpd +Summary: ZoneMinder configuration for Apache web server +Requires: %{name}-common%{?_isa} = %{version}-%{release} +Requires: httpd +Requires: php + +Conflicts: %{name}-nginx + +%description httpd +ZoneMinder is a set of applications which is intended to provide a complete +solution allowing you to capture, analyze, record and monitor any cameras you +have attached to a Linux based machine. It is designed to run on kernels which +support the Video For Linux (V4L) interface and has been tested with cameras +attached to BTTV cards, various USB cameras and IP network cameras. It is +designed to support as many cameras as you can attach to your computer without +too much degradation of performance. + +This sub-package contains configuration specific to Apache web server + +%package nginx +Summary: ZoneMinder configuration for Nginx web server +Requires: %{name}-common%{?_isa} = %{version}-%{release} +Requires: nginx +Requires: php-fpm +Requires: fcgiwrap + +Conflicts: %{name}-httpd + +%description nginx +ZoneMinder is a set of applications which is intended to provide a complete +solution allowing you to capture, analyze, record and monitor any cameras you +have attached to a Linux based machine. It is designed to run on kernels which +support the Video For Linux (V4L) interface and has been tested with cameras +attached to BTTV cards, various USB cameras and IP network cameras. It is +designed to support as many cameras as you can attach to your computer without +too much degradation of performance. + +This sub-package contains support for ZoneMinder with the Nginx web server + %prep %autosetup -p 1 -a 1 -%{__rm} -rf ./web/api/app/Plugin/Crud -%{__mv} -f crud-%{crud_version} ./web/api/app/Plugin/Crud +rm -rf ./web/api/app/Plugin/Crud +mv -f crud-%{crud_version} ./web/api/app/Plugin/Crud # The all powerful autosetup macro does not work after the second source tarball -%{__gzip} -dc %{_sourcedir}/cakephp-enum-behavior-%{ceb_version}.tar.gz | tar -xvvf - -%{__rm} -rf ./web/api/app/Plugin/CakePHP-Enum-Behavior -%{__mv} -f CakePHP-Enum-Behavior-%{ceb_version} ./web/api/app/Plugin/CakePHP-Enum-Behavior +gzip -dc %{_sourcedir}/cakephp-enum-behavior-%{ceb_version}.tar.gz | tar -xvvf - +rm -rf ./web/api/app/Plugin/CakePHP-Enum-Behavior +mv -f CakePHP-Enum-Behavior-%{ceb_version} ./web/api/app/Plugin/CakePHP-Enum-Behavior # Change the following default values ./utils/zmeditconfigdata.sh ZM_OPT_CAMBOZOLA yes @@ -148,9 +200,9 @@ too much degradation of performance. ./utils/zmeditconfigdata.sh ZM_OPT_FAST_DELETE no %build -%cmake \ +%cmake3 \ -DZM_WEB_USER="%{zmuid_final}" \ - -DZM_WEB_GROUP="%{zmuid_final}" \ + -DZM_WEB_GROUP="%{zmgid_final}" \ -DZM_TARGET_DISTRO="%{zmtargetdistro}" \ . @@ -172,10 +224,13 @@ find %{buildroot} \( -name .htaccess -or -name .editorconfig -or -name .packlist find %{buildroot}%{_datadir}/zoneminder/www/api \( -name cake -or -name cake.php \) -type f -exec sed -i 's\^#!/usr/bin/env bash$\#!%{_buildshell}\' {} \; -exec %{__chmod} 755 {} \; # Use the system cacert file rather then the one bundled with CakePHP -%{__rm} -f %{buildroot}%{_datadir}/zoneminder/www/api/lib/Cake/Config/cacert.pem -%{__ln_s} ../../../../../../../..%{_sysconfdir}/pki/tls/certs/ca-bundle.crt %{buildroot}%{_datadir}/zoneminder/www/api/lib/Cake/Config/cacert.pem +rm -f %{buildroot}%{_datadir}/zoneminder/www/api/lib/Cake/Config/cacert.pem +ln -s ../../../../../../../..%{_sysconfdir}/pki/tls/certs/ca-bundle.crt %{buildroot}%{_datadir}/zoneminder/www/api/lib/Cake/Config/cacert.pem -%post +# Handle the polkit file differently for web server agnostic support (see post) +rm -f %{buildroot}%{_datadir}/polkit-1/rules.d/com.zoneminder.systemctl.rules + +%post common # Initial installation if [ $1 -eq 1 ] ; then %systemd_post %{name}.service @@ -183,28 +238,48 @@ fi # Upgrade from a previous version of zoneminder if [ $1 -eq 2 ] ; then - # Add any new PTZ control configurations to the database (will not overwrite) %{_bindir}/zmcamtool.pl --import >/dev/null 2>&1 || : # Freshen the database %{_bindir}/zmupdate.pl -f >/dev/null 2>&1 || : - - # We can't run this automatically when new sql account permissions need to - # be manually added first - # Run zmupdate non-interactively - # zmupdate.pl --nointeractive fi +# Warn the end user to read the README file +echo -e "\nVERY IMPORTANT: Before starting ZoneMinder, you must read the README file\nto finish the installation or upgrade!" +echo -e "\nThe README file is located here: %{_pkgdocdir}-common/README\n" + +%post httpd +# For the case of changing from nginx <-> httpd, files in these folders must change ownership if they exist +%{_bindir}/chown -R %{zmuid_final}:%{zmgid_final} %{_sharedstatedir}/php/session/* >/dev/null 2>&1 || : +%{_bindir}/chown -R %{zmuid_final}:%{zmgid_final} %{_localstatedir}/log/zoneminder/* >/dev/null 2>&1 || : + +ln -sf %{_sysconfdir}/zm/www/com.zoneminder.systemctl.rules.httpd %{_datadir}/polkit-1/rules.d/com.zoneminder.systemctl.rules +# backwards compatibility +ln -sf %{_sysconfdir}/zm/www/zoneminder.httpd.conf %{_sysconfdir}/zm/www/zoneminder.conf + # Allow zoneminder access to local video sources, serial ports, and x10 %{_bindir}/gpasswd -a %{zmuid_final} video >/dev/null 2>&1 || : %{_bindir}/gpasswd -a %{zmuid_final} dialout >/dev/null 2>&1 || : -# Warn the end user to read the README file -echo -e "\nVERY IMPORTANT: Before starting ZoneMinder, you must read the README file\nto finish the installation or upgrade!" -echo -e "\nThe README file is located here: %{_pkgdocdir}/README\n" +%post nginx + +# Php package owns the session folder and sets group ownership to apache account +# We could override the folder permission, but adding nginx to the apache group works better +%{_bindir}/gpasswd -a nginx apache >/dev/null 2>&1 || : + +# For the case of changing from httpd <-> nginx, files in these folders must change ownership if they exist +%{_bindir}/chown -R nginx:nginx %{_sharedstatedir}/php/session/* >/dev/null 2>&1 || : +%{_bindir}/chown -R nginx:nginx %{_localstatedir}/log/zoneminder/* >/dev/null 2>&1 || : + +ln -sf %{_sysconfdir}/zm/www/com.zoneminder.systemctl.rules.nginx %{_datadir}/polkit-1/rules.d/com.zoneminder.systemctl.rules +# backwards compatibility +ln -sf %{_sysconfdir}/zm/www/zoneminder.nginx.conf %{_sysconfdir}/zm/www/zoneminder.conf + +# Allow zoneminder access to local video sources, serial ports, and x10 +%{_bindir}/gpasswd -a nginx video >/dev/null 2>&1 || : +%{_bindir}/gpasswd -a nginx dialout >/dev/null 2>&1 || : -%if 0%{?with_nginx} # Nginx does not create an SSL certificate like the apache package does so lets do that here if [ -f %{sslkey} -o -f %{sslcert} ]; then exit 0 @@ -230,7 +305,6 @@ SomeOrganizationalUnit ${FQDN} root@${FQDN} EOF -%endif %preun %systemd_preun %{name}.service @@ -238,19 +312,12 @@ EOF %postun %systemd_postun_with_restart %{name}.service -%triggerun -- zoneminder < 1.25.0-4 -# Save the current service runlevel info -# User must manually run systemd-sysv-convert --apply zoneminder -# to migrate them to systemd targets -%{_bindir}/systemd-sysv-convert --save zoneminder >/dev/null 2>&1 ||: - -# Run these because the SysV package being removed won't do them -/sbin/chkconfig --del zoneminder >/dev/null 2>&1 || : -/bin/systemctl try-restart zoneminder.service >/dev/null 2>&1 || : - %files +# nothing + +%files common %license COPYING -%doc AUTHORS README.md distros/redhat/readme/README distros/redhat/readme/README.https +%doc AUTHORS README.md distros/redhat/readme/README distros/redhat/readme/README.httpd distros/redhat/readme/README.nginx distros/redhat/readme/README.https # We want these two folders to have "normal" read permission # compared to the folder contents @@ -260,18 +327,11 @@ EOF # Config folder contents contain sensitive info # and should not be readable by normal users %{_sysconfdir}/zm/conf.d/README -%config(noreplace) %attr(640,root,%{zmgid_final}) %{_sysconfdir}/zm/zm.conf -%config(noreplace) %attr(640,root,%{zmgid_final}) %{_sysconfdir}/zm/conf.d/*.conf -%ghost %attr(640,root,%{zmgid_final}) %{_sysconfdir}/zm/conf.d/zmcustom.conf -%config(noreplace) %attr(644,root,root) %{_sysconfdir}/zm/www/zoneminder.conf -%config(noreplace) %{_sysconfdir}/zm/www/zoneminder.php-fpm.conf %config(noreplace) %{_sysconfdir}/logrotate.d/zoneminder -%{_tmpfilesdir}/zoneminder.conf %{_unitdir}/zoneminder.service %{_datadir}/polkit-1/actions/com.zoneminder.systemctl.policy -%{_datadir}/polkit-1/rules.d/com.zoneminder.systemctl.rules %{_bindir}/zmsystemctl.pl %{_bindir}/zma @@ -304,6 +364,17 @@ EOF %{_datadir}/zoneminder/ %{_datadir}/applications/*zoneminder.desktop +%files httpd +%config(noreplace) %attr(640,root,%{zmgid_final}) %{_sysconfdir}/zm/zm.conf +%config(noreplace) %attr(640,root,%{zmgid_final}) %{_sysconfdir}/zm/conf.d/0*.conf +%ghost %attr(640,root,%{zmgid_final}) %{_sysconfdir}/zm/conf.d/zmcustom.conf +%config(noreplace) %{_sysconfdir}/zm/www/zoneminder.httpd.conf +%ghost %{_sysconfdir}/zm/www/zoneminder.conf +%config(noreplace) %{_sysconfdir}/zm/www/com.zoneminder.systemctl.rules.httpd +%ghost %{_datadir}/polkit-1/rules.d/com.zoneminder.systemctl.rules + +%{_unitdir}/zoneminder.service.d/zm-httpd.conf +%{_tmpfilesdir}/zoneminder.httpd.tmpfiles.conf %dir %attr(755,%{zmuid_final},%{zmgid_final}) %{_sharedstatedir}/zoneminder %dir %attr(755,%{zmuid_final},%{zmgid_final}) %{_sharedstatedir}/zoneminder/events %dir %attr(755,%{zmuid_final},%{zmgid_final}) %{_sharedstatedir}/zoneminder/images @@ -313,9 +384,44 @@ EOF %dir %attr(755,%{zmuid_final},%{zmgid_final}) %{_localstatedir}/cache/zoneminder %dir %attr(755,%{zmuid_final},%{zmgid_final}) %{_localstatedir}/log/zoneminder %dir %attr(755,%{zmuid_final},%{zmgid_final}) %{_localstatedir}/spool/zoneminder-upload -%dir %attr(755,%{zmuid_final},%{zmgid_final}) %{_localstatedir}/run/zoneminder + +%files nginx +%config(noreplace) %attr(640,root,nginx) %{_sysconfdir}/zm/zm.conf +%config(noreplace) %attr(640,root,nginx) %{_sysconfdir}/zm/conf.d/*.conf +%ghost %attr(640,root,nginx) %{_sysconfdir}/zm/conf.d/zmcustom.conf +%config(noreplace) %{_sysconfdir}/zm/www/zoneminder.nginx.conf +%config(noreplace) %{_sysconfdir}/zm/www/redirect.nginx.conf +%ghost %{_sysconfdir}/zm/www/zoneminder.conf +%config(noreplace) %{_sysconfdir}/zm/www/com.zoneminder.systemctl.rules.nginx +%ghost %{_datadir}/polkit-1/rules.d/com.zoneminder.systemctl.rules + +%config(noreplace) %{_sysconfdir}/php-fpm.d/zoneminder.php-fpm.conf + + +%{_unitdir}/zoneminder.service.d/zm-nginx.conf +%{_tmpfilesdir}/zoneminder.nginx.tmpfiles.conf +%dir %attr(755,nginx,nginx) %{_sharedstatedir}/zoneminder +%dir %attr(755,nginx,nginx) %{_sharedstatedir}/zoneminder/events +%dir %attr(755,nginx,nginx) %{_sharedstatedir}/zoneminder/images +%dir %attr(755,nginx,nginx) %{_sharedstatedir}/zoneminder/sock +%dir %attr(755,nginx,nginx) %{_sharedstatedir}/zoneminder/swap +%dir %attr(755,nginx,nginx) %{_sharedstatedir}/zoneminder/temp +%dir %attr(755,nginx,nginx) %{_localstatedir}/cache/zoneminder +%dir %attr(755,nginx,nginx) %{_localstatedir}/log/zoneminder +%dir %attr(755,nginx,nginx) %{_localstatedir}/spool/zoneminder-upload %changelog +* Tue Dec 11 2018 Andrew Bauer - 1.33.0-1 +- Bump tp 1.33.0 Development + +* Sat Dec 08 2018 Andrew Bauer - 1.32.3-1 +- 1.32.3 Release +- Break into sub-packages + +* Tue Nov 13 2018 Antonio Trande - 1.32.2-2 +- Rebuild for ffmpeg-3.4.5 on el7 +- Use CMake3 + * Sat Oct 13 2018 Andrew Bauer - 1.32.2-1 - 1.32.2 release - Bug fix release diff --git a/distros/ubuntu1604/control b/distros/ubuntu1604/control index d4fe74e79..d91fd4f53 100644 --- a/distros/ubuntu1604/control +++ b/distros/ubuntu1604/control @@ -3,13 +3,14 @@ Section: net Priority: optional Maintainer: Dmitry Smirnov Uploaders: Vagrant Cascadian -Build-Depends: debhelper (>= 9), dh-systemd, python-sphinx | python3-sphinx, apache2-dev, dh-linktree +Build-Depends: debhelper (>= 9), dh-systemd, python-sphinx | python3-sphinx, apache2-dev, dh-linktree, dh-systemd, dh-apache2 ,cmake ,libx264-dev, libmp4v2-dev ,libavdevice-dev (>= 6:10~) ,libavcodec-dev (>= 6:10~) ,libavformat-dev (>= 6:10~) ,libavutil-dev (>= 6:10~) + ,libswresample-dev ,libswscale-dev (>= 6:10~) ,ffmpeg | libav-tools ,net-tools @@ -41,7 +42,9 @@ Package: zoneminder Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends} ,javascript-common - ,libmp4v2-2, libx264-142|libx264-148|libx264-152, libswscale-ffmpeg3|libswscale4|libswscale3 + ,libmp4v2-2, libx264-142|libx264-148|libx264-152 + ,libswscale-ffmpeg3|libswscale4|libswscale3|libswscale5 + ,libswresample2|libswresample3|libswresample24 ,ffmpeg | libav-tools ,libdate-manip-perl, libmime-lite-perl, libmime-tools-perl ,libdbd-mysql-perl diff --git a/docs/faq.rst b/docs/faq.rst index 7735ff38f..a703fd065 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -311,7 +311,7 @@ There are a number of specific reasons why processor loads can be high either by The main causes are. - * Using a video palette other than greyscale or RGB24. This can cause a relatively minor performace hit, though still significant. Although some cameras and cards require using planar palettes ZM currently doesn't support this format internally and each frame is converted to an RGB representation prior to processing. Unless you have compelling reasons for using YUV or reduced RGB type palettes such as hitting USB transfer limits I would experiment to see if RGB24 or greyscale is quicker. Put your monitors into 'Monitor' mode so that only the capture daemons are running and monitor the process load of these (the 'zmc' processes) using top. Try it with various palettes to see if it makes a difference. + * Using a video palette other than greyscale or RGB24. This can cause a relatively minor performance hit, though still significant. Although some cameras and cards require using planar palettes ZM currently doesn't support this format internally and each frame is converted to an RGB representation prior to processing. Unless you have compelling reasons for using YUV or reduced RGB type palettes such as hitting USB transfer limits I would experiment to see if RGB24 or greyscale is quicker. Put your monitors into 'Monitor' mode so that only the capture daemons are running and monitor the process load of these (the 'zmc' processes) using top. Try it with various palettes to see if it makes a difference. * Big image sizes. A image of 640x480 requires at least four times the processing of a 320x240 image. Experiment with different sizes to see what effect it may have. Sometimes a large image is just two interlaced smaller frames so has no real benefit anyway. This is especially true for analog cameras/cards as image height over 320 (NTSC) or 352 PAL) are invariably interlaced. * Capture frame rates. Unless there's a compelling reason in your case there is often little benefit in running cameras at 25fps when 5-10fps would often get you results just as good. Try changing your monitor settings to limit your cameras to lower frame rates. You can still configure ZM to ignore these limits and capture as fast as possible when motion is detected. * Run function. Obviously running in Record or Mocord modes or in Modect with lots of events generates a lot of DB and file activity and so CPU and load will increase. diff --git a/docs/userguide/definemonitor.rst b/docs/userguide/definemonitor.rst index bcd195570..7641cb751 100644 --- a/docs/userguide/definemonitor.rst +++ b/docs/userguide/definemonitor.rst @@ -58,7 +58,7 @@ Maximum FPS Alarm Maximum FPS If you have specified a Maximum FPS it may be that you don’t want this limitation to apply when your monitor is recording motion or other event. This setting allows you to override the Maximum FPS value if this circumstance occurs. As with the Maximum FPS setting leaving this blank implies no limit so if you have set a maximum fps in the previous option then when an alarm occurs this limit would be ignored and ZoneMinder would capture as fast as possible for the duration of the alarm, returning to the limited value after the alarm has concluded. Equally you could set this to the same, or higher (or even lower) value than Maximum FPS for more precise control over the capture rate in the event of an alarm. - **IMPORTANT:** This field is subject to the same limitations as the Maxium FPS field. Ignoring these limitations will produce undesriable results. + **IMPORTANT:** This field is subject to the same limitations as the Maximum FPS field. Ignoring these limitations will produce undesriable results. Reference Image Blend %ge Each analysed image in ZoneMinder is a composite of previous images and is formed by applying the current image as a certain percentage of the previous reference image. Thus, if we entered the value of 10 here, each image’s part in the reference image will diminish by a factor of 0.9 each time round. So a typical reference image will be 10% the previous image, 9% the one before that and then 8.1%, 7.2%, 6.5% and so on of the rest of the way. An image will effectively vanish around 25 images later than when it was added. This blend value is what is specified here and if higher will make slower progressing events less detectable as the reference image would change more quickly. Similarly events will be deemed to be over much sooner as the reference image adapts to the new images more quickly. In signal processing terms the higher this value the steeper the event attack and decay of the signal. It depends on your particular requirements what the appropriate value would be for you but start with 10 here and adjust it (usually down) later if necessary. @@ -205,7 +205,8 @@ Warm-up Frames Pre/Post Event Image Buffer These options determine how many frames from before and after an event should be preserved with it. This allows you to view what happened immediately prior and subsequent to the event. A value of 10 for both of these will get you started but if you get a lot of short events and would prefer them to run together to form fewer longer ones then increase the Post Event buffer size. The pre-event buffer is a true buffer and should not really exceed half the ring buffer size. However the post-event buffer is just a count that is applied to captured frames and so can be managed more flexibly. You should also bear in mind the frame rate of the camera when choosing these values. For instance a network camera capturing at 1FPS will give you 10 seconds before and after each event if you chose 10 here. This may well be too much and pad out events more than necessary. However a fast video card may capture at 25FPS and you will want to ensure that this setting enables you to view a reasonable time frame pre and post event. Stream Replay Image Buffer - This option ... + The number of frames buffered to allow pausing and rewinding of the stream when live viewing a monitor. A value of 0 disables the feature. + Frames are buffered to ZM_PATH_SWAP. If this path points to a physical drive, a lot of IO will be caused during live view / montage. If you experience high system load in those situations, either disable the feature or use a RAM drive for ZM_PATH_SWAP. Alarm Frame Count This option allows you to specify how many consecutive alarm frames must occur before an alarm event is generated. The usual, and default, value is 1 which implies that any alarm frame will cause or participate in an event. You can enter any value up to 16 here to eliminate bogus events caused perhaps by screen flickers or other transients. Values over 3 or 4 are unlikely to be useful however. Please note that if you have statistics recording enabled then currently statistics are not recorded for the first ‘Alarm Frame Count’-1 frames of an event. So if you set this value to 5 then the first 4 frames will be missing statistics whereas the more usual value of 1 will ensure that all alarm frames have statistics recorded. diff --git a/misc/CMakeLists.txt b/misc/CMakeLists.txt index 3f92bd5da..990b8ce06 100644 --- a/misc/CMakeLists.txt +++ b/misc/CMakeLists.txt @@ -2,6 +2,7 @@ # Create files from the .in files configure_file(apache.conf.in "${CMAKE_CURRENT_BINARY_DIR}/apache.conf" @ONLY) +configure_file(nginx.conf.in "${CMAKE_CURRENT_BINARY_DIR}/nginx.conf" @ONLY) configure_file(logrotate.conf.in "${CMAKE_CURRENT_BINARY_DIR}/logrotate.conf" @ONLY) configure_file(syslog.conf.in "${CMAKE_CURRENT_BINARY_DIR}/syslog.conf" @ONLY) configure_file(com.zoneminder.systemctl.policy.in "${CMAKE_CURRENT_BINARY_DIR}/com.zoneminder.systemctl.policy" @ONLY) diff --git a/misc/nginx.conf.in b/misc/nginx.conf.in new file mode 100644 index 000000000..47dc3ce68 --- /dev/null +++ b/misc/nginx.conf.in @@ -0,0 +1,61 @@ +# +# PLEASE NOTE THAT THIS FILE IS INTENDED FOR GUIDANCE ONLY AND MAY NOT BE APPROPRIATE FOR YOUR DISTRIBUTION +# + +server { + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + server_name = localhost $hostname; + + ssl_certificate "/etc/pki/tls/certs/localhost.crt"; + ssl_certificate_key "/etc/pki/tls/private/localhost.key"; + ssl_session_cache shared:SSL:1m; + ssl_session_timeout 10m; + ssl_ciphers PROFILE=SYSTEM; + ssl_prefer_server_ciphers on; + + # Auto redirect to server/zm when no url suffix was given + location = / { + return 301 zm; + } + + location /cgi-bin-zm { + gzip off; + alias "@ZM_CGIDIR@"; + + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $request_filename; + fastcgi_pass unix:/run/fcgiwrap.sock; + } + + location /zm/cache { + alias "@ZM_CACHEDIR@"; + } + + location /zm { + gzip off; + alias "@ZM_WEBDIR@"; + index index.php; + + location ~ \.php$ { + try_files $uri =404; + expires epoch; + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $request_filename; + fastcgi_index index.php; + fastcgi_pass unix:/run/php-fpm/www.sock; + } + + location ~ \.(jpg|jpeg|gif|png|ico)$ { + access_log off; + expires 33d; + } + + location /zm/api/ { + alias "@ZM_WEBDIR@"; + rewrite ^/zm/api(.+)$ /zm/api/app/webroot/index.php?p=$1 last; + } + } + +} + diff --git a/onvif/proxy/lib/ONVIF/Device/Interfaces/Device/DevicePort.pm b/onvif/proxy/lib/ONVIF/Device/Interfaces/Device/DevicePort.pm index 35f259f31..c3c488ba1 100644 --- a/onvif/proxy/lib/ONVIF/Device/Interfaces/Device/DevicePort.pm +++ b/onvif/proxy/lib/ONVIF/Device/Interfaces/Device/DevicePort.pm @@ -25,15 +25,13 @@ sub GetServices { soap_action => 'http://www.onvif.org/ver10/device/wsdl/GetServices', style => 'document', body => { - - - 'use' => 'literal', + use => 'literal', namespace => 'http://schemas.xmlsoap.org/wsdl/soap/', encodingStyle => '', parts => [qw( ONVIF::Device::Elements::GetServices )], }, header => { - + }, headerfault => { @@ -50,9 +48,7 @@ sub GetServiceCapabilities { soap_action => 'http://www.onvif.org/ver10/device/wsdl/GetServiceCapabilities', style => 'document', body => { - - - 'use' => 'literal', + use => 'literal', namespace => 'http://schemas.xmlsoap.org/wsdl/soap/', encodingStyle => '', parts => [qw( ONVIF::Device::Elements::GetServiceCapabilities )], @@ -3059,7 +3055,7 @@ Returns a L object. @@ -3069,7 +3065,7 @@ Returns a L object. @@ -3085,7 +3081,7 @@ Returns a L object. diff --git a/onvif/proxy/lib/ONVIF/PTZ/Interfaces/PTZ/PTZPort.pm b/onvif/proxy/lib/ONVIF/PTZ/Interfaces/PTZ/PTZPort.pm index 8d42d439e..d1c5faa29 100644 --- a/onvif/proxy/lib/ONVIF/PTZ/Interfaces/PTZ/PTZPort.pm +++ b/onvif/proxy/lib/ONVIF/PTZ/Interfaces/PTZ/PTZPort.pm @@ -830,7 +830,7 @@ Returns a L object. diff --git a/scripts/CMakeLists.txt b/scripts/CMakeLists.txt index f1bfa2ed1..bd1885fbe 100644 --- a/scripts/CMakeLists.txt +++ b/scripts/CMakeLists.txt @@ -10,6 +10,7 @@ configure_file(zmdc.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmdc.pl" @ONLY) configure_file(zmfilter.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmfilter.pl" @ONLY) configure_file(zmonvif-probe.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmonvif-probe.pl" @ONLY) configure_file(zmpkg.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmpkg.pl" @ONLY) +configure_file(zmrecover.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmrecover.pl" @ONLY) configure_file(zmtrack.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmtrack.pl" @ONLY) configure_file(zmtrigger.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmtrigger.pl" @ONLY) configure_file(zmupdate.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmupdate.pl" @ONLY) @@ -31,11 +32,11 @@ configure_file(zm.in "${CMAKE_CURRENT_BINARY_DIR}/zm" @ONLY) file(GLOB perlscripts "*.pl") FOREACH(PERLSCRIPT ${perlscripts}) get_filename_component(PERLSCRIPTNAME ${PERLSCRIPT} NAME) - POD2MAN(${PERLSCRIPT} zoneminder-${PERLSCRIPTNAME} 8) + POD2MAN(${PERLSCRIPT} zoneminder-${PERLSCRIPTNAME} 8 ${ZM_MANPAGE_DEST_PREFIX}) ENDFOREACH(PERLSCRIPT ${perlscripts}) # Install the perl scripts -install(FILES "${CMAKE_CURRENT_BINARY_DIR}/zmaudit.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmcontrol.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmdc.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmfilter.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmonvif-probe.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmpkg.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmtrack.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmtrigger.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmupdate.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmvideo.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmwatch.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmcamtool.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmtelemetry.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmstats.pl" DESTINATION "${CMAKE_INSTALL_FULL_BINDIR}" PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/zmaudit.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmcontrol.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmdc.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmfilter.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmonvif-probe.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmpkg.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmrecover.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmtrack.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmtrigger.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmupdate.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmvideo.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmwatch.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmcamtool.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmtelemetry.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmstats.pl" DESTINATION "${CMAKE_INSTALL_FULL_BINDIR}" PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) if(NOT ZM_NO_X10) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/zmx10.pl" DESTINATION "${CMAKE_INSTALL_FULL_BINDIR}" PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) endif(NOT ZM_NO_X10) diff --git a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in index eac13cb75..27d993894 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in +++ b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in @@ -1202,6 +1202,20 @@ our @options = ( category => 'logging', }, { + name => 'ZM_LOG_FFMPEG', + default => 'yes', + description => 'Log FFMPEG messages', + help => q` + When enabled (default is on), this option will log FFMPEG messages. + FFMPEG messages can be useful when debugging streaming issues. However, + depending on your distro and FFMPEG version, this may also result in + more logs than you'd typically like to see. If all your streams are working + well, you may choose to turn this off. + `, + type => $types{boolean}, + category => 'logging', + }, +{ name => 'ZM_LOG_DEBUG', default => 'no', description => 'Switch debugging on', diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Database.pm b/scripts/ZoneMinder/lib/ZoneMinder/Database.pm index dcc797bd4..173c0033c 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Database.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Database.pm @@ -208,7 +208,7 @@ sub zmDbGetMonitor { return undef; } my $res = $sth->execute($id); - if ( $res ) { + if ( !$res ) { Error("Can't execute '$sql': ".$sth->errstr()); return undef; } diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Event.pm b/scripts/ZoneMinder/lib/ZoneMinder/Event.pm index 5916f8cdb..9c6fe3cd5 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Event.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Event.pm @@ -31,12 +31,16 @@ use warnings; require ZoneMinder::Base; require ZoneMinder::Object; require ZoneMinder::Storage; +require ZoneMinder::Frame; require Date::Manip; require File::Find; require File::Path; require File::Copy; require File::Basename; require Number::Bytes::Human; +require Date::Parse; +require POSIX; +use Date::Format qw(time2str); #our @ISA = qw(ZoneMinder::Object); use parent qw(ZoneMinder::Object); @@ -50,9 +54,8 @@ use parent qw(ZoneMinder::Object); use ZoneMinder::Config qw(:all); use ZoneMinder::Logger qw(:all); use ZoneMinder::Database qw(:all); -require Date::Parse; -use vars qw/ $table $primary_key %fields $serial @identified_by/; +use vars qw/ $table $primary_key %fields $serial @identified_by %defaults/; $table = 'Events'; @identified_by = ('Id'); $serial = $primary_key = 'Id'; @@ -84,10 +87,21 @@ $serial = $primary_key = 'Id'; StateId Orientation DiskSpace + SaveJPEGs Scheme ); +%defaults = ( + Cause => q`'Unknown'`, + DefaultVideo => q`''`, + TotScore => '0', + Archived => '0', + Videoed => '0', + Uploaded => '0', + Emailed => '0', + Messaged => '0', + Executed => '0', +); -use POSIX; sub Time { if ( @_ > 1 ) { @@ -101,56 +115,10 @@ sub Time { return $_[0]{Time}; } -sub Name { - if ( @_ > 1 ) { - $_[0]{Name} = $_[1]; - } - return $_[0]{Name}; -} # end sub Name - -sub find { - shift if $_[0] eq 'ZoneMinder::Event'; - my %sql_filters = @_; - - my $sql = 'SELECT * FROM Events'; - my @sql_filters; - my @sql_values; - - if ( exists $sql_filters{Name} ) { - push @sql_filters , ' Name = ? '; - push @sql_values, $sql_filters{Name}; - } - if ( exists $sql_filters{Id} ) { - push @sql_filters , ' Id = ? '; - push @sql_values, $sql_filters{Id}; - } - - $sql .= ' WHERE ' . join(' AND ', @sql_filters ) if @sql_filters; - $sql .= ' LIMIT ' . $sql_filters{limit} if $sql_filters{limit}; - - my $sth = $ZoneMinder::Database::dbh->prepare_cached( $sql ) - or Fatal( "Can't prepare '$sql': ".$ZoneMinder::Database::dbh->errstr() ); - my $res = $sth->execute( @sql_values ) - or Fatal( "Can't execute '$sql': ".$sth->errstr() ); - - my @results; - - while( my $db_filter = $sth->fetchrow_hashref() ) { - my $filter = new ZoneMinder::Event( $$db_filter{Id}, $db_filter ); - push @results, $filter; - } # end while - $sth->finish(); - return @results; -} - -sub find_one { - my @results = find(@_); - return $results[0] if @results; -} - sub getPath { return Path( @_ ); } + sub Path { my $event = shift; @@ -170,6 +138,7 @@ sub Path { sub Scheme { my $self = shift; + $$self{Scheme} = shift if @_; if ( ! $$self{Scheme} ) { @@ -186,16 +155,15 @@ sub Scheme { sub RelativePath { my $event = shift; - if ( @_ ) { - $$event{RelativePath} = $_[0]; - } + + $$event{RelativePath} = shift if @_; if ( ! $$event{RelativePath} ) { if ( $$event{Scheme} eq 'Deep' ) { if ( $event->Time() ) { $$event{RelativePath} = join('/', $event->{MonitorId}, - strftime( '%y/%m/%d/%H/%M/%S', + POSIX::strftime( '%y/%m/%d/%H/%M/%S', localtime($event->Time()) ), ); @@ -207,7 +175,7 @@ sub RelativePath { if ( $event->Time() ) { $$event{RelativePath} = join('/', $event->{MonitorId}, - strftime( '%Y-%m-%d', localtime($event->Time())), + POSIX::strftime('%Y-%m-%d', localtime($event->Time())), $event->{Id}, ); } else { @@ -227,16 +195,15 @@ sub RelativePath { sub LinkPath { my $event = shift; - if ( @_ ) { - $$event{LinkPath} = $_[0]; - } + + $$event{LinkPath} = shift if @_; if ( ! $$event{LinkPath} ) { if ( $$event{Scheme} eq 'Deep' ) { if ( $event->Time() ) { $$event{LinkPath} = join('/', $event->{MonitorId}, - strftime( '%y/%m/%d', + POSIX::strftime( '%y/%m/%d', localtime($event->Time()) ), '.'.$$event{Id} @@ -483,9 +450,23 @@ sub delete_files { } } # end sub delete_files +sub StorageId { + my $event = shift; + if ( @_ ) { + $$event{StorageId} = shift; + delete $$event{Storage}; + delete $$event{Path}; + } + return $$event{StorageId}; +} + sub Storage { if ( @_ > 1 ) { $_[0]{Storage} = $_[1]; + if ( $_[0]{Storage} ) { + $_[0]{StorageId} = $_[0]{Storage}->Id(); + delete $_[0]{Path}; + } } if ( ! $_[0]{Storage} ) { $_[0]{Storage} = new ZoneMinder::Storage($_[0]{StorageId}); @@ -693,6 +674,92 @@ Debug("Done deleting files, returning"); return $error; } # end sub MoveTo +# Assumes $path is absolute +# +sub recover_timestamps { + my ( $Event, $path ) = @_; + $path = $Event->Path() if ! $path; + + if ( !opendir(DIR, $path) ) { + Error("Can't open directory '$path': $!"); + return; + } + my @contents = readdir(DIR); + Debug('Have ' . @contents . " files in $path"); + closedir(DIR); + + my @mp4_files = grep( /^\d+\-video\.mp4$/, @contents); + if ( @mp4_files ) { + $$Event{DefaultVideo} = $mp4_files[0]; + } + + my @analyse_jpgs = grep( /^\d+\-analyse\.jpg$/, @contents); + if ( @analyse_jpgs ) { + $$Event{Save_JPEGs} |= 2; + } + + my @capture_jpgs = grep( /^\d+\-capture\.jpg$/, @contents); + if ( @capture_jpgs ) { + $$Event{Frames} = scalar @capture_jpgs; + $$Event{Save_JPEGs} |= 1; + # can get start and end times from stat'ing first and last jpg + @capture_jpgs = sort { $a cmp $b } @capture_jpgs; + my $first_file = "$path/$capture_jpgs[0]"; + ( $first_file ) = $first_file =~ /^(.*)$/; + my $first_timestamp = (stat($first_file))[9]; + + my $last_file = "$path/$capture_jpgs[@capture_jpgs-1]"; + ( $last_file ) = $last_file =~ /^(.*)$/; + my $last_timestamp = (stat($last_file))[9]; + + my $duration = $last_timestamp - $first_timestamp; + $Event->Length($duration); + $Event->StartTime( Date::Format::time2str('%Y-%m-%d %H:%M:%S', $first_timestamp) ); + $Event->EndTime( Date::Format::time2str('%Y-%m-%d %H:%M:%S', $last_timestamp) ); + Debug("From capture Jpegs have duration $duration = $last_timestamp - $first_timestamp : $$Event{StartTime} to $$Event{EndTime}"); + $ZoneMinder::Database::dbh->begin_work(); + foreach my $jpg ( @capture_jpgs ) { + my ( $id ) = $jpg =~ /^(\d+)\-capture\.jpg$/; + + if ( ! ZoneMinder::Frame->find_one( EventId=>$$Event{Id}, FrameId=>$id ) ) { + my $file = "$path/$jpg"; + ( $file ) = $file =~ /^(.*)$/; + my $timestamp = (stat($file))[9]; + my $Frame = new ZoneMinder::Frame(); + $Frame->save({ + EventId=>$$Event{Id}, FrameId=>$id, + TimeStamp=>Date::Format::time2str('%Y-%m-%d %H:%M:%S',$timestamp), + Delta => $timestamp - $first_timestamp, + Type=>'Normal', + Score=>0, + }); + } + } + $ZoneMinder::Database::dbh->commit(); + } elsif ( @mp4_files ) { + my $file = "$path/$mp4_files[0]"; + ( $file ) = $file =~ /^(.*)$/; + + my $first_timestamp = (stat($file))[9]; + my $output = `ffprobe $file 2>&1`; + my ($duration) = $output =~ /Duration: [:\.0-9]+/gm; + Debug("From mp4 have duration $duration, start: $first_timestamp"); + + my ( $h, $m, $s, $u ); + if ( $duration =~ m/(\d+):(\d+):(\d+)\.(\d+)/ ) { + ( $h, $m, $s, $u ) = ($1, $2, $3, $4 ); + Debug("( $h, $m, $s, $u ) from /^(\\d{2}):(\\d{2}):(\\d{2})\.(\\d+)/"); + } + my $seconds = ($h*60*60)+($m*60)+$s; + $Event->Length($seconds.'.'.$u); + $Event->StartTime( Date::Format::time2str('%Y-%m-%d %H:%M:%S', $first_timestamp) ); + $Event->EndTime( Date::Format::time2str('%Y-%m-%d %H:%M:%S', $first_timestamp+$seconds) ); + } + if ( @mp4_files ) { + $Event->DefaultVideo($mp4_files[0]); + } +} + 1; __END__ diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm b/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm index f1a72c8cf..73ebc2d10 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Filter.pm @@ -30,6 +30,7 @@ use warnings; require ZoneMinder::Base; require Date::Manip; +require POSIX; use parent qw(ZoneMinder::Object); @@ -48,8 +49,6 @@ use ZoneMinder::Database qw(:all); require ZoneMinder::Storage; require ZoneMinder::Server; -use POSIX; - sub Name { if ( @_ > 1 ) { $_[0]{Name} = $_[1]; @@ -435,7 +434,7 @@ sub DateTimeToSQL { Error( "Unable to parse date string '$dt_str'\n" ); return( undef ); } - return( strftime( "%Y-%m-%d %H:%M:%S", localtime( $dt_val ) ) ); + return( POSIX::strftime( "%Y-%m-%d %H:%M:%S", localtime( $dt_val ) ) ); } 1; diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Frame.pm b/scripts/ZoneMinder/lib/ZoneMinder/Frame.pm new file mode 100644 index 000000000..6ce8f9ca0 --- /dev/null +++ b/scripts/ZoneMinder/lib/ZoneMinder/Frame.pm @@ -0,0 +1,78 @@ +# ========================================================================== +# +# ZoneMinder Monitor Module, $Date$, $Revision$ +# Copyright (C) 2001-2008 Philip Coombes +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# ========================================================================== +# +# This module contains the common definitions and functions used by the rest +# of the ZoneMinder scripts +# +package ZoneMinder::Frame; + +use 5.006; +use strict; +use warnings; + +require ZoneMinder::Base; +require ZoneMinder::Object; + +use parent qw(ZoneMinder::Object); + +use vars qw/ $table $primary_key %fields /; +$table = 'Frames'; +$primary_key = 'Id'; + +%fields = ( + Id => 'Id', + EventId => 'EventId', + FrameId => 'FrameId', + Type => 'Type', + TimeStamp => 'TimeStamp', + Delta => 'Delta', + Score => 'Score', +); + +sub Event { + return new ZoneMinder::Event( $_[0]{EventId} ); +} # end sub Event + +1; +__END__ + +=head1 NAME + +ZoneMinder::Frame - Perl Class for Frames + +=head1 SYNOPSIS + +use ZoneMinder::Frame; + +=head1 AUTHOR + +Isaac Connor, Eisaac@zoneminder.comE + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2001-2017 ZoneMinder LLC + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself, either Perl version 5.8.3 or, +at your option, any later version of Perl 5 you may have available. + + +=cut diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Logger.pm b/scripts/ZoneMinder/lib/ZoneMinder/Logger.pm index d826b9e91..a2edf7b42 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Logger.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Logger.pm @@ -91,7 +91,7 @@ use ZoneMinder::Config qw(:all); use DBI; use Carp; -use POSIX; +require POSIX; use IO::Handle; use Data::Dumper; use Time::HiRes qw/gettimeofday/; @@ -310,7 +310,7 @@ sub reinitialise { # Bit of a nasty hack to reopen connections to log files and the DB my $syslogLevel = $this->syslogLevel(); - $this->syslogLevel( NOLOG ); + $this->syslogLevel(NOLOG); $this->syslogLevel($syslogLevel) if $syslogLevel > NOLOG; my $logfileLevel = $this->fileLevel(); @@ -321,11 +321,10 @@ sub reinitialise { $this->databaseLevel(NOLOG); $this->databaseLevel($databaseLevel) if $databaseLevel > NOLOG; - my $screenLevel = $this->termLevel(); + $this->{hasTerm} = -t STDERR; + my $termLevel = $this->termLevel(); $this->termLevel(NOLOG); - $this->termLevel($screenLevel) if $screenLevel > NOLOG; - - $this->{sth} = undef; + $this->termLevel($termLevel) if $termLevel > NOLOG; } # Prevents undefined logging levels @@ -439,16 +438,13 @@ sub databaseLevel { my $databaseLevel = shift; if ( defined($databaseLevel) ) { $databaseLevel = $this->limit($databaseLevel); - if ( $this->{databaseLevel} != $databaseLevel ) { - if ( ( $databaseLevel > NOLOG ) and ( $this->{databaseLevel} <= NOLOG ) ) { - if ( ! ( $ZoneMinder::Database::dbh or ZoneMinder::Database::zmDbConnect() ) ) { - Warning("Failed connecting to db. Not using database logging."); - $this->{databaseLevel} = NOLOG; - return NOLOG; - } - } - $this->{databaseLevel} = $databaseLevel; + if ( $databaseLevel > NOLOG ) { + $this->{dbh} = ZoneMinder::Database::zmDbConnect(); + } else { + undef($this->{dbh}); } + $this->{sth} = undef; + $this->{databaseLevel} = $databaseLevel; } return $this->{databaseLevel}; } @@ -539,7 +535,7 @@ sub logPrint { if ( $level <= $this->{fileLevel} or $level <= $this->{termLevel} ) { my $message = sprintf( '%s.%06d %s[%d].%s [%s:%d] [%s]' - , strftime('%x %H:%M:%S', localtime($seconds)) + , POSIX::strftime('%x %H:%M:%S', localtime($seconds)) , $microseconds , $this->{id} , $$ diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Memory.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/Memory.pm.in index 5fbc0dc54..c04a66c42 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Memory.pm.in +++ b/scripts/ZoneMinder/lib/ZoneMinder/Memory.pm.in @@ -816,7 +816,7 @@ shared_data The general mapped memory section size The size, in bytes, of this section valid Flag indicating whether this section has been initialised active Flag indicating whether this monitor is active (enabled/disabled) -signal Flag indicating whether this monitor is reciving a valid signal +signal Flag indicating whether this monitor is receiving a valid signal state The current monitor state, see the STATE constants below last_write_index The last index, in the image buffer, that an image has been saved to last_read_index The last index, in the image buffer, that an image has been analysed from diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm b/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm index 2d8400a31..47ec7c557 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm @@ -36,17 +36,6 @@ require ZoneMinder::Server; #our @ISA = qw(Exporter ZoneMinder::Base); use parent qw(ZoneMinder::Object); -# ========================================================================== -# -# General Utility Functions -# -# ========================================================================== - -use ZoneMinder::Config qw(:all); -use ZoneMinder::Logger qw(:all); -use ZoneMinder::Database qw(:all); - -use POSIX; use vars qw/ $table $primary_key /; $table = 'Monitors'; $primary_key = 'Id'; diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Object.pm b/scripts/ZoneMinder/lib/ZoneMinder/Object.pm index f779255b2..a3f08a402 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Object.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Object.pm @@ -27,6 +27,8 @@ package ZoneMinder::Object; use 5.006; use strict; use warnings; +use Time::HiRes qw{ gettimeofday tv_interval }; +use Carp qw( cluck ); require ZoneMinder::Base; @@ -167,17 +169,6 @@ sub lock_and_load { } # end sub lock_and_load -sub AUTOLOAD { - my ( $self, $newvalue ) = @_; - my $type = ref($_[0]); - my $name = $AUTOLOAD; - $name =~ s/.*://; - if ( @_ > 1 ) { - return $_[0]{$name} = $_[1]; - } - return $_[0]{$name}; -} - sub save { my ( $self, $data, $force_insert ) = @_; @@ -187,7 +178,12 @@ sub save { $log->error("No type in Object::save. self:$self from $caller:$line"); } my $local_dbh = eval '$'.$type.'::dbh'; - $local_dbh = $ZoneMinder::Database::dbh if ! $local_dbh; + if ( ! $local_dbh ) { + $local_dbh = $ZoneMinder::Database::dbh; + if ( $debug or DEBUG_ALL ) { + $log->debug("Using global dbh"); + } + } $self->set( $data ? $data : {} ); if ( $debug or DEBUG_ALL ) { if ( $data ) { @@ -196,7 +192,6 @@ sub save { } } } -#$debug = 0; my $table = eval '$'.$type.'::table'; my $fields = eval '\%'.$type.'::fields'; @@ -297,6 +292,7 @@ $log->debug("No serial") if $debug; if ( $need_serial ) { if ( $serial ) { + $log->debug("Getting auto_increments"); my $s = qq{SELECT `auto_increment` FROM INFORMATION_SCHEMA.TABLES WHERE table_name = '$table'}; @$self{@identified_by} = @sql{@$fields{@identified_by}} = $local_dbh->selectrow_array( $s ); #@$self{@identified_by} = @sql{@$fields{@identified_by}} = $local_dbh->selectrow_array( q{SELECT nextval('} . $serial . q{')} ); @@ -345,7 +341,7 @@ $log->debug("No serial") if $debug; } # end if } # end if ZoneMinder::Database::end_transaction( $local_dbh, $ac ); - $self->load(); + #$self->load(); #if ( $$fields{id} ) { #if ( ! $ZoneMinder::Object::cache{$type}{$$self{id}} ) { #$ZoneMinder::Object::cache{$type}{$$self{id}} = $self; @@ -368,6 +364,7 @@ sub set { $log->warn("ZoneMinder::Object::set called on an object ($type) with no fields".$@); } # end if my %defaults = eval('%'.$type.'::defaults'); + if ( ref $params ne 'HASH' ) { my ( $caller, undef, $line ) = caller; $log->error("$type -> set called with non-hash params from $caller $line"); @@ -456,7 +453,420 @@ sub transform { sub to_string { my $type = ref($_[0]); my $fields = eval '\%'.$type.'::fields'; - return $type . ': '. join(' ' , map { $_[0]{$_} ? "$_ => $_[0]{$_}" : () } sort { $a cmp $b } keys %$fields ); + if ( $fields and %{$fields} ) { + return $type . ': '. join(' ', map { $_[0]{$_} ? "$_ => $_[0]{$_}" : () } sort { $a cmp $b } keys %$fields ); + } + return $type . ': '. join(' ', map { $_ .' => '.(defined $_[0]{$_} ? $_[0]{$_} : 'undef') } sort { $a cmp $b } keys %{$_[0]} ); +} + +# We make this a separate function so that we can use it to generate the sql statements for each value in an OR +sub find_operators { + my ( $field, $type, $operator, $value ) = @_; +$log->debug("find_operators: field($field) type($type) op($operator) value($value)") if DEBUG_ALL; + +my $add_placeholder = ( ! ( $field =~ /\?/ ) ) ? 1 : 0; + + if ( sets::isin( $operator, [ '=', '!=', '<', '>', '<=', '>=', '<<=' ] ) ) { + return ( $field.$type.' ' . $operator . ( $add_placeholder ? ' ?' : '' ), $value ); + } elsif ( $operator eq 'not' ) { + return ( '( NOT ' . $field.$type.')', $value ); + } elsif ( sets::isin( $operator, [ '&&', '<@', '@>' ] ) ) { + if ( ref $value eq 'ARRAY' ) { + if ( $field =~ /^\(/ ) { + return ( 'ARRAY('.$field.$type.') ' . $operator . ' ?', $value ); + } else { + return ( $field.$type.' ' . $operator . ' ?', $value ); + } # emd of + } else { + return ( $field.$type.' ' . $operator . ' ?', [ $value ] ); + } # end if + } elsif ( $operator eq 'exists' ) { + return ( $value ? '' : 'NOT ' ) . 'EXISTS ' . $field.$type; + } elsif ( sets::isin( $operator, [ 'in', 'not in' ] ) ) { + if ( ref $value eq 'ARRAY' ) { + return ( $field.$type.' ' . $operator . ' ('. join(',', map { '?' } @{$value} ) . ')', @{$value} ); + } else { + return ( $field.$type.' ' . $operator . ' (?)', $value ); + } # end if + } elsif ( $operator eq 'contains' ) { + return ( '? IN '.$field.$type, $value ); + } elsif ( $operator eq 'does not contain' ) { + return ( '? NOT IN '.$field.$type, $value ); + } elsif ( sets::isin( $operator, [ 'like','ilike' ] ) ) { + return $field.'::text ' . $operator . ' ?', $value; + } elsif ( $operator eq 'null_or_<=' ) { + return '('.$field.$type.' IS NULL OR '.$field.$type.' <= ?)', $value; + } elsif ( $operator eq 'is null or <=' ) { + return '('.$field.$type.' IS NULL OR '.$field.$type.' <= ?)', $value; + } elsif ( $operator eq 'null_or_>=' ) { + return '('.$field.$type.' IS NULL OR '.$field.$type.' >= ?)', $value; + } elsif ( $operator eq 'is null or >=' ) { + return '('.$field.$type.' IS NULL OR '.$field.$type.' >= ?)', $value; + } elsif ( $operator eq 'null_or_>' or $operator eq 'is null or >' ) { + return '('.$field.$type.' IS NULL OR '.$field.$type.' > ?)', $value; + } elsif ( $operator eq 'null_or_<' or $operator eq 'is null or <' ) { + return '('.$field.$type.' IS NULL OR '.$field.$type.' < ?)', $value; + } elsif ( $operator eq 'null_or_=' or $operator eq 'is null or =' ) { + return '('.$field.$type.' IS NULL OR '.$field.$type.' = ?)', $value; + } elsif ( $operator eq 'null or in' or $operator eq 'is null or in' ) { + return '('.$field.$type.' IS NULL OR '.$field.$type.' IN ('.join(',', map { '?' } @{$value} ) . '))', @{$value}; + } elsif ( $operator eq 'null or not in' ) { + return '('.$field.$type.' IS NULL OR '.$field.$type.' NOT IN ('.join(',', map { '?' } @{$value} ) . '))', @{$value}; + } elsif ( $operator eq 'exists' ) { + return ( $value ? ' EXISTS ' : 'NOT EXISTS ' ).$field; + } elsif ( $operator eq 'lc' ) { + return 'lower('.$field.$type.') = ?', $value; + } elsif ( $operator eq 'uc' ) { + return 'upper('.$field.$type.') = ?', $value; + } elsif ( $operator eq 'trunc' ) { + return 'trunc('.$field.$type.') = ?', $value; + } elsif ( $operator eq 'any' ) { + if ( ref $value eq 'ARRAY' ) { + return '(' . join(',', map { '?' } @{$value} ).") = ANY($field)", @{$value}; + } else { + return "? = ANY($field)", $value; + } # end if + } elsif ( $operator eq 'not any' ) { + if ( ref $value eq 'ARRAY' ) { + return '(' . join(',', map { '?' } @{$value} ).") != ANY($field)", @{$value}; + } else { + return "? != ANY($field)", $value; + } # end if + } elsif ( $operator eq 'is null' ) { + if ( $value ) { + return $field.$type. ' is null'; + } else { + return $field.$type. ' is not null'; + } # end if + } elsif ( $operator eq 'is not null' ) { + if ( $value ) { + return $field.$type. ' is not null'; + } else { + return $field.$type. ' is null'; + } # end if + } else { +$log->warn("find_operators: op not found field($field) type($type) op($operator) value($value)"); + } # end if + return; +} # end sub find_operators + +sub get_fields_values { + my ( $object_type, $search, $param_keys ) = @_; + + my @used_fields; + my @where; + my @values; + no strict 'refs'; + + foreach my $k ( @$param_keys ) { + if ( $k eq 'or' ) { + my $or_ref = ref $$search{or}; + + if ( $or_ref eq 'HASH' ) { + my @keys = keys %{$$search{or}}; + if ( @keys ) { + my ( $where, $values, $used_fields ) = get_fields_values( $object_type, $$search{or}, \@keys ); + + push @where, '('.join(' OR ', @{$where} ).')'; + push @values, @{$values}; + } else { + $log->error("No keys in or"); + } + + } elsif ( $or_ref eq 'ARRAY' ) { + my %s = @{$$search{or}}; + my ( $where, $values, $used_fields ) = get_fields_values( $object_type, \%s, [ keys %s ] ); + push @where, '('.join(' OR ', @{$where} ).')'; + push @values, @{$values}; + + } else { + $log->error("Deprecated use of or $or_ref for $$search{or}"); + } # end if + push @used_fields, $k; + next; + } elsif ( $k eq 'and' ) { + my $and_ref = ref $$search{and}; + if ( $and_ref eq 'HASH' ) { + my @keys = keys %{$$search{and}}; + if ( @keys ) { + my ( $where, $values, $used_fields ) = get_fields_values( $object_type, $$search{and}, \@keys ); + + push @where, '('.join(' AND ', @{$where} ).')'; + push @values, @{$values}; + } else { + $log->error("No keys in and"); + } + } elsif ( $and_ref eq 'ARRAY' and @{$$search{and}} ) { + my @sub_where; + + for( my $p_index = 0; $p_index < @{$$search{and}}; $p_index += 2 ) { + my %p = ( $$search{and}[$p_index], $$search{and}[$p_index+1] ); + + my ( $where, $values, $used_fields ) = get_fields_values( $object_type, \%p, [ keys %p ] ); + push @sub_where, @{$where}; + push @values, @{$values}; + } + push @where, '('.join(' AND ', @sub_where ).')'; + } else { + $log->error("incorrect ref of and $and_ref"); + } + push @used_fields, $k; + next; + } + my ( $field, $type, $function ) = $k =~ /^([_\+\w\-]+)(::\w+\[?\]?)?[\s_]*(.*)?$/; + $type = '' if ! defined $type; +$log->debug("$object_type param $field($type) func($function) " . ( ref $$search{$k} eq 'ARRAY' ? join(',',@{$$search{$k}}) : $$search{$k} ) ) if DEBUG_ALL; + + foreach ( 'find_fields', 'fields' ) { + my $fields = \%{$object_type.'::'.$_}; + if ( ! $fields ) { + $log->debug("No $fields in $object_type") if DEBUG_ALL; + next; + } # end if + + if ( ! $$fields{$field} ) { + #$log->debug("No $field in $_ for $object_type") if DEBUG_ALL; + next; + } # end if + +# This allows mainly for find_fields to reference multiple values, opinion in Project, value + foreach my $db_field ( ref $$fields{$field} eq 'ARRAY' ? @{$$fields{$field}} : $$fields{$field} ) { + if ( ! $function ) { + $db_field .= $type; + + if ( ref $$search{$k} eq 'ARRAY' ) { +$log->debug("Have array for $k $$search{$k}") if DEBUG_ALL; + + if ( ! ( $db_field =~ /\?/ ) ) { + if ( @{$$search{$k}} != 1 ) { + push @where, $db_field .' IN ('.join(',', map {'?'} @{$$search{$k}} ) . ')'; + } else { + push @where, $db_field.'=?'; + } # end if + } else { +$log->debug("Have question ? for $k $$search{$k} $db_field") if DEBUG_ALL; + + $db_field =~ s/=/IN/g; + my $question_replacement = '('.join(',', map {'?'} @{$$search{$k}} ) . ')'; + $db_field =~ s/\?/$question_replacement/; + push @where, $db_field; + } + push @values, @{$$search{$k}}; + } elsif ( ref $$search{$k} eq 'HASH' ) { + foreach my $p_k ( keys %{$$search{$k}} ) { + my $v = $$search{$k}{$p_k}; + if ( ref $v eq 'ARRAY' ) { + push @where, $db_field.' IN ('.join(',', map {'?'} @{$v} ) . ')'; + push @values, $p_k, @{$v}; + } else { + push @where, $db_field.'=?'; + push @values, $p_k, $v; + } # end if + } # end foreach p_k + } elsif ( ! defined $$search{$k} ) { + push @where, $db_field.' IS NULL'; + } else { + if ( ! ( $db_field =~ /\?/ ) ) { + push @where, $db_field .'=?'; + } else { + push @where, $db_field; + } + push @values, $$search{$k}; + } # end if + push @used_fields, $k; + } else { + #my @w = +#ref $search{$k} eq 'ARRAY' ? + #map { find_operators( $field, $type, $function, $_ ); } @{$search{$k}} : + my ( $w, @v ) = find_operators( $db_field, $type, $function, $$search{$k} ); + if ( $w ) { + #push @where, '(' . join(' OR ', @w ) . ')'; + push @where, $w; + push @values, @v if @v; + push @used_fields, $k; + } # end if @w + } # end if has function or not + } # end foreach db_field + } # end foreach find_field + } # end foreach k + return ( \@where, \@values, \@used_fields ); +} + +sub find { + no strict 'refs'; + my $object_type = shift; + my $debug = ${$object_type.'::debug'}; + $debug = DEBUG_ALL if ! $debug; + + my $starttime = [gettimeofday] if $debug; + my $params; + if ( @_ == 1 ) { + $params = $_[0]; + if ( ref $params ne 'HASH' ) { + $log->error("params $params was not a has"); + } # end if + } else { + $params = { @_ }; + } # end if + + my $local_dbh = ${$object_type.'::dbh'}; + if ( $$params{dbh} ) { + $local_dbh = $$params{dbh}; + delete $$params{dbh}; + } elsif ( ! $local_dbh ) { + $local_dbh = $dbh if ! $local_dbh; + } # end if + + my $sql = find_sql( $object_type, $params); + + my $do_cache = $$sql{columns} ne '*' ? 0 : 1; + +#$log->debug( 'find prepare: ' . sprintf('%.4f', tv_interval($starttime)*1000) ." useconds") if $debug; + my $data = $local_dbh->selectall_arrayref($$sql{sql}, { Slice => {} }, @{$$sql{values}}); + if ( ! $data ) { + $log->error('Error ' . $local_dbh->errstr() . " loading $object_type ($$sql{sql}) (". join(',', map { ref $_ eq 'ARRAY' ? 'ARRAY('.join(',',@$_).')' : $_ } @{$$sql{values}} ) . ')' ); + return (); + #} elsif ( ( ! @$data ) and $debug ) { + #$log->debug("No $type ($sql) (@values) " ); + } elsif ( $debug ) { + $log->debug("Loading Debug:$debug $object_type ($$sql{sql}) (".join(',', map { ref $_ eq 'ARRAY' ? join(',', @{$_}) : $_ } @{$$sql{values}}).') # of results:' . @$data . ' in ' . sprintf('%.4f', tv_interval($starttime)*1000) .' useconds' ); + } # end if + + my $fields = \%{$object_type.'::fields'}; + my $primary_key = ${$object_type.'::primary_key'}; + if ( ! $primary_key ) { + Error( 'NO primary_key for type ' . $object_type ); + return; + } # end if + if ( ! ($fields and keys %{$fields}) ) { + return map { new($object_type, $$_{$primary_key}, $_ ) } @$data; + } elsif ( $$fields{$primary_key} ) { + return map { new($object_type, $_->{$$fields{$primary_key}}, $_) } @$data; + } else { + my @identified_by = eval '@'.$object_type.'::identified_by'; + if ( ! @identified_by ) { + $log->debug("Multi key object $object_type but no identified by $fields") if $debug; + } # end if + return map { new($object_type, \@identified_by, $_, !$do_cache) } @$data; + } # end if +} # end sub find + +sub find_one { + my $object_type = shift; + my $params; + if ( @_ == 1 ) { + $params = $_[0]; + } else { + %{$params} = @_; + } # end if + $$params{limit}=1; + my @Results = $object_type->find(%$params); + my ( $caller, undef, $line ) = caller; +$log->debug("returning to $caller:$line from find_one") if DEBUG_ALL; + return $Results[0] if @Results; +} # end sub find_one + +sub find_sql { + no strict 'refs'; + my $object_type = shift; + + my $debug = ${$object_type.'::debug'}; + $debug = DEBUG_ALL if ! $debug; + + my $params; + if ( @_ == 1 ) { + $params = $_[0]; + if ( ref $params ne 'HASH' ) { + $log->error("params $params was not a has"); + } # end if + } else { + $params = { @_ }; + } # end if + + my %sql = ( + ( distinct => ( exists $$params{distinct} ? 1:0 ) ), + ( columns => ( exists $$params{columns} ? $$params{columns} : '*' ) ), + ( table => ( exists $$params{table} ? $$params{table} : ${$object_type.'::table'} )), + 'group by'=> $$params{'group by'}, + limit => $$params{limit}, + offset => $$params{offset}, + ); + if ( exists $$params{order} ) { + $sql{order} = $$params{order}; + } else { + my $order = eval '$'.$object_type.'::default_sort'; +#$log->debug("default sort: $object_type :: default_sort = $order") if DEBUG_ALL; + $sql{order} = $order if $order; + } # end if + delete @$params{'distinct','columns','table','group by','limit','offset','order'}; + + my @where; + my @values; + if ( exists $$params{custom} ) { + push @where, '(' . (shift @{$$params{custom}}) . ')'; + push @values, @{$$params{custom}}; + delete $$params{custom}; + } # end if + + my @param_keys = keys %$params; + + # no operators, just which fields are being searched on. Mostly just useful for detetion of the deleted field. + my %used_fields; + + # We use this search hash so that we can mash it up and leave the params hash alone + my %search; + @search{@param_keys} = @$params{@param_keys}; + + my ( $where, $values, $used_fields ) = get_fields_values( $object_type, \%search, \@param_keys ); + delete @search{@{$used_fields}}; + @used_fields{ @{$used_fields} } = @{$used_fields}; + push @where, @{$where}; + push @values, @{$values}; + + my $fields = \%{$object_type.'::fields'}; + +#optimise this + if ( $$fields{deleted} and ! $used_fields{deleted} ) { + push @where, 'deleted=?'; + push @values, 0; + } # end if + $sql{where} = \@where; + $sql{values} = \@values; + $sql{used_fields} = \%used_fields; + + foreach my $k ( keys %search ) { + $log->error("Extra parameters in $object_type ::find $k => $search{$k}"); + Carp::cluck("Extra parameters in $object_type ::find $k => $search{$k}"); + } # end foreach + + $sql{sql} = join( ' ', + ( 'SELECT', ( $sql{distinct} ? ('DISTINCT') : () ) ), + ( $sql{columns}, 'FROM', $sql{table} ), + ( @{$sql{where}} ? ('WHERE', join(' AND ', @{$sql{where}})) : () ), + ( $sql{order} ? ( 'ORDER BY', $sql{order} ) : () ), + ( $sql{'group by'} ? ( 'GROUP BY', $sql{'group by'} ) : () ), + ( $sql{limit} ? ( 'LIMIT', $sql{limit}) : () ), + ( $sql{offset} ? ( 'OFFSET', $sql{offset} ) : () ), + ); + #$log->debug("Loading Debug:$debug $object_type ($sql) (".join(',', map { ref $_ eq 'ARRAY' ? join(',', @{$_}) : $_ } @values).')' ) if $debug; + return \%sql; +} # end sub find_sql + +sub AUTOLOAD { + my $type = ref($_[0]); + Carp::cluck("No type in autoload") if ! $type; + if ( DEBUG_ALL ) { + Carp::cluck("Using AUTOLOAD $AUTOLOAD"); + } + my $name = $AUTOLOAD; + $name =~ s/.*://; + if ( @_ > 1 ) { + return $_[0]{$name} = $_[1]; + } + return $_[0]{$name}; +} + +sub DESTROY { } 1; diff --git a/scripts/zmaudit.pl.in b/scripts/zmaudit.pl.in index 7a5017fbd..9b0163999 100644 --- a/scripts/zmaudit.pl.in +++ b/scripts/zmaudit.pl.in @@ -176,8 +176,10 @@ MAIN: while( $loop ) { } # end while can't connect to the db my @Storage_Areas; + my @all_Storage_Areas = ZoneMinder::Storage->find(); + if ( defined $storage_id ) { - @Storage_Areas = ZoneMinder::Storage->find( Id=>$storage_id ); + @Storage_Areas = map { $$_{Id} == $storage_id ? $_ : () } @all_Storage_Areas; if ( !@Storage_Areas ) { Error("No Storage Area found with Id $storage_id"); Term(); @@ -403,27 +405,28 @@ MAIN: while( $loop ) { $$Event{RelativePath} = $event_dir; $Event->MonitorId( $monitor_dir ); $Event->StorageId( $Storage->Id() ); - $Event->StartTime( POSIX::strftime('%Y-%m-%d %H:%M:%S', gmtime(time_of_youngest_file($$Event{Path})) ) ); + $Event->StartTime( POSIX::strftime('%Y-%m-%d %H:%M:%S', gmtime(time_of_youngest_file($Event->Path())) ) ); } # end foreach event } if ( ! $$Storage{Scheme} ) { Error("Storage Scheme not set on $$Storage{Name}"); - if ( ! chdir( $monitor_dir ) ) { - Error( "Can't chdir directory '$$Storage{Path}/$monitor_dir': $!" ); + if ( ! chdir($monitor_dir) ) { + Error("Can't chdir directory '$$Storage{Path}/$monitor_dir': $!"); next; } - if ( ! opendir( DIR, "." ) ) { - Error( "Can't open directory '$$Storage{Path}/$monitor_dir': $!" ); + if ( ! opendir(DIR, '.') ) { + Error("Can't open directory '$$Storage{Path}/$monitor_dir': $!"); next; } - my @temp_events = sort { $b <=> $a } grep { -d $_ && $_ =~ /^\d+$/ } readdir( DIR ); - closedir( DIR ); + my @temp_events = sort { $b <=> $a } grep { -d $_ && $_ =~ /^\d+$/ } readdir(DIR); + closedir(DIR); my $count = 0; foreach my $event ( @temp_events ) { my $Event = $fs_events->{$event} = new ZoneMinder::Event(); $$Event{Id} = $event; #$$Event{Path} = $event_path; + $$Event{Scheme} = 'Shallow'; $Event->MonitorId( $monitor_dir ); $Event->StorageId( $Storage->Id() ); } # end foreach event @@ -454,6 +457,12 @@ MAIN: while( $loop ) { my $Event = $fs_events->{$fs_event_id}; if ( ! defined( $db_events->{$fs_event_id} ) ) { + # Long running zmaudits can find events that were created after we loaded all db events. + # So do a secondary lookup + if ( ZoneMinder::Event->find_one(Id=>$fs_event_id) ) { + Debug("$$Event{Id} found in secondary lookup."); + next; + } my $age = $Event->age(); if ( $age > $Config{ZM_AUDIT_MIN_AGE} ) { @@ -522,7 +531,7 @@ MAIN: while( $loop ) { # If we found the monitor in the file system my $fs_events = $fs_monitors->{$db_monitor}; - while ( my ( $db_event, $age ) = each( %$db_events ) ) { +EVENT: while ( my ( $db_event, $age ) = each( %$db_events ) ) { if ( ! ($fs_events and defined( $fs_events->{$db_event} ) ) ) { Debug("Don't have an fs event for $db_event"); my $Event = ZoneMinder::Event->find_one( Id=>$db_event ); @@ -531,6 +540,21 @@ MAIN: while( $loop ) { next; } Debug("Event $db_event is not in fs. Should have been at ".$Event->Path()); + # Check for existence in other Storage Areas + foreach my $Storage ( @all_Storage_Areas ) { + next if $$Storage{Id} == $$Event{StorageId}; + + my $path = $Storage->Path().'/'.$Event->RelativePath(); + if ( -e $path ) { + Info("Event $$Event{Id} found at $path instead of $$Event{Path}"); + if ( confirm('update', 'updating') ) { + $Event->save({StorageId=>$$Storage{Id}}); + next EVENT; + } + } else { + Debug("$$Event{Id} Not found at $path"); + } + } if ( $Event->Archived() ) { Warning("Event $$Event{Id} is Archived. Taking no further action on it."); next; @@ -557,7 +581,7 @@ MAIN: while( $loop ) { Debug("Database event $$Event{Id} apparently exists at " . $Event->Path() ); } else { if ( $age > $Config{ZM_AUDIT_MIN_AGE} ) { - aud_print( "Database event '$db_monitor/$db_event' does not exist at " . $Event->Path().' in filesystem, deleting' ); + aud_print("Database event '$db_monitor/$db_event' does not exist at " . $Event->Path().' in filesystem, deleting'); if ( confirm() ) { $Event->delete(); $cleaned = 1; @@ -567,12 +591,13 @@ MAIN: while( $loop ) { } } # end if exists in filesystem } else { - Debug("Found fs event for $db_event, $age at " . $$fs_events{$db_event}->Path()); - my $Event = new ZoneMinder::Event( $db_event ); - if ( ! $Event->check_for_in_filesystem() ) { + Debug("Found fs event for id $db_event, $age seconds old at " . $$fs_events{$db_event}->Path()); + my $Event = ZoneMinder::Event->find_one( Id=>$db_event ); + if ( $Event and ! $Event->check_for_in_filesystem() ) { Warning("Not found at " . $Event->Path() . ' was found at ' . $$fs_events{$db_event}->Path() ); Warning($Event->to_string()); Warning($$fs_events{$db_event}->to_string()); + $$Event{Scheme} = '' if ! defined $$Event{Scheme}; if ( $$fs_events{$db_event}->Scheme() ne $Event->Scheme() ) { Info("Updating scheme on event $$Event{Id} from $$Event{Scheme} to $$fs_events{$db_event}{Scheme}"); $Event->Scheme($$fs_events{$db_event}->Scheme()); @@ -956,10 +981,11 @@ sub deleteSwapImage { # Deletes empty sub directories of the given path. # Does not delete the path if empty. Is not meant to be recursive. +# Assumes absolute path sub delete_empty_subdirs { my $DIR; if ( !opendir($DIR, $_[0]) ) { - Error("delete_empty_directories: Can't open directory '".getcwd()."/$_[0]': $!" ); + Error("delete_empty_subdirs: Can't open directory '/$_[0]': $!" ); return; } my @contents = map { ( $_ eq '.' or $_ eq '..' ) ? () : $_ } readdir( $DIR ); @@ -972,17 +998,18 @@ sub delete_empty_subdirs { closedir($DIR); } +# Assumes absolute path sub delete_empty_directories { my $DIR; if ( !opendir($DIR, $_[0]) ) { - Error("delete_empty_directories: Can't open directory '".getcwd()."/$_[0]': $!" ); + Error("delete_empty_directories: Can't open directory '/$_[0]': $!" ); return; } my @contents = map { ( $_ eq '.' or $_ eq '..' ) ? () : $_ } readdir( $DIR ); #Debug("delete_empty_directories $_[0] has " . @contents .' entries:' . ( @contents <= 2 ? join(',',@contents) : '' )); my @dirs = map { -d $_[0].'/'.$_ ? $_ : () } @contents; if ( @dirs ) { - Debug("Have " . @dirs . " dirs"); + Debug("Have " . @dirs . " dirs in $_[0]"); foreach ( @dirs ) { delete_empty_directories( $_[0].'/'.$_ ); } diff --git a/scripts/zmdc.pl.in b/scripts/zmdc.pl.in index 4aee70314..b5903f3da 100644 --- a/scripts/zmdc.pl.in +++ b/scripts/zmdc.pl.in @@ -258,7 +258,7 @@ sub run { my $fd = 0; - # THis also closes dbh and CLIENT and SERVER + # This also closes dbh and CLIENT and SERVER while( $fd < POSIX::sysconf(&POSIX::_SC_OPEN_MAX) ) { POSIX::close($fd++); } diff --git a/scripts/zmrecover.pl.in b/scripts/zmrecover.pl.in new file mode 100644 index 000000000..02083d757 --- /dev/null +++ b/scripts/zmrecover.pl.in @@ -0,0 +1,491 @@ +#!/usr/bin/perl -wT + +use strict; +use bytes; + +# ========================================================================== +# +# These are the elements you can edit to suit your installation +# +# ========================================================================== + +use constant RECOVER_TAG => '(r)'; # Tag to append to event name when recovered +use constant RECOVER_TEXT => 'Recovered.'; # Text to append to event notes when recovered + +# ========================================================================== +# +# You shouldn't need to change anything from here downwards +# +# ========================================================================== + +@EXTRA_PERL_LIB@ +use ZoneMinder; +use DBI; +use POSIX; +use File::Find; +use Time::HiRes qw/gettimeofday/; +use Getopt::Long; +use autouse 'Pod::Usage'=>qw(pod2usage); + +use constant ZM_RECOVER_PID => '@ZM_RUNDIR@/zmrecover.pid'; + + +$ENV{PATH} = '/bin:/usr/bin:/usr/local/bin'; +$ENV{SHELL} = '/bin/sh' if exists $ENV{SHELL}; +delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; + +my $report = 0; +my $interactive = 1; +my $monitor_id = 0; +my $version; +my $force = 0; +my $server_id = undef; +my $storage_id = undef; + +logInit(); + +GetOptions( + force =>\$force, + 'interactive=i' =>\$interactive, + 'monitor_id=i' =>\$monitor_id, + report =>\$report, + 'server_id=i' =>\$server_id, + 'storage_id=i' =>\$storage_id, + version =>\$version + ) or pod2usage(-exitstatus => -1); + +if ( $version ) { + print( ZoneMinder::Base::ZM_VERSION . "\n"); + exit(0); +} +if ( ($report + $interactive) > 1 ) { + print( STDERR "Error, only one option may be specified\n" ); + pod2usage(-exitstatus => -1); +} + +if ( -e ZM_RECOVER_PID ) { + local $/ = undef; + open FILE, ZM_RECOVER_PID or die "Couldn't open file: $!"; + binmode FILE; + my $pid = ; + close FILE; + if ( $force ) { + Error("zmrecover.pl appears to already be running at pid $pid. Continuing." ); + } else { + Fatal("zmrecover.pl appears to already be running at pid $pid. If not, please delete " . + ZM_RECOVER_PID . " or use the --force command line option." ); + } +} # end if ZM_RECOVER_PID exists + +if ( open(my $PID, '>', ZM_RECOVER_PID) ) { + print($PID $$); + close($PID); +} else { + Error( "Can't open pid file at " . ZM_PID ); +} + +sub HupHandler { + Info('Received HUP, reloading'); + &ZoneMinder::Logger::logHupHandler(); +} +sub TermHandler { + Info('Received TERM, exiting'); + Term(); +} +sub Term { + unlink ZM_RECOVER_PID; + exit(0); +} +$SIG{HUP} = \&HupHandler; +$SIG{TERM} = \&TermHandler; +$SIG{INT} = \&TermHandler; + +my $dbh = zmDbConnect(); +if ( ! $dbh ) { + Error('Unable to connect to database'); + Term(); +} # end if + +$| = 1; + +require ZoneMinder::Monitor; +require ZoneMinder::Storage; +require ZoneMinder::Event; + + +my @Storage_Areas; +if ( defined $storage_id ) { + @Storage_Areas = ZoneMinder::Storage->find( Id=>$storage_id ); + if ( !@Storage_Areas ) { + Error("No Storage Area found with Id $storage_id"); + Term(); + } + Info("Recovering from Storage Area $Storage_Areas[0]{Id} $Storage_Areas[0]{Name} at $Storage_Areas[0]{Path}"); +} elsif ( $server_id ) { + @Storage_Areas = ZoneMinder::Storage->find( ServerId => $server_id ); + if ( ! @Storage_Areas ) { + Error("No Storage Area found with ServerId =" . $server_id); + Term(); + } + foreach my $Storage ( @Storage_Areas ) { + Info('Recovering from ' . $Storage->Name() . ' at ' . $Storage->Path() . ' on ' . $Storage->Server()->Name() ); + } +} else { + @Storage_Areas = ZoneMinder::Storage->find(); + Info("Recovering from All Storage Areas"); +} + +my @Monitors = ZoneMinder::Monitor->find(); +Debug("@Monitors"); + foreach my $Monitor ( @Monitors ) { + Debug("Monitor " . $Monitor->to_string() ) + } + my %Monitors = map { $$_{Id} => $_ } @Monitors; + #ZoneMinder::Monitor->find( + + # ($monitor_id ? ( Id=>$monitor_id ) : () ), + + #); + Debug("Found " . (keys %Monitors) . " monitors"); + foreach my $id ( keys %Monitors ) { + Debug("Monitor $id $Monitors{$id}{Name}"); + } + + foreach my $Storage ( @Storage_Areas ) { + Debug('Checking events in ' . $Storage->Path() ); + if ( ! chdir($Storage->Path()) ) { + Error('Unable to change dir to ' . $Storage->Path()); + next; + } # end if + + # Please note that this glob will take all files beginning with a digit. + foreach my $monitor ( glob('[0-9]*') ) { + if ( $monitor =~ /\D/ ) { + Debug("Weird non digit characters in $monitor"); + next; + } + # De-taint + ( my $monitor_dir ) = ( $monitor =~ /^(\d+)$/ ); + if ( $monitor_id and ( $monitor_id != $monitor_dir ) ) { + Debug("Skipping monitor $monitor_dir because we are only interested in monitor $monitor_id"); + next; + } + if ( ! $Monitors{$monitor_dir} ) { + Warning("There is no monitor in the database for $$Storage{Path}/$monitor_dir. Skipping it."); + next; + } + my $Monitor = $Monitors{$monitor_dir}; + + Debug("Found filesystem monitor '$monitor_dir'"); + + { + my @day_dirs = glob("$monitor_dir/[0-9][0-9]/[0-9][0-9]/[0-9][0-9]"); + Debug(qq`Checking for Deep Events under $$Storage{Path} using glob("$monitor_dir/[0-9][0-9]/[0-9][0-9]/[0-9][0-9]") returned `. scalar @day_dirs . ' days with events'); + foreach my $day_dir ( @day_dirs ) { + Debug("Checking day dir $day_dir"); + ( $day_dir ) = ( $day_dir =~ /^(.*)$/ ); # De-taint + if ( !chdir($day_dir) ) { + Error("Can't chdir to '$$Storage{Path}/$day_dir': $!"); + next; + } + if ( ! opendir(DIR, '.') ) { + Error("Can't open directory '$$Storage{Path}/$day_dir': $!"); + next; + } + my %event_ids_by_path; + + my @event_links = sort { $b <=> $a } grep { -l $_ } readdir( DIR ); + Debug('Have ' . (scalar @event_links) . ' event links'); + closedir(DIR); + + my $count = 0; + foreach my $event_link ( @event_links ) { + # Event links start with a period and consist of the digits of the event id. + # Anything else is not an event link + my ($event_id) = $event_link =~ /^\.(\d+)$/; + if ( !$event_id ) { + Warning("Non-event link found $event_link in $day_dir, skipping"); + next; + } + Debug("Checking link $event_link"); + #Event path is hour/minute/sec + my $event_path = readlink($event_link); + + if ( !($event_path and -e $event_path) ) { + Warning("Event link $day_dir/$event_link does not point to valid target at $event_path"); + next; + } + if ( ! ZoneMinder::Event->find_one(Id=>$event_id) ) { + Info("Event not found in db for event data found at $$Storage{Path}/$day_dir/$event_path with Id=$event_id"); + if ( confirm() ) { + my $Event = new ZoneMinder::Event(); + $$Event{Id} = $event_id; + $$Event{Path} = join('/', $Storage->Path(), $day_dir, $event_path); + $$Event{RelativePath} = join('/', $day_dir, $event_path); + $$Event{Scheme} = 'Deep'; + $$Event{Name} = "Event $event_id recovered"; + $Event->MonitorId( $monitor_dir ); + $Event->StorageId( $Storage->Id() ); + $Event->DiskSpace( undef ); + $Event->Width( $Monitor->Width() ); + $Event->Height( $Monitor->Height() ); + $Event->Orientation( $Monitor->Orientation() ); + $Event->recover_timestamps(); + if ( $$Event{StartTime} ) { + $Event->save({}, 1); + Info("Event resurrected as " . $Event->to_string() ); + } else { + Warning("Unable to determine starttime. Not resurrecting this event."); + } + next; + } # end if resurrection + } # event path exists + } # end foreach event_link + + # Now check for events that have lost their link. We can determine event Id from .mp4 + + my @time_dirs = glob('[0-9][0-9]/[0-9][0-9]/[0-9][0-9]'); + foreach my $event_dir ( @time_dirs ) { + Debug("Checking time dir $event_dir"); + ( $event_dir ) = ( $event_dir =~ /^(.*)$/ ); # De-taint + + my $event_id = undef; + + if ( ! opendir(DIR, $event_dir) ) { + Error("Can't open directory '$$Storage{Path}/$day_dir': $!"); + next; + } + my @contents = readdir( DIR ); + Debug('Have ' . @contents . " files in $day_dir/$event_dir"); + closedir(DIR); + + my @mp4_files = grep( /^\d+\-video.mp4$/, @contents); + foreach my $mp4_file ( @mp4_files ) { + my ( $id ) = $mp4_file =~ /^([0-9]+)\-video\.mp4$/; + if ( $id ) { + $event_id = $id; + Debug("Got event id from mp4 file $mp4_file => $event_id"); + last; + } + } # end foreach mp4 + + if ( ! $event_id ) { + # Look for .id file + my @hidden_files = grep( /^\.\d+$/, @contents); + Debug('Have ' . @hidden_files . ' hidden files'); + if ( @hidden_files ) { + ( $event_id ) = $hidden_files[0] =~ /^.(\d+)$/; + } + } + + if ( $event_id and ! ZoneMinder::Event->find_one(Id=>$event_id) ) { + Info("Event not found in db for event data found at $$Storage{Path}/$monitor_dir/$day_dir/$event_dir"); + if ( confirm() ) { + my $Event = new ZoneMinder::Event(); + $$Event{Id} = $event_id; + $$Event{Path} = join('/', $Storage->Path(), $day_dir, $event_dir); + $$Event{RelativePath} = join('/', $day_dir, $event_dir); + $$Event{Scheme} = 'Deep'; + $$Event{Name} = "Event $event_id recovered"; + $Event->MonitorId( $monitor_dir ); + $Event->Width( $Monitor->Width() ); + $Event->Height( $Monitor->Height() ); + $Event->Orientation( $Monitor->Orientation() ); + $Event->StorageId( $Storage->Id() ); + $Event->DiskSpace( undef ); + $Event->recover_timestamps(); + if ( $$Event{StartTime} ) { + $Event->save({}, 1); + Info("Event resurrected as " . $Event->to_string() ); + } else { + Warning("Unable to determine starttime. Not resurrecting this event."); + } + next; + } + } # end if event found + + # Search in db for given timestamp? + + my ( undef, $year, $month, $day ) = split('/', $day_dir); + $year += 2000; + my ( $hour, $minute, $second ) = split('/', $event_dir); + my $StartTime =sprintf('%.4d-%.2d-%.2d %.2d:%.2d:%.2d', $year, $month, $day, $hour, $minute, $second); + my $Event = ZoneMinder::Event->find_one( + MonitorId=>$monitor_dir, + StartTime=>$StartTime, + ); + if ( $Event ) { + Debug("Found event matching starttime on monitor $monitor_dir at $StartTime: " . $Event->to_string()); + next; + } + + } # end foreach event_dir without link + chdir( $Storage->Path() ); + } # end foreach day dir + } + + Debug("Checking for Medium Scheme Events under $$Storage{Path}/$monitor_dir"); + { + my @event_dirs = glob("$monitor_dir/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/*"); + Debug(qq`glob("$monitor_dir/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/*") returned ` . scalar @event_dirs . " entries." ); + foreach my $event_dir ( @event_dirs ) { + if ( ! -d $event_dir ) { + Debug("$event_dir is not a dir. Skipping"); + next; + } + my ( $date, $event_id ) = $event_dir =~ /^$monitor_dir\/(\d{4}\-\d{2}\-\d{2})\/(\d+)$/; + if ( !$event_id ) { + Debug("Unable to parse date/event_id from $event_dir"); + next; + } + + my $Event = ZoneMinder::Event->find_one(Id=>$event_id); + if ( $Event ) { + Debug('Found event in the db, moving on.'); + next; + } + $Event = new ZoneMinder::Event(); + $$Event{Id} = $event_id; + $$Event{Path} = join('/', $Storage->Path(), $event_dir ); + Info("Have event $$Event{Id} at $$Event{Path}"); + if ( confirm() ) { + $$Event{Scheme} = 'Medium'; + $$Event{RelativePath} = $event_dir; + $$Event{Name} = "Event $event_id recovered"; + $Event->MonitorId( $monitor_dir ); + $Event->Width( $Monitor->Width() ); + $Event->Height( $Monitor->Height() ); + $Event->Orientation( $Monitor->Orientation() ); + $Event->StorageId( $Storage->Id() ); + $Event->recover_timestamps(); + if ( $$Event{StartTime} ) { + $Event->save({}, 1); + Info("Event resurrected as " . $Event->to_string() ); + } else { + Warning("Unable to determine starttime. Not resurrecting this event."); + } + } + } # end foreach event + } # end search for Medium + + # Shallow + Debug("Checking for ShallowScheme Events under $$Storage{Path}/$monitor_dir"); + if ( ! chdir($monitor_dir) ) { + Error("Can't chdir directory '$$Storage{Path}/$monitor_dir': $!"); + next; + } + if ( ! opendir(DIR, '.') ) { + Error("Can't open directory '$$Storage{Path}/$monitor_dir': $!"); + next; + } + my @temp_events = sort { $b <=> $a } grep { -d $_ && $_ =~ /^\d+$/ } readdir( DIR ); + closedir(DIR); + foreach my $event ( @temp_events ) { + my $Event = ZoneMinder::Event->find_one(Id=>$event); + if ( $Event ) { + Debug("Found an event in db for $event"); + next; + } + $Event = new ZoneMinder::Event(); + $$Event{Id} = $event; + $$Event{Path} = join('/', $Storage->Path(), $event ); + Info("Have event $$Event{Id} at $$Event{Path}"); + if ( confirm() ) { + $$Event{Scheme} = 'Shallow'; + $$Event{Name} = "Event $event recovered"; + #$$Event{Path} = $event_path; + $Event->MonitorId( $monitor_dir ); + $Event->Width( $Monitor->Width() ); + $Event->Height( $Monitor->Height() ); + $Event->Orientation( $Monitor->Orientation() ); + $Event->StorageId( $Storage->Id() ); + $Event->recover_timestamps(); + if ( $$Event{StartTime} ) { + $Event->save({}, 1); + Info("Event resurrected as " . $Event->to_string() ); + } else { + Warning("Unable to determine starttime. Not resurrecting this event."); + } + } + } # end foreach event + chdir( $Storage->Path() ); + } # end foreach monitor + + } # end foreach Storage Area + +Term(); + +sub confirm { + my $prompt = shift || 'resurrect'; + my $action = shift || 'resurrecting'; + + my $yesno = 0; + if ( $report ) { + print( "\n" ); + } elsif ( $interactive ) { + print(", $prompt Y/n/q: "); + my $char = <>; + chomp( $char ); + if ( $char eq 'q' ) { + Term(); + } + if ( !$char ) { + $char = 'y'; + } + $yesno = ( $char =~ /[yY]/ ); + } else { + Info($action); + $yesno = 1; + } + return $yesno; +} + +1; +__END__ + +=head1 NAME + +zmrecover.pl - ZoneMinder event file system and database recovery checker + +=head1 SYNOPSIS + + zmrecover.pl [-r,-report|-i,-interactive] + +=head1 DESCRIPTION + +This script checks for consistency between the event filesystem and +the database. If events are found in one and not the other they are +deleted (optionally). Additionally any monitor event directories that +do not correspond to a database monitor are similarly disposed of. +However monitors in the database that don't have a directory are left +alone as this is valid if they are newly created and have no events +yet. + +=head1 OPTIONS + + -i, --interactive - Ask before applying any changes + -m, --monitor_id - Only consider the given monitor + -r, --report - Just report don't actually do anything + -s, --storage_id - Specify a storage area to recover instead of all + -v, --version - Print the installed version of ZoneMinder + +=head1 COPYRIGHT + +ZoneMinder Recover Script +Copyright (C) 2018 ZoneMinder LLC + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +=cut diff --git a/scripts/zmupdate.pl.in b/scripts/zmupdate.pl.in index b66397740..8517e0277 100644 --- a/scripts/zmupdate.pl.in +++ b/scripts/zmupdate.pl.in @@ -353,8 +353,8 @@ if ( $version ) { $version = $detaint_version; if ( ZM_VERSION eq $version ) { - print( "\nDatabase already at version $version, update aborted.\n\n" ); - exit( 0 ); + print("\nDatabase already at version $version, update skipped.\n\n"); + exit(0); } print( "\nInitiating database upgrade to version ".ZM_VERSION." from version $version\n" ); @@ -923,10 +923,10 @@ if ( $version ) { die( "Can't find upgrade from version '$version'" ); } # Re-enable the privacy popup after each upgrade - #my $sql = "update Config set Value = 1 where Name = 'ZM_SHOW_PRIVACY'"; - #my $sth = $dbh->prepare_cached( $sql ) or die( "Can't prepare '$sql': ".$dbh->errstr() ); - #my $res = $sth->execute( ) or die( "Can't execute: ".$sth->errstr() ); - #$sth->finish(); + my $sql = "update Config set Value = 1 where Name = 'ZM_SHOW_PRIVACY'"; + my $sth = $dbh->prepare_cached( $sql ) or die( "Can't prepare '$sql': ".$dbh->errstr() ); + my $res = $sth->execute( ) or die( "Can't execute: ".$sth->errstr() ); + $sth->finish(); print( "\nDatabase upgrade to version ".ZM_VERSION." successful.\n\n" ); } zmDbDisconnect(); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2c0ffd4ab..d9bad9536 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -6,7 +6,58 @@ configure_file(zm_config.h.in "${CMAKE_CURRENT_BINARY_DIR}/zm_config.h" @ONLY) # Group together all the source files that are used by all the binaries (zmc, zmu, zms etc) set(ZM_BIN_SRC_FILES zm_analysis_thread.cpp - zm_box.cpp zm_buffer.cpp zm_camera.cpp zm_comms.cpp zm_config.cpp zm_coord.cpp zm_curl_camera.cpp zm.cpp zm_db.cpp zm_logger.cpp zm_event.cpp zm_eventstream.cpp zm_exception.cpp zm_file_camera.cpp zm_ffmpeg_camera.cpp zm_group.cpp zm_image.cpp zm_jpeg.cpp zm_libvlc_camera.cpp zm_local_camera.cpp zm_monitor.cpp zm_monitorstream.cpp zm_ffmpeg.cpp zm_ffmpeg_input.cpp zm_mpeg.cpp zm_packet.cpp zm_packetqueue.cpp zm_poly.cpp zm_regexp.cpp zm_remote_camera.cpp zm_remote_camera_http.cpp zm_remote_camera_nvsocket.cpp zm_remote_camera_rtsp.cpp zm_rtp.cpp zm_rtp_ctrl.cpp zm_rtp_data.cpp zm_rtp_source.cpp zm_rtsp.cpp zm_rtsp_auth.cpp zm_sdp.cpp zm_signal.cpp zm_stream.cpp zm_swscale.cpp zm_thread.cpp zm_time.cpp zm_timer.cpp zm_user.cpp zm_utils.cpp zm_video.cpp zm_videostore.cpp zm_zone.cpp zm_storage.cpp) + zm_box.cpp + zm_buffer.cpp + zm_camera.cpp + zm_comms.cpp + zm_config.cpp + zm_coord.cpp + zm_curl_camera.cpp + zm.cpp + zm_db.cpp + zm_logger.cpp + zm_event.cpp + zm_eventstream.cpp + zm_exception.cpp + zm_file_camera.cpp zm_ffmpeg_camera.cpp + zm_frame.cpp + zm_group.cpp + zm_image.cpp + zm_jpeg.cpp + zm_libvlc_camera.cpp + zm_local_camera.cpp + zm_monitor.cpp + zm_monitorstream.cpp + zm_ffmpeg.cpp + zm_ffmpeg_input.cpp + zm_mpeg.cpp + zm_packet.cpp + zm_packetqueue.cpp + zm_poly.cpp + zm_regexp.cpp + zm_remote_camera.cpp + zm_remote_camera_http.cpp + zm_remote_camera_nvsocket.cpp + zm_remote_camera_rtsp.cpp + zm_rtp.cpp + zm_rtp_ctrl.cpp + zm_rtp_data.cpp + zm_rtp_source.cpp + zm_rtsp.cpp + zm_rtsp_auth.cpp + zm_sdp.cpp + zm_signal.cpp + zm_stream.cpp + zm_swscale.cpp + zm_thread.cpp + zm_time.cpp + zm_timer.cpp + zm_user.cpp + zm_utils.cpp + zm_video.cpp + zm_videostore.cpp + zm_zone.cpp + zm_storage.cpp) # A fix for cmake recompiling the source files for every target. add_library(zm STATIC ${ZM_BIN_SRC_FILES}) @@ -21,7 +72,7 @@ target_link_libraries(zms zm ${ZM_EXTRA_LIBS} ${ZM_BIN_LIBS}) # Generate man files for the binaries destined for the bin folder FOREACH(CBINARY zmc zmu) - POD2MAN(${CMAKE_CURRENT_SOURCE_DIR}/${CBINARY}.cpp zoneminder-${CBINARY} 8) + POD2MAN(${CMAKE_CURRENT_SOURCE_DIR}/${CBINARY}.cpp zoneminder-${CBINARY} 8 ${ZM_MANPAGE_DEST_PREFIX}) ENDFOREACH(CBINARY zmc zmu) install(TARGETS zmc zmu RUNTIME DESTINATION "${CMAKE_INSTALL_FULL_BINDIR}" PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) diff --git a/src/zm.h b/src/zm.h index 092561378..ad0f89584 100644 --- a/src/zm.h +++ b/src/zm.h @@ -17,9 +17,6 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. // -#if !defined(PATH_MAX) -#define PATH_MAX 1024 -#endif #ifndef ZM_H #define ZM_H diff --git a/src/zm_comms.h b/src/zm_comms.h index 133c0d19f..e93951a85 100644 --- a/src/zm_comms.h +++ b/src/zm_comms.h @@ -20,6 +20,7 @@ #ifndef ZM_COMMS_H #define ZM_COMMS_H +#include "zm_logger.h" #include "zm_exception.h" #include diff --git a/src/zm_config.h.in b/src/zm_config.h.in index f321095df..87a0f2db4 100644 --- a/src/zm_config.h.in +++ b/src/zm_config.h.in @@ -20,9 +20,11 @@ #ifndef ZM_CONFIG_H #define ZM_CONFIG_H +#if !defined(PATH_MAX) +#define PATH_MAX 1024 +#endif #include "config.h" #include "zm_config_defines.h" -#include "zm.h" #include diff --git a/src/zm_event.cpp b/src/zm_event.cpp index bcebfdc88..bf9902fd1 100644 --- a/src/zm_event.cpp +++ b/src/zm_event.cpp @@ -123,6 +123,7 @@ Event::Event( tot_score = 0; max_score = 0; have_video_keyframe = false; + alarm_frame_written = false; struct tm *stime = localtime(&start_time.tv_sec); @@ -177,8 +178,7 @@ Event::Event( Error("Can't mkdir %s: %s", path.c_str(), strerror(errno)); } } else { - // Shallow Storage - path = stringtf("/%" PRIu64, id); + path += stringtf("/%" PRIu64, id); if ( mkdir(path.c_str(), 0755) ) { if ( errno != EEXIST ) Error("Can't mkdir %s: %s", path.c_str(), strerror(errno)); @@ -242,6 +242,9 @@ Event::~Event() { videoStore = NULL; } + if ( frame_data.size() ) + WriteDbFrames(); + // Should not be static because we are multi-threaded char sql[ZM_SQL_MED_BUFSIZ]; struct DeltaTimeval delta_time; @@ -255,20 +258,25 @@ Event::~Event() { id, frames, end_time.tv_sec, delta_time.positive?"":"-", delta_time.sec, delta_time.fsec); db_mutex.lock(); if ( mysql_query(&dbconn, sql) ) { + db_mutex.unlock(); Error("Can't insert frame: %s", mysql_error(&dbconn)); } else { + db_mutex.unlock(); Debug(1,"Success writing last frame"); } - db_mutex.unlock(); } snprintf(sql, sizeof(sql), "UPDATE Events SET Name='%s %" PRIu64 "', EndTime = from_unixtime( %ld ), Length = %s%ld.%02ld, Frames = %d, AlarmFrames = %d, TotScore = %d, AvgScore = %d, MaxScore = %d, DefaultVideo = '%s' WHERE Id = %" PRIu64, - monitor->EventPrefix(), id, end_time.tv_sec, delta_time.positive?"":"-", delta_time.sec, delta_time.fsec, frames, alarm_frames, tot_score, (int)(alarm_frames?(tot_score/alarm_frames):0), max_score, video_name, id ); + monitor->EventPrefix(), id, end_time.tv_sec, + delta_time.positive?"":"-", delta_time.sec, delta_time.fsec, + frames, alarm_frames, + tot_score, (int)(alarm_frames?(tot_score/alarm_frames):0), max_score, + video_name, id ); db_mutex.lock(); while ( mysql_query(&dbconn, sql) && !zm_terminate ) { - Error("Can't update event: %s reason: %s", sql, mysql_error(&dbconn)); db_mutex.unlock(); + Error("Can't update event: %s reason: %s", sql, mysql_error(&dbconn)); sleep(1); db_mutex.lock(); } @@ -361,7 +369,7 @@ void Event::updateNotes(const StringSetMap &newNoteSetMap) { createNotes(notes); Debug(2, "Updating notes for event %d, '%s'", id, notes.c_str()); - static char sql[ZM_SQL_MED_BUFSIZ]; + static char sql[ZM_SQL_LGE_BUFSIZ]; #if USE_PREPARED_SQL static MYSQL_STMT *stmt = 0; @@ -508,6 +516,35 @@ void Event::AddPacket(ZMPacket *packet, int score, Image *alarm_image) { return; } +void Event::WriteDbFrames() { + static char sql[ZM_SQL_LGE_BUFSIZ]; + char * sql_ptr = (char *)&sql; + sql_ptr += snprintf(sql, sizeof(sql), + "INSERT INTO Frames ( EventId, FrameId, Type, TimeStamp, Delta, Score ) VALUES " + ); + while ( frame_data.size() ) { + Frame *frame = frame_data.front(); + frame_data.pop(); + sql_ptr += snprintf(sql_ptr, sizeof(sql)-(sql_ptr-(char *)&sql), "( %" PRIu64 ", %d, '%s', from_unixtime( %ld ), %s%ld.%02ld, %d ), ", + id, frame->frame_id, frame_type_names[frame->type], + frame->timestamp.tv_sec, + frame->delta.positive?"":"-", + frame->delta.sec, + frame->delta.fsec, + frame->score); + delete frame; + } + *(sql_ptr-2) = '\0'; + db_mutex.lock(); + if ( mysql_query(&dbconn, sql) ) { + Error("Can't insert frames: %s", mysql_error(&dbconn)); + Error("SQL was %s", sql); + db_mutex.unlock(); + return; + } + db_mutex.unlock(); +} + void Event::AddFrame(Image *image, struct timeval timestamp, int score, Image *alarm_image) { if ( !timestamp.tv_sec ) { Debug(1, "Not adding new frame, zero timestamp"); @@ -532,6 +569,13 @@ void Event::AddFrame(Image *image, struct timeval timestamp, int score, Image *a std::string snapshot_file = path + "/snapshot.jpg"; WriteFrameImage(image, timestamp, snapshot_file.c_str()); } + // The first frame with a score will be the frame that alarmed the event + if (!alarm_frame_written && score > 0) { + alarm_frame_written = true; + char alarm_file[PATH_MAX]; + snprintf(alarm_file, sizeof(alarm_file), "%s/alarm.jpg", path.c_str()); + WriteFrameImage(image, timestamp, alarm_file); + } } struct DeltaTimeval delta_time; @@ -545,21 +589,14 @@ void Event::AddFrame(Image *image, struct timeval timestamp, int score, Image *a bool db_frame = ( frame_type != BULK ) || (!frames) || ((frames%config.bulk_frame_interval)==0) ; if ( db_frame ) { - Debug(1, "Adding frame %d of type \"%s\" to DB", frames, Event::frame_type_names[frame_type]); static char sql[ZM_SQL_MED_BUFSIZ]; - snprintf(sql, sizeof(sql), - "INSERT INTO Frames ( EventId, FrameId, Type, TimeStamp, Delta, Score )" - " VALUES ( %" PRIu64 ", %d, '%s', from_unixtime( %ld ), %s%ld.%02ld, %d )", - id, frames, frame_type_names[frame_type], timestamp.tv_sec, delta_time.positive?"":"-", delta_time.sec, delta_time.fsec, score); - db_mutex.lock(); - if ( mysql_query(&dbconn, sql) ) { - Error("Can't insert frame: %s", mysql_error(&dbconn)); - Error("SQL was %s", sql); - db_mutex.unlock(); - return; + + frame_data.push( new Frame(id, frames, frame_type, timestamp, delta_time, score ) ); + if ( frame_data.size() > 10 ) { + WriteDbFrames(); + Debug(1, "Adding 10 frames to DB"); + last_db_frame = frames; } - db_mutex.unlock(); - last_db_frame = frames; // We are writing a Bulk frame if ( frame_type == BULK ) { @@ -582,7 +619,7 @@ void Event::AddFrame(Image *image, struct timeval timestamp, int score, Image *a db_mutex.lock(); } db_mutex.unlock(); - } + } // end if frame_type == BULK } // end if db_frame end_time = timestamp; diff --git a/src/zm_event.h b/src/zm_event.h index c354d58e3..a7c9ed8fa 100644 --- a/src/zm_event.h +++ b/src/zm_event.h @@ -33,6 +33,7 @@ #include #include +#include #include "zm.h" #include "zm_image.h" @@ -47,7 +48,10 @@ class Monitor; class EventStream; #define MAX_PRE_ALARM_FRAMES 16 // Maximum number of prealarm frames that can be stored +typedef uint64_t event_id_t; + typedef enum { NORMAL=0, BULK, ALARM } FrameType; +#include "zm_frame.h" // // Class describing events, i.e. captured periods of activity. // @@ -62,7 +66,6 @@ class Event { typedef std::map StringSetMap; protected: - typedef enum { NORMAL=0, BULK, ALARM } FrameType; static const char * frame_type_names[3]; struct PreAlarmData { @@ -71,6 +74,7 @@ class Event { unsigned int score; Image *alarm_frame; }; + std::queue frame_data; static int pre_alarm_count; static PreAlarmData pre_alarm_data[MAX_PRE_ALARM_FRAMES]; @@ -83,6 +87,7 @@ class Event { StringSetMap noteSetMap; int frames; int alarm_frames; + bool alarm_frame_written; unsigned int tot_score; unsigned int max_score; std::string path; @@ -125,6 +130,7 @@ class Event { private: void AddFramesInternal( int n_frames, int start_frame, Image **images, struct timeval **timestamps ); + void WriteDbFrames(); public: static const char *getSubPath( struct tm *time ) { diff --git a/src/zm_eventstream.cpp b/src/zm_eventstream.cpp index 4ce6316e4..3af8f199b 100644 --- a/src/zm_eventstream.cpp +++ b/src/zm_eventstream.cpp @@ -110,10 +110,10 @@ bool EventStream::loadInitialEventData( uint64_t init_event_id, unsigned int ini bool EventStream::loadEventData(uint64_t event_id) { static char sql[ZM_SQL_MED_BUFSIZ]; - snprintf(sql, sizeof(sql), "SELECT MonitorId, StorageId, Frames," - " unix_timestamp( StartTime ) AS StartTimestamp," - " (SELECT max(Delta)-min(Delta) FROM Frames WHERE EventId=Events.Id) AS Duration, DefaultVideo, Scheme" - " FROM Events WHERE Id = %" PRIu64, event_id); + snprintf(sql, sizeof(sql), + "SELECT MonitorId, StorageId, Frames, unix_timestamp( StartTime ) AS StartTimestamp, " + "(SELECT max(Delta)-min(Delta) FROM Frames WHERE EventId=Events.Id) AS Duration, " + "DefaultVideo, Scheme, SaveJPEGs FROM Events WHERE Id = %" PRIu64, event_id); if ( mysql_query(&dbconn, sql) ) { Error("Can't run query: %s", mysql_error(&dbconn)); @@ -155,6 +155,7 @@ bool EventStream::loadEventData(uint64_t event_id) { } else { event_data->scheme = Storage::SHALLOW; } + event_data->SaveJPEGs = dbrow[7] == NULL ? 0 : atoi(dbrow[7]); mysql_free_result( result ); Storage * storage = new Storage(event_data->storage_id); @@ -270,6 +271,7 @@ bool EventStream::loadEventData(uint64_t event_id) { if ( event_data->video_file[0] ) { char filepath[PATH_MAX]; snprintf(filepath, sizeof(filepath), "%s/%s", event_data->path, event_data->video_file); + Debug(1, "Loading video file from %s", filepath); ffmpeg_input = new FFmpeg_Input(); if ( 0 > ffmpeg_input->Open( filepath ) ) { Warning("Unable to open ffmpeg_input %s/%s", event_data->path, event_data->video_file); @@ -599,14 +601,17 @@ bool EventStream::sendFrame(int delta_us) { // This needs to be abstracted. If we are saving jpgs, then load the capture file. If we are only saving analysis frames, then send that. // // This is also wrong, need to have this info stored in the event! FIXME - if ( monitor->GetOptSaveJPEGs() & 1 ) { + if ( event_data->SaveJPEGs & 1 ) { snprintf( filepath, sizeof(filepath), staticConfig.capture_file_format, event_data->path, curr_frame_id ); - } else if ( monitor->GetOptSaveJPEGs() & 2 ) { + } else if ( event_data->SaveJPEGs & 2 ) { snprintf( filepath, sizeof(filepath), staticConfig.analyse_file_format, event_data->path, curr_frame_id ); if ( stat( filepath, &filestat ) < 0 ) { Debug(1, "analyze file %s not found will try to stream from other", filepath); snprintf( filepath, sizeof(filepath), staticConfig.capture_file_format, event_data->path, curr_frame_id ); - filepath[0] = 0; + if ( stat( filepath, &filestat ) < 0 ) { + Debug(1, "capture file %s not found either", filepath); + filepath[0] = 0; + } } } else if ( ! ffmpeg_input ) { diff --git a/src/zm_eventstream.h b/src/zm_eventstream.h index 0c9ee0db3..81e1641d6 100644 --- a/src/zm_eventstream.h +++ b/src/zm_eventstream.h @@ -65,6 +65,7 @@ class EventStream : public StreamBase { FrameData *frames; char video_file[PATH_MAX]; Storage::Schemes scheme; + int SaveJPEGs; }; protected: diff --git a/src/zm_exception.h b/src/zm_exception.h index 83d1ecab1..a02653b88 100644 --- a/src/zm_exception.h +++ b/src/zm_exception.h @@ -20,8 +20,6 @@ #ifndef ZM_EXCEPTION_H #define ZM_EXCEPTION_H -#include "zm.h" - #include class Exception diff --git a/src/zm_ffmpeg.cpp b/src/zm_ffmpeg.cpp index 2baebea32..392cb0464 100644 --- a/src/zm_ffmpeg.cpp +++ b/src/zm_ffmpeg.cpp @@ -24,15 +24,63 @@ #if HAVE_LIBAVCODEC || HAVE_LIBAVUTIL || HAVE_LIBSWSCALE +void log_libav_callback( void *ptr, int level, const char *fmt, va_list vargs ) { + Logger *log = Logger::fetch(); + int log_level = 0; + if ( level == AV_LOG_QUIET ) { // -8 + log_level = Logger::NOLOG; + } else if ( level == AV_LOG_PANIC ) { //0 + log_level = Logger::PANIC; + } else if ( level == AV_LOG_FATAL ) { // 8 + log_level = Logger::FATAL; + } else if ( level == AV_LOG_ERROR ) { // 16 + log_level = Logger::WARNING; // ffmpeg outputs a lot of errors that don't really affect anything. + //log_level = Logger::ERROR; + } else if ( level == AV_LOG_WARNING ) { //24 + log_level = Logger::INFO; + //log_level = Logger::WARNING; + } else if ( level == AV_LOG_INFO ) { //32 + log_level = Logger::DEBUG1; + //log_level = Logger::INFO; + } else if ( level == AV_LOG_VERBOSE ) { //40 + log_level = Logger::DEBUG2; + } else if ( level == AV_LOG_DEBUG ) { //48 + log_level = Logger::DEBUG3; +#ifdef AV_LOG_TRACE + } else if ( level == AV_LOG_TRACE ) { + log_level = Logger::DEBUG8; +#endif +#ifdef AV_LOG_MAX_OFFSET + } else if ( level == AV_LOG_MAX_OFFSET ) { + log_level = Logger::DEBUG9; +#endif + } else { + Error("Unknown log level %d", level); + } + + if ( log ) { + char logString[8192]; + vsnprintf(logString, sizeof(logString)-1, fmt, vargs); + log->logPrint(false, __FILE__, __LINE__, log_level, logString); + } +} + void FFMPEGInit() { static bool bInit = false; if ( !bInit ) { - //if ( logDebugging() ) - //av_log_set_level( AV_LOG_DEBUG ); - //else + if ( logDebugging() ) + av_log_set_level( AV_LOG_DEBUG ); + else av_log_set_level( AV_LOG_QUIET ); + if ( config.log_ffmpeg ) + av_log_set_callback(log_libav_callback); + else + Info("Not enabling ffmpeg logs, as LOG_FFMPEG is disabled in options"); +#if LIBAVCODEC_VERSION_CHECK(58, 18, 0, 64, 0) +#else av_register_all(); +#endif avformat_network_init(); bInit = true; } @@ -268,6 +316,11 @@ void zm_dump_stream_format(AVFormatContext *ic, int i, int index, int is_output) int flags = (is_output ? ic->oformat->flags : ic->iformat->flags); AVStream *st = ic->streams[i]; AVDictionaryEntry *lang = av_dict_get(st->metadata, "language", NULL, 0); +#if LIBAVCODEC_VERSION_CHECK(57, 64, 0, 64, 0) + AVCodecParameters *codec = st->codecpar; +#else + AVCodecContext *codec = st->codec; +#endif Debug(1, " Stream #%d:%d", index, i); @@ -277,19 +330,20 @@ void zm_dump_stream_format(AVFormatContext *ic, int i, int index, int is_output) Debug(1, "[0x%x]", st->id); if (lang) Debug(1, "language (%s)", lang->value); - Debug(1, "frames:%d, timebase: %d/%d", st->nb_frames, st->time_base.num, st->time_base.den); + Debug(1, ", frames:%d, frame_size:%d timebase: %d/%d", + st->codec_info_nb_frames, + codec->frame_size, + st->time_base.num, + st->time_base.den); avcodec_string(buf, sizeof(buf), st->codec, is_output); Debug(1, ": %s", buf); -#if LIBAVCODEC_VERSION_CHECK(57, 64, 0, 64, 0) - AVCodecParameters *codec = st->codecpar; -#else - AVCodecContext *codec = st->codec; -#endif - if (st->sample_aspect_ratio.num && // default - av_cmp_q(st->sample_aspect_ratio, codec->sample_aspect_ratio)) { + if ( st->sample_aspect_ratio.num && // default + av_cmp_q(st->sample_aspect_ratio, codec->sample_aspect_ratio) + ) { AVRational display_aspect_ratio; - av_reduce(&display_aspect_ratio.num, &display_aspect_ratio.den, + av_reduce(&display_aspect_ratio.num, + &display_aspect_ratio.den, codec->width * (int64_t)st->sample_aspect_ratio.num, codec->height * (int64_t)st->sample_aspect_ratio.den, 1024 * 1024); @@ -298,7 +352,7 @@ void zm_dump_stream_format(AVFormatContext *ic, int i, int index, int is_output) display_aspect_ratio.num, display_aspect_ratio.den); } - if (st->codec->codec_type == AVMEDIA_TYPE_VIDEO) { + if ( st->codec->codec_type == AVMEDIA_TYPE_VIDEO ) { int fps = st->avg_frame_rate.den && st->avg_frame_rate.num; int tbn = st->time_base.den && st->time_base.num; int tbc = st->codec->time_base.den && st->codec->time_base.num; @@ -331,7 +385,6 @@ void zm_dump_stream_format(AVFormatContext *ic, int i, int index, int is_output) Debug(1, " (visual impaired)"); if (st->disposition & AV_DISPOSITION_CLEAN_EFFECTS) Debug(1, " (clean effects)"); - Debug(1, "\n"); //dump_metadata(NULL, st->metadata, " "); diff --git a/src/zm_ffmpeg_camera.cpp b/src/zm_ffmpeg_camera.cpp index 34838673f..2fbd1c759 100644 --- a/src/zm_ffmpeg_camera.cpp +++ b/src/zm_ffmpeg_camera.cpp @@ -136,6 +136,7 @@ FfmpegCamera::FfmpegCamera( mFrame = NULL; frameCount = 0; mCanCapture = false; + error_count = 0; } // end FFmpegCamera::FFmpegCamera @@ -209,6 +210,8 @@ int FfmpegCamera::OpenFfmpeg() { int ret; + error_count = 0; + // Open the input, not necessarily a file #if !LIBAVFORMAT_VERSION_CHECK(53, 2, 0, 4, 0) Debug(1, "Calling av_open_input_file"); diff --git a/src/zm_ffmpeg_camera.h b/src/zm_ffmpeg_camera.h index 84bef697a..7db3a75b5 100644 --- a/src/zm_ffmpeg_camera.h +++ b/src/zm_ffmpeg_camera.h @@ -73,6 +73,8 @@ class FfmpegCamera : public Camera { bool mCanCapture; #endif // HAVE_LIBAVFORMAT + int error_count; + public: FfmpegCamera( int p_id, diff --git a/src/zm_frame.cpp b/src/zm_frame.cpp new file mode 100644 index 000000000..bf47bdbf5 --- /dev/null +++ b/src/zm_frame.cpp @@ -0,0 +1,18 @@ +#include "zm_frame.h" + +Frame::Frame( + event_id_t p_event_id, + int p_frame_id, + FrameType p_type, + struct timeval p_timestamp, + struct DeltaTimeval p_delta, + int p_score + ) : + event_id(p_event_id), + frame_id(p_frame_id), + type(p_type), + timestamp(p_timestamp), + delta(p_delta), + score(p_score) +{ +} diff --git a/src/zm_frame.h b/src/zm_frame.h new file mode 100644 index 000000000..84190e779 --- /dev/null +++ b/src/zm_frame.h @@ -0,0 +1,54 @@ +// +// ZoneMinder Frame Class Interfaces, $Date$, $Revision$ +// Copyright (C) 2001-2008 Philip Coombes +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// + +#ifndef ZM_FRAME_H +#define ZM_FRAME_H + +#include +#include +class Frame; + +#include "zm_event.h" +#include "zm_time.h" + +// +// This describes a frame record +// +class Frame { + +public: + Frame( + event_id_t p_event_id, + int p_frame_id, + FrameType p_type, + struct timeval p_timestamp, + struct DeltaTimeval p_delta, + int p_score + ); + + event_id_t event_id; + int frame_id; + FrameType type; + struct timeval timestamp; + struct DeltaTimeval delta; + int score; + +}; + +#endif // ZM_FRAME_H diff --git a/src/zm_logger.cpp b/src/zm_logger.cpp index 4c0685352..f6fc37a6f 100644 --- a/src/zm_logger.cpp +++ b/src/zm_logger.cpp @@ -443,6 +443,7 @@ void Logger::closeSyslog() { void Logger::logPrint( bool hex, const char * const filepath, const int line, const int level, const char *fstring, ... ) { if ( level > mEffectiveLevel ) return; + log_mutex.lock(); char timeString[64]; char logString[8192]; va_list argPtr; @@ -578,8 +579,10 @@ void Logger::logPrint( bool hex, const char * const filepath, const int line, co abort(); exit(-1); } + log_mutex.unlock(); } + void logInit(const char *name, const Logger::Options &options) { if ( !Logger::smInstance ) Logger::smInstance = new Logger(); diff --git a/src/zm_logger.h b/src/zm_logger.h index 8e2ab6c9d..f65c5ec31 100644 --- a/src/zm_logger.h +++ b/src/zm_logger.h @@ -30,6 +30,8 @@ #endif // HAVE_SYS_SYSCALL_H #include +#include "zm_thread.h" + class Logger { public: enum { @@ -82,6 +84,8 @@ private: static bool smInitialised; static Logger *smInstance; + RecursiveMutex log_mutex; + static StringMap smCodes; static IntMap smSyslogPriorities; diff --git a/src/zm_monitor.cpp b/src/zm_monitor.cpp index 835b922f9..2cf8f5c0a 100644 --- a/src/zm_monitor.cpp +++ b/src/zm_monitor.cpp @@ -913,7 +913,6 @@ bool Monitor::connect() { } // Monitor::connect Monitor::~Monitor() { - if ( mem_ptr ) { if ( event ) { Info( "%s: image_count:%d - Closing event %" PRIu64 ", shutting down", name, image_count, event->Id() ); @@ -988,6 +987,14 @@ Monitor::~Monitor() { videoStore = NULL; } + if ( n_linked_monitors ) { + for( int i = 0; i < n_linked_monitors; i++ ) { + delete linked_monitors[i]; + } + delete[] linked_monitors; + linked_monitors = 0; + } + if ( timestamps ) { delete[] timestamps; timestamps = 0; @@ -2064,8 +2071,8 @@ void Monitor::ReloadLinkedMonitors(const char *p_linked_monitors) { db_mutex.lock(); snprintf(sql, sizeof(sql), "SELECT Id, Name FROM Monitors WHERE Id = %d AND Function != 'None' AND Function != 'Monitor' AND Enabled = 1", link_ids[i] ); if ( mysql_query(&dbconn, sql) ) { + db_mutex.unlock(); Error("Can't run query: %s", mysql_error(&dbconn)); - db_mutex.unlock(); continue; } diff --git a/src/zm_packet.cpp b/src/zm_packet.cpp index 3576e4d47..ecd18a5b8 100644 --- a/src/zm_packet.cpp +++ b/src/zm_packet.cpp @@ -37,6 +37,7 @@ ZMPacket::ZMPacket( ) { analysis_image = NULL; image_index = -1; score = -1; + codec_imgsize = 0; } ZMPacket::ZMPacket( ZMPacket &p ) { @@ -209,7 +210,7 @@ AVFrame *ZMPacket::get_out_frame( const AVCodecContext *ctx ) { return NULL; } #if LIBAVUTIL_VERSION_CHECK(54, 6, 0, 6, 0) - int codec_imgsize = av_image_get_buffer_size( + codec_imgsize = av_image_get_buffer_size( ctx->pix_fmt, ctx->width, ctx->height, 1); @@ -223,7 +224,7 @@ AVFrame *ZMPacket::get_out_frame( const AVCodecContext *ctx ) { ctx->height, 1); #else - int codec_imgsize = avpicture_get_size( + codec_imgsize = avpicture_get_size( ctx->pix_fmt, ctx->width, ctx->height); diff --git a/src/zm_packet.h b/src/zm_packet.h index e2aa89090..1d7dc34de 100644 --- a/src/zm_packet.h +++ b/src/zm_packet.h @@ -46,6 +46,7 @@ class ZMPacket { struct timeval *timestamp; AVMediaType codec_type; int image_index; + int codec_imgsize; public: AVPacket *av_packet() { return &packet; } @@ -64,6 +65,7 @@ class ZMPacket { void lock() { mutex.lock(); }; void unlock() { mutex.unlock(); }; AVFrame *get_out_frame( const AVCodecContext *ctx ); + int get_codec_imgsize() { return codec_imgsize; }; }; #endif /* ZM_PACKET_H */ diff --git a/src/zm_remote_camera.cpp b/src/zm_remote_camera.cpp index 25a963a17..d80532d54 100644 --- a/src/zm_remote_camera.cpp +++ b/src/zm_remote_camera.cpp @@ -52,7 +52,7 @@ RemoteCamera::RemoteCamera( RemoteCamera::~RemoteCamera() { if ( hp != NULL ) { - freeaddrinfo(hp); + freeaddrinfo(hp); hp = NULL; } if ( mAuthenticator ) { @@ -68,8 +68,8 @@ void RemoteCamera::Initialise() { if( host.empty() ) Fatal( "No host specified for remote camera" ); - if( port.empty() ) - Fatal( "No port specified for remote camera" ); + if ( port.empty() ) + Fatal("No port specified for remote camera"); //if( path.empty() ) //Fatal( "No path specified for remote camera" ); @@ -99,6 +99,12 @@ void RemoteCamera::Initialise() { if ( ret != 0 ) { Fatal( "Can't getaddrinfo(%s port %s): %s", host.c_str(), port.c_str(), gai_strerror(ret) ); } + struct addrinfo *p = NULL; + int addr_count = 0; + for ( p = hp; p != NULL; p = p->ai_next ) { + addr_count++; + } + Debug(1, "%d addresses returned", addr_count); } int RemoteCamera::Read( int fd, char *buf, int size ) { diff --git a/src/zm_remote_camera_http.cpp b/src/zm_remote_camera_http.cpp index d33406b07..1e7946478 100644 --- a/src/zm_remote_camera_http.cpp +++ b/src/zm_remote_camera_http.cpp @@ -35,6 +35,14 @@ #include #endif +#if HAVE_LIBPCRE +static RegExpr *header_expr = 0; +static RegExpr *status_expr = 0; +static RegExpr *connection_expr = 0; +static RegExpr *content_length_expr = 0; +static RegExpr *content_type_expr = 0; +#endif + RemoteCameraHttp::RemoteCameraHttp( unsigned int p_monitor_id, const std::string &p_method, @@ -72,9 +80,9 @@ RemoteCameraHttp::RemoteCameraHttp( if ( p_method == "simple" ) method = SIMPLE; - else if ( p_method == "regexp" ) + else if ( p_method == "regexp" ) { method = REGEXP; - else + } else Fatal( "Unrecognised method '%s' when creating HTTP camera %d", p_method.c_str(), monitor_id ); if ( capture ) { Initialise(); @@ -115,10 +123,25 @@ void RemoteCameraHttp::Initialise() { mode = SINGLE_IMAGE; format = UNDEF; state = HEADER; -} + +#if HAVE_LIBPCRE + if ( method == REGEXP ) { + if ( !header_expr ) + header_expr = new RegExpr("^(.+?\r?\n\r?\n)", PCRE_DOTALL); + if ( !status_expr ) + status_expr = new RegExpr("^HTTP/(1\\.[01]) +([0-9]+) +(.+?)\r?\n", PCRE_CASELESS); + if ( !connection_expr ) + connection_expr = new RegExpr("Connection: ?(.+?)\r?\n", PCRE_CASELESS); + if ( !content_length_expr ) + content_length_expr = new RegExpr("Content-length: ?([0-9]+)\r?\n", PCRE_CASELESS); + if ( !content_type_expr ) + content_type_expr = new RegExpr("Content-type: ?(.+?)(?:; ?boundary=\x22?(.+?)\x22?)?\r?\n", PCRE_CASELESS); + } +#endif +} // end void RemoteCameraHttp::Initialise() int RemoteCameraHttp::Connect() { - struct addrinfo *p; + struct addrinfo *p = NULL; for ( p = hp; p != NULL; p = p->ai_next ) { sd = socket( p->ai_family, p->ai_socktype, p->ai_protocol ); @@ -153,24 +176,23 @@ int RemoteCameraHttp::Connect() { } // end int RemoteCameraHttp::Connect() int RemoteCameraHttp::Disconnect() { - close( sd ); + close(sd); sd = -1; - Debug( 3, "Disconnected from host" ); - return( 0 ); + Debug(3, "Disconnected from host"); + return 0; } int RemoteCameraHttp::SendRequest() { - Debug( 2, "Sending request: %s", request.c_str() ); - if ( write( sd, request.data(), request.length() ) < 0 ) - { - Error( "Can't write: %s", strerror(errno) ); + Debug(2, "Sending request: %s", request.c_str()); + if ( write(sd, request.data(), request.length()) < 0 ) { + Error("Can't write: %s", strerror(errno)); Disconnect(); - return( -1 ); + return -1; } format = UNDEF; state = HEADER; - Debug( 3, "Request sent" ); - return( 0 ); + Debug(3, "Request sent"); + return 0; } /* Return codes are as follows: @@ -266,9 +288,9 @@ int RemoteCameraHttp::ReadData( Buffer &buffer, unsigned int bytes_expected ) { total_bytes_to_read -= bytes_read; } while ( total_bytes_to_read ); - Debug( 4, buffer ); + Debug(4, buffer); - return( total_bytes_read ); + return total_bytes_read; } int RemoteCameraHttp::GetResponse() { @@ -289,33 +311,23 @@ int RemoteCameraHttp::GetResponse() { //int subcontent_length = 0; //const char *subcontent_type = ""; - while ( true ) { + while ( !zm_terminate ) { switch( state ) { case HEADER : { - static RegExpr *header_expr = 0; - static RegExpr *status_expr = 0; - static RegExpr *connection_expr = 0; - static RegExpr *content_length_expr = 0; - static RegExpr *content_type_expr = 0; - while ( !( buffer_len = ReadData(buffer) ) && !zm_terminate ) { Debug(4, "Timeout waiting for REGEXP HEADER"); } if ( buffer_len < 0 ) { - Error( "Unable to read header data" ); - return( -1 ); + Error("Unable to read header data"); + return -1; } bytes += buffer_len; - if ( !header_expr ) - header_expr = new RegExpr( "^(.+?\r?\n\r?\n)", PCRE_DOTALL ); if ( header_expr->Match( (char*)buffer, buffer.size() ) == 2 ) { header = header_expr->MatchString( 1 ); header_len = header_expr->MatchLength( 1 ); - Debug( 4, "Captured header (%d bytes):\n'%s'", header_len, header ); + Debug(4, "Captured header (%d bytes):\n'%s'", header_len, header); - if ( !status_expr ) - status_expr = new RegExpr( "^HTTP/(1\\.[01]) +([0-9]+) +(.+?)\r?\n", PCRE_CASELESS ); if ( status_expr->Match( header, header_len ) < 4 ) { Error( "Unable to extract HTTP status from header" ); return( -1 ); @@ -353,22 +365,16 @@ int RemoteCameraHttp::GetResponse() { } Debug( 3, "Got status '%d' (%s), http version %s", status_code, status_mesg, http_version ); - if ( !connection_expr ) - connection_expr = new RegExpr( "Connection: ?(.+?)\r?\n", PCRE_CASELESS ); if ( connection_expr->Match( header, header_len ) == 2 ) { connection_type = connection_expr->MatchString( 1 ); Debug( 3, "Got connection '%s'", connection_type ); } - if ( !content_length_expr ) - content_length_expr = new RegExpr( "Content-length: ?([0-9]+)\r?\n", PCRE_CASELESS ); if ( content_length_expr->Match( header, header_len ) == 2 ) { content_length = atoi( content_length_expr->MatchString( 1 ) ); Debug( 3, "Got content length '%d'", content_length ); } - if ( !content_type_expr ) - content_type_expr = new RegExpr( "Content-type: ?(.+?)(?:; ?boundary=\x22?(.+?)\x22?)?\r?\n", PCRE_CASELESS ); if ( content_type_expr->Match( header, header_len ) >= 2 ) { content_type = content_type_expr->MatchString( 1 ); Debug( 3, "Got content type '%s'\n", content_type ); @@ -525,7 +531,7 @@ int RemoteCameraHttp::GetResponse() { state = SUBHEADER; } Debug( 3, "Returning %d (%d) bytes of captured content", content_length, buffer.size() ); - return( content_length ); + return content_length; } case HEADERCONT : case SUBHEADERCONT : @@ -588,7 +594,7 @@ int RemoteCameraHttp::GetResponse() { static char content_boundary[64]; static int content_boundary_len; - while ( true ) { + while ( !zm_terminate ) { switch( state ) { case HEADER : { @@ -611,11 +617,11 @@ int RemoteCameraHttp::GetResponse() { case HEADERCONT : { while ( !( buffer_len = ReadData(buffer) ) && !zm_terminate ) { - Debug(4, "Timeout waiting for HEADERCONT"); + Debug(1, "Timeout waiting for HEADERCONT"); } if ( buffer_len < 0 ) { - Error( "Unable to read header" ); - return( -1 ); + Error("Unable to read header"); + return -1; } bytes += buffer_len; @@ -625,9 +631,10 @@ int RemoteCameraHttp::GetResponse() { bool all_headers = false; while( true ) { - int crlf_len = memspn( header_ptr, "\r\n", header_len ); + int crlf_len = memspn(header_ptr, "\r\n", header_len); if ( n_headers ) { if ( (crlf_len == 2 && !strncmp( header_ptr, "\n\n", crlf_len )) || (crlf_len == 4 && !strncmp( header_ptr, "\r\n\r\n", crlf_len )) ) { + Debug(3, "Have double linefeed, done headers"); *header_ptr = '\0'; header_ptr += crlf_len; header_len -= buffer.consume( header_ptr-(char *)buffer ); @@ -687,7 +694,7 @@ int RemoteCameraHttp::GetResponse() { start_ptr = http_header; end_ptr = start_ptr+strspn( start_ptr, "10." ); - // FIXME WHy are we memsetting every time? Can we not do it once? + // FIXME Why are we memsetting every time? Can we not do it once? memset( http_version, 0, sizeof(http_version) ); strncpy( http_version, start_ptr, end_ptr-start_ptr ); @@ -771,7 +778,7 @@ int RemoteCameraHttp::GetResponse() { strcpy( content_type, start_ptr ); Debug( 3, "Got content type '%s'", content_type ); } - } + } // end if content_type_header if ( !strcasecmp( content_type, "image/jpeg" ) || !strcasecmp( content_type, "image/jpg" ) ) { // Single image @@ -806,10 +813,10 @@ int RemoteCameraHttp::GetResponse() { return( -1 ); } } else { - Debug( 3, "Unable to extract entire header from stream, continuing" ); + Debug(3, "Unable to extract entire header from stream, continuing"); state = HEADERCONT; //return( -1 ); - } + } // end if all_headers break; } case SUBHEADER : diff --git a/src/zm_signal.cpp b/src/zm_signal.cpp index 034ff7d66..e13bfdc9c 100644 --- a/src/zm_signal.cpp +++ b/src/zm_signal.cpp @@ -31,13 +31,15 @@ bool zm_terminate = false; RETSIGTYPE zm_hup_handler(int signal) { - Info("Got signal %d (%s), reloading", signal, strsignal(signal)); + // Shouldn't do complex things in signal handlers, logging is complex and can block due to mutexes. + //Info("Got signal %d (%s), reloading", signal, strsignal(signal)); zm_reload = true; } RETSIGTYPE zm_term_handler(int signal) { - Info("Got signal %d (%s), exiting", signal, strsignal(signal)); + // Shouldn't do complex things in signal handlers, logging is complex and can block due to mutexes. + //Info("Got signal %d (%s), exiting", signal, strsignal(signal)); zm_terminate = true; } diff --git a/src/zm_stream.cpp b/src/zm_stream.cpp index 4ba480363..7a66fa4de 100644 --- a/src/zm_stream.cpp +++ b/src/zm_stream.cpp @@ -32,8 +32,8 @@ StreamBase::~StreamBase() { #if HAVE_LIBAVCODEC if ( vid_stream ) { - delete vid_stream; - vid_stream = NULL; + delete vid_stream; + vid_stream = NULL; } #endif closeComms(); @@ -274,7 +274,7 @@ void StreamBase::openComms() { } } - unsigned int length = snprintf( + unsigned int length = snprintf( sock_path_lock, sizeof(sock_path_lock), "%s/zms-%06d.lock", @@ -286,11 +286,23 @@ void StreamBase::openComms() { } Debug(1, "Trying to open the lock on %s", sock_path_lock); + // Under systemd, we get chrooted to something like /tmp/systemd-apache-blh/ so the dir may not exist. + if ( mkdir(staticConfig.PATH_SOCKS.c_str(), 0755) ) { + if ( errno != EEXIST ) { + Error("Can't mkdir %s: %s", staticConfig.PATH_SOCKS.c_str(), strerror(errno)); + return; + } else { + Debug(3, "SOCKS dir %s already exists", staticConfig.PATH_SOCKS.c_str() ); + } + } else { + Debug(3, "Success making SOCKS dir %s", staticConfig.PATH_SOCKS.c_str() ); + } + lock_fd = open(sock_path_lock, O_CREAT|O_WRONLY, S_IRUSR | S_IWUSR); if ( lock_fd <= 0 ) { Error("Unable to open sock lock file %s: %s", sock_path_lock, strerror(errno)); lock_fd = 0; - } else if ( flock(lock_fd, LOCK_EX) != 0 ) { + } else if ( flock(lock_fd, LOCK_EX) != 0 ) { Error("Unable to lock sock lock file %s: %s", sock_path_lock, strerror(errno)); close(lock_fd); lock_fd = 0; @@ -301,8 +313,8 @@ void StreamBase::openComms() { sd = socket(AF_UNIX, SOCK_DGRAM, 0); if ( sd < 0 ) { Fatal("Can't create socket: %s", strerror(errno)); - } else { - Debug(1, "Have socket %d", sd); + } else { + Debug(3, "Have socket %d", sd); } length = snprintf( @@ -324,7 +336,7 @@ void StreamBase::openComms() { strncpy(loc_addr.sun_path, loc_sock_path, sizeof(loc_addr.sun_path)); loc_addr.sun_family = AF_UNIX; - Debug(3, "Binding to %s", loc_sock_path); + Debug(3, "Binding to %s", loc_sock_path); if ( ::bind(sd, (struct sockaddr *)&loc_addr, strlen(loc_addr.sun_path)+sizeof(loc_addr.sun_family)+1) < 0 ) { Fatal("Can't bind: %s", strerror(errno)); } @@ -335,7 +347,7 @@ void StreamBase::openComms() { gettimeofday(&last_comm_update, NULL); } // end if connKey > 0 - Debug(2, "comms open"); + Debug(3, "comms open"); } // end void StreamBase::openComms() void StreamBase::closeComms() { diff --git a/src/zm_thread.h b/src/zm_thread.h index 0c41a93a5..8cdfb892c 100644 --- a/src/zm_thread.h +++ b/src/zm_thread.h @@ -20,9 +20,12 @@ #ifndef ZM_THREAD_H #define ZM_THREAD_H +class RecursiveMutex; + + +#include "zm_config.h" #include #include -#include #ifdef HAVE_SYS_SYSCALL_H #include #endif // HAVE_SYS_SYSCALL_H diff --git a/src/zm_videostore.cpp b/src/zm_videostore.cpp index ba1c793a1..37590a5df 100644 --- a/src/zm_videostore.cpp +++ b/src/zm_videostore.cpp @@ -72,7 +72,7 @@ VideoStore::VideoStore( audio_out_ctx = NULL; out_frame = NULL; -#ifdef HAVE_LIBAVRESAMPLE +#if defined(HAVE_LIBSWRESAMPLE) || defined(HAVE_LIBAVRESAMPLE) resample_ctx = NULL; #endif FFMPEGInit(); @@ -374,14 +374,12 @@ bool VideoStore::open() { #if LIBAVCODEC_VERSION_CHECK(57, 64, 0, 64, 0) avformat_new_stream(oc, (const AVCodec *)(audio_in_ctx->codec)); #else - avformat_new_stream(oc, (AVCodec *)audio_in_ctx->codec); + avformat_new_stream(oc, (AVCodec *)audio_in_ctx->codec); #endif if ( !audio_out_stream ) { - Error("Unable to create audio out stream\n"); + Error("Unable to create audio out stream"); audio_out_stream = NULL; } else { - Debug(2, "setting parameters"); - #if LIBAVCODEC_VERSION_CHECK(57, 64, 0, 64, 0) audio_out_ctx = avcodec_alloc_context3(audio_out_codec); // Copy params from instream to ctx @@ -422,7 +420,7 @@ bool VideoStore::open() { } // end if is AAC if ( audio_out_stream ) { - if (oc->oformat->flags & AVFMT_GLOBALHEADER) { + if ( oc->oformat->flags & AVFMT_GLOBALHEADER ) { #if LIBAVCODEC_VERSION_CHECK(56, 35, 0, 64, 0) audio_out_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; #else @@ -435,7 +433,7 @@ bool VideoStore::open() { /* open the out file, if needed */ if ( !(out_format->flags & AVFMT_NOFILE) ) { if ( (ret = avio_open2(&oc->pb, filename, AVIO_FLAG_WRITE, NULL, NULL) ) < 0 ) { - Error("Could not open out file '%s': %s\n", filename, + Error("Could not open out file '%s': %s", filename, av_make_error_string(ret).c_str()); return false; } @@ -630,10 +628,16 @@ VideoStore::~VideoStore() { avcodec_free_context(&audio_out_ctx); #endif audio_out_ctx = NULL; -#ifdef HAVE_LIBAVRESAMPLE +#if defined(HAVE_LIBAVRESAMPLE) || defined(HAVE_LIBSWRESAMPLE) if ( resample_ctx ) { +#if defined(HAVE_LIBSWRESAMPLE) + swr_free(&resample_ctx); +#else +#if defined(HAVE_LIBAVRESAMPLE) avresample_close(resample_ctx); avresample_free(&resample_ctx); +#endif +#endif } if ( in_frame ) { av_frame_free(&in_frame); @@ -666,18 +670,18 @@ VideoStore::~VideoStore() { } // VideoStore::~VideoStore() bool VideoStore::setup_resampler() { - //I think this is unneccessary, we should be able to just pass in the decoder from the input. -#ifdef HAVE_LIBAVRESAMPLE +#if !defined(HAVE_LIBSWRESAMPLE) && !defined(HAVE_LIBAVRESAMPLE) + Error( + "Not built with resample library. " + "Cannot do audio conversion to AAC"); + return false; +#else + // Newer ffmpeg wants to keep everything separate... so have to lookup our own // decoder, can't reuse the one from the camera. - AVCodec *audio_in_codec = avcodec_find_decoder( -#if LIBAVCODEC_VERSION_CHECK(57, 64, 0, 64, 0) - audio_in_stream->codecpar->codec_id -#else - audio_in_ctx->codec_id -#endif - ); - if ( (ret = avcodec_open2(audio_in_ctx, audio_in_codec, NULL)) < 0 ) { + audio_in_codec = avcodec_find_decoder(audio_in_ctx->codec_id); + ret = avcodec_open2(audio_in_ctx, audio_in_codec, NULL); + if ( ret < 0 ) { Error("Can't open in codec!"); return false; } @@ -698,12 +702,14 @@ bool VideoStore::setup_resampler() { audio_out_stream = NULL; return false; } -#else audio_out_ctx = audio_out_stream->codec; #endif + // Some formats (i.e. WAV) do not produce the proper channel layout + if ( audio_in_ctx->channel_layout == 0 ) + audio_in_ctx->channel_layout = av_get_channel_layout("mono"); /* put sample parameters */ - audio_out_ctx->bit_rate = audio_in_ctx->bit_rate; + audio_out_ctx->bit_rate = audio_in_ctx->bit_rate <= 96000 ? audio_in_ctx->bit_rate : 96000; audio_out_ctx->sample_rate = audio_in_ctx->sample_rate; audio_out_ctx->sample_fmt = audio_in_ctx->sample_fmt; audio_out_ctx->channels = audio_in_ctx->channels; @@ -722,9 +728,9 @@ bool VideoStore::setup_resampler() { if ( audio_out_codec->supported_samplerates ) { int found = 0; - for ( int i=0; audio_out_codec->supported_samplerates[i]; i++ ) { + for ( unsigned int i = 0; audio_out_codec->supported_samplerates[i]; i++) { if ( audio_out_ctx->sample_rate == - audio_out_codec->supported_samplerates[i]) { + audio_out_codec->supported_samplerates[i] ) { found = 1; break; } @@ -734,8 +740,8 @@ bool VideoStore::setup_resampler() { } else { audio_out_ctx->sample_rate = audio_out_codec->supported_samplerates[0]; - Debug(1, "Sampel rate is no good, setting to (%d)", - audio_out_codec->supported_samplerates[0]); + Debug(1, "Sample rate is no good, setting to (%d)", + audio_out_codec->supported_samplerates[0]); } } @@ -749,7 +755,6 @@ bool VideoStore::setup_resampler() { // Example code doesn't set the codec tb. I think it just uses whatever defaults //audio_out_ctx->time_base = (AVRational){1, audio_out_ctx->sample_rate}; - AVDictionary *opts = NULL; // Needed to allow AAC if ( (ret = av_dict_set(&opts, "strict", "experimental", 0)) < 0 ) { @@ -818,35 +823,50 @@ bool VideoStore::setup_resampler() { } out_frame->sample_rate = audio_out_ctx->sample_rate; - // Setup the audio resampler -#ifdef HAVE_LIBSWRESAMPLE - resample_ctx = swr_alloc(); -#else -#ifdef HAVE_LIBAVRESAMPLE - resample_ctx = avresample_alloc_context(); -#endif -#endif +#if defined(HAVE_LIBSWRESAMPLE) + resample_ctx = swr_alloc_set_opts(NULL, + av_get_default_channel_layout(audio_out_ctx->channels), + audio_out_ctx->sample_fmt, + audio_out_ctx->sample_rate, + av_get_default_channel_layout(audio_in_ctx->channels), + audio_in_ctx->sample_fmt, + audio_in_ctx->sample_rate, + 0, NULL); if ( !resample_ctx ) { - Error("Could not allocate resample ctx\n"); + Error("Could not allocate resample context"); + av_frame_free(&in_frame); + av_frame_free(&out_frame); + return false; + } + if ( (ret = swr_init(resample_ctx)) < 0 ) { + Error("Could not open resampler"); + av_frame_free(&in_frame); + av_frame_free(&out_frame); + swr_free(&resample_ctx); + return false; + } +#else +#if defined(HAVE_LIBAVRESAMPLE) + // Setup the audio resampler + resample_ctx = avresample_alloc_context(); + + if ( !resample_ctx ) { + Error("Could not allocate resample ctx"); + av_frame_free(&in_frame); + av_frame_free(&out_frame); return false; } - uint64_t mono_layout = av_get_channel_layout("mono"); - // Some formats (i.e. WAV) do not produce the proper channel layout - if ( audio_in_ctx->channel_layout == 0 ) { - av_opt_set_channel_layout(resample_ctx, "in_channel_layout", mono_layout, 0); - Debug(1, "Bad channel layout. Had to set it to mono (%d).", mono_layout); - } else { - Debug(1, "channel layout. set it to (%d).", audio_in_ctx->channel_layout); - av_opt_set_channel_layout(resample_ctx, "in_channel_layout", - audio_in_ctx->channel_layout, 0); - } - - av_opt_set_sample_fmt(resample_ctx, "in_sample_fmt", audio_in_ctx->sample_fmt, 0); - av_opt_set_int(resample_ctx, "in_channels", audio_in_ctx->channels, 0); - // av_opt_set_int( resample_ctx, "out_channel_layout", - // audio_out_ctx->channel_layout, 0); - av_opt_set_int(resample_ctx, "out_channel_layout", mono_layout, 0); + av_opt_set_int(resample_ctx, "in_channel_layout", + audio_in_ctx->channel_layout, 0); + av_opt_set_int(resample_ctx, "in_sample_fmt", + audio_in_ctx->sample_fmt, 0); + av_opt_set_int(resample_ctx, "in_sample_rate", + audio_in_ctx->sample_rate, 0); + av_opt_set_int(resample_ctx, "in_channels", + audio_in_ctx->channels, 0); + av_opt_set_int(resample_ctx, "out_channel_layout", + audio_in_ctx->channel_layout, 0); av_opt_set_int(resample_ctx, "out_sample_fmt", audio_out_ctx->sample_fmt, 0); av_opt_set_int(resample_ctx, "out_sample_rate", @@ -854,11 +874,13 @@ bool VideoStore::setup_resampler() { av_opt_set_int(resample_ctx, "out_channels", audio_out_ctx->channels, 0); -#ifdef HAVE_LIBAVRESAMPLE if ( (ret = avresample_open(resample_ctx)) < 0 ) { - Error("Could not open resample ctx\n"); + Error("Could not open resample ctx"); return false; + } else { + Debug(2, "Success opening resampler"); } +#endif #endif out_frame->nb_samples = audio_out_ctx->frame_size; @@ -868,30 +890,28 @@ bool VideoStore::setup_resampler() { // The codec gives us the frame size, in samples, we calculate the size of the // samples buffer in bytes unsigned int audioSampleBuffer_size = av_samples_get_buffer_size( - NULL, audio_out_ctx->channels, audio_out_ctx->frame_size, + NULL, audio_out_ctx->channels, + audio_out_ctx->frame_size, audio_out_ctx->sample_fmt, 0); converted_in_samples = (uint8_t *)av_malloc(audioSampleBuffer_size); if ( !converted_in_samples ) { - Error("Could not allocate converted in sample pointers\n"); + Error("Could not allocate converted in sample pointers"); return false; + } else { + Debug(2, "Frame Size %d, sample buffer size %d", audio_out_ctx->frame_size, audioSampleBuffer_size); } // Setup the data pointers in the AVFrame if ( avcodec_fill_audio_frame(out_frame, audio_out_ctx->channels, - audio_out_ctx->sample_fmt, - (const uint8_t *)converted_in_samples, - audioSampleBuffer_size, 0) < 0 ) { - Error("Could not allocate converted in sample pointers\n"); + audio_out_ctx->sample_fmt, + (const uint8_t *)converted_in_samples, + audioSampleBuffer_size, 0) < 0) { + Error("Could not allocate converted in sample pointers"); return false; } return true; -#else - Error( - "Not built with libavresample library. Cannot do audio conversion to " - "AAC"); - return false; #endif } // end bool VideoStore::setup_resampler() @@ -938,7 +958,7 @@ int VideoStore::writeVideoFramePacket( ZMPacket * zm_packet ) { swscale.Convert( zm_packet->image, zm_packet->buffer, - codec_imgsize, + zm_packet->codec_imgsize, (AVPixelFormat)zm_packet->image->AVPixFormat(), video_out_ctx->pix_fmt, video_out_ctx->width, @@ -1038,15 +1058,15 @@ int VideoStore::writeVideoFramePacket( ZMPacket * zm_packet ) { opkt.dts = av_rescale_q(opkt.dts, video_out_ctx->time_base, video_out_stream->time_base); int64_t duration; - if ( ipkt->duration ) { + if ( zm_packet->in_frame->pkt_duration ) { duration = av_rescale_q( - ipkt->duration, + zm_packet->in_frame->pkt_duration, video_in_stream->time_base, video_out_stream->time_base); Debug(1, "duration from ipkt: pts(%" PRId64 ") - last_pts(%" PRId64 ") = (%" PRId64 ") => (%" PRId64 ") (%d/%d) (%d/%d)", - ipkt->pts, + zm_packet->in_frame->pkt_pts, video_last_pts, - ipkt->duration, + zm_packet->in_frame->pkt_duration, duration, video_in_stream->time_base.num, video_in_stream->time_base.den, @@ -1056,17 +1076,17 @@ int VideoStore::writeVideoFramePacket( ZMPacket * zm_packet ) { } else { duration = av_rescale_q( - ipkt->pts - video_last_pts, + zm_packet->in_frame->pkt_pts - video_last_pts, video_in_stream->time_base, video_out_stream->time_base); Debug(1, "duration calc: pts(%" PRId64 ") - last_pts(%" PRId64 ") = (%" PRId64 ") => (%" PRId64 ")", - ipkt->pts, + zm_packet->in_frame->pkt_pts, video_last_pts, - ipkt->pts - video_last_pts, + zm_packet->in_frame->pkt_pts - video_last_pts, duration ); if ( duration <= 0 ) { - duration = ipkt->duration ? ipkt->duration : av_rescale_q(1,video_in_stream->time_base, video_out_stream->time_base); + duration = zm_packet->in_frame->pkt_duration ? zm_packet->in_frame->pkt_duration : av_rescale_q(1,video_in_stream->time_base, video_out_stream->time_base); } } opkt.duration = duration; @@ -1174,9 +1194,9 @@ int VideoStore::writeAudioFramePacket(ZMPacket *zm_packet) { if ( audio_out_codec ) { Debug(3, "Have audio codec"); -#ifdef HAVE_LIBAVRESAMPLE +#if defined(HAVE_LIBSWRESAMPLE) || defined(HAVE_LIBAVRESAMPLE) -#if LIBAVCODEC_VERSION_CHECK(57, 64, 0, 64, 0) + #if LIBAVCODEC_VERSION_CHECK(57, 64, 0, 64, 0) if ( (ret = avcodec_send_packet(audio_in_ctx, ipkt)) < 0 ) { Error("avcodec_send_packet fail %s", av_make_error_string(ret).c_str()); return 0; @@ -1186,11 +1206,11 @@ int VideoStore::writeAudioFramePacket(ZMPacket *zm_packet) { return 0; } Debug(2, - "Input Frame: samples(%d), format(%d), sample_rate(%d), channel " - "layout(%d)", - in_frame->nb_samples, in_frame->format, - in_frame->sample_rate, in_frame->channel_layout); -#else + "Input Frame: samples(%d), format(%d), sample_rate(%d), channel " + "layout(%d)", + in_frame->nb_samples, in_frame->format, + in_frame->sample_rate, in_frame->channel_layout); + #else /** * Decode the audio frame stored in the packet. * The in audio stream decoder is used to do this. @@ -1210,21 +1230,38 @@ int VideoStore::writeAudioFramePacket(ZMPacket *zm_packet) { Debug(2, "Not ready to transcode a frame yet."); return 0; } -#endif + #endif int frame_size = out_frame->nb_samples; in_frame->pts = audio_next_pts; // Resample the in into the audioSampleBuffer until we proceed the whole // decoded data - if ((ret = avresample_convert(resample_ctx, NULL, 0, 0, in_frame->data, - 0, in_frame->nb_samples)) < 0) { + #if defined(HAVE_LIBSWRESAMPLE) + Debug(2, "Converting %d to %d samples", in_frame->nb_samples, out_frame->nb_samples); + if ((ret = swr_convert(resample_ctx, + out_frame->data, frame_size, + (const uint8_t**)in_frame->data, + in_frame->nb_samples)) < 0) { + Error("Could not resample frame (error '%s')\n", + av_make_error_string(ret).c_str()); + av_frame_unref(in_frame); + return 0; + } + #else + #if defined(HAVE_LIBAVRESAMPLE) + if ((ret = + avresample_convert(resample_ctx, NULL, 0, 0, in_frame->data, + 0, in_frame->nb_samples)) < 0) { Error("Could not resample frame (error '%s')\n", av_make_error_string(ret).c_str()); av_frame_unref(in_frame); return 0; } + #endif + #endif av_frame_unref(in_frame); + #if defined(HAVE_LIBAVRESAMPLE) int samples_available = avresample_available(resample_ctx); if ( samples_available < frame_size ) { Debug(1, "Not enough samples yet (%d)", samples_available); @@ -1237,6 +1274,7 @@ int VideoStore::writeAudioFramePacket(ZMPacket *zm_packet) { Warning("Error reading resampled audio: "); return 0; } + #endif Debug(2, "Frame: samples(%d), format(%d), sample_rate(%d), channel layout(%d)", out_frame->nb_samples, out_frame->format, @@ -1245,7 +1283,7 @@ int VideoStore::writeAudioFramePacket(ZMPacket *zm_packet) { av_init_packet(&opkt); Debug(5, "after init packet"); -#if LIBAVCODEC_VERSION_CHECK(57, 64, 0, 64, 0) + #if LIBAVCODEC_VERSION_CHECK(57, 64, 0, 64, 0) if ((ret = avcodec_send_frame(audio_out_ctx, out_frame)) < 0) { Error("Could not send frame (error '%s')", av_make_error_string(ret).c_str()); @@ -1269,9 +1307,10 @@ int VideoStore::writeAudioFramePacket(ZMPacket *zm_packet) { // av_frame_unref( out_frame ); return 0; } -#else - if ( (ret = avcodec_encode_audio2(audio_out_ctx, &opkt, out_frame, - &data_present)) < 0) { + #else + if ( (ret = avcodec_encode_audio2( + audio_out_ctx, &opkt, out_frame, &data_present + )) < 0) { Error("Could not encode frame (error '%s')", av_make_error_string(ret).c_str()); zm_av_packet_unref(&opkt); @@ -1282,8 +1321,9 @@ int VideoStore::writeAudioFramePacket(ZMPacket *zm_packet) { zm_av_packet_unref(&opkt); return 0; } -#endif - + #endif +#else + Error("Have audio codec but no resampler?!"); #endif opkt.duration = out_frame->nb_samples; } else { diff --git a/src/zm_videostore.h b/src/zm_videostore.h index 40ebddf76..572023c8a 100644 --- a/src/zm_videostore.h +++ b/src/zm_videostore.h @@ -4,12 +4,11 @@ #include "zm_ffmpeg.h" extern "C" { #ifdef HAVE_LIBSWRESAMPLE -#include "libswresample/swresample.h" -#endif + #include "libswresample/swresample.h" #else -#ifdef HAVE_LIBAVRESAMPLE -#include "libavresample/avresample.h" -#endif + #ifdef HAVE_LIBAVRESAMPLE + #include "libavresample/avresample.h" + #endif #endif } @@ -66,7 +65,7 @@ int audio_in_stream_index; AVCodec *audio_out_codec; AVCodecContext *audio_out_ctx; #ifdef HAVE_LIBSWRESAMPLE - SwrContext* resample_ctx; + SwrContext *resample_ctx; #else #ifdef HAVE_LIBAVRESAMPLE AVAudioResampleContext* resample_ctx; diff --git a/src/zmu.cpp b/src/zmu.cpp index dd38cb0c0..af6cb603d 100644 --- a/src/zmu.cpp +++ b/src/zmu.cpp @@ -463,10 +463,10 @@ int main(int argc, char *argv[]) { } // end if auth if ( mon_id > 0 ) { - fprintf(stderr,"Monitor %d\n", mon_id); + //fprintf(stderr,"Monitor %d\n", mon_id); Monitor *monitor = Monitor::Load(mon_id, function&(ZMU_QUERY|ZMU_ZONES), Monitor::QUERY); if ( monitor ) { - fprintf(stderr,"Monitor %d(%s)\n", monitor->Id(), monitor->Name()); + //fprintf(stderr,"Monitor %d(%s)\n", monitor->Id(), monitor->Name()); if ( verbose ) { printf("Monitor %d(%s)\n", monitor->Id(), monitor->Name()); } @@ -479,9 +479,9 @@ int main(int argc, char *argv[]) { bool have_output = false; if ( function & ZMU_STATE ) { Monitor::State state = monitor->GetState(); - if ( verbose ) + if ( verbose ) { printf("Current state: %s\n", state==Monitor::ALARM?"Alarm":(state==Monitor::ALERT?"Alert":"Idle")); - else { + } else { if ( have_output ) printf("%c", separator); printf("%d", state); have_output = true; @@ -560,6 +560,11 @@ int main(int argc, char *argv[]) { if ( verbose ) printf( "Forcing alarm on\n" ); monitor->ForceAlarmOn( config.forced_alarm_score, "Forced Web" ); + while ( monitor->GetState() != Monitor::ALARM ) { + // Wait for monitor to notice. + usleep(1000); + } + printf( "Alarmed event id: %" PRIu64 "\n", monitor->GetLastEventId() ); } if ( function & ZMU_NOALARM ) { if ( verbose ) diff --git a/utils/do_debian_package.sh b/utils/do_debian_package.sh index 71f1ec460..a87c03379 100755 --- a/utils/do_debian_package.sh +++ b/utils/do_debian_package.sh @@ -76,7 +76,7 @@ fi; if [ "$DISTROS" == "" ]; then if [ "$RELEASE" != "" ]; then - DISTROS="xenial,bionic,trusty" + DISTROS="xenial,bionic,cosmic,disco,trusty" else DISTROS=`lsb_release -a 2>/dev/null | grep Codename | awk '{print $2}'`; fi; diff --git a/version b/version index c78d39b8e..7aa332e41 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.32.2 +1.33.0 diff --git a/web/api/app/Controller/GroupsController.php b/web/api/app/Controller/GroupsController.php index 7859f492e..6f0a88300 100644 --- a/web/api/app/Controller/GroupsController.php +++ b/web/api/app/Controller/GroupsController.php @@ -16,8 +16,10 @@ class GroupsController extends AppController { public function beforeFilter() { parent::beforeFilter(); - $canView = $this->Session->Read('groupsPermission'); - if ( $canView == 'None' ) { + global $user; + # We already tested for auth in appController, so we just need to test for specific permission + $canView = (!$user) || ($user['Groups'] != 'None'); + if ( !$canView ) { throw new UnauthorizedException(__('Insufficient Privileges')); return; } @@ -63,16 +65,23 @@ class GroupsController extends AppController { * @return void */ public function add() { - if ($this->request->is('post')) { + if ( $this->request->is('post') ) { - if ($this->Session->Read('groupPermission') != 'Edit') { - throw new UnauthorizedException(__('Insufficient privileges')); + global $user; + # We already tested for auth in appController, + # so we just need to test for specific permission + $canEdit = (!$user) || ($user['Groups'] == 'Edit'); + if ( !$canEdit ) { + throw new UnauthorizedException(__('Insufficient Privileges')); return; - } + } $this->Group->create(); - if ($this->Group->save($this->request->data)) { - return $this->flash(__('The group has been saved.'), array('action' => 'index')); + if ( $this->Group->save($this->request->data) ) { + return $this->flash( + __('The group has been saved.'), + array('action' => 'index') + ); } } $monitors = $this->Group->Monitor->find('list'); @@ -86,17 +95,24 @@ class GroupsController extends AppController { * @param string $id * @return void */ - public function edit($id = null) { - if (!$this->Group->exists($id)) { + public function edit( $id = null ) { + if ( !$this->Group->exists($id) ) { throw new NotFoundException(__('Invalid group')); } if ( $this->request->is(array('post', 'put'))) { - if ( $this->Session->Read('groupPermission') != 'Edit' ) { - throw new UnauthorizedException(__('Insufficient privileges')); + global $user; + # We already tested for auth in appController, + # so we just need to test for specific permission + $canEdit = (!$user) || ($user['Groups'] == 'Edit'); + if ( !$canEdit ) { + throw new UnauthorizedException(__('Insufficient Privileges')); return; } - if ($this->Group->save($this->request->data)) { - return $this->flash(__('The group has been saved.'), array('action' => 'index')); + if ( $this->Group->save($this->request->data) ) { + return $this->flash( + __('The group has been saved.'), + array('action' => 'index') + ); } else { $message = 'Error'; } @@ -108,7 +124,7 @@ class GroupsController extends AppController { $this->set(array( 'message' => $message, 'monitors'=> $monitors, - '_serialize' => array('message',) + '_serialize' => array('message') )); } @@ -121,19 +137,30 @@ class GroupsController extends AppController { */ public function delete($id = null) { $this->Group->id = $id; - if (!$this->Group->exists()) { + if ( !$this->Group->exists() ) { throw new NotFoundException(__('Invalid group')); } $this->request->allowMethod('post', 'delete'); - if ( $this->Session->Read('groupPermission') != 'Edit' ) { - throw new UnauthorizedException(__('Insufficient privileges')); - return; - } - if ($this->Group->delete()) { - return $this->flash(__('The group has been deleted.'), array('action' => 'index')); + global $user; + # We already tested for auth in appController, + # so we just need to test for specific permission + $canEdit = (!$user) || ($user['Groups'] == 'Edit'); + if ( !$canEdit ) { + throw new UnauthorizedException(__('Insufficient Privileges')); + return; + } + + if ( $this->Group->delete() ) { + return $this->flash( + __('The group has been deleted.'), + array('action' => 'index') + ); } else { - return $this->flash(__('The group could not be deleted. Please, try again.'), array('action' => 'index')); + return $this->flash( + __('The group could not be deleted. Please, try again.'), + array('action' => 'index') + ); } } // end function delete } // end class GroupController diff --git a/web/api/app/Controller/HostController.php b/web/api/app/Controller/HostController.php index 017bce798..ce101c73d 100644 --- a/web/api/app/Controller/HostController.php +++ b/web/api/app/Controller/HostController.php @@ -65,18 +65,18 @@ class HostController extends AppController { $isZmAuth = $this->Config->find('first',array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_OPT_USE_AUTH')))['Config']['Value']; if ( $isZmAuth ) { + // In future, we may want to completely move to AUTH_HASH_LOGINS and return &auth= for all cases require_once "../../../includes/auth.php"; # in the event we directly call getCredentials.json $this->Session->read('user'); # this is needed for command line/curl to recognize a session $zmAuthRelay = $this->Config->find('first',array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_AUTH_RELAY')))['Config']['Value']; if ( $zmAuthRelay == 'hashed' ) { $zmAuthHashIps = $this->Config->find('first',array('conditions' => array('Config.' . $this->Config->primaryKey => 'ZM_AUTH_HASH_IPS')))['Config']['Value']; - $credentials = 'auth='.generateAuthHash($zmAuthHashIps); - } else if ( $zmAuthRelay == 'plain' ) { + // make sure auth is regenerated each time we call this API + $credentials = 'auth='.generateAuthHash($zmAuthHashIps,true); + } else { // user will need to append the store password here $credentials = 'user='.$this->Session->read('user.Username').'&pass='; $appendPassword = 1; - } else if ( $zmAuthRelay == 'none' ) { - $credentials = 'user='.$this->Session->read('user.Username'); } } return array($credentials, $appendPassword); diff --git a/web/api/app/Controller/MonitorsController.php b/web/api/app/Controller/MonitorsController.php index 185a06c84..8908955b5 100644 --- a/web/api/app/Controller/MonitorsController.php +++ b/web/api/app/Controller/MonitorsController.php @@ -182,6 +182,9 @@ class MonitorsController extends AppController { ($Monitor['ServerId']==ZM_SERVER_ID) ) ) { + if ( !defined('ZM_SERVER_ID')) { + Logger::Debug("Not defined ZM_SERVER_ID"); + } $this->daemonControl($this->Monitor->id, 'start'); } } else { @@ -349,26 +352,29 @@ class MonitorsController extends AppController { )); } - public function daemonControl($id, $command, $monitor=null, $daemon=null) { + public function daemonControl($id, $command, $daemon=null) { + + // Need to see if it is local or remote + $monitor = $this->Monitor->find('first', array( + 'fields' => array('Type', 'Function', 'Device'), + 'conditions' => array('Id' => $id) + )); + $monitor = $monitor['Monitor']; + $daemons = array(); - - if ( !$monitor ) { - // Need to see if it is local or remote - $monitor = $this->Monitor->find('first', array( - 'fields' => array('Type', 'Function', 'Device'), - 'conditions' => array('Id' => $id) - )); - $monitor = $monitor['Monitor']; - } - - if ( $monitor['Function'] == 'Monitor' ) { - array_push($daemons, 'zmc'); + if ( ! $daemon ) { + if ( $monitor['Function'] == 'Monitor' ) { + array_push($daemons, 'zmc'); + } else { + array_push($daemons, 'zmc', 'zma'); + } } else { - array_push($daemons, 'zmc', 'zma'); + array_push($daemons, $daemon); } $zm_path_bin = Configure::read('ZM_PATH_BIN'); + $status_text = ''; foreach ( $daemons as $daemon ) { $args = ''; if ( $daemon == 'zmc' and $monitor['Type'] == 'Local' ) { @@ -378,7 +384,14 @@ class MonitorsController extends AppController { } $shellcmd = escapeshellcmd("$zm_path_bin/zmdc.pl $command $daemon $args"); - $status = exec( $shellcmd ); + Logger::Debug("Command $shellcmd"); + $status = exec($shellcmd); + $status_text .= $status."\n"; } - } + $this->set(array( + 'status' => 'ok', + 'statustext' => $status_text, + '_serialize' => array('status','statustext'), + )); + } // end function daemonControl } // end class MonitorsController diff --git a/web/api/app/Model/Event.php b/web/api/app/Model/Event.php index a59393d9a..14dbd377e 100644 --- a/web/api/app/Model/Event.php +++ b/web/api/app/Model/Event.php @@ -100,7 +100,7 @@ class Event extends AppModel { ), ); - public function Relative_Path($event) { + public function Relative_Path($event) { $event_path = ''; if ( $event['Scheme'] == 'Deep' ) { diff --git a/web/api/app/Model/Group.php b/web/api/app/Model/Group.php index e27460424..108f9b9c7 100644 --- a/web/api/app/Model/Group.php +++ b/web/api/app/Model/Group.php @@ -38,8 +38,8 @@ class Group extends AppModel { */ public $validate = array( 'Name' => array( - 'notEmpty' => array( - 'rule' => array('notEmpty'))), + 'notBlank' => array( + 'rule' => array('notBlank'))), 'Id' => array( 'numeric' => array( 'rule' => array('numeric'), diff --git a/web/api/app/View/Monitors/json/daemon_control.ctp b/web/api/app/View/Monitors/json/daemon_control.ctp new file mode 100644 index 000000000..4dbb0a055 --- /dev/null +++ b/web/api/app/View/Monitors/json/daemon_control.ctp @@ -0,0 +1 @@ +echo json_encode($status_text); diff --git a/web/api/app/View/Monitors/xml/daemon_control.ctp b/web/api/app/View/Monitors/xml/daemon_control.ctp new file mode 100644 index 000000000..8e7d3a290 --- /dev/null +++ b/web/api/app/View/Monitors/xml/daemon_control.ctp @@ -0,0 +1,2 @@ +$xml = Xml::fromArray(array('response' => $status_text)); +echo $xml->asXML(); diff --git a/web/includes/Event.php b/web/includes/Event.php index fb7af931e..27d9ae015 100644 --- a/web/includes/Event.php +++ b/web/includes/Event.php @@ -198,32 +198,34 @@ class Event { public function getStreamSrc( $args=array(), $querySep='&' ) { - $streamSrc = ZM_BASE_PROTOCOL.'://'; + $streamSrc = ''; + $Server = null; if ( $this->Storage()->ServerId() ) { + # The Event may have been moved to Storage on another server, + # So prefer viewing the Event from the Server that is actually + # storing the video $Server = $this->Storage()->Server(); - $streamSrc .= $Server->Hostname(); - if ( ZM_MIN_STREAMING_PORT ) { - $streamSrc .= ':'.(ZM_MIN_STREAMING_PORT+$this->{'MonitorId'}); - } } else if ( $this->Monitor()->ServerId() ) { # Assume that the server that recorded it has it $Server = $this->Monitor()->Server(); - $streamSrc .= $Server->Hostname(); - if ( ZM_MIN_STREAMING_PORT ) { - $streamSrc .= ':'.(ZM_MIN_STREAMING_PORT+$this->{'MonitorId'}); - } - } else if ( ZM_MIN_STREAMING_PORT ) { - $streamSrc .= $_SERVER['SERVER_NAME'].':'.(ZM_MIN_STREAMING_PORT+$this->{'MonitorId'}); } else { - $streamSrc .= $_SERVER['HTTP_HOST']; + # A default Server will result in the use of ZM_DIR_EVENTS + $Server = new Server(); } + # If we are in a multi-port setup, then use the multiport, else by + # passing null Server->Url will use the Port set in the Server setting + $streamSrc .= $Server->Url( + ZM_MIN_STREAMING_PORT ? + ZM_MIN_STREAMING_PORT+$this->{'MonitorId'} : + null); + if ( $this->{'DefaultVideo'} and $args['mode'] != 'jpeg' ) { - $streamSrc .= ( ZM_BASE_PATH != '/' ? ZM_BASE_PATH : '' ).'/index.php'; + $streamSrc .= $Server->PathToIndex(); $args['eid'] = $this->{'Id'}; $args['view'] = 'view_video'; } else { - $streamSrc .= ZM_PATH_ZMS; + $streamSrc .= $Server->PathToZMS(); $args['source'] = 'event'; $args['event'] = $this->{'Id'}; @@ -238,10 +240,10 @@ class Event { if ( ZM_OPT_USE_AUTH ) { if ( ZM_AUTH_RELAY == 'hashed' ) { $args['auth'] = generateAuthHash(ZM_AUTH_HASH_IPS); - } elseif ( ZM_AUTH_RELAY == 'plain' ) { + } else if ( ZM_AUTH_RELAY == 'plain' ) { $args['user'] = $_SESSION['username']; $args['pass'] = $_SESSION['password']; - } elseif ( ZM_AUTH_RELAY == 'none' ) { + } else if ( ZM_AUTH_RELAY == 'none' ) { $args['user'] = $_SESSION['username']; } } @@ -328,27 +330,21 @@ class Event { # The thumbnail is theoretically the image with the most motion. # We always store at least 1 image when capturing - $streamSrc = ZM_BASE_PROTOCOL.'://'; + $streamSrc = ''; + $Server = null; if ( $this->Storage()->ServerId() ) { $Server = $this->Storage()->Server(); - $streamSrc .= $Server->Hostname(); - if ( ZM_MIN_STREAMING_PORT ) { - $streamSrc .= ':'.(ZM_MIN_STREAMING_PORT+$this->{'MonitorId'}); - } } else if ( $this->Monitor()->ServerId() ) { + # Assume that the server that recorded it has it $Server = $this->Monitor()->Server(); - $streamSrc .= $Server->Hostname(); - if ( ZM_MIN_STREAMING_PORT ) { - $streamSrc .= ':'.(ZM_MIN_STREAMING_PORT+$this->{'MonitorId'}); - } - - } else if ( ZM_MIN_STREAMING_PORT ) { - $streamSrc .= $_SERVER['SERVER_NAME'].':'.(ZM_MIN_STREAMING_PORT+$this->{'MonitorId'}); } else { - $streamSrc .= $_SERVER['HTTP_HOST']; - } + $Server = new Server(); + } + $streamSrc .= $Server->UrlToIndex( + ZM_MIN_STREAMING_PORT ? + ZM_MIN_STREAMING_PORT+$this->{'MonitorId'} : + null); - $streamSrc .= ( ZM_BASE_PATH != '/' ? ZM_BASE_PATH : '' ).'/index.php'; $args['eid'] = $this->{'Id'}; $args['fid'] = 'snapshot'; $args['view'] = 'image'; @@ -358,10 +354,10 @@ class Event { if ( ZM_OPT_USE_AUTH ) { if ( ZM_AUTH_RELAY == 'hashed' ) { $args['auth'] = generateAuthHash(ZM_AUTH_HASH_IPS); - } elseif ( ZM_AUTH_RELAY == 'plain' ) { + } else if ( ZM_AUTH_RELAY == 'plain' ) { $args['user'] = $_SESSION['username']; $args['pass'] = $_SESSION['password']; - } elseif ( ZM_AUTH_RELAY == 'none' ) { + } else if ( ZM_AUTH_RELAY == 'none' ) { $args['user'] = $_SESSION['username']; } } @@ -576,7 +572,7 @@ class Event { $Server = $Storage->ServerId() ? $Storage->Server() : $this->Monitor()->Server(); if ( $Server->Id() != ZM_SERVER_ID ) { - $url = $Server->Url() . '/zm/api/events/'.$this->{'Id'}.'.json'; + $url = $Server->UrlToApi() . '/events/'.$this->{'Id'}.'.json'; if ( ZM_OPT_USE_AUTH ) { if ( ZM_AUTH_RELAY == 'hashed' ) { $url .= '?auth='.generateAuthHash( ZM_AUTH_HASH_IPS ); @@ -620,7 +616,7 @@ class Event { $Server = $Storage->ServerId() ? $Storage->Server() : $this->Monitor()->Server(); if ( $Server->Id() != ZM_SERVER_ID ) { - $url = $Server->Url() . '/zm/api/events/'.$this->{'Id'}.'.json'; + $url = $Server->UrlToApi() . '/events/'.$this->{'Id'}.'.json'; if ( ZM_OPT_USE_AUTH ) { if ( ZM_AUTH_RELAY == 'hashed' ) { $url .= '?auth='.generateAuthHash( ZM_AUTH_HASH_IPS ); diff --git a/web/includes/Group.php b/web/includes/Group.php index 6c8338a55..82c1daba9 100644 --- a/web/includes/Group.php +++ b/web/includes/Group.php @@ -120,12 +120,13 @@ class Group { } else { return null; } - } + } # end function find_one public function delete() { if ( array_key_exists('Id', $this) ) { - dbQuery( 'DELETE FROM Groups_Monitors WHERE GroupId=?', array($this->{'Id'}) ); - dbQuery( 'DELETE FROM Groups WHERE Id=?', array($this->{'Id'}) ); + dbQuery('DELETE FROM Groups_Monitors WHERE GroupId=?', array($this->{'Id'})); + dbQuery('UPDATE Groups SET ParentId=NULL WHERE ParentId=?', array($this->{'Id'})); + dbQuery('DELETE FROM Groups WHERE Id=?', array($this->{'Id'})); if ( isset($_COOKIE['zmGroup']) ) { if ( $this->{'Id'} == $_COOKIE['zmGroup'] ) { unset($_COOKIE['zmGroup']); @@ -150,16 +151,17 @@ class Group { $this->{$k} = $v; } } - } + } # end function set + public function depth( $new = null ) { if ( isset($new) ) { $this->{'depth'} = $new; } - if ( ! array_key_exists('depth', $this) or ($this->{'depth'} == null) ) { - $this->{'depth'} = 1; + if ( !array_key_exists('depth', $this) or ($this->{'depth'} === null) ) { + $this->{'depth'} = 0; if ( $this->{'ParentId'} != null ) { $Parent = Group::find_one(array('Id'=>$this->{'ParentId'})); - $this->{'depth'} += $Parent->depth(); + $this->{'depth'} += $Parent->depth()+1; } } return $this->{'depth'}; @@ -210,7 +212,7 @@ class Group { $children[$Group->ParentId()] = array(); $children[$Group->ParentId()][] = $Group; } - } + } # end foreach function get_options($Group) { global $children; @@ -221,7 +223,8 @@ class Group { } } return $options; - } + } # end function get_options + $group_options = array(); foreach ( $Groups as $id=>$Group ) { if ( ! $Group->ParentId() ) { @@ -229,7 +232,7 @@ class Group { } } return $group_options; - } + } # end function get_dropdown_options public static function get_group_sql($group_id) { $groupSql = ''; diff --git a/web/includes/Monitor.php b/web/includes/Monitor.php index 79e7a4082..718e590c4 100644 --- a/web/includes/Monitor.php +++ b/web/includes/Monitor.php @@ -284,21 +284,12 @@ private $control_fields = array( } } - public function getStreamSrc( $args, $querySep='&' ) { + public function getStreamSrc($args, $querySep='&') { - $streamSrc = ZM_BASE_PROTOCOL.'://'; - if ( isset($this->{'ServerId'}) and $this->{'ServerId'} ) { - $Server = new Server( $this->{'ServerId'} ); - $streamSrc .= $Server->Hostname(); - if ( ZM_MIN_STREAMING_PORT ) { - $streamSrc .= ':'.(ZM_MIN_STREAMING_PORT+$this->{'Id'}); - } - } else if ( ZM_MIN_STREAMING_PORT ) { - $streamSrc .= $_SERVER['SERVER_NAME'].':'.(ZM_MIN_STREAMING_PORT+$this->{'Id'}); - } else { - $streamSrc .= $_SERVER['HTTP_HOST']; - } - $streamSrc .= ZM_PATH_ZMS; + $streamSrc = $this->Server()->UrlToZMS( + ZM_MIN_STREAMING_PORT ? + ZM_MIN_STREAMING_PORT+$this->{'Id'} : + null); $args['monitor'] = $this->{'Id'}; @@ -319,9 +310,9 @@ private $control_fields = array( $args['rand'] = time(); } - $streamSrc .= '?'.http_build_query( $args,'', $querySep ); + $streamSrc .= '?'.http_build_query($args,'', $querySep); - return( $streamSrc ); + return $streamSrc; } // end function getStreamSrc public function Width($new = null) { @@ -464,7 +455,7 @@ private $control_fields = array( } else if ( $this->ServerId() ) { $Server = $this->Server(); - $url = ZM_BASE_PROTOCOL . '://'.$Server->Hostname().'/zm/api/monitors/'.$this->{'Id'}.'.json'; + $url = $Server->UrlToApi().'/monitors/daemonControl/'.$this->{'Id'}.'/'.$mode.'/zmc.json'; if ( ZM_OPT_USE_AUTH ) { if ( ZM_AUTH_RELAY == 'hashed' ) { $url .= '?auth='.generateAuthHash( ZM_AUTH_HASH_IPS ); @@ -476,17 +467,8 @@ private $control_fields = array( } } Logger::Debug("sending command to $url"); - $data = array('Monitor[Function]' => $this->{'Function'} ); - // use key 'http' even if you send the request to https://... - $options = array( - 'http' => array( - 'header' => "Content-type: application/x-www-form-urlencoded\r\n", - 'method' => 'POST', - 'content' => http_build_query($data) - ) - ); - $context = stream_context_create($options); + $context = stream_context_create(); try { $result = file_get_contents($url, false, $context); if ($result === FALSE) { /* Handle error */ @@ -522,6 +504,33 @@ private $control_fields = array( daemonControl( 'reload', 'zma', '-m '.$this->{'Id'} ); } } + } else if ( $this->ServerId() ) { + $Server = $this->Server(); + + $url = ZM_BASE_PROTOCOL . '://'.$Server->Hostname().'/zm/api/monitors/daemonControl/'.$this->{'Id'}.'/'.$mode.'/zma.json'; + if ( ZM_OPT_USE_AUTH ) { + if ( ZM_AUTH_RELAY == 'hashed' ) { + $url .= '?auth='.generateAuthHash( ZM_AUTH_HASH_IPS ); + } elseif ( ZM_AUTH_RELAY == 'plain' ) { + $url = '?user='.$_SESSION['username']; + $url = '?pass='.$_SESSION['password']; + } elseif ( ZM_AUTH_RELAY == 'none' ) { + $url = '?user='.$_SESSION['username']; + } + } + Logger::Debug("sending command to $url"); + + $context = stream_context_create(); + try { + $result = file_get_contents($url, false, $context); + if ($result === FALSE) { /* Handle error */ + Error("Error restarting zma using $url"); + } + } catch ( Exception $e ) { + Error("Except $e thrown trying to restart zma"); + } + } else { + Error("Server not assigned to Monitor in a multi-server setup. Please assign a server to the Monitor."); } // end if we are on the recording server } // end public function zmaControl @@ -604,13 +613,15 @@ private $control_fields = array( $source = preg_replace( '/^.*\//', '', $this->{'Path'} ); } elseif ( $this->{'Type'} == 'Ffmpeg' || $this->{'Type'} == 'Libvlc' || $this->{'Type'} == 'WebSite' ) { $url_parts = parse_url( $this->{'Path'} ); - if ( ZM_WEB_FILTER_SOURCE == 'Hostname' ) { # Filter out everything but the hostname + if ( ZM_WEB_FILTER_SOURCE == 'Hostname' ) { + # Filter out everything but the hostname if ( isset($url_parts['host']) ) { $source = $url_parts['host']; } else { $source = $this->{'Path'}; } - } elseif ( ZM_WEB_FILTER_SOURCE == "NoCredentials" ) { # Filter out sensitive and common items + } elseif ( ZM_WEB_FILTER_SOURCE == "NoCredentials" ) { + # Filter out sensitive and common items unset($url_parts['user']); unset($url_parts['pass']); #unset($url_parts['scheme']); @@ -629,8 +640,8 @@ private $control_fields = array( return $source; } // end function Source - public function Url() { - return $this->Server()->Url( ZM_MIN_STREAMING_PORT ? (ZM_MIN_STREAMING_PORT+$this->Id()) : null ); + public function UrlToIndex() { + return $this->Server()->UrlToIndex(ZM_MIN_STREAMING_PORT ? (ZM_MIN_STREAMING_PORT+$this->Id()) : null); } } // end class Monitor diff --git a/web/includes/Server.php b/web/includes/Server.php index 09d415e63..7424aafdf 100644 --- a/web/includes/Server.php +++ b/web/includes/Server.php @@ -5,23 +5,27 @@ $server_cache = array(); class Server { private $defaults = array( - 'Id' => null, - 'Name' => '', - 'Hostname' => '', - 'zmaudit' => 1, - 'zmstats' => 1, - 'zmtrigger' => 0, + 'Id' => null, + 'Name' => '', + 'Protocol' => '', + 'Hostname' => '', + 'Port' => null, + 'PathToIndex' => '/zm/index.php', + 'PathToZMS' => ZM_PATH_ZMS, + 'PathToApi' => '/zm/api', + 'zmaudit' => 1, + 'zmstats' => 1, + 'zmtrigger' => 0, ); - - public function __construct( $IdOrRow = NULL ) { - global $server_cache; + public function __construct($IdOrRow = NULL) { + global $server_cache; $row = NULL; if ( $IdOrRow ) { if ( is_integer($IdOrRow) or ctype_digit($IdOrRow) ) { $row = dbFetchOne('SELECT * FROM Servers WHERE Id=?', NULL, array($IdOrRow)); if ( !$row ) { - Error("Unable to load Server record for Id=" . $IdOrRow); + Error('Unable to load Server record for Id='.$IdOrRow); } } elseif ( is_array($IdOrRow) ) { $row = $IdOrRow; @@ -33,32 +37,105 @@ class Server { } $server_cache[$row['Id']] = $this; } else { - $this->{'Name'} = ''; - $this->{'Hostname'} = ''; + # Set defaults + foreach ( $this->defaults as $k => $v ) $this->{$k} = $v; } } + public function Hostname( $new = null ) { + if ( $new != null ) + $this->{'Hostname'} = $new; + + if ( isset( $this->{'Hostname'}) and ( $this->{'Hostname'} != '' ) ) { + return $this->{'Hostname'}; + } else if ( $this->Id() ) { + return $this->{'Name'}; + } + $result = explode(':',$_SERVER['HTTP_HOST']); + return $result[0]; + } + + public function Protocol( $new = null ) { + if ( $new != null ) + $this->{'Protocol'} = $new; + + if ( isset($this->{'Protocol'}) and ( $this->{'Protocol'} != '' ) ) { + return $this->{'Protocol'}; + } + + return ( + ( isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ) + or + ( isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and ( $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' ) ) + ) ? 'https' : 'http'; + } + + public function Port( $new = '' ) { + if ( $new != '' ) + $this->{'Port'} = $new; + + if ( isset($this->{'Port'}) and $this->{'Port'} ) { + return $this->{'Port'}; + } + + if ( isset($_SERVER['HTTP_X_FORWARDED_PORT']) ) { + return $_SERVER['HTTP_X_FORWARDED_PORT']; + } + + return $_SERVER['SERVER_PORT']; + } + + public function PathToZMS( $new = null ) { + if ( $new != null ) + $this{'PathToZMS'} = $new; + if ( $this->Id() and $this->{'PathToZMS'} ) { + return $this->{'PathToZMS'}; + } else { + return ZM_PATH_ZMS; + } + } + + public function UrlToZMS( $port = null ) { + return $this->Url($port).$this->PathToZMS(); + } + public function Url( $port = null ) { - $url = ZM_BASE_PROTOCOL . '://'; - if ( $this->Id() ) { - $url .= $this->Hostname(); - } else { - $url .= $_SERVER['SERVER_NAME']; - } + $url = $this->Protocol().'://'; + $url .= $this->Hostname(); if ( $port ) { $url .= ':'.$port; } else { - $url .= ':'.$_SERVER['SERVER_PORT']; + $url .= ':'.$this->Port(); } - $url .= $_SERVER['PHP_SELF']; return $url; } - public function Hostname() { - if ( isset( $this->{'Hostname'} ) and ( $this->{'Hostname'} != '' ) ) { - return $this->{'Hostname'}; - } - return $this->{'Name'}; - } + + public function PathToIndex( $new = null ) { + if ( $new != null ) + $this->{'PathToIndex'} = $new; + + if ( isset($this->{'PathToIndex'}) and $this->{'PathToIndex'} ) { + return $this->{'PathToIndex'}; + } + return $_SERVER['PHP_SELF']; + } + + public function UrlToIndex( $port=null ) { + return $this->Url($port).$this->PathToIndex(); + } + public function UrlToApi( $port=null ) { + return $this->Url($port).$this->PathToApi(); + } + public function PathToApi( $new = null ) { + if ( $new != null ) + $this->{'PathToApi'} = $new; + + if ( isset($this->{'PathToApi'}) and $this->{'PathToApi'} ) { + return $this->{'PathToApi'}; + } + return '/zm/api'; + } + public function __call($fn, array $args){ if ( count($args) ) { $this->{$fn} = $args[0]; @@ -66,13 +143,13 @@ class Server { if ( array_key_exists($fn, $this) ) { return $this->{$fn}; } else { - if ( array_key_exists( $fn, $this->defaults ) ) { + if ( array_key_exists($fn, $this->defaults) ) { return $this->defaults{$fn}; } else { $backTrace = debug_backtrace(); $file = $backTrace[1]['file']; $line = $backTrace[1]['line']; - Warning( "Unknown function call Server->$fn from $file:$line" ); + Warning("Unknown function call Server->$fn from $file:$line"); } } } @@ -117,7 +194,7 @@ class Server { } $results = dbFetchAll( $sql, NULL, $values ); if ( $results ) { - return array_map( function($id){ return new Server($id); }, $results ); + return array_map(function($id){ return new Server($id); }, $results); } return array(); } @@ -137,5 +214,5 @@ class Server { return $results[0]; } -} +} # end class Server ?> diff --git a/web/includes/actions.php b/web/includes/actions.php index e33339bc3..9ff6fbc49 100644 --- a/web/includes/actions.php +++ b/web/includes/actions.php @@ -474,7 +474,8 @@ if ( canEdit('Monitors') ) { $maxSeq = dbFetchOne('SELECT MAX(Sequence) AS MaxSequence FROM Monitors', 'MaxSequence'); $changes[] = 'Sequence = '.($maxSeq+1); - if ( dbQuery('INSERT INTO Monitors SET '.implode(', ', $changes)) ) { + $sql = 'INSERT INTO Monitors SET '.implode(', ', $changes); + if ( dbQuery($sql) ) { $mid = dbInsertId(); $zoneArea = $_REQUEST['newMonitor']['Width'] * $_REQUEST['newMonitor']['Height']; dbQuery("INSERT INTO Zones SET MonitorId = ?, Name = 'All', Type = 'Active', Units = 'Percent', NumCoords = 4, Coords = ?, Area=?, AlarmRGB = 0xff0000, CheckMethod = 'Blobs', MinPixelThreshold = 25, MinAlarmPixels=?, MaxAlarmPixels=?, FilterX = 3, FilterY = 3, MinFilterPixels=?, MaxFilterPixels=?, MinBlobPixels=?, MinBlobs = 1", array( $mid, sprintf( "%d,%d %d,%d %d,%d %d,%d", 0, 0, $_REQUEST['newMonitor']['Width']-1, 0, $_REQUEST['newMonitor']['Width']-1, $_REQUEST['newMonitor']['Height']-1, 0, $_REQUEST['newMonitor']['Height']-1 ), $zoneArea, intval(($zoneArea*3)/100), intval(($zoneArea*75)/100), intval(($zoneArea*3)/100), intval(($zoneArea*75)/100), intval(($zoneArea*2)/100) ) ); @@ -486,6 +487,7 @@ if ( canEdit('Monitors') ) { } else { Error('Error saving new Monitor.'); + $error_message = dbError($sql); return; } } else { @@ -537,7 +539,7 @@ if ( canEdit('Monitors') ) { $new_monitor = new Monitor($mid); //fixDevices(); - if ( $monitor['Type'] != 'WebSite' ) { + if ( $new_monitor->Type() != 'WebSite' ) { $new_monitor->zmcControl('start'); $new_monitor->zmaControl('start'); } @@ -640,16 +642,11 @@ if ( canEdit('Groups') ) { $refreshParent = true; } else if ( $action == 'delete' ) { if ( !empty($_REQUEST['gid']) ) { - if ( is_array($_REQUEST['gid']) ) { - foreach ( $_REQUEST['gid'] as $gid ) { - $Group = new Group($gid); - $Group->delete(); - } - } else { - $Group = new Group($_REQUEST['gid'] ); + foreach ( Group::find(array('Id'=>$_REQUEST['gid'])) as $Group ) { $Group->delete(); } } + $redirect = ZM_BASE_URL.$_SERVER['PHP_SELF'].'?view=groups'; $refreshParent = true; } # end if action } // end if can edit groups @@ -831,6 +828,22 @@ if ( canEdit('System') ) { return; } if ( $action == 'options' && isset($_REQUEST['tab']) ) { + $config = array(); + $configCat = array(); + $configCats = array(); + + $result = $dbConn->query('SELECT * FROM Config ORDER BY Id ASC'); + if ( !$result ) + echo mysql_error(); + while( $row = dbFetchNext($result) ) { + $config[$row['Name']] = $row; + if ( !($configCat = &$configCats[$row['Category']]) ) { + $configCats[$row['Category']] = array(); + $configCat = &$configCats[$row['Category']]; + } + $configCat[$row['Name']] = $row; + } + $configCat = $configCats[$_REQUEST['tab']]; $changed = false; foreach ( $configCat as $name=>$value ) { diff --git a/web/includes/functions.php b/web/includes/functions.php index 3bce551bb..70fc37998 100644 --- a/web/includes/functions.php +++ b/web/includes/functions.php @@ -53,7 +53,7 @@ function CORSHeaders() { preg_match('/^(https?:\/\/)?'.preg_quote($Server->Name(),'/').'/i', $_SERVER['HTTP_ORIGIN']) ) { $valid = true; - Logger::Debug("Setting Access-Controll-Allow-Origin from " . $_SERVER['HTTP_ORIGIN']); + Logger::Debug("Setting Access-Control-Allow-Origin from " . $_SERVER['HTTP_ORIGIN']); header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']); header('Access-Control-Allow-Headers: x-requested-with,x-request'); break; @@ -2282,7 +2282,6 @@ function unparse_url($parsed_url, $substitutions = array() ) { // PP - POST request handler for PHP which does not need extensions // credit: http://wezfurlong.org/blog/2006/nov/http-post-from-php-without-curl/ - function do_request($method, $url, $data=array(), $optional_headers = null) { global $php_errormsg; @@ -2326,7 +2325,7 @@ function do_post_request($url, $data, $optional_headers = null) { } // The following works around php not being built with semaphore functions. -if (!function_exists('sem_get')) { +if ( !function_exists('sem_get') ) { function sem_get($key) { return fopen(__FILE__ . '.sem.' . $key, 'w+'); } @@ -2338,7 +2337,7 @@ if (!function_exists('sem_get')) { } } -if( !function_exists('ftok') ) { +if ( !function_exists('ftok') ) { function ftok($filename = "", $proj = "") { if ( empty($filename) || !file_exists($filename) ) { return -1; diff --git a/web/includes/lang.php b/web/includes/lang.php index 0c40e7f60..38f1179d8 100644 --- a/web/includes/lang.php +++ b/web/includes/lang.php @@ -1,6 +1,6 @@ $value) { - if ( ! array_key_exists( $key, $SLANG ) ) - $SLANG[$key] = $DLANG[$key]; - } + require_once($langFile); + require_once('lang/default.php'); + foreach ($DLANG as $key => $value) { + if ( ! array_key_exists($key, $SLANG) ) + $SLANG[$key] = $DLANG[$key]; + } } - // // Date and time formats fallback, if not set up by the language file already // -defined("DATE_FMT_CONSOLE_LONG") or define("DATE_FMT_CONSOLE_LONG", "D jS M, g:ia"); // This is the main console date/time, date() or strftime() format -defined("DATE_FMT_CONSOLE_SHORT") or define( "DATE_FMT_CONSOLE_SHORT", "%H:%M" ); // This is the xHTML console date/time, date() or strftime() format +defined('DATE_FMT_CONSOLE_LONG') or define('DATE_FMT_CONSOLE_LONG', 'D jS M, g:ia'); // This is the main console date/time, date() or strftime() format +defined('DATE_FMT_CONSOLE_SHORT') or define('DATE_FMT_CONSOLE_SHORT', '%H:%M'); // This is the xHTML console date/time, date() or strftime() format -defined("STRF_FMT_DATETIME") or define( "STRF_FMT_DATETIME", "%c" ); // Strftime locale aware format for dates with times -defined("STRF_FMT_DATE") or define( "STRF_FMT_DATE", "%x" ); // Strftime locale aware format for dates without times -defined("STRF_FMT_TIME") or define( "STRF_FMT_TIME", "%X" ); // Strftime locale aware format for times without dates +defined('STRF_FMT_DATETIME') or define('STRF_FMT_DATETIME', '%c'); // Strftime locale aware format for dates with times +defined('STRF_FMT_DATE') or define('STRF_FMT_DATE', '%x'); // Strftime locale aware format for dates without times +defined('STRF_FMT_TIME') or define('STRF_FMT_TIME', '%X'); // Strftime locale aware format for times without dates -defined("STRF_FMT_DATETIME_SHORT") or define( "STRF_FMT_DATETIME_SHORT", "%y/%m/%d %H:%M:%S" ); // Strftime shorter format for dates with time, not locale aware -defined("STRF_FMT_DATETIME_SHORTER") or define( "STRF_FMT_DATETIME_SHORTER", "%m/%d %H:%M:%S" );// Strftime shorter format for dates with time, not locale aware, used where space is tight +defined('STRF_FMT_DATETIME_SHORT') or define('STRF_FMT_DATETIME_SHORT', '%y/%m/%d %H:%M:%S'); // Strftime shorter format for dates with time, not locale aware +defined('STRF_FMT_DATETIME_SHORTER') or define('STRF_FMT_DATETIME_SHORTER','%m/%d %H:%M:%S');// Strftime shorter format for dates with time, not locale aware, used where space is tight ?> diff --git a/web/index.php b/web/index.php index 6e21cc0c6..ef5ea2a1e 100644 --- a/web/index.php +++ b/web/index.php @@ -169,6 +169,7 @@ if ( !is_writable(ZM_DIR_EVENTS) || !is_writable(ZM_DIR_IMAGES) ) { } # Globals +$error_message = null; $redirect = null; $view = null; if ( isset($_REQUEST['view']) ) diff --git a/web/js/Server.js b/web/js/Server.js new file mode 100644 index 000000000..61d1ff713 --- /dev/null +++ b/web/js/Server.js @@ -0,0 +1,28 @@ +'use strict'; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Server = function () { + function Server(json) { + _classCallCheck(this, Server); + + for (var k in json) { + this[k] = json[k]; + } + } + + _createClass(Server, [{ + key: 'url', + value: function url() { + var 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 Server; +}(); + +; diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index 7b2addeb4..78adecb75 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -586,6 +586,9 @@ $SLANG = array( 'Parameter' => 'Parameter', 'Password' => 'Password', 'PasswordsDifferent' => 'The new and confirm passwords are different', + 'PathToIndex' => 'Path To Index', + 'PathToZMS' => 'Path To ZMS', + 'PathToApi' => 'Path To Api', 'Paths' => 'Paths', 'Pause' => 'Pause', 'PhoneBW' => 'Phone B/W', diff --git a/web/skins/classic/css/base/jquery-ui-theme.css b/web/skins/classic/css/base/jquery-ui-theme.css deleted file mode 100644 index c50986c55..000000000 --- a/web/skins/classic/css/base/jquery-ui-theme.css +++ /dev/null @@ -1,410 +0,0 @@ -/*! - * jQuery UI CSS Framework 1.11.3 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - * - * http://api.jqueryui.com/category/theming/ - * - * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Arial%2CHelvetica%2Csans-serif&fsDefault=1em&fwDefault=normal&cornerRadius=3px&bgColorHeader=e9e9e9&bgTextureHeader=flat&borderColorHeader=dddddd&fcHeader=333333&iconColorHeader=444444&bgColorContent=ffffff&bgTextureContent=flat&borderColorContent=dddddd&fcContent=333333&iconColorContent=444444&bgColorDefault=f6f6f6&bgTextureDefault=flat&borderColorDefault=c5c5c5&fcDefault=454545&iconColorDefault=777777&bgColorHover=ededed&bgTextureHover=flat&borderColorHover=cccccc&fcHover=2b2b2b&iconColorHover=555555&bgColorActive=007fff&bgTextureActive=flat&borderColorActive=003eff&fcActive=ffffff&iconColorActive=ffffff&bgColorHighlight=fffa90&bgTextureHighlight=flat&borderColorHighlight=dad55e&fcHighlight=777620&iconColorHighlight=777620&bgColorError=fddfdf&bgTextureError=flat&borderColorError=f1a899&fcError=5f3f3f&iconColorError=cc0000&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=666666&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=5px&offsetTopShadow=0px&offsetLeftShadow=0px&cornerRadiusShadow=8px - */ - - -/* Component containers -----------------------------------*/ -.ui-widget { - font-family: Arial,Helvetica,sans-serif; - font-size: 1em; -} -.ui-widget .ui-widget { - font-size: 1em; -} -.ui-widget input, -.ui-widget select, -.ui-widget textarea, -.ui-widget button { - font-family: Arial,Helvetica,sans-serif; - font-size: 1em; -} -.ui-widget-content { - border: 1px solid #dddddd; - background: #ffffff; - color: #333333; -} -.ui-widget-content a { - color: #333333; -} -.ui-widget-header { - border: 1px solid #dddddd; - background: #e9e9e9; - color: #333333; - font-weight: bold; -} -.ui-widget-header a { - color: #333333; -} - -/* Interaction states -----------------------------------*/ -.ui-state-default, -.ui-widget-content .ui-state-default, -.ui-widget-header .ui-state-default { - border: 1px solid #c5c5c5; - background: #f6f6f6; - font-weight: normal; - color: #454545; -} -.ui-state-default a, -.ui-state-default a:link, -.ui-state-default a:visited { - color: #454545; - text-decoration: none; -} -.ui-state-hover, -.ui-widget-content .ui-state-hover, -.ui-widget-header .ui-state-hover, -.ui-state-focus, -.ui-widget-content .ui-state-focus, -.ui-widget-header .ui-state-focus { - border: 1px solid #cccccc; - background: #ededed; - font-weight: normal; - color: #2b2b2b; -} -.ui-state-hover a, -.ui-state-hover a:hover, -.ui-state-hover a:link, -.ui-state-hover a:visited, -.ui-state-focus a, -.ui-state-focus a:hover, -.ui-state-focus a:link, -.ui-state-focus a:visited { - color: #2b2b2b; - text-decoration: none; -} -.ui-state-active, -.ui-widget-content .ui-state-active, -.ui-widget-header .ui-state-active { - border: 1px solid #003eff; - background: #007fff; - font-weight: normal; - color: #ffffff; -} -.ui-state-active a, -.ui-state-active a:link, -.ui-state-active a:visited { - color: #ffffff; - text-decoration: none; -} - -/* Interaction Cues -----------------------------------*/ -.ui-state-highlight, -.ui-widget-content .ui-state-highlight, -.ui-widget-header .ui-state-highlight { - border: 1px solid #dad55e; - background: #fffa90; - color: #777620; -} -.ui-state-highlight a, -.ui-widget-content .ui-state-highlight a, -.ui-widget-header .ui-state-highlight a { - color: #777620; -} -.ui-state-error, -.ui-widget-content .ui-state-error, -.ui-widget-header .ui-state-error { - border: 1px solid #f1a899; - background: #fddfdf; - color: #5f3f3f; -} -.ui-state-error a, -.ui-widget-content .ui-state-error a, -.ui-widget-header .ui-state-error a { - color: #5f3f3f; -} -.ui-state-error-text, -.ui-widget-content .ui-state-error-text, -.ui-widget-header .ui-state-error-text { - color: #5f3f3f; -} -.ui-priority-primary, -.ui-widget-content .ui-priority-primary, -.ui-widget-header .ui-priority-primary { - font-weight: bold; -} -.ui-priority-secondary, -.ui-widget-content .ui-priority-secondary, -.ui-widget-header .ui-priority-secondary { - opacity: .7; - filter:Alpha(Opacity=70); /* support: IE8 */ - font-weight: normal; -} -.ui-state-disabled, -.ui-widget-content .ui-state-disabled, -.ui-widget-header .ui-state-disabled { - opacity: .35; - filter:Alpha(Opacity=35); /* support: IE8 */ - background-image: none; -} -.ui-state-disabled .ui-icon { - filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */ -} - -/* Icons -----------------------------------*/ - -/* states and images */ -.ui-icon { - width: 16px; - height: 16px; -} -.ui-icon, -.ui-widget-content .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_444444_256x240.png"); -} -.ui-widget-header .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_444444_256x240.png"); -} -.ui-state-default .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_777777_256x240.png"); -} -.ui-state-hover .ui-icon, -.ui-state-focus .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_555555_256x240.png"); -} -.ui-state-active .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_ffffff_256x240.png"); -} -.ui-state-highlight .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_777620_256x240.png"); -} -.ui-state-error .ui-icon, -.ui-state-error-text .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_cc0000_256x240.png"); -} - -/* positioning */ -.ui-icon-blank { background-position: 16px 16px; } -.ui-icon-carat-1-n { background-position: 0 0; } -.ui-icon-carat-1-ne { background-position: -16px 0; } -.ui-icon-carat-1-e { background-position: -32px 0; } -.ui-icon-carat-1-se { background-position: -48px 0; } -.ui-icon-carat-1-s { background-position: -64px 0; } -.ui-icon-carat-1-sw { background-position: -80px 0; } -.ui-icon-carat-1-w { background-position: -96px 0; } -.ui-icon-carat-1-nw { background-position: -112px 0; } -.ui-icon-carat-2-n-s { background-position: -128px 0; } -.ui-icon-carat-2-e-w { background-position: -144px 0; } -.ui-icon-triangle-1-n { background-position: 0 -16px; } -.ui-icon-triangle-1-ne { background-position: -16px -16px; } -.ui-icon-triangle-1-e { background-position: -32px -16px; } -.ui-icon-triangle-1-se { background-position: -48px -16px; } -.ui-icon-triangle-1-s { background-position: -64px -16px; } -.ui-icon-triangle-1-sw { background-position: -80px -16px; } -.ui-icon-triangle-1-w { background-position: -96px -16px; } -.ui-icon-triangle-1-nw { background-position: -112px -16px; } -.ui-icon-triangle-2-n-s { background-position: -128px -16px; } -.ui-icon-triangle-2-e-w { background-position: -144px -16px; } -.ui-icon-arrow-1-n { background-position: 0 -32px; } -.ui-icon-arrow-1-ne { background-position: -16px -32px; } -.ui-icon-arrow-1-e { background-position: -32px -32px; } -.ui-icon-arrow-1-se { background-position: -48px -32px; } -.ui-icon-arrow-1-s { background-position: -64px -32px; } -.ui-icon-arrow-1-sw { background-position: -80px -32px; } -.ui-icon-arrow-1-w { background-position: -96px -32px; } -.ui-icon-arrow-1-nw { background-position: -112px -32px; } -.ui-icon-arrow-2-n-s { background-position: -128px -32px; } -.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } -.ui-icon-arrow-2-e-w { background-position: -160px -32px; } -.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } -.ui-icon-arrowstop-1-n { background-position: -192px -32px; } -.ui-icon-arrowstop-1-e { background-position: -208px -32px; } -.ui-icon-arrowstop-1-s { background-position: -224px -32px; } -.ui-icon-arrowstop-1-w { background-position: -240px -32px; } -.ui-icon-arrowthick-1-n { background-position: 0 -48px; } -.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } -.ui-icon-arrowthick-1-e { background-position: -32px -48px; } -.ui-icon-arrowthick-1-se { background-position: -48px -48px; } -.ui-icon-arrowthick-1-s { background-position: -64px -48px; } -.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } -.ui-icon-arrowthick-1-w { background-position: -96px -48px; } -.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } -.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } -.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } -.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } -.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } -.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } -.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } -.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } -.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } -.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } -.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } -.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } -.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } -.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } -.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } -.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } -.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } -.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } -.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } -.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } -.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } -.ui-icon-arrow-4 { background-position: 0 -80px; } -.ui-icon-arrow-4-diag { background-position: -16px -80px; } -.ui-icon-extlink { background-position: -32px -80px; } -.ui-icon-newwin { background-position: -48px -80px; } -.ui-icon-refresh { background-position: -64px -80px; } -.ui-icon-shuffle { background-position: -80px -80px; } -.ui-icon-transfer-e-w { background-position: -96px -80px; } -.ui-icon-transferthick-e-w { background-position: -112px -80px; } -.ui-icon-folder-collapsed { background-position: 0 -96px; } -.ui-icon-folder-open { background-position: -16px -96px; } -.ui-icon-document { background-position: -32px -96px; } -.ui-icon-document-b { background-position: -48px -96px; } -.ui-icon-note { background-position: -64px -96px; } -.ui-icon-mail-closed { background-position: -80px -96px; } -.ui-icon-mail-open { background-position: -96px -96px; } -.ui-icon-suitcase { background-position: -112px -96px; } -.ui-icon-comment { background-position: -128px -96px; } -.ui-icon-person { background-position: -144px -96px; } -.ui-icon-print { background-position: -160px -96px; } -.ui-icon-trash { background-position: -176px -96px; } -.ui-icon-locked { background-position: -192px -96px; } -.ui-icon-unlocked { background-position: -208px -96px; } -.ui-icon-bookmark { background-position: -224px -96px; } -.ui-icon-tag { background-position: -240px -96px; } -.ui-icon-home { background-position: 0 -112px; } -.ui-icon-flag { background-position: -16px -112px; } -.ui-icon-calendar { background-position: -32px -112px; } -.ui-icon-cart { background-position: -48px -112px; } -.ui-icon-pencil { background-position: -64px -112px; } -.ui-icon-clock { background-position: -80px -112px; } -.ui-icon-disk { background-position: -96px -112px; } -.ui-icon-calculator { background-position: -112px -112px; } -.ui-icon-zoomin { background-position: -128px -112px; } -.ui-icon-zoomout { background-position: -144px -112px; } -.ui-icon-search { background-position: -160px -112px; } -.ui-icon-wrench { background-position: -176px -112px; } -.ui-icon-gear { background-position: -192px -112px; } -.ui-icon-heart { background-position: -208px -112px; } -.ui-icon-star { background-position: -224px -112px; } -.ui-icon-link { background-position: -240px -112px; } -.ui-icon-cancel { background-position: 0 -128px; } -.ui-icon-plus { background-position: -16px -128px; } -.ui-icon-plusthick { background-position: -32px -128px; } -.ui-icon-minus { background-position: -48px -128px; } -.ui-icon-minusthick { background-position: -64px -128px; } -.ui-icon-close { background-position: -80px -128px; } -.ui-icon-closethick { background-position: -96px -128px; } -.ui-icon-key { background-position: -112px -128px; } -.ui-icon-lightbulb { background-position: -128px -128px; } -.ui-icon-scissors { background-position: -144px -128px; } -.ui-icon-clipboard { background-position: -160px -128px; } -.ui-icon-copy { background-position: -176px -128px; } -.ui-icon-contact { background-position: -192px -128px; } -.ui-icon-image { background-position: -208px -128px; } -.ui-icon-video { background-position: -224px -128px; } -.ui-icon-script { background-position: -240px -128px; } -.ui-icon-alert { background-position: 0 -144px; } -.ui-icon-info { background-position: -16px -144px; } -.ui-icon-notice { background-position: -32px -144px; } -.ui-icon-help { background-position: -48px -144px; } -.ui-icon-check { background-position: -64px -144px; } -.ui-icon-bullet { background-position: -80px -144px; } -.ui-icon-radio-on { background-position: -96px -144px; } -.ui-icon-radio-off { background-position: -112px -144px; } -.ui-icon-pin-w { background-position: -128px -144px; } -.ui-icon-pin-s { background-position: -144px -144px; } -.ui-icon-play { background-position: 0 -160px; } -.ui-icon-pause { background-position: -16px -160px; } -.ui-icon-seek-next { background-position: -32px -160px; } -.ui-icon-seek-prev { background-position: -48px -160px; } -.ui-icon-seek-end { background-position: -64px -160px; } -.ui-icon-seek-start { background-position: -80px -160px; } -/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ -.ui-icon-seek-first { background-position: -80px -160px; } -.ui-icon-stop { background-position: -96px -160px; } -.ui-icon-eject { background-position: -112px -160px; } -.ui-icon-volume-off { background-position: -128px -160px; } -.ui-icon-volume-on { background-position: -144px -160px; } -.ui-icon-power { background-position: 0 -176px; } -.ui-icon-signal-diag { background-position: -16px -176px; } -.ui-icon-signal { background-position: -32px -176px; } -.ui-icon-battery-0 { background-position: -48px -176px; } -.ui-icon-battery-1 { background-position: -64px -176px; } -.ui-icon-battery-2 { background-position: -80px -176px; } -.ui-icon-battery-3 { background-position: -96px -176px; } -.ui-icon-circle-plus { background-position: 0 -192px; } -.ui-icon-circle-minus { background-position: -16px -192px; } -.ui-icon-circle-close { background-position: -32px -192px; } -.ui-icon-circle-triangle-e { background-position: -48px -192px; } -.ui-icon-circle-triangle-s { background-position: -64px -192px; } -.ui-icon-circle-triangle-w { background-position: -80px -192px; } -.ui-icon-circle-triangle-n { background-position: -96px -192px; } -.ui-icon-circle-arrow-e { background-position: -112px -192px; } -.ui-icon-circle-arrow-s { background-position: -128px -192px; } -.ui-icon-circle-arrow-w { background-position: -144px -192px; } -.ui-icon-circle-arrow-n { background-position: -160px -192px; } -.ui-icon-circle-zoomin { background-position: -176px -192px; } -.ui-icon-circle-zoomout { background-position: -192px -192px; } -.ui-icon-circle-check { background-position: -208px -192px; } -.ui-icon-circlesmall-plus { background-position: 0 -208px; } -.ui-icon-circlesmall-minus { background-position: -16px -208px; } -.ui-icon-circlesmall-close { background-position: -32px -208px; } -.ui-icon-squaresmall-plus { background-position: -48px -208px; } -.ui-icon-squaresmall-minus { background-position: -64px -208px; } -.ui-icon-squaresmall-close { background-position: -80px -208px; } -.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } -.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } -.ui-icon-grip-solid-vertical { background-position: -32px -224px; } -.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } -.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } -.ui-icon-grip-diagonal-se { background-position: -80px -224px; } - - -/* Misc visuals -----------------------------------*/ - -/* Corner radius */ -.ui-corner-all, -.ui-corner-top, -.ui-corner-left, -.ui-corner-tl { - border-top-left-radius: 3px; -} -.ui-corner-all, -.ui-corner-top, -.ui-corner-right, -.ui-corner-tr { - border-top-right-radius: 3px; -} -.ui-corner-all, -.ui-corner-bottom, -.ui-corner-left, -.ui-corner-bl { - border-bottom-left-radius: 3px; -} -.ui-corner-all, -.ui-corner-bottom, -.ui-corner-right, -.ui-corner-br { - border-bottom-right-radius: 3px; -} - -/* Overlays */ -.ui-widget-overlay { - background: #aaaaaa; - opacity: .3; - filter: Alpha(Opacity=30); /* support: IE8 */ -} -.ui-widget-shadow { - margin: 0px 0 0 0px; - padding: 5px; - background: #666666; - opacity: .3; - filter: Alpha(Opacity=30); /* support: IE8 */ - border-radius: 8px; -} diff --git a/web/skins/classic/css/base/skin.css b/web/skins/classic/css/base/skin.css index 3fdd5a12f..069952d40 100644 --- a/web/skins/classic/css/base/skin.css +++ b/web/skins/classic/css/base/skin.css @@ -150,6 +150,7 @@ input,textarea,select,button,.btn-primary { font-weight: 400; font-size: 100%; color: #333333; + background-color: #f8f8f8; text-align: left; border-radius:4px; } @@ -341,17 +342,17 @@ fieldset > legend { /* * Behavior classes */ -.alarm, .errorText, .error { - color: #ff3f34; -} -.alert, .warnText, .warning { - color: #ffa801; -} .ok, .infoText { color: #0fb9b1; } +.alert, .warnText, .warning, .disabledText { + color: #ffa801; +} +.alarm, .errorText, .error { + color: #ff3f34; +} .fakelink { color: #7f7fb2; diff --git a/web/skins/classic/css/classic/jquery-ui-theme.css b/web/skins/classic/css/classic/jquery-ui-theme.css deleted file mode 100644 index c50986c55..000000000 --- a/web/skins/classic/css/classic/jquery-ui-theme.css +++ /dev/null @@ -1,410 +0,0 @@ -/*! - * jQuery UI CSS Framework 1.11.3 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - * - * http://api.jqueryui.com/category/theming/ - * - * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Arial%2CHelvetica%2Csans-serif&fsDefault=1em&fwDefault=normal&cornerRadius=3px&bgColorHeader=e9e9e9&bgTextureHeader=flat&borderColorHeader=dddddd&fcHeader=333333&iconColorHeader=444444&bgColorContent=ffffff&bgTextureContent=flat&borderColorContent=dddddd&fcContent=333333&iconColorContent=444444&bgColorDefault=f6f6f6&bgTextureDefault=flat&borderColorDefault=c5c5c5&fcDefault=454545&iconColorDefault=777777&bgColorHover=ededed&bgTextureHover=flat&borderColorHover=cccccc&fcHover=2b2b2b&iconColorHover=555555&bgColorActive=007fff&bgTextureActive=flat&borderColorActive=003eff&fcActive=ffffff&iconColorActive=ffffff&bgColorHighlight=fffa90&bgTextureHighlight=flat&borderColorHighlight=dad55e&fcHighlight=777620&iconColorHighlight=777620&bgColorError=fddfdf&bgTextureError=flat&borderColorError=f1a899&fcError=5f3f3f&iconColorError=cc0000&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=666666&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=5px&offsetTopShadow=0px&offsetLeftShadow=0px&cornerRadiusShadow=8px - */ - - -/* Component containers -----------------------------------*/ -.ui-widget { - font-family: Arial,Helvetica,sans-serif; - font-size: 1em; -} -.ui-widget .ui-widget { - font-size: 1em; -} -.ui-widget input, -.ui-widget select, -.ui-widget textarea, -.ui-widget button { - font-family: Arial,Helvetica,sans-serif; - font-size: 1em; -} -.ui-widget-content { - border: 1px solid #dddddd; - background: #ffffff; - color: #333333; -} -.ui-widget-content a { - color: #333333; -} -.ui-widget-header { - border: 1px solid #dddddd; - background: #e9e9e9; - color: #333333; - font-weight: bold; -} -.ui-widget-header a { - color: #333333; -} - -/* Interaction states -----------------------------------*/ -.ui-state-default, -.ui-widget-content .ui-state-default, -.ui-widget-header .ui-state-default { - border: 1px solid #c5c5c5; - background: #f6f6f6; - font-weight: normal; - color: #454545; -} -.ui-state-default a, -.ui-state-default a:link, -.ui-state-default a:visited { - color: #454545; - text-decoration: none; -} -.ui-state-hover, -.ui-widget-content .ui-state-hover, -.ui-widget-header .ui-state-hover, -.ui-state-focus, -.ui-widget-content .ui-state-focus, -.ui-widget-header .ui-state-focus { - border: 1px solid #cccccc; - background: #ededed; - font-weight: normal; - color: #2b2b2b; -} -.ui-state-hover a, -.ui-state-hover a:hover, -.ui-state-hover a:link, -.ui-state-hover a:visited, -.ui-state-focus a, -.ui-state-focus a:hover, -.ui-state-focus a:link, -.ui-state-focus a:visited { - color: #2b2b2b; - text-decoration: none; -} -.ui-state-active, -.ui-widget-content .ui-state-active, -.ui-widget-header .ui-state-active { - border: 1px solid #003eff; - background: #007fff; - font-weight: normal; - color: #ffffff; -} -.ui-state-active a, -.ui-state-active a:link, -.ui-state-active a:visited { - color: #ffffff; - text-decoration: none; -} - -/* Interaction Cues -----------------------------------*/ -.ui-state-highlight, -.ui-widget-content .ui-state-highlight, -.ui-widget-header .ui-state-highlight { - border: 1px solid #dad55e; - background: #fffa90; - color: #777620; -} -.ui-state-highlight a, -.ui-widget-content .ui-state-highlight a, -.ui-widget-header .ui-state-highlight a { - color: #777620; -} -.ui-state-error, -.ui-widget-content .ui-state-error, -.ui-widget-header .ui-state-error { - border: 1px solid #f1a899; - background: #fddfdf; - color: #5f3f3f; -} -.ui-state-error a, -.ui-widget-content .ui-state-error a, -.ui-widget-header .ui-state-error a { - color: #5f3f3f; -} -.ui-state-error-text, -.ui-widget-content .ui-state-error-text, -.ui-widget-header .ui-state-error-text { - color: #5f3f3f; -} -.ui-priority-primary, -.ui-widget-content .ui-priority-primary, -.ui-widget-header .ui-priority-primary { - font-weight: bold; -} -.ui-priority-secondary, -.ui-widget-content .ui-priority-secondary, -.ui-widget-header .ui-priority-secondary { - opacity: .7; - filter:Alpha(Opacity=70); /* support: IE8 */ - font-weight: normal; -} -.ui-state-disabled, -.ui-widget-content .ui-state-disabled, -.ui-widget-header .ui-state-disabled { - opacity: .35; - filter:Alpha(Opacity=35); /* support: IE8 */ - background-image: none; -} -.ui-state-disabled .ui-icon { - filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */ -} - -/* Icons -----------------------------------*/ - -/* states and images */ -.ui-icon { - width: 16px; - height: 16px; -} -.ui-icon, -.ui-widget-content .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_444444_256x240.png"); -} -.ui-widget-header .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_444444_256x240.png"); -} -.ui-state-default .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_777777_256x240.png"); -} -.ui-state-hover .ui-icon, -.ui-state-focus .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_555555_256x240.png"); -} -.ui-state-active .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_ffffff_256x240.png"); -} -.ui-state-highlight .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_777620_256x240.png"); -} -.ui-state-error .ui-icon, -.ui-state-error-text .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_cc0000_256x240.png"); -} - -/* positioning */ -.ui-icon-blank { background-position: 16px 16px; } -.ui-icon-carat-1-n { background-position: 0 0; } -.ui-icon-carat-1-ne { background-position: -16px 0; } -.ui-icon-carat-1-e { background-position: -32px 0; } -.ui-icon-carat-1-se { background-position: -48px 0; } -.ui-icon-carat-1-s { background-position: -64px 0; } -.ui-icon-carat-1-sw { background-position: -80px 0; } -.ui-icon-carat-1-w { background-position: -96px 0; } -.ui-icon-carat-1-nw { background-position: -112px 0; } -.ui-icon-carat-2-n-s { background-position: -128px 0; } -.ui-icon-carat-2-e-w { background-position: -144px 0; } -.ui-icon-triangle-1-n { background-position: 0 -16px; } -.ui-icon-triangle-1-ne { background-position: -16px -16px; } -.ui-icon-triangle-1-e { background-position: -32px -16px; } -.ui-icon-triangle-1-se { background-position: -48px -16px; } -.ui-icon-triangle-1-s { background-position: -64px -16px; } -.ui-icon-triangle-1-sw { background-position: -80px -16px; } -.ui-icon-triangle-1-w { background-position: -96px -16px; } -.ui-icon-triangle-1-nw { background-position: -112px -16px; } -.ui-icon-triangle-2-n-s { background-position: -128px -16px; } -.ui-icon-triangle-2-e-w { background-position: -144px -16px; } -.ui-icon-arrow-1-n { background-position: 0 -32px; } -.ui-icon-arrow-1-ne { background-position: -16px -32px; } -.ui-icon-arrow-1-e { background-position: -32px -32px; } -.ui-icon-arrow-1-se { background-position: -48px -32px; } -.ui-icon-arrow-1-s { background-position: -64px -32px; } -.ui-icon-arrow-1-sw { background-position: -80px -32px; } -.ui-icon-arrow-1-w { background-position: -96px -32px; } -.ui-icon-arrow-1-nw { background-position: -112px -32px; } -.ui-icon-arrow-2-n-s { background-position: -128px -32px; } -.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } -.ui-icon-arrow-2-e-w { background-position: -160px -32px; } -.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } -.ui-icon-arrowstop-1-n { background-position: -192px -32px; } -.ui-icon-arrowstop-1-e { background-position: -208px -32px; } -.ui-icon-arrowstop-1-s { background-position: -224px -32px; } -.ui-icon-arrowstop-1-w { background-position: -240px -32px; } -.ui-icon-arrowthick-1-n { background-position: 0 -48px; } -.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } -.ui-icon-arrowthick-1-e { background-position: -32px -48px; } -.ui-icon-arrowthick-1-se { background-position: -48px -48px; } -.ui-icon-arrowthick-1-s { background-position: -64px -48px; } -.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } -.ui-icon-arrowthick-1-w { background-position: -96px -48px; } -.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } -.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } -.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } -.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } -.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } -.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } -.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } -.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } -.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } -.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } -.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } -.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } -.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } -.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } -.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } -.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } -.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } -.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } -.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } -.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } -.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } -.ui-icon-arrow-4 { background-position: 0 -80px; } -.ui-icon-arrow-4-diag { background-position: -16px -80px; } -.ui-icon-extlink { background-position: -32px -80px; } -.ui-icon-newwin { background-position: -48px -80px; } -.ui-icon-refresh { background-position: -64px -80px; } -.ui-icon-shuffle { background-position: -80px -80px; } -.ui-icon-transfer-e-w { background-position: -96px -80px; } -.ui-icon-transferthick-e-w { background-position: -112px -80px; } -.ui-icon-folder-collapsed { background-position: 0 -96px; } -.ui-icon-folder-open { background-position: -16px -96px; } -.ui-icon-document { background-position: -32px -96px; } -.ui-icon-document-b { background-position: -48px -96px; } -.ui-icon-note { background-position: -64px -96px; } -.ui-icon-mail-closed { background-position: -80px -96px; } -.ui-icon-mail-open { background-position: -96px -96px; } -.ui-icon-suitcase { background-position: -112px -96px; } -.ui-icon-comment { background-position: -128px -96px; } -.ui-icon-person { background-position: -144px -96px; } -.ui-icon-print { background-position: -160px -96px; } -.ui-icon-trash { background-position: -176px -96px; } -.ui-icon-locked { background-position: -192px -96px; } -.ui-icon-unlocked { background-position: -208px -96px; } -.ui-icon-bookmark { background-position: -224px -96px; } -.ui-icon-tag { background-position: -240px -96px; } -.ui-icon-home { background-position: 0 -112px; } -.ui-icon-flag { background-position: -16px -112px; } -.ui-icon-calendar { background-position: -32px -112px; } -.ui-icon-cart { background-position: -48px -112px; } -.ui-icon-pencil { background-position: -64px -112px; } -.ui-icon-clock { background-position: -80px -112px; } -.ui-icon-disk { background-position: -96px -112px; } -.ui-icon-calculator { background-position: -112px -112px; } -.ui-icon-zoomin { background-position: -128px -112px; } -.ui-icon-zoomout { background-position: -144px -112px; } -.ui-icon-search { background-position: -160px -112px; } -.ui-icon-wrench { background-position: -176px -112px; } -.ui-icon-gear { background-position: -192px -112px; } -.ui-icon-heart { background-position: -208px -112px; } -.ui-icon-star { background-position: -224px -112px; } -.ui-icon-link { background-position: -240px -112px; } -.ui-icon-cancel { background-position: 0 -128px; } -.ui-icon-plus { background-position: -16px -128px; } -.ui-icon-plusthick { background-position: -32px -128px; } -.ui-icon-minus { background-position: -48px -128px; } -.ui-icon-minusthick { background-position: -64px -128px; } -.ui-icon-close { background-position: -80px -128px; } -.ui-icon-closethick { background-position: -96px -128px; } -.ui-icon-key { background-position: -112px -128px; } -.ui-icon-lightbulb { background-position: -128px -128px; } -.ui-icon-scissors { background-position: -144px -128px; } -.ui-icon-clipboard { background-position: -160px -128px; } -.ui-icon-copy { background-position: -176px -128px; } -.ui-icon-contact { background-position: -192px -128px; } -.ui-icon-image { background-position: -208px -128px; } -.ui-icon-video { background-position: -224px -128px; } -.ui-icon-script { background-position: -240px -128px; } -.ui-icon-alert { background-position: 0 -144px; } -.ui-icon-info { background-position: -16px -144px; } -.ui-icon-notice { background-position: -32px -144px; } -.ui-icon-help { background-position: -48px -144px; } -.ui-icon-check { background-position: -64px -144px; } -.ui-icon-bullet { background-position: -80px -144px; } -.ui-icon-radio-on { background-position: -96px -144px; } -.ui-icon-radio-off { background-position: -112px -144px; } -.ui-icon-pin-w { background-position: -128px -144px; } -.ui-icon-pin-s { background-position: -144px -144px; } -.ui-icon-play { background-position: 0 -160px; } -.ui-icon-pause { background-position: -16px -160px; } -.ui-icon-seek-next { background-position: -32px -160px; } -.ui-icon-seek-prev { background-position: -48px -160px; } -.ui-icon-seek-end { background-position: -64px -160px; } -.ui-icon-seek-start { background-position: -80px -160px; } -/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ -.ui-icon-seek-first { background-position: -80px -160px; } -.ui-icon-stop { background-position: -96px -160px; } -.ui-icon-eject { background-position: -112px -160px; } -.ui-icon-volume-off { background-position: -128px -160px; } -.ui-icon-volume-on { background-position: -144px -160px; } -.ui-icon-power { background-position: 0 -176px; } -.ui-icon-signal-diag { background-position: -16px -176px; } -.ui-icon-signal { background-position: -32px -176px; } -.ui-icon-battery-0 { background-position: -48px -176px; } -.ui-icon-battery-1 { background-position: -64px -176px; } -.ui-icon-battery-2 { background-position: -80px -176px; } -.ui-icon-battery-3 { background-position: -96px -176px; } -.ui-icon-circle-plus { background-position: 0 -192px; } -.ui-icon-circle-minus { background-position: -16px -192px; } -.ui-icon-circle-close { background-position: -32px -192px; } -.ui-icon-circle-triangle-e { background-position: -48px -192px; } -.ui-icon-circle-triangle-s { background-position: -64px -192px; } -.ui-icon-circle-triangle-w { background-position: -80px -192px; } -.ui-icon-circle-triangle-n { background-position: -96px -192px; } -.ui-icon-circle-arrow-e { background-position: -112px -192px; } -.ui-icon-circle-arrow-s { background-position: -128px -192px; } -.ui-icon-circle-arrow-w { background-position: -144px -192px; } -.ui-icon-circle-arrow-n { background-position: -160px -192px; } -.ui-icon-circle-zoomin { background-position: -176px -192px; } -.ui-icon-circle-zoomout { background-position: -192px -192px; } -.ui-icon-circle-check { background-position: -208px -192px; } -.ui-icon-circlesmall-plus { background-position: 0 -208px; } -.ui-icon-circlesmall-minus { background-position: -16px -208px; } -.ui-icon-circlesmall-close { background-position: -32px -208px; } -.ui-icon-squaresmall-plus { background-position: -48px -208px; } -.ui-icon-squaresmall-minus { background-position: -64px -208px; } -.ui-icon-squaresmall-close { background-position: -80px -208px; } -.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } -.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } -.ui-icon-grip-solid-vertical { background-position: -32px -224px; } -.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } -.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } -.ui-icon-grip-diagonal-se { background-position: -80px -224px; } - - -/* Misc visuals -----------------------------------*/ - -/* Corner radius */ -.ui-corner-all, -.ui-corner-top, -.ui-corner-left, -.ui-corner-tl { - border-top-left-radius: 3px; -} -.ui-corner-all, -.ui-corner-top, -.ui-corner-right, -.ui-corner-tr { - border-top-right-radius: 3px; -} -.ui-corner-all, -.ui-corner-bottom, -.ui-corner-left, -.ui-corner-bl { - border-bottom-left-radius: 3px; -} -.ui-corner-all, -.ui-corner-bottom, -.ui-corner-right, -.ui-corner-br { - border-bottom-right-radius: 3px; -} - -/* Overlays */ -.ui-widget-overlay { - background: #aaaaaa; - opacity: .3; - filter: Alpha(Opacity=30); /* support: IE8 */ -} -.ui-widget-shadow { - margin: 0px 0 0 0px; - padding: 5px; - background: #666666; - opacity: .3; - filter: Alpha(Opacity=30); /* support: IE8 */ - border-radius: 8px; -} diff --git a/web/skins/classic/css/classic/skin.css b/web/skins/classic/css/classic/skin.css index ecd9f3949..b90412093 100644 --- a/web/skins/classic/css/classic/skin.css +++ b/web/skins/classic/css/classic/skin.css @@ -85,7 +85,8 @@ input,textarea,select,button { border: 1px #7f7fb2 solid; font-family: inherit; font-size: 100%; - color: #333333; + color: #333333; + background-color: #eeeeee; } input[type=text], input[type=password], textarea { diff --git a/web/skins/classic/css/dark/jquery-ui-theme.css b/web/skins/classic/css/dark/jquery-ui-theme.css deleted file mode 100644 index 6288b262e..000000000 --- a/web/skins/classic/css/dark/jquery-ui-theme.css +++ /dev/null @@ -1,410 +0,0 @@ -/*! - * jQuery UI CSS Framework 1.11.4 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - * - * http://api.jqueryui.com/category/theming/ - * - * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Arial%2CHelvetica%2Csans-serif&fsDefault=1em&fwDefault=normal&cornerRadius=2px&bgColorHeader=%23222222&bgTextureHeader=flat&borderColorHeader=%23222222&fcHeader=%23eeeeee&iconColorHeader=%23777777&bgColorContent=%23222222&bgTextureContent=flat&borderColorContent=%23dddddd&fcContent=%23dddddd&iconColorContent=%23444444&bgColorDefault=%23444444&bgTextureDefault=flat&borderColorDefault=%23444444&fcDefault=%23eeeeee&iconColorDefault=%23777777&bgColorHover=%23555555&bgTextureHover=flat&borderColorHover=%23444444&fcHover=%23eeeeee&iconColorHover=%23777777&bgColorActive=%23666666&bgTextureActive=flat&borderColorActive=%23666666&fcActive=%23eeeeee&iconColorActive=%23777777&bgColorHighlight=%23222222&bgTextureHighlight=flat&borderColorHighlight=%23777777&fcHighlight=%23eeeeee&iconColorHighlight=%23777777&bgColorError=%23fddfdf&bgTextureError=flat&borderColorError=%23f1a899&fcError=%235f3f3f&iconColorError=%23cc0000&bgColorOverlay=%23aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=%23666666&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=5px&offsetTopShadow=0px&offsetLeftShadow=0px&cornerRadiusShadow=8px&bgImgOpacityHeader=&bgImgOpacityContent=&bgImgOpacityDefault=&bgImgOpacityHover=&bgImgOpacityActive=&bgImgOpacityHighlight=&bgImgOpacityError= - */ - - -/* Component containers -----------------------------------*/ -.ui-widget { - font-family: Arial,Helvetica,sans-serif; - font-size: 1em; -} -.ui-widget .ui-widget { - font-size: 1em; -} -.ui-widget input, -.ui-widget select, -.ui-widget textarea, -.ui-widget button { - font-family: Arial,Helvetica,sans-serif; - font-size: 1em; -} -.ui-widget-content { - border: 1px solid #dddddd; - background: #222222; - color: #dddddd; -} -.ui-widget-content a { - color: #dddddd; -} -.ui-widget-header { - border: 1px solid #222222; - background: #222222; - color: #eeeeee; - font-weight: bold; -} -.ui-widget-header a { - color: #eeeeee; -} - -/* Interaction states -----------------------------------*/ -.ui-state-default, -.ui-widget-content .ui-state-default, -.ui-widget-header .ui-state-default { - border: 1px solid #444444; - background: #444444; - font-weight: normal; - color: #eeeeee; -} -.ui-state-default a, -.ui-state-default a:link, -.ui-state-default a:visited { - color: #eeeeee; - text-decoration: none; -} -.ui-state-hover, -.ui-widget-content .ui-state-hover, -.ui-widget-header .ui-state-hover, -.ui-state-focus, -.ui-widget-content .ui-state-focus, -.ui-widget-header .ui-state-focus { - border: 1px solid #444444; - background: #555555; - font-weight: normal; - color: #eeeeee; -} -.ui-state-hover a, -.ui-state-hover a:hover, -.ui-state-hover a:link, -.ui-state-hover a:visited, -.ui-state-focus a, -.ui-state-focus a:hover, -.ui-state-focus a:link, -.ui-state-focus a:visited { - color: #eeeeee; - text-decoration: none; -} -.ui-state-active, -.ui-widget-content .ui-state-active, -.ui-widget-header .ui-state-active { - border: 1px solid #666666; - background: #666666; - font-weight: normal; - color: #eeeeee; -} -.ui-state-active a, -.ui-state-active a:link, -.ui-state-active a:visited { - color: #eeeeee; - text-decoration: none; -} - -/* Interaction Cues -----------------------------------*/ -.ui-state-highlight, -.ui-widget-content .ui-state-highlight, -.ui-widget-header .ui-state-highlight { - border: 1px solid #777777; - background: #222222; - color: #eeeeee; -} -.ui-state-highlight a, -.ui-widget-content .ui-state-highlight a, -.ui-widget-header .ui-state-highlight a { - color: #eeeeee; -} -.ui-state-error, -.ui-widget-content .ui-state-error, -.ui-widget-header .ui-state-error { - border: 1px solid #f1a899; - background: #fddfdf; - color: #5f3f3f; -} -.ui-state-error a, -.ui-widget-content .ui-state-error a, -.ui-widget-header .ui-state-error a { - color: #5f3f3f; -} -.ui-state-error-text, -.ui-widget-content .ui-state-error-text, -.ui-widget-header .ui-state-error-text { - color: #5f3f3f; -} -.ui-priority-primary, -.ui-widget-content .ui-priority-primary, -.ui-widget-header .ui-priority-primary { - font-weight: bold; -} -.ui-priority-secondary, -.ui-widget-content .ui-priority-secondary, -.ui-widget-header .ui-priority-secondary { - opacity: .7; - filter:Alpha(Opacity=70); /* support: IE8 */ - font-weight: normal; -} -.ui-state-disabled, -.ui-widget-content .ui-state-disabled, -.ui-widget-header .ui-state-disabled { - opacity: .35; - filter:Alpha(Opacity=35); /* support: IE8 */ - background-image: none; -} -.ui-state-disabled .ui-icon { - filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */ -} - -/* Icons -----------------------------------*/ - -/* states and images */ -.ui-icon { - width: 16px; - height: 16px; -} -.ui-icon, -.ui-widget-content .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_444444_256x240.png"); -} -.ui-widget-header .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_777777_256x240.png"); -} -.ui-state-default .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_777777_256x240.png"); -} -.ui-state-hover .ui-icon, -.ui-state-focus .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_777777_256x240.png"); -} -.ui-state-active .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_777777_256x240.png"); -} -.ui-state-highlight .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_777777_256x240.png"); -} -.ui-state-error .ui-icon, -.ui-state-error-text .ui-icon { - background-image: url("../skins/classic/graphics/ui-icons_cc0000_256x240.png"); -} - -/* positioning */ -.ui-icon-blank { background-position: 16px 16px; } -.ui-icon-carat-1-n { background-position: 0 0; } -.ui-icon-carat-1-ne { background-position: -16px 0; } -.ui-icon-carat-1-e { background-position: -32px 0; } -.ui-icon-carat-1-se { background-position: -48px 0; } -.ui-icon-carat-1-s { background-position: -64px 0; } -.ui-icon-carat-1-sw { background-position: -80px 0; } -.ui-icon-carat-1-w { background-position: -96px 0; } -.ui-icon-carat-1-nw { background-position: -112px 0; } -.ui-icon-carat-2-n-s { background-position: -128px 0; } -.ui-icon-carat-2-e-w { background-position: -144px 0; } -.ui-icon-triangle-1-n { background-position: 0 -16px; } -.ui-icon-triangle-1-ne { background-position: -16px -16px; } -.ui-icon-triangle-1-e { background-position: -32px -16px; } -.ui-icon-triangle-1-se { background-position: -48px -16px; } -.ui-icon-triangle-1-s { background-position: -64px -16px; } -.ui-icon-triangle-1-sw { background-position: -80px -16px; } -.ui-icon-triangle-1-w { background-position: -96px -16px; } -.ui-icon-triangle-1-nw { background-position: -112px -16px; } -.ui-icon-triangle-2-n-s { background-position: -128px -16px; } -.ui-icon-triangle-2-e-w { background-position: -144px -16px; } -.ui-icon-arrow-1-n { background-position: 0 -32px; } -.ui-icon-arrow-1-ne { background-position: -16px -32px; } -.ui-icon-arrow-1-e { background-position: -32px -32px; } -.ui-icon-arrow-1-se { background-position: -48px -32px; } -.ui-icon-arrow-1-s { background-position: -64px -32px; } -.ui-icon-arrow-1-sw { background-position: -80px -32px; } -.ui-icon-arrow-1-w { background-position: -96px -32px; } -.ui-icon-arrow-1-nw { background-position: -112px -32px; } -.ui-icon-arrow-2-n-s { background-position: -128px -32px; } -.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } -.ui-icon-arrow-2-e-w { background-position: -160px -32px; } -.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } -.ui-icon-arrowstop-1-n { background-position: -192px -32px; } -.ui-icon-arrowstop-1-e { background-position: -208px -32px; } -.ui-icon-arrowstop-1-s { background-position: -224px -32px; } -.ui-icon-arrowstop-1-w { background-position: -240px -32px; } -.ui-icon-arrowthick-1-n { background-position: 0 -48px; } -.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } -.ui-icon-arrowthick-1-e { background-position: -32px -48px; } -.ui-icon-arrowthick-1-se { background-position: -48px -48px; } -.ui-icon-arrowthick-1-s { background-position: -64px -48px; } -.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } -.ui-icon-arrowthick-1-w { background-position: -96px -48px; } -.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } -.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } -.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } -.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } -.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } -.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } -.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } -.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } -.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } -.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } -.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } -.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } -.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } -.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } -.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } -.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } -.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } -.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } -.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } -.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } -.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } -.ui-icon-arrow-4 { background-position: 0 -80px; } -.ui-icon-arrow-4-diag { background-position: -16px -80px; } -.ui-icon-extlink { background-position: -32px -80px; } -.ui-icon-newwin { background-position: -48px -80px; } -.ui-icon-refresh { background-position: -64px -80px; } -.ui-icon-shuffle { background-position: -80px -80px; } -.ui-icon-transfer-e-w { background-position: -96px -80px; } -.ui-icon-transferthick-e-w { background-position: -112px -80px; } -.ui-icon-folder-collapsed { background-position: 0 -96px; } -.ui-icon-folder-open { background-position: -16px -96px; } -.ui-icon-document { background-position: -32px -96px; } -.ui-icon-document-b { background-position: -48px -96px; } -.ui-icon-note { background-position: -64px -96px; } -.ui-icon-mail-closed { background-position: -80px -96px; } -.ui-icon-mail-open { background-position: -96px -96px; } -.ui-icon-suitcase { background-position: -112px -96px; } -.ui-icon-comment { background-position: -128px -96px; } -.ui-icon-person { background-position: -144px -96px; } -.ui-icon-print { background-position: -160px -96px; } -.ui-icon-trash { background-position: -176px -96px; } -.ui-icon-locked { background-position: -192px -96px; } -.ui-icon-unlocked { background-position: -208px -96px; } -.ui-icon-bookmark { background-position: -224px -96px; } -.ui-icon-tag { background-position: -240px -96px; } -.ui-icon-home { background-position: 0 -112px; } -.ui-icon-flag { background-position: -16px -112px; } -.ui-icon-calendar { background-position: -32px -112px; } -.ui-icon-cart { background-position: -48px -112px; } -.ui-icon-pencil { background-position: -64px -112px; } -.ui-icon-clock { background-position: -80px -112px; } -.ui-icon-disk { background-position: -96px -112px; } -.ui-icon-calculator { background-position: -112px -112px; } -.ui-icon-zoomin { background-position: -128px -112px; } -.ui-icon-zoomout { background-position: -144px -112px; } -.ui-icon-search { background-position: -160px -112px; } -.ui-icon-wrench { background-position: -176px -112px; } -.ui-icon-gear { background-position: -192px -112px; } -.ui-icon-heart { background-position: -208px -112px; } -.ui-icon-star { background-position: -224px -112px; } -.ui-icon-link { background-position: -240px -112px; } -.ui-icon-cancel { background-position: 0 -128px; } -.ui-icon-plus { background-position: -16px -128px; } -.ui-icon-plusthick { background-position: -32px -128px; } -.ui-icon-minus { background-position: -48px -128px; } -.ui-icon-minusthick { background-position: -64px -128px; } -.ui-icon-close { background-position: -80px -128px; } -.ui-icon-closethick { background-position: -96px -128px; } -.ui-icon-key { background-position: -112px -128px; } -.ui-icon-lightbulb { background-position: -128px -128px; } -.ui-icon-scissors { background-position: -144px -128px; } -.ui-icon-clipboard { background-position: -160px -128px; } -.ui-icon-copy { background-position: -176px -128px; } -.ui-icon-contact { background-position: -192px -128px; } -.ui-icon-image { background-position: -208px -128px; } -.ui-icon-video { background-position: -224px -128px; } -.ui-icon-script { background-position: -240px -128px; } -.ui-icon-alert { background-position: 0 -144px; } -.ui-icon-info { background-position: -16px -144px; } -.ui-icon-notice { background-position: -32px -144px; } -.ui-icon-help { background-position: -48px -144px; } -.ui-icon-check { background-position: -64px -144px; } -.ui-icon-bullet { background-position: -80px -144px; } -.ui-icon-radio-on { background-position: -96px -144px; } -.ui-icon-radio-off { background-position: -112px -144px; } -.ui-icon-pin-w { background-position: -128px -144px; } -.ui-icon-pin-s { background-position: -144px -144px; } -.ui-icon-play { background-position: 0 -160px; } -.ui-icon-pause { background-position: -16px -160px; } -.ui-icon-seek-next { background-position: -32px -160px; } -.ui-icon-seek-prev { background-position: -48px -160px; } -.ui-icon-seek-end { background-position: -64px -160px; } -.ui-icon-seek-start { background-position: -80px -160px; } -/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ -.ui-icon-seek-first { background-position: -80px -160px; } -.ui-icon-stop { background-position: -96px -160px; } -.ui-icon-eject { background-position: -112px -160px; } -.ui-icon-volume-off { background-position: -128px -160px; } -.ui-icon-volume-on { background-position: -144px -160px; } -.ui-icon-power { background-position: 0 -176px; } -.ui-icon-signal-diag { background-position: -16px -176px; } -.ui-icon-signal { background-position: -32px -176px; } -.ui-icon-battery-0 { background-position: -48px -176px; } -.ui-icon-battery-1 { background-position: -64px -176px; } -.ui-icon-battery-2 { background-position: -80px -176px; } -.ui-icon-battery-3 { background-position: -96px -176px; } -.ui-icon-circle-plus { background-position: 0 -192px; } -.ui-icon-circle-minus { background-position: -16px -192px; } -.ui-icon-circle-close { background-position: -32px -192px; } -.ui-icon-circle-triangle-e { background-position: -48px -192px; } -.ui-icon-circle-triangle-s { background-position: -64px -192px; } -.ui-icon-circle-triangle-w { background-position: -80px -192px; } -.ui-icon-circle-triangle-n { background-position: -96px -192px; } -.ui-icon-circle-arrow-e { background-position: -112px -192px; } -.ui-icon-circle-arrow-s { background-position: -128px -192px; } -.ui-icon-circle-arrow-w { background-position: -144px -192px; } -.ui-icon-circle-arrow-n { background-position: -160px -192px; } -.ui-icon-circle-zoomin { background-position: -176px -192px; } -.ui-icon-circle-zoomout { background-position: -192px -192px; } -.ui-icon-circle-check { background-position: -208px -192px; } -.ui-icon-circlesmall-plus { background-position: 0 -208px; } -.ui-icon-circlesmall-minus { background-position: -16px -208px; } -.ui-icon-circlesmall-close { background-position: -32px -208px; } -.ui-icon-squaresmall-plus { background-position: -48px -208px; } -.ui-icon-squaresmall-minus { background-position: -64px -208px; } -.ui-icon-squaresmall-close { background-position: -80px -208px; } -.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } -.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } -.ui-icon-grip-solid-vertical { background-position: -32px -224px; } -.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } -.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } -.ui-icon-grip-diagonal-se { background-position: -80px -224px; } - - -/* Misc visuals -----------------------------------*/ - -/* Corner radius */ -.ui-corner-all, -.ui-corner-top, -.ui-corner-left, -.ui-corner-tl { - border-top-left-radius: 2px; -} -.ui-corner-all, -.ui-corner-top, -.ui-corner-right, -.ui-corner-tr { - border-top-right-radius: 2px; -} -.ui-corner-all, -.ui-corner-bottom, -.ui-corner-left, -.ui-corner-bl { - border-bottom-left-radius: 2px; -} -.ui-corner-all, -.ui-corner-bottom, -.ui-corner-right, -.ui-corner-br { - border-bottom-right-radius: 2px; -} - -/* Overlays */ -.ui-widget-overlay { - background: #aaaaaa; - opacity: .3; - filter: Alpha(Opacity=30); /* support: IE8 */ -} -.ui-widget-shadow { - margin: 0px 0 0 0px; - padding: 5px; - background: #666666; - opacity: .3; - filter: Alpha(Opacity=30); /* support: IE8 */ - border-radius: 8px; -} diff --git a/web/skins/classic/includes/functions.php b/web/skins/classic/includes/functions.php index b21157607..196c9fe89 100644 --- a/web/skins/classic/includes/functions.php +++ b/web/skins/classic/includes/functions.php @@ -81,10 +81,10 @@ echo output_link_if_exists( array( 'css/'.$css.'/skin.css', 'css/base/views/'.$basename.'.css', 'css/'.$css.'/views/'.$basename.'.css', - '/js/dateTimePicker/jquery-ui-timepicker-addon.css', - '/js/jquery-ui-1.12.1/jquery-ui.structure.min.css', - '/css/jquery-ui-1.12.1/jquery-ui.theme.min.css', - '/css/'.$css.'/jquery-ui-theme.css', + 'js/dateTimePicker/jquery-ui-timepicker-addon.css', + 'js/jquery-ui-1.12.1/jquery-ui.structure.min.css', + 'js/jquery-ui-1.12.1/jquery-ui.theme.min.css', + 'css/'.$css.'/jquery-ui-theme.css', ) ); ?> @@ -119,6 +119,7 @@ echo output_link_if_exists( array( + - - + +'; + global $error_message; + if ( $error_message ) { + echo '
'.$error_message.'
'; + } +} // end function getBodyTopHTML + function getNavBarHTML($reload = null) { # Provide a facility to turn off the headers if you put headers=0 into the url if ( isset($_REQUEST['navbar']) and $_REQUEST['navbar']=='0' ) @@ -235,11 +245,6 @@ function getNavBarHTML($reload = null) { $running = daemonCheck(); $status = $running?translate('Running'):translate('Stopped'); ?> -