luci-base: correctness fixes

validation:

use correct argument position for apply

network:

spec.need_tag -> port.need_tag agrees with old lua compat

widgets:

rv.length is undefined, use firstChild

form:

Follow-up to 315dbfc749
checkDepends recursion fix and implement cache lookup

uci:

improve timeout and Promise handling

ui:

follow-up to 92381c3ca2
renderListing sort: put directories first
getActiveTabId: check isNaN for tab state
getScrollParent: fix evaluation logic
fadeOutNotification: implement immediate timeout
openDropdown: accelerate draw via getBoundingClientRect

form:
ensure FlagValue parse always resolves

fs:
parse all 'expect' keys in RpcReply

luci:

Use the same source of truth in both the check and the dispatch
for flushRequestQueue

string check for dom string additions

Signed-off-by: Paul Donald <newtwen+github@gmail.com>
This commit is contained in:
Paul Donald
2026-05-26 17:33:06 +03:00
parent 4a63b98525
commit a818ac89bb
9 changed files with 92 additions and 67 deletions
@@ -319,7 +319,7 @@ Zone = AbstractFirewallItem.extend({
this.data = section; this.data = section;
} }
else if (name != null) { else if (name != null) {
var sections = uci.get('firewall', 'zone'); var sections = uci.sections('firewall', 'zone');
for (var i = 0; i < sections.length; i++) { for (var i = 0; i < sections.length; i++) {
if (sections[i].name != name) if (sections[i].name != name)
@@ -7,7 +7,7 @@
const scope = this; const scope = this;
uci.loadPackage('luci').catch(); uci.loadPackage('luci').catch(() => {});
const callSessionAccess = rpc.declare({ const callSessionAccess = rpc.declare({
object: 'session', object: 'session',
@@ -752,15 +752,18 @@ const CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
* @param {Event} ev * @param {Event} ev
* @param {number} n * @param {number} n
*/ */
checkDepends(ev, n) { checkDepends(ev, n, cache) {
if (cache == null)
cache = Object.create(null);
let changed = false; let changed = false;
for (let i = 0, s = this.children[0]; (s = this.children[i]) != null; i++) for (let i = 0, s = this.children[0]; (s = this.children[i]) != null; i++)
if (s.checkDepends(ev, n)) if (s.checkDepends(ev, n, cache))
changed = true; changed = true;
if (changed && (n ?? 0) < 10) if (changed && (n ?? 0) < 10)
this.checkDepends(ev, (n ?? 10) + 1); this.checkDepends(ev, (n ?? 0) + 1, cache);
ui.tabs.updateTabs(ev, this.root); ui.tabs.updateTabs(ev, this.root);
}, },
@@ -772,9 +775,12 @@ const CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
* @param {string} section_id * @param {string} section_id
* @returns {boolean} * @returns {boolean}
*/ */
isDependencySatisfied(depends, config_name, section_id) { isDependencySatisfied(depends, config_name, section_id, cache) {
let def = false; let def = false;
if (cache == null)
cache = Object.create(null);
if (!Array.isArray(depends) || !depends.length) if (!Array.isArray(depends) || !depends.length)
return true; return true;
@@ -792,8 +798,17 @@ const CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
istat = false; istat = false;
} }
else { else {
const res = this.lookupOption(dep, section_id, config_name); const key = `${config_name}::${section_id}::${dep}`;
const val = (res && res[0].isActive(res[1])) ? res[0].formvalue(res[1]) : null; let val;
if (key in cache) {
val = cache[key];
}
else {
const res = this.lookupOption(dep, section_id, config_name);
val = (res && res[0].isActive(res[1])) ? res[0].formvalue(res[1]) : null;
cache[key] = val;
}
const equal = contains const equal = contains
? isContained(val, depends[i][dep]) ? isContained(val, depends[i][dep])
@@ -1782,9 +1797,9 @@ const CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
* @param {string} section_id * @param {string} section_id
* @returns {boolean} * @returns {boolean}
*/ */
checkDepends(section_id) { checkDepends(section_id, cache) {
const config_name = this.uciconfig ?? this.section.uciconfig ?? this.map.config; const config_name = this.uciconfig ?? this.section.uciconfig ?? this.map.config;
const active = this.map.isDependencySatisfied(this.deps, config_name, section_id); const active = this.map.isDependencySatisfied(this.deps, config_name, section_id, cache);
if (active) if (active)
this.updateDefaultValue(section_id); this.updateDefaultValue(section_id);
@@ -5158,6 +5173,7 @@ const CBIFlagValue = CBIValue.extend(/** @lends LuCI.form.Flag.prototype */ {
else if (!this.retain) { else if (!this.retain) {
return Promise.resolve(this.remove(section_id)); return Promise.resolve(this.remove(section_id));
} }
return Promise.resolve();
}, },
}); });
@@ -6095,9 +6111,9 @@ const CBISectionValue = CBIValue.extend(/** @lends LuCI.form.SectionValue.protot
* @param {string} section_id * @param {string} section_id
* @returns {null} * @returns {null}
*/ */
checkDepends(section_id) { checkDepends(section_id, cache) {
this.subsection.checkDepends(section_id); this.subsection.checkDepends(section_id, cache);
return CBIValue.prototype.checkDepends.apply(this, [ section_id ]); return CBIValue.prototype.checkDepends.apply(this, [ section_id, cache ]);
}, },
/** /**
@@ -109,8 +109,6 @@ function handleRpcReply(expect, rc) {
let e = new Error(_('Unexpected reply data format')); e.name = 'TypeError'; let e = new Error(_('Unexpected reply data format')); e.name = 'TypeError';
throw e; throw e;
} }
break;
} }
} }
@@ -257,7 +257,7 @@
res = res.apply(this, callArgs); res = res.apply(this, callArgs);
if (symStack && symStack.length > 1) if (symStack && symStack.length > 1)
symStack.shift(protoCtx); symStack.shift();
else else
delete superContext[slotIdx]; delete superContext[slotIdx];
} }
@@ -571,8 +571,9 @@
} }
requestQueue.length = 0; requestQueue.length = 0;
const requestBaseURL = Request.expandURL(classes.rpc.getBaseURL());
Request.request(rpcBaseURL, reqopt).then(reply => { Request.request(requestBaseURL, reqopt).then(reply => {
let json = null, req = null; let json = null, req = null;
try { json = reply.json() } try { json = reply.json() }
@@ -1583,7 +1584,7 @@
else if (this.elem(html)) { else if (this.elem(html)) {
elem = html; elem = html;
} }
else if (html.charCodeAt(0) === 60) { else if (typeof(html) === 'string' && html.charCodeAt(0) === 60) {
elem = this.parse(html); elem = this.parse(html);
} }
else { else {
@@ -2673,6 +2674,9 @@
* has no sub-features. * has no sub-features.
*/ */
hasSystemFeature() { hasSystemFeature() {
if (!this.isObject(sysFeatures))
return null;
const ft = sysFeatures[arguments[0]]; const ft = sysFeatures[arguments[0]];
if (arguments.length == 2) if (arguments.length == 2)
@@ -2831,7 +2835,7 @@
* @returns {string} * @returns {string}
* Return the joined URL path. * Return the joined URL path.
*/ */
path(prefix = '', parts) { path(prefix = '', ...parts) {
const url = [ prefix ]; const url = [ prefix ];
for (let i = 0; i < parts.length; i++){ for (let i = 0; i < parts.length; i++){
@@ -468,7 +468,7 @@ function initNetworkState(refresh) {
if (port.device != null) { if (port.device != null) {
spec.device = port.device; spec.device = port.device;
spec.tagged = spec.need_tag; spec.tagged = port.need_tag;
netdevs[port.num] = port.device; netdevs[port.num] = port.device;
} }
@@ -207,10 +207,10 @@ var CBIZoneSelect = form.ListValue.extend({
emptyval.parentNode.removeChild(emptyval); emptyval.parentNode.removeChild(emptyval);
} }
else { else {
const anyval = node.querySelector('[data-value="*"]') || ''; const anyval = node.querySelector('[data-value="*"]');
let emptyval = node.querySelector('[data-value=""]') || ''; let emptyval = node.querySelector('[data-value=""]');
if (emptyval == null && anyval) { if (!emptyval && anyval) {
emptyval = anyval.cloneNode(true); emptyval = anyval.cloneNode(true);
emptyval.removeAttribute('display'); emptyval.removeAttribute('display');
emptyval.removeAttribute('selected'); emptyval.removeAttribute('selected');
@@ -559,7 +559,7 @@ var CBINetworkSelect = form.ListValue.extend({
if (values.indexOf(name) == -1) if (values.indexOf(name) == -1)
continue; continue;
if (rv.length) if (rv.firstChild)
L.dom.append(rv, ' '); L.dom.append(rv, ' ');
L.dom.append(rv, this.renderIfaceBadge(network)); L.dom.append(rv, this.renderIfaceBadge(network));
@@ -988,32 +988,33 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
* @returns {Promise<number>} * @returns {Promise<number>}
* Returns a promise resolving/rejecting with the `ubus` RPC status code. * Returns a promise resolving/rejecting with the `ubus` RPC status code.
*/ */
apply(timeout) { apply(timeout = 10) {
const self = this; const self = this;
const date = new Date();
if (typeof(timeout) != 'number' || timeout < 1) if (typeof timeout !== 'number' || timeout < 1)
timeout = 10; timeout = 10;
return self.callApply(timeout, true).then(rv => { return self.callApply(timeout, true).then(rv => {
if (rv != 0) if (rv != 0)
return Promise.reject(rv); return Promise.reject(rv);
const try_deadline = date.getTime() + 1000 * timeout; const try_deadline = Date.now() + timeout * 1000;
const try_confirm = () => {
return self.callConfirm().then(rv => { return new Promise((resolve, reject) => {
if (rv != 0) { const try_confirm = () => {
if (date.getTime() < try_deadline) self.callConfirm().then(rv => {
if (rv === 0)
return resolve(rv);
if (Date.now() < try_deadline)
window.setTimeout(try_confirm, 250); window.setTimeout(try_confirm, 250);
else else
return Promise.reject(rv); reject(rv);
} }).catch(reject);
};
return rv; window.setTimeout(try_confirm, 1000);
}); });
};
window.setTimeout(try_confirm, 1000);
}); });
}, },
@@ -1265,24 +1265,18 @@ const UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
* @returns {document} * @returns {document}
*/ */
getScrollParent(element) { getScrollParent(element) {
let parent = element; let parent = element.parentElement;
let style = getComputedStyle(element);
const excludeStaticParent = (style.position === 'absolute');
if (style.position === 'fixed') while (parent) {
return document.body; const style = getComputedStyle(parent);
while ((parent = parent.parentElement) != null) {
style = getComputedStyle(parent);
if (excludeStaticParent && style.position === 'static')
continue;
if (/(auto|scroll)/.test(style.overflow + style.overflowY + style.overflowX)) if (/(auto|scroll)/.test(style.overflow + style.overflowY + style.overflowX))
return parent; return parent;
parent = parent.parentElement;
} }
return document.body; return document.scrollingElement || document.documentElement;
}, },
@@ -1349,14 +1343,12 @@ const UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
const containerRect = scrollParent.getBoundingClientRect(); const containerRect = scrollParent.getBoundingClientRect();
const itemHeight = li[Math.max(0, li.length - 2)].getBoundingClientRect().height; const itemHeight = li.length ? li[Math.max(0, li.length - 2)].getBoundingClientRect().height : 0;
let fullHeight = 0; const visibleItems = (items == -1 ? li.length : items);
const fullHeight = itemHeight * visibleItems;
const spaceAbove = rect.top - containerRect.top; const spaceAbove = rect.top - containerRect.top;
const spaceBelow = containerRect.bottom - rect.bottom; const spaceBelow = containerRect.bottom - rect.bottom;
for (let i = 0; i < (items == -1 ? li.length : items); i++)
fullHeight += li[i].getBoundingClientRect().height;
if (fullHeight <= spaceBelow) { if (fullHeight <= spaceBelow) {
ul.style.top = `${rect.height}px`; ul.style.top = `${rect.height}px`;
ul.style.maxHeight = `${spaceBelow}px`; ul.style.maxHeight = `${spaceBelow}px`;
@@ -1597,7 +1589,7 @@ const UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
/** /**
* @private * @private
* @param {Node} sb * @param {Node} sb
* @param {string[]} values * @param {Object<string, boolean>} values
*/ */
setValues(sb, values) { setValues(sb, values) {
const ul = sb.querySelector('ul'); const ul = sb.querySelector('ul');
@@ -1927,7 +1919,7 @@ const UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
const li = active.nextElementSibling; const li = active.nextElementSibling;
this.setFocus(sb, li); this.setFocus(sb, li);
if (this.options.create && li == li.parentNode.lastElementChild) { if (this.options.create && li == li.parentNode.lastElementChild) {
const input = li.querySelector('input:not([type="hidden"]):not([type="checkbox"]'); const input = li.querySelector('input:not([type="hidden"]):not([type="checkbox"])');
if (input) input.focus(); if (input) input.focus();
} }
ev.preventDefault(); ev.preventDefault();
@@ -3461,8 +3453,7 @@ const UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */
const rows = E('ul'); const rows = E('ul');
list.sort((a, b) => { list.sort((a, b) => {
return L.naturalCompare(a.type == 'directory', b.type == 'directory') || return (b.type == 'directory') - (a.type == 'directory') || L.naturalCompare(a.name, b.name);
L.naturalCompare(a.name, b.name);
}); });
for (let i = 0; i < list.length; i++) { for (let i = 0; i < list.length; i++) {
@@ -4452,7 +4443,7 @@ const UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
if (element.parentNode) { if (element.parentNode) {
element.parentNode.removeChild(element); element.parentNode.removeChild(element);
} }
}); }, 0);
} }
} }
@@ -4809,7 +4800,7 @@ const UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
getActiveTabId(pane) { getActiveTabId(pane) {
const path = this.getPathForPane(pane); const path = this.getPathForPane(pane);
const p = +(this.getActiveTabState().paths[path]); const p = +(this.getActiveTabState().paths[path]);
return p ?? 0; return isNaN(p) ? 0 : p;
}, },
/** /**
@@ -5064,11 +5055,26 @@ const UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
return new Promise((resolveFn, rejectFn) => { return new Promise((resolveFn, rejectFn) => {
const img = new Image(); const img = new Image();
let timer = window.setTimeout(() => {
timer = null;
rejectFn();
}, 1000);
img.onload = resolveFn; img.onload = ev => {
img.onerror = rejectFn; if (timer !== null)
window.clearTimeout(timer);
timer = null;
img.onload = img.onerror = null;
resolveFn(ev);
};
window.setTimeout(rejectFn, 1000); img.onerror = ev => {
if (timer !== null)
window.clearTimeout(timer);
timer = null;
img.onload = img.onerror = null;
rejectFn(ev);
};
img.src = target; img.src = target;
}); });
@@ -549,7 +549,7 @@ const ValidatorFactory = baseclass.extend(/** @lends LuCI.validation.ValidatorFa
const x = parseInt(this.value, 16) | 0; const x = parseInt(this.value, 16) | 0;
const isll = (((x & 0xffc0) ^ 0xfe80) === 0); const isll = (((x & 0xffc0) ^ 0xfe80) === 0);
return this.assert(isll && this.apply('ip6addr', nomask), return this.assert(isll && this.apply('ip6addr', null, nomask),
_('valid IPv6 Link Local address')); _('valid IPv6 Link Local address'));
}, },
@@ -564,7 +564,7 @@ const ValidatorFactory = baseclass.extend(/** @lends LuCI.validation.ValidatorFa
const x = parseInt(this.value, 16) | 0; const x = parseInt(this.value, 16) | 0;
const isula = (((x & 0xfe00) ^ 0xfc00) === 0); const isula = (((x & 0xfe00) ^ 0xfc00) === 0);
return this.assert(isula && this.apply('ip6addr', nomask), return this.assert(isula && this.apply('ip6addr', null, nomask),
_('valid IPv6 ULA address')); _('valid IPv6 ULA address'));
}, },