From a995d72ebd5f561e0a53183a1a7347bb24756758 Mon Sep 17 00:00:00 2001 From: Andrew Bauer Date: Sat, 22 Aug 2015 12:36:49 -0500 Subject: [PATCH 01/20] add warning and help text for maxfps fields --- web/lang/en_gb.php | 11 ++++++++++- web/skins/classic/views/monitor.php | 14 ++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index bf0258caf..6112aaf8c 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -903,7 +903,16 @@ $OLANG = array( 'OPTIONS_EXIF' => array( 'Help' => "Enable this option to embed EXIF data into each jpeg frame." ), - + 'OPTIONS_MAXFPS' => array( + 'Help' => "This field has certain limitations when used for non-local devices.~~ ". + "Failure to adhere to these limitations will cause a delay in live video, irregular frame skipping, ". + "and missed events~~". + "For streaming IP cameras, do not use this field to reduce the frame rate. Set the frame rate in the". + " camera, instead. You can, however, use a value that is slightly higher than the frame rate in the camera. ". + "In this case, this helps keep the cpu from being overtaxed in the event of a network problem.~~". + "Some, mostly older, IP cameras support snapshot mode. In this case ZoneMinder is actively polling the camera ". + "for new images. In this case, it is safe to use thie field." + ), // 'LANG_DEFAULT' => array( // 'Prompt' => "This is a new prompt for this option", diff --git a/web/skins/classic/views/monitor.php b/web/skins/classic/views/monitor.php index 5920409ad..7935e4f5c 100644 --- a/web/skins/classic/views/monitor.php +++ b/web/skins/classic/views/monitor.php @@ -680,9 +680,19 @@ switch ( $tab ) - - +  () +  () + + + + From dabe32a593ae30b1df94ffb94ec3ad19b927c012 Mon Sep 17 00:00:00 2001 From: Andy Bauer Date: Sat, 21 Nov 2015 18:21:18 -0600 Subject: [PATCH 02/20] Error on missing submodules --- CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index cf9337809..193b484b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,12 @@ set(zoneminder_VERSION "1.28.109") # make API version a minor of ZM version set(zoneminder_API_VERSION "${zoneminder_VERSION}.1") +# Make sure the submodules are there +if( NOT EXISTS "${CMAKE_SOURCE_DIR}/web/api/app/Plugin/Crud/.git" ) +message( SEND_ERROR "The git submodules are not available. Please run +git submodule update --init --recursive") +endif( NOT EXISTS "${CMAKE_SOURCE_DIR}/web/api/app/Plugin/Crud/.git" ) + # CMake does not allow out-of-source build if CMakeCache.exists # in the source folder. Abort and notify the user if( From 5390605797d2ec8db4c6eb81752cd6e6c1dc2fac Mon Sep 17 00:00:00 2001 From: SteveGilvarry Date: Thu, 3 Dec 2015 10:23:19 +1100 Subject: [PATCH 03/20] Add v to front of version string in version->parse to force conversion of decimal to dotted decimal versions, and change from ge to > to prevent reapplying current version --- scripts/zmupdate.pl.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/zmupdate.pl.in b/scripts/zmupdate.pl.in index e9f4a2dc2..969c72cb6 100644 --- a/scripts/zmupdate.pl.in +++ b/scripts/zmupdate.pl.in @@ -1044,7 +1044,7 @@ if ( $version ) foreach my $patch ( @files ) { my ( $v ) = $patch =~ /^zm_update\-([\d\.]+)\.sql$/; #PP make sure we use version compare - if ( version->parse($v) ge version->parse($version) ) { + if ( version->parse('v' . $v) > version->parse('v' . $version) ) { print( "Upgrading DB to $v from $version\n" ); patchDB( $dbh, $v ); if ( $dbh->errstr() ) { From 1957c5672f8286333e0a58b11f3721c296a95ed4 Mon Sep 17 00:00:00 2001 From: Andrew Bauer Date: Fri, 4 Dec 2015 12:03:18 -0600 Subject: [PATCH 04/20] Check for the presence of CrudControllerTrait.php instead of .git --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 63425f4b3..b07156919 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,10 +9,10 @@ set(zoneminder_VERSION "1.28.109") set(zoneminder_API_VERSION "${zoneminder_VERSION}.1") # Make sure the submodules are there -if( NOT EXISTS "${CMAKE_SOURCE_DIR}/web/api/app/Plugin/Crud/.git" ) +if( NOT EXISTS "${CMAKE_SOURCE_DIR}/web/api/app/Plugin/Crud/Lib/CrudControllerTrait.php" ) message( SEND_ERROR "The git submodules are not available. Please run git submodule update --init --recursive") -endif( NOT EXISTS "${CMAKE_SOURCE_DIR}/web/api/app/Plugin/Crud/.git" ) +endif( NOT EXISTS "${CMAKE_SOURCE_DIR}/web/api/app/Plugin/Crud/Lib/CrudControllerTrait.php" ) # CMake does not allow out-of-source build if CMakeCache.exists # in the source folder. Abort and notify the user From e637915823ab643216f44759a6cd495a05042d15 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Fri, 4 Dec 2015 15:39:37 -0500 Subject: [PATCH 05/20] add a lock around the socket creating/deleting. --- src/zm_stream.cpp | 22 ++++++++++++++++++++++ src/zm_stream.h | 3 +++ 2 files changed, 25 insertions(+) diff --git a/src/zm_stream.cpp b/src/zm_stream.cpp index 30a420bb1..4e9a4da10 100644 --- a/src/zm_stream.cpp +++ b/src/zm_stream.cpp @@ -286,6 +286,23 @@ void StreamBase::openComms() { if ( connkey > 0 ) { + + snprintf( sock_path_lock, sizeof(sock_path_lock), "%s/zms-%06d.lock", config.path_socks, connkey); + + lock_fd = open(sock_path_lock, O_CREAT|O_WRONLY, S_IRUSR | S_IWUSR); + if (lock_fd <= 0 || flock(lock_fd, LOCK_SH|LOCK_NB) != 0) + { + Error("Unable to lock sock lock file %s: %s", sock_path_lock, strerror(errno) ); + + close(lock_fd); + lock_fd = 0; + } + else + { + Debug( 1, "We have obtained a read lock on %s fd: %d", sock_path_lock, lock_fd); + } + + sd = socket( AF_UNIX, SOCK_DGRAM, 0 ); if ( sd < 0 ) { @@ -321,6 +338,11 @@ void StreamBase::closeComms() { unlink( loc_sock_path ); } + if (lock_fd > 0) + { + loc_sock_path[0] = '\0'; + close(lock_fd); //close it rather than unlock it incase it got deleted. + } } } diff --git a/src/zm_stream.h b/src/zm_stream.h index ef442ab8e..9936688a8 100644 --- a/src/zm_stream.h +++ b/src/zm_stream.h @@ -78,6 +78,8 @@ protected: struct sockaddr_un loc_addr; char rem_sock_path[PATH_MAX]; struct sockaddr_un rem_addr; + char sock_path_lock[PATH_MAX]; + int lock_fd; protected: bool paused; @@ -127,6 +129,7 @@ public: connkey = 0; sd = -1; + lock_fd = 0; memset( &loc_sock_path, 0, sizeof(loc_sock_path) ); memset( &loc_addr, 0, sizeof(loc_addr) ); memset( &rem_sock_path, 0, sizeof(rem_sock_path) ); From be36301779f38dc1757ebd6c832634e15b6fc8d8 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Fri, 4 Dec 2015 16:35:27 -0500 Subject: [PATCH 06/20] use exclusive lock, add includes --- src/zm_stream.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/zm_stream.cpp b/src/zm_stream.cpp index 4e9a4da10..f50671338 100644 --- a/src/zm_stream.cpp +++ b/src/zm_stream.cpp @@ -18,6 +18,10 @@ // #include +#include +#include +#include +#include #include "zm.h" #include "zm_mpeg.h" @@ -290,7 +294,7 @@ void StreamBase::openComms() snprintf( sock_path_lock, sizeof(sock_path_lock), "%s/zms-%06d.lock", config.path_socks, connkey); lock_fd = open(sock_path_lock, O_CREAT|O_WRONLY, S_IRUSR | S_IWUSR); - if (lock_fd <= 0 || flock(lock_fd, LOCK_SH|LOCK_NB) != 0) + if (lock_fd <= 0 || flock(lock_fd, LOCK_EX) != 0) { Error("Unable to lock sock lock file %s: %s", sock_path_lock, strerror(errno) ); @@ -299,7 +303,7 @@ void StreamBase::openComms() } else { - Debug( 1, "We have obtained a read lock on %s fd: %d", sock_path_lock, lock_fd); + Debug( 1, "We have obtained a lock on %s fd: %d", sock_path_lock, lock_fd); } From 0fc68e2e91de480ddeab92aee23a17c142fb5b0d Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Fri, 4 Dec 2015 16:38:24 -0500 Subject: [PATCH 07/20] delete lock when we are done with it --- src/zm_stream.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zm_stream.cpp b/src/zm_stream.cpp index f50671338..c66638759 100644 --- a/src/zm_stream.cpp +++ b/src/zm_stream.cpp @@ -344,8 +344,8 @@ void StreamBase::closeComms() } if (lock_fd > 0) { - loc_sock_path[0] = '\0'; close(lock_fd); //close it rather than unlock it incase it got deleted. + unlink(sock_path_lock); } } } From d6b2e1959f8fa3941e1c2e50cb075ea032658c25 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 8 Dec 2015 08:40:44 -0500 Subject: [PATCH 08/20] add a 1/8th scale option, which is useful for 1920x1080 streams --- web/skins/classic/includes/config.php | 1 + 1 file changed, 1 insertion(+) diff --git a/web/skins/classic/includes/config.php b/web/skins/classic/includes/config.php index d586f90c8..459bbc587 100644 --- a/web/skins/classic/includes/config.php +++ b/web/skins/classic/includes/config.php @@ -40,6 +40,7 @@ $scales = array( "50" => "1/2x", "33" => "1/3x", "25" => "1/4x", + "12.5" => "1/8x", ); $bwArray = array( From 7c298c58ed0aab83b860ac2c768d125f3c2a3bbf Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 8 Dec 2015 16:20:38 -0500 Subject: [PATCH 09/20] Add code to detect the change in REALM from older to newer firmware --- .../lib/ZoneMinder/Control/TVIP862.pm | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Control/TVIP862.pm b/scripts/ZoneMinder/lib/ZoneMinder/Control/TVIP862.pm index 050e2bb75..d7c669583 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Control/TVIP862.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Control/TVIP862.pm @@ -138,6 +138,30 @@ sub open ."'" ); $self->{ua}->credentials($ADDRESS,$REALM,$USERNAME,$PASSWORD); + + # Detect REALM + my $req = HTTP::Request->new( GET=>"http://".$ADDRESS."/cgi/ptdc.cgi" ); + my $res = $self->{ua}->request($req); + + if ( ! $res->is_success ) { + Debug("Need newer REALM"); + if ( $res->status_line() eq '401 Unauthorized' ) { + my $headers = $res->headers(); + foreach my $k ( keys %$headers ) { + Debug("Initial Header $k => $$headers{$k}"); + } # end foreach + if ( $$headers{'www-authenticate'} ) { + my ( $auth, $tokens ) = $$headers{'www-authenticate'} =~ /^(\w+)\s+(.*)$/; + if ( $tokens =~ /\w+="([^"]+)"/i ) { + $REALM = $1; + Debug( "Changing REALM to $REALM" ); + $self->{ua}->credentials($ADDRESS,$REALM,$USERNAME,$PASSWORD); + } # end if + } else { + Debug("No headers line"); + } # end if headers + } # end if $res->status_line() eq '401 Unauthorized' + } # end if ! $res->is_success } sub close From 53dec9f26dca517a2d19dec286628c67c88344d4 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Wed, 9 Dec 2015 10:43:23 -0500 Subject: [PATCH 10/20] check for setting of __REQUEST['object'] to silence php NOTICE --- web/includes/actions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/includes/actions.php b/web/includes/actions.php index b357d3222..6d94bafb2 100644 --- a/web/includes/actions.php +++ b/web/includes/actions.php @@ -766,7 +766,7 @@ if ( !empty($action) ) // System edit actions if ( canEdit( 'System' ) ) { - if ( $_REQUEST['object'] == 'server' ) { + if ( isset( $_REQUEST['object'] ) and ( $_REQUEST['object'] == 'server' ) ) { if ( $action == "save" ) { if ( !empty($_REQUEST['id']) ) From af76d19646f4334164a530b239ddd17a91349070 Mon Sep 17 00:00:00 2001 From: SteveGilvarry Date: Fri, 18 Dec 2015 01:47:10 +1100 Subject: [PATCH 11/20] Add a constrained namespace implementation around Authenticator class to prevent conflict with libvlc live555. --- src/zm_remote_camera.cpp | 2 +- src/zm_remote_camera.h | 2 +- src/zm_remote_camera_http.cpp | 4 ++-- src/zm_rtsp.cpp | 4 ++-- src/zm_rtsp.h | 2 +- src/zm_rtsp_auth.cpp | 4 ++++ src/zm_rtsp_auth.h | 4 ++++ 7 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/zm_remote_camera.cpp b/src/zm_remote_camera.cpp index 608311649..8ac4a51ba 100644 --- a/src/zm_remote_camera.cpp +++ b/src/zm_remote_camera.cpp @@ -71,7 +71,7 @@ void RemoteCamera::Initialise() } mNeedAuth = false; - mAuthenticator = new Authenticator(username,password); + mAuthenticator = new zm::Authenticator(username,password); struct addrinfo hints; memset(&hints, 0, sizeof(hints)); diff --git a/src/zm_remote_camera.h b/src/zm_remote_camera.h index 44c00bf65..7e3ae79a8 100644 --- a/src/zm_remote_camera.h +++ b/src/zm_remote_camera.h @@ -50,7 +50,7 @@ protected: // fill required fields and set needAuth // subsequent requests can set the required authentication header. bool mNeedAuth; - Authenticator* mAuthenticator; + zm::Authenticator* mAuthenticator; protected: struct addrinfo *hp; diff --git a/src/zm_remote_camera_http.cpp b/src/zm_remote_camera_http.cpp index 44004564f..440c24e05 100644 --- a/src/zm_remote_camera_http.cpp +++ b/src/zm_remote_camera_http.cpp @@ -306,7 +306,7 @@ int RemoteCameraHttp::GetResponse() std::string Header = header; mAuthenticator->checkAuthResponse(Header); - if ( mAuthenticator->auth_method() == AUTH_DIGEST ) { + if ( mAuthenticator->auth_method() == zm::AUTH_DIGEST ) { Debug( 2, "Need Digest Authentication" ); request = stringtf( "GET %s HTTP/%s\r\n", path.c_str(), config.http_version ); request += stringtf( "User-Agent: %s/%s\r\n", config.http_ua, ZM_VERSION ); @@ -750,7 +750,7 @@ Debug(3, "Need more data buffer %d < content length %d", buffer.size(), content_ Debug(2, "Checking for digest auth in %s", authenticate_header ); mAuthenticator->checkAuthResponse(Header); - if ( mAuthenticator->auth_method() == AUTH_DIGEST ) { + if ( mAuthenticator->auth_method() == zm::AUTH_DIGEST ) { Debug( 2, "Need Digest Authentication" ); request = stringtf( "GET %s HTTP/%s\r\n", path.c_str(), config.http_version ); request += stringtf( "User-Agent: %s/%s\r\n", config.http_ua, ZM_VERSION ); diff --git a/src/zm_rtsp.cpp b/src/zm_rtsp.cpp index 0dbcb7329..7ac80e9b6 100644 --- a/src/zm_rtsp.cpp +++ b/src/zm_rtsp.cpp @@ -203,9 +203,9 @@ RtspThread::RtspThread( int id, RtspMethod method, const std::string &protocol, mNeedAuth = false; StringVector parts = split(auth,":"); if (parts.size() > 1) - mAuthenticator = new Authenticator(parts[0], parts[1]); + mAuthenticator = new zm::Authenticator(parts[0], parts[1]); else - mAuthenticator = new Authenticator(parts[0], ""); + mAuthenticator = new zm::Authenticator(parts[0], ""); } RtspThread::~RtspThread() diff --git a/src/zm_rtsp.h b/src/zm_rtsp.h index acd28e651..192200d0b 100644 --- a/src/zm_rtsp.h +++ b/src/zm_rtsp.h @@ -66,7 +66,7 @@ private: // subsequent requests can set the required authentication header. bool mNeedAuth; int respCode; - Authenticator* mAuthenticator; + zm::Authenticator* mAuthenticator; std::string mHttpSession; ///< Only for RTSP over HTTP sessions diff --git a/src/zm_rtsp_auth.cpp b/src/zm_rtsp_auth.cpp index fd9087afa..10ecf3475 100644 --- a/src/zm_rtsp_auth.cpp +++ b/src/zm_rtsp_auth.cpp @@ -24,6 +24,8 @@ #include #include +namespace zm { + Authenticator::Authenticator(std::string &username, std::string password) { #ifdef HAVE_GCRYPT_H // Special initialisation for libgcrypt @@ -227,3 +229,5 @@ void Authenticator::checkAuthResponse(std::string &response) { Debug( 2, "Didn't find auth line in %s", authLine.c_str()); } } + +} // namespace zm diff --git a/src/zm_rtsp_auth.h b/src/zm_rtsp_auth.h index 23745f9c7..079dec639 100644 --- a/src/zm_rtsp_auth.h +++ b/src/zm_rtsp_auth.h @@ -32,6 +32,8 @@ #include #endif // HAVE_GCRYPT_H || HAVE_LIBCRYPTO +namespace zm { + enum AuthMethod { AUTH_UNDEFINED = 0, AUTH_BASIC = 1, AUTH_DIGEST = 2 }; class Authenticator { public: @@ -62,4 +64,6 @@ private: int nc; }; +} // namespace zm + #endif // ZM_RTSP_AUTH_H From ca45ac23fa412b3b02e8dd601a3e7995b18c2574 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Fri, 18 Dec 2015 13:30:42 -0500 Subject: [PATCH 12/20] increase height of group edit window --- web/skins/classic/js/dark.js | 2 +- web/skins/classic/js/flat.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/skins/classic/js/dark.js b/web/skins/classic/js/dark.js index 06a940482..b8ab38888 100644 --- a/web/skins/classic/js/dark.js +++ b/web/skins/classic/js/dark.js @@ -42,7 +42,7 @@ var popupSizes = { 'frame': { 'addWidth': 32, 'minWidth': 384, 'addHeight': 100 }, 'frames': { 'width': 500, 'height': 600 }, 'function': { 'width': 300, 'height': 92 }, - 'group': { 'width': 360, 'height': 180 }, + 'group': { 'width': 360, 'height': 300 }, 'groups': { 'width': 440, 'height': 220 }, 'image': { 'addWidth': 48, 'addHeight': 80 }, 'log': { 'width': 1080, 'height': 720 }, diff --git a/web/skins/classic/js/flat.js b/web/skins/classic/js/flat.js index 3b1882250..fc03b91e0 100644 --- a/web/skins/classic/js/flat.js +++ b/web/skins/classic/js/flat.js @@ -42,7 +42,7 @@ var popupSizes = { 'frame': { 'addWidth': 32, 'minWidth': 384, 'addHeight': 100 }, 'frames': { 'width': 500, 'height': 600 }, 'function': { 'width': 300, 'height': 140 }, - 'group': { 'width': 360, 'height': 180 }, + 'group': { 'width': 360, 'height': 300 }, 'groups': { 'width': 440, 'height': 220 }, 'image': { 'addWidth': 48, 'addHeight': 80 }, 'log': { 'width': 1080, 'height': 720 }, From 983d87f69b84e062e9bf6976cef8fe27687a263e Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Fri, 18 Dec 2015 13:34:44 -0500 Subject: [PATCH 13/20] increase height and width of groups window to fit buttons in --- web/skins/classic/js/dark.js | 2 +- web/skins/classic/js/flat.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/skins/classic/js/dark.js b/web/skins/classic/js/dark.js index b8ab38888..f8867feda 100644 --- a/web/skins/classic/js/dark.js +++ b/web/skins/classic/js/dark.js @@ -43,7 +43,7 @@ var popupSizes = { 'frames': { 'width': 500, 'height': 600 }, 'function': { 'width': 300, 'height': 92 }, 'group': { 'width': 360, 'height': 300 }, - 'groups': { 'width': 440, 'height': 220 }, + 'groups': { 'width': 540, 'height': 420 }, 'image': { 'addWidth': 48, 'addHeight': 80 }, 'log': { 'width': 1080, 'height': 720 }, 'login': { 'width': 720, 'height': 480 }, diff --git a/web/skins/classic/js/flat.js b/web/skins/classic/js/flat.js index fc03b91e0..6d5a89bf7 100644 --- a/web/skins/classic/js/flat.js +++ b/web/skins/classic/js/flat.js @@ -43,7 +43,7 @@ var popupSizes = { 'frames': { 'width': 500, 'height': 600 }, 'function': { 'width': 300, 'height': 140 }, 'group': { 'width': 360, 'height': 300 }, - 'groups': { 'width': 440, 'height': 220 }, + 'groups': { 'width': 540, 'height': 420 }, 'image': { 'addWidth': 48, 'addHeight': 80 }, 'log': { 'width': 1080, 'height': 720 }, 'login': { 'width': 720, 'height': 480 }, From 9e2b1ba3cb9cdf1d1393fb4e340038dfd5beedc8 Mon Sep 17 00:00:00 2001 From: arjunrc Date: Fri, 18 Dec 2015 15:08:40 -0500 Subject: [PATCH 14/20] remove hardcoded version --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 2d376f03d..1e7f35737 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,9 +51,9 @@ copyright = u'2014, https://github.com/ZoneMinder/ZoneMinder/graphs/contributors # built documents. # # The short X.Y version. -version = '1.28.1' +#version = '1.28.1' # The full version, including alpha/beta/rc tags. -release = '1.28.1' +#release = '1.28.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 511421bff1f864156fac15255d5449e9a25f2427 Mon Sep 17 00:00:00 2001 From: arjunrc Date: Fri, 18 Dec 2015 15:08:56 -0500 Subject: [PATCH 15/20] rewrite --- docs/installationguide/ubuntu.rst | 540 +++++++++++++++++++++++------- 1 file changed, 428 insertions(+), 112 deletions(-) diff --git a/docs/installationguide/ubuntu.rst b/docs/installationguide/ubuntu.rst index b95d46a89..fa065cb96 100644 --- a/docs/installationguide/ubuntu.rst +++ b/docs/installationguide/ubuntu.rst @@ -1,64 +1,466 @@ Ubuntu ====== -Option A: Install a ready made package ---------------------------------------- +.. contents:: -Installation procedure (common for all versions of Ubuntu) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Easy Way: Install ZoneMinder from a package (Ubuntu 15.x+) +----------------------------------------------------------- +These instructions are for a brand new ubuntu 15.04 system which does not have ZM installed. -It is important that you first apply any system software upgrades first to Ubuntu, especially if you have just created a new image of Ubuntu. -Not doing this may cause the PPA process to fail and complain about various unmet dependencies. - -If you also plan to install the database in the same server (which is typically the case), first do: +**Step 1**: Make sure we add the correct packages :: - sudo apt-get install mysql-server + sudo add-apt-repository ppa:iconnor/zoneminder-master + sudo apt-get update -This will ask you for a user and password to configure for Zoneminder. -Note that when you install the PPA, it will also create a username of zmuser and a password of zmpass irrespective of what you select at this stage - -Now add the ppa repository path: +if you don't have mysql already installed: :: - sudo apt-add-repository ppa:iconnor/zoneminder + sudo apt-get install mysql-server + +This will ask you to set up a master password for the DB (you are asked for the mysql root password when installing mysql server). + +**Step 2**: Install ZoneMinder -Once you have updated the repository then update and install the package.: - :: - sudo apt-get update - sudo apt-get install zoneminder + sudo apt-get install zoneminder +**Step 3**: Configure the Database +:: + sudo mysql -uroot -p < /usr/share/zoneminder/db/zm_create.sql + mysql -uroot -p -e "grant select,insert,update,delete,create,alter,index,lock tables on zm.* to 'zmuser'@localhost identified by 'zmpass';" -Post Install steps for Ubuntu 15.x or newer (systemd) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +You don't really need this, but no harm (needed if you are upgrading) :: sudo /usr/bin/zmupdate.pl + +**Step 4**: Configure systemd to recognize ZoneMinder and configure Apache correctly: + +:: + sudo systemctl enable zoneminder sudo a2enconf zoneminder + sudo a2enmod cgi + sudo chown -R www-data:www-data /usr/share/zoneminder/ + + +We need this for API routing to work: + +:: + + sudo a2enmod rewrite + +This is probably a bug with iconnor's PPA as of Oct 3, 2015 with package 1.28.107. After installing, ``zm.conf`` does not have the right read permissions, so we need to fix that. This may go away in future PPA releases: + +:: + + sudo chown www-data:www-data /etc/zm/zm.conf + +We also need to install php5-gd (as of 1.28.107, this is not installed) + +:: + + sudo apt-get install php5-gd + +**Step 5**: Edit Timezone in PHP + +:: + + vi /etc/php5/apache2/php.ini + +Look for [Date] and inside it you will see a date.timezone +that is commented. remove the comment and specific your timezone. +Please make sure the timezone is valid (see this: http://php.net/manual/en/timezones.php) + +In my case: + +:: + + date.timezone = America/New_York + +**Step 6**: Restart services + +:: + sudo service apache2 reload - systemctl restart zoneminder + sudo systemctl restart zoneminder -You should now be able to view the zoneminder interface at ``http://localhost/zm`` (replace localhost with your server IP if you are accessing it remotely) -.. image:: images/zm_first_screen_post_install.png +**Step 7: make sure live streaming works**: Make sure you can view Monitor streams: + +startup ZM console in your browser, go to ``Options->Path`` and make sure ``PATH_ZMS`` is set to ``/zm/cgi-bin/nph-zms`` and restart ZM (you should not need to do this for packages, as this should automatically work) + + +**Step 8**: If you have changed your DB login/password from zmuser/zmpass, the API won't know about it + +If you changed the DB password **after** installing ZM, the APIs will not be able to connect to the DB. + +If you have, go to ``zoneminder/www/api/app/Config`` & Edit ``database.php`` + +There is a class there called ``DATABASE_CONFIG`` - change the ``$default`` array to reflect your new details. Example: + +:: + + public $default = array( + 'datasource' => 'Database/Mysql', + 'persistent' => false, + 'host' => 'localhost', + 'login' => 'mynewDBusername', + 'password' => 'mynewDBpassword' + 'database' => 'zm', + 'prefix' => '', + //'encoding' => 'utf8', + ); + + +You are done. Lets proceed to make sure everything works: + +Making sure ZM and APIs work: + +1. open up a browser and go to ``http://localhost/zm`` - should bring up ZM +2. (OPTIONAL - just for peace of mind) open up a tab and go to ``http://localhost/zm/api`` - should bring up a screen showing CakePHP version with some green color boxes. Green is good. If you see red, or you don't see green, there may be a problem (should not happen). Ignore any warnings in yellow saying "DebugKit" not installed. You don't need it +3. open up a tab in the same browser and go to ``http://localhost/zm/api/host/getVersion.json`` + +If it responds with something like: + +:: + + { + "version": "1.28.107", + "apiversion": "1.28.107.1" + } + + +**Then your APIs are working** + +Make sure ZM and APIs work with security: +1. Enable OPT_AUTH in ZM +2. Log out of ZM in browser +3. Open a NEW tab in the SAME BROWSER (important) and go to ``http://localhost/zm/api/host/getVersion.json`` - should give you "Unauthorized" along with a lot more of text +4. Go to another tab in the SAME BROWSER (important) and log into ZM +5. Repeat step 3 and it should give you the ZM and API version + +**Congrats** your installation is complete + + +Easy Way: Install ZoneMinder from a package (Ubuntu 14.x) +----------------------------------------------------------- +**These instructions are for a brand new ubuntu 14.x system which does not have ZM installed.** + +**Step 1:** Install ZoneMinder + +:: + + sudo add-apt-repository ppa:iconnor/zoneminder-master + sudo apt-get update + sudo apt-get install zoneminder + +(just press OK for the prompts you get) + +**Step 2:** Set up DB + +:: + + sudo mysql -uroot -p < /usr/share/zoneminder/db/zm_create.sql + mysql -uroot -p -e "grant select,insert,update,delete,create,alter,index,lock tables on zm.* to 'zmuser'@localhost identified by 'zmpass';" + +**Step 3:** Set up Apache + +:: + + sudo a2enconf zoneminder + sudo a2enmod rewrite + sudo a2enmod cgi + +**Step 4:**:Some tweaks that will be needed: + +Edit /etc/init.d/zoneminder: + +add a ``sleep 10`` right after line 25 that reads ``echo -n "Starting $prog:"`` +(The reason we need this sleep is to make sure ZM starts after mysqld starts) + +As of Oct 3 2015, zm.conf is not readable by ZM. This is likely a bug and will go away in the next package + +:: + + sudo chown www-data:www-data /etc/zm/zm.conf + + + +**Step 5**: If you have changed your DB login/password + +If you changed the DB password **after** installing ZM, the APIs will not be able to connect to the DB. + +If you have, go to zoneminder/www/api/app/Config & Edit ``database.php`` + +There is a class there called ``DATABASE_CONFIG`` - change the ``$default`` array to reflect your new details. Example: + +:: + + public $default = array( + 'datasource' => 'Database/Mysql', + 'persistent' => false, + 'host' => 'localhost', + 'login' => 'mynewDBusername', + 'password' => 'mynewDBpassword' + 'database' => 'zm', + 'prefix' => '', + //'encoding' => 'utf8',` + ); + +We also need to install php5-gd (as of 1.28.107, this is not installed) + +:: + + sudo apt-get install php5-gd + + +**Step 6**: Edit Timezone in PHP + +vi /etc/php5/apache2/php.ini +Look for [Date] and inside it you will see a date.timezone +that is commented. remove the comment and specific your timezone. +Please make sure the timezone is valid (see [this](http://php.net/manual/en/timezones.php)) + +In my case: + +:: + + date.timezone = America/New_York + + +**Step 7: make sure live streaming works**: Make sure you can view Monitor streams: + +startup ZM console in your browser, go to ``Options->Path`` and make sure ``PATH_ZMS`` is set to ``/zm/cgi-bin/nph-zms`` and restart ZM (you should not need to do this for packages, as this should automatically work) + + + +restart: + +:: + + sudo service apache2 restart + /etc/init.d/zoneminder restart + +**Step 8**: Making sure ZM and APIs work: (optional - only if you need APIs) + +1. open up a browser and go to ``http://localhost/zm`` - should bring up ZM +2. (OPTIONAL - just for peace of mind) open up a tab and go to ``http://localhost/zm/api`` - should bring up a screen showing CakePHP version with some green color boxes. Green is good. If you see red, or you don't see green, there may be a problem (should not happen). Ignore any warnings in yellow saying "DebugKit" not installed. You don't need it +3. open up a tab in the same browser and go to ``http://localhost/zm/api/host/getVersion.json`` + +If it responds with something like: + +:: + + { + "version": "1.28.107", + "apiversion": "1.28.107.1" + } + +Then your APIs are working + +Make sure you can view Monitor View: +1. Open up ZM, configure your monitors and verify you can view Monitor feeds. +2. If not, open up ZM console in your browser, go to ``Options->Path`` and make sure ``PATH_ZMS`` is set to ``/zm/cgi-bin/nph-zms`` and restart ZM (you should not need to do this for packages, as this should automatically work) + +Make sure ZM and APIs work with security: +1. Enable OPT_AUTH in ZM +2. Log out of ZM in browser +3. Open a NEW tab in the SAME BROWSER (important) and go to ``http://localhost/zm/api/host/getVersion.json`` - should give you "Unauthorized" along with a lot more of text +4. Go to another tab in the SAME BROWSER (important) and log into ZM +5. Repeat step 3 and it should give you the ZM and API version + +**Congrats** Your installation is complete -Post install steps for Ubuntu 14.x or older (SystemV) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Harder Way: Build Package From Source +------------------------------------------- +(These instructions assume installation from source on a ubuntu 15.x+ system) + +**Step 1:** First make sure you have the needed tools + +:: + + sudo apt-get update + sudo apt-get install cmake git + +**Step 2:** Next up make sure you have all the dependencies + +:: + + sudo apt-get install apache2 mysql-server php5 php5-mysql build-essential libmysqlclient-dev libssl-dev libbz2-dev libpcre3-dev libdbi-perl libarchive-zip-perl libdate-manip-perl libdevice-serialport-perl libmime-perl libpcre3 libwww-perl libdbd-mysql-perl libsys-mmap-perl yasm automake autoconf libjpeg8-dev libjpeg8 apache2 libapache2-mod-php5 php5-cli libphp-serialization-perl libgnutls-dev libjpeg8-dev libavcodec-dev libavformat-dev libswscale-dev libavutil-dev libv4l-dev libtool ffmpeg libnetpbm10-dev libavdevice-dev libmime-lite-perl dh-autoreconf dpatch policykit-1 libpolkit-gobject-1-dev libextutils-pkgconfig-perl libcurl3 libvlc-dev libcurl4-openssl-dev curl php5-gd + +(you are asked for the mysql root password when installing mysql server - put in a password that you'd like). + +**Step 3:** Download ZoneMinder source code and compile+install: + +:: + + git clone https://github.com/ZoneMinder/ZoneMinder.git + cd ZoneMinder/ + git submodule init + git submodule update + cmake . + make + sudo make install + +**Step 4:** Now make sure your symlinks to events and images are set correctly: + +:: + + sudo ./zmlinkcontent.sh + +**Step 5:** Now lets make sure ZM has DB permissions to write to the DB: + +:: + + mysql -uroot -p -e "grant select,insert,update,delete,create,alter,index,lock tables on zm.* to 'zmuser'@localhost identified by 'zmpass';" + +**Step 6:** Now lets create the DB & its tables that ZM needs + +:: + + mysql -uroot -p + Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch + AllowOverride All + Require all granted + + + Alias /zm /usr/local/share/zoneminder/www + + php_flag register_globals off + Options Indexes FollowSymLinks + + DirectoryIndex index.php + + + + + AllowOverride All + + +**Step 11:** Now lets make sure ZM can read/write to the zoneminder directory: + +:: + + sudo chown -R www-data:www-data /usr/local/share/zoneminder/ + + +**Step 12:** Make sure you can view Monitor View + +1. Open up ZM, configure your monitors and verify you can view Monitor feeds +2. If not, open up ZM console in your browser, go to ``Options->Path`` and make sure ``PATH_ZMS`` is set to ``/zm/cgi-bin/nph-zms`` and restart ZM + +**Step 13**: Edit Timezone in PHP + +vi /etc/php5/apache2/php.ini +Look for [Date] and inside it you will see a date.timezone +that is commented. remove the comment and specific your timezone. +Please make sure the timezone is valid (see http://php.net/manual/en/timezones.php) + +In my case: + +:: + + date.timezone = America/New_York + +**Step 14:** Finally, lets make a config change to apache (needed for htaccess overrides to work for APIs) +Edit /etc/apache2/apache2.conf and add this: + +:: + + + AllowOverride All + Require all granted + + +Restart apache + +:: + + sudo service apache2 reload + +You are done. Lets proceed to make sure everything works: + +Making sure ZM and APIs work: + +1. open up a browser and go to ``http://localhost/zm`` - should bring up ZM +2. (OPTIONAL - just for peace of mind) open up a tab and go to ``http://localhost/zm/api`` - should bring up a screen showing CakePHP version with some green color boxes. Green is good. If you see red, or you don't see green, there may be a problem (should not happen). Ignore any warnings in yellow saying "DebugKit" not installed. You don't need it +3. open up a tab in the same browser and go to ``http://localhost/zm/api/host/getVersion.json`` + +If it responds with something like: + +:: + + { + "version": "1.28.107", + "apiversion": "1.28.107.1" + } + +Then your APIs are working + +Make sure ZM and APIs work with security: +1. Enable OPT_AUTH in ZM +2. Log out of ZM in browser +3. Open a NEW tab in the SAME BROWSER (important) and go to ``http://localhost/zm/api/host/getVersion.json`` - should give you "Unauthorized" along with a lot more of text +4. Go to another tab in the SAME BROWSER (important) and log into ZM +5. Repeat step 3 and it should give you the ZM and API version + +**Congrats** your installation is complete + Suggested changes to MySQL (Optional but recommended) -""""""""""""""""""""""""""""""""""""""""""""""""""""""""" +------------------------------------------------------ For most of you Zoneminder will run just fine with the default MySQL settings. There are a couple of settings that may, in time, provide beneficial especially if you have a number of cameras and many events with a lot of files. One setting we recommend is the "innodb_file_per_table" This will be a default setting in MySQL 5.6 but should be added in MySQL 5.5 which comes with Ubuntu 14.04. A description can be found here: http://dev.mysql.com/doc/refman/5.5/en/innodb-multiple-tablespaces.html To add "innodb_file_per_table" edit the my.cnf file: @@ -72,90 +474,4 @@ Save and exit. Restart MySQL ``service mysql restart`` -Adding a sleep for mysql dependency (Ubuntu 14.x and below only) -"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" -We recommend you add a "sleep" command just after ``start() {`` in ``/etc/init.d/zoneminder`` to make sure mysql starts before ZoneMinder does. To do this, -simply modify ``/etc/init.d/zoneminder`` at around line 25 (where you will find the start function) to look like this: - -:: - - start() { - echo -n "Making sure mysql started... Sleeping for 10 seconds..." - sleep 10 - echo -n "Starting $prog: " - -Making Apache aware of ZoneMinder -"""""""""""""""""""""""""""""""""""""""""""" - -Next, we need to make sure apache knows about zoneminder's configuration for apache. - -:: - - ln -s /etc/zm/apache.conf /etc/apache2/conf-available/zoneminder.conf - -If you are upgrading from an old version: - -:: - - sudo /usr/bin/zmupdate.pl - -Then look for ``/etc/apache2/conf-enable/zm.conf`` - if you have it, please remove it - -:: - - a2enconf zoneminder - adduser www-data video - - -lets make sure we restart apache: - -:: - - service apache2 restart - - -You should now be able to view the zoneminder interface at ``http://localhost/zm`` (replace localhost with your server IP if you are accessing it remotely) - -.. image:: images/zm_first_screen_post_install.png - - -Finally, in the zoneminder web interface, go to Options->Paths, change PATH_ZMS to ``/zm/cgi-bin/nph-zms`` - - -Option B: Build Package From Source -------------------------------------------- - -A fresh build based on master branch running Ubuntu 1204 LTS. Will likely work for other versions as well.:: - - root@host:~# aptitude install -y apache2 mysql-server php5 php5-mysql build-essential libmysqlclient-dev libssl-dev libbz2-dev libpcre3-dev libdbi-perl libarchive-zip-perl libdate-manip-perl libdevice-serialport-perl libmime-perl libpcre3 libwww-perl libdbd-mysql-perl libsys-mmap-perl yasm automake autoconf libjpeg8-dev libjpeg8 apache2-mpm-prefork libapache2-mod-php5 php5-cli libphp-serialization-perl libgnutls-dev libjpeg8-dev libavcodec-dev libavformat-dev libswscale-dev libavutil-dev libv4l-dev libtool ffmpeg libnetpbm10-dev libavdevice-dev libmime-lite-perl dh-autoreconf dpatch; - - root@host:~# git clone https://github.com/ZoneMinder/ZoneMinder.git zoneminder; - root@host:~# cd zoneminder; - root@host:~# ln -s distros/ubuntu1204 debian; - root@host:~# dpkg-checkbuilddeps; - root@host:~# dpkg-buildpackage; - - -One level above you'll now find a deb package matching the architecture of the build host\::: - - root@host:~# ls -1 ~/zoneminder\*; - /root/zoneminder_1.26.4-1_amd64.changes - /root/zoneminder_1.26.4-1_amd64.deb - /root/zoneminder_1.26.4-1.dsc - /root/zoneminder_1.26.4-1.tar.gz - - -The dpkg command itself does not resolve dependencies. That's what high-level interfaces like aptitude and apt-get are normally for. Unfortunately, unlike RPM, there's no easy way to install a separate deb package not contained with any repository. - -To overcome this "limitation" we'll use dpkg only to install the zoneminder package and apt-get to fetch all needed dependencies afterwards. Running dpkg-reconfigure in the end will ensure that the setup scripts e.g. for database provisioning were executed.:: - - root@host:~# dpkg -i /root/zoneminder_1.26.4-1_amd64.deb; apt-get install -f; - root@host:~# dpkg-reconfigure zoneminder; - -Alternatively you may also use gdebi to automatically resolve dependencies during installation\::: - - root@host:~# aptitude install -y gdebi; - root@host:~# gdebi /root/zoneminder_1.26.4-1_amd64.deb; - - sudo apt-get install apache2 mysql-server php5 php5-mysql build-essential libmysqlclient-dev libssl-dev libbz2-dev libpcre3-dev libdbi-perl libarchive-zip-perl libdate-manip-perl libdevice-serialport-perl libmime-perl libpcre3 libwww-perl libdbd-mysql-perl libsys-mmap-perl yasm automake autoconf libjpeg-turbo8-dev libjpeg-turbo8 apache2-mpm-prefork libapache2-mod-php5 php5-cli From b444061cc6993618fc56805381f276f7087f58a4 Mon Sep 17 00:00:00 2001 From: arjunrc Date: Fri, 18 Dec 2015 15:11:05 -0500 Subject: [PATCH 16/20] removed -master from ppa - when 1.29 releases it will be stable --- docs/installationguide/ubuntu.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installationguide/ubuntu.rst b/docs/installationguide/ubuntu.rst index fa065cb96..1d5412077 100644 --- a/docs/installationguide/ubuntu.rst +++ b/docs/installationguide/ubuntu.rst @@ -11,7 +11,7 @@ These instructions are for a brand new ubuntu 15.04 system which does not have Z :: - sudo add-apt-repository ppa:iconnor/zoneminder-master + sudo add-apt-repository ppa:iconnor/zoneminder sudo apt-get update if you don't have mysql already installed: @@ -158,7 +158,7 @@ Easy Way: Install ZoneMinder from a package (Ubuntu 14.x) :: - sudo add-apt-repository ppa:iconnor/zoneminder-master + sudo add-apt-repository ppa:iconnor/zoneminder sudo apt-get update sudo apt-get install zoneminder From 3fe64f8ce656c97a252e25fda442622f8911991d Mon Sep 17 00:00:00 2001 From: arjunrc Date: Sat, 19 Dec 2015 13:38:54 -0500 Subject: [PATCH 17/20] rtd theme --- docs/conf.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1e7f35737..11d1fb392 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -98,14 +98,15 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +#html_theme = 'default' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -html_theme_options = { - "stickysidebar": "true" -} +#html_theme_options = { +# "stickysidebar": "true" +#} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] @@ -129,8 +130,8 @@ html_theme_options = { # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] -html_style='zmstyles.css' +#html_static_path = ['_static'] +#html_style='zmstyles.css' # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied From 2763c3d92ed8235967c15c5126ec9b92e5e278e3 Mon Sep 17 00:00:00 2001 From: arjunrc Date: Sat, 19 Dec 2015 13:40:21 -0500 Subject: [PATCH 18/20] deleted --- docs/_static/zmstyles.css | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 docs/_static/zmstyles.css diff --git a/docs/_static/zmstyles.css b/docs/_static/zmstyles.css deleted file mode 100644 index 8f59f3910..000000000 --- a/docs/_static/zmstyles.css +++ /dev/null @@ -1,13 +0,0 @@ -@import url("default.css"); - -div.admonition-note { -border-top: 2px solid red; -border-bottom: 2px solid red; -border-left: 2px solid red; -border-right: 2px solid red; -background-color: #ff6347 -} - -img { - border: 1px solid black !important; -} From c4bdb127ce4b4c98ea988dfb025a79425e82b1c4 Mon Sep 17 00:00:00 2001 From: arjunrc Date: Sat, 19 Dec 2015 16:53:30 -0500 Subject: [PATCH 19/20] fixed custom style breaking rtd theme --- docs/conf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 11d1fb392..109161b24 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,6 +15,9 @@ import sys import os +def setup(app): + app.add_stylesheet('zmstyle.css') + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -130,7 +133,7 @@ html_theme = 'sphinx_rtd_theme' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -#html_static_path = ['_static'] +html_static_path = ['_static'] #html_style='zmstyles.css' # Add any extra paths that contain custom files (such as robots.txt or From cbea550d70f58f4d8d2ba2fd137d99623d17bbd5 Mon Sep 17 00:00:00 2001 From: arjunrc Date: Sat, 19 Dec 2015 16:53:47 -0500 Subject: [PATCH 20/20] custom theming for ZM --- docs/_static/zmstyle.css | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/_static/zmstyle.css diff --git a/docs/_static/zmstyle.css b/docs/_static/zmstyle.css new file mode 100644 index 000000000..b8f0235f3 --- /dev/null +++ b/docs/_static/zmstyle.css @@ -0,0 +1,3 @@ +img { + border: 1px solid black !important; + }