/* ----------------------------------------------------------------------------- * NetAlertX * Open Source Network Guard / WIFI & LAN intrusion detector * * cache.js - Front module. Cache primitives, settings, strings, and device * data caching. Loaded FIRST — no dependencies on other NAX files. * All cross-file calls (handleSuccess, showSpinner, etc.) are * call-time dependencies resolved after page load. *------------------------------------------------------------------------------- # jokob@duck.com GNU GPLv3 ----------------------------------------------------------------------------- */ // Cache version stamp — injected by header.php from the app's .VERSION file. // Changes automatically on every release, busting stale localStorage caches. // Falls back to a build-time constant so local dev without PHP still works. const NAX_CACHE_VERSION = (typeof window.NAX_APP_VERSION !== 'undefined') ? window.NAX_APP_VERSION : 'dev'; // ----------------------------------------------------------------------------- // Central registry of all localStorage cache keys. // Use these constants (and the helper functions for dynamic keys) everywhere // instead of bare string literals to prevent silent typo bugs. // ----------------------------------------------------------------------------- const CACHE_KEYS = { // --- Init flags (dynamic) --- // Stores "true" when an AJAX init call completes. Use initFlag(name) below. initFlag: (name) => `${name}_completed`, // --- Settings --- // Stores the value of a setting by its setKey. nax_set_ setting: (key) => `nax_set_${key}`, // Stores the resolved options array for a setting. nax_set_opt_ settingOpts: (key) => `nax_set_opt_${key}`, // --- Language strings --- // Stores a translated string. pia_lang__ langString: (key, langCode) => `pia_lang_${key}_${langCode}`, LANG_FALLBACK: 'en_us', // fallback language code // --- Devices --- DEVICES_ALL: 'devicesListAll_JSON', // full device list from table_devices.json DEVICES_TOPOLOGY: 'devicesListNew', // filtered/sorted list for network topology // --- UI state --- VISIBLE_MACS: 'ntx_visible_macs', // comma-separated MACs visible in current view SHOW_ARCHIVED: 'showArchived', // topology show-archived toggle (network page) SHOW_OFFLINE: 'showOffline', // topology show-offline toggle (network page) // --- Internal init tracking --- GRAPHQL_STARTED: 'graphQLServerStarted', // set when GraphQL server responds STRINGS_COUNT: 'cacheStringsCountCompleted', // count of language packs loaded COMPLETED_CALLS: 'completedCalls', // comma-joined list of completed init calls INIT_TIMESTAMP: 'nax_init_timestamp', // ms timestamp of last successful cache init CACHE_VERSION: 'nax_cache_version', // version stamp for auto-bust on deploy }; // ----------------------------------------------------------------------------- // localStorage cache helpers // ----------------------------------------------------------------------------- function getCache(key) { // check cache cachedValue = localStorage.getItem(key) if(cachedValue) { return cachedValue; } return ""; } // ----------------------------------------------------------------------------- function setCache(key, data) { localStorage.setItem(key, data); } // ----------------------------------------------------------------------------- // Fetch data from a server-generated JSON file via query_json.php. // Returns a Promise resolving with the "data" array from the response. // ----------------------------------------------------------------------------- function fetchJson(file) { return new Promise((resolve, reject) => { $.get('php/server/query_json.php', { file: file, nocache: Date.now() }) .done((res) => resolve(res['data'] || [])) .fail((err) => reject(err)); }); } // ----------------------------------------------------------------------------- // Safely parse and normalize device cache data. // Handles both direct array format and { data: [...] } format. // Returns an array, or empty array on failure. function parseDeviceCache(cachedStr) { if (!cachedStr || cachedStr === "") { return []; } let parsed; try { parsed = JSON.parse(cachedStr); } catch (err) { console.error('[parseDeviceCache] Failed to parse:', err); return []; } // If result is an object with a .data property, extract it (handles legacy format) if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && Array.isArray(parsed.data)) { console.warn('[parseDeviceCache] Extracting .data property from wrapper object'); parsed = parsed.data; } // Ensure result is an array if (!Array.isArray(parsed)) { console.error('[parseDeviceCache] Result is not an array:', parsed); return []; } return parsed; } // ----------------------------------------------------------------------------- // Returns the API token, base URL, and a ready-to-use Authorization header // object for all backend API calls. Centralises the repeated // getSetting("API_TOKEN") + getApiBase() pattern. // ----------------------------------------------------------------------------- function getAuthContext() { const token = getSetting('API_TOKEN'); const apiBase = getApiBase(); return { token, apiBase, authHeader: { 'Authorization': 'Bearer ' + token }, }; } // ----------------------------------------------------------------------------- // Get settings from the .json file generated by the python backend // and cache them, if available, with options // ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- // Bootstrap: fetch API_TOKEN and GRAPHQL_PORT directly from app.conf via the // PHP helper endpoint. Runs before cacheSettings so that API calls made during // or after init always have a token available — even if table_settings.json // hasn't been generated yet. Writes values into the setting() namespace so // getSetting("API_TOKEN") and getSetting("GRAPHQL_PORT") work immediately. // ----------------------------------------------------------------------------- function cacheApiConfig() { return new Promise((resolve, reject) => { if (getCache(CACHE_KEYS.initFlag('cacheApiConfig')) === 'true') { resolve(); return; } $.get('php/server/app_config.php', { nocache: Date.now() }) .done((res) => { if (res && res.api_token) { setCache(CACHE_KEYS.setting('API_TOKEN'), res.api_token); setCache(CACHE_KEYS.setting('GRAPHQL_PORT'), String(res.graphql_port || 20212)); handleSuccess('cacheApiConfig'); resolve(); } else { console.warn('[cacheApiConfig] Response missing api_token — will rely on cacheSettings fallback'); resolve(); // non-fatal: cacheSettings will still populate these } }) .fail((err) => { console.warn('[cacheApiConfig] Failed to reach app_config.php:', err); resolve(); // non-fatal fallback }); }); } function cacheSettings() { return new Promise((resolve, reject) => { if(getCache(CACHE_KEYS.initFlag('cacheSettings')) === "true") { resolve(); return; } // plugins.json may not exist on first boot — treat its absence as non-fatal Promise.all([fetchJson('table_settings.json'), fetchJson('plugins.json').catch(() => [])]) .then(([settingsArr, pluginsArr]) => { pluginsData = pluginsArr; settingsData = settingsArr; // Defensive: Accept either array or object with .data property // for both settings and plugins if (!Array.isArray(settingsData)) { if (settingsData && Array.isArray(settingsData.data)) { settingsData = settingsData.data; } else { console.error('[cacheSettings] settingsData is not an array:', settingsData); reject(new Error('settingsData is not an array')); return; } } // Normalize plugins array too (may have { data: [...] } format) if (!Array.isArray(pluginsData)) { if (pluginsData && Array.isArray(pluginsData.data)) { pluginsData = pluginsData.data; } else { console.warn('[cacheSettings] pluginsData is not an array, treating as empty'); pluginsData = []; } } 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(CACHE_KEYS.setting(set.setKey), set.setValue) setCache(CACHE_KEYS.settingOpts(set.setKey), resolvedOptions) }); handleSuccess('cacheSettings'); resolve(); }) .catch((err) => { handleFailure('cacheSettings'); reject(err); }); }); } // ----------------------------------------------------------------------------- // Get a setting options value by key function getSettingOptions (key) { result = getCache(CACHE_KEYS.settingOpts(key)); if (result == "") { result = [] } return result; } // ----------------------------------------------------------------------------- // Get a setting value by key function getSetting (key) { result = getCache(CACHE_KEYS.setting(key)); return result; } // ----------------------------------------------------------------------------- // Get language string // ----------------------------------------------------------------------------- function cacheStrings() { return new Promise((resolve, reject) => { if(getCache(CACHE_KEYS.initFlag('cacheStrings')) === "true") { resolve(); return; } // 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(CACHE_KEYS.langString(key, language_code), value); }); // Fetch strings and translations from plugins (non-fatal — file may // not exist on first boot or immediately after a cache clear) fetchJson('table_plugins_language_strings.json') .catch((pluginError) => { console.warn('[cacheStrings] Plugin language strings unavailable (non-fatal):', pluginError); return []; // treat as empty list }) .then((data) => { // Defensive: ensure data is an array (fetchJson may return // an object, undefined, or empty string on edge cases) if (!Array.isArray(data)) { data = []; } // Store plugin translations data.forEach((langString) => { setCache(CACHE_KEYS.langString(langString.String_Key, langString.Language_Code), langString.String_Value); }); // Handle successful completion of language processing handleSuccess('cacheStrings'); resolveLang(); }); }) .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(error); }); }); } // ----------------------------------------------------------------------------- // Get translated language string function getString(key) { function fetchString(key) { lang_code = getLangCode(); let result = getCache(CACHE_KEYS.langString(key, lang_code)); if (isEmpty(result)) { result = getCache(CACHE_KEYS.langString(key, CACHE_KEYS.LANG_FALLBACK)); } 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; case 'Vietnamese (vi_vn)': lang_code = 'vi_vn'; break; } return lang_code; } // ----------------------------------------------------------------------------- // A function to get a device property using the mac address as key and DB column name as parameter // for the value to be returned function getDevDataByMac(macAddress, dbColumn) { const sessionDataKey = CACHE_KEYS.DEVICES_ALL; const devicesCache = getCache(sessionDataKey); if (!devicesCache || devicesCache == "") { console.warn(`[getDevDataByMac] Cache key "${sessionDataKey}" is empty — cache may not be initialized yet.`); return null; } const devices = parseDeviceCache(devicesCache); if (devices.length === 0) { return null; } 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) => { if(getCache(CACHE_KEYS.initFlag('cacheDevices')) === "true") { resolve(); return; } fetchJson('table_devices.json') .then((arr) => { devicesListAll_JSON = arr; devicesListAll_JSON_str = JSON.stringify(devicesListAll_JSON) if(devicesListAll_JSON_str == "") { showSpinner() setTimeout(() => { cacheDevices() }, 1000); } setCache(CACHE_KEYS.DEVICES_ALL, devicesListAll_JSON_str) handleSuccess('cacheDevices'); resolve(); }) .catch((err) => { handleFailure('cacheDevices'); reject(err); }); } ); } var devicesListAll_JSON = []; // this will contain a list off all devices