From bb2beda12af7e4c3fe77a014756e392347fa5f45 Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Wed, 29 May 2024 19:24:43 +1000 Subject: [PATCH] =?UTF-8?q?=E2=9A=99=20settings=20saving=20improvements=20?= =?UTF-8?q?+=20refactor=20-=20DB=20lock=20v0.1=20#685?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 2 +- front/css/app.css | 43 ++-- front/js/modal.js | 13 +- front/js/settings_utils.js | 273 ++++++++++++++++++++++-- front/php/server/db.php | 11 + front/php/server/dbHelper.php | 28 ++- front/php/server/util.php | 6 +- front/php/templates/language/en_us.json | 4 +- front/php/templates/notification.php | 2 +- front/settings.php | 254 +--------------------- 10 files changed, 334 insertions(+), 302 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index e874efe9..23cd5a55 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,7 +26,7 @@ services: - ${APP_DATA_LOCATION}/pihole/etc-pihole/pihole-FTL.db:/etc/pihole/pihole-FTL.db - ${DEV_LOCATION}/server:/app/server - ${DEV_LOCATION}/dockerfiles:/app/dockerfiles - - ${APP_DATA_LOCATION}/netalertx/php.ini:/etc/php/8.2/fpm/php.ini + # - ${APP_DATA_LOCATION}/netalertx/php.ini:/etc/php/8.2/fpm/php.ini - ${DEV_LOCATION}/install:/app/install - ${DEV_LOCATION}/front/css:/app/front/css - ${DEV_LOCATION}/front/img:/app/front/img diff --git a/front/css/app.css b/front/css/app.css index 6c39b26a..1d2bbd9f 100755 --- a/front/css/app.css +++ b/front/css/app.css @@ -492,28 +492,34 @@ /* ----------------------------------------------------------------------------- Notification float banner ----------------------------------------------------------------------------- */ -.pa_alert_notification { +.notification_modal { + 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 */ + left: 0; + right: 0; width: 80%; z-index: 9999; - position: fixed; - top: 30px; - margin: auto; - transform: translate(0, 0); - + top: 100px; display: none; + margin-left: auto; + margin-right: auto; } + +.modal_green +{ + color: #258744; + background-color: #d4edda; + border-color: #c3e6cb; +} + +.modal_grey +{ + color: white; + background-color: darkgrey; + border-color: #000000; +} + /* ticker setup */ .ticker-li { @@ -979,6 +985,11 @@ input[readonly] { border-color: #258744; } +.settings-sticky-bottom-section .form-group +{ + margin-bottom: 0px; +} + .clear-filter { opacity: 0.5; diff --git a/front/js/modal.js b/front/js/modal.js index ad0786bc..5efcbfab 100755 --- a/front/js/modal.js +++ b/front/js/modal.js @@ -186,17 +186,24 @@ function modalWarningOK() { } // ----------------------------------------------------------------------------- -function showMessage(textMessage = "") { +function showMessage(textMessage = "", timeout = 3000, colorClass = "modal_green") { if (textMessage.toLowerCase().includes("error")) { // show error alert(textMessage); } else { - // show temporal notification + // show temporary notification + $("#notification").removeClass(); // remove all classes + $("#notification").addClass("alert alert-dimissible notification_modal"); // add default ones + $("#notification").addClass(colorClass); // add color modifiers + + // message $("#alert-message").html(textMessage); + + // timeout $("#notification").fadeIn(1, function () { window.setTimeout(function () { $("#notification").fadeOut(500); - }, 3000); + }, timeout); }); } } diff --git a/front/js/settings_utils.js b/front/js/settings_utils.js index 78818c58..ffd9641b 100755 --- a/front/js/settings_utils.js +++ b/front/js/settings_utils.js @@ -201,10 +201,94 @@ } } +// ------------------------------------------------------------------- +// Validation +// ------------------------------------------------------------------- +function settingsCollectedCorrectly(settingsArray, settingsJSON_DB) { + + // check if the required UI_LANG setting is in the array - if not something went wrong + $.each(settingsArray, function(index, value) { + if (value[1] == "UI_LANG") { + if(isEmpty(value[3]) == true) + { + console.log(`⚠ Error: Required setting UI_LANG not found`); + showModalOk('ERROR', getString('settings_missing_block')); + + return false; + } + } + }); + + const settingsCodeNames = settingsJSON_DB.map(setting => setting.Code_Name); + const detailedCodeNames = settingsArray.map(item => item[1]); + + const missingCodeNamesOnPage = detailedCodeNames.filter(codeName => !settingsCodeNames.includes(codeName)); + const missingCodeNamesInDB = settingsCodeNames.filter(codeName => !detailedCodeNames.includes(codeName)); + + // check if the number of settings on the page and in the DB are the same + if (missingCodeNamesOnPage.length !== missingCodeNamesInDB.length) { + + console.log(`⚠ Error: The following settings are missing in the DB or on the page (Reload page to fix):`); + console.log(missingCodeNamesOnPage); + console.log(missingCodeNamesInDB); + + showModalOk('ERROR', getString('settings_missing_block')); + + return false; + } + + // all OK + return true; +} // ------------------------------------------------------------------- // Manipulating Editable List options // ------------------------------------------------------------------- +// --------------------------------------------------------- +function addList(element) +{ + + const fromId = $(element).attr('my-input-from'); + const toId = $(element).attr('my-input-to'); + + input = $(`#${fromId}`).val(); + $(`#${toId}`).append($("").attr("value", input).text(input)); + + // clear input + $(`#${fromId}`).val(""); + + settingsChanged(); +} +// --------------------------------------------------------- +function removeFromList(element) +{ + settingsChanged(); + $(`#${$(element).attr('my-input')}`).find("option:last").remove(); + +} +// --------------------------------------------------------- +function addInterface() +{ + ipMask = $('#ipMask').val(); + ipInterface = $('#ipInterface').val(); + + full = ipMask + " --interface=" + ipInterface; + + console.log(full) + + if(ipMask == "" || ipInterface == "") + { + showModalOk ('Validation error', 'Specify both, the network mask and the interface'); + } else { + $('#SCAN_SUBNETS').append($('').attr('value', full).text(full)); + + $('#ipMask').val(''); + $('#ipInterface').val(''); + + settingsChanged(); + } +} + // ------------------------------------------------------------------- // Function to remove an item from the select element @@ -305,32 +389,175 @@ function filterRows(inputText) { }); } -setTimeout(() => { + setTimeout(() => { - // Event listener for input change - $('#settingsSearch').on('input', function() { - var searchText = $(this).val(); - // hide the setting overview dashboard - $('#settingsOverview').collapse('hide'); + // Event listener for input change + $('#settingsSearch').on('input', function() { + var searchText = $(this).val(); + // hide the setting overview dashboard + $('#settingsOverview').collapse('hide'); - filterRows(searchText); + filterRows(searchText); + }); + + // Event listener for input focus + // var firstFocus = true; + $('#settingsSearch').on('focus', function() { + openAllSettings() + }); + + + + }, 1000); + + + + // ----------------------------------------------------------------------------- + // handling events on the backend initiated by the front end START + // ----------------------------------------------------------------------------- + + modalEventStatusId = 'modal-message-front-event' + + // -------------------------------------------------------- + // Calls a backend function to add a front-end event (specified by the attributes 'data-myevent' and 'data-myparam-plugin' on the passed element) to an execution queue + function addToExecutionQueue(element) + { + + // value has to be in format event|param. e.g. run|ARPSCAN + action = `${getGuid()}|${$(element).attr('data-myevent')}|${$(element).attr('data-myparam-plugin')}` + + $.ajax({ + method: "POST", + url: "php/server/util.php", + data: { function: "addToExecutionQueue", action: action }, + success: function(data, textStatus) { + // showModalOk ('Result', data ); + + // show message + showModalOk(getString("general_event_title"), `${getString("general_event_description")}

`); + + updateModalState() + } + }) + } + + // -------------------------------------------------------- + // Updating the execution queue in in modal pop-up + function updateModalState() { + setTimeout(function() { + // Fetch the content from the log file using an AJAX request + $.ajax({ + url: '/log/execution_queue.log', + type: 'GET', + success: function(data) { + // Update the content of the HTML element (e.g., a div with id 'logContent') + $('#'+modalEventStatusId).html(data); + + updateModalState(); + }, + error: function() { + // Handle error, such as the file not being found + $('#logContent').html('Error: Log file not found.'); + } + }); + }, 2000); + } + + + // ----------------------------------------------------------------------------- + // handling events on the backend initiated by the front end END + // ----------------------------------------------------------------------------- + + +// --------------------------------------------------------- +// UNUSED? +function getParam(targetId, key, skipCache = false) { + + skipCacheQuery = ""; + + if(skipCache) + { + skipCacheQuery = "&skipcache"; + } + + // get parameter value + $.get('php/server/parameters.php?action=get&defaultValue=0¶meter='+ key + skipCacheQuery, function(data) { + + var result = data; + + result = result.replaceAll('"', ''); + + document.getElementById(targetId).innerHTML = result.replaceAll('"', ''); }); +} - // Event listener for input focus - // var firstFocus = true; - $('#settingsSearch').on('focus', function() { - openAllSettings() - }); + // ----------------------------------------------------------------------------- + // Show/hide the metadata settings + // ----------------------------------------------------------------------------- + function toggleMetadata(element) + { + const id = $(element).attr('my-to-toggle'); + + $(`#${id}`).toggle(); + } + + + + // --------------------------------------------------------- + // Helper methods + // --------------------------------------------------------- + // Toggle readonly mode of the target element specified by the id in the "my-input-toggle-readonly" attribute + function overrideToggle(element) { + settingsChanged(); + + targetId = $(element).attr("my-input-toggle-readonly"); + + inputElement = $(`#${targetId}`)[0]; + + if (!inputElement) { + console.error("Input element not found!"); + return; + } + + if (inputElement.type === "text" || inputElement.type === "password") { + inputElement.readOnly = !inputElement.readOnly; + } else if (inputElement.type === "checkbox") { + inputElement.disabled = !inputElement.disabled; + } else { + console.warn("Unsupported input type. Only text, password, and checkbox inputs are supported."); + } + + } + + + // --------------------------------------------------------- + // generate a list of options for a input select + function generateInputOptions(pluginsData, set, input, isMultiSelect = false) + { + multi = isMultiSelect ? "multiple" : ""; + + // optionsArray = getSettingOptions(set['Code_Name'] ) + valuesArray = createArray(set['Value']); + + // create unique ID + var targetLocation = set['Code_Name'] + "_initSettingDropdown"; + + // execute AJAX callabck + SQL query resolution + initSettingDropdown(set['Code_Name'] , valuesArray, targetLocation, generateDropdownOptions) + + // main selection dropdown wrapper + input += ` + `; + + return input; + } - -}, 1000); - - - - - - - - - diff --git a/front/php/server/db.php b/front/php/server/db.php index 40e7b21f..48243f8e 100755 --- a/front/php/server/db.php +++ b/front/php/server/db.php @@ -12,6 +12,8 @@ // DB File Path $DBFILE = dirname(__FILE__).'/../../../db/app.db'; +$db_locked = false; + //------------------------------------------------------------------------------ // Connect DB //------------------------------------------------------------------------------ @@ -20,12 +22,21 @@ function SQLite3_connect ($trytoreconnect) { try { // connect to database + + global $db_locked; + + $db_locked = false; + // return new SQLite3($DBFILE, SQLITE3_OPEN_READONLY); return new SQLite3($DBFILE, SQLITE3_OPEN_READWRITE); } catch (Exception $exception) { // sqlite3 throws an exception when it is unable to connect + global $db_locked; + + $db_locked = true; + // try to reconnect one time after 3 seconds if($trytoreconnect) diff --git a/front/php/server/dbHelper.php b/front/php/server/dbHelper.php index 134395e3..7cf6239e 100755 --- a/front/php/server/dbHelper.php +++ b/front/php/server/dbHelper.php @@ -269,16 +269,26 @@ function delete($columnName, $id, $dbtable) // check if the database is locked //------------------------------------------------------------------------------ function checkLock() { - global $db; - try { - $db->exec('BEGIN EXCLUSIVE TRANSACTION'); - $db->exec('COMMIT'); - echo 0; // Not locked - return 0; - } catch (Exception $e) { - echo 1; // Locked - return 1; + global $DBFILE, $db_locked; + + $file = fopen($DBFILE, 'r+'); + + if (!$file or $db_locked) { + echo 1; // Could not open the file + return; } + + if (flock($file, LOCK_EX | LOCK_NB)) { + // Lock acquired, meaning the database is not locked by another process + flock($file, LOCK_UN); // Release the lock + echo 0; // Not locked + } else { + // Could not acquire lock, meaning the database is locked + echo 1; // Locked + } + + fclose($file); } + ?> diff --git a/front/php/server/util.php b/front/php/server/util.php index ee9fbaa4..2e221281 100755 --- a/front/php/server/util.php +++ b/front/php/server/util.php @@ -387,8 +387,10 @@ function saveSettings() // Replace the original file with the temporary file rename($tempConfPath, $fullConfPath); - displayMessage("
Settings saved to the app.conf file.

A time-stamped backup of the previous file created.

Reloading...
", - FALSE, TRUE, TRUE, TRUE); + // displayMessage(lang('settings_saved'), + // FALSE, TRUE, TRUE, TRUE); + + echo "OK"; } diff --git a/front/php/templates/language/en_us.json b/front/php/templates/language/en_us.json index dc18e630..da6a8fec 100755 --- a/front/php/templates/language/en_us.json +++ b/front/php/templates/language/en_us.json @@ -657,7 +657,7 @@ "settings_imported": "Last time settings were imported from the app.conf file", "settings_imported_label": "Settings imported", "settings_missing": "Not all settings loaded! High load on the database or app startup sequence. Click the 🔄 reload button in the top.", - "settings_missing_block": "Not all settings were loaded correctly. This is probably caused by a high load on the database. Click the 🔄 reload button in the top.", + "settings_missing_block": "Error: Settings not loaded correctly. Click the reload button 🔄 at the top, alternatively, check the browser log for details (F12).", "settings_old": "Importing settings and re-initializing...", "settings_other_scanners": "Other, non-device scanner plugins that are currently enabled.", "settings_other_scanners_icon": "fa-solid fa-recycle", @@ -665,7 +665,7 @@ "settings_publishers": "Enabled notification gateways - publishers, that will send a notification depending on your settings.", "settings_publishers_icon": "fa-solid fa-comment-dots", "settings_publishers_label": "Publishers", - "settings_saved": "
Settings saved to the app.conf file.

A time-stamped backup of the previous file created.

Reloading...
", + "settings_saved": "
Settings saved.

Reloading...


", "settings_system_icon": "fa-solid fa-gear", "settings_system_label": "System", "settings_update_item_warning": "Update the value below. Be careful to follow the previous format. Validation is not performed.", diff --git a/front/php/templates/notification.php b/front/php/templates/notification.php index 44948790..eb05804d 100755 --- a/front/php/templates/notification.php +++ b/front/php/templates/notification.php @@ -147,7 +147,7 @@ -
+
Alert message
diff --git a/front/settings.php b/front/settings.php index e75ae3dc..e32b63bf 100755 --- a/front/settings.php +++ b/front/settings.php @@ -601,65 +601,6 @@ $settingsJSON_DB = json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX } - // --------------------------------------------------------- - // generate a list of options for a input select - function generateInputOptions(pluginsData, set, input, isMultiSelect = false) - { - multi = isMultiSelect ? "multiple" : ""; - - // optionsArray = getSettingOptions(set['Code_Name'] ) - valuesArray = createArray(set['Value']); - - // create unique ID - var targetLocation = set['Code_Name'] + "_initSettingDropdown"; - - // execute AJAX callabck + SQL query resolution - initSettingDropdown(set['Code_Name'] , valuesArray, targetLocation, generateDropdownOptions) - - // main selection dropdown wrapper - input += ` - `; - - return input; - } - - - - // --------------------------------------------------------- - // Helper methods - // --------------------------------------------------------- - // Toggle readonly mode of teh target element specified by the id in the "my-input-toggle-readonly" attribute - function overrideToggle(element) { - settingsChanged(); - - targetId = $(element).attr("my-input-toggle-readonly"); - - inputElement = $(`#${targetId}`)[0]; - - if (!inputElement) { - console.error("Input element not found!"); - return; - } - - if (inputElement.type === "text" || inputElement.type === "password") { - inputElement.readOnly = !inputElement.readOnly; - } else if (inputElement.type === "checkbox") { - inputElement.disabled = !inputElement.disabled; - } else { - console.warn("Unsupported input type. Only text, password, and checkbox inputs are supported."); - } - - } - - // number of settings has to be equal to // display the name of the first person // echo $settingsJson[0]->name; @@ -670,61 +611,16 @@ $settingsJSON_DB = json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX // Wrong number of settings processing if(settingsNumberJSON != settingsNumberDB) { - showModalOk('WARNING', ""); + showModalOk('WARNING', getString("settings_missing")); } - // --------------------------------------------------------- - function addList(element) - { - - const fromId = $(element).attr('my-input-from'); - const toId = $(element).attr('my-input-to'); - - input = $(`#${fromId}`).val(); - $(`#${toId}`).append($("").attr("value", input).text(input)); - - // clear input - $(`#${fromId}`).val(""); - - settingsChanged(); - } - // --------------------------------------------------------- - function removeFromList(element) - { - settingsChanged(); - $(`#${$(element).attr('my-input')}`).find("option:last").remove(); - - } - // --------------------------------------------------------- - function addInterface() - { - ipMask = $('#ipMask').val(); - ipInterface = $('#ipInterface').val(); - - full = ipMask + " --interface=" + ipInterface; - - console.log(full) - - if(ipMask == "" || ipInterface == "") - { - showModalOk ('Validation error', 'Specify both, the network mask and the interface'); - } else { - $('#SCAN_SUBNETS').append($('').attr('value', full).text(full)); - - $('#ipMask').val(''); - $('#ipInterface').val(''); - - settingsChanged(); - } - } - // --------------------------------------------------------- function saveSettings() { if(settingsNumberJSON != settingsNumberDB) { console.log(`Error settingsNumberJSON != settingsNumberDB: ${settingsNumberJSON} != ${settingsNumberDB}`); - showModalOk('WARNING', ""); + showModalOk('WARNING', getString("settings_missing_block")); setTimeout(() => { clearCache() @@ -778,49 +674,8 @@ $settingsJSON_DB = json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX } }); - console.log(settingsArray); - // sanity check to make sure settings were loaded & collected correctly - sanityCheck_notOK = true - $.each(settingsArray, function(index, value) { - // Do something with each element of the array - if(value[1] == "UI_LANG") - { - sanityCheck_notOK = isEmpty(value[3]) - } - }); - - // if ok, double check the number collected of settings is correct - if(sanityCheck_notOK == false) - { - - console.log(settingsArray); - - // Step 1: Extract Code_Name values from settingsList - const settingsCodeNames = settingsJSON_DB.map(setting => setting.Code_Name); - - // Step 2: Extract second elements from detailedList - const detailedCodeNames = settingsArray.map(item => item[1]); - - // Step 3: Find missing Code_Name values - const missingCodeNamesOnPage = detailedCodeNames.filter(codeName => !settingsCodeNames.includes(codeName)); - const missingCodeNamesInDB = settingsCodeNames.filter(codeName => !detailedCodeNames.includes(codeName)); - - - if(missingCodeNamesOnPage.length != missingCodeNamesInDB.length) - { - sanityCheck_notOK = true; - - console.log(`⚠ Error: The following settings are missing in the DB or on the page (Reload page to fix):`); - console.log(missingCodeNamesOnPage); - console.log(missingCodeNamesInDB); - - showModalOk('WARNING', ""); - } - - } - - if(sanityCheck_notOK == false && false) + if(settingsCollectedCorrectly(settingsArray, settingsJSON_DB)) { // trigger a save settings event in the backend $.ajax({ @@ -831,7 +686,7 @@ $settingsJSON_DB = json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX settings: JSON.stringify(settingsArray) }, success: function(data, textStatus) { - showModalOk ('Result', data ); + showMessage (getString("settings_saved"), 5000, "modal_grey"); // Remove navigation prompt "Are you sure you want to leave..." window.onbeforeunload = null; @@ -848,30 +703,15 @@ $settingsJSON_DB = json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX } } - - // --------------------------------------------------------- - function getParam(targetId, key, skipCache = false) { - skipCacheQuery = ""; - if(skipCache) - { - skipCacheQuery = "&skipcache"; - } + - // get parameter value - $.get('php/server/parameters.php?action=get&defaultValue=0¶meter='+ key + skipCacheQuery, function(data) { - var result = data; - - result = result.replaceAll('"', ''); - - document.getElementById(targetId).innerHTML = result.replaceAll('"', ''); - }); - } + + - -