⛄ Sync 2026-04-23 20:32:35

This commit is contained in:
github-actions[bot] committed 2026-04-23 20:32:35 +08:00
1 parent 17738c6f79
commit fe51d2b304
206 files changed
+725671

No files matched your search

@@ -0,0 +1,183 @@
'use strict';
var s = [0x0000, 0x0000, 0x0000, 0x0000];
/**
* Multiply two 64-bit values represented as arrays of four 16-bit words.
*
* Arrays use little-endian word order (least-significant 16-bit word first).
* The result is truncated to the lower 64 bits and returned as a 4-element
* array of 16-bit words.
*
* @param {Array<number>} a - Multiplicand (4 × 16-bit words, little-endian)
* @param {Array<number>} b - Multiplier (4 × 16-bit words, little-endian)
* @returns {Array<number>} Product as 4 × 16-bit words (little-endian)
*/
function mul(a, b) {
var r = [0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000];
for (var j = 0; j < 4; j++) {
var k = 0;
for (var i = 0; i < 4; i++) {
var t = a[i] * b[j] + r[i+j] + k;
r[i+j] = t & 0xffff;
k = t >>> 16;
}
r[j+4] = k;
}
r.length = 4;
return r;
}
/**
* Add a small integer to a 64-bit value represented as four 16-bit words.
*
* Treats `a` as a little-endian 64-bit value (4 × 16-bit words). Adds the
* integer `n` to the least-significant word and propagates carry across
* subsequent 16-bit words. The result is truncated to 64 bits and returned
* as a 4-element array of 16-bit words (little-endian).
*
* @param {Array<number>} a - Addend as 4 × 16-bit words (little-endian)
* @param {number} n - Value to add (integer carry)
* @returns {Array<number>} Sum as 4 × 16-bit words (little-endian)
*/
function add(a, n) {
var r = [0x0000, 0x0000, 0x0000, 0x0000],
k = n;
for (var i = 0; i < 4; i++) {
var t = a[i] + k;
r[i] = t & 0xffff;
k = t >>> 16;
}
return r;
}
/**
* Shift a 64-bit value (4 × 16-bit words, little-endian) right by `n` bits.
*
* The input array is treated as little-endian 16-bit words. Bits shifted out
* on the right are discarded; the returned array contains the lower 64-bit
* result after the logical right shift.
*
* @param {Array<number>} a - Source value as 4 × 16-bit words (little-endian)
* @param {number} n - Number of bits to shift right (non-negative integer)
* @returns {Array<number>} Shifted value as 4 × 16-bit words (little-endian)
*/
function shr(a, n) {
var r = [a[0], a[1], a[2], a[3], 0x0000],
i = 4,
k = 0;
for (; n > 16; n -= 16, i--)
for (var j = 0; j < 4; j++)
r[j] = r[j+1];
for (; i > 0; i--) {
var s = r[i-1];
r[i-1] = (s >>> n) | k;
k = ((s & ((1 << n) - 1)) << (16 - n));
}
r.length = 4;
return r;
}
return L.Class.extend({
/**
* Seed the PRNG state.
*
* The seed is treated as a 32-bit integer; the lower 16 bits are stored
* in `s[0]`, the upper 16 bits in `s[1]`. `s[2]` and `s[3]` are zeroed.
*
* @param {number} n - Seed value (32-bit integer)
* @returns {void}
*/
seed: function(n) {
n = (n - 1)|0;
s[0] = n & 0xffff;
s[1] = n >>> 16;
s[2] = 0;
s[3] = 0;
},
/**
* Produce the next PRNG 32-bit integer.
*
* Advances the internal state and returns a 32-bit pseudo-random integer
* derived from the current state.
*
* @returns {number} 32-bit pseudo-random integer (JS number)
*/
int: function() {
s = mul(s, [0x7f2d, 0x4c95, 0xf42d, 0x5851]);
s = add(s, 1);
var r = shr(s, 33);
return (r[1] << 16) | r[0];
},
/**
* Return a pseudo-random value.
*
* Overloads:
* - get() -> number in [0, 1]
* - get(upper) -> integer in [1, upper]
* - get(lower, upper) -> integer in [lower, upper]
*
* @param {number} [lower=0] - Lower bound (when two args supplied)
* @param {number} [upper=0] - Upper bound (when one or two args supplied)
* @returns {number} Random value (float in [0,1] or integer in requested range)
*/
get: function() {
var r = (this.int() % 0x7fffffff) / 0x7fffffff, l, u;
switch (arguments.length) {
case 0:
return r;
case 1:
l = 1;
u = arguments[0]|0;
break;
case 2:
l = arguments[0]|0;
u = arguments[1]|0;
break;
}
return Math.floor(r * (u - l + 1)) + l;
},
/**
* Derive a deterministic hex color from an input string.
*
* The color is produced by seeding the PRNG from a string-derived
* hash and producing RGB components. Returns a `#rrggbb` hex string.
*
* @param {string} string - Input string used to derive the color
* @returns {string} Hex color string in `#rrggbb` format
*/
derive_color: function(string) {
this.seed(parseInt(sfh(string), 16));
var r = this.get(128),
g = this.get(128),
min = 0,
max = 128;
if ((r + g) < 128)
min = 128 - r - g;
else
max = 255 - r - g;
var b = min + Math.floor(this.get() * (max - min));
return '#%02x%02x%02x'.format(0xff - r, 0xff - g, 0xff - b);
}
});
@@ -0,0 +1,295 @@
'use strict';
'require poll';
'require rpc';
'require uci';
'require fs';
'require ui';
'require view';
/* Note that any view implementing this log reader requires the log read
acl permission */
const callLogRead = rpc.declare({
object: 'log',
method: 'read',
params: [ 'lines', 'stream', 'oneshot' ],
expect: { log: [] }
});
var CBILogreadBox = function(logtag, name) {
return L.view.extend({
logFacilityFilter: 'any',
invertLogFacilitySearch: false,
logSeverityFilter: 'any',
invertLogSeveritySearch: false,
logTextFilter: '',
invertLogTextSearch: false,
logTagFilter: logtag ? logtag : '',
logName: name ? name : _('System Log'),
fetchMaxRows: 1000,
facilities: [
['any', 'any', _('Any')],
['0', 'kern', _('Kernel')],
['1', 'user', _('User')],
['2', 'mail', _('Mail')],
['3', 'daemon', _('Daemon')],
['4', 'auth', _('Auth')],
['5', 'syslog', _('Syslog')],
['6', 'lpr', _('LPR')],
['7', 'news', _('News')],
['8', 'uucp', _('UUCP')],
['9', 'cron', _('Cron')],
['10', 'authpriv', _('Auth Priv')],
['11', 'ftp', _('FTP')],
['12', 'ntp', _('NTP')],
['13', 'security', _('Log audit')],
['14', 'console', _('Log alert')],
['15', 'cron', _('Scheduling daemon')],
['16', 'local0', _('Local 0')],
['17', 'local1', _('Local 1')],
['18', 'local2', _('Local 2')],
['19', 'local3', _('Local 3')],
['20', 'local4', _('Local 4')],
['21', 'local5', _('Local 5')],
['22', 'local6', _('Local 6')],
['23', 'local7', _('Local 7')]
],
severity: [
['any','any', _('Any')],
['0', 'emerg', _('Emergency')],
['1', 'alert', _('Alert')],
['2', 'crit', _('Critical')],
['3', 'err', _('Error')],
['4', 'warn', _('Warning')],
['5', 'notice', _('Notice')],
['6', 'info', _('Info')],
['7', 'debug', _('Debug')]
],
async retrieveLog() {
try {
const tz = uci.get('system', '@system[0]', 'zonename')?.replaceAll(' ', '_');
const ts = uci.get('system', '@system[0]', 'clock_timestyle') || 0;
const hc = uci.get('system', '@system[0]', 'clock_hourcycle') || 0;
let loglines = await callLogRead(this.fetchMaxRows, false, true)
.then((logEntries) => {
const dateObj = new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: (ts == 0) ? 'long' : 'full',
hourCycle: (hc == 0) ? undefined : hc,
timeZone: tz
});
return logEntries.map(entry => {
const time = new Date(entry?.time);
const datestr = dateObj.format(time);
/* remember to add one since the 'any' entry occupies 1st position i.e. [0] */
const facility = this.facilities[Math.floor(entry?.priority / 8) + 1][1] ?? 'unknown';
const severity = this.severity[(entry?.priority % 8) + 1][1] ?? 'unknown';
return `[${datestr}] ${facility}.${severity}: ${entry?.msg}`;
});
})
.catch(function (){
return Promise.all([
L.resolveDefault(fs.stat('/usr/libexec/syslog-wrapper'), null),
]).then((stat) => {
const logger = stat[0]?.path;
return fs.exec_direct(logger)
.then(logdata => {
return logdata.trim().split(/\n/);
});
});
});
loglines = loglines.filter(line => {
const sevMatch = this.logSeverityFilter === 'any' || line.includes(`.${this.logSeverityFilter}`);
const facMatch = this.logFacilityFilter === 'any' || line.includes(`${this.logFacilityFilter}.`);
return (this.invertLogSeveritySearch != sevMatch)
&& (this.invertLogFacilitySearch != facMatch);
});
loglines = loglines.filter(line => {
return line.toLowerCase().includes(this.logTagFilter?.toLowerCase());
});
loglines = loglines.filter(line => {
const match = line.includes(this.logTextFilter);
return this.invertLogTextSearch ? !match : match;
});
return {
value: loglines?.join('\n'),
rows: loglines?.length + 1
};
}
catch (err) {
ui.addNotification(null, E('p', {}, _('Unable to load log data: ' + err.message)));
return {
value: '',
rows: 0
};
}
},
async pollLog() {
const element = document.getElementById('syslog');
if (element) {
const log = await this.retrieveLog();
element.value = log?.value;
element.rows = log?.rows;
}
},
async load() {
poll.add(this.pollLog.bind(this));
return Promise.all([
uci.load('system'),
]).then(() => this.retrieveLog());
},
render(loglines) {
const scrollDownButton = E('button', {
'id': 'scrollDownButton',
'class': 'cbi-button cbi-button-neutral'
}, _('Scroll to tail', 'scroll to bottom (the tail) of the log file')
);
scrollDownButton.addEventListener('click', () => {
scrollUpButton.scrollIntoView();
scrollDownButton.blur();
});
const scrollUpButton = E('button', {
'id' : 'scrollUpButton',
'class': 'cbi-button cbi-button-neutral'
}, _('Scroll to head', 'scroll to top (the head) of the log file')
);
scrollUpButton.addEventListener('click', () => {
scrollDownButton.scrollIntoView();
scrollUpButton.blur();
});
const self = this;
// Create facility invert checkbox
const facilityInvert = E('input', {
'id': 'invertLogFacilitySearch',
'type': 'checkbox',
'class': 'cbi-input-checkbox',
});
// Create facility select-dropdown from facilities map
const facilitySelect = E('select', {
'id': 'logFacilitySelect',
'class': 'cbi-input-select',
'style': 'margin-bottom:10px',
},
this.facilities.map(([ , val, label]) =>
(val == 'any') ? E('option', { value: val, selected: '' }, label) : E('option', { value: val }, label)
));
// Create severity invert checkbox
const severityInvert = E('input', {
'id': 'invertLogSeveritySearch',
'type': 'checkbox',
'class': 'cbi-input-checkbox',
});
// Create severity select-dropdown from facilities map
const severitySelect = E('select', {
'id': 'logSeveritySelect',
'class': 'cbi-input-select',
},
this.severity.map(([ , val, label]) =>
(val == 'any') ? E('option', { value: val, selected: '' }, label) : E('option', { value: val }, label)
));
// Create raw text search invert checkbox
const filterTextInvert = E('input', {
'id': 'invertLogTextSearch',
'type': 'checkbox',
'class': 'cbi-input-checkbox',
});
// Create raw text search text input
const filterTextInput = E('input', {
'id': 'logTextFilter',
'class': 'cbi-input-text',
});
// Create max rows input
const filterMaxRows = E('input', {
'id': 'logMaxRows',
'type': 'number',
'class': 'cbi-input',
});
/**
* Update the form when log filter parameters change
*/
function handleLogFilterChange() {
self.logFacilityFilter = facilitySelect.value;
self.invertLogFacilitySearch = facilityInvert.checked;
self.logSeverityFilter = severitySelect.value;
self.invertLogSeveritySearch = severityInvert.checked;
self.logTextFilter = filterTextInput.value;
self.invertLogTextSearch = filterTextInvert.checked;
self.fetchMaxRows = Number.parseInt(filterMaxRows.value);
self.pollLog();
}
facilitySelect.addEventListener('change', handleLogFilterChange);
facilityInvert.addEventListener('change', handleLogFilterChange);
severitySelect.addEventListener('change', handleLogFilterChange);
severityInvert.addEventListener('change', handleLogFilterChange);
filterTextInput.addEventListener('input', handleLogFilterChange);
filterTextInvert.addEventListener('change', handleLogFilterChange);
filterMaxRows.addEventListener('change', handleLogFilterChange);
return E([], [
E('h2', {}, [ this.logName ]),
E('div', { 'id': 'content_syslog' }, [
E('div', { class: 'cbi-section-descr' }, this.logTagFilter ? _('The syslog output, pre-filtered for messages related to: ' + this.logTagFilter) : '') ,
E('div', { 'style': 'margin-bottom:10px' }, [
E('label', { 'for': 'invertLogFacilitySearch', 'style': 'margin-right:5px' }, _('Not')),
facilityInvert,
E('label', { 'for': 'logFacilitySelect', 'style': 'margin: 0 5px' }, _('facility:')),
facilitySelect,
E('label', { 'for': 'invertLogSeveritySearch', 'style': 'margin: 0 5px' }, _('Not')),
severityInvert,
E('label', { 'for': 'logSeveritySelect', 'style': 'margin: 0 5px' }, _('severity:')),
severitySelect,
]),
E('div', { 'style': 'margin-bottom:10px' }, [
E('label', { 'for': 'invertLogTextSearch', 'style': 'margin-right:5px' }, _('Not')),
filterTextInvert,
E('label', { 'for': 'logTextFilter', 'style': 'margin: 0 5px' }, _('including:')),
filterTextInput,
E('label', { 'for': 'logMaxRows', 'style': 'margin: 0 5px' }, _('Max rows:')),
filterMaxRows,
]),
E('div', {'style': 'padding-bottom: 20px'}, [scrollDownButton]),
E('textarea', {
'id': 'syslog',
'style': 'font-size:12px',
'readonly': 'readonly',
'wrap': 'off',
'rows': loglines?.rows,
}, [ loglines?.value ]),
E('div', {'style': 'padding-bottom: 20px'}, [scrollUpButton])
])
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
};
return L.Class.extend({
LogreadBox: CBILogreadBox,
});
@@ -0,0 +1,855 @@
'use strict';
'require ui';
'require form';
'require network';
'require firewall';
'require fs';
'require uci';
/**
* Get users found in `/etc/passwd`.
* @returns {string[]}
*/
function getUsers() {
return fs.lines('/etc/passwd').then(function(lines) {
return lines.map(function(line) { return line.split(/:/)[0] });
});
}
/**
* Get users found in `/etc/group`.
* @returns {string[]}
*/
function getGroups() {
return fs.lines('/etc/group').then(function(lines) {
return lines.map(function(line) { return line.split(/:/)[0] });
});
}
/**
* Get bridge devices or Layer 3 devices of a network object.
* @param {object} network
* @returns {string[]}
*/
function getDevices(network) {
if (network.isBridge()) {
var devices = network.getDevices();
return devices ? devices : [];
} else {
return L.toArray(network.getL3Device());
}
}
var CBIZoneSelect = form.ListValue.extend({
__name__: 'CBI.ZoneSelect',
load(section_id) {
return Promise.all([ firewall.getZones(), network.getNetworks() ]).then(L.bind(function(zn) {
this.zones = zn[0];
this.networks = zn[1];
return this.super('load', section_id);
}, this));
},
filter(section_id, value) {
return true;
},
lookupZone(name) {
return this.zones.filter(function(zone) { return zone.getName() == name })[0];
},
lookupNetwork(name) {
return this.networks.filter(function(network) { return network.getName() == name })[0];
},
renderWidget(section_id, option_index, cfgvalue) {
const values = L.toArray((cfgvalue != null) ? cfgvalue : this.default);
let isOutputOnly = false;
const choices = {};
let datatype_str = 'ucifw4zonename';
if (!L.hasSystemFeature('firewall4'))
datatype_str = `and(${datatype_str},maxlength(11))`;
if (this.allowany && this.nocreate)
datatype_str = `or(${datatype_str},"*")`;
if (this.multiple)
datatype_str = `list(${datatype_str})`;
if (this.option == 'dest') {
for (let c of this.section.children) {
const opt = c;
if (opt.option == 'src') {
const val = opt.cfgvalue(section_id) || opt.default;
isOutputOnly = (val == null || val == '');
break;
}
}
this.title = isOutputOnly ? _('Output zone') : _('Destination zone');
}
if (this.allowlocal) {
choices[''] = E('span', {
'class': 'zonebadge',
'style': firewall.getZoneColorStyle(null)
}, [
E('strong', _('Device')),
(this.allowany || this.allowlocal)
? E('span', ' (%s)'.format(this.option != 'dest' ? _('output') : _('input'))) : ''
]);
}
else if (!this.multiple && (this.rmempty || this.optional)) {
choices[''] = E('span', {
'class': 'zonebadge',
'style': firewall.getZoneColorStyle(null)
}, E('em', _('unspecified')));
}
if (this.allowany) {
choices['*'] = E('span', {
'class': 'zonebadge',
'style': firewall.getZoneColorStyle(null)
}, [
E('strong', _('Any zone')),
(this.allowany && this.allowlocal && !isOutputOnly) ? E('span', ' (%s)'.format(_('forward'))) : ''
]);
}
for (let zone of this.zones) {
const name = zone.getName();
const networks = zone.getNetworks();
const ifaces = [];
if (!this.filter(section_id, name))
continue;
for (let n of networks) {
const network = this.lookupNetwork(n);
if (!network)
continue;
const span = E('span', {
'class': 'ifacebadge' + (network.isUp() ? ' ifacebadge-active' : '')
}, network.getName() + ': ');
const devices = getDevices(network);
for (let d of devices) {
span.appendChild(E('img', {
'title': d.getI18n(),
'src': L.resource('icons/%s%s.svg'.format(d.getType(), d.isUp() ? '' : '_disabled'))
}));
}
if (!devices.length)
span.appendChild(E('em', _('(empty)')));
ifaces.push(span);
}
if (!ifaces.length)
ifaces.push(E('em', _('(empty)')));
choices[name] = E('span', {
'class': 'zonebadge',
'style': firewall.getZoneColorStyle(zone)
}, [ E('strong', name) ].concat(ifaces));
}
const widget = new ui.Dropdown(values, choices, {
id: this.cbid(section_id),
sort: true,
multiple: this.multiple,
optional: this.optional || this.rmempty,
disabled: (this.readonly != null) ? this.readonly : this.map.readonly,
select_placeholder: E('em', _('unspecified')),
display_items: this.display_size || this.size || 3,
dropdown_items: this.dropdown_size || this.size || 5,
validate: L.bind(this.validate, this, section_id),
datatype: datatype_str,
create: !this.nocreate,
create_markup: '' +
'<li data-value="{{value}}">' +
'<span class="zonebadge" style="background:repeating-linear-gradient(45deg,rgba(204,204,204,0.5),rgba(204,204,204,0.5) 5px,rgba(255,255,255,0.5) 5px,rgba(255,255,255,0.5) 10px)">' +
'<strong>{{value}}:</strong> <em>('+_('create')+')</em>' +
'</span>' +
'</li>'
});
const elem = widget.render();
if (this.option == 'src') {
elem.addEventListener('cbi-dropdown-change', L.bind(function(ev) {
const opt = this.map.lookupOption('dest', section_id);
const val = ev.detail.instance.getValue();
if (opt == null)
return;
const cbid = opt[0].cbid(section_id);
const label = document.querySelector('label[for="widget.%s"]'.format(cbid));
const node = document.getElementById(cbid);
L.dom.content(label, val == '' ? _('Output zone') : _('Destination zone'));
if (val == '') {
if (L.dom.callClassMethod(node, 'getValue') == '')
L.dom.callClassMethod(node, 'setValue', '*');
const emptyval = node.querySelector('[data-value=""]');
const anyval = node.querySelector('[data-value="*"]');
L.dom.content(anyval.querySelector('span'), E('strong', _('Any zone')));
if (emptyval != null)
emptyval.parentNode.removeChild(emptyval);
}
else {
const anyval = node.querySelector('[data-value="*"]') || '';
let emptyval = node.querySelector('[data-value=""]') || '';
if (emptyval == null && anyval) {
emptyval = anyval.cloneNode(true);
emptyval.removeAttribute('display');
emptyval.removeAttribute('selected');
emptyval.setAttribute('data-value', '');
}
if (opt[0]?.allowlocal && emptyval)
L.dom.content(emptyval.querySelector('span'), [
E('strong', _('Device')), E('span', ' (%s)'.format(_('input')))
]);
if (opt[0]?.allowany && anyval && emptyval) {
L.dom.content(anyval.querySelector('span'), [
E('strong', _('Any zone')), E('span', ' (%s)'.format(_('forward')))
]);
anyval.parentNode.insertBefore(emptyval, anyval);
}
}
}, this));
}
else if (isOutputOnly) {
const emptyval = elem.querySelector('[data-value=""]');
emptyval.parentNode.removeChild(emptyval);
}
return elem;
},
});
var CBIZoneForwards = form.DummyValue.extend({
__name__: 'CBI.ZoneForwards',
load(section_id) {
return Promise.all([
firewall.getDefaults(),
firewall.getZones(),
network.getNetworks(),
network.getDevices()
]).then(L.bind(function(dznd) {
this.defaults = dznd[0];
this.zones = dznd[1];
this.networks = dznd[2];
this.devices = dznd[3];
return this.super('load', section_id);
}, this));
},
renderZone(zone) {
const name = zone.getName();
const networks = zone.getNetworks();
const devices = zone.getDevices();
const subnets = zone.getSubnets();
const ifaces = [];
for (let n of networks) {
const network = this.networks.filter(function(net) { return net.getName() == n })[0];
if (!network)
continue;
const span = E('span', {
'class': 'ifacebadge' + (network.isUp() ? ' ifacebadge-active' : '')
}, network.getName() + ': ');
const subdevs = getDevices(network);
for (let s of subdevs) {
span.appendChild(E('img', {
'title': s.getI18n(),
'src': L.resource('icons/%s%s.svg'.format(s.getType(), s.isUp() ? '' : '_disabled'))
}));
}
if (!subdevs.length)
span.appendChild(E('em', _('(empty)')));
ifaces.push(span);
}
for (let d of devices) {
const device = this.devices.filter(function(dev) { return dev.getName() == d })[0];
const title = device ? device.getI18n() : _('Absent Interface');
const type = device ? device.getType() : 'ethernet';
const up = device ? device.isUp() : false;
ifaces.push(E('span', { 'class': 'ifacebadge' }, [
E('img', {
'title': title,
'src': L.resource('icons/%s%s.svg'.format(type, up ? '' : '_disabled'))
}),
device ? device.getName() : d
]));
}
if (subnets.length > 0)
ifaces.push(E('span', { 'class': 'ifacebadge' }, [ '{ %s }'.format(subnets.join('; ')) ]));
if (!ifaces.length)
ifaces.push(E('span', { 'class': 'ifacebadge' }, E('em', _('(empty)'))));
return E('label', {
'class': 'zonebadge cbi-tooltip-container',
'style': firewall.getZoneColorStyle(zone)
}, [
E('strong', name),
E('div', { 'class': 'cbi-tooltip' }, ifaces)
]);
},
renderWidget(section_id, option_index, cfgvalue) {
const value = (cfgvalue != null) ? cfgvalue : this.default;
const zone = this.zones.filter(function(z) { return z.getName() == value })[0];
if (!zone)
return E([]);
const forwards = zone.getForwardingsBy('src');
const dzones = [];
for (var i = 0; i < forwards.length; i++) {
const dzone = forwards[i].getDestinationZone();
if (!dzone)
continue;
dzones.push(this.renderZone(dzone));
}
if (!dzones.length)
dzones.push(E('label', { 'class': 'zonebadge zonebadge-empty' },
E('strong', this.defaults.getForward())));
else
dzones.push(E('label', { 'class': 'zonebadge zonebadge-empty' },
E('strong', '%s %s'.format(this.defaults.getForward(), ('all others')))));
return E('div', { 'class': 'zone-forwards' }, [
E('div', { 'class': 'zone-src' }, this.renderZone(zone)),
E('span', '⇒'),
E('div', { 'class': 'zone-dest' }, dzones)
]);
},
});
const CBIIPSelect = form.ListValue.extend({
__name__: 'CBI.IPSelect',
load(section_id) {
return network.getDevices().then(L.bind(function(devices) {
this.devices = devices;
return this.super('load', section_id);
}, this));
},
filter(section_id, value) {
return true;
},
renderIfaceBadge(device, ip) {
return E('div', {}, [
ip,
' ',
E('span', { 'class': 'ifacebadge', }, [ device.getName(),
E('img', {
'title': device.getI18n(),
'src': L.resource('icons/%s%s.svg'.format(device.getType(), device.isUp() ? '' : '_disabled'))
})
]),
]);
},
renderWidget(section_id, option_index, cfgvalue) {
let values = L.toArray((cfgvalue != null) ? cfgvalue : this.default);
const choices = {};
const checked = {};
for (const val of values)
checked[val] = true;
values = [];
if (!this.multiple && (this.rmempty || this.optional))
choices[''] = E('em', _('unspecified'));
for (const device of (this.devices || [])) {
const name = device.getName();
if (name == this.exclude || !this.filter(section_id, name))
continue;
if (name == 'loopback' && !this.loopback)
continue;
if (this.novirtual && device.isVirtual())
continue;
for (const ip of [...device.getIPAddrs(), ...device.getIP6Addrs()]) {
const iponly = ip.split('/')?.[0]
if (checked[iponly])
values.push(iponly);
choices[iponly] = this.renderIfaceBadge(device, iponly);
}
}
const widget = new ui.Dropdown(this.multiple ? values : values[0], choices, {
id: this.cbid(section_id),
sort: true,
multiple: this.multiple,
optional: this.optional || this.rmempty,
disabled: (this.readonly != null) ? this.readonly : this.map.readonly,
select_placeholder: E('em', _('unspecified')),
display_items: this.display_size || this.size || 2,
dropdown_items: this.dropdown_size || this.size || 5,
datatype: this.multiple ? 'list(ipaddr)' : 'ipaddr',
validate: L.bind(this.validate, this, section_id),
create: false,
});
return widget.render();
},
textvalue(section_id) {
const cfgvalue = this.cfgvalue(section_id);
const values = L.toArray((cfgvalue != null) ? cfgvalue : this.default);
const rv = E([]);
for (const device of (this.devices || [])) {
for (const ip of [...device.getIPAddrs(), ...device.getIP6Addrs()]) {
const iponly = ip.split('/')[0];
if (values.indexOf(iponly) === -1)
continue;
if (rv.childNodes.length)
rv.appendChild(document.createTextNode(' '));
rv.appendChild(this.renderIfaceBadge(device, iponly));
}
}
if (!rv.firstChild)
rv.appendChild(E('em', _('unspecified')));
return rv;
},
});
var CBINetworkSelect = form.ListValue.extend({
__name__: 'CBI.NetworkSelect',
load(section_id) {
return network.getNetworks().then(L.bind(function(networks) {
this.networks = networks;
return this.super('load', section_id);
}, this));
},
filter(section_id, value) {
return true;
},
renderIfaceBadge(network) {
const span = E('span', { 'class': 'ifacebadge' }, network.getName() + ': ');
const devices = getDevices(network);
for (let d of devices) {
span.appendChild(E('img', {
'title': d.getI18n(),
'src': L.resource('icons/%s%s.svg'.format(d.getType(), d.isUp() ? '' : '_disabled'))
}));
}
if (!devices.length) {
span.appendChild(E('em', { 'class': 'hide-close' }, _('(no interfaces attached)')));
span.appendChild(E('em', { 'class': 'hide-open' }, '-'));
}
return span;
},
renderWidget(section_id, option_index, cfgvalue) {
let values = L.toArray((cfgvalue != null) ? cfgvalue : this.default);
const choices = {};
const checked = {};
for (var i = 0; i < values.length; i++)
checked[values[i]] = true;
values = [];
if (!this.multiple && (this.rmempty || this.optional))
choices[''] = E('em', _('unspecified'));
for (let network of this.networks) {
const name = network.getName();
if (name == this.exclude || !this.filter(section_id, name))
continue;
if (name == 'loopback' && !this.loopback)
continue;
if (this.novirtual && network.isVirtual())
continue;
if (checked[name])
values.push(name);
choices[name] = this.renderIfaceBadge(network);
}
const widget = new ui.Dropdown(this.multiple ? values : values[0], choices, {
id: this.cbid(section_id),
sort: true,
multiple: this.multiple,
optional: this.optional || this.rmempty,
disabled: (this.readonly != null) ? this.readonly : this.map.readonly,
select_placeholder: E('em', _('unspecified')),
display_items: this.display_size || this.size || 3,
dropdown_items: this.dropdown_size || this.size || 5,
datatype: this.multiple ? 'list(uciname)' : 'uciname',
validate: L.bind(this.validate, this, section_id),
create: !this.nocreate,
create_markup: '' +
'<li data-value="{{value}}">' +
'<span class="ifacebadge" style="background:repeating-linear-gradient(45deg,rgba(204,204,204,0.5),rgba(204,204,204,0.5) 5px,rgba(255,255,255,0.5) 5px,rgba(255,255,255,0.5) 10px)">' +
'{{value}}: <em>('+_('create')+')</em>' +
'</span>' +
'</li>'
});
return widget.render();
},
textvalue(section_id) {
const cfgvalue = this.cfgvalue(section_id);
const values = L.toArray((cfgvalue != null) ? cfgvalue : this.default);
const rv = E([]);
for (let network of this.networks) {
const name = network.getName();
if (values.indexOf(name) == -1)
continue;
if (rv.length)
L.dom.append(rv, ' ');
L.dom.append(rv, this.renderIfaceBadge(network));
}
if (!rv.firstChild)
rv.appendChild(E('em', _('unspecified')));
return rv;
},
});
var CBIDeviceSelect = form.ListValue.extend({
__name__: 'CBI.DeviceSelect',
load(section_id) {
return Promise.all([
network.getDevices(),
this.noaliases ? null : network.getNetworks()
]).then(L.bind(function(data) {
this.devices = data[0];
this.networks = data[1];
return this.super('load', section_id);
}, this));
},
filter(section_id, value) {
return true;
},
renderWidget(section_id, option_index, cfgvalue) {
var values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
choices = {},
checked = {},
order = [];
for (var i = 0; i < values.length; i++)
checked[values[i]] = true;
values = [];
if (!this.multiple && (this.rmempty || this.optional))
choices[''] = E('em', _('unspecified'));
for (let device of this.devices) {
const name = device.getName();
const type = device.getType();
if (name == 'lo' || name == this.exclude || !this.filter(section_id, name))
continue;
if (this.noaliases && type == 'alias')
continue;
if (this.nobridges && type == 'bridge')
continue;
if (this.noinactive && device.isUp() == false)
continue;
const item = E([
E('img', {
'title': device.getI18n(),
'src': L.resource('icons/%s%s.svg'.format(type, device.isUp() ? '' : '_disabled'))
}),
E('span', { 'class': 'hide-open' }, [ name ]),
E('span', { 'class': 'hide-close'}, [ device.getI18n() ])
]);
const networks = device.getNetworks();
if (networks.length > 0)
L.dom.append(item.lastChild, [ ' (', networks.map(function(n) { return n.getName() }).join(', '), ')' ]);
if (checked[name])
values.push(name);
choices[name] = item;
order.push(name);
}
if (this.networks != null) {
for (let net of this.networks) {
const device = network.instantiateDevice('@%s'.format(net.getName()), net);
const name = device.getName();
if (name == '@loopback' || name == this.exclude || !this.filter(section_id, name))
continue;
if (this.noinactive && net.isUp() == false)
continue;
const item = E([
E('img', {
'title': device.getI18n(),
'src': L.resource('icons/alias%s.svg'.format(device.isUp() ? '' : '_disabled'))
}),
E('span', { 'class': 'hide-open' }, [ name ]),
E('span', { 'class': 'hide-close'}, [ device.getI18n() ])
]);
if (checked[name])
values.push(name);
choices[name] = item;
order.push(name);
}
}
if (this.includeips) {
this.devices.forEach(net_dev => {
['getIPAddrs', 'getIP6Addrs'].forEach(fn => {
net_dev[fn]().forEach(addr => {
const name = addr.split('/')[0];
if (checked[name]) values.push(name);
choices[name] = E([], [name, ' (', E('strong', net_dev.getName()), ')']);
order.push(name);
});
});
});
}
if (!this.nocreate) {
const keys = Object.keys(checked).sort(L.naturalCompare);
for (let k of keys) {
if (choices.hasOwnProperty(k))
continue;
choices[k] = E([
E('img', {
'title': _('Absent Interface'),
'src': L.resource('icons/ethernet_disabled.svg')
}),
E('span', { 'class': 'hide-open' }, [ k ]),
E('span', { 'class': 'hide-close'}, [ '%s: "%h"'.format(_('Absent Interface'), k) ])
]);
values.push(k);
order.push(k);
}
}
const widget = new ui.Dropdown(this.multiple ? values : values[0], choices, {
id: this.cbid(section_id),
sort: order,
multiple: this.multiple,
optional: this.optional || this.rmempty,
disabled: (this.readonly != null) ? this.readonly : this.map.readonly,
select_placeholder: E('em', _('unspecified')),
display_items: this.display_size || this.size || 3,
dropdown_items: this.dropdown_size || this.size || 5,
validate: L.bind(this.validate, this, section_id),
create: !this.nocreate,
create_markup: '' +
'<li data-value="{{value}}">' +
'<img title="'+_('Custom Interface')+': &quot;{{value}}&quot;" src="'+L.resource('icons/ethernet_disabled.svg')+'" />' +
'<span class="hide-open">{{value}}</span>' +
'<span class="hide-close">'+_('Custom Interface')+': "{{value}}"</span>' +
'</li>'
});
return widget.render();
},
});
var CBIUserSelect = form.ListValue.extend({
__name__: 'CBI.UserSelect',
load(section_id) {
return getUsers().then(L.bind(function(users) {
delete this.keylist;
delete this.vallist;
for (var i = 0; i < users.length; i++) {
this.value(users[i]);
}
return this.super('load', section_id);
}, this));
},
filter(section_id, value) {
return true;
},
});
var CBIGroupSelect = form.ListValue.extend({
__name__: 'CBI.GroupSelect',
load(section_id) {
return getGroups().then(L.bind(function(groups) {
for (var i = 0; i < groups.length; i++) {
this.value(groups[i]);
}
return this.super('load', section_id);
}, this));
},
filter(section_id, value) {
return true;
},
});
var CBIWifidogxGroupSelect = form.ListValue.extend({
__name__: 'CBI.WifidogxGroupSelect',
// group_type: 1: Domain Group, 2: MAC Group
// add a new property to store the group type
group_type: '1',
load: function(section_id) {
this.sappList = [];
var sections = uci.sections('wifidogx', 'group');
for (var i = 0; i < sections.length; i++) {
if (sections[i]['g_type'] == this.group_type)
this.sappList.push(sections[i]['.name']);
}
return this.super('load', section_id);
},
setGroupType: function(group_type) {
if (group_type == 'mac') {
this.group_type = '2';
} else if (group_type == 'wildcard') {
this.group_type = '3';
}
},
filter: function(section_id, value) {
return true;
},
renderWidget: function(section_id, option_index, cfgvalue) {
var values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
choices = {},
checked = {};
for (var i = 0; i < values.length; i++)
checked[values[i]] = true;
values = [];
if (!this.multiple && (this.rmempty || this.optional))
choices[''] = E('em', _('unspecified'));
for (var i = 0; i < this.sappList.length; i++) {
var name = this.sappList[i];
if (checked[name])
values.push(name);
choices[name] = E('span', { 'class': 'ifacebadge' }, name);
}
var widget = new ui.Dropdown(this.multiple ? values : values[0], choices, {
id: this.cbid(section_id),
sort: true,
multiple: this.multiple,
optional: this.optional || this.rmempty,
disabled: (this.readonly != null) ? this.readonly : this.map.readonly,
select_placeholder: E('em', _('unspecified')),
display_items: this.display_size || this.size || 3,
dropdown_items: this.dropdown_size || this.size || 5,
validate: L.bind(this.validate, this, section_id),
create: !this.nocreate,
create_markup: '' +
'<li data-value="{{value}}">' +
'<span class="ifacebadge" style="background:repeating-linear-gradient(45deg,rgba(204,204,204,0.5),rgba(204,204,204,0.5) 5px,rgba(255,255,255,0.5) 5px,rgba(255,255,255,0.5) 10px)">' +
'{{value}}: <em>('+_('create')+')</em>' +
'</span>' +
'</li>'
});
return widget.render();
}
});
return L.Class.extend({
ZoneSelect: CBIZoneSelect,
ZoneForwards: CBIZoneForwards,
IPSelect: CBIIPSelect,
NetworkSelect: CBINetworkSelect,
DeviceSelect: CBIDeviceSelect,
UserSelect: CBIUserSelect,
GroupSelect: CBIGroupSelect,
WifidogxGroupSelect: CBIWifidogxGroupSelect,
});