luci-base: correctness fixes
authorPaul Donald <redacted>
Tue, 26 May 2026 14:33:06 +0000 (17:33 +0300)
committerPaul Donald <redacted>
Wed, 27 May 2026 12:49:44 +0000 (15:49 +0300)
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 315dbfc7498e2f43afb0119b992915e8f311bc37
checkDepends recursion fix and implement cache lookup

uci:

improve timeout and Promise handling

ui:

follow-up to 92381c3ca285c2303a59c74509fb4d6a422fece3
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 <redacted>
modules/luci-base/htdocs/luci-static/resources/firewall.js
modules/luci-base/htdocs/luci-static/resources/form.js
modules/luci-base/htdocs/luci-static/resources/fs.js
modules/luci-base/htdocs/luci-static/resources/luci.js
modules/luci-base/htdocs/luci-static/resources/network.js
modules/luci-base/htdocs/luci-static/resources/tools/widgets.js
modules/luci-base/htdocs/luci-static/resources/uci.js
modules/luci-base/htdocs/luci-static/resources/ui.js
modules/luci-base/htdocs/luci-static/resources/validation.js

index 8db4268daede3bd72bf8dd94d7e795a9051f4000..5eefc5cbd979a1a88351c4853c77357cda8fa49d 100644 (file)
@@ -319,7 +319,7 @@ Zone = AbstractFirewallItem.extend({
                        this.data = section;
                }
                else if (name != null) {
-                       var sections = uci.get('firewall', 'zone');
+                       var sections = uci.sections('firewall', 'zone');
 
                        for (var i = 0; i < sections.length; i++) {
                                if (sections[i].name != name)
index 7e11d9299f33ca19a528713b5e19737feb9059b7..d8e13802480e79faef634a699af69e21ab94e0e0 100644 (file)
@@ -7,7 +7,7 @@
 
 const scope = this;
 
-uci.loadPackage('luci').catch();
+uci.loadPackage('luci').catch(() => {});
 
 const callSessionAccess = rpc.declare({
        object: 'session',
@@ -752,15 +752,18 @@ const CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
         * @param {Event} ev
         * @param {number} n
         */
-       checkDepends(ev, n) {
+       checkDepends(ev, n, cache) {
+               if (cache == null)
+                       cache = Object.create(null);
+
                let changed = false;
 
                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;
 
                if (changed && (n ?? 0) < 10)
-                       this.checkDepends(ev, (n ?? 10) + 1);
+                       this.checkDepends(ev, (n ?? 0) + 1, cache);
 
                ui.tabs.updateTabs(ev, this.root);
        },
@@ -772,9 +775,12 @@ const CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
         * @param {string} section_id
         * @returns {boolean}
         */
-       isDependencySatisfied(depends, config_name, section_id) {
+       isDependencySatisfied(depends, config_name, section_id, cache) {
                let def = false;
 
+               if (cache == null)
+                       cache = Object.create(null);
+
                if (!Array.isArray(depends) || !depends.length)
                        return true;
 
@@ -792,8 +798,17 @@ const CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ {
                                        istat = false;
                                }
                                else {
-                                       const res = this.lookupOption(dep, section_id, config_name);
-                                       const val = (res && res[0].isActive(res[1])) ? res[0].formvalue(res[1]) : null;
+                                       const key = `${config_name}::${section_id}::${dep}`;
+                                       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
                                                ? isContained(val, depends[i][dep])
@@ -1782,9 +1797,9 @@ const CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract
         * @param {string} section_id
         * @returns {boolean}
         */
-       checkDepends(section_id) {
+       checkDepends(section_id, cache) {
                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)
                        this.updateDefaultValue(section_id);
@@ -5158,6 +5173,7 @@ const CBIFlagValue = CBIValue.extend(/** @lends LuCI.form.Flag.prototype */ {
                else if (!this.retain) {
                        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
         * @returns {null}
         */
-       checkDepends(section_id) {
-               this.subsection.checkDepends(section_id);
-               return CBIValue.prototype.checkDepends.apply(this, [ section_id ]);
+       checkDepends(section_id, cache) {
+               this.subsection.checkDepends(section_id, cache);
+               return CBIValue.prototype.checkDepends.apply(this, [ section_id, cache ]);
        },
 
        /**
index 20213bdbd37ec91fa6c8131f7a0b41ab749ba215..c35fc75f07118f044aed0c7855bc9f95f188fd4d 100644 (file)
@@ -109,8 +109,6 @@ function handleRpcReply(expect, rc) {
                                let e = new Error(_('Unexpected reply data format')); e.name = 'TypeError';
                                throw e;
                        }
-
-                       break;
                }
        }
 
index 68852e47b6aa56be524d9f03b1b99fd233483162..61eb0c1c45f78f33115202c23bb85f7d715bed11 100644 (file)
                                        res = res.apply(this, callArgs);
 
                                        if (symStack && symStack.length > 1)
-                                               symStack.shift(protoCtx);
+                                               symStack.shift();
                                        else
                                                delete superContext[slotIdx];
                                }
                }
 
                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;
 
                        try { json = reply.json() }
                        else if (this.elem(html)) {
                                elem = html;
                        }
-                       else if (html.charCodeAt(0) === 60) {
+                       else if (typeof(html) === 'string' && html.charCodeAt(0) === 60) {
                                elem = this.parse(html);
                        }
                        else {
                 * has no sub-features.
                 */
                hasSystemFeature() {
+                       if (!this.isObject(sysFeatures))
+                               return null;
+
                        const ft = sysFeatures[arguments[0]];
 
                        if (arguments.length == 2)
                 * @returns {string}
                 * Return the joined URL path.
                 */
-               path(prefix = '', parts) {
+               path(prefix = '', ...parts) {
                        const url = [ prefix ];
 
                        for (let i = 0; i < parts.length; i++){                         
index 4d519b002affc9f5a07649f83b4478d20297cc0f..b5e4c2b13da7cbc3d26063615e552424ff4eba30 100644 (file)
@@ -468,7 +468,7 @@ function initNetworkState(refresh) {
 
                                                                if (port.device != null) {
                                                                        spec.device = port.device;
-                                                                       spec.tagged = spec.need_tag;
+                                                                       spec.tagged = port.need_tag;
                                                                        netdevs[port.num] = port.device;
                                                                }
 
index 5f7f4ac6206f65c3df392f12f1804f9568b9d697..881143d2efb605d0bf33d90f6c88896e29bb7c28 100644 (file)
@@ -207,10 +207,10 @@ var CBIZoneSelect = form.ListValue.extend({
                                                emptyval.parentNode.removeChild(emptyval);
                                }
                                else {
-                                       const anyval = node.querySelector('[data-value="*"]') || '';
-                                       let emptyval = node.querySelector('[data-value=""]') || '';
+                                       const anyval = node.querySelector('[data-value="*"]');
+                                       let emptyval = node.querySelector('[data-value=""]');
 
-                                       if (emptyval == null && anyval) {
+                                       if (!emptyval && anyval) {
                                                emptyval = anyval.cloneNode(true);
                                                emptyval.removeAttribute('display');
                                                emptyval.removeAttribute('selected');
@@ -559,7 +559,7 @@ var CBINetworkSelect = form.ListValue.extend({
                        if (values.indexOf(name) == -1)
                                continue;
 
-                       if (rv.length)
+                       if (rv.firstChild)
                                L.dom.append(rv, ' ');
 
                        L.dom.append(rv, this.renderIfaceBadge(network));
index f53b548e3eeb3ae45dfe9c8e3a09fdb77f30c78f..b4b44f60ef826d3eb32225fc9d0ff37df02ee2c8 100644 (file)
@@ -988,32 +988,33 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
         * @returns {Promise<number>}
         * Returns a promise resolving/rejecting with the `ubus` RPC status code.
         */
-       apply(timeout) {
+       apply(timeout = 10) {
                const self = this;
-               const date = new Date();
 
-               if (typeof(timeout) != 'number' || timeout < 1)
+               if (typeof timeout !== 'number' || timeout < 1)
                        timeout = 10;
 
                return self.callApply(timeout, true).then(rv => {
                        if (rv != 0)
                                return Promise.reject(rv);
 
-                       const try_deadline = date.getTime() + 1000 * timeout;
-                       const try_confirm = () => {
-                               return self.callConfirm().then(rv => {
-                                       if (rv != 0) {
-                                               if (date.getTime() < try_deadline)
+                       const try_deadline = Date.now() + timeout * 1000;
+
+                       return new Promise((resolve, reject) => {
+                               const try_confirm = () => {
+                                       self.callConfirm().then(rv => {
+                                               if (rv === 0)
+                                                       return resolve(rv);
+
+                                               if (Date.now() < try_deadline)
                                                        window.setTimeout(try_confirm, 250);
                                                else
-                                                       return Promise.reject(rv);
-                                       }
-
-                                       return rv;
-                               });
-                       };
+                                                       reject(rv);
+                                       }).catch(reject);
+                               };
 
-                       window.setTimeout(try_confirm, 1000);
+                               window.setTimeout(try_confirm, 1000);
+                       });
                });
        },
 
index b21b365d7be14da0955e0c0294d03dfc1f6ee927..de466abe009de5c55764bc0594092b5b3da2d5ea 100644 (file)
@@ -1265,24 +1265,18 @@ const UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
         * @returns {document}
         */
        getScrollParent(element) {
-               let parent = element;
-               let style = getComputedStyle(element);
-               const excludeStaticParent = (style.position === 'absolute');
+               let parent = element.parentElement;
 
-               if (style.position === 'fixed')
-                       return document.body;
-
-               while ((parent = parent.parentElement) != null) {
-                       style = getComputedStyle(parent);
-
-                       if (excludeStaticParent && style.position === 'static')
-                               continue;
+               while (parent) {
+                       const style = getComputedStyle(parent);
 
                        if (/(auto|scroll)/.test(style.overflow + style.overflowY + style.overflowX))
                                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(() => {
                                const containerRect = scrollParent.getBoundingClientRect();
-                               const itemHeight = li[Math.max(0, li.length - 2)].getBoundingClientRect().height;
-                               let fullHeight = 0;
+                               const itemHeight = li.length ? li[Math.max(0, li.length - 2)].getBoundingClientRect().height : 0;
+                               const visibleItems = (items == -1 ? li.length : items);
+                               const fullHeight = itemHeight * visibleItems;
                                const spaceAbove = rect.top - containerRect.top;
                                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) {
                                        ul.style.top = `${rect.height}px`;
                                        ul.style.maxHeight = `${spaceBelow}px`;
@@ -1597,7 +1589,7 @@ const UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        /**
         * @private
         * @param {Node} sb
-        * @param {string[]} values
+        * @param {Object<string, boolean>} values
         */
        setValues(sb, values) {
                const ul = sb.querySelector('ul');
@@ -1927,7 +1919,7 @@ const UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                                        const li = active.nextElementSibling;
                                        this.setFocus(sb, li);
                                        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();
                                        }
                                        ev.preventDefault();
@@ -3461,8 +3453,7 @@ const UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */
                const rows = E('ul');
 
                list.sort((a, b) => {
-                       return L.naturalCompare(a.type == 'directory', b.type == 'directory') ||
-                                  L.naturalCompare(a.name, b.name);
+                       return (b.type == 'directory') - (a.type == 'directory') || L.naturalCompare(a.name, b.name);
                });
 
                for (let i = 0; i < list.length; i++) {
@@ -4452,7 +4443,7 @@ const UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                                        if (element.parentNode) {
                                                element.parentNode.removeChild(element);
                                        }
-                               });
+                               }, 0);
                        }
                }
 
@@ -4809,7 +4800,7 @@ const UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
                getActiveTabId(pane) {
                        const path = this.getPathForPane(pane);
                        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) => {
                        const img = new Image();
+                       let timer = window.setTimeout(() => {
+                               timer = null;
+                               rejectFn();
+                       }, 1000);
+
+                       img.onload = ev => {
+                               if (timer !== null)
+                                       window.clearTimeout(timer);
+                               timer = null;
+                               img.onload = img.onerror = null;
+                               resolveFn(ev);
+                       };
 
-                       img.onload = resolveFn;
-                       img.onerror = rejectFn;
-
-                       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;
                });
index 5e0e7e0d158f93b19b698b9ebc9cf96d82b7b9a3..abd9e0094811491197d6b332abf9821397f13ad8 100644 (file)
@@ -549,7 +549,7 @@ const ValidatorFactory = baseclass.extend(/** @lends LuCI.validation.ValidatorFa
                        const x = parseInt(this.value, 16) | 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'));
                },
 
@@ -564,7 +564,7 @@ const ValidatorFactory = baseclass.extend(/** @lends LuCI.validation.ValidatorFa
                        const x = parseInt(this.value, 16) | 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'));
                },
 
git clone https://git.99rst.org/PROJECT