From 0e5c2af981c14709753364c24dbbb7c245b27f06 Mon Sep 17 00:00:00 2001 From: pucherot Date: Mon, 1 Feb 2021 21:30:51 +0100 Subject: [PATCH] v2.70 --- back/pialert.py | 78 +- back/update_vendors.sh | 2 +- config/version.conf | 4 +- db/pialert.db | Bin 159744 -> 167936 bytes docs/VERSIONS_HISTORY.md | 17 + front/css/pialert.css | 36 +- front/deviceDetails.php | 890 ++++++++++++--------- front/devices.php | 325 ++++---- front/events.php | 308 ++++--- front/index.php | 325 ++++---- front/js/pialert_common.js | 86 +- front/php/server/db.php | 62 +- front/php/server/devices.php | 588 ++++++++------ front/php/server/events.php | 170 ++-- front/php/server/parameters.php | 88 ++ front/php/server/util.php | 17 +- front/php/templates/footer.php | 30 +- front/php/templates/header.php | 133 +-- front/php/templates/notification.php | 35 + front/presence.php | 236 ++---- install/pialert_install.sh | 187 ++--- install/pialert_update.sh | 55 +- tar/create_tar.sh | 1 - tar/{pialert_2.61.tar => pialert_2.70.tar} | Bin 58572800 -> 58583040 bytes tar/pialert_latest.tar | Bin 58572800 -> 58583040 bytes 25 files changed, 2011 insertions(+), 1662 deletions(-) create mode 100644 front/php/server/parameters.php create mode 100644 front/php/templates/notification.php rename tar/{pialert_2.61.tar => pialert_2.70.tar} (99%) diff --git a/back/pialert.py b/back/pialert.py index 80611dfa..781b1032 100644 --- a/back/pialert.py +++ b/back/pialert.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # #------------------------------------------------------------------------------- -# Pi.Alert v2.61 / 2021-01-25 +# Pi.Alert v2.70 / 2021-02-01 # Open Source Network Guard / WIFI & LAN intrusion detector # # pialert.py - Back module. Network scanner @@ -398,6 +398,10 @@ def scan_network (): print (' Updating Devices Info...') update_devices_data_from_scan () + # Resolve devices names + print_log (' Resolve devices names...') + update_devices_names() + # Void false connection - disconnections print (' Voiding false (ghost) disconnections...') void_ghost_disconnections () @@ -905,6 +909,78 @@ def update_devices_data_from_scan (): print_log ('Update devices end') +#------------------------------------------------------------------------------- +# Feature #43 - Resoltion name for unknown devices +def update_devices_names (): + # Initialize variables + recordsToUpdate = [] + ignored = 0 + notFound = 0 + + # Devices without name + print (' Trying to resolve devices without name...', end='') + for device in sql.execute ("SELECT * FROM Devices WHERE dev_Name IN ('(unknown)','') ") : + # Resolve device name + newName = resolve_device_name (device['dev_MAC'], device['dev_LastIP']) + + if newName == -1 : + notFound += 1 + elif newName == -2 : + ignored += 1 + else : + recordsToUpdate.append ([newName, device['dev_MAC']]) + # progress bar + print ('.', end='') + sys.stdout.flush() + + # Print log + print ('') + print (" Names updated: ", len(recordsToUpdate) ) + # DEBUG - print list of record to update + # print (recordsToUpdate) + + # update devices + sql.executemany ("UPDATE Devices SET dev_Name = ? WHERE dev_MAC = ? ", recordsToUpdate ) + + # DEBUG - print number of rows updated + # print (sql.rowcount) + +#------------------------------------------------------------------------------- +def resolve_device_name (pMAC, pIP): + try : + pMACstr = str(pMAC) + + # Check MAC parameter + mac = pMACstr.replace (':','') + if len(pMACstr) != 17 or len(mac) != 12 : + return -2 + + # Resolve name with DIG + dig_args = ['dig', '+short', '-x', pIP] + newName = subprocess.check_output (dig_args, universal_newlines=True) + + # Check if Eliminate local domain + newName = newName.strip() + if len(newName) == 0 : + return -2 + + # Eliminate local domain + if newName.endswith('.') : + newName = newName[:-1] + if newName.endswith('.lan') : + newName = newName[:-4] + if newName.endswith('.local') : + newName = newName[:-6] + if newName.endswith('.home') : + newName = newName[:-5] + + # Return newName + return newName + + # not Found + except subprocess.CalledProcessError : + return -1 + #------------------------------------------------------------------------------- def void_ghost_disconnections (): # Void connect ghost events (disconnect event exists in last X min.) diff --git a/back/update_vendors.sh b/back/update_vendors.sh index 2044705f..9d93574b 100644 --- a/back/update_vendors.sh +++ b/back/update_vendors.sh @@ -3,7 +3,7 @@ # Pi.Alert # Open Source Network Guard / WIFI & LAN intrusion detector # -# vendors_db_update.sh - Back module. IEEE Vendors db update +# update_vendors.sh - Back module. IEEE Vendors db update # ------------------------------------------------------------------------------ # Puche 2021 pi.alert.application@gmail.com GNU GPLv3 # ------------------------------------------------------------------------------ diff --git a/config/version.conf b/config/version.conf index e95ea5b7..7fc3c716 100644 --- a/config/version.conf +++ b/config/version.conf @@ -1,3 +1,3 @@ -VERSION = '2.61' +VERSION = '2.70' VERSION_YEAR = '2021' -VERSION_DATE = '2021-01-25' +VERSION_DATE = '2021-02-01' diff --git a/db/pialert.db b/db/pialert.db index a91387e9ccc5ccc09158af106571600a99cdd063..f094d8e2470b89e616af177559e9838dfc57fb6e 100644 GIT binary patch delta 649 zcmZp8z}c{XYl5_3Dgy(9CJ;jbQ}aX}W5(2t3G?|G^O^*<2{5uG$nY?HVBp`(cZJWL z_Yj90yD{4zwkZrB*iuj$-11&r^_@l%1#d8 zVA#8tu1-`O$Pb^66F#;4OS_A}~lpHjwHz{w)Z z5Xmr|uYu8sgU6QV6!#tOnu(6$+antoOIW12E-^^!D>Je+vax~v+hoMn$OB<+f6>DD zij{@`BEzcblB*c=r@MADnybe)^0SHSOEcDI=B1=oc)CQyqj&=1m1525h24yz^$3sn z`zb)B6f{sYXvQ`2v5PAzGB&cqO)N+(iuZI0NGwXsO)W_+D%Nlgas~O%)6c~!84X7wQIK05&q1njU$OEw=wj?nrCl#Ru!BU4>5#s3Nr%wT#cMxIP)3aFic>u0vZMcY(3KtG&9OEN>1On zf>CsO-U>!}MzQIAD;T3WMcKslrI{}j diff --git a/docs/VERSIONS_HISTORY.md b/docs/VERSIONS_HISTORY.md index 18332ec5..115a2338 100644 --- a/docs/VERSIONS_HISTORY.md +++ b/docs/VERSIONS_HISTORY.md @@ -3,6 +3,8 @@ | Version | Description | | ------- | --------------------------------------------------------------- | + | v2.70 | New features & Usability improvements in the web prontal | + | v2.61 | Bug fixing | | v2.60 | Improved the compability of installation process (Ubuntu) | | v2.56 | Bug fixing | | v2.55 | Bug fixing | @@ -11,6 +13,21 @@ | v2.50 | First public release | +## Pi.Alert v2.70 + + - Added Client names resolution #43 + - Added Check to mark devices as "known" #16 + - Remember "Show XXX entries" dropdown value #16 #26 + - Remember "sorting" in devices #16 + - Remember "Device panel " in device detail #16 + - Added "All" option to "Show x Entries" option #16 + - Added optional Location field (Door, Basement, etc.) to devices #16 + - "Device updated successfully" message now is not modal #16 + - Now is possible to delete Devices #16 + - Added Device Type Singleboard Computer (SBC) #16 + - Allowed to use " in device name #42 + + ## Pi.Alert v2.60 - `pialert.conf` moved from `back` to `config` folder diff --git a/front/css/pialert.css b/front/css/pialert.css index 75e68839..43378ec9 100644 --- a/front/css/pialert.css +++ b/front/css/pialert.css @@ -1,6 +1,11 @@ -/******************************************************************************* -* Pi.alert CSS -*******************************************************************************/ +/* ----------------------------------------------------------------------------- +# Pi.Alert +# Open Source Network Guard / WIFI & LAN intrusion detector +# +# pialert.css - Front module. CSS styles +#------------------------------------------------------------------------------- +# Puche 2021 pi.alert.application@gmail.com GNU GPLv3 +----------------------------------------------------------------------------- */ /* ----------------------------------------------------------------------------- Global Variables @@ -371,3 +376,28 @@ z-index: 100; } +/* ----------------------------------------------------------------------------- + Notification float banner +----------------------------------------------------------------------------- */ +.pa_alert_notification { + text-align: center; + font-size: large; + font-weight: bold; + color: #258744; + + background-color: #d4edda; + border-color: #c3e6cb; + border-radius: 5px; + + max-width: 1000px; /* 80% wrapper 1250px */ + width: 80%; + z-index: 9999; + + position: fixed; + top: 30px; + margin: auto; + transform: translate(0,0); + + display: none; +} + diff --git a/front/deviceDetails.php b/front/deviceDetails.php index f15bf508..e452bfcd 100644 --- a/front/deviceDetails.php +++ b/front/deviceDetails.php @@ -1,4 +1,12 @@ - + + @@ -8,15 +16,15 @@
+ +

 Quering device info...

- - Sessions, Presence & Alerts period: - - @@ -24,92 +32,73 @@ -
-
+ + -
- +
+ +
- @@ -154,10 +116,13 @@
+ +

Events

- + +
@@ -179,6 +144,7 @@
+
@@ -208,86 +174,113 @@ diff --git a/front/index.php b/front/index.php index 58937cdc..1d3ecead 100644 --- a/front/index.php +++ b/front/index.php @@ -1,4 +1,12 @@ - + + @@ -11,102 +19,73 @@

Devices

- - - - New Devices period: - - -
+ +
- - + - + - + @@ -118,12 +97,15 @@
+ +

Devices

- + +
- +
@@ -142,6 +124,7 @@
Name
+
@@ -171,169 +154,165 @@ diff --git a/front/js/pialert_common.js b/front/js/pialert_common.js index 5778f71f..84408340 100644 --- a/front/js/pialert_common.js +++ b/front/js/pialert_common.js @@ -1,9 +1,89 @@ /* ----------------------------------------------------------------------------- - Pi.Alert Common Javascript functions +* Pi.Alert +* Open Source Network Guard / WIFI & LAN intrusion detector +* +* pialert_common.js - Front module. Common Javascript functions +*------------------------------------------------------------------------------- +* Puche 2021 pi.alert.application@gmail.com GNU GPLv3 ----------------------------------------------------------------------------- */ // ----------------------------------------------------------------------------- var timerRefreshData = '' +var modalCallbackFunction = ''; + + +// ----------------------------------------------------------------------------- +function showModal (title, message, btnCancel, btnOK, callbackFunction) { + // set captions + $('#modal-title').html (title); + $('#modal-message').html (message); + $('#modal-cancel').html (btnCancel); + $('#modal-OK').html (btnOK); + modalCallbackFunction = callbackFunction; + + // Show modal + $('#modal-warning').modal('show'); +} + +// ----------------------------------------------------------------------------- +function modalOK () { + // Hide modal + $('#modal-warning').modal('hide'); + + // timer to execute function + window.setTimeout( function() { + window[modalCallbackFunction](); + }, 100); +} + +// ----------------------------------------------------------------------------- +function showMessage (textMessage="") { + if (textMessage.toLowerCase().includes("error") ) { + // show error + alert (textMessage); + } else { + // show temporal notification + $("#alert-message").html (textMessage); + $("#notification").fadeIn(1, function () { + window.setTimeout( function() { + $("#notification").fadeOut(500) + }, 3000); + } ); + } +} + + +// ----------------------------------------------------------------------------- +function setParameter (parameter, value) { + // Retry + $.get('php/server/parameters.php?action=set¶meter=' + parameter + + '&value='+ value, + function(data) { + if (data != "OK") { + // Retry + sleep (200); + $.get('php/server/parameters.php?action=set¶meter=' + parameter + + '&value='+ value, + function(data) { + if (data != "OK") { + // alert (data); + } else { + // alert ("OK. Second attempt"); + }; + } ); + }; + } ); +} + + +// ----------------------------------------------------------------------------- +function sleep(milliseconds) { + const date = Date.now(); + let currentDate = null; + do { + currentDate = Date.now(); + } while (currentDate - date < milliseconds); +} // ----------------------------------------------------------------------------- @@ -35,5 +115,7 @@ function newTimerRefreshData (refeshFunction) { // ----------------------------------------------------------------------------- function debugTimer () { - document.getElementById ('pageTitle').innerHTML = (new Date().getSeconds()); + $('#pageTitle').html (new Date().getSeconds()); } + + diff --git a/front/php/server/db.php b/front/php/server/db.php index f4ffd6ef..8e3e7706 100644 --- a/front/php/server/db.php +++ b/front/php/server/db.php @@ -1,32 +1,40 @@ diff --git a/front/php/server/devices.php b/front/php/server/devices.php index aa76c287..1765a473 100644 --- a/front/php/server/devices.php +++ b/front/php/server/devices.php @@ -1,14 +1,25 @@ query($sql); + $row = $result -> fetchArray (SQLITE3_ASSOC); + $deviceData = $row; + + $deviceData['dev_FirstConnection'] = formatDate ($row['dev_FirstConnection']); // Date formated + $deviceData['dev_LastConnection'] = formatDate ($row['dev_LastConnection']); // Date formated + + // Count Totals + $condition = ' WHERE eve_MAC="'. $mac .'" AND eve_DateTime >= '. $periodDate; + + // Connections + $sql = 'SELECT COUNT(*) FROM Sessions + WHERE ses_MAC="'. $mac .'" + AND ( ses_DateTimeConnection >= '. $periodDate .' + OR ses_DateTimeDisconnection >= '. $periodDate .' + OR ses_StillConnected = 1 )'; + $result = $db->query($sql); + $row = $result -> fetchArray (SQLITE3_NUM); + $deviceData['dev_Sessions'] = $row[0]; + + // Events + $sql = 'SELECT COUNT(*) FROM Events '. $condition .' AND eve_EventType <> "Connected" AND eve_EventType <> "Disconnected" '; + $result = $db->query($sql); + $row = $result -> fetchArray (SQLITE3_NUM); + $deviceData['dev_Events'] = $row[0]; + + // Donw Alerts + $sql = 'SELECT COUNT(*) FROM Events '. $condition .' AND eve_EventType = "Device Down"'; + $result = $db->query($sql); + $row = $result -> fetchArray (SQLITE3_NUM); + $deviceData['dev_DownAlerts'] = $row[0]; + + // Presence hours + $sql = 'SELECT SUM (julianday (IFNULL (ses_DateTimeDisconnection, DATETIME("now"))) + - julianday (CASE WHEN ses_DateTimeConnection < '. $periodDate .' THEN '. $periodDate .' + ELSE ses_DateTimeConnection END)) *24 + FROM Sessions + WHERE ses_MAC="'. $mac .'" + AND ses_DateTimeConnection IS NOT NULL + AND (ses_DateTimeDisconnection IS NOT NULL OR ses_StillConnected = 1 ) + AND ( ses_DateTimeConnection >= '. $periodDate .' + OR ses_DateTimeDisconnection >= '. $periodDate .' + OR ses_StillConnected = 1 )'; + $result = $db->query($sql); + $row = $result -> fetchArray (SQLITE3_NUM); + $deviceData['dev_PresenceHours'] = round ($row[0]); + + // Return json + echo (json_encode ($deviceData)); +} + + +//------------------------------------------------------------------------------ +// Update Device Data +//------------------------------------------------------------------------------ +function setDeviceData() { + global $db; + + // sql + $sql = 'UPDATE Devices SET + dev_Name = "'. quotes($_REQUEST['name']) .'", + dev_Owner = "'. quotes($_REQUEST['owner']) .'", + dev_DeviceType = "'. quotes($_REQUEST['type']) .'", + dev_Vendor = "'. quotes($_REQUEST['vendor']) .'", + dev_Favorite = "'. quotes($_REQUEST['favorite']) .'", + dev_Group = "'. quotes($_REQUEST['group']) .'", + dev_Location = "'. quotes($_REQUEST['location']) .'", + dev_Comments = "'. quotes($_REQUEST['comments']) .'", + dev_StaticIP = "'. quotes($_REQUEST['staticIP']) .'", + dev_ScanCycle = "'. quotes($_REQUEST['scancycle']) .'", + dev_AlertEvents = "'. quotes($_REQUEST['alertevents']) .'", + dev_AlertDeviceDown = "'. quotes($_REQUEST['alertdown']) .'", + dev_SkipRepeated = "'. quotes($_REQUEST['skiprepeated']) .'", + dev_NewDevice = "'. quotes($_REQUEST['newdevice']) .'" + WHERE dev_MAC="' . $_REQUEST['mac'] .'"'; + // update Data + $result = $db->query($sql); + + // check result + if ($result == TRUE) { + echo "Device updated successfully"; + } else { + echo "Error updating device\n\n$sql \n\n". $db->lastErrorMsg(); + } +} + + +//------------------------------------------------------------------------------ +// Delete Device +//------------------------------------------------------------------------------ +function deleteDevice() { + global $db; + + // sql + $sql = 'DELETE FROM Devices WHERE dev_MAC="' . $_REQUEST['mac'] .'"'; + // execute sql + $result = $db->query($sql); + + // check result + if ($result == TRUE) { + echo "Device deleted successfully"; + } else { + echo "Error deleting device\n\n$sql \n\n". $db->lastErrorMsg(); + } +} + + //------------------------------------------------------------------------------ // Query total numbers of Devices by status //------------------------------------------------------------------------------ -function queryTotals() { +function getDevicesTotals() { global $db; // All @@ -42,43 +182,42 @@ function queryTotals() { $devices = $row[0]; // Connected - $result = $db->query('SELECT COUNT(*) FROM Devices ' . getDeviceCondition ('connected') ); + $result = $db->query('SELECT COUNT(*) FROM Devices '. getDeviceCondition ('connected') ); $row = $result -> fetchArray (SQLITE3_NUM); $connected = $row[0]; // New - $result = $db->query('SELECT COUNT(*) FROM Devices ' . getDeviceCondition ('new') ); + $result = $db->query('SELECT COUNT(*) FROM Devices '. getDeviceCondition ('new') ); $row = $result -> fetchArray (SQLITE3_NUM); $newDevices = $row[0]; // Down Alerts - $result = $db->query('SELECT COUNT(*) FROM Devices ' . getDeviceCondition ('down')); + $result = $db->query('SELECT COUNT(*) FROM Devices '. getDeviceCondition ('down')); $row = $result -> fetchArray (SQLITE3_NUM); $devicesDownAlert = $row[0]; - echo (json_encode (array ($devices, $connected, $newDevices, $devicesDownAlert))); + echo (json_encode (array ($devices, $connected, $newDevices, + $devicesDownAlert))); } //------------------------------------------------------------------------------ // Query the List of devices in a determined Status //------------------------------------------------------------------------------ -function queryList() { +function getDevicesList() { global $db; - // Request Parameters - $periodDate = getDateFromPeriod(); - // SQL $condition = getDeviceCondition ($_REQUEST['status']); - $result = $db->query('SELECT *, - CASE WHEN dev_AlertDeviceDown=1 AND dev_PresentLastScan=0 THEN "Down" - WHEN dev_FirstConnection >= ' . $periodDate . ' THEN "New" - WHEN dev_PresentLastScan=1 THEN "On-line" - ELSE "Off-line" - END AS dev_Status - FROM Devices ' . $condition); + $sql = 'SELECT *, CASE + WHEN dev_AlertDeviceDown=1 AND dev_PresentLastScan=0 THEN "Down" + WHEN dev_NewDevice=1 THEN "New" + WHEN dev_PresentLastScan=1 THEN "On-line" + ELSE "Off-line" + END AS dev_Status + FROM Devices '. $condition; + $result = $db->query($sql); // arrays of rows $tableData = array(); @@ -94,7 +233,7 @@ function queryList() { $row['dev_Status'], $row['dev_MAC'], // MAC (hidden) formatIPlong ($row['dev_LastIP']) // IP orderable - ); + ); } // Control no rows @@ -106,130 +245,13 @@ function queryList() { echo (json_encode ($tableData)); } -//------------------------------------------------------------------------------ -// Query the List of Owners -//------------------------------------------------------------------------------ -function queryOwners() { - global $db; - - // SQL - $result = $db->query('SELECT DISTINCT 1 as dev_Order, dev_Owner - FROM Devices - WHERE dev_Owner <> "(unknown)" AND dev_Owner <> "" - AND dev_Favorite = 1 - UNION - SELECT DISTINCT 2 as dev_Order, dev_Owner - FROM Devices - WHERE dev_Owner <> "(unknown)" AND dev_Owner <> "" - AND dev_Favorite = 0 - AND dev_Owner NOT IN (SELECT dev_Owner FROM Devices WHERE dev_Favorite = 1) - ORDER BY 1,2 '); - - // arrays of rows - $tableData = array(); - while ($row = $result -> fetchArray (SQLITE3_ASSOC)) { - $tableData[] = array ('order' => $row['dev_Order'], - 'name' => $row['dev_Owner']); - } - - // Return json - echo (json_encode ($tableData)); -} - - -//------------------------------------------------------------------------------ -// Query the List of types -//------------------------------------------------------------------------------ -function queryDeviceTypes() { - global $db; - - // SQL - $result = $db->query('SELECT DISTINCT 9 as dev_Order, dev_DeviceType - FROM Devices - WHERE dev_DeviceType NOT IN ("", - "Smartphone", "Tablet", - "Laptop", "Mini PC", "PC", "Printer", "Server", - "Game Console", "SmartTV", "TV Decoder", "Virtual Assistance", - "Clock", "House Appliance", "Phone", "Radio", - "AP", "NAS", "PLC", "Router") - - UNION SELECT 1 as dev_Order, "Smartphone" - UNION SELECT 1 as dev_Order, "Tablet" - - UNION SELECT 2 as dev_Order, "Laptop" - UNION SELECT 2 as dev_Order, "Mini PC" - UNION SELECT 2 as dev_Order, "PC" - UNION SELECT 2 as dev_Order, "Printer" - UNION SELECT 2 as dev_Order, "Server" - - UNION SELECT 3 as dev_Order, "Game Console" - UNION SELECT 3 as dev_Order, "SmartTV" - UNION SELECT 3 as dev_Order, "TV Decoder" - UNION SELECT 3 as dev_Order, "Virtual Assistance" - - UNION SELECT 4 as dev_Order, "Clock" - UNION SELECT 4 as dev_Order, "House Appliance" - UNION SELECT 4 as dev_Order, "Phone" - UNION SELECT 4 as dev_Order, "Radio" - - UNION SELECT 5 as dev_Order, "AP" - UNION SELECT 5 as dev_Order, "NAS" - UNION SELECT 5 as dev_Order, "PLC" - UNION SELECT 5 as dev_Order, "Router" - - UNION SELECT 10 as dev_Order, "Other" - - ORDER BY 1,2 '); - - // arrays of rows - $tableData = array(); - while ($row = $result -> fetchArray (SQLITE3_ASSOC)) { - $tableData[] = array ('order' => $row['dev_Order'], - 'name' => $row['dev_DeviceType']); - } - - // Return json - echo (json_encode ($tableData)); -} - - -//------------------------------------------------------------------------------ -// Query the List of groups -//------------------------------------------------------------------------------ -function queryGroups() { - global $db; - - // SQL - $result = $db->query('SELECT DISTINCT 1 as dev_Order, dev_Group - FROM Devices - WHERE dev_Group <> "(unknown)" AND dev_Group <> "Others" AND dev_Group <> "" - UNION SELECT 1 as dev_Order, "Always on" - UNION SELECT 1 as dev_Order, "Friends" - UNION SELECT 1 as dev_Order, "Personal" - UNION SELECT 2 as dev_Order, "Others" - ORDER BY 1,2 '); - - // arrays of rows - $tableData = array(); - while ($row = $result -> fetchArray (SQLITE3_ASSOC)) { - $tableData[] = array ('order' => $row['dev_Order'], - 'name' => $row['dev_Group']); - } - - // Return json - echo (json_encode ($tableData)); -} - //------------------------------------------------------------------------------ // Query the List of devices for calendar //------------------------------------------------------------------------------ -function queryCalendarList() { +function getDevicesListCalendar() { global $db; - // Request Parameters - $periodDate = getDateFromPeriod(); - // SQL $condition = getDeviceCondition ($_REQUEST['status']); $result = $db->query('SELECT * FROM Devices ' . $condition); @@ -252,67 +274,175 @@ function queryCalendarList() { //------------------------------------------------------------------------------ -// Query Device Data +// Query the List of Owners //------------------------------------------------------------------------------ -function queryDeviceData() { +function getOwners() { global $db; - // Request Parameters - $periodDate = getDateFromPeriod(); - $mac = $_REQUEST['mac']; - - // Device Data - $result = $db->query('SELECT *, - CASE WHEN dev_AlertDeviceDown=1 AND dev_PresentLastScan=0 THEN "Down" - WHEN dev_PresentLastScan=1 THEN "On-line" - ELSE "Off-line" END as dev_Status - FROM Devices - WHERE dev_MAC="' . $mac .'"'); - - $row = $result -> fetchArray (SQLITE3_ASSOC); - $deviceData = $row; - $deviceData['dev_FirstConnection'] = formatDate ($row['dev_FirstConnection']); // Date formated - $deviceData['dev_LastConnection'] = formatDate ($row['dev_LastConnection']); // Date formated - - // Count Totals - $condicion = ' WHERE eve_MAC="' . $mac .'" AND eve_DateTime >= ' . $periodDate; - - // Connections - $result = $db->query('SELECT COUNT(*) FROM Sessions - WHERE ses_MAC="' . $mac .'" - AND ( ses_DateTimeConnection >= ' . $periodDate . ' - OR ses_DateTimeDisconnection >= ' . $periodDate . ' - OR ses_StillConnected = 1 ) '); - $row = $result -> fetchArray (SQLITE3_NUM); - $deviceData['dev_Sessions'] = $row[0]; - - // Events - $result = $db->query('SELECT COUNT(*) FROM Events ' . $condicion . ' AND eve_EventType <> "Connected" AND eve_EventType <> "Disconnected" '); - $row = $result -> fetchArray (SQLITE3_NUM); - $deviceData['dev_Events'] = $row[0]; - - // Donw Alerts - $result = $db->query('SELECT COUNT(*) FROM Events ' . $condicion . ' AND eve_EventType = "Device Down"'); - $row = $result -> fetchArray (SQLITE3_NUM); - $deviceData['dev_DownAlerts'] = $row[0]; - - // Presence hours - $result = $db->query('SELECT SUM (julianday (IFNULL (ses_DateTimeDisconnection, DATETIME("now"))) - - julianday (CASE WHEN ses_DateTimeConnection < ' . $periodDate . ' THEN ' . $periodDate . ' - ELSE ses_DateTimeConnection END)) *24 - FROM Sessions - WHERE ses_MAC="' . $mac .'" - AND ses_DateTimeConnection IS NOT NULL - AND (ses_DateTimeDisconnection IS NOT NULL OR ses_StillConnected = 1 ) - AND ( ses_DateTimeConnection >= ' . $periodDate . ' - OR ses_DateTimeDisconnection >= ' . $periodDate . ' - OR ses_StillConnected = 1 ) '); - $row = $result -> fetchArray (SQLITE3_NUM); - $deviceData['dev_PresenceHours'] = round ($row[0]); + // SQL + $sql = 'SELECT DISTINCT 1 as dev_Order, dev_Owner + FROM Devices + WHERE dev_Owner <> "(unknown)" AND dev_Owner <> "" + AND dev_Favorite = 1 + UNION + SELECT DISTINCT 2 as dev_Order, dev_Owner + FROM Devices + WHERE dev_Owner <> "(unknown)" AND dev_Owner <> "" + AND dev_Favorite = 0 + AND dev_Owner NOT IN + (SELECT dev_Owner FROM Devices WHERE dev_Favorite = 1) + ORDER BY 1,2 '; + $result = $db->query($sql); + // arrays of rows + $tableData = array(); + while ($row = $result -> fetchArray (SQLITE3_ASSOC)) { + $tableData[] = array ('order' => $row['dev_Order'], + 'name' => $row['dev_Owner']); + } // Return json - echo (json_encode ($deviceData)); + echo (json_encode ($tableData)); +} + + +//------------------------------------------------------------------------------ +// Query the List of types +//------------------------------------------------------------------------------ +function getDeviceTypes() { + global $db; + + // SQL + $sql = 'SELECT DISTINCT 9 as dev_Order, dev_DeviceType + FROM Devices + WHERE dev_DeviceType NOT IN ("", + "Smartphone", "Tablet", + "Laptop", "Mini PC", "PC", "Printer", "Server", "Singleboard Computer (SBC)", + "Game Console", "SmartTV", "TV Decoder", "Virtual Assistance", + "Clock", "House Appliance", "Phone", "Radio", + "AP", "NAS", "PLC", "Router") + + UNION SELECT 1 as dev_Order, "Smartphone" + UNION SELECT 1 as dev_Order, "Tablet" + + UNION SELECT 2 as dev_Order, "Laptop" + UNION SELECT 2 as dev_Order, "Mini PC" + UNION SELECT 2 as dev_Order, "PC" + UNION SELECT 2 as dev_Order, "Printer" + UNION SELECT 2 as dev_Order, "Server" + UNION SELECT 2 as dev_Order, "Singleboard Computer (SBC)" + + UNION SELECT 3 as dev_Order, "Game Console" + UNION SELECT 3 as dev_Order, "SmartTV" + UNION SELECT 3 as dev_Order, "TV Decoder" + UNION SELECT 3 as dev_Order, "Virtual Assistance" + + UNION SELECT 4 as dev_Order, "Clock" + UNION SELECT 4 as dev_Order, "House Appliance" + UNION SELECT 4 as dev_Order, "Phone" + UNION SELECT 4 as dev_Order, "Radio" + + UNION SELECT 5 as dev_Order, "AP" + UNION SELECT 5 as dev_Order, "NAS" + UNION SELECT 5 as dev_Order, "PLC" + UNION SELECT 5 as dev_Order, "Router" + + UNION SELECT 10 as dev_Order, "Other" + + ORDER BY 1,2'; + $result = $db->query($sql); + + // arrays of rows + $tableData = array(); + while ($row = $result -> fetchArray (SQLITE3_ASSOC)) { + $tableData[] = array ('order' => $row['dev_Order'], + 'name' => $row['dev_DeviceType']); + } + + // Return json + echo (json_encode ($tableData)); +} + + +//------------------------------------------------------------------------------ +// Query the List of groups +//------------------------------------------------------------------------------ +function getGroups() { + global $db; + + // SQL + $sql = 'SELECT DISTINCT 1 as dev_Order, dev_Group + FROM Devices + WHERE dev_Group NOT IN ("(unknown)", "Others") AND dev_Group <> "" + UNION SELECT 1 as dev_Order, "Always on" + UNION SELECT 1 as dev_Order, "Friends" + UNION SELECT 1 as dev_Order, "Personal" + UNION SELECT 2 as dev_Order, "Others" + ORDER BY 1,2 '; + $result = $db->query($sql); + + // arrays of rows + $tableData = array(); + while ($row = $result -> fetchArray (SQLITE3_ASSOC)) { + $tableData[] = array ('order' => $row['dev_Order'], + 'name' => $row['dev_Group']); + } + + // Return json + echo (json_encode ($tableData)); +} + + +//------------------------------------------------------------------------------ +// Query the List of locations +//------------------------------------------------------------------------------ +function getLocations() { + global $db; + + // SQL + $sql = 'SELECT DISTINCT 9 as dev_Order, dev_Location + FROM Devices + WHERE dev_Location <> "" + AND dev_Location NOT IN ( + "Bathroom", "Bedroom", "Dining room", "Hallway", + "Kitchen", "Laundry", "Living room", "Study", + "Attic", "Basement", "Garage", + "Back yard", "Garden", "Terrace", + "Other") + + UNION SELECT 1 as dev_Order, "Bathroom" + UNION SELECT 1 as dev_Order, "Bedroom" + UNION SELECT 1 as dev_Order, "Dining room" + UNION SELECT 1 as dev_Order, "Hall" + UNION SELECT 1 as dev_Order, "Kitchen" + UNION SELECT 1 as dev_Order, "Laundry" + UNION SELECT 1 as dev_Order, "Living room" + UNION SELECT 1 as dev_Order, "Study" + + UNION SELECT 2 as dev_Order, "Attic" + UNION SELECT 2 as dev_Order, "Basement" + UNION SELECT 2 as dev_Order, "Garage" + + UNION SELECT 3 as dev_Order, "Back yard" + UNION SELECT 3 as dev_Order, "Garden" + UNION SELECT 3 as dev_Order, "Terrace" + + UNION SELECT 10 as dev_Order, "Other" + ORDER BY 1,2 '; + + + + $result = $db->query($sql); + + // arrays of rows + $tableData = array(); + while ($row = $result -> fetchArray (SQLITE3_ASSOC)) { + $tableData[] = array ('order' => $row['dev_Order'], + 'name' => $row['dev_Location']); + } + + // Return json + echo (json_encode ($tableData)); } @@ -320,58 +450,14 @@ function queryDeviceData() { // Status Where conditions //------------------------------------------------------------------------------ function getDeviceCondition ($deviceStatus) { - // Request Parameters - $periodDate = getDateFromPeriod(); - switch ($deviceStatus) { - case 'all': - return ''; - case 'connected': - return 'WHERE dev_PresentLastScan=1'; - case 'new': - return 'WHERE dev_FirstConnection >= ' . $periodDate; - case 'down': - return 'WHERE dev_AlertDeviceDown=1 AND dev_PresentLastScan=0'; - case 'favorites': - return 'WHERE dev_Favorite=1'; - default: - return 'WHERE 1=0'; + case 'all': return ''; break; + case 'connected': return 'WHERE dev_PresentLastScan=1'; break; + case 'new': return 'WHERE dev_NewDevice=1'; break; + case 'down': return 'WHERE dev_AlertDeviceDown=1 AND dev_PresentLastScan=0'; break; + case 'favorites': return 'WHERE dev_Favorite=1'; break; + default: return 'WHERE 1=0'; break; } } - -//------------------------------------------------------------------------------ -// Update Device Data -//------------------------------------------------------------------------------ -function updateDeviceData() { - global $db; - - // sql - $sql = 'UPDATE Devices SET - dev_Name = "'. $_REQUEST['name'] .'", - dev_Owner = "'. $_REQUEST['owner'] .'", - dev_DeviceType = "'. $_REQUEST['type'] .'", - dev_Vendor = "'. $_REQUEST['vendor'] .'", - dev_Favorite = "'. $_REQUEST['favorite'] .'", - dev_Group = "'. $_REQUEST['group'] .'", - dev_Comments = "'. $_REQUEST['comments'] .'", - dev_StaticIP = "'. $_REQUEST['staticIP'] .'", - dev_ScanCycle = "'. $_REQUEST['scancycle'] .'", - dev_AlertEvents = "'. $_REQUEST['alertevents'] .'", - dev_AlertDeviceDown = "'. $_REQUEST['alertdown'] .'", - dev_SkipRepeated = "'. $_REQUEST['skiprepeated'] .'" - WHERE dev_MAC="' . $_REQUEST['mac'] .'"'; - // update Data - $result = $db->query($sql); - - // check result - if ($result == TRUE) { - echo "Device updated successfully"; - } else { - echo "Error updating device\n\n". $sql .'\n\n' . $db->lastErrorMsg(); - } - -} - - ?> diff --git a/front/php/server/events.php b/front/php/server/events.php index 8e073973..1d0fa228 100644 --- a/front/php/server/events.php +++ b/front/php/server/events.php @@ -1,9 +1,20 @@ = '. $periodDate .' - OR ses_DateTimeDisconnection >= '. $periodDate .' - OR ses_StillConnected = 1 ) '; + $SQL = $SQL2 . ' WHERE ( ses_DateTimeConnection >= '. $periodDate .' OR ses_DateTimeDisconnection >= '. $periodDate .' OR ses_StillConnected = 1 ) '; break; case 'missing': $SQL = $SQL2 . ' WHERE (ses_DateTimeConnection IS NULL AND ses_DateTimeDisconnection >= '. $periodDate .' ) OR (ses_DateTimeDisconnection IS NULL AND ses_StillConnected = 0 AND ses_DateTimeConnection >= '. $periodDate .' )'; break; - case 'voided': - $SQL = $SQL1 .' AND eve_EventType LIKE "VOIDED%" '; - break; - case 'new': - $SQL = $SQL1 .' AND eve_EventType = "New Device" '; - break; - case 'down': - $SQL = $SQL1 .' AND eve_EventType = "Device Down" '; - break; - default: - $SQL = $SQL1 .' AND 1==0 '; - break; + case 'voided': $SQL = $SQL1 .' AND eve_EventType LIKE "VOIDED%" '; break; + case 'new': $SQL = $SQL1 .' AND eve_EventType = "New Device" '; break; + case 'down': $SQL = $SQL1 .' AND eve_EventType = "Device Down" '; break; + default: $SQL = $SQL1 .' AND 1==0 '; break; } // Query @@ -187,7 +186,7 @@ function queryList() { //------------------------------------------------------------------------------ // Query Device Sessions //------------------------------------------------------------------------------ -function queryDeviceSessions() { +function getDeviceSessions() { global $db; // Request Parameters @@ -195,16 +194,17 @@ function queryDeviceSessions() { $periodDate = getDateFromPeriod(); // SQL - $result = $db->query('SELECT IFNULL (ses_DateTimeConnection, ses_DateTimeDisconnection) ses_DateTimeOrder, - ses_EventTypeConnection, ses_DateTimeConnection, - ses_EventTypeDisconnection, ses_DateTimeDisconnection, ses_StillConnected, - ses_IP, ses_AdditionalInfo - FROM Sessions - WHERE ses_MAC="' . $mac .'" - AND ( ses_DateTimeConnection >= '. $periodDate .' - OR ses_DateTimeDisconnection >= '. $periodDate .' - OR ses_StillConnected = 1 ) '); - + $SQL = 'SELECT IFNULL (ses_DateTimeConnection, ses_DateTimeDisconnection) ses_DateTimeOrder, + ses_EventTypeConnection, ses_DateTimeConnection, + ses_EventTypeDisconnection, ses_DateTimeDisconnection, ses_StillConnected, + ses_IP, ses_AdditionalInfo + FROM Sessions + WHERE ses_MAC="' . $mac .'" + AND ( ses_DateTimeConnection >= '. $periodDate .' + OR ses_DateTimeDisconnection >= '. $periodDate .' + OR ses_StillConnected = 1 ) '; + $result = $db->query($SQL); + // arrays of rows $tableData = array(); while ($row = $result -> fetchArray (SQLITE3_ASSOC)) { @@ -228,7 +228,7 @@ function queryDeviceSessions() { if ($row['ses_EventTypeConnection'] == '' || $row['ses_EventTypeDisconnection'] == '') { $dur = '...'; } elseif ($row['ses_StillConnected'] == true) { - $dur = formatDateDiff ($row['ses_DateTimeConnection'], ''); //******************************************************************************************* + $dur = formatDateDiff ($row['ses_DateTimeConnection'], ''); //*********** } else { $dur = formatDateDiff ($row['ses_DateTimeConnection'], $row['ses_DateTimeDisconnection']); } @@ -256,7 +256,7 @@ function queryDeviceSessions() { //------------------------------------------------------------------------------ // Query Device Presence Calendar //------------------------------------------------------------------------------ -function queryDevicePresence() { +function getDevicePresence() { global $db; // Request Parameters @@ -266,24 +266,26 @@ function queryDevicePresence() { $endDate = '"'. formatDateISO ($_REQUEST ['end']) .'"'; // SQL - $result = $db->query('SELECT ses_EventTypeConnection, ses_DateTimeConnection, - ses_EventTypeDisconnection, ses_DateTimeDisconnection, ses_IP, ses_AdditionalInfo, - - CASE WHEN ses_EventTypeConnection = "" THEN - IFNULL ((SELECT MAX(ses_DateTimeDisconnection) FROM Sessions AS SES2 WHERE SES2.ses_MAC = SES1.ses_MAC AND SES2.ses_DateTimeDisconnection < SES1.ses_DateTimeDisconnection), DATETIME(ses_DateTimeDisconnection, "-1 hour")) - ELSE ses_DateTimeConnection - END AS ses_DateTimeConnectionCorrected, + $SQL = 'SELECT ses_EventTypeConnection, ses_DateTimeConnection, + ses_EventTypeDisconnection, ses_DateTimeDisconnection, ses_IP, ses_AdditionalInfo, + + CASE + WHEN ses_EventTypeConnection = "" THEN + IFNULL ((SELECT MAX(ses_DateTimeDisconnection) FROM Sessions AS SES2 WHERE SES2.ses_MAC = SES1.ses_MAC AND SES2.ses_DateTimeDisconnection < SES1.ses_DateTimeDisconnection), DATETIME(ses_DateTimeDisconnection, "-1 hour")) + ELSE ses_DateTimeConnection + END AS ses_DateTimeConnectionCorrected, - CASE WHEN ses_EventTypeDisconnection = "" THEN - (SELECT MIN(ses_DateTimeConnection) FROM Sessions AS SES2 WHERE SES2.ses_MAC = SES1.ses_MAC AND SES2.ses_DateTimeConnection > SES1.ses_DateTimeConnection) - ELSE ses_DateTimeDisconnection - END AS ses_DateTimeDisconnectionCorrected + CASE + WHEN ses_EventTypeDisconnection = "" THEN + (SELECT MIN(ses_DateTimeConnection) FROM Sessions AS SES2 WHERE SES2.ses_MAC = SES1.ses_MAC AND SES2.ses_DateTimeConnection > SES1.ses_DateTimeConnection) + ELSE ses_DateTimeDisconnection + END AS ses_DateTimeDisconnectionCorrected - FROM Sessions AS SES1 - WHERE ses_MAC="' . $mac .'" - AND (ses_DateTimeConnectionCorrected <= date('. $endDate .') - AND (ses_DateTimeDisconnectionCorrected >= date('. $startDate .') OR ses_StillConnected = 1 )) - '); + FROM Sessions AS SES1 + WHERE ses_MAC="' . $mac .'" + AND (ses_DateTimeConnectionCorrected <= date('. $endDate .') + AND (ses_DateTimeDisconnectionCorrected >= date('. $startDate .') OR ses_StillConnected = 1 )) '; + $result = $db->query($SQL); // arrays of rows while ($row = $result -> fetchArray (SQLITE3_ASSOC)) { @@ -300,10 +302,6 @@ function queryDevicePresence() { 'IP: ' . $row['ses_IP']; // Save row data -// 'start' => formatDateISO ($row['ses_DateTimeConnectionCorrected']), -// 'end' => formatDateISO ($row['ses_DateTimeDisconnectionCorrected']), -// 'start' => $row['ses_DateTimeConnectionCorrected'], -// 'end' => $row['ses_DateTimeDisconnectionCorrected'], $tableData[] = array( 'title' => '', 'start' => formatDateISO ($row['ses_DateTimeConnectionCorrected']), @@ -326,32 +324,33 @@ function queryDevicePresence() { //------------------------------------------------------------------------------ // Query Presence Calendar for all Devices //------------------------------------------------------------------------------ -function queryCalendarPresence() { +function getEventsCalendar() { global $db; // Request Parameters - $periodDate = getDateFromPeriod(); $startDate = '"'. $_REQUEST ['start'] .'"'; $endDate = '"'. $_REQUEST ['end'] .'"'; // SQL - $result = $db->query('SELECT ses_MAC, ses_EventTypeConnection, ses_DateTimeConnection, - ses_EventTypeDisconnection, ses_DateTimeDisconnection, ses_IP, ses_AdditionalInfo, - - CASE WHEN ses_EventTypeConnection = "" THEN - IFNULL ((SELECT MAX(ses_DateTimeDisconnection) FROM Sessions AS SES2 WHERE SES2.ses_MAC = SES1.ses_MAC AND SES2.ses_DateTimeDisconnection < SES1.ses_DateTimeDisconnection), DATETIME(ses_DateTimeDisconnection, "-1 hour")) - ELSE ses_DateTimeConnection - END AS ses_DateTimeConnectionCorrected, + $SQL = 'SELECT ses_MAC, ses_EventTypeConnection, ses_DateTimeConnection, + ses_EventTypeDisconnection, ses_DateTimeDisconnection, ses_IP, ses_AdditionalInfo, + + CASE + WHEN ses_EventTypeConnection = "" THEN + IFNULL ((SELECT MAX(ses_DateTimeDisconnection) FROM Sessions AS SES2 WHERE SES2.ses_MAC = SES1.ses_MAC AND SES2.ses_DateTimeDisconnection < SES1.ses_DateTimeDisconnection), DATETIME(ses_DateTimeDisconnection, "-1 hour")) + ELSE ses_DateTimeConnection + END AS ses_DateTimeConnectionCorrected, - CASE WHEN ses_EventTypeDisconnection = "" THEN - (SELECT MIN(ses_DateTimeConnection) FROM Sessions AS SES2 WHERE SES2.ses_MAC = SES1.ses_MAC AND SES2.ses_DateTimeConnection > SES1.ses_DateTimeConnection) - ELSE ses_DateTimeDisconnection - END AS ses_DateTimeDisconnectionCorrected + CASE + WHEN ses_EventTypeDisconnection = "" THEN + (SELECT MIN(ses_DateTimeConnection) FROM Sessions AS SES2 WHERE SES2.ses_MAC = SES1.ses_MAC AND SES2.ses_DateTimeConnection > SES1.ses_DateTimeConnection) + ELSE ses_DateTimeDisconnection + END AS ses_DateTimeDisconnectionCorrected - FROM Sessions AS SES1 - WHERE ( ses_DateTimeConnectionCorrected <= Date('. $endDate .') - AND (ses_DateTimeDisconnectionCorrected >= Date('. $startDate .') OR ses_StillConnected = 1 )) - '); + FROM Sessions AS SES1 + WHERE ( ses_DateTimeConnectionCorrected <= Date('. $endDate .') + AND (ses_DateTimeDisconnectionCorrected >= Date('. $startDate .') OR ses_StillConnected = 1 )) '; + $result = $db->query($SQL); // arrays of rows while ($row = $result -> fetchArray (SQLITE3_ASSOC)) { @@ -392,7 +391,7 @@ function queryCalendarPresence() { //------------------------------------------------------------------------------ // Query Device events //------------------------------------------------------------------------------ -function queryDeviceEvents() { +function getDeviceEvents() { global $db; // Request Parameters @@ -401,13 +400,13 @@ function queryDeviceEvents() { $hideConnections = $_REQUEST ['hideConnections']; // SQL - $result = $db->query('SELECT eve_DateTime, eve_EventType, eve_IP, eve_AdditionalInfo - FROM Events - WHERE eve_MAC="'. $mac .'" AND eve_DateTime >= '. $periodDate .' - AND ( (eve_EventType <> "Connected" AND eve_EventType <> "Disconnected" AND - eve_EventType <> "VOIDED - Connected" AND eve_EventType <> "VOIDED - Disconnected") - OR "'. $hideConnections .'" = "false" ) - '); + $SQL = 'SELECT eve_DateTime, eve_EventType, eve_IP, eve_AdditionalInfo + FROM Events + WHERE eve_MAC="'. $mac .'" AND eve_DateTime >= '. $periodDate .' + AND ( (eve_EventType <> "Connected" AND eve_EventType <> "Disconnected" AND + eve_EventType <> "VOIDED - Connected" AND eve_EventType <> "VOIDED - Disconnected") + OR "'. $hideConnections .'" = "false" ) '; + $result = $db->query($SQL); // arrays of rows $tableData = array(); @@ -425,5 +424,4 @@ function queryDeviceEvents() { echo (json_encode ($tableData)); } - ?> diff --git a/front/php/server/parameters.php b/front/php/server/parameters.php new file mode 100644 index 00000000..6c0a7cd6 --- /dev/null +++ b/front/php/server/parameters.php @@ -0,0 +1,88 @@ +query($sql); + $row = $result -> fetchArray (SQLITE3_NUM); + $value = $row[0]; + + echo (json_encode ($value)); +} + + +//------------------------------------------------------------------------------ +// Set Parameter Value +//------------------------------------------------------------------------------ +function setParameter() { + global $db; + + // Update value + $sql = 'UPDATE Parameters SET par_Value="'. quotes ($_REQUEST['value']) .'" + WHERE par_ID="'. quotes($_REQUEST['parameter']) .'"'; + $result = $db->query($sql); + + if (! $result == TRUE) { + echo "Error updating parameter\n\n$sql \n\n". $db->lastErrorMsg(); + return; + } + + $changes = $db->changes(); + if ($changes == 0) { + // Insert new value + $sql = 'INSERT INTO Parameters (par_ID, par_Value) + VALUES ("'. quotes($_REQUEST['parameter']) .'", + "'. quotes($_REQUEST['value']) .'")'; + $result = $db->query($sql); + + if (! $result == TRUE) { + echo "Error creating parameter\n\n$sql \n\n". $db->lastErrorMsg(); + return; + } + } + + echo 'OK'; +} + +?> diff --git a/front/php/server/util.php b/front/php/server/util.php index 30a6d6d3..a4f345f3 100644 --- a/front/php/server/util.php +++ b/front/php/server/util.php @@ -1,4 +1,13 @@ diff --git a/front/php/templates/footer.php b/front/php/templates/footer.php index 1ef34db2..a5cdd03d 100644 --- a/front/php/templates/footer.php +++ b/front/php/templates/footer.php @@ -2,7 +2,7 @@ # Pi.Alert # Open Source Network Guard / WIFI & LAN intrusion detector # -# footer.php - Front module. Common footer to all the front pages +# footer.php - Front module. Common footer to all the web pages #------------------------------------------------------------------------------- # Puche 2021 pi.alert.application@gmail.com GNU GPLv3 #--------------------------------------------------------------------------- --> @@ -11,9 +11,7 @@
- +
- +
+ - +
+ - + + - + + - + - + - + - - + - diff --git a/front/php/templates/header.php b/front/php/templates/header.php index d1cbee52..69a26ea1 100644 --- a/front/php/templates/header.php +++ b/front/php/templates/header.php @@ -2,52 +2,58 @@ # Pi.Alert # Open Source Network Guard / WIFI & LAN intrusion detector # -# header.php - Front module. Common header to all the front pages +# header.php - Front module. Common header to all the web pages #------------------------------------------------------------------------------- # Puche 2021 pi.alert.application@gmail.com GNU GPLv3 #--------------------------------------------------------------------------- --> + + Pi.Alert + + + + + - - + + @@ -56,6 +62,7 @@
+ +
+ +
- - +
+ +
- - + - + - + @@ -118,20 +98,27 @@
+ +

Devices

- +
+ +
@@ -156,15 +143,6 @@ - - - - - - - - - @@ -178,23 +156,19 @@ diff --git a/install/pialert_install.sh b/install/pialert_install.sh index 61ee30d0..97bbaf18 100644 --- a/install/pialert_install.sh +++ b/install/pialert_install.sh @@ -8,50 +8,49 @@ # Puche 2021 pi.alert.application@gmail.com GNU GPLv3 # ------------------------------------------------------------------------------ - # ------------------------------------------------------------------------------ # Variables # ------------------------------------------------------------------------------ -COLS=70 -ROWS=12 - -INSTALL_DIR=~ -PIALERT_HOME="$INSTALL_DIR/pialert" - -LIGHTTPD_CONF_DIR="/etc/lighttpd" -WEBROOT="/var/www/html" -PIALERT_DEFAULT_PAGE=false - -LOG="pialert_install_`date +"%Y-%m-%d_%H-%M"`.log" - -MAIN_IP=`ip -o route get 1 | sed -n 's/.*src \([0-9.]\+\).*/\1/p'` - -PIHOLE_INSTALL=false -PIHOLE_ACTIVE=false -DHCP_ACTIVATE=false -DHCP_ACTIVE=false - -DHCP_RANGE_START="192.168.1.200" -DHCP_RANGE_END="192.168.1.251" -DHCP_ROUTER="192.168.1.1" -DHCP_LEASE="1" -DHCP_DOMAIN="local" - -USE_PYTHON_VERSION=0 -PYTHON_BIN=python - -REPORT_MAIL=False -REPORT_TO=user@gmail.com -SMTP_SERVER=smtp.gmail.com -SMTP_PORT=587 -SMTP_USER=user@gmail.com -SMTP_PASS=password - -DDNS_ACTIVE=False -DDNS_DOMAIN='your_domain.freeddns.org' -DDNS_USER='dynu_user' -DDNS_PASSWORD='A0000000B0000000C0000000D0000000' -DDNS_UPDATE_URL='https://api.dynu.com/nic/update?' + COLS=70 + ROWS=12 + + INSTALL_DIR=~ + PIALERT_HOME="$INSTALL_DIR/pialert" + + LIGHTTPD_CONF_DIR="/etc/lighttpd" + WEBROOT="/var/www/html" + PIALERT_DEFAULT_PAGE=false + + LOG="pialert_install_`date +"%Y-%m-%d_%H-%M"`.log" + + MAIN_IP=`ip -o route get 1 | sed -n 's/.*src \([0-9.]\+\).*/\1/p'` + + PIHOLE_INSTALL=false + PIHOLE_ACTIVE=false + DHCP_ACTIVATE=false + DHCP_ACTIVE=false + + DHCP_RANGE_START="192.168.1.200" + DHCP_RANGE_END="192.168.1.251" + DHCP_ROUTER="192.168.1.1" + DHCP_LEASE="1" + DHCP_DOMAIN="local" + + USE_PYTHON_VERSION=0 + PYTHON_BIN=python + + REPORT_MAIL=False + REPORT_TO=user@gmail.com + SMTP_SERVER=smtp.gmail.com + SMTP_PORT=587 + SMTP_USER=user@gmail.com + SMTP_PASS=password + + DDNS_ACTIVE=False + DDNS_DOMAIN='your_domain.freeddns.org' + DDNS_USER='dynu_user' + DDNS_PASSWORD='A0000000B0000000C0000000D0000000' + DDNS_UPDATE_URL='https://api.dynu.com/nic/update?' # ------------------------------------------------------------------------------ @@ -84,13 +83,14 @@ main() { move_logfile } + # ------------------------------------------------------------------------------ # Ask config questions # ------------------------------------------------------------------------------ ask_config() { # Ask installation ask_yesno "This script will install Pi.Alert in this system using this path:\n$PIALERT_HOME" \ - "Do you want to continue ?" + "Do you want to continue ?" if ! $ANSWER ; then exit 1 fi @@ -107,11 +107,11 @@ ask_config() { "Perfect: Pi-hole Installation not necessary" else ask_yesno "Pi-hole is not installed." \ - "Do you want to install Pi-hole before installing Pi.Alert ?" "YES" + "Do you want to install Pi-hole before installing Pi.Alert ?" "YES" if $ANSWER ; then PIHOLE_INSTALL=true msgbox "In the installation wizard of Pi-hole, select this options" \ - "'Install web admin interface' & 'Install web server lighttpd'" + "'Install web admin interface' & 'Install web server lighttpd'" fi fi @@ -119,21 +119,20 @@ ask_config() { DHCP_ACTIVE=false DHCP_ACTIVATE=false if $PIHOLE_ACTIVE ; then - DHCP_ACTIVE=`sudo grep DHCP_ACTIVE /etc/pihole/setupVars.conf | - awk -F= '/./{print $2}'` + DHCP_ACTIVE=`sudo grep DHCP_ACTIVE /etc/pihole/setupVars.conf | awk -F= '/./{print $2}'` if [ "$DHCP_ACTIVE" = "" ] ; then DHCP_ACTIVE=false; fi if ! $DHCP_ACTIVE ; then ask_yesno "Pi-hole DHCP server is not active." \ - "Do you want to activate Pi-hole DHCP server ?" - if $ANSWER ; then - DHCP_ACTIVATE=true - fi + "Do you want to activate Pi-hole DHCP server ?" + if $ANSWER ; then + DHCP_ACTIVATE=true + fi fi elif $PIHOLE_INSTALL ; then ask_yesno "Pi-hole installation." \ - "Do you want to activate Pi-hole DHCP server ?" + "Do you want to activate Pi-hole DHCP server ?" if $ANSWER ; then DHCP_ACTIVATE=true fi @@ -150,7 +149,7 @@ ask_config() { PIALERT_DEFAULT_PAGE=false if ! $PIHOLE_ACTIVE && ! $PIHOLE_INSTALL; then ask_yesno "As Pi-hole is not going to be available in this system," \ - "Do you want to use Pi.Alert as default web server page ?" "YES" + "Do you want to use Pi.Alert as default web server page ?" "YES" if $ANSWER ; then PIALERT_DEFAULT_PAGE=true fi @@ -158,10 +157,10 @@ ask_config() { # Ask Python version ask_option "What Python version do you want to use ?" \ - 3 \ - 0 " - Use Python already installed in the system (DEFAULT)" \ - 2 " - Use Python 2" \ - 3 " - Use Python 3" + 3 \ + 0 " - Use Python already installed in the system (DEFAULT)" \ + 2 " - Use Python 2" \ + 3 " - Use Python 3" if [ "$ANSWER" = "" ] ; then USE_PYTHON_VERSION=0 else @@ -171,10 +170,10 @@ ask_config() { # Ask e-mail notification config MAIL_REPORT=false ask_yesno "Pi.Alert can notify you by e-mail when a network event occurs" \ - "Do you want to activate this feature ?" + "Do you want to activate this feature ?" if $ANSWER ; then ask_yesno "e-mail notification needs a SMTP server (i.e. smtp.gmail.com)" \ - "Do you want to continue activating this feature ?" + "Do you want to continue activating this feature ?" MAIL_REPORT=$ANSWER fi @@ -195,10 +194,10 @@ ask_config() { # Ask Dynamic DNS config DDNS_ACTIVE=false ask_yesno "Pi.Alert can update your Dynamic DNS IP (i.e with www.dynu.net)" \ - "Do you want to activate this feature ?" + "Do you want to activate this feature ?" if $ANSWER ; then ask_yesno "Dynamics DNS updater needs a DNS with IP Update Protocol" \ - "(i.e with www.dynu.net). Do you want to continue ?" + "(i.e with www.dynu.net). Do you want to continue ?" DDNS_ACTIVE=$ANSWER fi @@ -218,11 +217,12 @@ ask_config() { # Final config message msgbox "Configuration finished. To updete the configuration, edit file:" \ - "$PIALERT_HOME/config/pialert.conf" + "$PIALERT_HOME/config/pialert.conf" msgbox "" "The installation will start now" } + # ------------------------------------------------------------------------------ # Install Pi-hole # ------------------------------------------------------------------------------ @@ -250,6 +250,7 @@ install_pihole() { PIHOLE_ACTIVE=true } + # ------------------------------------------------------------------------------ # Activate DHCP # ------------------------------------------------------------------------------ @@ -264,8 +265,7 @@ activate_DHCP() { print_msg "- Checking if DHCP is active..." if [ -e /etc/pihole ]; then - DHCP_ACTIVE= \ - `grep DHCP_ACTIVE /etc/pihole/setupVars.conf | awk -F= '/./{print $2}'` + DHCP_ACTIVE= `grep DHCP_ACTIVE /etc/pihole/setupVars.conf | awk -F= '/./{print $2}'` fi if $DHCP_ACTIVE ; then @@ -273,11 +273,11 @@ activate_DHCP() { fi print_msg "- Activating DHCP..." - sudo pihole -a enabledhcp "$DHCP_RANGE_START" "$DHCP_RANGE_END" \ - "$DHCP_ROUTER" "$DHCP_LEASE" "$DHCP_DOMAIN" 2>&1 >> "$LOG" + sudo pihole -a enabledhcp "$DHCP_RANGE_START" "$DHCP_RANGE_END" "$DHCP_ROUTER" "$DHCP_LEASE" "$DHCP_DOMAIN" 2>&1 >> "$LOG" DHCP_ACTIVE=true } + # ------------------------------------------------------------------------------ # Add Pi.Alert DNS # ------------------------------------------------------------------------------ @@ -322,6 +322,7 @@ install_lighttpd() { sudo /etc/init.d/lighttpd restart 2>&1 >> "$LOG" } + # ------------------------------------------------------------------------------ # Install arp-scan & dnsutils # ------------------------------------------------------------------------------ @@ -384,6 +385,7 @@ install_python() { fi } + # ------------------------------------------------------------------------------ # Check Python versions available # ------------------------------------------------------------------------------ @@ -426,6 +428,7 @@ install_pialert() { set_pialert_default_page } + # ------------------------------------------------------------------------------ # Download and uncompress Pi.Alert # ------------------------------------------------------------------------------ @@ -436,19 +439,18 @@ download_pialert() { fi print_msg "- Downloading installation tar file..." - curl -Lo "$INSTALL_DIR/pialert_latest.tar" \ - https://github.com/pucherot/Pi.Alert/raw/main/tar/pialert_latest.tar + curl -Lo "$INSTALL_DIR/pialert_latest.tar" https://github.com/pucherot/Pi.Alert/raw/main/tar/pialert_latest.tar echo "" print_msg "- Uncompressing tar file" - tar xf "$INSTALL_DIR/pialert_latest.tar" -C "$INSTALL_DIR" \ - --checkpoint=100 --checkpoint-action="ttyout=." 2>&1 >> "$LOG" + tar xf "$INSTALL_DIR/pialert_latest.tar" -C "$INSTALL_DIR" --checkpoint=100 --checkpoint-action="ttyout=." 2>&1 >> "$LOG" echo "" print_msg "- Deleting downloaded tar file..." rm -r "$INSTALL_DIR/pialert_latest.tar" } + # ------------------------------------------------------------------------------ # Configure Pi.Alert parameters # ------------------------------------------------------------------------------ @@ -474,6 +476,7 @@ configure_pialert() { set_pialert_parameter DHCP_ACTIVE "$DHCP_ACTIVE" } + # ------------------------------------------------------------------------------ # Set Pi.Alert parameter # ------------------------------------------------------------------------------ @@ -486,8 +489,7 @@ set_pialert_parameter() { VALUE="$2" fi - sed -i "/^$1.*=/s|=.*|= $VALUE|" $PIALERT_HOME/config/pialert.conf \ - 2>&1 >> "$LOG" + sed -i "/^$1.*=/s|=.*|= $VALUE|" $PIALERT_HOME/config/pialert.conf 2>&1 >> "$LOG" } @@ -497,20 +499,16 @@ set_pialert_parameter() { test_pialert() { print_msg "- Testing Pi.Alert HW vendors database update process..." print_msg "*** PLEASE WAIT A COUPLE OF MINUTES..." - stdbuf -i0 -o0 -e0 \ - $PYTHON_BIN $PIALERT_HOME/back/pialert.py update_vendors_silent 2>&1 \ - | tee -ai "$LOG" + stdbuf -i0 -o0 -e0 $PYTHON_BIN $PIALERT_HOME/back/pialert.py update_vendors_silent 2>&1 | tee -ai "$LOG" echo "" print_msg "- Testing Pi.Alert Internet IP Lookup..." - stdbuf -i0 -o0 -e0 \ - $PYTHON_BIN $PIALERT_HOME/back/pialert.py internet_IP 2>&1 | tee -ai "$LOG" + stdbuf -i0 -o0 -e0 $PYTHON_BIN $PIALERT_HOME/back/pialert.py internet_IP 2>&1 | tee -ai "$LOG" echo "" print_msg "- Testing Pi.Alert Network scan..." print_msg "*** PLEASE WAIT A COUPLE OF MINUTES..." - stdbuf -i0 -o0 -e0 \ - $PYTHON_BIN $PIALERT_HOME/back/pialert.py 1 2>&1 | tee -ai "$LOG" + stdbuf -i0 -o0 -e0 $PYTHON_BIN $PIALERT_HOME/back/pialert.py 1 2>&1 | tee -ai "$LOG" } # ------------------------------------------------------------------------------ @@ -528,8 +526,7 @@ add_jobs_to_crontab() { sed -i "s/\/$PYTHON_BIN/g" $PIALERT_HOME/install/pialert.cron fi - (crontab -l 2>/dev/null || : ; cat $PIALERT_HOME/install/pialert.cron) | \ - crontab - + (crontab -l 2>/dev/null || : ; cat $PIALERT_HOME/install/pialert.cron) | crontab - } # ------------------------------------------------------------------------------ @@ -551,20 +548,16 @@ publish_pialert() { print_msg "- Configuring http://pi.alert/ redirection..." if [ -e "$LIGHTTPD_CONF_DIR/conf-available/pialert_front.conf" ] ; then - sudo rm -r "$LIGHTTPD_CONF_DIR/conf-available/pialert_front.conf" \ - 2>&1 >> "$LOG" + sudo rm -r "$LIGHTTPD_CONF_DIR/conf-available/pialert_front.conf" 2>&1 >> "$LOG" fi - sudo cp "$PIALERT_HOME/install/pialert_front.conf" \ - "$LIGHTTPD_CONF_DIR/conf-available" 2>&1 >> "$LOG" + sudo cp "$PIALERT_HOME/install/pialert_front.conf" "$LIGHTTPD_CONF_DIR/conf-available" 2>&1 >> "$LOG" if [ -e "$LIGHTTPD_CONF_DIR/conf-enabled/pialert_front.conf" ] || \ [ -L "$LIGHTTPD_CONF_DIR/conf-enabled/pialert_front.conf" ] ; then - sudo rm -r "$LIGHTTPD_CONF_DIR/conf-enabled/pialert_front.conf" \ - 2>&1 >> "$LOG" + sudo rm -r "$LIGHTTPD_CONF_DIR/conf-enabled/pialert_front.conf" 2>&1 >> "$LOG" fi - sudo ln -s ../conf-available/pialert_front.conf \ - "$LIGHTTPD_CONF_DIR/conf-enabled/pialert_front.conf" 2>&1 >> "$LOG" + sudo ln -s ../conf-available/pialert_front.conf "$LIGHTTPD_CONF_DIR/conf-enabled/pialert_front.conf" 2>&1 >> "$LOG" print_msg "- Restarting lighttpd..." sudo /etc/init.d/lighttpd restart 2>&1 >> "$LOG" @@ -584,8 +577,7 @@ set_pialert_default_page() { if [ -e "$WEBROOT/index.lighttpd.html.orig" ] ; then sudo rm "$WEBROOT/index.lighttpd.html" 2>&1 >> "$LOG" else - sudo mv "$WEBROOT/index.lighttpd.html" \ - "$WEBROOT/index.lighttpd.html.orig" 2>&1 >> "$LOG" + sudo mv "$WEBROOT/index.lighttpd.html" "$WEBROOT/index.lighttpd.html.orig" 2>&1 >> "$LOG" fi fi @@ -638,8 +630,7 @@ msgbox() { END_DIALOG=false while ! $END_DIALOG ; do - whiptail --title "Pi.Alert Installation" --msgbox "$LINE1\\n\\n$LINE2" \ - $ROWS $COLS + whiptail --title "Pi.Alert Installation" --msgbox "$LINE1\\n\\n$LINE2" $ROWS $COLS BUTTON=$? ask_cancel ANSWER=true @@ -658,8 +649,7 @@ ask_yesno() { END_DIALOG=false while ! $END_DIALOG ; do - whiptail --title "Pi.Alert Installation" --yesno $DEF_BUTTON \ - "$LINE1\\n\\n$LINE2" $ROWS $COLS + whiptail --title "Pi.Alert Installation" --yesno $DEF_BUTTON "$LINE1\\n\\n$LINE2" $ROWS $COLS BUTTON=$? ask_cancel done @@ -677,8 +667,7 @@ ask_option() { END_DIALOG=false while ! $END_DIALOG ; do - ANSWER=$(whiptail --title "Pi.Alert Installation" --menu "$1" $ROWS $COLS \ - "${MENU_ARGS[@]}" 3>&2 2>&1 1>&3 ) + ANSWER=$(whiptail --title "Pi.Alert Installation" --menu "$1" $ROWS $COLS "${MENU_ARGS[@]}" 3>&2 2>&1 1>&3 ) BUTTON=$? ask_cancel CANCEL done @@ -690,8 +679,7 @@ ask_input() { END_DIALOG=false while ! $END_DIALOG ; do - ANSWER=$(whiptail --title "Pi.Alert Installation" --inputbox \ - "$LINE1\\n\\n$LINE2" $ROWS $COLS "$3" 3>&2 2>&1 1>&3 ) + ANSWER=$(whiptail --title "Pi.Alert Installation" --inputbox "$LINE1\\n\\n$LINE2" $ROWS $COLS "$3" 3>&2 2>&1 1>&3 ) BUTTON=$? ask_cancel CANCEL @@ -709,8 +697,7 @@ ask_cancel() { if [ "$BUTTON" = "1" ] && [ "$1" = "CANCEL" ] ; then BUTTON="255"; fi if [ "$BUTTON" = "255" ] ; then - whiptail --title "Pi.Alert Installation" --yesno --defaultno "$LINE0" \ - $ROWS $COLS + whiptail --title "Pi.Alert Installation" --yesno --defaultno "$LINE0" $ROWS $COLS if [ "$?" = "0" ] ; then process_error "Installation Aborted by User" diff --git a/install/pialert_update.sh b/install/pialert_update.sh index 03aabf96..024314d5 100644 --- a/install/pialert_update.sh +++ b/install/pialert_update.sh @@ -8,7 +8,6 @@ # Puche 2021 pi.alert.application@gmail.com GNU GPLv3 # ------------------------------------------------------------------------------ - # ------------------------------------------------------------------------------ # Variables # ------------------------------------------------------------------------------ @@ -57,8 +56,7 @@ create_backup() { print_msg "- Creating new Pi.Alert backup..." cd "$INSTALL_DIR" - tar cvf "$INSTALL_DIR"/pialert_update_backup_`date +"%Y-%m-%d_%H-%M"`.tar \ - pialert --checkpoint=100 --checkpoint-action="ttyout=." 2>&1 >> "$LOG" + tar cvf "$INSTALL_DIR"/pialert_update_backup_`date +"%Y-%m-%d_%H-%M"`.tar pialert --checkpoint=100 --checkpoint-action="ttyout=." 2>&1 >> "$LOG" echo "" } @@ -97,13 +95,13 @@ download_pialert() { fi print_msg "- Downloading update file..." - curl -Lo "$INSTALL_DIR/pialert_latest.tar" \ - https://github.com/pucherot/Pi.Alert/raw/main/tar/pialert_latest.tar + curl -Lo "$INSTALL_DIR/pialert_latest.tar" https://github.com/pucherot/Pi.Alert/raw/main/tar/pialert_latest.tar echo "" print_msg "- Uncompressing tar file" tar xf "$INSTALL_DIR/pialert_latest.tar" -C "$INSTALL_DIR" \ - --exclude='pialert/config/pialert.conf' --exclude='pialert/db/pialert.db' \ + --exclude='pialert/config/pialert.conf' \ + --exclude='pialert/db/pialert.db' \ --exclude='pialert/log/*' \ --checkpoint=100 --checkpoint-action="ttyout=." 2>&1 >> "$LOG" echo "" @@ -117,32 +115,47 @@ download_pialert() { # ------------------------------------------------------------------------------ update_config() { print_msg "- Config backup..." - cp "$PIALERT_HOME/config/pialert.conf" \ - "$PIALERT_HOME/config/pialert.conf.back" 2>&1 >> "$LOG" + cp "$PIALERT_HOME/config/pialert.conf" "$PIALERT_HOME/config/pialert.conf.back" 2>&1 >> "$LOG" print_msg "- Updating config file..." - sed -i '/VERSION/d' "$PIALERT_HOME/config/pialert.conf" 2>&1 >> "$LOG" - sed -i 's/PA_FRONT_URL/REPORT_DEVICE_URL/g' \ - "$PIALERT_HOME/config/pialert.conf" 2>&1 >> "$LOG" + sed -i '/VERSION/d' "$PIALERT_HOME/config/pialert.conf" 2>&1 >> "$LOG" + sed -i 's/PA_FRONT_URL/REPORT_DEVICE_URL/g' "$PIALERT_HOME/config/pialert.conf" 2>&1 >> "$LOG" if ! grep -Fq PIALERT_PATH "$PIALERT_HOME/config/pialert.conf" ; then - echo "PIALERT_PATH = '$PIALERT_HOME'" >> \ - "$PIALERT_HOME/config/pialert.conf" + echo "PIALERT_PATH = '$PIALERT_HOME'" >> "$PIALERT_HOME/config/pialert.conf" fi if ! grep -Fq QUERY_MYIP_SERVER "$PIALERT_HOME/config/pialert.conf" ; then - echo "QUERY_MYIP_SERVER = 'http://ipv4.icanhazip.com'" >> \ - "$PIALERT_HOME/config/pialert.conf" + echo "QUERY_MYIP_SERVER = 'http://ipv4.icanhazip.com'" >> "$PIALERT_HOME/config/pialert.conf" fi } # ------------------------------------------------------------------------------ -# +# DB DDL # ------------------------------------------------------------------------------ update_db() { print_msg "- Updating DB permissions..." sudo chgrp -R www-data $PIALERT_HOME/db 2>&1 >> "$LOG" chmod -R 770 $PIALERT_HOME/db 2>&1 >> "$LOG" + + print_msg "- Checking Parameters table..." + TAB=`sqlite3 $PIALERT_HOME/db/pialert.db "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='Parameters' COLLATE NOCASE;"` 2>&1 >> "$LOG" + if [ "TAB" == "0" ] ; then + print_msg " - Checking Parameters table..." + sqlite3 $PIALERT_HOME/db/pialert.db "CREATE TABLE Parameters (par_ID STRING (50) PRIMARY KEY NOT NULL COLLATE NOCASE, par_Value STRING (250));" 2>&1 >> "$LOG" + sqlite3 $PIALERT_HOME/db/pialert.db "CREATE INDEX IDX_par_ID ON Parameters (par_ID COLLATE NOCASE);" 2>&1 >> "$LOG" + fi + + print_msg "- Checking Devices new columns..." + COL=`sqlite3 pialert.db "SELECT COUNT(*) FROM PRAGMA_TABLE_INFO ('Devices') WHERE name='dev_NewDevice' COLLATE NOCASE";` 2>&1 >> "$LOG" + if [ "TAB" == "0" ] ; then + sqlite3 $PIALERT_HOME/db/pialert.db "ALTER TABLE Devices ADD COLUMN dev_NewDevice BOOLEAN NOT NULL DEFAULT (1) CHECK (dev_NewDevice IN (0, 1) );" 2>&1 >> "$LOG" + sqlite3 $PIALERT_HOME/db/pialert.db "CREATE INDEX IDX_dev_NewDevice ON Devices (dev_NewDevice);" + + COL=`sqlite3 pialert.db "SELECT COUNT(*) FROM PRAGMA_TABLE_INFO ('Devices') WHERE name='dev_Location' COLLATE NOCASE";` 2>&1 >> "$LOG" + if [ "TAB" == "0" ] ; then + sqlite3 $PIALERT_HOME/db/pialert.db "ALTER TABLE Devices ADD COLUMN dev_Location STRING(250) COLLATE NOCASE);" 2>&1 >> "$LOG" + fi } # ------------------------------------------------------------------------------ @@ -151,20 +164,16 @@ update_db() { test_pialert() { print_msg "- Testing Pi.Alert HW vendors database update process..." print_msg "*** PLEASE WAIT A COUPLE OF MINUTES..." - stdbuf -i0 -o0 -e0 \ - $PYTHON_BIN $PIALERT_HOME/back/pialert.py update_vendors_silent 2>&1 \ - | tee -ai "$LOG" + stdbuf -i0 -o0 -e0 $PYTHON_BIN $PIALERT_HOME/back/pialert.py update_vendors_silent 2>&1 | tee -ai "$LOG" echo "" print_msg "- Testing Pi.Alert Internet IP Lookup..." - stdbuf -i0 -o0 -e0 \ - $PYTHON_BIN $PIALERT_HOME/back/pialert.py internet_IP 2>&1 | tee -ai "$LOG" + stdbuf -i0 -o0 -e0 $PYTHON_BIN $PIALERT_HOME/back/pialert.py internet_IP 2>&1 | tee -ai "$LOG" echo "" print_msg "- Testing Pi.Alert Network scan..." print_msg "*** PLEASE WAIT A COUPLE OF MINUTES..." - stdbuf -i0 -o0 -e0 \ - $PYTHON_BIN $PIALERT_HOME/back/pialert.py 1 2>&1 | tee -ai "$LOG" + stdbuf -i0 -o0 -e0 $PYTHON_BIN $PIALERT_HOME/back/pialert.py 1 2>&1 | tee -ai "$LOG" } # ------------------------------------------------------------------------------ diff --git a/tar/create_tar.sh b/tar/create_tar.sh index a943c122..aecfd4fb 100644 --- a/tar/create_tar.sh +++ b/tar/create_tar.sh @@ -25,4 +25,3 @@ tar cvf pialert/tar/pialert_$PIALERT_VERSION.tar --exclude="pialert/tar" --exclu ln -s pialert_$PIALERT_VERSION.tar pialert/tar/pialert_latest.tar ls -l pialert/tar/pialert*.tar - diff --git a/tar/pialert_2.61.tar b/tar/pialert_2.70.tar similarity index 99% rename from tar/pialert_2.61.tar rename to tar/pialert_2.70.tar index ef7019c0f6671048403fa7a5f11397ed65f529ab..c3dbb34dbf345259582d167d5c1f330731d76b55 100644 GIT binary patch delta 40389 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 uXJsZ%HC{K#$LG%@*e6x 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 diff --git a/tar/pialert_latest.tar b/tar/pialert_latest.tar index ef7019c0f6671048403fa7a5f11397ed65f529ab..c3dbb34dbf345259582d167d5c1f330731d76b55 100644 GIT binary patch delta 40389 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 uXJsZ%HC{K#$LG%@*e6x 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