/* ----------------------------------------------------------------------------- * 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; } } // ---------------------------------------------------- /** * Encode special HTML characters into HTML entities. * * Prevents HTML injection/XSS when displaying untrusted text * inside HTML content contexts. * * Example: *