'use strict'; 'require view'; 'require poll'; 'require fs'; 'require dom'; 'require ui'; 'require uci'; 'require form'; 'require network'; 'require tools.widgets as widgets'; /* Cipher helper function */ function resolveCipher(cipherRaw) { const cipherMap = { '2': { name: 'TKIP', aes: false }, '4': { name: 'CCMP', aes: true }, '6': { name: 'BIP', aes: false }, '8': { name: 'GCMP', aes: true }, '9': { name: 'GCMP-256', aes: true }, '10': { name: 'CCMP-256', aes: true }, '11': { name: 'BIP-GMAC-128', aes: false }, '12': { name: 'BIP-GMAC-256', aes: false }, '13': { name: 'BIP-CMAC-256', aes: false } }; if (!cipherRaw) return { name: 'unknown', aes: false }; // lookup by OUI suite id (e.g. "00-0f-ac:4") if (cipherRaw.includes(':')) { const id = cipherRaw.split(':').pop(); if (cipherMap[id]) return cipherMap[id]; return { name: `unknown (${cipherRaw})`, aes: false }; } // fallback: lookup by name (e.g. raw "CCMP" from iwinfo) for (const k in cipherMap) { if (cipherMap[k].name === cipherRaw) return cipherMap[k]; } return { name: cipherRaw, aes: false }; } /* normalize a bssid to upper case for case-insensitive comparison */ function normBssid(bssid) { return (bssid || '').toUpperCase(); } /* identity of all networks already configured as travelmate uplink */ function configuredUplinks(iface) { const sections = uci.sections('wireless', 'wifi-iface'); const list = []; for (let i = 0; i < sections.length; i++) { if (sections[i].network === iface && sections[i].mode === 'sta') { list.push({ device: sections[i].device || '', ssid: sections[i].ssid || '', bssid: normBssid(sections[i].bssid) }); } } return list; } /* check whether a scanned network is already configured, an uplink without bssid matches every bssid of the same ssid */ function isConfigured(list, radio, ssid, bssid) { for (let i = 0; i < list.length; i++) { if (list[i].device !== radio || list[i].ssid !== ssid) { continue; } if (!list[i].bssid || list[i].bssid === normBssid(bssid)) { return true; } } return false; } /* scan dialog styles, aligned with the overview page */ function scanStyle() { return E('style', { 'type': 'text/css' }, [ '#trm-scan {' + '--trm-card-bg: rgba(128,128,128,.07);' + '--trm-card-border: rgba(128,128,128,.28);' + '--trm-muted: GrayText;' + '--trm-ok: #1f8a5f;' + '--trm-err: #c0392b;' + '--trm-warn: #b7791f;' + '}' + '@media (prefers-color-scheme: dark) {' + '#trm-scan {' + '--trm-ok: #63c79b;' + '--trm-err: #e8897e;' + '--trm-warn: #e0b35c;' + '}}' + '#trm-scan .trm-list { display: grid; gap: .5em; margin-bottom: .75em; }' + '#trm-scan .trm-card { display: grid; gap: .4em; background: var(--trm-card-bg); border: 1px solid var(--trm-card-border); border-radius: 8px; padding: .6em .8em; min-width: 0; }' + '#trm-scan .trm-head { display: flex; align-items: baseline; gap: .6em; }' + '#trm-scan .trm-ssid { flex: 1 1 auto; min-width: 0; font-size: 1.1em; font-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }' + '#trm-scan .trm-qual { flex: 0 0 auto; font-variant-numeric: tabular-nums; }' + '#trm-scan .trm-bar { height: .35em; border-radius: 3px; background: var(--trm-card-border); overflow: hidden; }' + '#trm-scan .trm-bar-fill { height: 100%; background: var(--trm-muted); }' + '#trm-scan .trm-card[data-signal="good"] .trm-qual { color: var(--trm-ok); }' + '#trm-scan .trm-card[data-signal="good"] .trm-bar-fill { background: var(--trm-ok); }' + '#trm-scan .trm-card[data-signal="weak"] .trm-qual { color: var(--trm-warn); }' + '#trm-scan .trm-card[data-signal="weak"] .trm-bar-fill { background: var(--trm-warn); }' + '#trm-scan .trm-card[data-signal="low"] .trm-qual { color: var(--trm-err); }' + '#trm-scan .trm-card[data-signal="low"] .trm-bar-fill { background: var(--trm-err); }' + '#trm-scan .trm-foot { display: flex; flex-wrap: wrap; align-items: center; gap: .35em .6em; font-size: .85em; color: var(--trm-muted); }' + '#trm-scan .trm-mono { font-family: monospace; }' + '#trm-scan .trm-chip { padding: .1em .5em; border-radius: 6px; border: 1px solid var(--trm-card-border); }' + '#trm-scan .trm-chip-open { color: var(--trm-warn); border-color: var(--trm-warn); }' + '#trm-scan .trm-act { margin-left: auto; }' + '#trm-scan .trm-known { color: var(--trm-ok); }' + '#trm-scan .trm-empty { color: var(--trm-muted); padding: .5em 0; }' ]); } /* render a single scan result card */ function scanCard(quality, minQuality, ssidNode, channel, bssid, encLabel, encOpen, action) { const value = isNaN(quality) ? 0 : Math.max(0, Math.min(100, quality)); let signal = 'low'; if (value >= Math.min(100, minQuality + 25)) { signal = 'good'; } else if (value >= minQuality) { signal = 'weak'; } return E('div', { 'class': 'trm-card', 'data-signal': signal }, [ E('div', { 'class': 'trm-head' }, [ E('div', { 'class': 'trm-ssid' }, [ssidNode]), E('span', { 'class': 'trm-qual' }, [value + ' %']) ]), E('div', { 'class': 'trm-bar' }, [ E('div', { 'class': 'trm-bar-fill', 'style': 'width:' + value + '%' }) ]), E('div', { 'class': 'trm-foot' }, [ E('span', { 'class': 'trm-chip' + (encOpen ? ' trm-chip-open' : '') }, [encLabel]), E('span', {}, [_('Channel') + ' ' + channel]), E('span', { 'class': 'trm-mono' }, [bssid]), E('div', { 'class': 'trm-act' }, [action]) ]) ]); } /* change the status of travelmate stations */ function handleToggle(sid) { let w_device, w_ssid, w_bssid, t_sections, row, element, value, enabled; w_device = uci.get('wireless', sid, 'device'); w_ssid = uci.get('wireless', sid, 'ssid'); w_bssid = uci.get('wireless', sid, 'bssid'); t_sections = uci.sections('travelmate', 'uplink'); for (let i = 0; i < t_sections.length; i++) { if (t_sections[i].device === w_device && t_sections[i].ssid === w_ssid && normBssid(t_sections[i].bssid) === normBssid(w_bssid)) { value = t_sections[i]['enabled']; value = (value == 0 ? 1 : 0); enabled = (value == 0 ? _('No') : _('Yes')); uci.set('travelmate', t_sections[i]['.name'], 'enabled', value); return uci.save().then(function () { row = document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(sid)); if (row) { element = row.querySelector('.cbi-value-field'); if (element) { element.textContent = enabled; } row.setAttribute('style', 'opacity: 0.5; color: #37c !important;'); } }); } } } /* remove wireless and stale travelmate sections */ function handleRemove(sid) { let w_sections, t_sections, match, row, open, count; uci.remove('wireless', sid); w_sections = uci.sections('wireless', 'wifi-iface'); t_sections = uci.sections('travelmate', 'uplink'); for (let i = 0; i < t_sections.length; i++) { match = false; for (let j = 0; j < w_sections.length; j++) { if (t_sections[i].device === w_sections[j].device && t_sections[i].ssid === w_sections[j].ssid && normBssid(t_sections[i].bssid) === normBssid(w_sections[j].bssid)) { match = true; break; } } if (match === false) { open = +t_sections[i].opensta || 0; if (open === 1) { count = +uci.get('travelmate', 'global', 'trm_autoaddcnt') || 0; if (count > 0) { count--; uci.set('travelmate', 'global', 'trm_autoaddcnt', count); } } uci.remove('travelmate', t_sections[i]['.name']); } } return uci.save().then(function () { row = document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(sid)); if (row) { row.setAttribute('style', 'opacity: 0.5; color: #a22 !important;'); } }); } /* add missing travelmate sections */ function handleSectionsAdd(iface) { let w_sections, t_sections, match; w_sections = uci.sections('wireless', 'wifi-iface'); t_sections = uci.sections('travelmate', 'uplink'); for (let i = 0; i < w_sections.length; i++) { if (w_sections[i].mode !== 'sta' || w_sections[i].network !== iface) { continue; } match = false; for (let j = 0; j < t_sections.length; j++) { if (w_sections[i].device === t_sections[j].device && w_sections[i].ssid === t_sections[j].ssid && normBssid(w_sections[i].bssid) === normBssid(t_sections[j].bssid)) { match = true; break; } } if (match === false) { let vpn_stdservice = uci.get('travelmate', 'global', 'trm_stdvpnservice'); let vpn_stdiface = uci.get('travelmate', 'global', 'trm_stdvpniface'); let sid = uci.add('travelmate', 'uplink'); uci.set('travelmate', sid, 'enabled', '1'); uci.set('travelmate', sid, 'device', w_sections[i].device); uci.set('travelmate', sid, 'ssid', w_sections[i].ssid); uci.set('travelmate', sid, 'bssid', w_sections[i].bssid); if (vpn_stdservice && vpn_stdiface) { uci.set('travelmate', sid, 'vpn', '1'); uci.set('travelmate', sid, 'vpnservice', vpn_stdservice); uci.set('travelmate', sid, 'vpniface', vpn_stdiface); } } } } /* update travelmate sections */ function handleSectionsVal(action, section_id, option, value) { let w_device, w_ssid, w_bssid, t_sections; w_device = uci.get('wireless', section_id, 'device'); w_ssid = uci.get('wireless', section_id, 'ssid'); w_bssid = uci.get('wireless', section_id, 'bssid'); t_sections = uci.sections('travelmate', 'uplink'); for (let i = 0; i < t_sections.length; i++) { if (t_sections[i].device === w_device && t_sections[i].ssid === w_ssid && normBssid(t_sections[i].bssid) === normBssid(w_bssid)) { if (action === 'get') { return t_sections[i][option]; } else if (action === 'set') { return uci.set('travelmate', t_sections[i]['.name'], option, value); } else if (action === 'del') { return uci.unset('travelmate', t_sections[i]['.name'], option); } } } } /* update travelmate status */ function handleStatus() { let parseErrCount = 0; poll.add(function () { return Promise.all([ L.resolveDefault(fs.stat('/var/run/travelmate/travelmate.refresh'), null).then(function (stat) { if (!stat) return; return L.resolveDefault(fs.read_direct('/var/run/travelmate/travelmate.refresh'), null).then(async function (content) { await fs.remove('/var/run/travelmate/travelmate.refresh'); if (content === 'ui_reload') { location.reload(); } else if (content === 'cfg_reload') { if (document.readyState === 'complete') { uci.unload('wireless'); uci.unload('travelmate'); } await Promise.all([ uci.load('wireless'), uci.load('travelmate') ]); const rows = document.querySelectorAll('.cbi-section-table-row[data-sid]'); for (let i = 0; i < rows.length; i++) { const item = rows[i].querySelector('.cbi-value-field[data-title="Enabled"]'); const value = handleSectionsVal('get', rows[i].getAttribute('data-sid'), 'enabled'); if (item) { item.textContent = (value === '1' ? _('Yes') : _('No')); } } } }); }), L.resolveDefault(fs.stat('/var/run/travelmate/travelmate.runtime.json'), null).then(function (stat) { if (!stat) return; return L.resolveDefault(fs.read_direct('/var/run/travelmate/travelmate.runtime.json'), null).then(function (content) { if (!content) return; let info = null; try { info = JSON.parse(content); parseErrCount = 0; } catch (e) { parseErrCount++; if (parseErrCount >= 5) { ui.addNotification(null, E('p', _('Unable to parse the travelmate runtime information!')), 'error'); poll.stop(); } return; } if (!info || !info.data) return; const vpnMatch = (info.data.run_flags || '').match(/vpn:\s*(.)/); const uplinkId = (info.data.station_id || '').trim().split('/'); const oldUplinkView = document.getElementsByName('uplinkStation'); const w_sections = uci.sections('wireless', 'wifi-iface'); const vpnStatus = vpnMatch ? vpnMatch[1] : '✘'; let t_device, t_ssid, t_bssid, newUplinkView, uplinkColor; t_device = uplinkId[0]; t_bssid = uplinkId[uplinkId.length - 1]; for (let i = 1; i < uplinkId.length - 1; i++) { if (!t_ssid) { t_ssid = uplinkId[i]; } else { t_ssid = t_ssid + '/' + uplinkId[i]; } } if (t_ssid === '-') { if (oldUplinkView.length > 0) { oldUplinkView[0].removeAttribute('style'); oldUplinkView[0].removeAttribute('name'); } } else { uplinkColor = (vpnStatus === '✔' ? 'rgb(68, 170, 68)' : 'rgb(51, 119, 204)'); for (let i = 0; i < w_sections.length; i++) { newUplinkView = document.getElementById('cbi-wireless-' + w_sections[i]['.name']); if (t_device === w_sections[i].device && t_ssid === w_sections[i].ssid && normBssid(t_bssid) === normBssid(w_sections[i].bssid || '-')) { if (oldUplinkView.length === 0 && newUplinkView) { newUplinkView.setAttribute('name', 'uplinkStation'); newUplinkView.setAttribute('style', 'text-align: left !important; color: ' + uplinkColor + ' !important;font-weight: bold !important;'); } else if (oldUplinkView.length > 0 && newUplinkView && oldUplinkView[0].getAttribute('id') !== newUplinkView.getAttribute('id')) { oldUplinkView[0].removeAttribute('style'); oldUplinkView[0].removeAttribute('name'); newUplinkView.setAttribute('name', 'uplinkStation'); newUplinkView.setAttribute('style', 'text-align: left !important; color: ' + uplinkColor + ' !important;font-weight: bold !important;'); } else if (newUplinkView && newUplinkView.style.color !== uplinkColor) { newUplinkView.setAttribute('style', 'text-align: left !important; color: ' + uplinkColor + ' !important;font-weight: bold !important;'); } } } } }); }) ]); }, 2); } return view.extend({ load: function () { return Promise.all([ uci.load('wireless').catch(() => 0), uci.load('travelmate').catch(() => 0) ]); }, render: function () { /* basic config check */ if (!uci.sections('wireless', 'wifi-device').length) { ui.addNotification(null, E('p', _('No wireless config / radio found!')), 'error'); return; } else if (!uci.get('travelmate', 'global')) { ui.addNotification(null, E('p', _('No travelmate config found!')), 'error'); return; } /* main map */ let m, s, o, count; let iface = uci.get('travelmate', 'global', 'trm_iface') || 'trm_wwan'; m = new form.Map('wireless'); m.chain('travelmate'); s = m.section(form.GridSection, 'wifi-iface', null, _('Overview of all configured uplinks for travelmate. \ You can edit, remove or prioritize existing uplinks by drag & drop and scan for new ones.
\ The currently used uplink connection is emphasized in blue, \ an encrypted VPN uplink connection is emphasized in green.')); s.filter = function (section_id) { return (uci.get('wireless', section_id, 'network') == iface && uci.get('wireless', section_id, 'mode') == 'sta'); }; s.anonymous = true; s.sortable = true; s.tab('wireless', _('Wireless Settings')); s.tab('travelmate', _('Travelmate Settings')); s.tab('vpn', _('VPN Settings')); s.renderRowActions = function (section_id) { const btns = [ E('button', { 'class': 'cbi-button drag-handle center', 'style': 'float:none;margin-right:.4em;cursor:move;', 'draggable': true, 'dragstart': L.bind(function (ev) { this.handleDragStart(ev, this.handleDrag); }, this), 'dragend': L.bind(function (ev) { this.handleDragEnd(ev, this.handleDrag); }, this), 'touchmove': L.bind(function (ev) { this.handleTouchMove(ev); }, this), 'touchend': L.bind(function (ev) { this.handleTouchEnd(ev); }, this), 'title': _('Drag to reorder'), 'disabled': this.map.readonly || null }, '☰'), E('button', { 'class': 'cbi-button cbi-button-action important', 'style': 'float:none;margin-right:.4em;', 'title': _('Edit this network'), 'click': ui.createHandlerFn(this, 'renderMoreOptionsModal', section_id) }, _('Edit')), E('button', { 'class': 'cbi-button cbi-button-apply', 'style': 'float:none;margin-right:.4em;', 'title': _('Enable/Disable this network'), 'click': ui.createHandlerFn(this, handleToggle, section_id) }, _('On/Off')), E('button', { 'class': 'cbi-button cbi-button-negative remove', 'title': _('Remove this network'), 'click': ui.createHandlerFn(this, handleRemove, section_id) }, _('Remove')) ]; return E('td', { 'class': 'td cbi-section-table-cell nowrap cbi-section-actions' }, E('div', btns)); }; o = s.taboption('travelmate', form.Flag, '_enabled', _('Enabled')); o.uciconfig = 'travelmate'; o.ucisection = 'uplink'; o.ucioption = 'enabled'; o.rmempty = false; o.cfgvalue = function (section_id) { return handleSectionsVal('get', section_id, 'enabled'); } o.write = function (section_id, value) { return handleSectionsVal('set', section_id, 'enabled', value); } o = s.taboption('wireless', form.Value, 'device', _('Device')); o.readonly = true; o = s.taboption('wireless', form.Value, 'ssid', _('SSID')); o.datatype = 'maxlength(32)'; o.readonly = true; o = s.taboption('wireless', form.Value, 'bssid', _('BSSID')); o.datatype = 'macaddr'; o.readonly = true; o.textvalue = function (section_id) { const bssid = uci.get('wireless', section_id, 'bssid'); return bssid ? normBssid(bssid) : null; }; o = s.taboption('wireless', form.ListValue, 'encryption', _('Encryption')); o.value('sae', _('WPA3 PSK (SAE)')); o.value('sae-mixed', _('Mixed WPA2/WPA3 PSK (CCMP)')); o.value('psk2+ccmp', _('WPA2 PSK (CCMP)')); o.value('psk2+tkip', _('WPA2 PSK (TKIP)')); o.value('psk+ccmp', _('WPA PSK (CCMP)')); o.value('psk+tkip', _('WPA PSK (TKIP)')); o.value('psk-mixed+ccmp', _('Mixed WPA/WPA2 PSK (CCMP)')); o.value('psk-mixed+tkip', _('Mixed WPA/WPA2 PSK (TKIP)')); o.value('wpa3', _('WPA3 802.1X')); o.value('wpa3-mixed', _('Mixed WPA2/WPA3 802.1X')); o.value('wpa2+ccmp', _('WPA2 802.1X (CCMP)')); o.value('wpa2+tkip', _('WPA2 802.1X (TKIP)')); o.value('wpa+ccmp', _('WPA 802.1X (CCMP)')); o.value('wpa+tkip', _('WPA 802.1X (TKIP)')); o.value('wpa-mixed+ccmp', _('Mixed WPA/WPA2 802.1X (CCMP)')); o.value('wpa-mixed+tkip', _('Mixed WPA/WPA2 802.1X (TKIP)')); o.value('owe', _('WPA3 OWE')); o.value('none', _('Open')); o.default = 'none'; o.textvalue = function (section_id) { let cfgvalue = this.map.data.get('wireless', section_id, 'encryption'); switch (cfgvalue) { case 'sae': cfgvalue = _('WPA3 PSK (SAE)'); break; case 'sae-mixed': cfgvalue = _('Mixed WPA2/WPA3 PSK (CCMP)'); break; case 'psk2+ccmp': cfgvalue = _('WPA2 PSK (CCMP)'); break; case 'psk2+tkip': cfgvalue = _('WPA2 PSK (TKIP)'); break; case 'psk+ccmp': cfgvalue = _('WPA PSK (CCMP)'); break; case 'psk+tkip': cfgvalue = _('WPA PSK (TKIP)'); break; case 'psk-mixed+ccmp': cfgvalue = _('Mixed WPA/WPA2 PSK (CCMP)'); break; case 'psk-mixed+tkip': cfgvalue = _('Mixed WPA/WPA2 PSK (TKIP)'); break; case 'wpa3': cfgvalue = _('WPA3 802.1X'); break; case 'wpa3-mixed': cfgvalue = _('Mixed WPA2/WPA3 802.1X'); break; case 'wpa2+ccmp': cfgvalue = _('WPA2 802.1X (CCMP)'); break; case 'wpa2+tkip': cfgvalue = _('WPA2 802.1X (TKIP)'); break; case 'wpa+ccmp': cfgvalue = _('WPA 802.1X (CCMP)'); break; case 'wpa+tkip': cfgvalue = _('WPA 802.1X (TKIP)'); break; case 'wpa-mixed+ccmp': cfgvalue = _('Mixed WPA/WPA2 802.1X (CCMP)'); break; case 'wpa-mixed+tkip': cfgvalue = _('Mixed WPA/WPA2 802.1X (TKIP)'); break; case 'owe': cfgvalue = _('WPA3 OWE'); break; case 'none': cfgvalue = _('Open'); break; } return cfgvalue; }; handleStatus(); /* modal wireless tab */ o = s.taboption('wireless', form.Value, 'key', _('Password')); o.datatype = 'wpakey'; o.depends({ encryption: 'sae', '!contains': true }); o.depends({ encryption: 'psk', '!contains': true }); o.modalonly = true; o.password = true; o = s.taboption('wireless', form.Value, 'password', _('Password')); o.datatype = 'wpakey'; o.depends({ encryption: 'wpa', '!contains': true }); o.modalonly = true; o.password = true; o = s.taboption('wireless', form.ListValue, 'eap_type', _('EAP-Method')); o.depends({ encryption: 'wpa', '!contains': true }); o.value('tls', _('TLS')); o.value('ttls', _('TTLS')); o.value('peap', _('PEAP')); o.value('fast', _('FAST')); o.default = 'peap'; o.modalonly = true; o = s.taboption('wireless', form.ListValue, 'auth', _('Authentication')); o.value('PAP', _('PAP')); o.value('CHAP', _('CHAP')); o.value('MSCHAP', _('MSCHAP')); o.value('MSCHAPV2', _('MSCHAPV2')); o.value('EAP-GTC', _('EAP-GTC')); o.value('EAP-MD5', _('EAP-MD5')); o.value('EAP-MSCHAPV2', _('EAP-MSCHAPV2')); o.value('EAP-TLS', _('EAP-TLS')); o.value('auth=PAP', _('auth=PAP')); o.value('auth=MSCHAPV2', _('auth=MSCHAPV2')); o.default = 'EAP-MSCHAPV2'; o.depends({ encryption: 'wpa', '!contains': true }); o.modalonly = true; o = s.taboption('wireless', form.Value, 'identity', _('Identity')); o.depends({ encryption: 'wpa', '!contains': true }); o.modalonly = true; o = s.taboption('wireless', form.Value, 'anonymous_identity', _('Anonymous Identity')); o.depends({ encryption: 'wpa', '!contains': true }); o.modalonly = true; o = s.taboption('wireless', form.ListValue, 'ieee80211w', _('Mgmt. Frame Protection')); o.depends({ encryption: 'sae', '!contains': true }); o.depends({ encryption: 'owe', '!contains': true }); o.depends({ encryption: 'wpa', '!contains': true }); o.depends({ encryption: 'psk', '!contains': true }); o.value('', _('Disabled')); o.value('1', _('Optional')); o.value('2', _('Required')); o.modalonly = true; o.defaults = { '2': [{ encryption: 'sae' }, { encryption: 'owe' }, { encryption: 'wpa3' }, { encryption: 'wpa3-mixed' }], '1': [{ encryption: 'sae-mixed' }], '': [] }; o = s.taboption('wireless', form.Flag, 'ca_cert_usesystem', _('Use system certificates'), _("Validate server certificate using built-in system CA bundle")); o.depends({ encryption: 'wpa', '!contains': true }); o.enabled = '1'; o.disabled = '0'; o.modalonly = true; o.default = o.disabled; o = s.taboption('wireless', form.Value, 'ca_cert', _('Path to CA-Certificate')); o.depends({ encryption: 'wpa', '!contains': true }); o.depends({ ca_cert_usesystem: '0' }); o.modalonly = true; o.rmempty = true; o = s.taboption('wireless', form.Value, 'client_cert', _('Path to Client-Certificate')); o.depends({ eap_type: 'tls' }); o.modalonly = true; o.rmempty = true; o = s.taboption('wireless', form.Value, 'priv_key', _('Path to Private Key')); o.depends({ eap_type: 'tls' }); o.modalonly = true; o.rmempty = true; o = s.taboption('wireless', form.Value, 'priv_key_pwd', _('Password of Private Key')); o.depends({ eap_type: 'tls' }); o.modalonly = true; o.password = true; o.rmempty = true; /* modal travelmate tab */ o = s.taboption('travelmate', form.Value, '_ssid', _('SSID')); o.modalonly = true; o.uciconfig = 'travelmate'; o.ucisection = 'uplink'; o.ucioption = 'ssid'; o.rmempty = false; o.readonly = true; o.cfgvalue = function (section_id) { return handleSectionsVal('get', section_id, 'ssid'); } o = s.taboption('travelmate', form.Value, '_bssid', _('BSSID')); o.modalonly = true; o.uciconfig = 'travelmate'; o.ucisection = 'uplink'; o.ucioption = 'bssid'; o.rmempty = true; o.readonly = true; o.cfgvalue = function (section_id) { return handleSectionsVal('get', section_id, 'bssid'); } o = s.taboption('travelmate', form.Flag, '_opensta', _('Auto Added Open Uplink'), _('This option is selected by default if this uplink was added automatically and counts as \'Open Uplink\'.')); o.rmempty = true; o.modalonly = true; o.uciconfig = 'travelmate'; o.ucisection = 'uplink'; o.ucioption = 'opensta'; o.cfgvalue = function (section_id) { return handleSectionsVal('get', section_id, 'opensta'); } o.write = function (section_id, value) { count = +uci.get('travelmate', 'global', 'trm_autoaddcnt') || 0; count++; uci.set('travelmate', 'global', 'trm_autoaddcnt', count); return handleSectionsVal('set', section_id, 'opensta', value); } o.remove = function (section_id, value) { count = +uci.get('travelmate', 'global', 'trm_autoaddcnt') || 0; if (count > 0) { count--; uci.set('travelmate', 'global', 'trm_autoaddcnt', count); } return handleSectionsVal('set', section_id, 'opensta', value); } o = s.taboption('travelmate', form.Value, '_macaddr', _('MAC Address'), _('Use the specified MAC address for this uplink.')); o.modalonly = true; o.uciconfig = 'travelmate'; o.ucisection = 'uplink'; o.ucioption = 'macaddr'; o.nocreate = false; o.rmempty = true; o.datatype = 'macaddr'; o.cfgvalue = function (section_id) { return handleSectionsVal('get', section_id, 'macaddr'); } o.write = function (section_id, value) { return handleSectionsVal('set', section_id, 'macaddr', value); } o.remove = function (section_id, value) { return handleSectionsVal('set', section_id, 'macaddr', value); } o = s.taboption('travelmate', form.ListValue, '_revive', _('Revive Uplink'), _('Re-enable this uplink after the selected number of run cycles if it has been disabled by the retry limit. The number of revivals is limited by the global retry limit.')); o.modalonly = true; o.uciconfig = 'travelmate'; o.ucisection = 'uplink'; o.ucioption = 'revive'; o.nocreate = false; o.rmempty = true; o.default = '0'; o.value('0', _('disabled')); o.value('10', _('10 run cycles')); o.value('30', _('30 run cycles')); o.value('60', _('60 run cycles')); o.cfgvalue = function (section_id) { return handleSectionsVal('get', section_id, 'revive'); } o.write = function (section_id, value) { return handleSectionsVal('set', section_id, 'revive', value); } o.remove = function (section_id, value) { return handleSectionsVal('set', section_id, 'revive', value); } o = s.taboption('travelmate', form.FileUpload, '_script', _('Auto Login Script'), _('External script reference which will be called for automated captive portal logins.')); o.root_directory = '/etc/travelmate'; o.enable_remove = false; o.enable_upload = false; o.modalonly = true; o.uciconfig = 'travelmate'; o.ucisection = 'uplink'; o.ucioption = 'script'; o.renderWidget = function (section_id, option_index, cfgvalue) { let browserEl = new ui.FileUpload((cfgvalue != null) ? cfgvalue : this.default, { id: this.cbid(section_id), name: this.cbid(section_id), show_hidden: this.show_hidden, enable_upload: this.enable_upload, enable_remove: this.enable_remove, root_directory: this.root_directory, disabled: (this.readonly != null) ? this.readonly : this.map.readonly }); browserEl.renderListing = function (container, path, list) { return ui.FileUpload.prototype.renderListing.apply(this, [ container, path, list.filter(function (entry) { return ((entry.type == 'directory') || (entry.type == 'file' && entry.name.match(/\.login$/))); }) ]); }; return browserEl.render(); }; o.cfgvalue = function (section_id) { return handleSectionsVal('get', section_id, 'script'); } o.write = function (section_id, value) { return handleSectionsVal('set', section_id, 'script', value); } o.remove = function (section_id) { return handleSectionsVal('del', section_id, 'script'); } o = s.taboption('travelmate', form.Value, '_args', _('Script Arguments'), _('Space separated list of additional arguments passed to the Auto Login Script, i.e. username and password')); o.modalonly = true; o.uciconfig = 'travelmate'; o.ucisection = 'uplink'; o.ucioption = 'script_args'; o.rmempty = true; o.depends({ _script: '/etc/travelmate', '!contains': true }); o.cfgvalue = function (section_id) { return handleSectionsVal('get', section_id, 'script_args'); } o.write = function (section_id, value) { return handleSectionsVal('set', section_id, 'script_args', value); } o.remove = function (section_id) { return handleSectionsVal('del', section_id, 'script_args'); } /* modal vpn tab */ o = s.taboption('vpn', form.Flag, '_vpn', _('VPN Hook'), _('Automatically handle VPN connections.
\ Please note: This feature requires the additional configuration of Wireguard or OpenVPN.')); o.rmempty = true; o.modalonly = true; o.uciconfig = 'travelmate'; o.ucisection = 'uplink'; o.ucioption = 'vpn'; o.cfgvalue = function (section_id) { return handleSectionsVal('get', section_id, 'vpn'); } o.write = function (section_id, value) { return handleSectionsVal('set', section_id, 'vpn', value); } o.remove = function (section_id, value) { return handleSectionsVal('set', section_id, 'vpn', value); } o = s.taboption('vpn', form.ListValue, '_vpnservice', _('VPN Service')); o.value('wireguard'); o.value('openvpn'); o.optional = true; o.modalonly = true; o.uciconfig = 'travelmate'; o.ucisection = 'uplink'; o.ucioption = 'vpnservice'; o.cfgvalue = function (section_id) { return handleSectionsVal('get', section_id, 'vpnservice'); } o.write = function (section_id, value) { return handleSectionsVal('set', section_id, 'vpnservice', value); } o = s.taboption('vpn', widgets.NetworkSelect, '_vpniface', _('VPN Interface'), _('The logical vpn network interface like \'wg0\'.')); o.nocreate = true; o.optional = true; o.modalonly = true; o.uciconfig = 'travelmate'; o.ucisection = 'uplink'; o.ucioption = 'vpniface'; o.cfgvalue = function (section_id) { return handleSectionsVal('get', section_id, 'vpniface'); } o.write = function (section_id, value) { return handleSectionsVal('set', section_id, 'vpniface', value); } /* scan buttons */ s = m.section(form.GridSection, 'wifi-device'); s.anonymous = true; s.addremove = false; s.render = function () { return network.getWifiDevices().then(L.bind(function (radios) { let radio, ifname, btns = []; for (let i = 0; i < radios.length; i++) { radio = radios[i].sid; if (radio) { btns.push(E('button', { 'class': 'cbi-button cbi-button-apply', 'style': 'float:none;margin-right:.4em;', 'id': radio, 'click': ui.createHandlerFn(this, 'handleScan', radio) }, [_('Scan on %s...').format(radio)])) } } return E('div', { 'class': 'left', 'style': 'display:flex; flex-direction:column' }, E('div', { 'class': 'left', 'style': 'padding-top:5px; padding-bottom:5px' }, btns)); }, this)) }; /* modal 'scan' dialog */ s.handleScan = function (radio) { poll.stop(); const minQuality = parseInt(uci.get('travelmate', 'global', 'trm_minquality') || '35', 10); const uplinks = configuredUplinks(iface); let list = E('div', { 'class': 'trm-list' }, [ E('em', { 'class': 'spinning' }, _('Starting wireless scan on \'%s\'...').format(radio)) ]); let md = ui.showModal(_('Wireless Scan'), [ E('div', { 'id': 'trm-scan' }, [scanStyle(), list]), E('div', { 'class': 'right' }, [ E('button', { 'class': 'btn', 'style': 'float:none;margin-right:.4em;', 'click': ui.hideModal }, _('Dismiss')), E('button', { 'class': 'cbi-button cbi-button-positive important', 'id': 'scan-btn', 'disabled': 'disabled', 'click': L.bind(this.handleScan, this, radio) }, _('Repeat Scan')) ]) ]); md.style.maxWidth = '90%'; md.style.maxHeight = 'none'; return L.resolveDefault(fs.exec_direct('/etc/init.d/travelmate', ['scan', radio])) .then(L.bind(function () { return L.resolveDefault(fs.read_direct('/var/run/travelmate/travelmate.scan'), '') .then(L.bind(function (res) { let lines, strength, channel, bssid, wpa, cipher, auth, tbl_ssid, ssid, rows = []; if (res) { lines = res.split('\n'); for (let i = 0; i < lines.length; i++) { if (lines[i].match(/^\s*\d+/)) { /* result columns */ strength = lines[i].slice(0, 3).trim(); channel = lines[i].slice(3, 7).trim(); bssid = lines[i].slice(7, 25).trim(); wpa = lines[i].slice(25, 37).trim(); cipher = lines[i].slice(37, 48).trim(); auth = lines[i].slice(48, 59).trim().split(','); ssid = lines[i].slice(59).trim(); /* SSID preparation */ if (ssid === 'hidden') { tbl_ssid = E('em', {}, [_('hidden')]); } else { ssid = ssid.replace(/^"(.*)"$/, '$1'); tbl_ssid = ssid; } /* WPA detection */ let hasWPA1 = wpa.includes("WPA1"); let hasWPA2 = wpa.includes("WPA2"); let hasWPA3 = wpa.includes("WPA3"); /* Auth detection */ let hasPSK = auth.some(a => a.includes("PSK")); let hasSAE = auth.some(a => a.includes("SAE")); let has8021x = auth.some(a => a.includes("802.1X")); let hasOWE = auth.includes("OWE"); let hasSuiteB = auth.some(a => a.includes("SUITE-B")); let resCipher = resolveCipher(cipher); /* encryption classification */ let tbl_encryption = ''; let encryption = 'none'; if (cipher === '-' && wpa === '-') { tbl_encryption = _('Open'); encryption = 'none'; } else if (hasOWE) { tbl_encryption = `WPA3 OWE (${resCipher.name})`; encryption = 'owe'; } else if (hasSuiteB) { tbl_encryption = `WPA3 Enterprise (${resCipher.name})`; encryption = 'wpa3'; } else if (hasWPA2 && hasWPA3 && hasPSK && !has8021x) { tbl_encryption = `Mixed WPA2/WPA3 PSK (${resCipher.name})`; encryption = 'sae-mixed'; } else if (hasWPA2 && hasWPA3 && has8021x) { tbl_encryption = `Mixed WPA2/WPA3 802.1X (${resCipher.name})`; encryption = 'wpa3-mixed'; } else if (hasWPA3 && hasSAE && !has8021x) { tbl_encryption = `WPA3 PSK (SAE)`; encryption = 'sae'; } else if (hasWPA3 && has8021x) { tbl_encryption = `WPA3 802.1X (${resCipher.name})`; encryption = 'wpa3'; } else if (hasWPA1 && hasWPA2 && has8021x) { tbl_encryption = `Mixed WPA/WPA2 802.1X (${resCipher.name})`; encryption = resCipher.aes ? 'wpa-mixed+ccmp' : 'wpa-mixed+tkip'; } else if (hasWPA2 && has8021x) { tbl_encryption = `WPA2 802.1X (${resCipher.name})`; encryption = resCipher.aes ? 'wpa2+ccmp' : 'wpa2+tkip'; } else if (hasWPA1 && has8021x) { tbl_encryption = `WPA 802.1X (${resCipher.name})`; encryption = resCipher.aes ? 'wpa+ccmp' : 'wpa+tkip'; } else if (hasWPA1 && hasWPA2 && hasPSK) { tbl_encryption = `Mixed WPA/WPA2 PSK (${resCipher.name})`; encryption = resCipher.aes ? 'psk-mixed+ccmp' : 'psk-mixed+tkip'; } else if (hasWPA2 && hasPSK) { tbl_encryption = `WPA2 PSK (${resCipher.name})`; encryption = resCipher.aes ? 'psk2+ccmp' : 'psk2+tkip'; } else if (hasWPA1 && hasPSK) { tbl_encryption = `WPA PSK (${resCipher.name})`; encryption = resCipher.aes ? 'psk+ccmp' : 'psk+tkip'; } else { tbl_encryption = _('unknown'); encryption = 'none'; } /* push result card into the list */ const known = isConfigured(uplinks, radio, ssid, bssid); const action = known ? E('span', { 'class': 'trm-known' }, [_('Configured')]) : E('button', { 'class': 'cbi-button cbi-button-action', 'click': ui.createHandlerFn(this, 'handleAdd', radio, iface, ssid, bssid, encryption) }, _('Add Uplink...')); rows.push(scanCard(parseInt(strength, 10), minQuality, tbl_ssid, channel, bssid, tbl_encryption, encryption === 'none', action)); } } } else { rows.push(E('div', { 'class': 'trm-empty' }, [_('Empty resultset')])); } dom.content(list, rows); document.getElementById('scan-btn').disabled = false; poll.start(); }, this)); }, this)); }; /* modal 'add' dialog */ s.handleAdd = function (radio, iface, ssid, bssid, encryption, ev) { var m2, s2, o2; m2 = new form.Map('wireless'), s2 = m2.section(form.NamedSection, '_add_trm'); s2.render = function () { return Promise.all([ {}, this.renderUCISection('_add_trm') ]).then(this.renderContents.bind(this)); }; o2 = s2.option(form.Value, 'device', _('Device Name')); o2.default = radio; o2.readonly = true; o2 = s2.option(form.Value, 'network', _('Interface Name')); o2.default = iface; o2.readonly = true; if (ssid === "hidden") { o2 = s2.option(form.Value, 'ssid', _('SSID (hidden)')); o2.placeholder = 'hidden SSID'; } else { o2 = s2.option(form.Value, 'ssid', _('SSID')); o2.default = ssid; } o2.datatype = 'maxlength(32)'; o2.rmempty = false; o2 = s2.option(form.Flag, 'ignore_bssid', _('Ignore BSSID')); if (ssid === 'hidden') { o2.default = '0'; } else { o2.default = '1'; } o2 = s2.option(form.Value, 'bssid', _('BSSID')); o2.depends({ ignore_bssid: '0' }); o2.datatype = 'macaddr'; o2.rmempty = true; o2.default = bssid; o2 = s2.option(form.ListValue, 'encryption', _('Encryption')); o2.value('sae', _('WPA3 PSK (SAE)')); o2.value('sae-mixed', _('Mixed WPA2/WPA3 PSK (CCMP)')); o2.value('psk2+ccmp', _('WPA2 PSK (CCMP)')); o2.value('psk2+tkip', _('WPA2 PSK (TKIP)')); o2.value('psk+ccmp', _('WPA PSK (CCMP)')); o2.value('psk+tkip', _('WPA PSK (TKIP)')); o2.value('psk-mixed+ccmp', _('Mixed WPA/WPA2 PSK (CCMP)')); o2.value('psk-mixed+tkip', _('Mixed WPA/WPA2 PSK (TKIP)')); o2.value('wpa3', _('WPA3 802.1X')); o2.value('wpa3-mixed', _('Mixed WPA2/WPA3 802.1X')); o2.value('wpa2+ccmp', _('WPA2 802.1X (CCMP)')); o2.value('wpa2+tkip', _('WPA2 802.1X (TKIP)')); o2.value('wpa+ccmp', _('WPA 802.1X (CCMP)')); o2.value('wpa+tkip', _('WPA 802.1X (TKIP)')); o2.value('wpa-mixed+ccmp', _('Mixed WPA/WPA2 802.1X (CCMP)')); o2.value('wpa-mixed+tkip', _('Mixed WPA/WPA2 802.1X (TKIP)')); o2.value('owe', _('WPA3 OWE')); o2.value('none', _('Open')); o2.default = encryption; o2 = s2.option(form.Value, 'key', _('Password')); o2.depends({ encryption: 'sae', '!contains': true }); o2.depends({ encryption: 'psk', '!contains': true }); o2.datatype = 'wpakey'; o2.password = true; o2 = s2.option(form.Value, 'password', _('Password')); o2.depends({ encryption: 'wpa', '!contains': true }); o2.datatype = 'wpakey'; o2.password = true; o2 = s2.option(form.ListValue, 'eap_type', _('EAP-Method')); o2.depends({ encryption: 'wpa', '!contains': true }); o2.value('tls', _('TLS')); o2.value('ttls', _('TTLS')); o2.value('peap', _('PEAP')); o2.value('fast', _('FAST')); o2.default = 'peap'; o2 = s2.option(form.ListValue, 'auth', _('Authentication')); o2.depends({ encryption: 'wpa', '!contains': true }); o2.value('PAP', _('PAP')); o2.value('CHAP', _('CHAP')); o2.value('MSCHAP', _('MSCHAP')); o2.value('MSCHAPV2', _('MSCHAPV2')); o2.value('EAP-GTC', _('EAP-GTC')); o2.value('EAP-MD5', _('EAP-MD5')); o2.value('EAP-MSCHAPV2', _('EAP-MSCHAPV2')); o2.value('EAP-TLS', _('EAP-TLS')); o2.value('auth=PAP', _('auth=PAP')); o2.value('auth=MSCHAPV2', _('auth=MSCHAPV2')); o2.default = 'EAP-MSCHAPV2'; o2 = s2.option(form.Value, 'identity', _('Identity')); o2.depends({ encryption: 'wpa', '!contains': true }); o2 = s2.option(form.Value, 'anonymous_identity', _('Anonymous Identity')); o2.depends({ encryption: 'wpa', '!contains': true }); o2 = s2.option(form.ListValue, 'ieee80211w', _('Mgmt. Frame Protection')); o2.depends({ encryption: 'sae', '!contains': true }); o2.depends({ encryption: 'owe', '!contains': true }); o2.depends({ encryption: 'wpa', '!contains': true }); o2.depends({ encryption: 'psk', '!contains': true }); o2.value('', _('Disabled')); o2.value('1', _('Optional')); o2.value('2', _('Required')); o2.defaults = { '2': [{ encryption: 'sae' }, { encryption: 'owe' }, { encryption: 'wpa3' }, { encryption: 'wpa3-mixed' }], '1': [{ encryption: 'sae-mixed' }], '': [] }; o2 = s2.option(form.Flag, 'ca_cert_usesystem', _('Use system certificates'), _("Validate server certificate using built-in system CA bundle")); o2.depends({ encryption: 'wpa', '!contains': true }); o2.enabled = '1'; o2.disabled = '0'; o2.default = o2.disabled; o2 = s2.option(form.Value, 'ca_cert', _('Path to CA-Certificate')); o2.depends({ encryption: 'wpa', '!contains': true }); o2.depends({ ca_cert_usesystem: '0' }); o2.rmempty = true; o2 = s2.option(form.Value, 'client_cert', _('Path to Client-Certificate')); o2.depends({ eap_type: 'tls' }); o2.rmempty = true; o2 = s2.option(form.Value, 'priv_key', _('Path to Private Key')); o2.depends({ eap_type: 'tls' }); o2.rmempty = true; o2 = s2.option(form.Value, 'priv_key_pwd', _('Password of Private Key')); o2.depends({ eap_type: 'tls' }); o2.password = true; o2.rmempty = true; return m2.render().then(L.bind(function (elements) { ui.showModal(_('Add Uplink "%h"').format(ssid), [ elements, E('div', { 'class': 'right' }, [ E('button', { 'class': 'btn', 'style': 'float:none;margin-right:.4em;', 'click': ui.hideModal }, _('Dismiss')), E('button', { 'class': 'cbi-button cbi-button-positive important', 'click': ui.createHandlerFn(this, 'handleCommit', m2) }, _('Save')) ]) ]); }, this)); }; /* save new uplink */ s.handleCommit = function (map, ev) { const w_sections = uci.sections('wireless', 'wifi-iface'); const device = L.toArray(map.lookupOption('device', '_add_trm'))[0].formvalue('_add_trm'); const network = L.toArray(map.lookupOption('network', '_add_trm'))[0].formvalue('_add_trm'); const ssid = L.toArray(map.lookupOption('ssid', '_add_trm'))[0].formvalue('_add_trm'); const ignore_bssid = L.toArray(map.lookupOption('ignore_bssid', '_add_trm'))[0].formvalue('_add_trm'); const bssid = L.toArray(map.lookupOption('bssid', '_add_trm'))[0].formvalue('_add_trm'); const encryption = L.toArray(map.lookupOption('encryption', '_add_trm'))[0].formvalue('_add_trm'); let password = null; let eap_type, auth, identity, anonymous_identity, ca_cert_usesystem, ca_cert, ieee80211w; let client_cert, priv_key, priv_key_pwd; if (encryption.includes('wpa')) { eap_type = L.toArray(map.lookupOption('eap_type', '_add_trm'))[0].formvalue('_add_trm'); auth = L.toArray(map.lookupOption('auth', '_add_trm'))[0].formvalue('_add_trm'); identity = L.toArray(map.lookupOption('identity', '_add_trm'))[0].formvalue('_add_trm'); anonymous_identity = L.toArray(map.lookupOption('anonymous_identity', '_add_trm'))[0].formvalue('_add_trm'); password = L.toArray(map.lookupOption('password', '_add_trm'))[0].formvalue('_add_trm'); ca_cert_usesystem = L.toArray(map.lookupOption('ca_cert_usesystem', '_add_trm'))[0].formvalue('_add_trm'); ca_cert = L.toArray(map.lookupOption('ca_cert', '_add_trm'))[0].formvalue('_add_trm'); ieee80211w = L.toArray(map.lookupOption('ieee80211w', '_add_trm'))[0].formvalue('_add_trm'); if (eap_type.includes('tls')) { client_cert = L.toArray(map.lookupOption('client_cert', '_add_trm'))[0].formvalue('_add_trm'); priv_key = L.toArray(map.lookupOption('priv_key', '_add_trm'))[0].formvalue('_add_trm'); priv_key_pwd = L.toArray(map.lookupOption('priv_key_pwd', '_add_trm'))[0].formvalue('_add_trm'); } } else { password = L.toArray(map.lookupOption('key', '_add_trm'))[0].formvalue('_add_trm'); } if (!ssid || ((encryption.includes('psk') || encryption.includes('wpa') || encryption.includes('sae')) && !password)) { if (!ssid) { ui.addNotification(null, E('p', _('Empty SSID, the uplink station could not be saved.')), 'error'); } else { ui.addNotification(null, E('p', _('Empty Password, the uplink station could not be saved.')), 'error'); } return ui.hideModal(); } for (let i = 0; i < w_sections.length; i++) { if (w_sections[i].device === device && w_sections[i].ssid === ssid) { if (ignore_bssid === '1' || (ignore_bssid === '0' && normBssid(w_sections[i].bssid) === normBssid(bssid))) { ui.addNotification(null, E('p', _('Duplicate wireless entry, the uplink station could not be saved.')), 'error'); return ui.hideModal(); } } } var offset = w_sections.length, new_sid = 'trm_uplink' + (++offset); while (uci.get('wireless', new_sid)) { new_sid = 'trm_uplink' + (++offset); } uci.add('wireless', 'wifi-iface', new_sid); uci.set('wireless', new_sid, 'device', device); uci.set('wireless', new_sid, 'mode', 'sta'); uci.set('wireless', new_sid, 'network', network); uci.set('wireless', new_sid, 'ssid', ssid); if (ignore_bssid === '0') { uci.set('wireless', new_sid, 'bssid', bssid); } uci.set('wireless', new_sid, 'encryption', encryption); if (encryption.includes('wpa')) { uci.set('wireless', new_sid, 'eap_type', eap_type); uci.set('wireless', new_sid, 'auth', auth); uci.set('wireless', new_sid, 'identity', identity); uci.set('wireless', new_sid, 'anonymous_identity', anonymous_identity); uci.set('wireless', new_sid, 'password', password); uci.set('wireless', new_sid, 'ca_cert_usesystem', ca_cert_usesystem); uci.set('wireless', new_sid, 'ca_cert', ca_cert); uci.set('wireless', new_sid, 'ieee80211w', ieee80211w); if (eap_type.includes('tls')) { uci.set('wireless', new_sid, 'client_cert', client_cert); uci.set('wireless', new_sid, 'priv_key', priv_key); uci.set('wireless', new_sid, 'priv_key_pwd', priv_key_pwd); } } else { uci.set('wireless', new_sid, 'key', password); } uci.set('wireless', new_sid, 'disabled', '1'); handleSectionsAdd(network); uci.save() .then(L.bind(this.map.load, this.map)) .then(L.bind(this.map.reset, this.map)) .then(function () { let row = document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(new_sid)); if (row) { row.setAttribute('style', 'opacity: 0.5; color: #4a4 !important;'); } }) .then(ui.hideModal) .catch(function (err) { ui.hideModal(); ui.addNotification(null, E('p', _('Saving the new uplink failed: %s').format(err)), 'error'); }); }; return m.render(); }, handleReset: null });