/* ----------------------------------------------------------------------------- * 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)"; const allLanguages = ["ar_ar","ca_ca","cs_cz","de_de","en_us","es_es","fa_fa","fr_fr","it_it","ja_jp","nb_no","pl_pl","pt_br","pt_pt","ru_ru","sv_sv","tr_tr","uk_ua","zh_cn"]; // needs to be same as in lang.php var settingsJSON = {} // ----------------------------------------------------------------------------- // Simple session cache withe expiration managed via cookies // ----------------------------------------------------------------------------- function getCache(key, noCookie = false) { // check cache cachedValue = localStorage.getItem(key) // console.log(cachedValue); if(cachedValue) { // // check if not expired // if(noCookie || getCookie(key + '_session_expiry') != "") // { return cachedValue; // } } return ""; } // ----------------------------------------------------------------------------- function setCache(key, data, expirationMinutes='') { localStorage.setItem(key, data); // // create cookie if expiration set to handle refresh of data // if (expirationMinutes != '') // { // setCookie ('cache_session_expiry', 'OK', 1) // } } // ----------------------------------------------------------------------------- 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'; } // ----------------------------------------------------------------------------- function deleteAllCookies() { // Array of cookies var allCookies = document.cookie.split(";"); // For each cookie for (var i = 0; i < allCookies.length; i++) { var cookie = allCookies[i].trim(); var eqPos = cookie.indexOf("="); var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 UTC"; } } // ----------------------------------------------------------------------------- // Get settings from the .json file generated by the python backend // and cache them, if available, with options // ----------------------------------------------------------------------------- function cacheSettings() { return new Promise((resolve, reject) => { if(!getCache('cacheSettings_completed') === true) { $.get('php/server/query_json.php', { file: 'table_settings.json', nocache: Date.now() }, function(resSet) { $.get('php/server/query_json.php', { file: 'plugins.json', nocache: Date.now() }, function(resPlug) { pluginsData = resPlug["data"]; settingsData = resSet["data"]; settingsData.forEach((set) => { resolvedOptions = createArray(set.setOptions) resolvedOptionsOld = resolvedOptions setPlugObj = {}; options_params = []; resolved = "" // proceed only if first option item contains something to resolve if( !set.setKey.includes("__metadata") && resolvedOptions.length != 0 && resolvedOptions[0].includes("{value}")) { // get setting definition from the plugin config if available setPlugObj = getPluginSettingObject(pluginsData, set.setKey) // check if options contains parameters and resolve if(setPlugObj != {} && setPlugObj["options_params"]) { // get option_params for {value} resolution options_params = setPlugObj["options_params"] if(options_params != []) { // handles only strings of length == 1 resolved = resolveParams(options_params, resolvedOptions[0]) if(resolved.includes('"')) // check if list of strings { resolvedOptions = `[${resolved}]` } else // one value only { resolvedOptions = `["${resolved}"]` } } } } setCache(`nax_set_${set.setKey}`, set.setValue) setCache(`nax_set_opt_${set.setKey}`, resolvedOptions) }); }).then(() => handleSuccess('cacheSettings', resolve())).catch(() => handleFailure('cacheSettings', reject("cacheSettings already completed"))); // handle AJAX synchronization }) } }); } // ----------------------------------------------------------------------------- // Get a setting options value by key function getSettingOptions (key) { // handle initial load to make sure everything is set-up and cached // handleFirstLoad() result = getCache(`nax_set_opt_${key}`, true); if (result == "") { console.log(`Setting options with key "${key}" not found`) result = [] } return result; } // ----------------------------------------------------------------------------- // Get a setting value by key function getSetting (key) { // handle initial load to make sure everything is set-up and cached // handleFirstLoad() result = getCache(`nax_set_${key}`, true); if (result == "") { console.log(`Setting with key "${key}" not found`) } return result; } // ----------------------------------------------------------------------------- // Get language string // ----------------------------------------------------------------------------- function cacheStrings() { return new Promise((resolve, reject) => { // Create a promise for each language (include en_us by default as fallback) languagesToLoad = ['en_us'] additionalLanguage = getLangCode() if(additionalLanguage != 'en_us') { languagesToLoad.push(additionalLanguage) } console.log(languagesToLoad); const languagePromises = languagesToLoad.map((language_code) => { return new Promise((resolveLang, rejectLang) => { // Fetch core strings and translations $.get(`php/templates/language/${language_code}.json?nocache=${Date.now()}`) .done((res) => { // Iterate over each key-value pair and store the translations Object.entries(res).forEach(([key, value]) => { setCache(`pia_lang_${key}_${language_code}`, value); }); // Fetch strings and translations from plugins $.get('php/server/query_json.php', { file: 'table_plugins_language_strings.json', nocache: Date.now() }) .done((pluginRes) => { const data = pluginRes["data"]; // Store plugin translations data.forEach((langString) => { setCache(`pia_lang_${langString.String_Key}_${langString.Language_Code}`, langString.String_Value); }); // Handle successful completion of language processing handleSuccess(`cacheStrings`, resolveLang); }) .fail((pluginError) => { // Handle failure in plugin strings fetching rejectLang(pluginError); }); }) .fail((error) => { // Handle failure in core strings fetching rejectLang(error); }); }); }); // Wait for all language promises to complete Promise.all(languagePromises) .then(() => { // All languages processed successfully resolve(); }) .catch((error) => { // Handle failure in any of the language processing handleFailure('cacheStrings', reject); }); }); } // ----------------------------------------------------------------------------- // Get translated language string function getString(key) { function fetchString(key) { lang_code = getLangCode(); let result = getCache(`pia_lang_${key}_${lang_code}`, true); if (isEmpty(result)) { result = getCache(`pia_lang_${key}_en_us`, true); } return result; } if (isAppInitialized()) { return fetchString(key); } else { callAfterAppInitialized(() => fetchString(key)); } } // ----------------------------------------------------------------------------- // Get current language ISO code // below has to match exactly the values in /front/php/templates/language/lang.php & /front/js/common.js function getLangCode() { UI_LANG = getSetting("UI_LANG"); let lang_code = 'en_us'; switch (UI_LANG) { case 'English (en_us)': lang_code = 'en_us'; break; case 'Spanish (es_es)': lang_code = 'es_es'; break; case 'German (de_de)': lang_code = 'de_de'; break; case 'Farsi (fa_fa)': lang_code = 'fa_fa'; break; case 'French (fr_fr)': lang_code = 'fr_fr'; break; case 'Norwegian (nb_no)': lang_code = 'nb_no'; break; case 'Polish (pl_pl)': lang_code = 'pl_pl'; break; case 'Portuguese (pt_br)': lang_code = 'pt_br'; break; case 'Portuguese (pt_pt)': lang_code = 'pt_pt'; break; case 'Turkish (tr_tr)': lang_code = 'tr_tr'; break; case 'Swedish (sv_sv)': lang_code = 'sv_sv'; break; case 'Italian (it_it)': lang_code = 'it_it'; break; case 'Japanese (ja_jp)': lang_code = 'ja_jp'; break; case 'Russian (ru_ru)': lang_code = 'ru_ru'; break; case 'Chinese (zh_cn)': lang_code = 'zh_cn'; break; case 'Czech (cs_cz)': lang_code = 'cs_cz'; break; case 'Arabic (ar_ar)': lang_code = 'ar_ar'; break; case 'Catalan (ca_ca)': lang_code = 'ca_ca'; break; case 'Ukrainian (uk_uk)': lang_code = 'uk_ua'; break; } return lang_code; } // ----------------------------------------------------------------------------- // String utilities // ----------------------------------------------------------------------------- function localizeTimestamp(input) { let tz = getSetting("TIMEZONE") || 'Europe/Berlin'; 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) { const date = new Date(str); if (!isFinite(date)) { console.error(`ERROR: Couldn't parse date: '${str}' with TIMEZONE ${tz}`); return 'Failed conversion - Check browser console'; } 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(date); } } // ---------------------------------------------------- /** * 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); } // ----------------------------------------------------------------------------- function saveData(functionName, id, value) { $.ajax({ method: "GET", url: "php/server/devices.php", data: { action: functionName, id: id, value:value }, success: function(data) { if(sanitize(data) == 'OK') { showMessage("Saved") // Remove navigation prompt "Are you sure you want to leave..." window.onbeforeunload = null; } else { showMessage("ERROR") } } }); } // ----------------------------------------------------------------------------- // 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), }); return params.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 === '') { 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; } // ----------------------------------------------------------------------------- // A function to get a device property using the mac address as key and DB column nakme as parameter // for the value to be returned function getDevDataByMac(macAddress, dbColumn) { const sessionDataKey = 'devicesListAll_JSON'; const devicesCache = getCache(sessionDataKey); if (!devicesCache || devicesCache == "") { console.error(`Session variable "${sessionDataKey}" not found.`); return null; } const devices = JSON.parse(devicesCache); for (const device of devices) { if (device["devMac"].toLowerCase() === macAddress.toLowerCase()) { if(dbColumn) { return device[dbColumn]; } else { return device } } } console.error("⚠ Device with MAC not found:" + macAddress) return null; // Return a default value if MAC address is not found } // ----------------------------------------------------------------------------- // Cache the devices as one JSON function cacheDevices() { return new Promise((resolve, reject) => { $.get('php/server/query_json.php', { file: 'table_devices.json', nocache: Date.now() }, function(data) { // console.log(data) devicesListAll_JSON = data["data"] devicesListAll_JSON_str = JSON.stringify(devicesListAll_JSON) if(devicesListAll_JSON_str == "") { showSpinner() setTimeout(() => { cacheDevices() }, 1000); } // console.log(devicesListAll_JSON_str); setCache('devicesListAll_JSON', devicesListAll_JSON_str) // console.log(getCache('devicesListAll_JSON')) }).then(() => handleSuccess('cacheDevices', resolve())).catch(() => handleFailure('cacheDevices', reject("cacheDevices already completed"))); // handle AJAX synchronization } ); } var devicesListAll_JSON = []; // this will contain a list off all devices // ----------------------------------------------------------------------------- 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') { const text = isEmpty(stringKey) ? "Loading" : getString(stringKey || "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}` $.ajax({ method: "POST", url: "php/server/util.php", data: { function: "addToExecutionQueue", 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) devicesList = getCache('devicesListAll_JSON', true); if (devicesList != '') { devicesList = JSON.parse (devicesList); } else { devicesList = []; } // only loop thru the filtered down list visibleDevices = getCache("ntx_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' // Execute $.ajax({ method: "POST", url: "php/server/util.php", data: { function: "addToExecutionQueue", action: `${getGuid()}|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') } }) } // ----------------------------------------------------------------------------- // initialize // ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- // Define a unique key for storing the flag in sessionStorage const sessionStorageKey = "myScriptExecuted_common_js"; var completedCalls = [] var completedCalls_final = ['cacheSettings', 'cacheStrings', 'cacheDevices']; var lang_completedCalls = 0; // ----------------------------------------------------------------------------- // Clearing all the caches function clearCache() { showSpinner(); sessionStorage.clear(); localStorage.clear(); setTimeout(() => { console.warn("clearChache called"); window.location.reload(); }, 500); } // ----------------------------------------------------------------------------- // Function to check if cache needs to be refreshed because of setting changes function checkSettingChanges() { $.get('php/server/query_json.php', { file: 'app_state.json', nocache: Date.now() }, function(appState) { const importedMilliseconds = parseInt(appState["settingsImported"] * 1000); const lastReloaded = parseInt(sessionStorage.getItem(sessionStorageKey + '_time')); if (importedMilliseconds > lastReloaded) { console.log("Cache needs to be refreshed because of setting changes"); setTimeout(() => { clearCache(); }, 500); } }); } // ----------------------------------------------------------------------------- // Display spinner and reload page if not yet initialized async function handleFirstLoad(callback) { if (!isAppInitialized()) { await new Promise(resolve => setTimeout(resolve, 1000)); callback(); } } // ----------------------------------------------------------------------------- // Execute callback once the app is initialized and GraphQL server is running async function callAfterAppInitialized(callback) { if (!isAppInitialized() || !(await isGraphQLServerRunning())) { setTimeout(() => { callAfterAppInitialized(callback); }, 500); } else { callback(); } } // ----------------------------------------------------------------------------- // Polling function to repeatedly check if the server is running async function waitForGraphQLServer() { const pollInterval = 2000; // 2 seconds between each check let serverRunning = false; while (!serverRunning) { serverRunning = await isGraphQLServerRunning(); if (!serverRunning) { console.log("GraphQL server not running, retrying in 2 seconds..."); await new Promise(resolve => setTimeout(resolve, pollInterval)); } } console.log("GraphQL server is now running."); } // ----------------------------------------------------------------------------- // Returns 1 if running, 0 otherwise async function isGraphQLServerRunning() { try { const response = await $.get('php/server/query_json.php', { file: 'app_state.json', nocache: Date.now()}); console.log("graphQLServerStarted: " + response["graphQLServerStarted"]); setCache("graphQLServerStarted", response["graphQLServerStarted"]); return response["graphQLServerStarted"]; } catch (error) { console.error("Failed to check GraphQL server status:", error); return false; } } // ----------------------------------------------------------------------------- // Check if the code has been executed before by checking sessionStorage function isAppInitialized() { lang_shouldBeCompletedCalls = getLangCode() == 'en_us' ? 1 : 2; // check if each ajax call completed succesfully $.each(completedCalls_final, function(index, call_name){ if(getCache(call_name + "_completed") != "true") { console.log(`[isAppInitialized] AJAX call ${call_name} unsuccesful: ${getCache(call_name + "_completed")}`) return false; } }); // check if all required languages chached if(parseInt(getCache("cacheStringsCountCompleted")) != lang_shouldBeCompletedCalls) { console.log(`[isAppInitialized] AJAX call cacheStrings unsuccesful: ${getCache("cacheStringsCountCompleted")} out of ${lang_shouldBeCompletedCalls}`) return false; } return true; } // ----------------------------------------------------------------------------- // Main execution logic async function executeOnce() { showSpinner(); if (!isAppInitialized()) { try { await waitForGraphQLServer(); // Wait for the server to start await cacheDevices(); await cacheSettings(); await cacheStrings(); console.log("All AJAX callbacks have completed"); onAllCallsComplete(); } catch (error) { console.error("Error:", error); } } } // ----------------------------------------------------------------------------- // Function to handle successful completion of an AJAX call const handleSuccess = (callName) => { console.log(`AJAX call successful: ${callName}`); if(callName.includes("cacheStrings")) { completed_tmp = getCache("cacheStringsCountCompleted"); completed_tmp == "" ? completed_tmp = 0 : completed_tmp = completed_tmp; completed_tmp++; setCache("cacheStringsCountCompleted", completed_tmp); } setCache(callName + "_completed", true) }; // ----------------------------------------------------------------------------- // Function to handle failure of an AJAX call const handleFailure = (callName, callback) => { msg = `AJAX call ${callName} failed` console.error(msg); // Implement retry logic here if needed // write_notification(msg, 'interrupt') }; // ----------------------------------------------------------------------------- // Function to execute when all AJAX calls have completed const onAllCallsComplete = () => { completedCalls = mergeUniqueArrays(getCache('completedCalls').split(','), completedCalls); setCache('completedCalls', completedCalls); // Check if all necessary strings are initialized if (areAllStringsInitialized()) { sessionStorage.setItem(sessionStorageKey, "true"); const millisecondsNow = Date.now(); sessionStorage.setItem(sessionStorageKey + '_time', millisecondsNow); console.log('✔ Cache initialized'); // setTimeout(() => { // location.reload() // }, 10); } else { // If not all strings are initialized, retry initialization console.log('❌ Not all strings are initialized. Retrying...'); executeOnce(); return; } // Call any other initialization functions here if needed }; // Function to check if all necessary strings are initialized const areAllStringsInitialized = () => { // Implement logic to check if all necessary strings are initialized // Return true if all strings are initialized, false otherwise return getString('UI_LANG_name') != "" }; // Call the function to execute the code executeOnce(); // Set timer for regular UI refresh if enabled setTimeout(() => { // page refresh if configured const refreshTime = getSetting("UI_REFRESH"); if (refreshTime && refreshTime !== "0" && refreshTime !== "") { console.log("Refreshing page becasue UI_REFRESH setting enabled."); newTimerRefreshData(clearCache, parseInt(refreshTime)*1000); } // Check if page needs to refresh due to setting changes checkSettingChanges() }, 10000); console.log("init common.js");