/* ----------------------------------------------------------------------------- * NetAlertX * Open Source Network Guard / WIFI & LAN intrusion detector * * common.js - Front module. Common Javascript functions *------------------------------------------------------------------------------- # Puche 2021 / 2022+ jokob jokob@duck.com GNU GPLv3 ----------------------------------------------------------------------------- */ // ----------------------------------------------------------------------------- var timerRefreshData = '' var emptyArr = ['undefined', "", undefined, null, 'null']; var UI_LANG = "English (en_us)"; // allLanguages is populated at init via fetchAllLanguages() from GET /languages. // Do not hardcode this list — add new languages to languages.json instead. let allLanguages = []; var settingsJSON = {} // NAX_CACHE_VERSION and CACHE_KEYS moved to cache.js // getCache, setCache, fetchJson, getAuthContext moved to cache.js // ----------------------------------------------------------------------------- // Fetch the canonical language list from GET /languages and populate allLanguages. // Must be called after the API token is available (e.g. alongside cacheStrings). // ----------------------------------------------------------------------------- function fetchAllLanguages(apiToken) { return fetch('/languages', { headers: { 'Authorization': 'Bearer ' + apiToken } }) .then(function(resp) { return resp.json(); }) .then(function(data) { if (data && data.success && Array.isArray(data.languages)) { allLanguages = data.languages.map(function(l) { return l.code; }); } }) .catch(function(err) { console.warn('[fetchAllLanguages] Failed to load language list:', err); }); } // ----------------------------------------------------------------------------- function setCookie (cookie, value, expirationMinutes='') { // Calc expiration date var expires = ''; if (typeof expirationMinutes === 'number') { expires = ';expires=' + new Date(Date.now() + expirationMinutes *60*1000).toUTCString(); } // Save Cookie document.cookie = cookie + "=" + value + expires; } // ----------------------------------------------------------------------------- function getCookie (cookie) { // Array of cookies var allCookies = document.cookie.split(';'); // For each cookie for (var i = 0; i < allCookies.length; i++) { var currentCookie = allCookies[i].trim(); // If the current cookie is the correct cookie if (currentCookie.indexOf (cookie +'=') == 0) { // Return value return currentCookie.substring (cookie.length+1); } } // Return empty (not found) return ""; } // ----------------------------------------------------------------------------- function deleteCookie (cookie) { document.cookie = cookie + '=;expires=Thu, 01 Jan 1970 00:00:00 UTC'; } // cacheApiConfig, cacheSettings, getSettingOptions, getSetting moved to cache.js // ----------------------------------------------------------------------------- // cacheStrings, getString, getLangCode moved to cache.js const tz = getSetting("TIMEZONE") || 'Europe/Berlin'; const LOCALE = getSetting('UI_LOCALE') || 'en-GB'; // ----------------------------------------------------------------------------- // DateTime utilities // ----------------------------------------------------------------------------- function localizeTimestamp(input) { input = String(input || '').trim(); // 1. Unix timestamps (10 or 13 digits) if (/^\d+$/.test(input)) { const ms = input.length === 10 ? parseInt(input, 10) * 1000 : parseInt(input, 10); return new Intl.DateTimeFormat('default', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).format(new Date(ms)); } // 2. European DD/MM/YYYY let match = input.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:[ ,]+(\d{1,2}:\d{2}(?::\d{2})?))?$/); if (match) { let [, d, m, y, t = "00:00:00", tzPart = ""] = match; const dNum = parseInt(d, 10); const mNum = parseInt(m, 10); if (dNum <= 12 && mNum > 12) { } else { const iso = `${y}-${m.padStart(2,'0')}-${d.padStart(2,'0')}T${t.length===5 ? t + ":00" : t}${tzPart}`; return formatSafe(iso, tz); } } // 3. US MM/DD/YYYY match = input.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:[ ,]+(\d{1,2}:\d{2}(?::\d{2})?))?(.*)$/); if (match) { let [, m, d, y, t = "00:00:00", tzPart = ""] = match; const iso = `${y}-${m.padStart(2,'0')}-${d.padStart(2,'0')}T${t.length===5?t+":00":t}${tzPart}`; return formatSafe(iso, tz); } // 4. ISO YYYY-MM-DD with optional Z/+offset match = input.match(/^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])[ T](\d{1,2}:\d{2}(?::\d{2})?)(Z|[+-]\d{2}:?\d{2})?$/); if (match) { let [, y, m, d, time, offset = ""] = match; const iso = `${y}-${m}-${d}T${time.length===5?time+":00":time}${offset}`; return formatSafe(iso, tz); } // 5. RFC2822 / "25 Aug 2025 13:45:22 +0200" match = input.match(/^\d{1,2} [A-Za-z]{3,} \d{4}/); if (match) { return formatSafe(input, tz); } // 6. DD-MM-YYYY with optional time match = input.match(/^(\d{1,2})-(\d{1,2})-(\d{4})(?:[ T](\d{1,2}:\d{2}(?::\d{2})?))?$/); if (match) { let [, d, m, y, time = "00:00:00"] = match; const iso = `${y}-${m.padStart(2,'0')}-${d.padStart(2,'0')}T${time.length===5?time+":00":time}`; return formatSafe(iso, tz); } // 7. Strict YYYY-DD-MM with optional time match = input.match(/^(\d{4})-(0[1-9]|[12]\d|3[01])-(0[1-9]|1[0-2])(?:[ T](\d{1,2}:\d{2}(?::\d{2})?))?$/); if (match) { let [, y, d, m, time = "00:00:00"] = match; const iso = `${y}-${m}-${d}T${time.length === 5 ? time + ":00" : time}`; return formatSafe(iso, tz); } // 8. Fallback return formatSafe(input, tz); function formatSafe(str, tz) { // CHECK: Does the input string have timezone information? // - Ends with Z: "2026-02-11T11:37:02Z" // - Has GMT±offset: "Wed Feb 11 2026 12:34:12 GMT+1100 (...)" // - Has offset at end: "2026-02-11 11:37:02+11:00" // - Has timezone name in parentheses: "(Australian Eastern Daylight Time)" const hasOffset = /Z$/i.test(str.trim()) || /GMT[+-]\d{2,4}/.test(str) || /[+-]\d{2}:?\d{2}$/.test(str.trim()) || /\([^)]+\)$/.test(str.trim()); // ⚠️ CRITICAL: All DB timestamps are stored in UTC without timezone markers. // If no offset is present, we must explicitly mark it as UTC by appending 'Z' // so JavaScript doesn't interpret it as local browser time. let isoStr = str.trim(); if (!hasOffset) { // Ensure proper ISO format before appending Z // Replace space with 'T' if needed: "2026-02-11 11:37:02" → "2026-02-11T11:37:02Z" isoStr = isoStr.trim().replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/, '$1T$2') + 'Z'; } const date = new Date(isoStr); if (!isFinite(date)) { console.error(`ERROR: Couldn't parse date: '${str}' with TIMEZONE ${tz}`); return 'Failed conversion'; } return new Intl.DateTimeFormat(LOCALE, { // Convert from UTC to user's configured timezone timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).format(date); } } /** * Returns start and end date for a given period. * @param {string} period - "1 day", "7 days", "1 month", "1 year", "100 years" * @returns {{start: string, end: string}} - Dates in "YYYY-MM-DD HH:MM:SS" format */ function getPeriodStartEnd(period) { const now = new Date(); let start = new Date(now); // default start = now let end = new Date(now); // default end = now switch (period) { case "1 day": start.setDate(now.getDate() - 1); break; case "7 days": start.setDate(now.getDate() - 7); break; case "1 month": start.setMonth(now.getMonth() - 1); break; case "1 year": start.setFullYear(now.getFullYear() - 1); break; case "100 years": start = new Date(0); // very old date for "all" break; default: console.warn("Unknown period, using 1 month as default"); start.setMonth(now.getMonth() - 1); } // Helper function to format date as "YYYY-MM-DD HH:MM:SS" const formatDate = (date) => { const pad = (n) => String(n).padStart(2, "0"); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` + `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; }; return { start: formatDate(start), end: formatDate(end) }; } // ----------------------------------------------------------------------------- // String utilities // ----------------------------------------------------------------------------- // ---------------------------------------------------- /** * Replaces double quotes within single-quoted strings, then converts all single quotes to double quotes, * while preserving the intended structure. * * @param {string} inputString - The input string to process. * @returns {string} - The processed string with transformations applied. */ function processQuotes(inputString) { // Step 1: Replace double quotes within single-quoted strings let tempString = inputString.replace(/'([^']*?)'/g, (match, p1) => { const escapedContent = p1.replace(/"/g, '_escaped_double_quote_'); // Temporarily replace double quotes return `'${escapedContent}'`; }); // Step 2: Replace all single quotes with double quotes tempString = tempString.replace(/'/g, '"'); // Step 3: Restore escaped double quotes const processedString = tempString.replace(/_escaped_double_quote_/g, "'"); return processedString; } // ---------------------------------------------------- function jsonSyntaxHighlight(json) { if (typeof json != 'string') { json = JSON.stringify(json, undefined, 2); } json = json.replace(/&/g, '&').replace(//g, '>'); return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { var cls = 'number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'key'; } else { cls = 'string'; } } else if (/true|false/.test(match)) { cls = 'boolean'; } else if (/null/.test(match)) { cls = 'null'; } return '' + match + ''; }); } // ---------------------------------------------------- function isValidBase64(str) { // Base64 characters set var base64CharacterSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; // Remove all valid characters from the string var invalidCharacters = str.replace(new RegExp('[' + base64CharacterSet + ']', 'g'), ''); // If there are any characters left, the string is invalid return invalidCharacters === ''; } // ------------------------------------------------------------------- // Utility function to check if the value is already Base64 function isBase64(value) { if (typeof value !== "string" || value.trim() === "") return false; // Must have valid length if (value.length % 4 !== 0) return false; // Valid Base64 characters const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/; if (!base64Regex.test(value)) return false; try { const decoded = atob(value); // Re-encode const reencoded = btoa(decoded); if (reencoded !== value) return false; // Extra verification: // Ensure decoding didn't silently drop bytes (atob bug) // Encode raw bytes: check if large char codes exist (invalid UTF-16) for (let i = 0; i < decoded.length; i++) { const code = decoded.charCodeAt(i); if (code > 255) return false; // invalid binary byte } return true; } catch (e) { return false; } } // ---------------------------------------------------- function isValidJSON(jsonString) { try { JSON.parse(jsonString); return true; } catch (e) { return false; } } // ---------------------------------------------------- // method to sanitize input so that HTML and other things don't break function encodeSpecialChars(str) { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // ---------------------------------------------------- function decodeSpecialChars(str) { return str .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '\''); } // ---------------------------------------------------- // base64 conversion of UTF8 chars function utf8ToBase64(str) { // Convert the string to a Uint8Array using TextEncoder const utf8Bytes = new TextEncoder().encode(str); // Convert the Uint8Array to a base64-encoded string return btoa(String.fromCharCode(...utf8Bytes)); } // ----------------------------------------------------------------------------- // General utilities // ----------------------------------------------------------------------------- // check if JSON object function isJsonObject(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); } // remove unnecessary lines from the result function sanitize(data) { return data.replace(/(\r\n|\n|\r)/gm,"").replace(/[^\x00-\x7F]/g, "") } // ----------------------------------------------------------------------------- // Check and handle locked database function handle_locked_DB(data) { if(data.includes('database is locked')) { // console.log(data) showSpinner() setTimeout(function() { console.warn("Database locked - reload") location.reload(); }, 5000); } } // ----------------------------------------------------------------------------- function numberArrayFromString(data) { data = JSON.parse(sanitize(data)); return data.replace(/\[|\]/g, '').split(',').map(Number); } // ----------------------------------------------------------------------------- // Update network parent/child relationship (network tree) function updateNetworkLeaf(leafMac, parentMac) { const { apiBase, authHeader } = getAuthContext(); const url = `${apiBase}/device/${leafMac}/update-column`; $.ajax({ method: "POST", url: url, headers: authHeader, data: JSON.stringify({ columnName: "devParentMAC", columnValue: parentMac }), contentType: "application/json", success: function(response) { if(response.success) { showMessage("Saved"); // Remove navigation prompt "Are you sure you want to leave..." window.onbeforeunload = null; } else { showMessage("ERROR: " + (response.error || "Unknown error")); } }, error: function(xhr, status, error) { console.error("Error updating network leaf:", status, error); showMessage("ERROR: " + (xhr.responseJSON?.error || error)); } }); } // ----------------------------------------------------------------------------- // Legacy function wrapper for backward compatibility function saveData(functionName, id, value) { if (functionName === 'updateNetworkLeaf') { updateNetworkLeaf(id, value); } else { console.warn("saveData called with unknown functionName:", functionName); showMessage("ERROR: Unknown function"); } } // ----------------------------------------------------------------------------- // create a link to the device function createDeviceLink(input) { if(checkMacOrInternet(input)) { return `${getDevDataByMac(input, "devName")}` } return input; } // ----------------------------------------------------------------------------- // remove an item from an array function removeItemFromArray(arr, value) { var index = arr.indexOf(value); if (index > -1) { arr.splice(index, 1); } return arr; } // ----------------------------------------------------------------------------- function sleep(milliseconds) { const date = Date.now(); let currentDate = null; do { currentDate = Date.now(); } while (currentDate - date < milliseconds); } // --------------------------------------------------------- somethingChanged = false; function settingsChanged() { somethingChanged = true; // Enable navigation prompt ... "Are you sure you want to leave..." window.onbeforeunload = function() { return true; }; } // ----------------------------------------------------------------------------- // Get Anchor from URL function getUrlAnchor(defaultValue){ target = defaultValue var url = window.location.href; if (url.includes("#")) { // default selection selectedTab = defaultValue // the #target from the url target = window.location.hash.substr(1) // get only the part between #...? if(target.includes('?')) { target = target.split('?')[0] } return target } } // ----------------------------------------------------------------------------- // get query string from URL function getQueryString(key){ params = new Proxy(new URLSearchParams(window.location.search), { get: (searchParams, prop) => searchParams.get(prop), }); tmp = params[key] if(emptyArr.includes(tmp)) { var queryParams = {}; fullUrl = window.location.toString(); // console.log(fullUrl); if (fullUrl.includes('?')) { var queryString = fullUrl.split('?')[1]; // Split the query string into individual parameters var paramsArray = queryString.split('&'); // Loop through the parameters array paramsArray.forEach(function(param) { // Split each parameter into key and value var keyValue = param.split('='); var keyTmp = decodeURIComponent(keyValue[0]); var value = decodeURIComponent(keyValue[1] || ''); // Store key-value pair in the queryParams object queryParams[keyTmp] = value; }); } // console.log(queryParams); tmp = queryParams[key] } result = emptyArr.includes(tmp) ? "" : tmp; return result } // ----------------------------------------------------------------------------- function translateHTMLcodes (text) { if (text == null || emptyArr.includes(text)) { return null; } else if (typeof text === 'string' || text instanceof String) { var text2 = text.replace(new RegExp(' ', 'g'), " "); text2 = text2.replace(new RegExp('<', 'g'), "<"); return text2; } return ""; } // ----------------------------------------------------------------------------- function stopTimerRefreshData () { try { clearTimeout (timerRefreshData); } catch (e) {} } // ----------------------------------------------------------------------------- function newTimerRefreshData (refeshFunction, timeToRefresh) { if(timeToRefresh && (timeToRefresh != 0 || timeToRefresh != "")) { time = parseInt(timeToRefresh) } else { time = 60000 } timerRefreshData = setTimeout (function() { refeshFunction(); }, time); } // ----------------------------------------------------------------------------- function debugTimer () { $('#pageTitle').html (new Date().getSeconds()); } // ----------------------------------------------------------------------------- function secondsSincePageLoad() { // Get the current time since the page was loaded var timeSincePageLoad = performance.now(); // Convert milliseconds to seconds var secondsAgo = Math.floor(timeSincePageLoad / 1000); return secondsAgo; } // ----------------------------------------------------------------------------- // Open url in new tab function openInNewTab (url) { window.open(url, "_blank"); } // ----------------------------------------------------------------------------- // Navigate to URL if the current URL is not in the provided list of URLs function openUrl(urls) { var currentUrl = window.location.href; var mainUrl = currentUrl.match(/^.*?(?=#|\?|$)/)[0]; // Extract main URL var isMatch = false; $.each(urls,function(index, obj){ // remove . for comaprison if in the string, e.g.: ./devices.php arrayUrl = obj.replace('.','') // check if we are on a url contained in the array if(mainUrl.includes(arrayUrl)) { isMatch = true; } }); // if we are not, redirect if (isMatch == false) { window.location.href = urls[0]; // Redirect to the first URL in the list if not found } } // ----------------------------------------------------------------------------- // force load URL in current window with specific anchor function forceLoadUrl(relativeUrl) { window.location.replace(relativeUrl); window.location.reload() } // ----------------------------------------------------------------------------- function navigateToDeviceWithIp (ip) { $.get('php/server/query_json.php', { file: 'table_devices.json', nocache: Date.now() }, function(res) { devices = res["data"]; mac = "" $.each(devices, function(index, obj) { if(obj.devLastIP.trim() == ip.trim()) { mac = obj.devMac; window.open('./deviceDetails.php?mac=' + mac , "_blank"); } }); }); } // ----------------------------------------------------------------------------- // Check if MAC or Internet function checkMacOrInternet(inputStr) { // Regular expression pattern for matching a MAC address const macPattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/; if (inputStr.toLowerCase() === 'internet') { return true; } else if (macPattern.test(inputStr)) { return true; } else { return false; } } // Alias function isValidMac(value) { return checkMacOrInternet(value); } // ----------------------------------------------------------------------------- // Gte MAC from query string function getMac(){ params = new Proxy(new URLSearchParams(window.location.search), { get: (searchParams, prop) => searchParams.get(prop), }); mac = params.mac; if (mac == "") { console.error("Couldn't retrieve mac"); } return mac; } // ----------------------------------------------------------------------------- // A function used to make the IP address orderable function isValidIPv6(ipAddress) { // Regular expression for IPv6 validation const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:){1,7}:|^([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}$|^([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}$|^([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}$|^([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}$|^[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})$/; return ipv6Regex.test(ipAddress); } function isValidIPv4(ip) { const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; return ipv4Regex.test(ip); } function formatIPlong(ipAddress) { if (ipAddress.includes(':') && isValidIPv6(ipAddress)) { const parts = ipAddress.split(':'); return parts.reduce((acc, part, index) => { if (part === '') { const remainingGroups = 8 - parts.length + 1; return acc << (16 * remainingGroups); } const hexValue = parseInt(part, 16); return acc | (hexValue << (112 - index * 16)); }, 0); } else { // Handle IPv4 address const parts = ipAddress.split('.'); if (parts.length !== 4) { console.log("⚠ Invalid IPv4 address: " + ipAddress); return -1; // or any other default value indicating an error } return (parseInt(parts[0]) << 24) | (parseInt(parts[1]) << 16) | (parseInt(parts[2]) << 8) | parseInt(parts[3]); } } // ----------------------------------------------------------------------------- // Check if MAC is a random one function isRandomMAC(mac) { isRandom = false; isRandom = ["2", "6", "A", "E", "a", "e"].includes(mac[1]); // if detected as random, make sure it doesn't start with a prefix which teh suer doesn't want to mark as random if(isRandom) { $.each(createArray(getSetting("UI_NOT_RANDOM_MAC")), function(index, prefix) { if(mac.startsWith(prefix)) { isRandom = false; } }); } return isRandom; } // --------------------------------------------------------- // Generate an array object from a string representation of an array function createArray(input) { // Is already array, return if (Array.isArray(input)) { return input; } // Empty array // if (input === '[]' || input === '') { if (input === '[]') { return []; } // handle integer if (typeof input === 'number') { input = input.toString(); } // Regex pattern for brackets const patternBrackets = /(^\s*\[)|(\]\s*$)/g; const replacement = ''; // Remove brackets const noBrackets = input.replace(patternBrackets, replacement); const options = []; // Detect the type of quote used after the opening bracket const firstChar = noBrackets.trim()[0]; const isDoubleQuoted = firstChar === '"'; const isSingleQuoted = firstChar === "'"; // Create array while handling commas within quoted segments let currentSegment = ''; let withinQuotes = false; for (let i = 0; i < noBrackets.length; i++) { const char = noBrackets[i]; if ((char === '"' && !isSingleQuoted) || (char === "'" && !isDoubleQuoted)) { withinQuotes = !withinQuotes; } if (char === ',' && !withinQuotes) { options.push(currentSegment.trim()); currentSegment = ''; } else { currentSegment += char; } } // Push the last segment options.push(currentSegment.trim()); // Remove quotes based on detected type options.forEach((item, index) => { let trimmedItem = item.trim(); // Check if the string starts and ends with the same type of quote if ((isDoubleQuoted && trimmedItem.startsWith('"') && trimmedItem.endsWith('"')) || (isSingleQuoted && trimmedItem.startsWith("'") && trimmedItem.endsWith("'"))) { // Remove the quotes trimmedItem = trimmedItem.substring(1, trimmedItem.length - 1); } options[index] = trimmedItem; }); return options; } // getDevDataByMac, cacheDevices, devicesListAll_JSON moved to cache.js // ----------------------------------------------------------------------------- function isEmpty(value) { return emptyArr.includes(value) } // ----------------------------------------------------------------------------- function mergeUniqueArrays(arr1, arr2) { let mergedArray = [...arr1]; // Make a copy of arr1 arr2.forEach(element => { if (!mergedArray.includes(element)) { mergedArray.push(element); } }); return mergedArray; } // ----------------------------------------------------------------------------- // Generate a GUID function getGuid() { return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) ); } // ----------------------------------------------------------------------------- // UI // ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- // Loading Spinner overlay // ----------------------------------------------------------------------------- let spinnerTimeout = null; let animationTime = 300 function showSpinner(stringKey = 'Loading') { let text = isEmpty(stringKey) ? "Loading..." : getString(stringKey || "Loading"); if (text == ""){ text = "Loading" } const spinner = $("#loadingSpinner"); const target = $(".spinnerTarget").first(); // Only use the first one if multiple exist $("#loadingSpinnerText").text(text); if (target.length) { // Position relative to target const offset = target.offset(); const width = target.outerWidth(); const height = target.outerHeight(); spinner.css({ position: "absolute", top: offset.top, left: offset.left, width: width, height: height, zIndex: 800 }); } else { // Fullscreen fallback spinner.css({ position: "fixed", top: 0, left: 0, width: "100%", height: "100%", zIndex: 800 }); } requestAnimationFrame(() => { spinner.addClass("visible"); spinner.fadeIn(animationTime); }); } function hideSpinner() { clearTimeout(spinnerTimeout); const spinner = $("#loadingSpinner"); if (!spinner.length) return; const target = $(".spinnerTarget").first(); if (target.length) { // Lock position to target const offset = target.offset(); const width = target.outerWidth(); const height = target.outerHeight(); spinner.css({ position: "absolute", top: offset.top, left: offset.left, width: width, height: height, zIndex: 800 }); } else { // Fullscreen fallback spinner.css({ position: "fixed", top: 0, left: 0, width: "100%", height: "100%", zIndex: 800 }); } // Trigger fade-out and only remove styles AFTER fade completes AND display is none spinner.removeClass("visible").fadeOut(animationTime, () => { // Ensure it's really hidden before resetting styles spinner.css({ display: "none" }); spinner.css({ position: "", top: "", left: "", width: "", height: "", zIndex: "" }); }); } // -------------------------------------------------------- // Calls a backend function to add a front-end event to an execution queue function updateApi(apiEndpoints) { // value has to be in format event|param. e.g. run|ARPSCAN action = `${getGuid()}|update_api|${apiEndpoints}` const { token: apiToken, apiBase: apiBaseUrl, authHeader } = getAuthContext(); const url = `${apiBaseUrl}/logs/add-to-execution-queue`; $.ajax({ method: "POST", url: url, headers: { ...authHeader, "Content-Type": "application/json" }, data: JSON.stringify({ action: action }), success: function(data, textStatus) { console.log(data) } }) } // ----------------------------------------------------------------------------- // handling smooth scrolling // ----------------------------------------------------------------------------- function setupSmoothScrolling() { // Function to scroll to the element function scrollToElement(id) { $('html, body').animate({ scrollTop: $("#" + id).offset().top - 50 }, 1000); } // Scroll to the element when clicking on anchor links $('a[href*="#"]').on('click', function(event) { var href = $(this).attr('href'); if (href !=='#' && href && href.includes('#') && !$(this).is('[data-toggle="collapse"]')) { var id = href.substring(href.indexOf("#") + 1); // Get the ID from the href attribute if ($("#" + id).length > 0) { event.preventDefault(); // Prevent default anchor behavior scrollToElement(id); // Scroll to the element } } }); // Check if there's an ID in the URL and scroll to it var url = window.location.href; if (url.includes("#")) { var idFromURL = url.substring(url.indexOf("#") + 1); if (idFromURL != "" && $("#" + idFromURL).length > 0) { scrollToElement(idFromURL); } } } // ------------------------------------------------------------------- // Function to check if options_params contains a parameter with type "sql" function hasSqlType(params) { for (let param of params) { if (param.type === "sql") { return true; // Found a parameter with type "sql" } } return false; // No parameter with type "sql" found } // ------------------------------------------------------------------- // Function to check if string is SQL query function isSQLQuery(query) { // Regular expression to match common SQL keywords and syntax with word boundaries var sqlRegex = /\b(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|FROM|JOIN|WHERE|SET|VALUES|GROUP BY|ORDER BY|LIMIT)\b/i; return sqlRegex.test(query); } // ------------------------------------------------------------------- // Get corresponding plugin setting object function getPluginSettingObject(pluginsData, setting_key, unique_prefix ) { result = {} unique_prefix == undefined ? unique_prefix = setting_key.split("_")[0] : unique_prefix = unique_prefix; $.each(pluginsData, function (i, plgnObj){ // go thru plugins if(plgnObj.unique_prefix == unique_prefix) { // go thru plugin settings $.each(plgnObj["settings"], function (j, setObj){ if(`${unique_prefix}_${setObj.function}` == setting_key) { result = setObj } }); } }); return result } // ------------------------------------------------------------------- // Resolve all option parameters function resolveParams(params, template) { params.forEach(param => { // Check if the template includes the parameter name if (template.includes("{" + param.name + "}")) { // If the parameter type is 'setting', retrieve setting value if (param.type == "setting") { var value = getSetting(param.value); // Remove brackets and single quotes, replace them with double quotes value = value.replace('[','').replace(']','').replace(/'/g, '"'); // Split the string into an array, remove empty elements const arr = value.split(',').filter(Boolean); // Join the array elements with commas const result = arr.join(', '); // Replace placeholder with setting value template = template.replace("{" + param.name + "}", result); } else { // If the parameter type is not 'setting', use the provided value template = template.replace("{" + param.name + "}", param.value); } } }); // Log the resolved template // console.log(template); // Return the resolved template return template; } // ----------------------------------------------------------------------------- // check if two arrays contain same values even if out of order function arraysContainSameValues(arr1, arr2) { // Check if both parameters are arrays if (!Array.isArray(arr1) || !Array.isArray(arr2)) { return false; } else { // Sort and stringify arrays, then compare return JSON.stringify(arr1.slice().sort()) === JSON.stringify(arr2.slice().sort()); } } // ----------------------------------------------------------------------------- // Hide elements on the page based on the supplied setting function hideUIelements(setKey) { hiddenSectionsSetting = getSetting(setKey) if(hiddenSectionsSetting != "") // handle if settings not yet initialized { sectionsArray = createArray(hiddenSectionsSetting) // remove spaces to get IDs var newArray = $.map(sectionsArray, function(value) { return value.replace(/\s/g, ''); }); $.each(newArray, function(index, hiddenSection) { if($('#' + hiddenSection)) { $('#' + hiddenSection).hide() } }); } } // ------------------------------------------------------------ function getDevicesList() { // Read cache (skip cookie expiry check) const cached = getCache(CACHE_KEYS.DEVICES_ALL); let devicesList = parseDeviceCache(cached); // only loop thru the filtered down list visibleDevices = getCache(CACHE_KEYS.VISIBLE_MACS) if(visibleDevices != "") { visibleDevicesMACs = visibleDevices.split(','); devicesList_tmp = []; // Iterate through the data and filter only visible devices $.each(devicesList, function(index, item) { // Check if the current item's MAC exists in visibleDevicesMACs if (visibleDevicesMACs.includes(item.devMac)) { devicesList_tmp.push(item); } }); // Update devicesList with the filtered items devicesList = devicesList_tmp; } return devicesList; } // ----------------------------------------------------------------------------- // apply theme $(document).ready(function() { let theme = getSetting("UI_theme"); if (theme) { theme = theme.replace("['","").replace("']",""); // Add the theme stylesheet setCookie("UI_theme", theme); switch(theme) { case "Dark": $('head').append(''); break; case "System": $('head').append(''); break } } else { setCookie("UI_theme", "Light"); } }); // ----------------------------------------------------------- // Restart Backend Python Server function askRestartBackend() { // Ask showModalWarning(getString('Maint_RestartServer'), getString('Maint_Restart_Server_noti_text'), getString('Gen_Cancel'), getString('Maint_RestartServer'), 'restartBackend'); } // ----------------------------------------------------------- function restartBackend() { modalEventStatusId = 'modal-message-front-event' const { token: apiToken, apiBase: apiBaseUrl, authHeader } = getAuthContext(); const url = `${apiBaseUrl}/logs/add-to-execution-queue`; // Execute $.ajax({ method: "POST", url: url, headers: { ...authHeader, "Content-Type": "application/json" }, data: JSON.stringify({ action: `cron_restart_backend` }), success: function(data, textStatus) { // showModalOk ('Result', data ); // show message showModalOk(getString("general_event_title"), `${getString("general_event_description")}

`); updateModalState() write_notification('[Maintenance] App manually restarted', 'info') } }) } // App lifecycle (completedCalls, executeOnce, handleSuccess, clearCache, etc.) moved to app-init.js