diff options
Diffstat (limited to 'modules/luci-base/htdocs')
14 files changed, 818 insertions, 291 deletions
diff --git a/modules/luci-base/htdocs/cgi-bin/luci b/modules/luci-base/htdocs/cgi-bin/luci index 529d1d0bc5..c5c9847346 100755 --- a/modules/luci-base/htdocs/cgi-bin/luci +++ b/modules/luci-base/htdocs/cgi-bin/luci @@ -2,4 +2,4 @@ require "luci.cacheloader" require "luci.sgi.cgi" luci.dispatcher.indexcache = "/tmp/luci-indexcache" -luci.sgi.cgi.run()
\ No newline at end of file +luci.sgi.cgi.run() diff --git a/modules/luci-base/htdocs/luci-static/resources/cbi.js b/modules/luci-base/htdocs/luci-static/resources/cbi.js index 92c41515fb..b66fe684a5 100644 --- a/modules/luci-base/htdocs/luci-static/resources/cbi.js +++ b/modules/luci-base/htdocs/luci-static/resources/cbi.js @@ -744,22 +744,22 @@ function cbi_update_table(table, data, placeholder) { if (!isElem(target)) return; - target.querySelectorAll('.tr.table-titles, .cbi-section-table-titles').forEach(function(thead) { + target.querySelectorAll('tr.table-titles, .tr.table-titles, .cbi-section-table-titles').forEach(function(thead) { var titles = []; - thead.querySelectorAll('.th').forEach(function(th) { + thead.querySelectorAll('th, .th').forEach(function(th) { titles.push(th); }); if (Array.isArray(data)) { - var n = 0, rows = target.querySelectorAll('.tr'); + var n = 0, rows = target.querySelectorAll('tr, .tr'); data.forEach(function(row) { - var trow = E('div', { 'class': 'tr' }); + var trow = E('tr', { 'class': 'tr' }); for (var i = 0; i < titles.length; i++) { var text = (titles[i].innerText || '').trim(); - var td = trow.appendChild(E('div', { + var td = trow.appendChild(E('td', { 'class': titles[i].className, 'data-title': (text !== '') ? text : null }, row[i] || '')); @@ -780,8 +780,8 @@ function cbi_update_table(table, data, placeholder) { target.removeChild(rows[n]); if (placeholder && target.firstElementChild === target.lastElementChild) { - var trow = target.appendChild(E('div', { 'class': 'tr placeholder' })); - var td = trow.appendChild(E('div', { 'class': titles[0].className }, placeholder)); + var trow = target.appendChild(E('tr', { 'class': 'tr placeholder' })); + var td = trow.appendChild(E('td', { 'class': titles[0].className }, placeholder)); td.classList.remove('th'); td.classList.add('td'); @@ -790,10 +790,10 @@ function cbi_update_table(table, data, placeholder) { else { thead.parentNode.style.display = 'none'; - thead.parentNode.querySelectorAll('.tr, .cbi-section-table-row').forEach(function(trow) { + thead.parentNode.querySelectorAll('tr, .tr, .cbi-section-table-row').forEach(function(trow) { if (trow !== thead) { var n = 0; - trow.querySelectorAll('.th, .td').forEach(function(td) { + trow.querySelectorAll('th, td, .th, .td').forEach(function(td) { if (n < titles.length) { var text = (titles[n++].innerText || '').trim(); if (text !== '') diff --git a/modules/luci-base/htdocs/luci-static/resources/firewall.js b/modules/luci-base/htdocs/luci-static/resources/firewall.js index b1c7de4358..4fa4954ba6 100644 --- a/modules/luci-base/htdocs/luci-static/resources/firewall.js +++ b/modules/luci-base/htdocs/luci-static/resources/firewall.js @@ -57,21 +57,7 @@ function getColorForName(forName) { else if (forName == 'wan') return '#f09090'; - random.seed(parseInt(sfh(forName), 16)); - - var r = random.get(128), - g = random.get(128), - min = 0, - max = 128; - - if ((r + g) < 128) - min = 128 - r - g; - else - max = 255 - r - g; - - var b = min + Math.floor(random.get() * (max - min)); - - return '#%02x%02x%02x'.format(0xff - r, 0xff - g, 0xff - b); + return random.derive_color(forName); } @@ -106,7 +92,6 @@ Firewall = L.Class.extend({ z = uci.add('firewall', 'zone'); uci.set('firewall', z, 'name', name); - uci.set('firewall', z, 'network', ' '); uci.set('firewall', z, 'input', d.getInput() || 'DROP'); uci.set('firewall', z, 'output', d.getOutput() || 'DROP'); uci.set('firewall', z, 'forward', d.getForward() || 'DROP'); @@ -347,17 +332,17 @@ Zone = AbstractFirewallItem.extend({ return false; newNetworks.push(network); - this.set('network', newNetworks.join(' ')); + this.set('network', newNetworks); return true; }, deleteNetwork: function(network) { var oldNetworks = this.getNetworks(), - newNetworks = oldNetworks.filter(function(net) { return net != network }); + newNetworks = oldNetworks.filter(function(net) { return net != network }); if (newNetworks.length > 0) - this.set('network', newNetworks.join(' ')); + this.set('network', newNetworks); else this.set('network', null); @@ -369,7 +354,7 @@ Zone = AbstractFirewallItem.extend({ }, clearNetworks: function() { - this.set('network', ' '); + this.set('network', null); }, getDevices: function() { diff --git a/modules/luci-base/htdocs/luci-static/resources/form.js b/modules/luci-base/htdocs/luci-static/resources/form.js index a50d457e21..52506d30e8 100644 --- a/modules/luci-base/htdocs/luci-static/resources/form.js +++ b/modules/luci-base/htdocs/luci-static/resources/form.js @@ -603,7 +603,7 @@ var CBIMap = CBIAbstractElement.extend(/** @lends LuCI.form.Map.prototype */ { E('p', {}, [ _('An error occurred while saving the form:') ]), E('p', {}, [ E('em', { 'style': 'white-space:pre' }, [ e.message ]) ]), E('div', { 'class': 'right' }, [ - E('button', { 'click': ui.hideModal }, [ _('Dismiss') ]) + E('button', { 'class': 'btn', 'click': ui.hideModal }, [ _('Dismiss') ]) ]) ]); } @@ -1054,6 +1054,138 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract return obj; }, + /** + * Query underlying option configuration values. + * + * This function is sensitive to the amount of arguments passed to it; + * if only one argument is specified, the configuration values of all + * options within this section are returned as dictionary. + * + * If both the section ID and an option name are supplied, this function + * returns the configuration value of the specified option only. + * + * @param {string} section_id + * The configuration section ID + * + * @param {string} [option] + * The name of the option to query + * + * @returns {null|string|string[]|Object<string, null|string|string[]>} + * Returns either a dictionary of option names and their corresponding + * configuration values or just a single configuration value, depending + * on the amount of passed arguments. + */ + cfgvalue: function(section_id, option) { + var rv = (arguments.length == 1) ? {} : null; + + for (var i = 0, o; (o = this.children[i]) != null; i++) + if (rv) + rv[o.option] = o.cfgvalue(section_id); + else if (o.option == option) + return o.cfgvalue(section_id); + + return rv; + }, + + /** + * Query underlying option widget input values. + * + * This function is sensitive to the amount of arguments passed to it; + * if only one argument is specified, the widget input values of all + * options within this section are returned as dictionary. + * + * If both the section ID and an option name are supplied, this function + * returns the widget input value of the specified option only. + * + * @param {string} section_id + * The configuration section ID + * + * @param {string} [option] + * The name of the option to query + * + * @returns {null|string|string[]|Object<string, null|string|string[]>} + * Returns either a dictionary of option names and their corresponding + * widget input values or just a single widget input value, depending + * on the amount of passed arguments. + */ + formvalue: function(section_id, option) { + var rv = (arguments.length == 1) ? {} : null; + + for (var i = 0, o; (o = this.children[i]) != null; i++) { + var func = this.map.root ? this.children[i].formvalue : this.children[i].cfgvalue; + + if (rv) + rv[o.option] = func.call(o, section_id); + else if (o.option == option) + return func.call(o, section_id); + } + + return rv; + }, + + /** + * Obtain underlying option LuCI.ui widget instances. + * + * This function is sensitive to the amount of arguments passed to it; + * if only one argument is specified, the LuCI.ui widget instances of all + * options within this section are returned as dictionary. + * + * If both the section ID and an option name are supplied, this function + * returns the LuCI.ui widget instance value of the specified option only. + * + * @param {string} section_id + * The configuration section ID + * + * @param {string} [option] + * The name of the option to query + * + * @returns {null|LuCI.ui.AbstractElement|Object<string, null|LuCI.ui.AbstractElement>} + * Returns either a dictionary of option names and their corresponding + * widget input values or just a single widget input value, depending + * on the amount of passed arguments. + */ + getUIElement: function(section_id, option) { + var rv = (arguments.length == 1) ? {} : null; + + for (var i = 0, o; (o = this.children[i]) != null; i++) + if (rv) + rv[o.option] = o.getUIElement(section_id); + else if (o.option == option) + return o.getUIElement(section_id); + + return rv; + }, + + /** + * Obtain underlying option objects. + * + * This function is sensitive to the amount of arguments passed to it; + * if no option name is specified, all options within this section are + * returned as dictionary. + * + * If an option name is supplied, this function returns the matching + * LuCI.form.AbstractValue instance only. + * + * @param {string} [option] + * The name of the option object to obtain + * + * @returns {null|LuCI.form.AbstractValue|Object<string, LuCI.form.AbstractValue>} + * Returns either a dictionary of option names and their corresponding + * option instance objects or just a single object instance value, + * depending on the amount of passed arguments. + */ + getOption: function(option) { + var rv = (arguments.length == 0) ? {} : null; + + for (var i = 0, o; (o = this.children[i]) != null; i++) + if (rv) + rv[o.option] = o; + else if (o.option == option) + return o; + + return rv; + }, + /** @private */ renderUCISection: function(section_id) { var renderTasks = []; @@ -1132,6 +1264,9 @@ var CBIAbstractSection = CBIAbstractElement.extend(/** @lends LuCI.form.Abstract var isEqual = function(x, y) { + if (typeof(y) == 'object' && y instanceof RegExp) + return (x == null) ? false : y.test(x); + if (x != null && y != null && typeof(x) != typeof(y)) return false; @@ -1372,6 +1507,21 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa */ /** + * Register a custom value change handler. + * + * If this property is set to a function value, the function is invoked + * whenever the value of the underlying UI input element is changing. + * + * The invoked handler function will receive the DOM click element as + * first and the underlying configuration section ID as well as the input + * value as second and third argument respectively. + * + * @name LuCI.form.AbstractValue.prototype#onchange + * @type function + * @default null + */ + + /** * Add a dependency contraint to the option. * * Dependency constraints allow making the presence of option elements @@ -1428,6 +1578,10 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa * Equivalent to the previous example. * </li> * <li> + * <code>opt.depends({ foo: /test/ })</code><br> + * Require the value of `foo` to match the regular expression `/test/`. + * </li> + * <li> * <code>opt.depends({ foo: "test", bar: "qrx" })</code><br> * Require the value of `foo` to be `test` and the value of `bar` to be * `qrx`. @@ -1449,11 +1603,11 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa * </li> * </ul> * - * @param {string|Object<string, string|boolean>} optionname_or_depends + * @param {string|Object<string, string|RegExp>} optionname_or_depends * The name of the option to depend on or an object describing multiple * dependencies which must be satified (a logical "and" expression). * - * @param {string} optionvalue + * @param {string} optionvalue|RegExp * When invoked with a plain option name as first argument, this parameter * specifies the expected value. In case an object is passed as first * argument, this parameter is ignored. @@ -1708,6 +1862,9 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa if (cval == null) cval = this.default; + if (Array.isArray(cval)) + cval = cval.join(' '); + return (cval != null) ? '%h'.format(cval) : null; }, @@ -1878,10 +2035,32 @@ var CBIAbstractValue = CBIAbstractElement.extend(/** @lends LuCI.form.AbstractVa * The configuration section ID */ remove: function(section_id) { - return this.map.data.unset( - this.uciconfig || this.section.uciconfig || this.map.config, - this.ucisection || section_id, - this.ucioption || this.option); + var this_cfg = this.uciconfig || this.section.uciconfig || this.map.config, + this_sid = this.ucisection || section_id, + this_opt = this.ucioption || this.option; + + for (var i = 0; i < this.section.children.length; i++) { + var sibling = this.section.children[i]; + + if (sibling === this || sibling.ucioption == null) + continue; + + var sibling_cfg = sibling.uciconfig || sibling.section.uciconfig || sibling.map.config, + sibling_sid = sibling.ucisection || section_id, + sibling_opt = sibling.ucioption || sibling.option; + + if (this_cfg != sibling_cfg || this_sid != sibling_sid || this_opt != sibling_opt) + continue; + + if (!sibling.isActive(section_id)) + continue; + + /* found another active option aliasing the same uci option name, + * so we can't remove the value */ + return; + } + + this.map.data.unset(this_cfg, this_sid, this_opt); } }); @@ -2065,7 +2244,7 @@ var CBITypedSection = CBIAbstractSection.extend(/** @lends LuCI.form.TypedSectio }); if (this.title != null && this.title != '') - sectionEl.appendChild(E('legend', {}, this.title)); + sectionEl.appendChild(E('h3', {}, this.title)); if (this.description != null && this.description != '') sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description)); @@ -2306,7 +2485,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p 'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null, 'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null }), - tableEl = E('div', { + tableEl = E('table', { 'class': 'table cbi-section-table' }); @@ -2324,7 +2503,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p if (sectionname == null) sectionname = cfgsections[i]; - var trEl = E('div', { + var trEl = E('tr', { 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]), 'class': 'tr cbi-section-table-row', 'data-sid': cfgsections[i], @@ -2352,8 +2531,8 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p } if (nodes.length == 0) - tableEl.appendChild(E('div', { 'class': 'tr cbi-section-table-row placeholder' }, - E('div', { 'class': 'td' }, + tableEl.appendChild(E('tr', { 'class': 'tr cbi-section-table-row placeholder' }, + E('td', { 'class': 'td' }, E('em', {}, _('This section contains no values yet'))))); sectionEl.appendChild(tableEl); @@ -2383,7 +2562,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p } if (has_titles) { - var trEl = E('div', { + var trEl = E('tr', { 'class': 'tr cbi-section-table-titles ' + anon_class, 'data-title': (!this.anonymous || this.sectiontitle) ? _('Name') : null }); @@ -2392,7 +2571,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p if (opt.modalonly) continue; - trEl.appendChild(E('div', { + trEl.appendChild(E('th', { 'class': 'th cbi-section-table-cell', 'data-widget': opt.__name__ })); @@ -2412,7 +2591,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p } if (this.sortable || this.extedit || this.addremove || has_more || has_action) - trEl.appendChild(E('div', { + trEl.appendChild(E('th', { 'class': 'th cbi-section-table-cell cbi-section-actions' })); @@ -2420,7 +2599,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p } if (has_descriptions && !this.nodescriptions) { - var trEl = E('div', { + var trEl = E('tr', { 'class': 'tr cbi-section-table-descr ' + anon_class }); @@ -2428,7 +2607,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p if (opt.modalonly) continue; - trEl.appendChild(E('div', { + trEl.appendChild(E('th', { 'class': 'th cbi-section-table-cell', 'data-widget': opt.__name__ }, opt.description)); @@ -2439,7 +2618,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p } if (this.sortable || this.extedit || this.addremove || has_more || has_action) - trEl.appendChild(E('div', { + trEl.appendChild(E('th', { 'class': 'th cbi-section-table-cell cbi-section-actions' })); @@ -2456,7 +2635,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p if (!this.sortable && !this.extedit && !this.addremove && !more_label) return E([]); - var tdEl = E('div', { + var tdEl = E('td', { 'class': 'td cbi-section-table-cell nowrap cbi-section-actions' }, E('div')); @@ -2614,7 +2793,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p /** @private */ handleModalSave: function(modalMap, ev) { - return modalMap.save() + return modalMap.save(null, true) .then(L.bind(this.map.load, this.map)) .then(L.bind(this.map.reset, this.map)) .then(ui.hideModal) @@ -2844,7 +3023,7 @@ var CBIGridSection = CBITableSection.extend(/** @lends LuCI.form.GridSection.pro descr = this.stripTags(opt.description).trim(), value = opt.textvalue(section_id); - return E('div', { + return E('td', { 'class': 'td cbi-value-field', 'data-title': (title != '') ? title : null, 'data-description': (descr != '') ? descr : null, @@ -2984,7 +3163,7 @@ var CBINamedSection = CBIAbstractSection.extend(/** @lends LuCI.form.NamedSectio }); if (typeof(this.title) === 'string' && this.title !== '') - sectionEl.appendChild(E('legend', {}, this.title)); + sectionEl.appendChild(E('h3', {}, this.title)); if (typeof(this.description) === 'string' && this.description !== '') sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description)); @@ -3114,6 +3293,20 @@ var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ { }, /** @private */ + handleValueChange: function(section_id, state, ev) { + if (typeof(this.onchange) != 'function') + return; + + var value = this.formvalue(section_id); + + if (isEqual(value, state.previousValue)) + return; + + state.previousValue = value; + this.onchange.call(this, ev, section_id, value); + }, + + /** @private */ renderFrame: function(section_id, in_table, option_index, nodes) { var config_name = this.uciconfig || this.section.uciconfig || this.map.config, depend_list = this.transformDepList(section_id), @@ -3121,7 +3314,7 @@ var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ { if (in_table) { var title = this.stripTags(this.title).trim(); - optionEl = E('div', { + optionEl = E('td', { 'class': 'td cbi-value-field', 'data-title': (title != '') ? title : null, 'data-description': this.stripTags(this.description).trim(), @@ -3185,6 +3378,9 @@ var CBIValue = CBIAbstractValue.extend(/** @lends LuCI.form.Value.prototype */ { optionEl.addEventListener('widget-change', L.bind(this.map.checkDepends, this.map)); + optionEl.addEventListener('widget-change', + L.bind(this.handleValueChange, this, section_id, {})); + dom.bindClassInstance(optionEl, this); return optionEl; @@ -3431,13 +3627,47 @@ var CBIFlagValue = CBIValue.extend(/** @lends LuCI.form.FlagValue.prototype */ { * @default 0 */ + /** + * Set a tooltip for the flag option. + * + * If set to a string, it will be used as-is as a tooltip. + * + * If set to a function, the function will be invoked and the return + * value will be shown as a tooltip. If the return value of the function + * is `null` no tooltip will be set. + * + * @name LuCI.form.TypedSection.prototype#tooltip + * @type string|function + * @default null + */ + + /** + * Set a tooltip icon. + * + * If set, this icon will be shown for the default one. + * This could also be a png icon from the resources directory. + * + * @name LuCI.form.TypedSection.prototype#tooltipicon + * @type string + * @default 'ℹ️'; + */ + /** @private */ renderWidget: function(section_id, option_index, cfgvalue) { + var tooltip = null; + + if (typeof(this.tooltip) == 'function') + tooltip = this.tooltip.apply(this, [section_id]); + else if (typeof(this.tooltip) == 'string') + tooltip = (arguments.length > 1) ? ''.format.apply(this.tooltip, this.varargs(arguments, 1)) : this.tooltip; + var widget = new ui.Checkbox((cfgvalue != null) ? cfgvalue : this.default, { id: this.cbid(section_id), value_enabled: this.enabled, value_disabled: this.disabled, validate: L.bind(this.validate, this, section_id), + tooltip: tooltip, + tooltipicon: this.tooltipicon, disabled: (this.readonly != null) ? this.readonly : this.map.readonly }); @@ -3724,11 +3954,21 @@ var CBIDummyValue = CBIValue.extend(/** @lends LuCI.form.DummyValue.prototype */ * @default null */ + /** + * Render the UCI option value as hidden using the HTML display: none style property. + * + * By default, the value is displayed + * + * @name LuCI.form.DummyValue.prototype#hidden + * @type boolean + * @default null + */ + /** @private */ renderWidget: function(section_id, option_index, cfgvalue) { var value = (cfgvalue != null) ? cfgvalue : this.default, hiddenEl = new ui.Hiddenfield(value, { id: this.cbid(section_id) }), - outputEl = E('div'); + outputEl = E('div', { 'style': this.hidden ? 'display:none' : null }); if (this.href && !((this.readonly != null) ? this.readonly : this.map.readonly)) outputEl.appendChild(E('a', { 'href': this.href })); diff --git a/modules/luci-base/htdocs/luci-static/resources/luci.js b/modules/luci-base/htdocs/luci-static/resources/luci.js index 83c2807d77..23853e2cc8 100644 --- a/modules/luci-base/htdocs/luci-static/resources/luci.js +++ b/modules/luci-base/htdocs/luci-static/resources/luci.js @@ -672,12 +672,12 @@ * if it is an object, it will be converted to JSON, in all other * cases it is converted to a string. * - * @property {Object<string, string>} [header] - * Specifies HTTP headers to set for the request. - * - * @property {function} [progress] - * An optional request callback function which receives ProgressEvent - * instances as sole argument during the HTTP request transfer. + * @property {Object<string, string>} [header] + * Specifies HTTP headers to set for the request. + * + * @property {function} [progress] + * An optional request callback function which receives ProgressEvent + * instances as sole argument during the HTTP request transfer. */ /** @@ -982,12 +982,13 @@ if (!Poll.active()) return; + var res_json = null; try { - callback(res, res.json(), res.duration); - } - catch (err) { - callback(res, null, res.duration); + res_json = res.json(); } + catch (err) {} + + callback(res, res_json, res.duration); }); }; @@ -2553,10 +2554,16 @@ rpcBaseURL = Session.getLocalData('rpcBaseURL'); if (rpcBaseURL == null) { + var msg = { + jsonrpc: '2.0', + id: 'init', + method: 'list', + params: undefined + }; var rpcFallbackURL = this.url('admin/ubus'); - rpcBaseURL = Request.get(env.ubuspath).then(function(res) { - return (rpcBaseURL = (res.status == 400) ? env.ubuspath : rpcFallbackURL); + rpcBaseURL = Request.post(env.ubuspath, msg, { nobatch: true }).then(function(res) { + return (rpcBaseURL = res.status == 200 ? env.ubuspath : rpcFallbackURL); }, function() { return (rpcBaseURL = rpcFallbackURL); }).then(function(url) { @@ -2965,7 +2972,12 @@ }).filter(function(e) { return (e[1] != null); }).sort(function(a, b) { - return (a[1] > b[1]); + if (a[1] < b[1]) + return -1; + else if (a[1] > b[1]) + return 1; + else + return 0; }).map(function(e) { return e[0]; }); diff --git a/modules/luci-base/htdocs/luci-static/resources/network.js b/modules/luci-base/htdocs/luci-static/resources/network.js index 8d825d73a0..17dd055e25 100644 --- a/modules/luci-base/htdocs/luci-static/resources/network.js +++ b/modules/luci-base/htdocs/luci-static/resources/network.js @@ -101,6 +101,15 @@ var _init = null, _protocols = {}, _protospecs = {}; +function strcmp(a, b) { + if (a > b) + return 1; + else if (a < b) + return -1; + else + return 0; +} + function getProtocolHandlers(cache) { return callNetworkProtoHandlers().then(function(protos) { /* Register "none" protocol */ @@ -384,12 +393,15 @@ function initNetworkState(refresh) { name: name, rawname: name, flags: dev.flags, + link: dev.link, stats: dev.stats, macaddr: dev.mac, type: dev.type, + devtype: dev.devtype, mtu: dev.mtu, qlen: dev.qlen, wireless: dev.wireless, + parent: dev.parent, ipaddrs: [], ip6addrs: [] }; @@ -430,6 +442,16 @@ function initNetworkState(refresh) { s.isBridge[name] = true; } + for (var name in luci_devs) { + var dev = luci_devs[name]; + + if (!dev.parent || dev.devtype != 'dsa') + continue; + + s.isSwitch[dev.parent] = true; + s.isSwitch[name] = true; + } + if (L.isObject(board_json.switch)) { for (var switchname in board_json.switch) { var layout = board_json.switch[switchname], @@ -540,7 +562,7 @@ function ifnameOf(obj) { } function networkSort(a, b) { - return a.getName() > b.getName(); + return strcmp(a.getName(), b.getName()); } function deviceSort(a, b) { @@ -551,7 +573,7 @@ function deviceSort(a, b) { if (weightA != weightB) return weightA - weightB; - return a.getName() > b.getName(); + return strcmp(a.getName(), b.getName()); } function formatWifiEncryption(enc) { @@ -1209,6 +1231,59 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ { } } + /* find bridge VLAN devices */ + var uciBridgeVLANs = uci.sections('network', 'bridge-vlan'); + for (var i = 0; i < uciBridgeVLANs.length; i++) { + var basedev = uciBridgeVLANs[i].device, + local = uciBridgeVLANs[i].local, + alias = uciBridgeVLANs[i].alias, + vid = +uciBridgeVLANs[i].vlan, + ports = L.toArray(uciBridgeVLANs[i].ports); + + if (local == '0') + continue; + + if (isNaN(vid) || vid < 0 || vid > 4095) + continue; + + var vlandev = '%s.%s'.format(basedev, alias || vid); + + _state.isBridge[basedev] = true; + + if (!_state.bridges.hasOwnProperty(basedev)) + _state.bridges[basedev] = { + name: basedev, + ifnames: [] + }; + + if (!devices.hasOwnProperty(vlandev)) + devices[vlandev] = this.instantiateDevice(vlandev); + + ports.forEach(function(port_name) { + var m = port_name.match(/^([^:]+)(?::[ut*]+)?$/), + p = m ? m[1] : null; + + if (!p) + return; + + if (_state.bridges[basedev].ifnames.filter(function(sd) { return sd.name == p }).length) + return; + + _state.netdevs[p] = _state.netdevs[p] || { + name: p, + ipaddrs: [], + ip6addrs: [], + type: 1, + devtype: 'ethernet', + stats: {}, + flags: {} + }; + + _state.bridges[basedev].ifnames.push(_state.netdevs[p]); + _state.netdevs[p].bridge = _state.bridges[basedev]; + }); + } + /* find wireless interfaces */ var uciWifiIfaces = uci.sections('wireless', 'wifi-iface'), networkCount = {}; @@ -1224,6 +1299,22 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ { devices[netid] = this.instantiateDevice(netid); } + /* find uci declared devices */ + var uciDevices = uci.sections('network', 'device'); + + for (var i = 0; i < uciDevices.length; i++) { + var type = uciDevices[i].type, + name = uciDevices[i].name; + + if (!type || !name || devices.hasOwnProperty(name)) + continue; + + if (type == 'bridge') + _state.isBridge[name] = true; + + devices[name] = this.instantiateDevice(name); + } + var rv = []; for (var netdev in devices) @@ -1340,7 +1431,7 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ { rv.push(this.lookupWifiNetwork(wifiIfaces[i]['.name'])); rv.sort(function(a, b) { - return (a.getID() > b.getID()); + return strcmp(a.getID(), b.getID()); }); return rv; @@ -1437,6 +1528,13 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ { } } + rv.sort(function(a, b) { + if (a.metric != b.metric) + return (a.metric - b.metric); + + return strcmp(a.interface, b.interface); + }); + return rv; }, this)); }, @@ -1712,7 +1810,9 @@ Hosts = baseclass.extend(/** @lends LuCI.network.Hosts.prototype */ { * the corresponding host. */ getHostnameByMACAddr: function(mac) { - return this.hosts[mac] ? this.hosts[mac].name : null; + return this.hosts[mac] + ? (this.hosts[mac].name || null) + : null; }, /** @@ -1727,7 +1827,9 @@ Hosts = baseclass.extend(/** @lends LuCI.network.Hosts.prototype */ { * the corresponding host. */ getIPAddrByMACAddr: function(mac) { - return this.hosts[mac] ? this.hosts[mac].ipv4 : null; + return this.hosts[mac] + ? (L.toArray(this.hosts[mac].ipaddrs || this.hosts[mac].ipv4)[0] || null) + : null; }, /** @@ -1742,7 +1844,9 @@ Hosts = baseclass.extend(/** @lends LuCI.network.Hosts.prototype */ { * the corresponding host. */ getIP6AddrByMACAddr: function(mac) { - return this.hosts[mac] ? this.hosts[mac].ipv6 : null; + return this.hosts[mac] + ? (L.toArray(this.hosts[mac].ip6addrs || this.hosts[mac].ipv6)[0] || null) + : null; }, /** @@ -1757,9 +1861,17 @@ Hosts = baseclass.extend(/** @lends LuCI.network.Hosts.prototype */ { * the corresponding host. */ getHostnameByIPAddr: function(ipaddr) { - for (var mac in this.hosts) - if (this.hosts[mac].ipv4 == ipaddr && this.hosts[mac].name != null) - return this.hosts[mac].name; + for (var mac in this.hosts) { + if (this.hosts[mac].name == null) + continue; + + var addrs = L.toArray(this.hosts[mac].ipaddrs || this.hosts[mac].ipv4); + + for (var i = 0; i < addrs.length; i++) + if (addrs[i] == ipaddr) + return this.hosts[mac].name; + } + return null; }, @@ -1775,16 +1887,21 @@ Hosts = baseclass.extend(/** @lends LuCI.network.Hosts.prototype */ { * the corresponding host. */ getMACAddrByIPAddr: function(ipaddr) { - for (var mac in this.hosts) - if (this.hosts[mac].ipv4 == ipaddr) - return mac; + for (var mac in this.hosts) { + var addrs = L.toArray(this.hosts[mac].ipaddrs || this.hosts[mac].ipv4); + + for (var i = 0; i < addrs.length; i++) + if (addrs[i] == ipaddr) + return mac; + } + return null; }, /** * Lookup the hostname associated with the given IPv6 address. * - * @param {string} ipaddr + * @param {string} ip6addr * The IPv6 address to lookup. * * @returns {null|string} @@ -1793,16 +1910,24 @@ Hosts = baseclass.extend(/** @lends LuCI.network.Hosts.prototype */ { * the corresponding host. */ getHostnameByIP6Addr: function(ip6addr) { - for (var mac in this.hosts) - if (this.hosts[mac].ipv6 == ip6addr && this.hosts[mac].name != null) - return this.hosts[mac].name; + for (var mac in this.hosts) { + if (this.hosts[mac].name == null) + continue; + + var addrs = L.toArray(this.hosts[mac].ip6addrs || this.hosts[mac].ipv6); + + for (var i = 0; i < addrs.length; i++) + if (addrs[i] == ip6addr) + return this.hosts[mac].name; + } + return null; }, /** * Lookup the MAC address associated with the given IPv6 address. * - * @param {string} ipaddr + * @param {string} ip6addr * The IPv6 address to lookup. * * @returns {null|string} @@ -1811,9 +1936,14 @@ Hosts = baseclass.extend(/** @lends LuCI.network.Hosts.prototype */ { * the corresponding host. */ getMACAddrByIP6Addr: function(ip6addr) { - for (var mac in this.hosts) - if (this.hosts[mac].ipv6 == ip6addr) - return mac; + for (var mac in this.hosts) { + var addrs = L.toArray(this.hosts[mac].ip6addrs || this.hosts[mac].ipv6); + + for (var i = 0; i < addrs.length; i++) + if (addrs[i] == ip6addr) + return mac; + } + return null; }, @@ -1840,14 +1970,18 @@ Hosts = baseclass.extend(/** @lends LuCI.network.Hosts.prototype */ { */ getMACHints: function(preferIp6) { var rv = []; + for (var mac in this.hosts) { var hint = this.hosts[mac].name || - this.hosts[mac][preferIp6 ? 'ipv6' : 'ipv4'] || - this.hosts[mac][preferIp6 ? 'ipv4' : 'ipv6']; + L.toArray(this.hosts[mac][preferIp6 ? 'ip6addrs' : 'ipaddrs'] || this.hosts[mac][preferIp6 ? 'ipv6' : 'ipv4'])[0] || + L.toArray(this.hosts[mac][preferIp6 ? 'ipaddrs' : 'ip6addrs'] || this.hosts[mac][preferIp6 ? 'ipv4' : 'ipv6'])[0]; rv.push([mac, hint]); } - return rv.sort(function(a, b) { return a[0] > b[0] }); + + return rv.sort(function(a, b) { + return strcmp(a[0], b[0]); + }); } }); @@ -2403,14 +2537,14 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ { * * Alias interfaces are interfaces layering on top of another interface * and are denoted by a special `@interfacename` notation in the - * underlying `ifname` option. + * underlying `device` option. * * @returns {null|string} * Returns the name of the parent interface if this logical interface * is an alias or `null` if it is not an alias interface. */ isAlias: function() { - var ifnames = L.toArray(uci.get('network', this.sid, 'ifname')), + var ifnames = L.toArray(uci.get('network', this.sid, 'device')), parent = null; for (var i = 0; i < ifnames.length; i++) @@ -2434,9 +2568,9 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ { return false; var empty = true, - ifname = this._get('ifname'); + device = this._get('device'); - if (ifname != null && ifname.match(/\S+/)) + if (device != null && device.match(/\S+/)) empty = false; if (empty == true && getWifiNetidBySid(this.sid) != null) @@ -2468,18 +2602,18 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ { * argument was invalid, if the device was already part of the logical * interface or if the logical interface is virtual. */ - addDevice: function(ifname) { - ifname = ifnameOf(ifname); + addDevice: function(device) { + device = ifnameOf(device); - if (ifname == null || this.isFloating()) + if (device == null || this.isFloating()) return false; - var wif = getWifiSidByIfname(ifname); + var wif = getWifiSidByIfname(device); if (wif != null) return appendValue('wireless', wif, 'network', this.sid); - return appendValue('network', this.sid, 'ifname', ifname); + return appendValue('network', this.sid, 'device', device); }, /** @@ -2495,20 +2629,20 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ { * argument was invalid, if the device was already part of the logical * interface or if the logical interface is virtual. */ - deleteDevice: function(ifname) { + deleteDevice: function(device) { var rv = false; - ifname = ifnameOf(ifname); + device = ifnameOf(device); - if (ifname == null || this.isFloating()) + if (device == null || this.isFloating()) return false; - var wif = getWifiSidByIfname(ifname); + var wif = getWifiSidByIfname(device); if (wif != null) rv = removeValue('wireless', wif, 'network', this.sid); - if (removeValue('network', this.sid, 'ifname', ifname)) + if (removeValue('network', this.sid, 'device', device)) rv = true; return rv; @@ -2534,7 +2668,7 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ { return new Device(ifname, this); } else { - var ifnames = L.toArray(uci.get('network', this.sid, 'ifname')); + var ifnames = L.toArray(uci.get('network', this.sid, 'device')); for (var i = 0; i < ifnames.length; i++) { var m = ifnames[i].match(/^([^:/]+)/); @@ -2589,13 +2723,10 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ { if (!this.isBridge() && !(this.isVirtual() && !this.isFloating())) return null; - var ifnames = L.toArray(uci.get('network', this.sid, 'ifname')); - - for (var i = 0; i < ifnames.length; i++) { - if (ifnames[i].charAt(0) == '@') - continue; + var device = uci.get('network', this.sid, 'device'); - var m = ifnames[i].match(/^([^:/]+)/); + if (device && device.charAt(0) != '@') { + var m = device.match(/^([^:/]+)/); if (m != null) rv.push(Network.prototype.instantiateDevice(m[1], this)); } @@ -2637,25 +2768,24 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ { * Returns `true` when this logical interface contains the given network * device or `false` if not. */ - containsDevice: function(ifname) { - ifname = ifnameOf(ifname); + containsDevice: function(device) { + device = ifnameOf(device); - if (ifname == null) + if (device == null) return false; - else if (this.isVirtual() && '%s-%s'.format(this.getProtocol(), this.sid) == ifname) + else if (this.isVirtual() && '%s-%s'.format(this.getProtocol(), this.sid) == device) return true; - else if (this.isBridge() && 'br-%s'.format(this.sid) == ifname) + else if (this.isBridge() && 'br-%s'.format(this.sid) == device) return true; - var ifnames = L.toArray(uci.get('network', this.sid, 'ifname')); - - for (var i = 0; i < ifnames.length; i++) { - var m = ifnames[i].match(/^([^:/]+)/); - if (m != null && m[1] == ifname) + var name = uci.get('network', this.sid, 'device'); + if (name) { + var m = name.match(/^([^:/]+)/); + if (m != null && m[1] == device) return true; } - var wif = getWifiSidByIfname(ifname); + var wif = getWifiSidByIfname(device); if (wif != null) { var networks = L.toArray(uci.get('wireless', wif, 'network')); @@ -2698,19 +2828,19 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ { * device and allows querying device details such as packet statistics or MTU. */ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ { - __init__: function(ifname, network) { - var wif = getWifiSidByIfname(ifname); + __init__: function(device, network) { + var wif = getWifiSidByIfname(device); if (wif != null) { var res = getWifiStateBySid(wif) || [], netid = getWifiNetidBySid(wif) || []; - this.wif = new WifiNetwork(wif, res[0], res[1], netid[0], res[2], { ifname: ifname }); - this.ifname = this.wif.getIfname(); + this.wif = new WifiNetwork(wif, res[0], res[1], netid[0], res[2], { ifname: device }); + this.device = this.wif.getIfname(); } - this.ifname = this.ifname || ifname; - this.dev = _state.netdevs[this.ifname]; + this.device = this.device || device; + this.dev = Object.assign({}, _state.netdevs[this.device]); this.network = network; }, @@ -2733,7 +2863,7 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ { * Returns the name of the device, e.g. `eth0` or `wlan0`. */ getName: function() { - return (this.wif != null ? this.wif.getIfname() : this.ifname); + return (this.wif != null ? this.wif.getIfname() : this.device); }, /** @@ -2781,7 +2911,7 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ { }, /** - * Get the type of the device.. + * Get the type of the device. * * @returns {string} * Returns a string describing the type of the network device: @@ -2794,17 +2924,17 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ { * - `ethernet` for all other device types */ getType: function() { - if (this.ifname != null && this.ifname.charAt(0) == '@') + if (this.device != null && this.device.charAt(0) == '@') return 'alias'; - else if (this.wif != null || isWifiIfname(this.ifname)) + else if (this.dev.devtype == 'wlan' || this.wif != null || isWifiIfname(this.device)) return 'wifi'; - else if (_state.isBridge[this.ifname]) + else if (this.dev.devtype == 'bridge' || _state.isBridge[this.device]) return 'bridge'; - else if (_state.isTunnel[this.ifname]) + else if (_state.isTunnel[this.device]) return 'tunnel'; - else if (this.ifname.indexOf('.') > -1) + else if (this.dev.devtype == 'vlan' || this.device.indexOf('.') > -1) return 'vlan'; - else if (_state.isSwitch[this.ifname]) + else if (this.dev.devtype == 'dsa' || _state.isSwitch[this.device]) return 'switch'; else return 'ethernet'; @@ -2821,7 +2951,7 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ { if (this.wif != null) return this.wif.getShortName(); - return this.ifname; + return this.device; }, /** @@ -2861,10 +2991,11 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ { return _('Bridge'); case 'switch': - return _('Ethernet Switch'); + return (_state.netdevs[this.device] && _state.netdevs[this.device].devtype == 'dsa') + ? _('Switch port') : _('Ethernet Switch'); case 'vlan': - return (_state.isSwitch[this.ifname] ? _('Switch VLAN') : _('Software VLAN')); + return (_state.isSwitch[this.device] ? _('Switch VLAN') : _('Software VLAN')); case 'tunnel': return _('Tunnel Interface'); @@ -2883,7 +3014,7 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ { * a Linux bridge. */ getPorts: function() { - var br = _state.bridges[this.ifname], + var br = _state.bridges[this.device], rv = []; if (br == null || !Array.isArray(br.ifnames)) @@ -2905,7 +3036,7 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ { * device is not a Linux bridge. */ getBridgeID: function() { - var br = _state.bridges[this.ifname]; + var br = _state.bridges[this.device]; return (br != null ? br.id : null); }, @@ -2917,7 +3048,7 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ { * enabled, else `false`. */ getBridgeSTP: function() { - var br = _state.bridges[this.ifname]; + var br = _state.bridges[this.device]; return (br != null ? !!br.stp : false); }, @@ -3004,6 +3135,47 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ { }, /** + * Get the carrier state of the network device. + * + * @returns {boolean} + * Returns true if the device has a carrier, e.g. when a cable is + * inserted into an ethernet port of false if there is none. + */ + getCarrier: function() { + var link = this._devstate('link'); + return (link != null ? link.carrier || false : false); + }, + + /** + * Get the current link speed of the network device if available. + * + * @returns {number|null} + * Returns the current speed of the network device in Mbps. If the + * device supports no ethernet speed levels, null is returned. + * If the device supports ethernet speeds but has no carrier, -1 is + * returned. + */ + getSpeed: function() { + var link = this._devstate('link'); + return (link != null ? link.speed || null : null); + }, + + /** + * Get the current duplex mode of the network device if available. + * + * @returns {string|null} + * Returns the current duplex mode of the network device. Returns + * either "full" or "half" if the device supports duplex modes or + * null if the duplex mode is unknown or unsupported. + */ + getDuplex: function() { + var link = this._devstate('link'), + duplex = link ? link.duplex : null; + + return (duplex != 'unknown') ? duplex : null; + }, + + /** * Get the primary logical interface this device is assigned to. * * @returns {null|LuCI.network.Protocol} @@ -3029,7 +3201,7 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ { var networks = enumerateNetworks.apply(L.network); for (var i = 0; i < networks.length; i++) - if (networks[i].containsDevice(this.ifname) || networks[i].getIfname() == this.ifname) + if (networks[i].containsDevice(this.device) || networks[i].getIfname() == this.device) this.networks.push(networks[i]); this.networks.sort(networkSort); @@ -3048,6 +3220,22 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ { */ getWifiNetwork: function() { return (this.wif != null ? this.wif : null); + }, + + /** + * Get the logical parent device of this device. + * + * In case of DSA switch ports, the parent device will be the DSA switch + * device itself, for VLAN devices, the parent refers to the base device + * etc. + * + * @returns {null|LuCI.network.Device} + * Returns a `Network.Device` instance representing the parent device or + * `null` when this device has no parent, as it is the case for e.g. + * ordinary ethernet interfaces. + */ + getParent: function() { + return this.dev.parent ? Network.prototype.instantiateDevice(this.dev.parent) : null; } }); @@ -3157,6 +3345,7 @@ WifiDevice = baseclass.extend(/** @lends LuCI.network.WifiDevice.prototype */ { * - `g` - Legacy 802.11g mode, 2.4 GHz, up to 54 Mbit/s * - `n` - IEEE 802.11n mode, 2.4 or 5 GHz, up to 600 Mbit/s * - `ac` - IEEE 802.11ac mode, 5 GHz, up to 6770 Mbit/s + * - `ax` - IEEE 802.11ax mode, 2.4 or 5 GHz */ getHWModes: function() { var hwmodes = this.ubus('dev', 'iwinfo', 'hwmodes'); @@ -3178,6 +3367,10 @@ WifiDevice = baseclass.extend(/** @lends LuCI.network.WifiDevice.prototype */ { * - `VHT40` - applicable to IEEE 802.11ac, 40 MHz wide channels * - `VHT80` - applicable to IEEE 802.11ac, 80 MHz wide channels * - `VHT160` - applicable to IEEE 802.11ac, 160 MHz wide channels + * - `HE20` - applicable to IEEE 802.11ax, 20 MHz wide channels + * - `HE40` - applicable to IEEE 802.11ax, 40 MHz wide channels + * - `HE80` - applicable to IEEE 802.11ax, 80 MHz wide channels + * - `HE160` - applicable to IEEE 802.11ax, 160 MHz wide channels */ getHTModes: function() { var htmodes = this.ubus('dev', 'iwinfo', 'htmodes'); @@ -3201,7 +3394,10 @@ WifiDevice = baseclass.extend(/** @lends LuCI.network.WifiDevice.prototype */ { modestr = ''; hwmodes.sort(function(a, b) { - return (a.length != b.length ? a.length > b.length : a > b); + if (a.length != b.length) + return a.length - b.length; + + return strcmp(a, b); }); modestr = hwmodes.join(''); @@ -3555,6 +3751,24 @@ WifiNetwork = baseclass.extend(/** @lends LuCI.network.WifiNetwork.prototype */ }, /** + * Get the Linux VLAN network device names. + * + * @returns {string[]} + * Returns the current Linux VLAN network device name as resolved + * from `ubus` runtime information or empty array if this network + * has no associated VLAN network devices. + */ + getVlanIfnames: function() { + var vlans = L.toArray(this.ubus('net', 'vlans')), + ifnames = []; + + for (var i = 0; i < vlans.length; i++) + ifnames.push(vlans[i]['ifname']); + + return ifnames; + }, + + /** * Get the name of the corresponding wifi radio device. * * @returns {null|string} @@ -3854,6 +4068,17 @@ WifiNetwork = baseclass.extend(/** @lends LuCI.network.WifiNetwork.prototype */ * @property {number} [nss] * Specifies the number of spatial streams used by the transmission. * Only applicable to VHT rates. + * + * @property {boolean} [he] + * Specifies whether this rate is an HE (IEEE 802.11ax) rate. + * + * @property {number} [he_gi] + * Specifies whether the guard interval used for the transmission. + * Only applicable to HE rates. + * + * @property {number} [he_dcm] + * Specifies whether dual concurrent modulation is used for the transmission. + * Only applicable to HE rates. */ /** @@ -3864,7 +4089,15 @@ WifiNetwork = baseclass.extend(/** @lends LuCI.network.WifiNetwork.prototype */ * with this network. */ getAssocList: function() { - return callIwinfoAssoclist(this.getIfname()); + var tasks = []; + var ifnames = [ this.getIfname() ].concat(this.getVlanIfnames()); + + for (var i = 0; i < ifnames.length; i++) + tasks.push(callIwinfoAssoclist(ifnames[i])); + + return Promise.all(tasks).then(function(values) { + return Array.prototype.concat.apply([], values); + }); }, /** diff --git a/modules/luci-base/htdocs/luci-static/resources/protocol/dhcp.js b/modules/luci-base/htdocs/luci-static/resources/protocol/dhcp.js index f0a3ec579c..71adc235ca 100644 --- a/modules/luci-base/htdocs/luci-static/resources/protocol/dhcp.js +++ b/modules/luci-base/htdocs/luci-static/resources/protocol/dhcp.js @@ -17,10 +17,13 @@ return network.registerProtocol('dhcp', { }, renderFormOptions: function(s) { - var dev = this.getL2Device() || this.getDevice(), o; + var o; o = s.taboption('general', form.Value, 'hostname', _('Hostname to send when requesting DHCP')); - o.datatype = 'hostname'; + o.default = ''; + o.value('', _('Send the hostname of this device')); + o.value('*', _('Do not send a hostname')); + o.datatype = 'or(hostname, "*")'; o.load = function(section_id) { return callFileRead('/proc/sys/kernel/hostname').then(L.bind(function(hostname) { this.placeholder = hostname; @@ -31,32 +34,9 @@ return network.registerProtocol('dhcp', { o = s.taboption('advanced', form.Flag, 'broadcast', _('Use broadcast flag'), _('Required for certain ISPs, e.g. Charter with DOCSIS 3')); o.default = o.disabled; - o = s.taboption('advanced', form.Flag, 'defaultroute', _('Use default gateway'), _('If unchecked, no default route is configured')); - o.default = o.enabled; - - o = s.taboption('advanced', form.Flag, 'peerdns', _('Use DNS servers advertised by peer'), _('If unchecked, the advertised DNS server addresses are ignored')); - o.default = o.enabled; - - o = s.taboption('advanced', form.DynamicList, 'dns', _('Use custom DNS servers')); - o.depends('peerdns', '0'); - o.datatype = 'ipaddr'; - o.cast = 'string'; - - o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric')); - o.placeholder = '0'; - o.datatype = 'uinteger'; - o = s.taboption('advanced', form.Value, 'clientid', _('Client ID to send when requesting DHCP')); o.datatype = 'hexstring'; s.taboption('advanced', form.Value, 'vendorid', _('Vendor Class to send when requesting DHCP')); - - o = s.taboption('advanced', form.Value, 'macaddr', _('Override MAC address')); - o.datatype = 'macaddr'; - o.placeholder = dev ? (dev.getMAC() || '') : ''; - - o = s.taboption('advanced', form.Value, 'mtu', _('Override MTU')); - o.placeholder = dev ? (dev.getMTU() || '1500') : '1500'; - o.datatype = 'max(9200)'; } }); diff --git a/modules/luci-base/htdocs/luci-static/resources/protocol/static.js b/modules/luci-base/htdocs/luci-static/resources/protocol/static.js index 2d70ae681f..42ebcefba4 100644 --- a/modules/luci-base/htdocs/luci-static/resources/protocol/static.js +++ b/modules/luci-base/htdocs/luci-static/resources/protocol/static.js @@ -173,34 +173,12 @@ return network.registerProtocol('static', { }, renderFormOptions: function(s) { - var dev = this.getL2Device() || this.getDevice(), o; + var o; s.taboption('general', this.CBIIPValue, 'ipaddr', _('IPv4 address')); s.taboption('general', this.CBINetmaskValue, 'netmask', _('IPv4 netmask')); s.taboption('general', this.CBIGatewayValue, 'gateway', _('IPv4 gateway')); s.taboption('general', this.CBIBroadcastValue, 'broadcast', _('IPv4 broadcast')); - s.taboption('general', form.DynamicList, 'dns', _('Use custom DNS servers')); - - o = s.taboption('general', form.Value, 'ip6assign', _('IPv6 assignment length'), _('Assign a part of given length of every public IPv6-prefix to this interface')); - o.value('', _('disabled')); - o.value('64'); - o.datatype = 'max(64)'; - - o = s.taboption('general', form.Value, 'ip6hint', _('IPv6 assignment hint'), _('Assign prefix parts using this hexadecimal subprefix ID for this interface.')); - o.placeholder = '0'; - o.validate = function(section_id, value) { - if (value == null || value == '') - return true; - - var n = parseInt(value, 16); - - if (!/^(0x)?[0-9a-fA-F]+$/.test(value) || isNaN(n) || n >= 0xffffffff) - return _('Expecting a hexadecimal assignment hint'); - - return true; - }; - for (var i = 33; i <= 64; i++) - o.depends('ip6assign', String(i)); o = s.taboption('general', form.DynamicList, 'ip6addr', _('IPv6 address')); o.datatype = 'ip6addr'; @@ -214,21 +192,5 @@ return network.registerProtocol('static', { o = s.taboption('general', form.Value, 'ip6prefix', _('IPv6 routed prefix'), _('Public prefix routed to this device for distribution to clients.')); o.datatype = 'ip6addr'; o.depends('ip6assign', ''); - - o = s.taboption('general', form.Value, 'ip6ifaceid', _('IPv6 suffix'), _("Optional. Allowed values: 'eui64', 'random', fixed value like '::1' or '::1:2'. When IPv6 prefix (like 'a:b:c:d::') is received from a delegating server, use the suffix (like '::1') to form the IPv6 address ('a:b:c:d::1') for the interface.")); - o.datatype = 'ip6hostid'; - o.placeholder = '::1'; - - o = s.taboption('advanced', form.Value, 'macaddr', _('Override MAC address')); - o.datatype = 'macaddr'; - o.placeholder = dev ? (dev.getMAC() || '') : ''; - - o = s.taboption('advanced', form.Value, 'mtu', _('Override MTU')); - o.datatype = 'max(9200)'; - o.placeholder = dev ? (dev.getMTU() || '1500') : '1500'; - - o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric')); - o.placeholder = this.getMetric() || '0'; - o.datatype = 'uinteger'; } }); diff --git a/modules/luci-base/htdocs/luci-static/resources/rpc.js b/modules/luci-base/htdocs/luci-static/resources/rpc.js index 7bfc913367..f37f7bb6a4 100644 --- a/modules/luci-base/htdocs/luci-static/resources/rpc.js +++ b/modules/luci-base/htdocs/luci-static/resources/rpc.js @@ -33,9 +33,6 @@ return baseclass.extend(/** @lends LuCI.rpc.prototype */ { req[i].params[2] ); } - else if (req.params) { - q += '/%s.%s'.format(req.params[1], req.params[2]); - } return request.post(rpcBaseURL + q, req, { timeout: (L.env.rpctimeout || 20) * 1000, diff --git a/modules/luci-base/htdocs/luci-static/resources/tools/prng.js b/modules/luci-base/htdocs/luci-static/resources/tools/prng.js index 752dc75ce8..b916cc7792 100644 --- a/modules/luci-base/htdocs/luci-static/resources/tools/prng.js +++ b/modules/luci-base/htdocs/luci-static/resources/tools/prng.js @@ -89,5 +89,23 @@ return L.Class.extend({ } return Math.floor(r * (u - l + 1)) + l; + }, + + 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); } }); diff --git a/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js b/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js index 7f724a17e4..3f2b7b9ffc 100644 --- a/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js +++ b/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js @@ -393,6 +393,7 @@ var CBINetworkSelect = form.ListValue.extend({ 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: '' + @@ -580,6 +581,8 @@ var CBIUserSelect = form.ListValue.extend({ load: function(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]); } diff --git a/modules/luci-base/htdocs/luci-static/resources/uci.js b/modules/luci-base/htdocs/luci-static/resources/uci.js index 640661f0f5..41e902c5fe 100644 --- a/modules/luci-base/htdocs/luci-static/resources/uci.js +++ b/modules/luci-base/htdocs/luci-static/resources/uci.js @@ -402,7 +402,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ { for (var s in v) if (!d || d[s] !== true) if (!type || v[s]['.type'] == type) - sa.push(Object.assign({ }, v[s], c ? c[s] : undefined)); + sa.push(Object.assign({ }, v[s], c ? c[s] : null)); if (n) for (var s in n) @@ -462,7 +462,7 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ { /* requested option in a just created section */ if (n[conf] && n[conf][sid]) { if (!n[conf]) - return undefined; + return null; if (opt == null) return n[conf][sid]; @@ -473,14 +473,9 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ { /* requested an option value */ if (opt != null) { /* check whether option was deleted */ - if (d[conf] && d[conf][sid]) { - if (d[conf][sid] === true) - return undefined; - - for (var i = 0; i < d[conf][sid].length; i++) - if (d[conf][sid][i] == opt) - return undefined; - } + if (d[conf] && d[conf][sid]) + if (d[conf][sid] === true || d[conf][sid][opt]) + return null; /* check whether option was changed */ if (c[conf] && c[conf][sid] && c[conf][sid][opt] != null) @@ -490,14 +485,34 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ { if (v[conf] && v[conf][sid]) return v[conf][sid][opt]; - return undefined; + return null; } /* requested an entire section */ - if (v[conf]) - return v[conf][sid]; + if (v[conf]) { + /* check whether entire section was deleted */ + if (d[conf] && d[conf][sid] === true) + return null; + + var s = v[conf][sid] || null; + + if (s) { + /* merge changes */ + if (c[conf] && c[conf][sid]) + for (var opt in c[conf][sid]) + if (c[conf][sid][opt] != null) + s[opt] = c[conf][sid][opt]; + + /* merge deletions */ + if (d[conf] && d[conf][sid]) + for (var opt in d[conf][sid]) + delete s[opt]; + } - return undefined; + return s; + } + + return null; }, /** @@ -555,28 +570,39 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ { /* undelete option */ if (d[conf] && d[conf][sid]) { - d[conf][sid] = d[conf][sid].filter(function(o) { return o !== opt }); + var empty = true; + + for (var key in d[conf][sid]) { + if (key != opt && d[conf][sid].hasOwnProperty(key)) { + empty = false; + break; + } + } - if (d[conf][sid].length == 0) + if (empty) delete d[conf][sid]; + else + delete d[conf][sid][opt]; } c[conf][sid][opt] = val; } else { - /* only delete in existing sections */ - if (!(v[conf] && v[conf][sid] && v[conf][sid].hasOwnProperty(opt)) && - !(c[conf] && c[conf][sid] && c[conf][sid].hasOwnProperty(opt))) - return; + /* revert any change for to-be-deleted option */ + if (c[conf] && c[conf][sid]) + delete c[conf][sid][opt]; - if (!d[conf]) - d[conf] = { }; + /* only delete existing options */ + if (v[conf] && v[conf][sid] && v[conf][sid].hasOwnProperty(opt)) { + if (!d[conf]) + d[conf] = { }; - if (!d[conf][sid]) - d[conf][sid] = [ ]; + if (!d[conf][sid]) + d[conf][sid] = { }; - if (d[conf][sid] !== true) - d[conf][sid].push(opt); + if (d[conf][sid] !== true) + d[conf][sid][opt] = true; + } } }, @@ -796,7 +822,11 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ { for (var conf in d) { for (var sid in d[conf]) { var o = d[conf][sid]; - tasks.push(self.callDelete(conf, sid, (o === true) ? null : o)); + + if (o === true) + tasks.push(self.callDelete(conf, sid, null)); + else + tasks.push(self.callDelete(conf, sid, Object.keys(o))); } pkgs[conf] = true; diff --git a/modules/luci-base/htdocs/luci-static/resources/ui.js b/modules/luci-base/htdocs/luci-static/resources/ui.js index 91a93b9e90..0e909b6dcc 100644 --- a/modules/luci-base/htdocs/luci-static/resources/ui.js +++ b/modules/luci-base/htdocs/luci-static/resources/ui.js @@ -102,6 +102,47 @@ var UIElement = baseclass.extend(/** @lends LuCI.ui.AbstractElement.prototype */ }, /** + * Set the current placeholder value of the input widget. + * + * @instance + * @memberof LuCI.ui.AbstractElement + * @param {string|string[]|null} value + * The placeholder to set for the input element. Only applicable to text + * inputs, not to radio buttons, selects or similar. + */ + setPlaceholder: function(value) { + var node = this.node ? this.node.querySelector('input,textarea') : null; + if (node) { + switch (node.getAttribute('type') || 'text') { + case 'password': + case 'search': + case 'tel': + case 'text': + case 'url': + if (value != null && value != '') + node.setAttribute('placeholder', value); + else + node.removeAttribute('placeholder'); + } + } + }, + + /** + * Check whether the input value was altered by the user. + * + * @instance + * @memberof LuCI.ui.AbstractElement + * @returns {boolean} + * Returns `true` if the input value has been altered by the user or + * `false` if it is unchaged. Note that if the user modifies the initial + * value and changes it back to the original state, it is still reported + * as changed. + */ + isChanged: function() { + return (this.node ? this.node.getAttribute('data-changed') : null) == 'true'; + }, + + /** * Check whether the current input value is valid. * * @instance @@ -569,6 +610,22 @@ var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ { frameEl.appendChild(E('label', { 'for': id })); + if (this.options.tooltip != null) { + var icon = "⚠️"; + + if (this.options.tooltipicon != null) + icon = this.options.tooltipicon; + + frameEl.appendChild( + E('label', { 'class': 'cbi-tooltip-container' },[ + icon, + E('div', { 'class': 'cbi-tooltip' }, + this.options.tooltip + ) + ]) + ); + } + return this.bind(frameEl); }, @@ -576,8 +633,9 @@ var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ { bind: function(frameEl) { this.node = frameEl; - this.setUpdateEvents(frameEl.lastElementChild.previousElementSibling, 'click', 'blur'); - this.setChangeEvents(frameEl.lastElementChild.previousElementSibling, 'change'); + var input = frameEl.querySelector('input[type="checkbox"]'); + this.setUpdateEvents(input, 'click', 'blur'); + this.setChangeEvents(input, 'change'); dom.bindClassInstance(frameEl, this); @@ -593,7 +651,7 @@ var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ { * Returns `true` when the checkbox is currently checked, otherwise `false`. */ isChecked: function() { - return this.node.lastElementChild.previousElementSibling.checked; + return this.node.querySelector('input[type="checkbox"]').checked; }, /** @override */ @@ -605,7 +663,7 @@ var UICheckbox = UIElement.extend(/** @lends LuCI.ui.Checkbox.prototype */ { /** @override */ setValue: function(value) { - this.node.lastElementChild.previousElementSibling.checked = (value == this.options.value_enabled); + this.node.querySelector('input[type="checkbox"]').checked = (value == this.options.value_enabled); } }); @@ -1144,6 +1202,28 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ { }, /** @private */ + getScrollParent: function(element) { + var parent = element, + style = getComputedStyle(element), + excludeStaticParent = (style.position === 'absolute'); + + if (style.position === 'fixed') + return document.body; + + while ((parent = parent.parentElement) != null) { + style = getComputedStyle(parent); + + if (excludeStaticParent && style.position === 'static') + continue; + + if (/(auto|scroll)/.test(style.overflow + style.overflowY + style.overflowX)) + return parent; + } + + return document.body; + }, + + /** @private */ openDropdown: function(sb) { var st = window.getComputedStyle(sb, null), ul = sb.querySelector('ul'), @@ -1151,7 +1231,8 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ { fl = findParent(sb, '.cbi-value-field'), sel = ul.querySelector('[selected]'), rect = sb.getBoundingClientRect(), - items = Math.min(this.options.dropdown_items, li.length); + items = Math.min(this.options.dropdown_items, li.length), + scrollParent = this.getScrollParent(sb); document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) { s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {})); @@ -1176,29 +1257,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ { ul.style.maxHeight = (vpHeight * 0.5) + 'px'; ul.style.WebkitOverflowScrolling = 'touch'; - var getScrollParent = function(element) { - var parent = element, - style = getComputedStyle(element), - excludeStaticParent = (style.position === 'absolute'); - - if (style.position === 'fixed') - return document.body; - - while ((parent = parent.parentElement) != null) { - style = getComputedStyle(parent); - - if (excludeStaticParent && style.position === 'static') - continue; - - if (/(auto|scroll)/.test(style.overflow + style.overflowY + style.overflowX)) - return parent; - } - - return document.body; - } - - var scrollParent = getScrollParent(sb), - scrollFrom = scrollParent.scrollTop, + var scrollFrom = scrollParent.scrollTop, scrollTo = scrollFrom + rect.top - vpHeight * 0.5; var scrollStep = function(timestamp) { @@ -1224,10 +1283,11 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ { ul.style.top = ul.style.bottom = ''; window.requestAnimationFrame(function() { - var itemHeight = li[Math.max(0, li.length - 2)].getBoundingClientRect().height, + var containerRect = scrollParent.getBoundingClientRect(), + itemHeight = li[Math.max(0, li.length - 2)].getBoundingClientRect().height, fullHeight = 0, - spaceAbove = rect.top, - spaceBelow = window.innerHeight - rect.height - rect.top; + spaceAbove = rect.top - containerRect.top, + spaceBelow = containerRect.bottom - rect.bottom; for (var i = 0; i < (items == -1 ? li.length : items); i++) fullHeight += li[i].getBoundingClientRect().height; @@ -1303,6 +1363,8 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ { /** @private */ toggleItem: function(sb, li, force_state) { + var ul = li.parentNode; + if (li.hasAttribute('unselectable')) return; @@ -1379,7 +1441,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ { this.closeDropdown(sb, true); } - this.saveValues(sb, li.parentNode); + this.saveValues(sb, ul); }, /** @private */ @@ -3556,9 +3618,11 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ { this.setActiveTabId(panes[selected], selected); } - panes[selected].dispatchEvent(new CustomEvent('cbi-tab-active', { - detail: { tab: panes[selected].getAttribute('data-tab') } - })); + requestAnimationFrame(L.bind(function(pane) { + pane.dispatchEvent(new CustomEvent('cbi-tab-active', { + detail: { tab: pane.getAttribute('data-tab') } + })); + }, this, panes[selected])); this.updateTabs(group); }, @@ -4001,7 +4065,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ { E('button', { 'class': 'btn', 'click': UI.prototype.hideModal - }, [ _('Dismiss') ]), ' ', + }, [ _('Close') ]), ' ', E('button', { 'class': 'cbi-button cbi-button-positive important', 'click': L.bind(this.apply, this, true) diff --git a/modules/luci-base/htdocs/luci-static/resources/validation.js b/modules/luci-base/htdocs/luci-static/resources/validation.js index d47392c239..28042ba8cd 100644 --- a/modules/luci-base/htdocs/luci-static/resources/validation.js +++ b/modules/luci-base/htdocs/luci-static/resources/validation.js @@ -272,18 +272,21 @@ var ValidatorFactory = baseclass.extend({ _('valid IPv6 prefix value (0-128)')); }, - cidr: function() { - return this.assert(this.apply('cidr4') || this.apply('cidr6'), _('valid IPv4 or IPv6 CIDR')); + cidr: function(negative) { + return this.assert(this.apply('cidr4', null, [negative]) || this.apply('cidr6', null, [negative]), + _('valid IPv4 or IPv6 CIDR')); }, - cidr4: function() { - var m = this.value.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,2})$/); - return this.assert(m && this.factory.parseIPv4(m[1]) && this.apply('ip4prefix', m[2]), _('valid IPv4 CIDR')); + cidr4: function(negative) { + var m = this.value.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(-)?(\d{1,2})$/); + return this.assert(m && this.factory.parseIPv4(m[1]) && (negative || !m[2]) && this.apply('ip4prefix', m[3]), + _('valid IPv4 CIDR')); }, - cidr6: function() { - var m = this.value.match(/^([0-9a-fA-F:.]+)\/(\d{1,3})$/); - return this.assert(m && this.factory.parseIPv6(m[1]) && this.apply('ip6prefix', m[2]), _('valid IPv6 CIDR')); + cidr6: function(negative) { + var m = this.value.match(/^([0-9a-fA-F:.]+)\/(-)?(\d{1,3})$/); + return this.assert(m && this.factory.parseIPv6(m[1]) && (negative || !m[2]) && this.apply('ip6prefix', m[3]), + _('valid IPv6 CIDR')); }, ipnet4: function() { @@ -304,18 +307,18 @@ var ValidatorFactory = baseclass.extend({ return this.assert(!(!v6 || v6[0] || v6[1] || v6[2] || v6[3]), _('valid IPv6 host id')); }, - ipmask: function() { - return this.assert(this.apply('ipmask4') || this.apply('ipmask6'), + ipmask: function(negative) { + return this.assert(this.apply('ipmask4', null, [negative]) || this.apply('ipmask6', null, [negative]), _('valid network in address/netmask notation')); }, - ipmask4: function() { - return this.assert(this.apply('cidr4') || this.apply('ipnet4') || this.apply('ip4addr'), + ipmask4: function(negative) { + return this.assert(this.apply('cidr4', null, [negative]) || this.apply('ipnet4') || this.apply('ip4addr'), _('valid IPv4 network')); }, - ipmask6: function() { - return this.assert(this.apply('cidr6') || this.apply('ipnet6') || this.apply('ip6addr'), + ipmask6: function(negative) { + return this.assert(this.apply('cidr6', null, [negative]) || this.apply('ipnet6') || this.apply('ip6addr'), _('valid IPv6 network')); }, @@ -358,8 +361,8 @@ var ValidatorFactory = baseclass.extend({ }, network: function() { - return this.assert(this.apply('uciname') || this.apply('host'), - _('valid UCI identifier, hostname or IP address')); + return this.assert(this.apply('uciname') || this.apply('hostname') || this.apply('ip4addr') || this.apply('ip6addr'), + _('valid UCI identifier, hostname or IP address range')); }, hostport: function(ipv4only) { |