var Debug = false; class UrlsBase { constructor(root) { this.root = root; const paths = ["ws", "upgrade", "config", "auth"]; paths.forEach((path) => { this[path] = new URL(path, root); this[path].protocol = root.protocol; }); if (this.root.protocol === "https:") { this.ws.protocol = "wss:"; } else { this.ws.protocol = "ws:"; } } } var Urls = null; var WebsockPingPong = null; var Websock = { send: function() { }, close: function() { } }; class SettingsBase { constructor() { this.counters = {}; this.resetCounters(); this.saved = false; } resetCounters() { this.counters.changed = 0; this.counters.reboot = 0; this.counters.reconnect = 0; this.counters.reload = 0; } } var Settings = new SettingsBase(); var Enumerable = {}; var FreeSize = 0; var Now = 0; var Ago = 0; class CmdOutputBase { constructor(elem) { this.elem = elem; this.lastScrollHeight = elem.scrollHeight; this.lastScrollTop = elem.scrollTop; this.followScroll = true; elem.addEventListener("scroll", () => { // in case we adjust the scroll manually const current = this.elem.scrollHeight - this.elem.scrollTop; const last = this.lastScrollHeight - this.lastScrollTop; if ((current - last) > 16) { this.followScroll = false; } // ...and, in case we return to the bottom row const offset = current - this.elem.offsetHeight; if (offset < 16) { this.followScroll = true; } this.lastScrollHeight = this.elem.scrollHeight; this.lastScrollTop = this.elem.scrollTop; }); } follow() { if (this.followScroll) { this.elem.scrollTop = this.elem.scrollHeight; this.lastScrollHeight = this.elem.scrollHeight; this.lastScrollTop = this.elem.scrollTop; } } clear() { this.elem.textContent = ""; this.followScroll = true; } push(line) { this.elem.appendChild(new Text(line)); } pushAndFollow(line) { this.elem.appendChild(new Text(`${line}\n`)); this.followScroll = true } } var CmdOutput = null; //removeIf(!light) var ColorPicker; //endRemoveIf(!light) //removeIf(!rfm69) var Rfm69 = { filters: {} }; //endRemoveIf(!rfm69) //removeIf(!sensor) var Magnitudes = { properties: {}, errors: {}, types: {}, units: { names: {}, supported: {} }, typePrefix: {}, prefixType: {} }; function magnitudeTypedKey(magnitude, name) { const prefix = Magnitudes.typePrefix[magnitude.type]; const index = magnitude.index_global; return `${prefix}${name}${index}`; } //endRemoveIf(!sensor) // ----------------------------------------------------------------------------- // Utils // ----------------------------------------------------------------------------- function showErrorNotification(message) { let container = document.getElementById("error-notification"); if (container.childElementCount > 0) { return false; } container.style.display = "inherit"; container.style.whiteSpace = "pre-wrap"; let notification = document.createElement("div"); notification.classList.add("pure-u-1"); notification.classList.add("pure-u-lg-1"); notification.textContent = message; container.appendChild(notification); return false; } function notifyError(message, source, lineno, colno, error) { let text = ""; if (error) { text = error.stack; } else { text = message; } text += "\n\nFor more info see the Debug Log and / or Developer Tools console."; return showErrorNotification(text); } window.onerror = notifyError; // TODO: per https://www.chromestatus.com/feature/6140064063029248, using should be enough with recent browsers // but, side menu needs to be reworked for it to correctly handle panel switching, since it uses // TODO: also could be done in htmlparser2 + gulp (even, preferably) function initExternalLinks() { for (let elem of document.getElementsByClassName("external")) { if (elem.tagName === "A") { elem.setAttribute("target", "_blank"); elem.setAttribute("rel", "noopener"); elem.setAttribute("tabindex", "-1"); } } } // TODO: note that we also include kv schema as 'data-settings-schema' on the container. // produce a 'set' and compare instead of just matching length? function fromSchema(source, schema) { if (schema.length !== source.length) { throw `Schema mismatch! Expected length ${schema.length} vs. ${source.length}`; } var target = {}; schema.forEach((key, index) => { target[key] = source[index]; }); return target; } function keepTime() { document.querySelector("span[data-key='ago']").textContent = Ago; ++Ago; if (0 === Now) { return; } let text = (new Date(Now * 1000)) .toISOString().substring(0, 19) .replace("T", " "); document.querySelector("span[data-key='now']").textContent = text; ++Now; } function setUptime(value) { let uptime = parseInt(value, 10); let seconds = uptime % 60; uptime = parseInt(uptime / 60, 10); let minutes = uptime % 60; uptime = parseInt(uptime / 60, 10); let hours = uptime % 24; uptime = parseInt(uptime / 24, 10); let days = uptime; let container = document.querySelector("span[data-key='uptime']"); container.textContent = days + "d " + zeroPad(hours, 2) + "h " + zeroPad(minutes, 2) + "m " + zeroPad(seconds, 2) + "s"; Ago = 0; } function zeroPad(number, positions) { return number.toString().padStart(positions, "0"); } function validatePassword(password) { // http://www.the-art-of-web.com/javascript/validate-password/ // at least one lowercase and one uppercase letter or number // at least eight characters (letters, numbers or special characters) // MUST be 8..63 printable ASCII characters. See: // https://en.wikipedia.org/wiki/Wi-Fi_Protected_Access#Target_users_(authentication_key_distribution) // https://github.com/xoseperez/espurna/issues/1151 const Pattern = /^(?=.*[A-Z\d])(?=.*[a-z])[\w~!@#$%^&*()<>,.?;:{}[\]\\|]{8,63}/; return ( (password !== undefined) && (typeof password === "string") && (password.length > 0) && Pattern.test(password)); } // Try to validate 'adminPass{0,1}', searching the first form containing both. // In case it's default webMode, avoid checking things when both fields are empty (`required === false`) function validateFormsPasswords(forms, required) { let [passwords] = Array.from(forms).filter( form => form.elements.adminPass0 && form.elements.adminPass1); if (passwords) { let first = passwords.elements.adminPass0; let second = passwords.elements.adminPass1; if (!required && !first.value.length && !second.value.length) { return true; } let firstValid = first.checkValidity() && validatePassword(first.value); let secondValid = second.checkValidity() && validatePassword(second.value); if (firstValid && secondValid) { if (first.value === second.value) { return true; } alert("Passwords are different!"); return false; } alert("The password you have entered is not valid, it must be 8..63 characters and have at least 1 lowercase and 1 uppercase / number!"); } return false; } // Same as above, but only applies to the general settings page. // Find the first available form that contains 'hostname' input function validateFormsHostname(forms) { // per. [RFC1035](https://datatracker.ietf.org/doc/html/rfc1035) // Hostname may contain: // - the ASCII letters 'a' through 'z' (case-insensitive), // - the digits '0' through '9', and the hyphen. // Hostname labels cannot begin or end with a hyphen. // No other symbols, punctuation characters, or blank spaces are permitted. let [hostname] = Array.from(forms).filter(form => form.elements.hostname); if (!hostname) { return true; } // Validation pattern is attached to the element itself, so just check that. // (and, we also re-use the hostname for fallback SSID, thus limited to 1...32 chars instead of 1...63) hostname = hostname.elements.hostname; let result = hostname.value.length && (!isChangedElement(hostname) || hostname.checkValidity()); if (!result) { alert("Hostname cannot be empty and may only contain the ASCII letters ('A' through 'Z' and 'a' through 'z'), the digits '0' through '9', and the hyphen ('-')! They can neither start or end with an hyphen."); } return result; } function validateForms(forms) { return validateFormsPasswords(forms) && validateFormsHostname(forms); } // Right now, group additions happen from: // - WebSocket, likely to happen exactly once per connection through processData handler(s). Specific keys trigger functions that append into the container element. // - User input. Same functions are triggered, but with an additional event for the container element that causes most recent element to be marked as changed. // Removal only happens from user input. MutationObserver will refresh checkboxes and cause everything to be marked as changed. // // TODO: distinguish 'current' state to avoid sending keys when adding and immediatly removing the latest node? // TODO: previous implementation relied on defaultValue and / or jquery $(...).val(), but this does not really work where 'line' only has