summaryrefslogtreecommitdiffhomepage
path: root/modules/luci-base/htdocs/luci-static
diff options
context:
space:
mode:
Diffstat (limited to 'modules/luci-base/htdocs/luci-static')
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/cbi.js1397
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/firewall.js568
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/form.js1676
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/luci.js1527
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/network.js2085
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/promis.min.js5
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/rpc.js160
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/tools/prng.js93
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/tools/widgets.js535
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/uci.js540
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/ui.js2069
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/validation.js568
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/xhr.js251
13 files changed, 9518 insertions, 1956 deletions
diff --git a/modules/luci-base/htdocs/luci-static/resources/cbi.js b/modules/luci-base/htdocs/luci-static/resources/cbi.js
index 67ddc6af36..d4d61eb381 100644
--- a/modules/luci-base/htdocs/luci-static/resources/cbi.js
+++ b/modules/luci-base/htdocs/luci-static/resources/cbi.js
@@ -98,540 +98,6 @@ function _(s) {
return (window.TR && TR[sfh(s)]) || s;
}
-function Int(x) {
- return (/^-?\d+$/.test(x) ? +x : NaN);
-}
-
-function Dec(x) {
- return (/^-?\d+(?:\.\d+)?$/.test(x) ? +x : NaN);
-}
-
-function IPv4(x) {
- if (!x.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/))
- return null;
-
- if (RegExp.$1 > 255 || RegExp.$2 > 255 || RegExp.$3 > 255 || RegExp.$4 > 255)
- return null;
-
- return [ +RegExp.$1, +RegExp.$2, +RegExp.$3, +RegExp.$4 ];
-}
-
-function IPv6(x) {
- if (x.match(/^([a-fA-F0-9:]+):(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/)) {
- var v6 = RegExp.$1, v4 = IPv4(RegExp.$2);
-
- if (!v4)
- return null;
-
- x = v6 + ':' + (v4[0] * 256 + v4[1]).toString(16)
- + ':' + (v4[2] * 256 + v4[3]).toString(16);
- }
-
- if (!x.match(/^[a-fA-F0-9:]+$/))
- return null;
-
- var prefix_suffix = x.split(/::/);
-
- if (prefix_suffix.length > 2)
- return null;
-
- var prefix = (prefix_suffix[0] || '0').split(/:/);
- var suffix = prefix_suffix.length > 1 ? (prefix_suffix[1] || '0').split(/:/) : [];
-
- if (suffix.length ? (prefix.length + suffix.length > 7)
- : ((prefix_suffix.length < 2 && prefix.length < 8) || prefix.length > 8))
- return null;
-
- var i, word;
- var words = [];
-
- for (i = 0, word = parseInt(prefix[0], 16); i < prefix.length; word = parseInt(prefix[++i], 16))
- if (prefix[i].length <= 4 && !isNaN(word) && word <= 0xFFFF)
- words.push(word);
- else
- return null;
-
- for (i = 0; i < (8 - prefix.length - suffix.length); i++)
- words.push(0);
-
- for (i = 0, word = parseInt(suffix[0], 16); i < suffix.length; word = parseInt(suffix[++i], 16))
- if (suffix[i].length <= 4 && !isNaN(word) && word <= 0xFFFF)
- words.push(word);
- else
- return null;
-
- return words;
-}
-
-var CBIValidatorPrototype = {
- apply: function(name, value, args) {
- var func;
-
- if (typeof(name) === 'function')
- func = name;
- else if (typeof(this.types[name]) === 'function')
- func = this.types[name];
- else
- return false;
-
- if (value !== undefined && value !== null)
- this.value = value;
-
- return func.apply(this, args);
- },
-
- assert: function(condition, message) {
- if (!condition) {
- this.field.classList.add('cbi-input-invalid');
- this.error = message;
- return false;
- }
-
- this.field.classList.remove('cbi-input-invalid');
- this.error = null;
- return true;
- },
-
- compile: function(code) {
- var pos = 0;
- var esc = false;
- var depth = 0;
- var stack = [ ];
-
- code += ',';
-
- for (var i = 0; i < code.length; i++) {
- if (esc) {
- esc = false;
- continue;
- }
-
- switch (code.charCodeAt(i))
- {
- case 92:
- esc = true;
- break;
-
- case 40:
- case 44:
- if (depth <= 0) {
- if (pos < i) {
- var label = code.substring(pos, i);
- label = label.replace(/\\(.)/g, '$1');
- label = label.replace(/^[ \t]+/g, '');
- label = label.replace(/[ \t]+$/g, '');
-
- if (label && !isNaN(label)) {
- stack.push(parseFloat(label));
- }
- else if (label.match(/^(['"]).*\1$/)) {
- stack.push(label.replace(/^(['"])(.*)\1$/, '$2'));
- }
- else if (typeof this.types[label] == 'function') {
- stack.push(this.types[label]);
- stack.push(null);
- }
- else {
- throw "Syntax error, unhandled token '"+label+"'";
- }
- }
-
- pos = i+1;
- }
-
- depth += (code.charCodeAt(i) == 40);
- break;
-
- case 41:
- if (--depth <= 0) {
- if (typeof stack[stack.length-2] != 'function')
- throw "Syntax error, argument list follows non-function";
-
- stack[stack.length-1] = this.compile(code.substring(pos, i));
- pos = i+1;
- }
-
- break;
- }
- }
-
- return stack;
- },
-
- validate: function() {
- /* element is detached */
- if (!findParent(this.field, 'form'))
- return true;
-
- this.field.classList.remove('cbi-input-invalid');
- this.value = matchesElem(this.field, 'select') ? this.field.options[this.field.selectedIndex].value : this.field.value;
- this.error = null;
-
- var valid;
-
- if (this.value.length === 0)
- valid = this.assert(this.optional, _('non-empty value'));
- else
- valid = this.vstack[0].apply(this, this.vstack[1]);
-
- if (!valid) {
- this.field.setAttribute('data-tooltip', _('Expecting %s').format(this.error));
- this.field.setAttribute('data-tooltip-style', 'error');
- this.field.dispatchEvent(new CustomEvent('validation-failure', { bubbles: true }));
- }
- else {
- this.field.removeAttribute('data-tooltip');
- this.field.removeAttribute('data-tooltip-style');
- this.field.dispatchEvent(new CustomEvent('validation-success', { bubbles: true }));
- }
-
- return valid;
- },
-
- types: {
- integer: function() {
- return this.assert(Int(this.value) !== NaN, _('valid integer value'));
- },
-
- uinteger: function() {
- return this.assert(Int(this.value) >= 0, _('positive integer value'));
- },
-
- float: function() {
- return this.assert(Dec(this.value) !== NaN, _('valid decimal value'));
- },
-
- ufloat: function() {
- return this.assert(Dec(this.value) >= 0, _('positive decimal value'));
- },
-
- ipaddr: function(nomask) {
- return this.assert(this.apply('ip4addr', null, [nomask]) || this.apply('ip6addr', null, [nomask]),
- nomask ? _('valid IP address') : _('valid IP address or prefix'));
- },
-
- ip4addr: function(nomask) {
- var re = nomask ? /^(\d+\.\d+\.\d+\.\d+)$/ : /^(\d+\.\d+\.\d+\.\d+)(?:\/(\d+\.\d+\.\d+\.\d+)|\/(\d{1,2}))?$/,
- m = this.value.match(re);
-
- return this.assert(m && IPv4(m[1]) && (m[2] ? IPv4(m[2]) : (m[3] ? this.apply('ip4prefix', m[3]) : true)),
- nomask ? _('valid IPv4 address') : _('valid IPv4 address or network'));
- },
-
- ip6addr: function(nomask) {
- var re = nomask ? /^([0-9a-fA-F:.]+)$/ : /^([0-9a-fA-F:.]+)(?:\/(\d{1,3}))?$/,
- m = this.value.match(re);
-
- return this.assert(m && IPv6(m[1]) && (m[2] ? this.apply('ip6prefix', m[2]) : true),
- nomask ? _('valid IPv6 address') : _('valid IPv6 address or prefix'));
- },
-
- ip4prefix: function() {
- return this.assert(!isNaN(this.value) && this.value >= 0 && this.value <= 32,
- _('valid IPv4 prefix value (0-32)'));
- },
-
- ip6prefix: function() {
- return this.assert(!isNaN(this.value) && this.value >= 0 && this.value <= 128,
- _('valid IPv6 prefix value (0-128)'));
- },
-
- cidr: function() {
- return this.assert(this.apply('cidr4') || this.apply('cidr6'), _('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 && IPv4(m[1]) && this.apply('ip4prefix', m[2]), _('valid IPv4 CIDR'));
- },
-
- cidr6: function() {
- var m = this.value.match(/^([0-9a-fA-F:.]+)\/(\d{1,3})$/);
- return this.assert(m && IPv6(m[1]) && this.apply('ip6prefix', m[2]), _('valid IPv6 CIDR'));
- },
-
- ipnet4: function() {
- var m = this.value.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
- return this.assert(m && IPv4(m[1]) && IPv4(m[2]), _('IPv4 network in address/netmask notation'));
- },
-
- ipnet6: function() {
- var m = this.value.match(/^([0-9a-fA-F:.]+)\/([0-9a-fA-F:.]+)$/);
- return this.assert(m && IPv6(m[1]) && IPv6(m[2]), _('IPv6 network in address/netmask notation'));
- },
-
- ip6hostid: function() {
- if (this.value == "eui64" || this.value == "random")
- return true;
-
- var v6 = IPv6(this.value);
- 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'),
- _('valid network in address/netmask notation'));
- },
-
- ipmask4: function() {
- return this.assert(this.apply('cidr4') || this.apply('ipnet4') || this.apply('ip4addr'),
- _('valid IPv4 network'));
- },
-
- ipmask6: function() {
- return this.assert(this.apply('cidr6') || this.apply('ipnet6') || this.apply('ip6addr'),
- _('valid IPv6 network'));
- },
-
- port: function() {
- var p = Int(this.value);
- return this.assert(p >= 0 && p <= 65535, _('valid port value'));
- },
-
- portrange: function() {
- if (this.value.match(/^(\d+)-(\d+)$/)) {
- var p1 = +RegExp.$1;
- var p2 = +RegExp.$2;
- return this.assert(p1 <= p2 && p2 <= 65535,
- _('valid port or port range (port1-port2)'));
- }
-
- return this.assert(this.apply('port'), _('valid port or port range (port1-port2)'));
- },
-
- macaddr: function() {
- return this.assert(this.value.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null,
- _('valid MAC address'));
- },
-
- host: function(ipv4only) {
- return this.assert(this.apply('hostname') || this.apply(ipv4only == 1 ? 'ip4addr' : 'ipaddr'),
- _('valid hostname or IP address'));
- },
-
- hostname: function(strict) {
- if (this.value.length <= 253)
- return this.assert(
- (this.value.match(/^[a-zA-Z0-9_]+$/) != null ||
- (this.value.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) &&
- this.value.match(/[^0-9.]/))) &&
- (!strict || !this.value.match(/^_/)),
- _('valid hostname'));
-
- return this.assert(false, _('valid hostname'));
- },
-
- network: function() {
- return this.assert(this.apply('uciname') || this.apply('host'),
- _('valid UCI identifier, hostname or IP address'));
- },
-
- hostport: function(ipv4only) {
- var hp = this.value.split(/:/);
- return this.assert(hp.length == 2 && this.apply('host', hp[0], [ipv4only]) && this.apply('port', hp[1]),
- _('valid host:port'));
- },
-
- ip4addrport: function() {
- var hp = this.value.split(/:/);
- return this.assert(hp.length == 2 && this.apply('ip4addr', hp[0], [true]) && this.apply('port', hp[1]),
- _('valid IPv4 address:port'));
- },
-
- ipaddrport: function(bracket) {
- var m4 = this.value.match(/^([^\[\]:]+):(\d+)$/),
- m6 = this.value.match((bracket == 1) ? /^\[(.+)\]:(\d+)$/ : /^([^\[\]]+):(\d+)$/);
-
- if (m4)
- return this.assert(this.apply('ip4addr', m4[1], [true]) && this.apply('port', m4[2]),
- _('valid address:port'));
-
- return this.assert(m6 && this.apply('ip6addr', m6[1], [true]) && this.apply('port', m6[2]),
- _('valid address:port'));
- },
-
- wpakey: function() {
- var v = this.value;
-
- if (v.length == 64)
- return this.assert(v.match(/^[a-fA-F0-9]{64}$/), _('valid hexadecimal WPA key'));
-
- return this.assert((v.length >= 8) && (v.length <= 63), _('key between 8 and 63 characters'));
- },
-
- wepkey: function() {
- var v = this.value;
-
- if (v.substr(0, 2) === 's:')
- v = v.substr(2);
-
- if ((v.length == 10) || (v.length == 26))
- return this.assert(v.match(/^[a-fA-F0-9]{10,26}$/), _('valid hexadecimal WEP key'));
-
- return this.assert((v.length === 5) || (v.length === 13), _('key with either 5 or 13 characters'));
- },
-
- uciname: function() {
- return this.assert(this.value.match(/^[a-zA-Z0-9_]+$/), _('valid UCI identifier'));
- },
-
- range: function(min, max) {
- var val = Dec(this.value);
- return this.assert(val >= +min && val <= +max, _('value between %f and %f').format(min, max));
- },
-
- min: function(min) {
- return this.assert(Dec(this.value) >= +min, _('value greater or equal to %f').format(min));
- },
-
- max: function(max) {
- return this.assert(Dec(this.value) <= +max, _('value smaller or equal to %f').format(max));
- },
-
- rangelength: function(min, max) {
- var val = '' + this.value;
- return this.assert((val.length >= +min) && (val.length <= +max),
- _('value between %d and %d characters').format(min, max));
- },
-
- minlength: function(min) {
- return this.assert((''+this.value).length >= +min,
- _('value with at least %d characters').format(min));
- },
-
- maxlength: function(max) {
- return this.assert((''+this.value).length <= +max,
- _('value with at most %d characters').format(max));
- },
-
- or: function() {
- var errors = [];
-
- for (var i = 0; i < arguments.length; i += 2) {
- if (typeof arguments[i] != 'function') {
- if (arguments[i] == this.value)
- return this.assert(true);
- errors.push('"%s"'.format(arguments[i]));
- i--;
- }
- else if (arguments[i].apply(this, arguments[i+1])) {
- return this.assert(true);
- }
- else {
- errors.push(this.error);
- }
- }
-
- return this.assert(false, _('one of:\n - %s'.format(errors.join('\n - '))));
- },
-
- and: function() {
- for (var i = 0; i < arguments.length; i += 2) {
- if (typeof arguments[i] != 'function') {
- if (arguments[i] != this.value)
- return this.assert(false, '"%s"'.format(arguments[i]));
- i--;
- }
- else if (!arguments[i].apply(this, arguments[i+1])) {
- return this.assert(false, this.error);
- }
- }
-
- return this.assert(true);
- },
-
- neg: function() {
- return this.apply('or', this.value.replace(/^[ \t]*![ \t]*/, ''), arguments);
- },
-
- list: function(subvalidator, subargs) {
- this.field.setAttribute('data-is-list', 'true');
-
- var tokens = this.value.match(/[^ \t]+/g);
- for (var i = 0; i < tokens.length; i++)
- if (!this.apply(subvalidator, tokens[i], subargs))
- return this.assert(false, this.error);
-
- return this.assert(true);
- },
-
- phonedigit: function() {
- return this.assert(this.value.match(/^[0-9\*#!\.]+$/),
- _('valid phone digit (0-9, "*", "#", "!" or ".")'));
- },
-
- timehhmmss: function() {
- return this.assert(this.value.match(/^[0-6][0-9]:[0-6][0-9]:[0-6][0-9]$/),
- _('valid time (HH:MM:SS)'));
- },
-
- dateyyyymmdd: function() {
- if (this.value.match(/^(\d\d\d\d)-(\d\d)-(\d\d)/)) {
- var year = +RegExp.$1,
- month = +RegExp.$2,
- day = +RegExp.$3,
- days_in_month = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
-
- function is_leap_year(year) {
- return ((!(year % 4) && (year % 100)) || !(year % 400));
- }
-
- function get_days_in_month(month, year) {
- return (month === 2 && is_leap_year(year)) ? 29 : days_in_month[month - 1];
- }
-
- /* Firewall rules in the past don't make sense */
- return this.assert(year >= 2015 && month && month <= 12 && day && day <= get_days_in_month(month, year),
- _('valid date (YYYY-MM-DD)'));
-
- }
-
- return this.assert(false, _('valid date (YYYY-MM-DD)'));
- },
-
- unique: function(subvalidator, subargs) {
- var ctx = this,
- option = findParent(ctx.field, '[data-type][data-name]'),
- section = findParent(option, '.cbi-section'),
- query = '[data-type="%s"][data-name="%s"]'.format(option.getAttribute('data-type'), option.getAttribute('data-name')),
- unique = true;
-
- section.querySelectorAll(query).forEach(function(sibling) {
- if (sibling === option)
- return;
-
- var input = sibling.querySelector('[data-type]'),
- values = input ? (input.getAttribute('data-is-list') ? input.value.match(/[^ \t]+/g) : [ input.value ]) : null;
-
- if (values !== null && values.indexOf(ctx.value) !== -1)
- unique = false;
- });
-
- if (!unique)
- return this.assert(false, _('unique value'));
-
- if (typeof(subvalidator) === 'function')
- return this.apply(subvalidator, undefined, subargs);
-
- return this.assert(true);
- },
-
- hexstring: function() {
- return this.assert(this.value.match(/^([a-f0-9][a-f0-9]|[A-F0-9][A-F0-9])+$/),
- _('hexadecimal encoded value'));
- }
- }
-};
-
-function CBIValidator(field, type, optional)
-{
- this.field = field;
- this.optional = optional;
- this.vstack = this.compile(type);
-}
-
-CBIValidator.prototype = CBIValidatorPrototype;
-
function cbi_d_add(field, dep, index) {
var obj = (typeof(field) === 'string') ? document.getElementById(field) : field;
@@ -738,6 +204,11 @@ function cbi_d_update() {
function cbi_init() {
var nodes;
+ document.querySelectorAll('.cbi-dropdown').forEach(function(node) {
+ cbi_dropdown_init(node);
+ node.addEventListener('cbi-dropdown-change', cbi_d_update);
+ });
+
nodes = document.querySelectorAll('[data-strings]');
for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
@@ -772,8 +243,8 @@ function cbi_init() {
nodes = document.querySelectorAll('[data-choices]');
for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
- var choices = JSON.parse(node.getAttribute('data-choices'));
- var options = {};
+ var choices = JSON.parse(node.getAttribute('data-choices')),
+ options = {};
for (var j = 0; j < choices[0].length; j++)
options[choices[0][j]] = choices[1][j];
@@ -781,15 +252,24 @@ function cbi_init() {
var def = (node.getAttribute('data-optional') === 'true')
? node.placeholder || '' : null;
- cbi_combobox_init(node, options, def,
- node.getAttribute('data-manual'));
+ var cb = new L.ui.Combobox(node.value, options, {
+ name: node.getAttribute('name'),
+ sort: choices[0],
+ select_placeholder: def || _('-- Please choose --'),
+ custom_placeholder: node.getAttribute('data-manual') || _('-- custom --')
+ });
+
+ var n = cb.render();
+ n.addEventListener('cbi-dropdown-change', cbi_d_update);
+ node.parentNode.replaceChild(n, node);
}
nodes = document.querySelectorAll('[data-dynlist]');
for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
- var choices = JSON.parse(node.getAttribute('data-dynlist'));
- var options = null;
+ var choices = JSON.parse(node.getAttribute('data-dynlist')),
+ values = JSON.parse(node.getAttribute('data-values') || '[]'),
+ options = null;
if (choices[0] && choices[0].length) {
options = {};
@@ -798,7 +278,17 @@ function cbi_init() {
options[choices[0][j]] = choices[1][j];
}
- cbi_dynlist_init(node, choices[2], choices[3], options);
+ var dl = new L.ui.DynamicList(values, options, {
+ name: node.getAttribute('data-prefix'),
+ sort: choices[0],
+ datatype: choices[2],
+ optional: choices[3],
+ placeholder: node.getAttribute('data-placeholder')
+ });
+
+ var n = dl.render();
+ n.addEventListener('cbi-dynlist-change', cbi_d_update);
+ node.parentNode.replaceChild(n, node);
}
nodes = document.querySelectorAll('[data-type]');
@@ -808,7 +298,6 @@ function cbi_init() {
node.getAttribute('data-type'));
}
- document.querySelectorAll('.cbi-dropdown').forEach(cbi_dropdown_init);
document.querySelectorAll('[data-browser]').forEach(cbi_browser_init);
document.querySelectorAll('.cbi-tooltip:not(:empty)').forEach(function(s) {
@@ -827,47 +316,16 @@ function cbi_init() {
i.addEventListener('mouseout', handler);
});
- cbi_d_update();
-}
-
-function cbi_combobox_init(id, values, def, man) {
- var obj = (typeof(id) === 'string') ? document.getElementById(id) : id;
- var sb = E('div', {
- 'name': obj.name,
- 'class': 'cbi-dropdown',
- 'display-items': 5,
- 'optional': obj.getAttribute('data-optional'),
- 'placeholder': _('-- Please choose --'),
- 'data-type': obj.getAttribute('data-type'),
- 'data-optional': obj.getAttribute('data-optional')
- }, [ E('ul') ]);
-
- if (!(obj.value in values) && obj.value.length) {
- sb.lastElementChild.appendChild(E('li', {
- 'data-value': obj.value,
- 'selected': ''
- }, obj.value.length ? obj.value : (def || _('-- Please choose --'))));
- }
+ document.querySelectorAll('[data-ui-widget]').forEach(function(node) {
+ var args = JSON.parse(node.getAttribute('data-ui-widget') || '[]'),
+ widget = new (Function.prototype.bind.apply(L.ui[args[0]], args)),
+ markup = widget.render();
- for (var i in values) {
- sb.lastElementChild.appendChild(E('li', {
- 'data-value': i,
- 'selected': (i == obj.value) ? '' : null
- }, values[i]));
- }
+ markup.addEventListener('widget-change', cbi_d_update);
+ node.parentNode.replaceChild(markup, node);
+ });
- sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, [
- E('input', {
- 'type': 'text',
- 'class': 'create-item-input',
- 'data-type': obj.getAttribute('data-type'),
- 'data-optional': true,
- 'placeholder': (man || _('-- custom --'))
- })
- ]));
-
- sb.value = obj.value;
- obj.parentNode.replaceChild(sb, obj);
+ cbi_d_update();
}
function cbi_filebrowser(id, defpath) {
@@ -893,156 +351,6 @@ function cbi_browser_init(field)
}), field.nextSibling);
}
-CBIDynamicList = {
- addItem: function(dl, value, text, flash) {
- var exists = false,
- new_item = E('div', { 'class': flash ? 'item flash' : 'item', 'tabindex': 0 }, [
- E('span', {}, text || value),
- E('input', {
- 'type': 'hidden',
- 'name': dl.getAttribute('data-prefix'),
- 'value': value })]);
-
- dl.querySelectorAll('.item, .add-item').forEach(function(item) {
- if (exists)
- return;
-
- var hidden = item.querySelector('input[type="hidden"]');
-
- if (hidden && hidden.value === value)
- exists = true;
- else if (!hidden || hidden.value >= value)
- exists = !!item.parentNode.insertBefore(new_item, item);
- });
-
- cbi_d_update();
- },
-
- removeItem: function(dl, item) {
- var sb = dl.querySelector('.cbi-dropdown');
- if (sb) {
- var value = item.querySelector('input[type="hidden"]').value;
-
- sb.querySelectorAll('ul > li').forEach(function(li) {
- if (li.getAttribute('data-value') === value)
- li.removeAttribute('unselectable');
- });
- }
-
- item.parentNode.removeChild(item);
- cbi_d_update();
- },
-
- handleClick: function(ev) {
- var dl = ev.currentTarget,
- item = findParent(ev.target, '.item');
-
- if (item) {
- this.removeItem(dl, item);
- }
- else if (matchesElem(ev.target, '.cbi-button-add')) {
- var input = ev.target.previousElementSibling;
- if (input.value.length && !input.classList.contains('cbi-input-invalid')) {
- this.addItem(dl, input.value, null, true);
- input.value = '';
- }
- }
- },
-
- handleDropdownChange: function(ev) {
- var dl = ev.currentTarget,
- sbIn = ev.detail.instance,
- sbEl = ev.detail.element,
- sbVal = ev.detail.value;
-
- if (sbVal === null)
- return;
-
- sbIn.setValues(sbEl, null);
- sbVal.element.setAttribute('unselectable', '');
-
- this.addItem(dl, sbVal.value, sbVal.text, true);
- },
-
- handleKeydown: function(ev) {
- var dl = ev.currentTarget,
- item = findParent(ev.target, '.item');
-
- if (item) {
- switch (ev.keyCode) {
- case 8: /* backspace */
- if (item.previousElementSibling)
- item.previousElementSibling.focus();
-
- this.removeItem(dl, item);
- break;
-
- case 46: /* delete */
- if (item.nextElementSibling) {
- if (item.nextElementSibling.classList.contains('item'))
- item.nextElementSibling.focus();
- else
- item.nextElementSibling.firstElementChild.focus();
- }
-
- this.removeItem(dl, item);
- break;
- }
- }
- else if (matchesElem(ev.target, '.cbi-input-text')) {
- switch (ev.keyCode) {
- case 13: /* enter */
- if (ev.target.value.length && !ev.target.classList.contains('cbi-input-invalid')) {
- this.addItem(dl, ev.target.value, null, true);
- ev.target.value = '';
- ev.target.blur();
- ev.target.focus();
- }
-
- ev.preventDefault();
- break;
- }
- }
- }
-};
-
-function cbi_dynlist_init(dl, datatype, optional, choices)
-{
- if (!(this instanceof cbi_dynlist_init))
- return new cbi_dynlist_init(dl, datatype, optional, choices);
-
- dl.classList.add('cbi-dynlist');
- dl.appendChild(E('div', { 'class': 'add-item' }, E('input', {
- 'type': 'text',
- 'name': 'cbi.dynlist.' + dl.getAttribute('data-prefix'),
- 'class': 'cbi-input-text',
- 'placeholder': dl.getAttribute('data-placeholder'),
- 'data-type': datatype,
- 'data-optional': true
- })));
-
- if (choices)
- cbi_combobox_init(dl.lastElementChild.lastElementChild, choices, '', _('-- custom --'));
- else
- dl.lastElementChild.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+'));
-
- dl.addEventListener('click', this.handleClick.bind(this));
- dl.addEventListener('keydown', this.handleKeydown.bind(this));
- dl.addEventListener('cbi-dropdown-change', this.handleDropdownChange.bind(this));
-
- try {
- var values = JSON.parse(dl.getAttribute('data-values') || '[]');
-
- if (typeof(values) === 'object' && Array.isArray(values))
- for (var i = 0; i < values.length; i++)
- this.addItem(dl, values[i], choices ? choices[values[i]] : null);
- }
- catch (e) {}
-}
-
-cbi_dynlist_init.prototype = CBIDynamicList;
-
-
function cbi_validate_form(form, errmsg)
{
/* if triggered by a section removal or addition, don't validate */
@@ -1078,7 +386,7 @@ function cbi_validate_field(cbid, optional, type)
var validatorFn;
try {
- var cbiValidator = new CBIValidator(field, type, optional);
+ var cbiValidator = L.validation.create(field, type, optional);
validatorFn = cbiValidator.validate.bind(cbiValidator);
}
catch(e) {
@@ -1258,7 +566,7 @@ String.prototype.format = function()
switch(pType) {
case 'b':
- subst = (+param || 0).toString(2);
+ subst = (~~param || 0).toString(2);
break;
case 'c':
@@ -1266,7 +574,7 @@ String.prototype.format = function()
break;
case 'd':
- subst = ~~(+param || 0);
+ subst = (~~param || 0);
break;
case 'u':
@@ -1280,7 +588,7 @@ String.prototype.format = function()
break;
case 'o':
- subst = (+param || 0).toString(8);
+ subst = (~~param || 0).toString(8);
break;
case 's':
@@ -1288,11 +596,11 @@ String.prototype.format = function()
break;
case 'x':
- subst = ('' + (+param || 0).toString(16)).toLowerCase();
+ subst = ('' + (~~param || 0).toString(16)).toLowerCase();
break;
case 'X':
- subst = ('' + (+param || 0).toString(16)).toUpperCase();
+ subst = ('' + (~~param || 0).toString(16)).toUpperCase();
break;
case 'h':
@@ -1425,622 +733,11 @@ if (typeof(window.CustomEvent) !== 'function') {
window.CustomEvent = CustomEvent;
}
-CBIDropdown = {
- openDropdown: function(sb) {
- var st = window.getComputedStyle(sb, null),
- ul = sb.querySelector('ul'),
- li = ul.querySelectorAll('li'),
- fl = findParent(sb, '.cbi-value-field'),
- sel = ul.querySelector('[selected]'),
- rect = sb.getBoundingClientRect(),
- items = Math.min(this.dropdown_items, li.length);
-
- document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
- s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
- });
-
- sb.setAttribute('open', '');
-
- var pv = ul.cloneNode(true);
- pv.classList.add('preview');
-
- if (fl)
- fl.classList.add('cbi-dropdown-open');
-
- if ('ontouchstart' in window) {
- var vpWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
- vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0),
- scrollFrom = window.pageYOffset,
- scrollTo = scrollFrom + rect.top - vpHeight * 0.5,
- start = null;
-
- ul.style.top = sb.offsetHeight + 'px';
- ul.style.left = -rect.left + 'px';
- ul.style.right = (rect.right - vpWidth) + 'px';
- ul.style.maxHeight = (vpHeight * 0.5) + 'px';
- ul.style.WebkitOverflowScrolling = 'touch';
-
- var scrollStep = function(timestamp) {
- if (!start) {
- start = timestamp;
- ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
- }
-
- var duration = Math.max(timestamp - start, 1);
- if (duration < 100) {
- document.body.scrollTop = scrollFrom + (scrollTo - scrollFrom) * (duration / 100);
- window.requestAnimationFrame(scrollStep);
- }
- else {
- document.body.scrollTop = scrollTo;
- }
- };
-
- window.requestAnimationFrame(scrollStep);
- }
- else {
- ul.style.maxHeight = '1px';
- ul.style.top = ul.style.bottom = '';
-
- window.requestAnimationFrame(function() {
- var height = items * li[Math.max(0, li.length - 2)].offsetHeight;
-
- ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
- ul.style[((rect.top + rect.height + height) > window.innerHeight) ? 'bottom' : 'top'] = rect.height + 'px';
- ul.style.maxHeight = height + 'px';
- });
- }
-
- ul.querySelectorAll('[selected] input[type="checkbox"]').forEach(function(c) {
- c.checked = true;
- });
-
- ul.classList.add('dropdown');
-
- sb.insertBefore(pv, ul.nextElementSibling);
-
- li.forEach(function(l) {
- l.setAttribute('tabindex', 0);
- });
-
- sb.lastElementChild.setAttribute('tabindex', 0);
-
- this.setFocus(sb, sel || li[0], true);
- },
-
- closeDropdown: function(sb, no_focus) {
- if (!sb.hasAttribute('open'))
- return;
-
- var pv = sb.querySelector('ul.preview'),
- ul = sb.querySelector('ul.dropdown'),
- li = ul.querySelectorAll('li'),
- fl = findParent(sb, '.cbi-value-field');
-
- li.forEach(function(l) { l.removeAttribute('tabindex'); });
- sb.lastElementChild.removeAttribute('tabindex');
-
- sb.removeChild(pv);
- sb.removeAttribute('open');
- sb.style.width = sb.style.height = '';
-
- ul.classList.remove('dropdown');
- ul.style.top = ul.style.bottom = ul.style.maxHeight = '';
-
- if (fl)
- fl.classList.remove('cbi-dropdown-open');
-
- if (!no_focus)
- this.setFocus(sb, sb);
-
- this.saveValues(sb, ul);
- },
-
- toggleItem: function(sb, li, force_state) {
- if (li.hasAttribute('unselectable'))
- return;
-
- if (this.multi) {
- var cbox = li.querySelector('input[type="checkbox"]'),
- items = li.parentNode.querySelectorAll('li'),
- label = sb.querySelector('ul.preview'),
- sel = li.parentNode.querySelectorAll('[selected]').length,
- more = sb.querySelector('.more'),
- ndisplay = this.display_items,
- n = 0;
-
- if (li.hasAttribute('selected')) {
- if (force_state !== true) {
- if (sel > 1 || this.optional) {
- li.removeAttribute('selected');
- cbox.checked = cbox.disabled = false;
- sel--;
- }
- else {
- cbox.disabled = true;
- }
- }
- }
- else {
- if (force_state !== false) {
- li.setAttribute('selected', '');
- cbox.checked = true;
- cbox.disabled = false;
- sel++;
- }
- }
-
- while (label.firstElementChild)
- label.removeChild(label.firstElementChild);
-
- for (var i = 0; i < items.length; i++) {
- items[i].removeAttribute('display');
- if (items[i].hasAttribute('selected')) {
- if (ndisplay-- > 0) {
- items[i].setAttribute('display', n++);
- label.appendChild(items[i].cloneNode(true));
- }
- var c = items[i].querySelector('input[type="checkbox"]');
- if (c)
- c.disabled = (sel == 1 && !this.optional);
- }
- }
-
- if (ndisplay < 0)
- sb.setAttribute('more', '');
- else
- sb.removeAttribute('more');
-
- if (ndisplay === this.display_items)
- sb.setAttribute('empty', '');
- else
- sb.removeAttribute('empty');
-
- more.innerHTML = (ndisplay === this.display_items) ? this.placeholder : '···';
- }
- else {
- var sel = li.parentNode.querySelector('[selected]');
- if (sel) {
- sel.removeAttribute('display');
- sel.removeAttribute('selected');
- }
-
- li.setAttribute('display', 0);
- li.setAttribute('selected', '');
-
- this.closeDropdown(sb, true);
- }
-
- this.saveValues(sb, li.parentNode);
- },
-
- transformItem: function(sb, li) {
- var cbox = E('form', {}, E('input', { type: 'checkbox', tabindex: -1, onclick: 'event.preventDefault()' })),
- label = E('label');
-
- while (li.firstChild)
- label.appendChild(li.firstChild);
-
- li.appendChild(cbox);
- li.appendChild(label);
- },
-
- saveValues: function(sb, ul) {
- var sel = ul.querySelectorAll('li[selected]'),
- div = sb.lastElementChild,
- strval = '',
- values = [];
-
- while (div.lastElementChild)
- div.removeChild(div.lastElementChild);
-
- sel.forEach(function (s) {
- if (s.hasAttribute('placeholder'))
- return;
-
- var v = {
- text: s.innerText,
- value: s.hasAttribute('data-value') ? s.getAttribute('data-value') : s.innerText,
- element: s
- };
-
- div.appendChild(E('input', {
- type: 'hidden',
- name: s.hasAttribute('name') ? s.getAttribute('name') : (sb.getAttribute('name') || ''),
- value: v.value
- }));
-
- values.push(v);
-
- strval += strval.length ? ' ' + v.value : v.value;
- });
-
- var detail = {
- instance: this,
- element: sb
- };
-
- if (this.multi)
- detail.values = values;
- else
- detail.value = values.length ? values[0] : null;
-
- sb.value = strval;
-
- sb.dispatchEvent(new CustomEvent('cbi-dropdown-change', {
- bubbles: true,
- detail: detail
- }));
-
- cbi_d_update();
- },
-
- setValues: function(sb, values) {
- var ul = sb.querySelector('ul');
-
- if (this.multi) {
- ul.querySelectorAll('li[data-value]').forEach(function(li) {
- if (values === null || !(li.getAttribute('data-value') in values))
- this.toggleItem(sb, li, false);
- else
- this.toggleItem(sb, li, true);
- });
- }
- else {
- var ph = ul.querySelector('li[placeholder]');
- if (ph)
- this.toggleItem(sb, ph);
-
- ul.querySelectorAll('li[data-value]').forEach(function(li) {
- if (values !== null && (li.getAttribute('data-value') in values))
- this.toggleItem(sb, li);
- });
- }
- },
-
- setFocus: function(sb, elem, scroll) {
- if (sb && sb.hasAttribute && sb.hasAttribute('locked-in'))
- return;
-
- if (sb.target && findParent(sb.target, 'ul.dropdown'))
- return;
-
- document.querySelectorAll('.focus').forEach(function(e) {
- if (!matchesElem(e, 'input')) {
- e.classList.remove('focus');
- e.blur();
- }
- });
-
- if (elem) {
- elem.focus();
- elem.classList.add('focus');
-
- if (scroll)
- elem.parentNode.scrollTop = elem.offsetTop - elem.parentNode.offsetTop;
- }
- },
-
- createItems: function(sb, value) {
- var sbox = this,
- val = (value || '').trim(),
- ul = sb.querySelector('ul');
-
- if (!sbox.multi)
- val = val.length ? [ val ] : [];
- else
- val = val.length ? val.split(/\s+/) : [];
-
- val.forEach(function(item) {
- var new_item = null;
-
- ul.childNodes.forEach(function(li) {
- if (li.getAttribute && li.getAttribute('data-value') === item)
- new_item = li;
- });
-
- if (!new_item) {
- var markup,
- tpl = sb.querySelector(sbox.template);
-
- if (tpl)
- markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^<!--|-->$/, '').trim();
- else
- markup = '<li data-value="{{value}}">{{value}}</li>';
-
- new_item = E(markup.replace(/{{value}}/g, item));
-
- if (sbox.multi) {
- sbox.transformItem(sb, new_item);
- }
- else {
- var old = ul.querySelector('li[created]');
- if (old)
- ul.removeChild(old);
-
- new_item.setAttribute('created', '');
- }
-
- new_item = ul.insertBefore(new_item, ul.lastElementChild);
- }
-
- sbox.toggleItem(sb, new_item, true);
- sbox.setFocus(sb, new_item, true);
- });
- },
-
- closeAllDropdowns: function() {
- document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
- s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
- });
- },
-
- handleClick: function(ev) {
- var sb = ev.currentTarget;
-
- if (!sb.hasAttribute('open')) {
- if (!matchesElem(ev.target, 'input'))
- this.openDropdown(sb);
- }
- else {
- var li = findParent(ev.target, 'li');
- if (li && li.parentNode.classList.contains('dropdown'))
- this.toggleItem(sb, li);
- else if (li && li.parentNode.classList.contains('preview'))
- this.closeDropdown(sb);
- }
-
- ev.preventDefault();
- ev.stopPropagation();
- },
-
- handleKeydown: function(ev) {
- var sb = ev.currentTarget;
-
- if (matchesElem(ev.target, 'input'))
- return;
-
- if (!sb.hasAttribute('open')) {
- switch (ev.keyCode) {
- case 37:
- case 38:
- case 39:
- case 40:
- this.openDropdown(sb);
- ev.preventDefault();
- }
- }
- else {
- var active = findParent(document.activeElement, 'li');
-
- switch (ev.keyCode) {
- case 27:
- this.closeDropdown(sb);
- break;
-
- case 13:
- if (active) {
- if (!active.hasAttribute('selected'))
- this.toggleItem(sb, active);
- this.closeDropdown(sb);
- ev.preventDefault();
- }
- break;
-
- case 32:
- if (active) {
- this.toggleItem(sb, active);
- ev.preventDefault();
- }
- break;
-
- case 38:
- if (active && active.previousElementSibling) {
- this.setFocus(sb, active.previousElementSibling);
- ev.preventDefault();
- }
- break;
-
- case 40:
- if (active && active.nextElementSibling) {
- this.setFocus(sb, active.nextElementSibling);
- ev.preventDefault();
- }
- break;
- }
- }
- },
-
- handleDropdownClose: function(ev) {
- var sb = ev.currentTarget;
-
- this.closeDropdown(sb, true);
- },
-
- handleDropdownSelect: function(ev) {
- var sb = ev.currentTarget,
- li = findParent(ev.target, 'li');
-
- if (!li)
- return;
-
- this.toggleItem(sb, li);
- this.closeDropdown(sb, true);
- },
-
- handleMouseover: function(ev) {
- var sb = ev.currentTarget;
-
- if (!sb.hasAttribute('open'))
- return;
-
- var li = findParent(ev.target, 'li');
-
- if (li && li.parentNode.classList.contains('dropdown'))
- this.setFocus(sb, li);
- },
-
- handleFocus: function(ev) {
- var sb = ev.currentTarget;
-
- document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
- if (s !== sb || sb.hasAttribute('open'))
- s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
- });
- },
-
- handleCanaryFocus: function(ev) {
- this.closeDropdown(ev.currentTarget.parentNode);
- },
-
- handleCreateKeydown: function(ev) {
- var input = ev.currentTarget,
- sb = findParent(input, '.cbi-dropdown');
-
- switch (ev.keyCode) {
- case 13:
- ev.preventDefault();
-
- if (input.classList.contains('cbi-input-invalid'))
- return;
-
- this.createItems(sb, input.value);
- input.value = '';
- input.blur();
- break;
- }
- },
-
- handleCreateFocus: function(ev) {
- var input = ev.currentTarget,
- cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
- sb = findParent(input, '.cbi-dropdown');
-
- if (cbox)
- cbox.checked = true;
-
- sb.setAttribute('locked-in', '');
- },
-
- handleCreateBlur: function(ev) {
- var input = ev.currentTarget,
- cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
- sb = findParent(input, '.cbi-dropdown');
-
- if (cbox)
- cbox.checked = false;
-
- sb.removeAttribute('locked-in');
- },
-
- handleCreateClick: function(ev) {
- ev.currentTarget.querySelector(this.create).focus();
- }
-};
-
function cbi_dropdown_init(sb) {
- if (!(this instanceof cbi_dropdown_init))
- return new cbi_dropdown_init(sb);
-
- this.multi = sb.hasAttribute('multiple');
- this.optional = sb.hasAttribute('optional');
- this.placeholder = sb.getAttribute('placeholder') || '---';
- this.display_items = parseInt(sb.getAttribute('display-items') || 3);
- this.dropdown_items = parseInt(sb.getAttribute('dropdown-items') || 5);
- this.create = sb.getAttribute('item-create') || '.create-item-input';
- this.template = sb.getAttribute('item-template') || 'script[type="item-template"]';
-
- var ul = sb.querySelector('ul'),
- more = sb.appendChild(E('span', { class: 'more', tabindex: -1 }, '···')),
- open = sb.appendChild(E('span', { class: 'open', tabindex: -1 }, '▾')),
- canary = sb.appendChild(E('div')),
- create = sb.querySelector(this.create),
- ndisplay = this.display_items,
- n = 0;
-
- if (this.multi) {
- var items = ul.querySelectorAll('li');
-
- for (var i = 0; i < items.length; i++) {
- this.transformItem(sb, items[i]);
-
- if (items[i].hasAttribute('selected') && ndisplay-- > 0)
- items[i].setAttribute('display', n++);
- }
- }
- else {
- if (this.optional && !ul.querySelector('li[data-value=""]')) {
- var placeholder = E('li', { placeholder: '' }, this.placeholder);
- ul.firstChild ? ul.insertBefore(placeholder, ul.firstChild) : ul.appendChild(placeholder);
- }
-
- var items = ul.querySelectorAll('li'),
- sel = sb.querySelectorAll('[selected]');
-
- sel.forEach(function(s) {
- s.removeAttribute('selected');
- });
-
- var s = sel[0] || items[0];
- if (s) {
- s.setAttribute('selected', '');
- s.setAttribute('display', n++);
- }
-
- ndisplay--;
- }
-
- this.saveValues(sb, ul);
-
- ul.setAttribute('tabindex', -1);
- sb.setAttribute('tabindex', 0);
-
- if (ndisplay < 0)
- sb.setAttribute('more', '')
- else
- sb.removeAttribute('more');
-
- if (ndisplay === this.display_items)
- sb.setAttribute('empty', '')
- else
- sb.removeAttribute('empty');
-
- more.innerHTML = (ndisplay === this.display_items) ? this.placeholder : '···';
-
-
- sb.addEventListener('click', this.handleClick.bind(this));
- sb.addEventListener('keydown', this.handleKeydown.bind(this));
- sb.addEventListener('cbi-dropdown-close', this.handleDropdownClose.bind(this));
- sb.addEventListener('cbi-dropdown-select', this.handleDropdownSelect.bind(this));
-
- if ('ontouchstart' in window) {
- sb.addEventListener('touchstart', function(ev) { ev.stopPropagation(); });
- window.addEventListener('touchstart', this.closeAllDropdowns);
- }
- else {
- sb.addEventListener('mouseover', this.handleMouseover.bind(this));
- sb.addEventListener('focus', this.handleFocus.bind(this));
-
- canary.addEventListener('focus', this.handleCanaryFocus.bind(this));
-
- window.addEventListener('mouseover', this.setFocus);
- window.addEventListener('click', this.closeAllDropdowns);
- }
-
- if (create) {
- create.addEventListener('keydown', this.handleCreateKeydown.bind(this));
- create.addEventListener('focus', this.handleCreateFocus.bind(this));
- create.addEventListener('blur', this.handleCreateBlur.bind(this));
-
- var li = findParent(create, 'li');
-
- li.setAttribute('unselectable', '');
- li.addEventListener('click', this.handleCreateClick.bind(this));
- }
+ var dl = new L.ui.Dropdown(sb, null, { name: sb.getAttribute('name') });
+ return dl.bind(sb);
}
-cbi_dropdown_init.prototype = CBIDropdown;
-
function cbi_update_table(table, data, placeholder) {
var target = isElem(table) ? table : document.querySelector(table);
diff --git a/modules/luci-base/htdocs/luci-static/resources/firewall.js b/modules/luci-base/htdocs/luci-static/resources/firewall.js
new file mode 100644
index 0000000000..9ae14e16d9
--- /dev/null
+++ b/modules/luci-base/htdocs/luci-static/resources/firewall.js
@@ -0,0 +1,568 @@
+'use strict';
+'require uci';
+'require rpc';
+'require tools.prng as random';
+
+
+function initFirewallState() {
+ return uci.load('firewall');
+}
+
+function parseEnum(s, values) {
+ if (s == null)
+ return null;
+
+ s = String(s).toUpperCase();
+
+ if (s == '')
+ return null;
+
+ for (var i = 0; i < values.length; i++)
+ if (values[i].toUpperCase().indexOf(s) == 0)
+ return values[i];
+
+ return null;
+}
+
+function parsePolicy(s, defaultValue) {
+ return parseEnum(s, ['DROP', 'REJECT', 'ACCEPT']) || (arguments.length < 2 ? null : defaultValue);
+}
+
+
+var Firewall, AbstractFirewallItem, Defaults, Zone, Forwarding, Redirect, Rule;
+
+function lookupZone(name) {
+ var z = uci.get('firewall', name);
+
+ if (z != null && z['.type'] == 'zone')
+ return new Zone(z['.name']);
+
+ var sections = uci.sections('firewall', 'zone');
+
+ for (var i = 0; i < sections.length; i++) {
+ if (sections[i].name != name)
+ continue;
+
+ return new Zone(sections[i]['.name']);
+ }
+
+ return null;
+}
+
+function getColorForName(forName) {
+ if (forName == null)
+ return '#eeeeee';
+ else if (forName == 'lan')
+ return '#90f090';
+ 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);
+}
+
+
+Firewall = L.Class.extend({
+ getDefaults: function() {
+ return initFirewallState().then(function() {
+ return new Defaults();
+ });
+ },
+
+ newZone: function() {
+ return initFirewallState().then(L.bind(function() {
+ var name = 'newzone',
+ count = 1;
+
+ while (this.getZone(name) != null)
+ name = 'newzone%d'.format(++count);
+
+ return this.addZone(name);
+ }, this));
+ },
+
+ addZone: function(name) {
+ return initFirewallState().then(L.bind(function() {
+ if (name == null || !/^[a-zA-Z0-9_]+$/.test(name))
+ return null;
+
+ if (this.getZone(name) != null)
+ return null;
+
+ var d = new Defaults(),
+ 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');
+
+ return new Zone(z);
+ }, this));
+ },
+
+ getZone: function(name) {
+ return initFirewallState().then(function() {
+ return lookupZone(name);
+ });
+ },
+
+ getZones: function() {
+ return initFirewallState().then(function() {
+ var sections = uci.sections('firewall', 'zone'),
+ zones = [];
+
+ for (var i = 0; i < sections.length; i++)
+ zones.push(new Zone(sections[i]['.name']));
+
+ zones.sort(function(a, b) { return a.getName() > b.getName() });
+
+ return zones;
+ });
+ },
+
+ getZoneByNetwork: function(network) {
+ return initFirewallState().then(function() {
+ var sections = uci.sections('firewall', 'zone');
+
+ for (var i = 0; i < sections.length; i++)
+ if (L.toArray(sections[i].network || sections[i].name).indexOf(network) != -1)
+ return new Zone(sections[i]['.name']);
+
+ return null;
+ });
+ },
+
+ deleteZone: function(name) {
+ return initFirewallState().then(function() {
+ var section = uci.get('firewall', name),
+ found = false;
+
+ if (section != null && section['.type'] == 'zone') {
+ found = true;
+ name = zone.name;
+ uci.remove('firewall', zone['.name']);
+ }
+ else if (name != null) {
+ var sections = uci.sections('firewall', 'zone');
+
+ for (var i = 0; i < sections.length; i++) {
+ if (sections[i].name != name)
+ continue;
+
+ found = true;
+ uci.remove('firewall', sections[i]['.name']);
+ }
+ }
+
+ if (found == true) {
+ sections = uci.sections('firewall');
+
+ for (var i = 0; i < sections.length; i++) {
+ if (sections[i]['.type'] != 'rule' &&
+ sections[i]['.type'] != 'redirect' &&
+ sections[i]['.type'] != 'forwarding')
+ continue;
+
+ if (sections[i].src == name || sections[i].dest == name)
+ uci.remove('firewall', sections[i]['.name']);
+ }
+ }
+
+ return found;
+ });
+ },
+
+ renameZone: function(oldName, newName) {
+ return initFirewallState().then(L.bind(function() {
+ if (oldName == null || newName == null || !/^[a-zA-Z0-9_]+$/.test(newName))
+ return false;
+
+ if (lookupZone(newName) != null)
+ return false;
+
+ var sections = uci.sections('firewall', 'zone'),
+ found = false;
+
+ for (var i = 0; i < sections.length; i++) {
+ if (sections[i].name != oldName)
+ continue;
+
+ if (L.toArray(sections[i].network).length == 0)
+ uci.set('firewall', sections[i]['.name'], 'network', oldName);
+
+ uci.set('firewall', sections[i]['.name'], 'name', newName);
+ found = true;
+ }
+
+ if (found == true) {
+ sections = uci.sections('firewall');
+
+ for (var i = 0; i < sections.length; i++) {
+ if (sections[i]['.type'] != 'rule' &&
+ sections[i]['.type'] != 'redirect' &&
+ sections[i]['.type'] != 'forwarding')
+ continue;
+
+ if (sections[i].src == oldName)
+ uci.set('firewall', sections[i]['.name'], 'src', newName);
+
+ if (sections[i].dest == oldName)
+ uci.set('firewall', sections[i]['.name'], 'dest', newName);
+ }
+ }
+
+ return found;
+ }, this));
+ },
+
+ deleteNetwork: function(network) {
+ return this.getZones().then(L.bind(function(zones) {
+ var rv = false;
+
+ for (var i = 0; i < zones.length; i++)
+ if (zones[i].deleteNetwork(network))
+ rv = true;
+
+ return rv;
+ }, this));
+ },
+
+ getColorForName: getColorForName
+});
+
+
+AbstractFirewallItem = L.Class.extend({
+ get: function(option) {
+ return uci.get('firewall', this.sid, option);
+ },
+
+ set: function(option, value) {
+ return uci.set('firewall', this.sid, option, value);
+ }
+});
+
+
+Defaults = AbstractFirewallItem.extend({
+ __init__: function() {
+ var sections = uci.sections('firewall', 'defaults');
+
+ for (var i = 0; i < sections.length; i++) {
+ this.sid = sections[i]['.name'];
+ break;
+ }
+
+ if (this.sid == null)
+ this.sid = uci.add('firewall', 'defaults');
+ },
+
+ isSynFlood: function() {
+ return (this.get('syn_flood') == '1');
+ },
+
+ isDropInvalid: function() {
+ return (this.get('drop_invalid') == '1');
+ },
+
+ getInput: function() {
+ return parsePolicy(this.get('input'), 'DROP');
+ },
+
+ getOutput: function() {
+ return parsePolicy(this.get('output'), 'DROP');
+ },
+
+ getForward: function() {
+ return parsePolicy(this.get('forward'), 'DROP');
+ }
+});
+
+
+Zone = AbstractFirewallItem.extend({
+ __init__: function(name) {
+ var section = uci.get('firewall', name);
+
+ if (section != null && section['.type'] == 'zone') {
+ this.sid = name;
+ this.data = section;
+ }
+ else if (name != null) {
+ var sections = uci.get('firewall', 'zone');
+
+ for (var i = 0; i < sections.length; i++) {
+ if (sections[i].name != name)
+ continue;
+
+ this.sid = sections[i]['.name'];
+ this.data = sections[i];
+ break;
+ }
+ }
+ },
+
+ isMasquerade: function() {
+ return (this.get('masq') == '1');
+ },
+
+ getName: function() {
+ return this.get('name');
+ },
+
+ getNetwork: function() {
+ return this.get('network');
+ },
+
+ getInput: function() {
+ return parsePolicy(this.get('input'), (new Defaults()).getInput());
+ },
+
+ getOutput: function() {
+ return parsePolicy(this.get('output'), (new Defaults()).getOutput());
+ },
+
+ getForward: function() {
+ return parsePolicy(this.get('forward'), (new Defaults()).getForward());
+ },
+
+ addNetwork: function(network) {
+ var section = uci.get('network', network);
+
+ if (section == null || section['.type'] != 'interface')
+ return false;
+
+ var newNetworks = this.getNetworks();
+
+ if (newNetworks.filter(function(net) { return net == network }).length)
+ return false;
+
+ newNetworks.push(network);
+ this.set('network', newNetworks.join(' '));
+
+ return true;
+ },
+
+ deleteNetwork: function(network) {
+ var oldNetworks = this.getNetworks(),
+ newNetworks = oldNetworks.filter(function(net) { return net != network });
+
+ if (newNetworks.length > 0)
+ this.set('network', newNetworks.join(' '));
+ else
+ this.set('network', ' ');
+
+ return (newNetworks.length < oldNetworks.length);
+ },
+
+ getNetworks: function() {
+ return L.toArray(this.get('network') || this.get('name'));
+ },
+
+ clearNetworks: function() {
+ this.set('network', ' ');
+ },
+
+ getDevices: function() {
+ return L.toArray(this.get('device'));
+ },
+
+ getSubnets: function() {
+ return L.toArray(this.get('subnet'));
+ },
+
+ getForwardingsBy: function(what) {
+ var sections = uci.sections('firewall', 'forwarding'),
+ forwards = [];
+
+ for (var i = 0; i < sections.length; i++) {
+ if (sections[i].src == null || sections[i].dest == null)
+ continue;
+
+ if (sections[i][what] != this.getName())
+ continue;
+
+ forwards.push(new Forwarding(sections[i]['.name']));
+ }
+
+ return forwards;
+ },
+
+ addForwardingTo: function(dest) {
+ var forwards = this.getForwardingsBy('src'),
+ zone = lookupZone(dest);
+
+ if (zone == null || zone.getName() == this.getName())
+ return null;
+
+ for (var i = 0; i < forwards.length; i++)
+ if (forwards[i].getDestination() == zone.getName())
+ return null;
+
+ var sid = uci.add('firewall', 'forwarding');
+
+ uci.set('firewall', sid, 'src', this.getName());
+ uci.set('firewall', sid, 'dest', zone.getName());
+
+ return new Forwarding(sid);
+ },
+
+ addForwardingFrom: function(src) {
+ var forwards = this.getForwardingsBy('dest'),
+ zone = lookupZone(src);
+
+ if (zone == null || zone.getName() == this.getName())
+ return null;
+
+ for (var i = 0; i < forwards.length; i++)
+ if (forwards[i].getSource() == zone.getName())
+ return null;
+
+ var sid = uci.add('firewall', 'forwarding');
+
+ uci.set('firewall', sid, 'src', zone.getName());
+ uci.set('firewall', sid, 'dest', this.getName());
+
+ return new Forwarding(sid);
+ },
+
+ deleteForwardingsBy: function(what) {
+ var sections = uci.sections('firewall', 'forwarding'),
+ found = false;
+
+ for (var i = 0; i < sections.length; i++) {
+ if (sections[i].src == null || sections[i].dest == null)
+ continue;
+
+ if (sections[i][what] != this.getName())
+ continue;
+
+ uci.remove('firewall', sections[i]['.name']);
+ found = true;
+ }
+
+ return found;
+ },
+
+ deleteForwarding: function(forwarding) {
+ if (!(forwarding instanceof Forwarding))
+ return false;
+
+ var section = uci.get('firewall', forwarding.sid);
+
+ if (!section || section['.type'] != 'forwarding')
+ return false;
+
+ uci.remove('firewall', section['.name']);
+
+ return true;
+ },
+
+ addRedirect: function(options) {
+ var sid = uci.add('firewall', 'redirect');
+
+ if (options != null && typeof(options) == 'object')
+ for (var key in options)
+ if (options.hasOwnProperty(key))
+ uci.set('firewall', sid, key, options[key]);
+
+ uci.set('firewall', sid, 'src', this.getName());
+
+ return new Redirect(sid);
+ },
+
+ addRule: function(options) {
+ var sid = uci.add('firewall', 'rule');
+
+ if (options != null && typeof(options) == 'object')
+ for (var key in options)
+ if (options.hasOwnProperty(key))
+ uci.set('firewall', sid, key, options[key]);
+
+ uci.set('firewall', sid, 'src', this.getName());
+
+ return new Redirect(sid);
+ },
+
+ getColor: function(forName) {
+ var name = (arguments.length > 0 ? forName : this.getName());
+
+ return getColorForName(name);
+ }
+});
+
+
+Forwarding = AbstractFirewallItem.extend({
+ __init__: function(sid) {
+ this.sid = sid;
+ },
+
+ getSource: function() {
+ return this.get('src');
+ },
+
+ getDestination: function() {
+ return this.get('dest');
+ },
+
+ getSourceZone: function() {
+ return lookupZone(this.getSource());
+ },
+
+ getDestinationZone: function() {
+ return lookupZone(this.getDestination());
+ }
+});
+
+
+Rule = AbstractFirewallItem.extend({
+ getSource: function() {
+ return this.get('src');
+ },
+
+ getDestination: function() {
+ return this.get('dest');
+ },
+
+ getSourceZone: function() {
+ return lookupZone(this.getSource());
+ },
+
+ getDestinationZone: function() {
+ return lookupZone(this.getDestination());
+ }
+});
+
+
+Redirect = AbstractFirewallItem.extend({
+ getSource: function() {
+ return this.get('src');
+ },
+
+ getDestination: function() {
+ return this.get('dest');
+ },
+
+ getSourceZone: function() {
+ return lookupZone(this.getSource());
+ },
+
+ getDestinationZone: function() {
+ return lookupZone(this.getDestination());
+ }
+});
+
+
+return Firewall;
diff --git a/modules/luci-base/htdocs/luci-static/resources/form.js b/modules/luci-base/htdocs/luci-static/resources/form.js
new file mode 100644
index 0000000000..ab0998943c
--- /dev/null
+++ b/modules/luci-base/htdocs/luci-static/resources/form.js
@@ -0,0 +1,1676 @@
+'use strict';
+'require ui';
+'require uci';
+
+var scope = this;
+
+var CBINode = Class.extend({
+ __init__: function(title, description) {
+ this.title = title || '';
+ this.description = description || '';
+ this.children = [];
+ },
+
+ append: function(obj) {
+ this.children.push(obj);
+ },
+
+ parse: function() {
+ var args = arguments;
+ this.children.forEach(function(child) {
+ child.parse.apply(child, args);
+ });
+ },
+
+ render: function() {
+ L.error('InternalError', 'Not implemented');
+ },
+
+ loadChildren: function(/* ... */) {
+ var tasks = [];
+
+ if (Array.isArray(this.children))
+ for (var i = 0; i < this.children.length; i++)
+ if (!this.children[i].disable)
+ tasks.push(this.children[i].load.apply(this.children[i], arguments));
+
+ return Promise.all(tasks);
+ },
+
+ renderChildren: function(tab_name /*, ... */) {
+ var tasks = [],
+ index = 0;
+
+ if (Array.isArray(this.children))
+ for (var i = 0; i < this.children.length; i++)
+ if (tab_name === null || this.children[i].tab === tab_name)
+ if (!this.children[i].disable)
+ tasks.push(this.children[i].render.apply(
+ this.children[i], this.varargs(arguments, 1, index++)));
+
+ return Promise.all(tasks);
+ },
+
+ stripTags: function(s) {
+ if (!s.match(/[<>]/))
+ return s;
+
+ var x = E('div', {}, s);
+ return x.textContent || x.innerText || '';
+ }
+});
+
+var CBIMap = CBINode.extend({
+ __init__: function(config /*, ... */) {
+ this.super('__init__', this.varargs(arguments, 1));
+
+ this.config = config;
+ this.parsechain = [ config ];
+ },
+
+ findElements: function(/* ... */) {
+ var q = null;
+
+ if (arguments.length == 1)
+ q = arguments[0];
+ else if (arguments.length == 2)
+ q = '[%s="%s"]'.format(arguments[0], arguments[1]);
+ else
+ L.error('InternalError', 'Expecting one or two arguments to findElements()');
+
+ return this.root.querySelectorAll(q);
+ },
+
+ findElement: function(/* ... */) {
+ var res = this.findElements.apply(this, arguments);
+ return res.length ? res[0] : null;
+ },
+
+ chain: function(config) {
+ if (this.parsechain.indexOf(config) == -1)
+ this.parsechain.push(config);
+ },
+
+ section: function(cbiClass /*, ... */) {
+ if (!CBIAbstractSection.isSubclass(cbiClass))
+ L.error('TypeError', 'Class must be a descendent of CBIAbstractSection');
+
+ var obj = cbiClass.instantiate(this.varargs(arguments, 1, this));
+ this.append(obj);
+ return obj;
+ },
+
+ load: function() {
+ return uci.load(this.parsechain || [ this.config ])
+ .then(this.loadChildren.bind(this));
+ },
+
+ parse: function() {
+ var tasks = [];
+
+ if (Array.isArray(this.children))
+ for (var i = 0; i < this.children.length; i++)
+ tasks.push(this.children[i].parse());
+
+ return Promise.all(tasks);
+ },
+
+ save: function() {
+ this.checkDepends();
+
+ return this.parse()
+ .then(uci.save.bind(uci))
+ .then(this.load.bind(this))
+ .then(this.renderContents.bind(this))
+ .catch(function(e) {
+ alert('Cannot save due to invalid values')
+ return Promise.reject();
+ });
+ },
+
+ reset: function() {
+ return this.renderContents();
+ },
+
+ render: function() {
+ return this.load().then(this.renderContents.bind(this));
+ },
+
+ renderContents: function() {
+ var mapEl = this.root || (this.root = E('div', {
+ 'id': 'cbi-%s'.format(this.config),
+ 'class': 'cbi-map',
+ 'cbi-dependency-check': L.bind(this.checkDepends, this)
+ }));
+
+ L.dom.bindClassInstance(mapEl, this);
+
+ return this.renderChildren(null).then(L.bind(function(nodes) {
+ var initialRender = !mapEl.firstChild;
+
+ L.dom.content(mapEl, null);
+
+ if (this.title != null && this.title != '')
+ mapEl.appendChild(E('h2', { 'name': 'content' }, this.title));
+
+ if (this.description != null && this.description != '')
+ mapEl.appendChild(E('div', { 'class': 'cbi-map-descr' }, this.description));
+
+ L.dom.append(mapEl, nodes);
+
+ if (!initialRender) {
+ mapEl.classList.remove('flash');
+
+ window.setTimeout(function() {
+ mapEl.classList.add('flash');
+ }, 1);
+ }
+
+ this.checkDepends();
+
+ return mapEl;
+ }, this));
+ },
+
+ lookupOption: function(name, section_id) {
+ var id, elem, sid, inst;
+
+ if (name.indexOf('.') > -1)
+ id = 'cbid.%s'.format(name);
+ else
+ id = 'cbid.%s.%s.%s'.format(this.config, section_id, name);
+
+ elem = this.findElement('data-field', id);
+ sid = elem ? id.split(/\./)[2] : null;
+ inst = elem ? L.dom.findClassInstance(elem) : null;
+
+ return (inst instanceof CBIAbstractValue) ? [ inst, sid ] : null;
+ },
+
+ checkDepends: function(ev, n) {
+ var changed = false;
+
+ for (var i = 0, s = this.children[0]; (s = this.children[i]) != null; i++)
+ if (s.checkDepends(ev, n))
+ changed = true;
+
+ if (changed && (n || 0) < 10)
+ this.checkDepends(ev, (n || 10) + 1);
+
+ ui.tabs.updateTabs(ev, this.root);
+ }
+});
+
+var CBIAbstractSection = CBINode.extend({
+ __init__: function(map, sectionType /*, ... */) {
+ this.super('__init__', this.varargs(arguments, 2));
+
+ this.sectiontype = sectionType;
+ this.map = map;
+ this.config = map.config;
+
+ this.optional = true;
+ this.addremove = false;
+ this.dynamic = false;
+ },
+
+ cfgsections: function() {
+ L.error('InternalError', 'Not implemented');
+ },
+
+ filter: function(section_id) {
+ return true;
+ },
+
+ load: function() {
+ var section_ids = this.cfgsections(),
+ tasks = [];
+
+ if (Array.isArray(this.children))
+ for (var i = 0; i < section_ids.length; i++)
+ tasks.push(this.loadChildren(section_ids[i])
+ .then(Function.prototype.bind.call(function(section_id, set_values) {
+ for (var i = 0; i < set_values.length; i++)
+ this.children[i].cfgvalue(section_id, set_values[i]);
+ }, this, section_ids[i])));
+
+ return Promise.all(tasks);
+ },
+
+ parse: function() {
+ var section_ids = this.cfgsections(),
+ tasks = [];
+
+ if (Array.isArray(this.children))
+ for (var i = 0; i < section_ids.length; i++)
+ for (var j = 0; j < this.children.length; j++)
+ tasks.push(this.children[j].parse(section_ids[i]));
+
+ return Promise.all(tasks);
+ },
+
+ tab: function(name, title, description) {
+ if (this.tabs && this.tabs[name])
+ throw 'Tab already declared';
+
+ var entry = {
+ name: name,
+ title: title,
+ description: description,
+ children: []
+ };
+
+ this.tabs = this.tabs || [];
+ this.tabs.push(entry);
+ this.tabs[name] = entry;
+
+ this.tab_names = this.tab_names || [];
+ this.tab_names.push(name);
+ },
+
+ option: function(cbiClass /*, ... */) {
+ if (!CBIAbstractValue.isSubclass(cbiClass))
+ throw L.error('TypeError', 'Class must be a descendent of CBIAbstractValue');
+
+ var obj = cbiClass.instantiate(this.varargs(arguments, 1, this.map, this));
+ this.append(obj);
+ return obj;
+ },
+
+ taboption: function(tabName /*, ... */) {
+ if (!this.tabs || !this.tabs[tabName])
+ throw L.error('ReferenceError', 'Associated tab not declared');
+
+ var obj = this.option.apply(this, this.varargs(arguments, 1));
+ obj.tab = tabName;
+ this.tabs[tabName].children.push(obj);
+ return obj;
+ },
+
+ renderUCISection: function(section_id) {
+ var renderTasks = [];
+
+ if (!this.tabs)
+ return this.renderOptions(null, section_id);
+
+ for (var i = 0; i < this.tab_names.length; i++)
+ renderTasks.push(this.renderOptions(this.tab_names[i], section_id));
+
+ return Promise.all(renderTasks)
+ .then(this.renderTabContainers.bind(this, section_id));
+ },
+
+ renderTabContainers: function(section_id, nodes) {
+ var config_name = this.uciconfig || this.map.config,
+ containerEls = E([]);
+
+ for (var i = 0; i < nodes.length; i++) {
+ var tab_name = this.tab_names[i],
+ tab_data = this.tabs[tab_name],
+ containerEl = E('div', {
+ 'id': 'container.%s.%s.%s'.format(config_name, section_id, tab_name),
+ 'data-tab': tab_name,
+ 'data-tab-title': tab_data.title,
+ 'data-tab-active': tab_name === this.selected_tab
+ });
+
+ if (tab_data.description != null && tab_data.description != '')
+ containerEl.appendChild(
+ E('div', { 'class': 'cbi-tab-descr' }, tab_data.description));
+
+ containerEl.appendChild(nodes[i]);
+ containerEls.appendChild(containerEl);
+ }
+
+ return containerEls;
+ },
+
+ renderOptions: function(tab_name, section_id) {
+ var in_table = (this instanceof CBITableSection);
+ return this.renderChildren(tab_name, section_id, in_table).then(function(nodes) {
+ var optionEls = E([]);
+ for (var i = 0; i < nodes.length; i++)
+ optionEls.appendChild(nodes[i]);
+ return optionEls;
+ });
+ },
+
+ checkDepends: function(ev, n) {
+ var changed = false,
+ sids = this.cfgsections();
+
+ for (var i = 0, sid = sids[0]; (sid = sids[i]) != null; i++) {
+ for (var j = 0, o = this.children[0]; (o = this.children[j]) != null; j++) {
+ var isActive = o.isActive(sid),
+ isSatisified = o.checkDepends(sid);
+
+ if (isActive != isSatisified) {
+ o.setActive(sid, !isActive);
+ changed = true;
+ }
+
+ if (!n && isActive)
+ o.triggerValidation(sid);
+ }
+ }
+
+ return changed;
+ }
+});
+
+
+var isEqual = function(x, y) {
+ if (x != null && y != null && typeof(x) != typeof(y))
+ return false;
+
+ if ((x == null && y != null) || (x != null && y == null))
+ return false;
+
+ if (Array.isArray(x)) {
+ if (x.length != y.length)
+ return false;
+
+ for (var i = 0; i < x.length; i++)
+ if (!isEqual(x[i], y[i]))
+ return false;
+ }
+ else if (typeof(x) == 'object') {
+ for (var k in x) {
+ if (x.hasOwnProperty(k) && !y.hasOwnProperty(k))
+ return false;
+
+ if (!isEqual(x[k], y[k]))
+ return false;
+ }
+
+ for (var k in y)
+ if (y.hasOwnProperty(k) && !x.hasOwnProperty(k))
+ return false;
+ }
+ else if (x != y) {
+ return false;
+ }
+
+ return true;
+};
+
+var CBIAbstractValue = CBINode.extend({
+ __init__: function(map, section, option /*, ... */) {
+ this.super('__init__', this.varargs(arguments, 3));
+
+ this.section = section;
+ this.option = option;
+ this.map = map;
+ this.config = map.config;
+
+ this.deps = [];
+ this.initial = {};
+ this.rmempty = true;
+ this.default = null;
+ this.size = null;
+ this.optional = false;
+ },
+
+ depends: function(field, value) {
+ var deps;
+
+ if (typeof(field) === 'string')
+ deps = {}, deps[field] = value;
+ else
+ deps = field;
+
+ this.deps.push(deps);
+ },
+
+ transformDepList: function(section_id, deplist) {
+ var list = deplist || this.deps,
+ deps = [];
+
+ if (Array.isArray(list)) {
+ for (var i = 0; i < list.length; i++) {
+ var dep = {};
+
+ for (var k in list[i]) {
+ if (list[i].hasOwnProperty(k)) {
+ if (k.charAt(0) === '!')
+ dep[k] = list[i][k];
+ else if (k.indexOf('.') !== -1)
+ dep['cbid.%s'.format(k)] = list[i][k];
+ else
+ dep['cbid.%s.%s.%s'.format(this.config, this.ucisection || section_id, k)] = list[i][k];
+ }
+ }
+
+ for (var k in dep) {
+ if (dep.hasOwnProperty(k)) {
+ deps.push(dep);
+ break;
+ }
+ }
+ }
+ }
+
+ return deps;
+ },
+
+ transformChoices: function() {
+ if (!Array.isArray(this.keylist) || this.keylist.length == 0)
+ return null;
+
+ var choices = {};
+
+ for (var i = 0; i < this.keylist.length; i++)
+ choices[this.keylist[i]] = this.vallist[i];
+
+ return choices;
+ },
+
+ checkDepends: function(section_id) {
+ var def = false;
+
+ if (!Array.isArray(this.deps) || !this.deps.length)
+ return true;
+
+ for (var i = 0; i < this.deps.length; i++) {
+ var istat = true,
+ reverse = false;
+
+ for (var dep in this.deps[i]) {
+ if (dep == '!reverse') {
+ reverse = true;
+ }
+ else if (dep == '!default') {
+ def = true;
+ istat = false;
+ }
+ else {
+ var res = this.map.lookupOption(dep, section_id),
+ val = res ? res[0].formvalue(res[1]) : null;
+
+ istat = (istat && isEqual(val, this.deps[i][dep]));
+ }
+ }
+
+ if (istat ^ reverse)
+ return true;
+ }
+
+ return def;
+ },
+
+ cbid: function(section_id) {
+ if (section_id == null)
+ L.error('TypeError', 'Section ID required');
+
+ return 'cbid.%s.%s.%s'.format(this.map.config, section_id, this.option);
+ },
+
+ load: function(section_id) {
+ if (section_id == null)
+ L.error('TypeError', 'Section ID required');
+
+ return uci.get(
+ this.uciconfig || this.map.config,
+ this.ucisection || section_id,
+ this.ucioption || this.option);
+ },
+
+ cfgvalue: function(section_id, set_value) {
+ if (section_id == null)
+ L.error('TypeError', 'Section ID required');
+
+ if (arguments.length == 2) {
+ this.data = this.data || {};
+ this.data[section_id] = set_value;
+ }
+
+ return this.data ? this.data[section_id] : null;
+ },
+
+ formvalue: function(section_id) {
+ var node = this.map.findElement('id', this.cbid(section_id));
+ return node ? L.dom.callClassMethod(node, 'getValue') : null;
+ },
+
+ textvalue: function(section_id) {
+ var cval = this.cfgvalue(section_id);
+
+ if (cval == null)
+ cval = this.default;
+
+ return (cval != null) ? '%h'.format(cval) : null;
+ },
+
+ validate: function(section_id, value) {
+ return true;
+ },
+
+ isValid: function(section_id) {
+ var node = this.map.findElement('id', this.cbid(section_id));
+ return node ? L.dom.callClassMethod(node, 'isValid') : true;
+ },
+
+ isActive: function(section_id) {
+ var field = this.map.findElement('data-field', this.cbid(section_id));
+ return (field != null && !field.classList.contains('hidden'));
+ },
+
+ setActive: function(section_id, active) {
+ var field = this.map.findElement('data-field', this.cbid(section_id));
+
+ if (field && field.classList.contains('hidden') == active) {
+ field.classList[active ? 'remove' : 'add']('hidden');
+ return true;
+ }
+
+ return false;
+ },
+
+ triggerValidation: function(section_id) {
+ var node = this.map.findElement('id', this.cbid(section_id));
+ return node ? L.dom.callClassMethod(node, 'triggerValidation') : true;
+ },
+
+ parse: function(section_id) {
+ var active = this.isActive(section_id),
+ cval = this.cfgvalue(section_id),
+ fval = active ? this.formvalue(section_id) : null;
+
+ if (active && !this.isValid(section_id))
+ return Promise.reject();
+
+ if (fval != '' && fval != null) {
+ if (this.forcewrite || !isEqual(cval, fval))
+ return Promise.resolve(this.write(section_id, fval));
+ }
+ else {
+ if (this.rmempty || this.optional) {
+ return Promise.resolve(this.remove(section_id));
+ }
+ else if (!isEqual(cval, fval)) {
+ console.log('This should have been catched by isValid()');
+ return Promise.reject();
+ }
+ }
+
+ return Promise.resolve();
+ },
+
+ write: function(section_id, formvalue) {
+ return uci.set(
+ this.uciconfig || this.map.config,
+ this.ucisection || section_id,
+ this.ucioption || this.option,
+ formvalue);
+ },
+
+ remove: function(section_id) {
+ return uci.unset(
+ this.uciconfig || this.map.config,
+ this.ucisection || section_id,
+ this.ucioption || this.option);
+ }
+});
+
+var CBITypedSection = CBIAbstractSection.extend({
+ __name__: 'CBI.TypedSection',
+
+ cfgsections: function() {
+ return uci.sections(this.uciconfig || this.map.config, this.sectiontype)
+ .map(function(s) { return s['.name'] })
+ .filter(L.bind(this.filter, this));
+ },
+
+ handleAdd: function(ev, name) {
+ var config_name = this.uciconfig || this.map.config;
+
+ uci.add(config_name, this.sectiontype, name);
+ this.map.save();
+ },
+
+ handleRemove: function(section_id, ev) {
+ var config_name = this.uciconfig || this.map.config;
+
+ uci.remove(config_name, section_id);
+ this.map.save();
+ },
+
+ renderSectionAdd: function(extra_class) {
+ if (!this.addremove)
+ return E([]);
+
+ var createEl = E('div', { 'class': 'cbi-section-create' }),
+ config_name = this.uciconfig || this.map.config;
+
+ if (extra_class != null)
+ createEl.classList.add(extra_class);
+
+ if (this.anonymous) {
+ createEl.appendChild(E('input', {
+ 'type': 'submit',
+ 'class': 'cbi-button cbi-button-add',
+ 'value': _('Add'),
+ 'title': _('Add'),
+ 'click': L.bind(this.handleAdd, this)
+ }));
+ }
+ else {
+ var nameEl = E('input', {
+ 'type': 'text',
+ 'class': 'cbi-section-create-name'
+ });
+
+ L.dom.append(createEl, [
+ E('div', {}, nameEl),
+ E('input', {
+ 'class': 'cbi-button cbi-button-add',
+ 'type': 'submit',
+ 'value': _('Add'),
+ 'title': _('Add'),
+ 'click': L.bind(function(ev) {
+ if (nameEl.classList.contains('cbi-input-invalid'))
+ return;
+
+ this.handleAdd(ev, nameEl.value);
+ }, this)
+ })
+ ]);
+
+ ui.addValidator(nameEl, 'uciname', true, 'blur', 'keyup');
+ }
+
+ return createEl;
+ },
+
+ renderContents: function(cfgsections, nodes) {
+ var section_id = null,
+ config_name = this.uciconfig || this.map.config,
+ sectionEl = E('div', {
+ 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
+ 'class': 'cbi-section'
+ });
+
+ if (this.title != null && this.title != '')
+ sectionEl.appendChild(E('legend', {}, this.title));
+
+ if (this.description != null && this.description != '')
+ sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
+
+ for (var i = 0; i < nodes.length; i++) {
+ if (this.addremove) {
+ sectionEl.appendChild(
+ E('div', { 'class': 'cbi-section-remove right' },
+ E('input', {
+ 'type': 'submit',
+ 'class': 'cbi-button',
+ 'name': 'cbi.rts.%s.%s'.format(config_name, cfgsections[i]),
+ 'value': _('Delete'),
+ 'data-section-id': cfgsections[i],
+ 'click': L.bind(this.handleRemove, this, cfgsections[i])
+ })));
+ }
+
+ if (!this.anonymous)
+ sectionEl.appendChild(E('h3', cfgsections[i].toUpperCase()));
+
+ sectionEl.appendChild(E('div', {
+ 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
+ 'class': this.tabs
+ ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node'
+ }, nodes[i]));
+
+ if (this.tabs)
+ ui.tabs.initTabGroup(sectionEl.lastChild.childNodes);
+ }
+
+ if (nodes.length == 0)
+ L.dom.append(sectionEl, [
+ E('em', _('This section contains no values yet')),
+ E('br'), E('br')
+ ]);
+
+ sectionEl.appendChild(this.renderSectionAdd());
+
+ L.dom.bindClassInstance(sectionEl, this);
+
+ return sectionEl;
+ },
+
+ render: function() {
+ var cfgsections = this.cfgsections(),
+ renderTasks = [];
+
+ for (var i = 0; i < cfgsections.length; i++)
+ renderTasks.push(this.renderUCISection(cfgsections[i]));
+
+ return Promise.all(renderTasks).then(this.renderContents.bind(this, cfgsections));
+ }
+});
+
+var CBITableSection = CBITypedSection.extend({
+ __name__: 'CBI.TableSection',
+
+ tab: function() {
+ throw 'Tabs are not supported by TableSection';
+ },
+
+ renderContents: function(cfgsections, nodes) {
+ var section_id = null,
+ config_name = this.uciconfig || this.map.config,
+ max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
+ has_more = max_cols < this.children.length,
+ sectionEl = E('div', {
+ 'id': 'cbi-%s-%s'.format(config_name, this.sectiontype),
+ 'class': 'cbi-section cbi-tblsection'
+ }),
+ tableEl = E('div', {
+ 'class': 'table cbi-section-table'
+ });
+
+ if (this.title != null && this.title != '')
+ sectionEl.appendChild(E('h3', {}, this.title));
+
+ if (this.description != null && this.description != '')
+ sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
+
+ tableEl.appendChild(this.renderHeaderRows(max_cols));
+
+ for (var i = 0; i < nodes.length; i++) {
+ var sectionname = this.stripTags((typeof(this.sectiontitle) == 'function')
+ ? String(this.sectiontitle(cfgsections[i]) || '') : cfgsections[i]).trim();
+
+ var trEl = E('div', {
+ 'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
+ 'class': 'tr cbi-section-table-row',
+ 'data-sid': cfgsections[i],
+ 'draggable': this.sortable ? true : null,
+ 'mousedown': this.sortable ? L.bind(this.handleDragInit, this) : null,
+ 'dragstart': this.sortable ? L.bind(this.handleDragStart, this) : null,
+ 'dragover': this.sortable ? L.bind(this.handleDragOver, this) : null,
+ 'dragenter': this.sortable ? L.bind(this.handleDragEnter, this) : null,
+ 'dragleave': this.sortable ? L.bind(this.handleDragLeave, this) : null,
+ 'dragend': this.sortable ? L.bind(this.handleDragEnd, this) : null,
+ 'drop': this.sortable ? L.bind(this.handleDrop, this) : null,
+ 'data-title': (sectionname && (!this.anonymous || this.sectiontitle)) ? sectionname : null
+ });
+
+ if (this.extedit || this.rowcolors)
+ trEl.classList.add(!(tableEl.childNodes.length % 2)
+ ? 'cbi-rowstyle-1' : 'cbi-rowstyle-2');
+
+ for (var j = 0; j < max_cols && nodes[i].firstChild; j++)
+ trEl.appendChild(nodes[i].firstChild);
+
+ trEl.appendChild(this.renderRowActions(cfgsections[i], has_more ? _('More…') : null));
+ tableEl.appendChild(trEl);
+ }
+
+ if (nodes.length == 0)
+ tableEl.appendChild(E('div', { 'class': 'tr cbi-section-table-row placeholder' },
+ E('div', { 'class': 'td' },
+ E('em', {}, _('This section contains no values yet')))));
+
+ sectionEl.appendChild(tableEl);
+
+ sectionEl.appendChild(this.renderSectionAdd('cbi-tblsection-create'));
+
+ L.dom.bindClassInstance(sectionEl, this);
+
+ return sectionEl;
+ },
+
+ renderHeaderRows: function(max_cols) {
+ var has_titles = false,
+ has_descriptions = false,
+ anon_class = (!this.anonymous || this.sectiontitle) ? 'named' : 'anonymous',
+ trEls = E([]);
+
+ for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
+ if (opt.optional || opt.modalonly)
+ continue;
+
+ has_titles = has_titles || !!opt.title;
+ has_descriptions = has_descriptions || !!opt.description;
+ }
+
+ if (has_titles) {
+ var trEl = E('div', {
+ 'class': 'tr cbi-section-table-titles ' + anon_class,
+ 'data-title': (!this.anonymous || this.sectiontitle) ? _('Name') : null
+ });
+
+ for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
+ if (opt.optional || opt.modalonly)
+ continue;
+
+ trEl.appendChild(E('div', {
+ 'class': 'th cbi-section-table-cell',
+ 'data-type': opt.__name__
+ }));
+
+ if (opt.width != null)
+ trEl.lastElementChild.style.width =
+ (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
+
+ if (opt.titleref)
+ trEl.lastElementChild.appendChild(E('a', {
+ 'href': opt.titleref,
+ 'class': 'cbi-title-ref',
+ 'title': this.titledesc || _('Go to relevant configuration page')
+ }, opt.title));
+ else
+ L.dom.content(trEl.lastElementChild, opt.title);
+ }
+
+ if (this.sortable || this.extedit || this.addremove || has_more)
+ trEl.appendChild(E('div', {
+ 'class': 'th cbi-section-table-cell cbi-section-actions'
+ }));
+
+ trEls.appendChild(trEl);
+ }
+
+ if (has_descriptions) {
+ var trEl = E('div', {
+ 'class': 'tr cbi-section-table-descr ' + anon_class
+ });
+
+ for (var i = 0, opt; i < max_cols && (opt = this.children[i]) != null; i++) {
+ if (opt.optional || opt.modalonly)
+ continue;
+
+ trEl.appendChild(E('div', {
+ 'class': 'th cbi-section-table-cell',
+ 'data-type': opt.__name__
+ }, opt.description));
+
+ if (opt.width != null)
+ trEl.lastElementChild.style.width =
+ (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
+ }
+
+ if (this.sortable || this.extedit || this.addremove || has_more)
+ trEl.appendChild(E('div', {
+ 'class': 'th cbi-section-table-cell cbi-section-actions'
+ }));
+
+ trEls.appendChild(trEl);
+ }
+
+ return trEls;
+ },
+
+ renderRowActions: function(section_id, more_label) {
+ var config_name = this.uciconfig || this.map.config;
+
+ if (!this.sortable && !this.extedit && !this.addremove && !more_label)
+ return E([]);
+
+ var tdEl = E('div', {
+ 'class': 'td cbi-section-table-cell nowrap cbi-section-actions'
+ }, E('div'));
+
+ if (this.sortable) {
+ L.dom.append(tdEl.lastElementChild, [
+ E('div', {
+ 'title': _('Drag to reorder'),
+ 'class': 'cbi-button drag-handle center',
+ 'style': 'cursor:move'
+ }, '☰')
+ ]);
+ }
+
+ if (this.extedit) {
+ var evFn = null;
+
+ if (typeof(this.extedit) == 'function')
+ evFn = L.bind(this.extedit, this);
+ else if (typeof(this.extedit) == 'string')
+ evFn = L.bind(function(sid, ev) {
+ location.href = this.extedit.format(sid);
+ }, this, section_id);
+
+ L.dom.append(tdEl.lastElementChild,
+ E('input', {
+ 'type': 'button',
+ 'value': _('Edit'),
+ 'title': _('Edit'),
+ 'class': 'cbi-button cbi-button-edit',
+ 'click': evFn
+ })
+ );
+ }
+
+ if (more_label) {
+ L.dom.append(tdEl.lastElementChild,
+ E('input', {
+ 'type': 'button',
+ 'value': more_label,
+ 'title': more_label,
+ 'class': 'cbi-button cbi-button-edit',
+ 'click': L.bind(this.renderMoreOptionsModal, this, section_id)
+ })
+ );
+ }
+
+ if (this.addremove) {
+ L.dom.append(tdEl.lastElementChild,
+ E('input', {
+ 'type': 'submit',
+ 'value': _('Delete'),
+ 'title': _('Delete'),
+ 'class': 'cbi-button cbi-button-remove',
+ 'click': L.bind(function(sid, ev) {
+ uci.remove(config_name, sid);
+ this.map.save();
+ }, this, section_id)
+ })
+ );
+ }
+
+ return tdEl;
+ },
+
+ handleDragInit: function(ev) {
+ scope.dragState = { node: ev.target };
+ },
+
+ handleDragStart: function(ev) {
+ if (!scope.dragState || !scope.dragState.node.classList.contains('drag-handle')) {
+ scope.dragState = null;
+ ev.preventDefault();
+ return false;
+ }
+
+ scope.dragState.node = L.dom.parent(scope.dragState.node, '.tr');
+ ev.dataTransfer.setData('text', 'drag');
+ ev.target.style.opacity = 0.4;
+ },
+
+ handleDragOver: function(ev) {
+ var n = scope.dragState.targetNode,
+ r = scope.dragState.rect,
+ t = r.top + r.height / 2;
+
+ if (ev.clientY <= t) {
+ n.classList.remove('drag-over-below');
+ n.classList.add('drag-over-above');
+ }
+ else {
+ n.classList.remove('drag-over-above');
+ n.classList.add('drag-over-below');
+ }
+
+ ev.dataTransfer.dropEffect = 'move';
+ ev.preventDefault();
+ return false;
+ },
+
+ handleDragEnter: function(ev) {
+ scope.dragState.rect = ev.currentTarget.getBoundingClientRect();
+ scope.dragState.targetNode = ev.currentTarget;
+ },
+
+ handleDragLeave: function(ev) {
+ ev.currentTarget.classList.remove('drag-over-above');
+ ev.currentTarget.classList.remove('drag-over-below');
+ },
+
+ handleDragEnd: function(ev) {
+ var n = ev.target;
+
+ n.style.opacity = '';
+ n.classList.add('flash');
+ n.parentNode.querySelectorAll('.drag-over-above, .drag-over-below')
+ .forEach(function(tr) {
+ tr.classList.remove('drag-over-above');
+ tr.classList.remove('drag-over-below');
+ });
+ },
+
+ handleDrop: function(ev) {
+ var s = scope.dragState;
+
+ if (s.node && s.targetNode) {
+ var config_name = this.uciconfig || this.map.config,
+ ref_node = s.targetNode,
+ after = false;
+
+ if (ref_node.classList.contains('drag-over-below')) {
+ ref_node = ref_node.nextElementSibling;
+ after = true;
+ }
+
+ var sid1 = s.node.getAttribute('data-sid'),
+ sid2 = s.targetNode.getAttribute('data-sid');
+
+ s.node.parentNode.insertBefore(s.node, ref_node);
+ uci.move(config_name, sid1, sid2, after);
+ }
+
+ scope.dragState = null;
+ ev.target.style.opacity = '';
+ ev.stopPropagation();
+ ev.preventDefault();
+ return false;
+ },
+
+ handleModalCancel: function(modalMap, ev) {
+ return Promise.resolve(L.ui.hideModal());
+ },
+
+ handleModalSave: function(modalMap, ev) {
+ return modalMap.save()
+ .then(L.bind(this.map.load, this.map))
+ .then(L.bind(this.map.reset, this.map))
+ .then(L.ui.hideModal)
+ .catch(function() {});
+ },
+
+ addModalOptions: function(modalSection, section_id, ev) {
+
+ },
+
+ renderMoreOptionsModal: function(section_id, ev) {
+ var parent = this.map,
+ title = parent.title,
+ name = null,
+ m = new CBIMap(this.map.config, null, null),
+ s = m.section(CBINamedSection, section_id, this.sectiontype);
+ s.tabs = this.tabs;
+ s.tab_names = this.tab_names;
+
+ if (typeof(this.sectiontitle) == 'function')
+ name = this.stripTags(String(this.sectiontitle(section_id) || ''));
+ else if (!this.anonymous)
+ name = section_id;
+
+ if (name)
+ title += ' - ' + name;
+
+ for (var i = 0; i < this.children.length; i++) {
+ var o1 = this.children[i];
+
+ if (o1.modalonly === false)
+ continue;
+
+ var o2 = s.option(o1.constructor, o1.option, o1.title, o1.description);
+
+ for (var k in o1) {
+ if (!o1.hasOwnProperty(k))
+ continue;
+
+ switch (k) {
+ case 'map':
+ case 'section':
+ case 'option':
+ case 'title':
+ case 'description':
+ continue;
+
+ default:
+ o2[k] = o1[k];
+ }
+ }
+ }
+
+ //ev.target.classList.add('spinning');
+ Promise.resolve(this.addModalOptions(s, section_id, ev)).then(L.bind(m.render, m)).then(L.bind(function(nodes) {
+ //ev.target.classList.remove('spinning');
+ L.ui.showModal(title, [
+ nodes,
+ E('div', { 'class': 'right' }, [
+ E('input', {
+ 'type': 'button',
+ 'class': 'btn',
+ 'click': L.bind(this.handleModalCancel, this, m),
+ 'value': _('Dismiss')
+ }), ' ',
+ E('input', {
+ 'type': 'button',
+ 'class': 'cbi-button cbi-button-positive important',
+ 'click': L.bind(this.handleModalSave, this, m),
+ 'value': _('Save')
+ })
+ ])
+ ], 'cbi-modal');
+ }, this)).catch(L.error);
+ }
+});
+
+var CBIGridSection = CBITableSection.extend({
+ tab: function(name, title, description) {
+ CBIAbstractSection.prototype.tab.call(this, name, title, description);
+ },
+
+ handleAdd: function(ev) {
+ var config_name = this.uciconfig || this.map.config,
+ section_id = uci.add(config_name, this.sectiontype);
+
+ this.addedSection = section_id;
+ this.renderMoreOptionsModal(section_id);
+ },
+
+ handleModalSave: function(/* ... */) {
+ return this.super('handleModalSave', arguments)
+ .then(L.bind(function() { this.addedSection = null }, this));
+ },
+
+ handleModalCancel: function(/* ... */) {
+ var config_name = this.uciconfig || this.map.config;
+
+ if (this.addedSection != null) {
+ uci.remove(config_name, this.addedSection);
+ this.addedSection = null;
+ }
+
+ return this.super('handleModalCancel', arguments);
+ },
+
+ renderUCISection: function(section_id) {
+ return this.renderOptions(null, section_id);
+ },
+
+ renderChildren: function(tab_name, section_id, in_table) {
+ var tasks = [], index = 0;
+
+ for (var i = 0, opt; (opt = this.children[i]) != null; i++) {
+ if (opt.disable || opt.modalonly)
+ continue;
+
+ if (opt.editable)
+ tasks.push(opt.render(index++, section_id, in_table));
+ else
+ tasks.push(this.renderTextValue(section_id, opt));
+ }
+
+ return Promise.all(tasks);
+ },
+
+ renderTextValue: function(section_id, opt) {
+ var title = this.stripTags(opt.title).trim(),
+ descr = this.stripTags(opt.description).trim(),
+ value = opt.textvalue(section_id);
+
+ return E('div', {
+ 'class': 'td cbi-value-field',
+ 'data-title': (title != '') ? title : opt.option,
+ 'data-description': (descr != '') ? descr : null,
+ 'data-name': opt.option,
+ 'data-type': opt.typename || opt.__name__
+ }, (value != null) ? value : E('em', _('none')));
+ },
+
+ renderRowActions: function(section_id) {
+ return this.super('renderRowActions', [ section_id, _('Edit') ]);
+ },
+
+ parse: function() {
+ var section_ids = this.cfgsections(),
+ tasks = [];
+
+ if (Array.isArray(this.children)) {
+ for (var i = 0; i < section_ids.length; i++) {
+ for (var j = 0; j < this.children.length; j++) {
+ if (!this.children[j].editable || this.children[j].modalonly)
+ continue;
+
+ tasks.push(this.children[j].parse(section_ids[i]));
+ }
+ }
+ }
+
+ return Promise.all(tasks);
+ }
+});
+
+var CBINamedSection = CBIAbstractSection.extend({
+ __name__: 'CBI.NamedSection',
+ __init__: function(map, section_id /*, ... */) {
+ this.super('__init__', this.varargs(arguments, 2, map));
+
+ this.section = section_id;
+ },
+
+ cfgsections: function() {
+ return [ this.section ];
+ },
+
+ handleAdd: function(ev) {
+ var section_id = this.section,
+ config_name = this.uciconfig || this.map.config;
+
+ uci.add(config_name, this.sectiontype, section_id);
+ this.map.save();
+ },
+
+ handleRemove: function(ev) {
+ var section_id = this.section,
+ config_name = this.uciconfig || this.map.config;
+
+ uci.remove(config_name, section_id);
+ this.map.save();
+ },
+
+ renderContents: function(data) {
+ var ucidata = data[0], nodes = data[1],
+ section_id = this.section,
+ config_name = this.uciconfig || this.map.config,
+ sectionEl = E('div', {
+ 'id': ucidata ? null : 'cbi-%s-%s'.format(config_name, section_id),
+ 'class': 'cbi-section'
+ });
+
+ if (typeof(this.title) === 'string' && this.title !== '')
+ sectionEl.appendChild(E('legend', {}, this.title));
+
+ if (typeof(this.description) === 'string' && this.description !== '')
+ sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description));
+
+ if (ucidata) {
+ if (this.addremove) {
+ sectionEl.appendChild(
+ E('div', { 'class': 'cbi-section-remove right' },
+ E('input', {
+ 'type': 'submit',
+ 'class': 'cbi-button',
+ 'value': _('Delete'),
+ 'click': L.bind(this.handleRemove, this)
+ })));
+ }
+
+ sectionEl.appendChild(E('div', {
+ 'id': 'cbi-%s-%s'.format(config_name, section_id),
+ 'class': this.tabs
+ ? 'cbi-section-node cbi-section-node-tabbed' : 'cbi-section-node'
+ }, nodes));
+
+ if (this.tabs)
+ ui.tabs.initTabGroup(sectionEl.lastChild.childNodes);
+ }
+ else if (this.addremove) {
+ sectionEl.appendChild(
+ E('input', {
+ 'type': 'submit',
+ 'class': 'cbi-button cbi-button-add',
+ 'value': _('Add'),
+ 'click': L.bind(this.handleAdd, this)
+ }));
+ }
+
+ L.dom.bindClassInstance(sectionEl, this);
+
+ return sectionEl;
+ },
+
+ render: function() {
+ var config_name = this.uciconfig || this.map.config,
+ section_id = this.section;
+
+ return Promise.all([
+ uci.get(config_name, section_id),
+ this.renderUCISection(section_id)
+ ]).then(this.renderContents.bind(this));
+ }
+});
+
+var CBIValue = CBIAbstractValue.extend({
+ __name__: 'CBI.Value',
+
+ value: function(key, val) {
+ this.keylist = this.keylist || [];
+ this.keylist.push(String(key));
+
+ this.vallist = this.vallist || [];
+ this.vallist.push(String(val != null ? val : key));
+ },
+
+ render: function(option_index, section_id, in_table) {
+ return Promise.resolve(this.cfgvalue(section_id))
+ .then(this.renderWidget.bind(this, section_id, option_index))
+ .then(this.renderFrame.bind(this, section_id, in_table, option_index));
+ },
+
+ renderFrame: function(section_id, in_table, option_index, nodes) {
+ var config_name = this.uciconfig || this.map.config,
+ depend_list = this.transformDepList(section_id),
+ optionEl;
+
+ if (in_table) {
+ optionEl = E('div', {
+ 'class': 'td cbi-value-field',
+ 'data-title': this.stripTags(this.title).trim(),
+ 'data-description': this.stripTags(this.description).trim(),
+ 'data-name': this.option,
+ 'data-type': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
+ }, E('div', {
+ 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
+ 'data-index': option_index,
+ 'data-depends': depend_list,
+ 'data-field': this.cbid(section_id)
+ }));
+ }
+ else {
+ optionEl = E('div', {
+ 'class': 'cbi-value',
+ 'id': 'cbi-%s-%s-%s'.format(config_name, section_id, this.option),
+ 'data-index': option_index,
+ 'data-depends': depend_list,
+ 'data-field': this.cbid(section_id),
+ 'data-name': this.option,
+ 'data-type': this.typename || (this.template ? this.template.replace(/^.+\//, '') : null) || this.__name__
+ });
+
+ if (this.last_child)
+ optionEl.classList.add('cbi-value-last');
+
+ if (typeof(this.title) === 'string' && this.title !== '') {
+ optionEl.appendChild(E('label', {
+ 'class': 'cbi-value-title',
+ 'for': 'widget.cbid.%s.%s.%s'.format(config_name, section_id, this.option)
+ },
+ this.titleref ? E('a', {
+ 'class': 'cbi-title-ref',
+ 'href': this.titleref,
+ 'title': this.titledesc || _('Go to relevant configuration page')
+ }, this.title) : this.title));
+
+ optionEl.appendChild(E('div', { 'class': 'cbi-value-field' }));
+ }
+ }
+
+ if (nodes)
+ (optionEl.lastChild || optionEl).appendChild(nodes);
+
+ if (!in_table && typeof(this.description) === 'string' && this.description !== '')
+ L.dom.append(optionEl.lastChild || optionEl,
+ E('div', { 'class': 'cbi-value-description' }, this.description));
+
+ if (depend_list && depend_list.length)
+ optionEl.classList.add('hidden');
+
+ optionEl.addEventListener('widget-change',
+ L.bind(this.map.checkDepends, this.map));
+
+ L.dom.bindClassInstance(optionEl, this);
+
+ return optionEl;
+ },
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var value = (cfgvalue != null) ? cfgvalue : this.default,
+ choices = this.transformChoices(),
+ widget;
+
+ if (choices) {
+ var placeholder = (this.optional || this.rmempty)
+ ? E('em', _('unspecified')) : _('-- Please choose --');
+
+ widget = new ui.Combobox(Array.isArray(value) ? value.join(' ') : value, choices, {
+ id: this.cbid(section_id),
+ sort: this.keylist,
+ optional: this.optional || this.rmempty,
+ datatype: this.datatype,
+ select_placeholder: this.placeholder || placeholder,
+ validate: L.bind(this.validate, this, section_id)
+ });
+ }
+ else {
+ widget = new ui.Textfield(Array.isArray(value) ? value.join(' ') : value, {
+ id: this.cbid(section_id),
+ password: this.password,
+ optional: this.optional || this.rmempty,
+ datatype: this.datatype,
+ placeholder: this.placeholder,
+ validate: L.bind(this.validate, this, section_id)
+ });
+ }
+
+ return widget.render();
+ }
+});
+
+var CBIDynamicList = CBIValue.extend({
+ __name__: 'CBI.DynamicList',
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var value = (cfgvalue != null) ? cfgvalue : this.default,
+ choices = this.transformChoices(),
+ items = L.toArray(value);
+
+ var widget = new ui.DynamicList(items, choices, {
+ id: this.cbid(section_id),
+ sort: this.keylist,
+ optional: this.optional || this.rmempty,
+ datatype: this.datatype,
+ placeholder: this.placeholder,
+ validate: L.bind(this.validate, this, section_id)
+ });
+
+ return widget.render();
+ },
+});
+
+var CBIListValue = CBIValue.extend({
+ __name__: 'CBI.ListValue',
+
+ __init__: function() {
+ this.super('__init__', arguments);
+ this.widget = 'select';
+ this.deplist = [];
+ },
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var choices = this.transformChoices();
+ var widget = new ui.Select((cfgvalue != null) ? cfgvalue : this.default, choices, {
+ id: this.cbid(section_id),
+ size: this.size,
+ sort: this.keylist,
+ optional: this.optional,
+ placeholder: this.placeholder,
+ validate: L.bind(this.validate, this, section_id)
+ });
+
+ return widget.render();
+ },
+});
+
+var CBIFlagValue = CBIValue.extend({
+ __name__: 'CBI.FlagValue',
+
+ __init__: function() {
+ this.super('__init__', arguments);
+
+ this.enabled = '1';
+ this.disabled = '0';
+ this.default = this.disabled;
+ },
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ 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)
+ });
+
+ return widget.render();
+ },
+
+ formvalue: function(section_id) {
+ var node = this.map.findElement('id', this.cbid(section_id)),
+ checked = node ? L.dom.callClassMethod(node, 'isChecked') : false;
+
+ return checked ? this.enabled : this.disabled;
+ },
+
+ textvalue: function(section_id) {
+ var cval = this.cfgvalue(section_id);
+
+ if (cval == null)
+ cval = this.default;
+
+ return (cval == this.enabled) ? _('Yes') : _('No');
+ },
+
+ parse: function(section_id) {
+ if (this.isActive(section_id)) {
+ var fval = this.formvalue(section_id);
+
+ if (!this.isValid(section_id))
+ return Promise.reject();
+
+ if (fval == this.default && (this.optional || this.rmempty))
+ return Promise.resolve(this.remove(section_id));
+ else
+ return Promise.resolve(this.write(section_id, fval));
+ }
+ else {
+ return Promise.resolve(this.remove(section_id));
+ }
+ },
+});
+
+var CBIMultiValue = CBIDynamicList.extend({
+ __name__: 'CBI.MultiValue',
+
+ __init__: function() {
+ this.super('__init__', arguments);
+ this.placeholder = _('-- Please choose --');
+ },
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var value = (cfgvalue != null) ? cfgvalue : this.default,
+ choices = this.transformChoices();
+
+ var widget = new ui.Dropdown(L.toArray(value), choices, {
+ id: this.cbid(section_id),
+ sort: this.keylist,
+ multiple: true,
+ optional: this.optional || this.rmempty,
+ select_placeholder: this.placeholder,
+ display_items: this.display_size || this.size || 3,
+ dropdown_items: this.dropdown_size || this.size || -1,
+ validate: L.bind(this.validate, this, section_id)
+ });
+
+ return widget.render();
+ },
+});
+
+var CBIDummyValue = CBIValue.extend({
+ __name__: 'CBI.DummyValue',
+
+ 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');
+
+ if (this.href)
+ outputEl.appendChild(E('a', { 'href': this.href }));
+
+ L.dom.append(outputEl.lastChild || outputEl,
+ this.rawhtml ? value : [ value ]);
+
+ return E([
+ outputEl,
+ hiddenEl.render()
+ ]);
+ },
+});
+
+var CBIButtonValue = CBIValue.extend({
+ __name__: 'CBI.ButtonValue',
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var value = (cfgvalue != null) ? cfgvalue : this.default;
+
+ if (value !== false)
+ return E([
+ E('input', {
+ 'type': 'hidden',
+ 'id': this.cbid(section_id)
+ }),
+ E('input', {
+ 'class': 'cbi-button cbi-button-%s'.format(this.inputstyle || 'button'),
+ 'type': 'submit',
+ //'id': this.cbid(section_id),
+ //'name': this.cbid(section_id),
+ 'value': this.inputtitle || this.title,
+ 'click': L.bind(function(ev) {
+ ev.target.previousElementSibling.value = ev.target.value;
+ this.map.save();
+ }, this)
+ })
+ ]);
+ else
+ return document.createTextNode(' - ');
+ }
+});
+
+var CBIHiddenValue = CBIValue.extend({
+ __name__: 'CBI.HiddenValue',
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var widget = new ui.Hiddenfield((cfgvalue != null) ? cfgvalue : this.default, {
+ id: this.cbid(section_id)
+ });
+
+ return widget.render();
+ }
+});
+
+var CBISectionValue = CBIValue.extend({
+ __name__: 'CBI.ContainerValue',
+ __init__: function(map, section, option, cbiClass /*, ... */) {
+ this.super('__init__', [map, section, option]);
+
+ if (!CBIAbstractSection.isSubclass(cbiClass))
+ throw 'Sub section must be a descendent of CBIAbstractSection';
+
+ this.subsection = cbiClass.instantiate(this.varargs(arguments, 4, this.map));
+ },
+
+ load: function(section_id) {
+ return this.subsection.load();
+ },
+
+ parse: function(section_id) {
+ return this.subsection.parse();
+ },
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ return this.subsection.render();
+ },
+
+ checkDepends: function(section_id) {
+ this.subsection.checkDepends();
+ return this.super('checkDepends');
+ },
+
+ write: function() {},
+ remove: function() {},
+ cfgvalue: function() { return null },
+ formvalue: function() { return null }
+});
+
+return L.Class.extend({
+ Map: CBIMap,
+ AbstractSection: CBIAbstractSection,
+ AbstractValue: CBIAbstractValue,
+
+ TypedSection: CBITypedSection,
+ TableSection: CBITableSection,
+ GridSection: CBIGridSection,
+ NamedSection: CBINamedSection,
+
+ Value: CBIValue,
+ DynamicList: CBIDynamicList,
+ ListValue: CBIListValue,
+ Flag: CBIFlagValue,
+ MultiValue: CBIMultiValue,
+ DummyValue: CBIDummyValue,
+ Button: CBIButtonValue,
+ HiddenValue: CBIHiddenValue,
+ SectionValue: CBISectionValue
+});
diff --git a/modules/luci-base/htdocs/luci-static/resources/luci.js b/modules/luci-base/htdocs/luci-static/resources/luci.js
index 4cb8bf4e5d..66f32d7223 100644
--- a/modules/luci-base/htdocs/luci-static/resources/luci.js
+++ b/modules/luci-base/htdocs/luci-static/resources/luci.js
@@ -1,511 +1,1326 @@
(function(window, document, undefined) {
- var modalDiv = null,
- tooltipDiv = null,
- tooltipTimeout = null,
- dummyElem = null,
- domParser = null;
+ 'use strict';
+
+ /* Object.assign polyfill for IE */
+ if (typeof Object.assign !== 'function') {
+ Object.defineProperty(Object, 'assign', {
+ value: function assign(target, varArgs) {
+ if (target == null)
+ throw new TypeError('Cannot convert undefined or null to object');
+
+ var to = Object(target);
+
+ for (var index = 1; index < arguments.length; index++)
+ if (arguments[index] != null)
+ for (var nextKey in arguments[index])
+ if (Object.prototype.hasOwnProperty.call(arguments[index], nextKey))
+ to[nextKey] = arguments[index][nextKey];
+
+ return to;
+ },
+ writable: true,
+ configurable: true
+ });
+ }
- LuCI.prototype = {
- /* URL construction helpers */
- path: function(prefix, parts) {
- var url = [ prefix || '' ];
+ /* Promise.finally polyfill */
+ if (typeof Promise.prototype.finally !== 'function') {
+ Promise.prototype.finally = function(fn) {
+ var onFinally = function(cb) {
+ return Promise.resolve(fn.call(this)).then(cb);
+ };
+
+ return this.then(
+ function(result) { return onFinally.call(this, function() { return result }) },
+ function(reason) { return onFinally.call(this, function() { return Promise.reject(reason) }) }
+ );
+ };
+ }
- for (var i = 0; i < parts.length; i++)
- if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts[i]))
- url.push('/', parts[i]);
+ /*
+ * Class declaration and inheritance helper
+ */
- if (url.length === 1)
- url.push('/');
+ var toCamelCase = function(s) {
+ return s.replace(/(?:^|[\. -])(.)/g, function(m0, m1) { return m1.toUpperCase() });
+ };
- return url.join('');
- },
+ var superContext = null, Class = Object.assign(function() {}, {
+ extend: function(properties) {
+ var props = {
+ __base__: { value: this.prototype },
+ __name__: { value: properties.__name__ || 'anonymous' }
+ };
- url: function() {
- return this.path(this.env.scriptname, arguments);
+ var ClassConstructor = function() {
+ if (!(this instanceof ClassConstructor))
+ throw new TypeError('Constructor must not be called without "new"');
+
+ if (Object.getPrototypeOf(this).hasOwnProperty('__init__')) {
+ if (typeof(this.__init__) != 'function')
+ throw new TypeError('Class __init__ member is not a function');
+
+ this.__init__.apply(this, arguments)
+ }
+ else {
+ this.super('__init__', arguments);
+ }
+ };
+
+ for (var key in properties)
+ if (!props[key] && properties.hasOwnProperty(key))
+ props[key] = { value: properties[key], writable: true };
+
+ ClassConstructor.prototype = Object.create(this.prototype, props);
+ ClassConstructor.prototype.constructor = ClassConstructor;
+ Object.assign(ClassConstructor, this);
+ ClassConstructor.displayName = toCamelCase(props.__name__.value + 'Class');
+
+ return ClassConstructor;
},
- resource: function() {
- return this.path(this.env.resource, arguments);
+ singleton: function(properties /*, ... */) {
+ return Class.extend(properties)
+ .instantiate(Class.prototype.varargs(arguments, 1));
},
- location: function() {
- return this.path(this.env.scriptname, this.env.requestpath);
+ instantiate: function(args) {
+ return new (Function.prototype.bind.apply(this,
+ Class.prototype.varargs(args, 0, null)))();
},
+ call: function(self, method) {
+ if (typeof(this.prototype[method]) != 'function')
+ throw new ReferenceError(method + ' is not defined in class');
- /* HTTP resource fetching */
- get: function(url, args, cb) {
- return this.poll(0, url, args, cb, false);
+ return this.prototype[method].apply(self, self.varargs(arguments, 1));
},
- post: function(url, args, cb) {
- return this.poll(0, url, args, cb, true);
+ isSubclass: function(_class) {
+ return (_class != null &&
+ typeof(_class) == 'function' &&
+ _class.prototype instanceof this);
},
- poll: function(interval, url, args, cb, post) {
- var data = post ? { token: this.env.token } : null;
+ prototype: {
+ varargs: function(args, offset /*, ... */) {
+ return Array.prototype.slice.call(arguments, 2)
+ .concat(Array.prototype.slice.call(args, offset));
+ },
- if (!/^(?:\/|\S+:\/\/)/.test(url))
- url = this.url(url);
+ super: function(key, callArgs) {
+ for (superContext = Object.getPrototypeOf(superContext ||
+ Object.getPrototypeOf(this));
+ superContext && !superContext.hasOwnProperty(key);
+ superContext = Object.getPrototypeOf(superContext)) { }
- if (typeof(args) === 'object' && args !== null) {
- data = data || {};
+ if (!superContext)
+ return null;
- for (var key in args)
- if (args.hasOwnProperty(key))
- switch (typeof(args[key])) {
- case 'string':
- case 'number':
- case 'boolean':
- data[key] = args[key];
- break;
+ var res = superContext[key];
- case 'object':
- data[key] = JSON.stringify(args[key]);
- break;
- }
+ if (arguments.length > 1) {
+ if (typeof(res) != 'function')
+ throw new ReferenceError(key + ' is not a function in base class');
+
+ if (typeof(callArgs) != 'object')
+ callArgs = this.varargs(arguments, 1);
+
+ res = res.apply(this, callArgs);
+ }
+
+ superContext = null;
+
+ return res;
+ },
+
+ toString: function() {
+ var s = '[' + this.constructor.displayName + ']', f = true;
+ for (var k in this) {
+ if (this.hasOwnProperty(k)) {
+ s += (f ? ' {\n' : '') + ' ' + k + ': ' + typeof(this[k]) + '\n';
+ f = false;
+ }
+ }
+ return s + (f ? '' : '}');
}
+ }
+ });
- if (interval > 0)
- return XHR.poll(interval, url, data, cb, post);
- else if (post)
- return XHR.post(url, data, cb);
- else
- return XHR.get(url, data, cb);
+
+ /*
+ * HTTP Request helper
+ */
+
+ var Headers = Class.extend({
+ __name__: 'LuCI.XHR.Headers',
+ __init__: function(xhr) {
+ var hdrs = this.headers = {};
+ xhr.getAllResponseHeaders().split(/\r\n/).forEach(function(line) {
+ var m = /^([^:]+):(.*)$/.exec(line);
+ if (m != null)
+ hdrs[m[1].trim().toLowerCase()] = m[2].trim();
+ });
},
- stop: function(entry) { XHR.stop(entry) },
- halt: function() { XHR.halt() },
- run: function() { XHR.run() },
+ has: function(name) {
+ return this.headers.hasOwnProperty(String(name).toLowerCase());
+ },
+ get: function(name) {
+ var key = String(name).toLowerCase();
+ return this.headers.hasOwnProperty(key) ? this.headers[key] : null;
+ }
+ });
+
+ var Response = Class.extend({
+ __name__: 'LuCI.XHR.Response',
+ __init__: function(xhr, url, duration, headers, content) {
+ this.ok = (xhr.status >= 200 && xhr.status <= 299);
+ this.status = xhr.status;
+ this.statusText = xhr.statusText;
+ this.headers = (headers != null) ? headers : new Headers(xhr);
+ this.duration = duration;
+ this.url = url;
+ this.xhr = xhr;
+
+ if (content != null && typeof(content) == 'object') {
+ this.responseJSON = content;
+ this.responseText = null;
+ }
+ else if (content != null) {
+ this.responseJSON = null;
+ this.responseText = String(content);
+ }
+ else {
+ this.responseJSON = null;
+ this.responseText = xhr.responseText;
+ }
+ },
- /* Modal dialog */
- showModal: function(title, children) {
- var dlg = modalDiv.firstElementChild;
+ clone: function(content) {
+ var copy = new Response(this.xhr, this.url, this.duration, this.headers, content);
- dlg.setAttribute('class', 'modal');
+ copy.ok = this.ok;
+ copy.status = this.status;
+ copy.statusText = this.statusText;
- this.dom.content(dlg, this.dom.create('h4', {}, title));
- this.dom.append(dlg, children);
+ return copy;
+ },
- document.body.classList.add('modal-overlay-active');
+ json: function() {
+ if (this.responseJSON == null)
+ this.responseJSON = JSON.parse(this.responseText);
- return dlg;
+ return this.responseJSON;
},
- hideModal: function() {
- document.body.classList.remove('modal-overlay-active');
+ text: function() {
+ if (this.responseText == null && this.responseJSON != null)
+ this.responseText = JSON.stringify(this.responseJSON);
+
+ return this.responseText;
+ }
+ });
+
+
+ var requestQueue = [];
+
+ function isQueueableRequest(opt) {
+ if (!classes.rpc)
+ return false;
+
+ if (opt.method != 'POST' || typeof(opt.content) != 'object')
+ return false;
+
+ if (opt.nobatch === true)
+ return false;
+
+ var rpcBaseURL = Request.expandURL(classes.rpc.getBaseURL());
+
+ return (rpcBaseURL != null && opt.url.indexOf(rpcBaseURL) == 0);
+ }
+
+ function flushRequestQueue() {
+ if (!requestQueue.length)
+ return;
+
+ var reqopt = Object.assign({}, requestQueue[0][0], { content: [], nobatch: true }),
+ batch = [];
+
+ for (var i = 0; i < requestQueue.length; i++) {
+ batch[i] = requestQueue[i];
+ reqopt.content[i] = batch[i][0].content;
+ }
+
+ requestQueue.length = 0;
+
+ Request.request(rpcBaseURL, reqopt).then(function(reply) {
+ var json = null, req = null;
+
+ try { json = reply.json() }
+ catch(e) { }
+
+ while ((req = batch.shift()) != null)
+ if (Array.isArray(json) && json.length)
+ req[2].call(reqopt, reply.clone(json.shift()));
+ else
+ req[1].call(reqopt, new Error('No related RPC reply'));
+ }).catch(function(error) {
+ var req = null;
+
+ while ((req = batch.shift()) != null)
+ req[1].call(reqopt, error);
+ });
+ }
+
+ var Request = Class.singleton({
+ __name__: 'LuCI.Request',
+
+ interceptors: [],
+
+ expandURL: function(url) {
+ if (!/^(?:[^/]+:)?\/\//.test(url))
+ url = location.protocol + '//' + location.host + url;
+
+ return url;
},
+ request: function(target, options) {
+ var state = { xhr: new XMLHttpRequest(), url: this.expandURL(target), start: Date.now() },
+ opt = Object.assign({}, options, state),
+ content = null,
+ contenttype = null,
+ callback = this.handleReadyStateChange;
+
+ return new Promise(function(resolveFn, rejectFn) {
+ opt.xhr.onreadystatechange = callback.bind(opt, resolveFn, rejectFn);
+ opt.method = String(opt.method || 'GET').toUpperCase();
+
+ if ('query' in opt) {
+ var q = (opt.query != null) ? Object.keys(opt.query).map(function(k) {
+ if (opt.query[k] != null) {
+ var v = (typeof(opt.query[k]) == 'object')
+ ? JSON.stringify(opt.query[k])
+ : String(opt.query[k]);
+
+ return '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(v));
+ }
+ else {
+ return encodeURIComponent(k);
+ }
+ }).join('&') : '';
+
+ if (q !== '') {
+ switch (opt.method) {
+ case 'GET':
+ case 'HEAD':
+ case 'OPTIONS':
+ opt.url += ((/\?/).test(opt.url) ? '&' : '?') + q;
+ break;
- /* Tooltip */
- showTooltip: function(ev) {
- var target = findParent(ev.target, '[data-tooltip]');
+ default:
+ if (content == null) {
+ content = q;
+ contenttype = 'application/x-www-form-urlencoded';
+ }
+ }
+ }
+ }
- if (!target)
- return;
+ if (!opt.cache)
+ opt.url += ((/\?/).test(opt.url) ? '&' : '?') + (new Date()).getTime();
- if (tooltipTimeout !== null) {
- window.clearTimeout(tooltipTimeout);
- tooltipTimeout = null;
- }
+ if (isQueueableRequest(opt)) {
+ requestQueue.push([opt, rejectFn, resolveFn]);
+ requestAnimationFrame(flushRequestQueue);
+ return;
+ }
+
+ if ('username' in opt && 'password' in opt)
+ opt.xhr.open(opt.method, opt.url, true, opt.username, opt.password);
+ else
+ opt.xhr.open(opt.method, opt.url, true);
- var rect = target.getBoundingClientRect(),
- x = rect.left + window.pageXOffset,
- y = rect.top + rect.height + window.pageYOffset;
+ opt.xhr.responseType = 'text';
- tooltipDiv.className = 'cbi-tooltip';
- tooltipDiv.innerHTML = '▲ ';
- tooltipDiv.firstChild.data += target.getAttribute('data-tooltip');
+ if ('overrideMimeType' in opt.xhr)
+ opt.xhr.overrideMimeType('application/octet-stream');
- if (target.hasAttribute('data-tooltip-style'))
- tooltipDiv.classList.add(target.getAttribute('data-tooltip-style'));
+ if ('timeout' in opt)
+ opt.xhr.timeout = +opt.timeout;
- if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) {
- y -= (tooltipDiv.offsetHeight + target.offsetHeight);
- tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2);
- }
+ if ('credentials' in opt)
+ opt.xhr.withCredentials = !!opt.credentials;
+
+ if (opt.content != null) {
+ switch (typeof(opt.content)) {
+ case 'function':
+ content = opt.content(xhr);
+ break;
+
+ case 'object':
+ content = JSON.stringify(opt.content);
+ contenttype = 'application/json';
+ break;
+
+ default:
+ content = String(opt.content);
+ }
+ }
+
+ if ('headers' in opt)
+ for (var header in opt.headers)
+ if (opt.headers.hasOwnProperty(header)) {
+ if (header.toLowerCase() != 'content-type')
+ opt.xhr.setRequestHeader(header, opt.headers[header]);
+ else
+ contenttype = opt.headers[header];
+ }
- tooltipDiv.style.top = y + 'px';
- tooltipDiv.style.left = x + 'px';
- tooltipDiv.style.opacity = 1;
+ if (contenttype != null)
+ opt.xhr.setRequestHeader('Content-Type', contenttype);
- tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', {
- bubbles: true,
- detail: { target: target }
- }));
+ try {
+ opt.xhr.send(content);
+ }
+ catch (e) {
+ rejectFn.call(opt, e);
+ }
+ });
},
- hideTooltip: function(ev) {
- if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv ||
- tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget))
+ handleReadyStateChange: function(resolveFn, rejectFn, ev) {
+ var xhr = this.xhr;
+
+ if (xhr.readyState !== 4)
return;
- if (tooltipTimeout !== null) {
- window.clearTimeout(tooltipTimeout);
- tooltipTimeout = null;
+ if (xhr.status === 0 && xhr.statusText === '') {
+ rejectFn.call(this, new Error('XHR request aborted by browser'));
}
+ else {
+ var response = new Response(
+ xhr, xhr.responseURL || this.url, Date.now() - this.start);
- tooltipDiv.style.opacity = 0;
- tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
-
- tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
+ Promise.all(Request.interceptors.map(function(fn) { return fn(response) }))
+ .then(resolveFn.bind(this, response))
+ .catch(rejectFn.bind(this));
+ }
},
+ get: function(url, options) {
+ return this.request(url, Object.assign({ method: 'GET' }, options));
+ },
- /* Widget helper */
- itemlist: function(node, items, separators) {
- var children = [];
+ post: function(url, data, options) {
+ return this.request(url, Object.assign({ method: 'POST', content: data }, options));
+ },
- if (!Array.isArray(separators))
- separators = [ separators || E('br') ];
+ addInterceptor: function(interceptorFn) {
+ if (typeof(interceptorFn) == 'function')
+ this.interceptors.push(interceptorFn);
+ return interceptorFn;
+ },
- for (var i = 0; i < items.length; i += 2) {
- if (items[i+1] !== null && items[i+1] !== undefined) {
- var sep = separators[(i/2) % separators.length],
- cld = [];
+ removeInterceptor: function(interceptorFn) {
+ var oldlen = this.interceptors.length, i = oldlen;
+ while (i--)
+ if (this.interceptors[i] === interceptorFn)
+ this.interceptors.splice(i, 1);
+ return (this.interceptors.length < oldlen);
+ },
- children.push(E('span', { class: 'nowrap' }, [
- items[i] ? E('strong', items[i] + ': ') : '',
- items[i+1]
- ]));
+ poll: {
+ add: function(interval, url, options, callback) {
+ if (isNaN(interval) || interval <= 0)
+ throw new TypeError('Invalid poll interval');
- if ((i+2) < items.length)
- children.push(this.dom.elem(sep) ? sep.cloneNode(true) : sep);
- }
- }
+ var ival = interval >>> 0,
+ opts = Object.assign({}, options, { timeout: ival * 1000 - 5 });
- this.dom.content(node, children);
+ return Poll.add(function() {
+ return Request.request(url, options).then(function(res) {
+ if (!Poll.active())
+ return;
- return node;
+ try {
+ callback(res, res.json(), res.duration);
+ }
+ catch (err) {
+ callback(res, null, res.duration);
+ }
+ });
+ }, ival);
+ },
+
+ remove: function(entry) { return Poll.remove(entry) },
+ start: function() { return Poll.start() },
+ stop: function() { return Poll.stop() },
+ active: function() { return Poll.active() }
}
- };
+ });
- /* Tabs */
- LuCI.prototype.tabs = {
- init: function() {
- var groups = [], prevGroup = null, currGroup = null;
+ var Poll = Class.singleton({
+ __name__: 'LuCI.Poll',
- document.querySelectorAll('[data-tab]').forEach(function(tab) {
- var parent = tab.parentNode;
+ queue: [],
- if (!parent.hasAttribute('data-tab-group'))
- parent.setAttribute('data-tab-group', groups.length);
+ add: function(fn, interval) {
+ if (interval == null || interval <= 0)
+ interval = window.L ? window.L.env.pollinterval : null;
- currGroup = +parent.getAttribute('data-tab-group');
+ if (isNaN(interval) || typeof(fn) != 'function')
+ throw new TypeError('Invalid argument to LuCI.Poll.add()');
- if (currGroup !== prevGroup) {
- prevGroup = currGroup;
+ for (var i = 0; i < this.queue.length; i++)
+ if (this.queue[i].fn === fn)
+ return false;
- if (!groups[currGroup])
- groups[currGroup] = [];
- }
+ var e = {
+ r: true,
+ i: interval >>> 0,
+ fn: fn
+ };
- groups[currGroup].push(tab);
- });
+ this.queue.push(e);
+
+ if (this.tick != null && !this.active())
+ this.start();
+
+ return true;
+ },
+
+ remove: function(entry) {
+ if (typeof(fn) != 'function')
+ throw new TypeError('Invalid argument to LuCI.Poll.remove()');
- for (var i = 0; i < groups.length; i++)
- this.initTabGroup(groups[i]);
+ var len = this.queue.length;
- document.addEventListener('dependency-update', this.updateTabs.bind(this));
+ for (var i = len; i > 0; i--)
+ if (this.queue[i-1].fn === fn)
+ this.queue.splice(i-1, 1);
- this.updateTabs();
+ if (!this.queue.length && this.stop())
+ this.tick = 0;
- if (!groups.length)
- this.setActiveTabId(-1, -1);
+ return (this.queue.length != len);
},
- initTabGroup: function(panes) {
- if (!Array.isArray(panes) || panes.length === 0)
- return;
+ start: function() {
+ if (this.active())
+ return false;
+
+ this.tick = 0;
- var menu = E('ul', { 'class': 'cbi-tabmenu' }),
- group = panes[0].parentNode,
- groupId = +group.getAttribute('data-tab-group'),
- selected = null;
-
- for (var i = 0, pane; pane = panes[i]; i++) {
- var name = pane.getAttribute('data-tab'),
- title = pane.getAttribute('data-tab-title'),
- active = pane.getAttribute('data-tab-active') === 'true';
-
- menu.appendChild(E('li', {
- 'class': active ? 'cbi-tab' : 'cbi-tab-disabled',
- 'data-tab': name
- }, E('a', {
- 'href': '#',
- 'click': this.switchTab.bind(this)
- }, title)));
-
- if (active)
- selected = i;
+ if (this.queue.length) {
+ this.timer = window.setInterval(this.step, 1000);
+ this.step();
+ document.dispatchEvent(new CustomEvent('poll-start'));
}
- group.parentNode.insertBefore(menu, group);
+ return true;
+ },
- if (selected === null) {
- selected = this.getActiveTabId(groupId);
+ stop: function() {
+ if (!this.active())
+ return false;
- if (selected < 0 || selected >= panes.length)
- selected = 0;
+ document.dispatchEvent(new CustomEvent('poll-stop'));
+ window.clearInterval(this.timer);
+ delete this.timer;
+ delete this.tick;
+ return true;
+ },
- menu.childNodes[selected].classList.add('cbi-tab');
- menu.childNodes[selected].classList.remove('cbi-tab-disabled');
- panes[selected].setAttribute('data-tab-active', 'true');
+ step: function() {
+ for (var i = 0, e = null; (e = Poll.queue[i]) != null; i++) {
+ if ((Poll.tick % e.i) != 0)
+ continue;
- this.setActiveTabId(groupId, selected);
+ if (!e.r)
+ continue;
+
+ e.r = false;
+
+ Promise.resolve(e.fn()).finally((function() { this.r = true }).bind(e));
}
+
+ Poll.tick = (Poll.tick + 1) % Math.pow(2, 32);
},
- getActiveTabState: function() {
- var page = document.body.getAttribute('data-page');
+ active: function() {
+ return (this.timer != null);
+ }
+ });
- try {
- var val = JSON.parse(window.sessionStorage.getItem('tab'));
- if (val.page === page && Array.isArray(val.groups))
- return val;
- }
- catch(e) {}
- window.sessionStorage.removeItem('tab');
- return { page: page, groups: [] };
+ var dummyElem = null,
+ domParser = null,
+ originalCBIInit = null,
+ rpcBaseURL = null,
+ classes = {};
+
+ var LuCI = Class.extend({
+ __name__: 'LuCI',
+ __init__: function(env) {
+
+ document.querySelectorAll('script[src*="/luci.js"]').forEach(function(s) {
+ if (env.base_url == null || env.base_url == '')
+ env.base_url = s.getAttribute('src').replace(/\/luci\.js(?:\?v=[^?]+)?$/, '');
+ });
+
+ if (env.base_url == null)
+ this.error('InternalError', 'Cannot find url of luci.js');
+
+ Object.assign(this.env, env);
+
+ document.addEventListener('poll-start', function(ev) {
+ document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
+ e.style.display = (e.id == 'xhr_poll_status_off') ? 'none' : '';
+ });
+ });
+
+ document.addEventListener('poll-stop', function(ev) {
+ document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
+ e.style.display = (e.id == 'xhr_poll_status_on') ? 'none' : '';
+ });
+ });
+
+ var domReady = new Promise(function(resolveFn, rejectFn) {
+ document.addEventListener('DOMContentLoaded', resolveFn);
+ });
+
+ Promise.all([
+ domReady,
+ this.require('ui'),
+ this.require('rpc'),
+ this.require('form'),
+ this.probeRPCBaseURL()
+ ]).then(this.setupDOM.bind(this)).catch(this.error);
+
+ originalCBIInit = window.cbi_init;
+ window.cbi_init = function() {};
},
- getActiveTabId: function(groupId) {
- return +this.getActiveTabState().groups[groupId] || 0;
+ raise: function(type, fmt /*, ...*/) {
+ var e = null,
+ msg = fmt ? String.prototype.format.apply(fmt, this.varargs(arguments, 2)) : null,
+ stack = null;
+
+ if (type instanceof Error) {
+ e = type;
+ stack = (e.stack || '').split(/\n/);
+
+ if (msg)
+ e.message = msg + ': ' + e.message;
+ }
+ else {
+ e = new (window[type || 'Error'] || Error)(msg || 'Unspecified error');
+ e.name = type || 'Error';
+ }
+
+ if (window.console && console.debug)
+ console.debug(e);
+
+ throw e;
},
- setActiveTabId: function(groupId, tabIndex) {
+ error: function(type, fmt /*, ...*/) {
try {
- var state = this.getActiveTabState();
- state.groups[groupId] = tabIndex;
+ L.raise.apply(L, Array.prototype.slice.call(arguments));
+ }
+ catch (e) {
+ var stack = (e.stack || '').split(/\n/).map(function(frame) {
+ frame = frame.replace(/(.*?)@(.+):(\d+):(\d+)/g, 'at $1 ($2:$3:$4)').trim();
+ return frame ? ' ' + frame : '';
+ });
+
+ if (!/^ at /.test(stack[0]))
+ stack.shift();
+
+ if (/\braise /.test(stack[0]))
+ stack.shift();
- window.sessionStorage.setItem('tab', JSON.stringify(state));
+ if (/\berror /.test(stack[0]))
+ stack.shift();
+
+ stack = stack.length ? '\n' + stack.join('\n') : '';
+
+ if (L.ui)
+ L.ui.showModal(e.name || _('Runtime error'),
+ E('pre', { 'class': 'alert-message error' }, e.message + stack));
+ else
+ L.dom.content(document.querySelector('#maincontent'),
+ E('pre', { 'class': 'alert-message error' }, e + stack));
+
+ throw e;
}
- catch (e) { return false; }
+ },
- return true;
+ bind: function(fn, self /*, ... */) {
+ return Function.prototype.bind.apply(fn, this.varargs(arguments, 2, self));
},
- updateTabs: function(ev) {
- document.querySelectorAll('[data-tab-title]').forEach(function(pane) {
- var menu = pane.parentNode.previousElementSibling,
- tab = menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))),
- n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
+ /* Class require */
+ require: function(name, from) {
+ var L = this, url = null, from = from || [];
- if (!pane.firstElementChild) {
- tab.style.display = 'none';
- tab.classList.remove('flash');
- }
- else if (tab.style.display === 'none') {
- tab.style.display = '';
- requestAnimationFrame(function() { tab.classList.add('flash') });
- }
+ /* Class already loaded */
+ if (classes[name] != null) {
+ /* Circular dependency */
+ if (from.indexOf(name) != -1)
+ L.raise('DependencyError',
+ 'Circular dependency: class "%s" depends on "%s"',
+ name, from.join('" which depends on "'));
- if (n_errors) {
- tab.setAttribute('data-errors', n_errors);
- tab.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors));
- tab.setAttribute('data-tooltip-style', 'error');
- }
- else {
- tab.removeAttribute('data-errors');
- tab.removeAttribute('data-tooltip');
- }
- });
- },
+ return classes[name];
+ }
- switchTab: function(ev) {
- var tab = ev.target.parentNode,
- name = tab.getAttribute('data-tab'),
- menu = tab.parentNode,
- group = menu.nextElementSibling,
- groupId = +group.getAttribute('data-tab-group'),
- index = 0;
+ url = '%s/%s.js'.format(L.env.base_url, name.replace(/\./g, '/'));
+ from = [ name ].concat(from);
- ev.preventDefault();
+ var compileClass = function(res) {
+ if (!res.ok)
+ L.raise('NetworkError',
+ 'HTTP error %d while loading class file "%s"', res.status, url);
- if (!tab.classList.contains('cbi-tab-disabled'))
- return;
+ var source = res.text(),
+ requirematch = /^require[ \t]+(\S+)(?:[ \t]+as[ \t]+([a-zA-Z_]\S*))?$/,
+ strictmatch = /^use[ \t]+strict$/,
+ depends = [],
+ args = '';
- menu.querySelectorAll('[data-tab]').forEach(function(tab) {
- tab.classList.remove('cbi-tab');
- tab.classList.remove('cbi-tab-disabled');
- tab.classList.add(
- tab.getAttribute('data-tab') === name ? 'cbi-tab' : 'cbi-tab-disabled');
- });
+ /* find require statements in source */
+ for (var i = 0, off = -1, quote = -1, esc = false; i < source.length; i++) {
+ var chr = source.charCodeAt(i);
- group.childNodes.forEach(function(pane) {
- if (L.dom.matches(pane, '[data-tab]')) {
- if (pane.getAttribute('data-tab') === name) {
- pane.setAttribute('data-tab-active', 'true');
- L.tabs.setActiveTabId(groupId, index);
+ if (esc) {
+ esc = false;
}
- else {
- pane.setAttribute('data-tab-active', 'false');
+ else if (chr == 92) {
+ esc = true;
}
+ else if (chr == quote) {
+ var s = source.substring(off, i),
+ m = requirematch.exec(s);
+
+ if (m) {
+ var dep = m[1], as = m[2] || dep.replace(/[^a-zA-Z0-9_]/g, '_');
+ depends.push(L.require(dep, from));
+ args += ', ' + as;
+ }
+ else if (!strictmatch.exec(s)) {
+ break;
+ }
- index++;
+ off = -1;
+ quote = -1;
+ }
+ else if (quote == -1 && (chr == 34 || chr == 39)) {
+ off = i + 1;
+ quote = chr;
+ }
}
- });
- }
- };
- /* DOM manipulation */
- LuCI.prototype.dom = {
- elem: function(e) {
- return (typeof(e) === 'object' && e !== null && 'nodeType' in e);
- },
+ /* load dependencies and instantiate class */
+ return Promise.all(depends).then(function(instances) {
+ var _factory, _class;
+
+ try {
+ _factory = eval(
+ '(function(window, document, L%s) { %s })\n\n//# sourceURL=%s\n'
+ .format(args, source, res.url));
+ }
+ catch (error) {
+ L.raise('SyntaxError', '%s\n in %s:%s',
+ error.message, res.url, error.lineNumber || '?');
+ }
- parse: function(s) {
- var elem;
+ _factory.displayName = toCamelCase(name + 'ClassFactory');
+ _class = _factory.apply(_factory, [window, document, L].concat(instances));
- try {
- domParser = domParser || new DOMParser();
- elem = domParser.parseFromString(s, 'text/html').body.firstChild;
- }
- catch(e) {}
+ if (!Class.isSubclass(_class))
+ L.error('TypeError', '"%s" factory yields invalid constructor', name);
+
+ if (_class.displayName == 'AnonymousClass')
+ _class.displayName = toCamelCase(name + 'Class');
+
+ var ptr = Object.getPrototypeOf(L),
+ parts = name.split(/\./),
+ instance = new _class();
+
+ for (var i = 0; ptr && i < parts.length - 1; i++)
+ ptr = ptr[parts[i]];
+
+ if (ptr)
+ ptr[parts[i]] = instance;
+
+ classes[name] = instance;
+
+ return instance;
+ });
+ };
+
+ /* Request class file */
+ classes[name] = Request.get(url, { cache: true }).then(compileClass);
- if (!elem) {
+ return classes[name];
+ },
+
+ /* DOM setup */
+ probeRPCBaseURL: function() {
+ if (rpcBaseURL == null) {
try {
- dummyElem = dummyElem || document.createElement('div');
- dummyElem.innerHTML = s;
- elem = dummyElem.firstChild;
+ rpcBaseURL = window.sessionStorage.getItem('rpcBaseURL');
}
- catch (e) {}
+ catch (e) { }
}
- return elem || null;
- },
+ if (rpcBaseURL == null) {
+ var rpcFallbackURL = this.url('admin/ubus');
- matches: function(node, selector) {
- var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
- return m ? m.call(node, selector) : false;
+ rpcBaseURL = Request.get('/ubus/').then(function(res) {
+ return (rpcBaseURL = (res.status == 400) ? '/ubus/' : rpcFallbackURL);
+ }, function() {
+ return (rpcBaseURL = rpcFallbackURL);
+ }).then(function(url) {
+ try {
+ window.sessionStorage.setItem('rpcBaseURL', url);
+ }
+ catch (e) { }
+
+ return url;
+ });
+ }
+
+ return Promise.resolve(rpcBaseURL);
},
- parent: function(node, selector) {
- if (this.elem(node) && node.closest)
- return node.closest(selector);
+ setupDOM: function(res) {
+ var domEv = res[0],
+ uiClass = res[1],
+ rpcClass = res[2],
+ formClass = res[3],
+ rpcBaseURL = res[4];
+
+ rpcClass.setBaseURL(rpcBaseURL);
+
+ Request.addInterceptor(function(res) {
+ if (res.status != 403 || res.headers.get('X-LuCI-Login-Required') != 'yes')
+ return;
+
+ Poll.stop();
+
+ L.ui.showModal(_('Session expired'), [
+ E('div', { class: 'alert-message warning' },
+ _('A new login is required since the authentication session expired.')),
+ E('div', { class: 'right' },
+ E('div', {
+ class: 'btn primary',
+ click: function() {
+ var loc = window.location;
+ window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
+ }
+ }, _('To login…')))
+ ]);
+
+ throw 'Session expired';
+ });
- while (this.elem(node))
- if (this.matches(node, selector))
- return node;
- else
- node = node.parentNode;
+ originalCBIInit();
+
+ Poll.start();
- return null;
+ document.dispatchEvent(new CustomEvent('luci-loaded'));
},
- append: function(node, children) {
- if (!this.elem(node))
- return null;
+ env: {},
- if (Array.isArray(children)) {
- for (var i = 0; i < children.length; i++)
- if (this.elem(children[i]))
- node.appendChild(children[i]);
- else if (children !== null && children !== undefined)
- node.appendChild(document.createTextNode('' + children[i]));
+ /* URL construction helpers */
+ path: function(prefix, parts) {
+ var url = [ prefix || '' ];
- return node.lastChild;
- }
- else if (typeof(children) === 'function') {
- return this.append(node, children(node));
- }
- else if (this.elem(children)) {
- return node.appendChild(children);
- }
- else if (children !== null && children !== undefined) {
- node.innerHTML = '' + children;
- return node.lastChild;
- }
+ for (var i = 0; i < parts.length; i++)
+ if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts[i]))
+ url.push('/', parts[i]);
- return null;
+ if (url.length === 1)
+ url.push('/');
+
+ return url.join('');
},
- content: function(node, children) {
- if (!this.elem(node))
- return null;
+ url: function() {
+ return this.path(this.env.scriptname, arguments);
+ },
- while (node.firstChild)
- node.removeChild(node.firstChild);
+ resource: function() {
+ return this.path(this.env.resource, arguments);
+ },
- return this.append(node, children);
+ location: function() {
+ return this.path(this.env.scriptname, this.env.requestpath);
},
- attr: function(node, key, val) {
- if (!this.elem(node))
- return null;
- var attr = null;
+ /* Data helpers */
+ isObject: function(val) {
+ return (val != null && typeof(val) == 'object');
+ },
- if (typeof(key) === 'object' && key !== null)
- attr = key;
- else if (typeof(key) === 'string')
- attr = {}, attr[key] = val;
+ sortedKeys: function(obj, key, sortmode) {
+ if (obj == null || typeof(obj) != 'object')
+ return [];
- for (key in attr) {
- if (!attr.hasOwnProperty(key) || attr[key] === null || attr[key] === undefined)
- continue;
+ return Object.keys(obj).map(function(e) {
+ var v = (key != null) ? obj[e][key] : e;
- switch (typeof(attr[key])) {
- case 'function':
- node.addEventListener(key, attr[key]);
+ switch (sortmode) {
+ case 'addr':
+ v = (v != null) ? v.replace(/(?:^|[.:])([0-9a-fA-F]{1,4})/g,
+ function(m0, m1) { return ('000' + m1.toLowerCase()).substr(-4) }) : null;
break;
- case 'object':
- node.setAttribute(key, JSON.stringify(attr[key]));
+ case 'num':
+ v = (v != null) ? +v : null;
break;
-
- default:
- node.setAttribute(key, attr[key]);
}
- }
+
+ return [ e, v ];
+ }).filter(function(e) {
+ return (e[1] != null);
+ }).sort(function(a, b) {
+ return (a[1] > b[1]);
+ }).map(function(e) {
+ return e[0];
+ });
},
- create: function() {
- var html = arguments[0],
- attr = (arguments[1] instanceof Object && !Array.isArray(arguments[1])) ? arguments[1] : null,
- data = attr ? arguments[2] : arguments[1],
- elem;
+ toArray: function(val) {
+ if (val == null)
+ return [];
+ else if (Array.isArray(val))
+ return val;
+ else if (typeof(val) == 'object')
+ return [ val ];
+
+ var s = String(val).trim();
+
+ if (s == '')
+ return [];
- if (this.elem(html))
- elem = html;
- else if (html.charCodeAt(0) === 60)
- elem = this.parse(html);
+ return s.split(/\s+/);
+ },
+
+
+ /* HTTP resource fetching */
+ get: function(url, args, cb) {
+ return this.poll(null, url, args, cb, false);
+ },
+
+ post: function(url, args, cb) {
+ return this.poll(null, url, args, cb, true);
+ },
+
+ poll: function(interval, url, args, cb, post) {
+ if (interval !== null && interval <= 0)
+ interval = this.env.pollinterval;
+
+ var data = post ? { token: this.env.token } : null,
+ method = post ? 'POST' : 'GET';
+
+ if (!/^(?:\/|\S+:\/\/)/.test(url))
+ url = this.url(url);
+
+ if (args != null)
+ data = Object.assign(data || {}, args);
+
+ if (interval !== null)
+ return Request.poll.add(interval, url, { method: method, query: data }, cb);
else
- elem = document.createElement(html);
+ return Request.request(url, { method: method, query: data })
+ .then(function(res) {
+ var json = null;
+ if (/^application\/json\b/.test(res.headers.get('Content-Type')))
+ try { json = res.json() } catch(e) {}
+ cb(res.xhr, json, res.duration);
+ });
+ },
+
+ stop: function(entry) { return Poll.remove(entry) },
+ halt: function() { return Poll.stop() },
+ run: function() { return Poll.start() },
+
+ /* DOM manipulation */
+ dom: Class.singleton({
+ __name__: 'LuCI.DOM',
+
+ elem: function(e) {
+ return (e != null && typeof(e) == 'object' && 'nodeType' in e);
+ },
+
+ parse: function(s) {
+ var elem;
+
+ try {
+ domParser = domParser || new DOMParser();
+ elem = domParser.parseFromString(s, 'text/html').body.firstChild;
+ }
+ catch(e) {}
+
+ if (!elem) {
+ try {
+ dummyElem = dummyElem || document.createElement('div');
+ dummyElem.innerHTML = s;
+ elem = dummyElem.firstChild;
+ }
+ catch (e) {}
+ }
+
+ return elem || null;
+ },
+
+ matches: function(node, selector) {
+ var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
+ return m ? m.call(node, selector) : false;
+ },
+
+ parent: function(node, selector) {
+ if (this.elem(node) && node.closest)
+ return node.closest(selector);
+
+ while (this.elem(node))
+ if (this.matches(node, selector))
+ return node;
+ else
+ node = node.parentNode;
- if (!elem)
return null;
+ },
- this.attr(elem, attr);
- this.append(elem, data);
+ append: function(node, children) {
+ if (!this.elem(node))
+ return null;
- return elem;
- }
- };
+ if (Array.isArray(children)) {
+ for (var i = 0; i < children.length; i++)
+ if (this.elem(children[i]))
+ node.appendChild(children[i]);
+ else if (children !== null && children !== undefined)
+ node.appendChild(document.createTextNode('' + children[i]));
- /* Setup */
- LuCI.prototype.setupDOM = function(ev) {
- this.tabs.init();
- };
+ return node.lastChild;
+ }
+ else if (typeof(children) === 'function') {
+ return this.append(node, children(node));
+ }
+ else if (this.elem(children)) {
+ return node.appendChild(children);
+ }
+ else if (children !== null && children !== undefined) {
+ node.innerHTML = '' + children;
+ return node.lastChild;
+ }
- function LuCI(env) {
- this.env = env;
+ return null;
+ },
- modalDiv = document.body.appendChild(
- this.dom.create('div', { id: 'modal_overlay' },
- this.dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true })));
+ content: function(node, children) {
+ if (!this.elem(node))
+ return null;
- tooltipDiv = document.body.appendChild(this.dom.create('div', { class: 'cbi-tooltip' }));
+ var dataNodes = node.querySelectorAll('[data-idref]');
- document.addEventListener('mouseover', this.showTooltip.bind(this), true);
- document.addEventListener('mouseout', this.hideTooltip.bind(this), true);
- document.addEventListener('focus', this.showTooltip.bind(this), true);
- document.addEventListener('blur', this.hideTooltip.bind(this), true);
+ for (var i = 0; i < dataNodes.length; i++)
+ delete this.registry[dataNodes[i].getAttribute('data-idref')];
- document.addEventListener('DOMContentLoaded', this.setupDOM.bind(this));
- }
+ while (node.firstChild)
+ node.removeChild(node.firstChild);
+
+ return this.append(node, children);
+ },
+
+ attr: function(node, key, val) {
+ if (!this.elem(node))
+ return null;
+
+ var attr = null;
+
+ if (typeof(key) === 'object' && key !== null)
+ attr = key;
+ else if (typeof(key) === 'string')
+ attr = {}, attr[key] = val;
+
+ for (key in attr) {
+ if (!attr.hasOwnProperty(key) || attr[key] == null)
+ continue;
+
+ switch (typeof(attr[key])) {
+ case 'function':
+ node.addEventListener(key, attr[key]);
+ break;
+
+ case 'object':
+ node.setAttribute(key, JSON.stringify(attr[key]));
+ break;
+
+ default:
+ node.setAttribute(key, attr[key]);
+ }
+ }
+ },
+
+ create: function() {
+ var html = arguments[0],
+ attr = arguments[1],
+ data = arguments[2],
+ elem;
+
+ if (!(attr instanceof Object) || Array.isArray(attr))
+ data = attr, attr = null;
+
+ if (Array.isArray(html)) {
+ elem = document.createDocumentFragment();
+ for (var i = 0; i < html.length; i++)
+ elem.appendChild(this.create(html[i]));
+ }
+ else if (this.elem(html)) {
+ elem = html;
+ }
+ else if (html.charCodeAt(0) === 60) {
+ elem = this.parse(html);
+ }
+ else {
+ elem = document.createElement(html);
+ }
+
+ if (!elem)
+ return null;
+
+ this.attr(elem, attr);
+ this.append(elem, data);
+
+ return elem;
+ },
+
+ registry: {},
+
+ data: function(node, key, val) {
+ var id = node.getAttribute('data-idref');
+
+ /* clear all data */
+ if (arguments.length > 1 && key == null) {
+ if (id != null) {
+ node.removeAttribute('data-idref');
+ val = this.registry[id]
+ delete this.registry[id];
+ return val;
+ }
+
+ return null;
+ }
+
+ /* clear a key */
+ else if (arguments.length > 2 && key != null && val == null) {
+ if (id != null) {
+ val = this.registry[id][key];
+ delete this.registry[id][key];
+ return val;
+ }
+
+ return null;
+ }
+
+ /* set a key */
+ else if (arguments.length > 2 && key != null && val != null) {
+ if (id == null) {
+ do { id = Math.floor(Math.random() * 0xffffffff).toString(16) }
+ while (this.registry.hasOwnProperty(id));
+
+ node.setAttribute('data-idref', id);
+ this.registry[id] = {};
+ }
+
+ return (this.registry[id][key] = val);
+ }
+
+ /* get all data */
+ else if (arguments.length == 1) {
+ if (id != null)
+ return this.registry[id];
+
+ return null;
+ }
+
+ /* get a key */
+ else if (arguments.length == 2) {
+ if (id != null)
+ return this.registry[id][key];
+ }
+
+ return null;
+ },
+
+ bindClassInstance: function(node, inst) {
+ if (!(inst instanceof Class))
+ L.error('TypeError', 'Argument must be a class instance');
+
+ return this.data(node, '_class', inst);
+ },
+
+ findClassInstance: function(node) {
+ var inst = null;
+
+ do {
+ inst = this.data(node, '_class');
+ node = node.parentNode;
+ }
+ while (!(inst instanceof Class) && node != null);
+
+ return inst;
+ },
+
+ callClassMethod: function(node, method /*, ... */) {
+ var inst = this.findClassInstance(node);
+
+ if (inst == null || typeof(inst[method]) != 'function')
+ return null;
+
+ return inst[method].apply(inst, inst.varargs(arguments, 2));
+ },
+
+ isEmpty: function(node) {
+ for (var child = node.firstElementChild; child != null; child = child.nextElementSibling)
+ if (!child.classList.contains('hidden'))
+ return false;
+
+ return true;
+ }
+ }),
+
+ Poll: Poll,
+ Class: Class,
+ Request: Request,
+
+ view: Class.extend({
+ __name__: 'LuCI.View',
+
+ __init__: function() {
+ var vp = document.getElementById('view');
+
+ L.dom.content(vp, E('div', { 'class': 'spinning' }, _('Loading view…')));
+
+ return Promise.resolve(this.load())
+ .then(L.bind(this.render, this))
+ .then(L.bind(function(nodes) {
+ var vp = document.getElementById('view');
+
+ L.dom.content(vp, nodes);
+ L.dom.append(vp, this.addFooter());
+ }, this)).catch(L.error);
+ },
+
+ load: function() {},
+ render: function() {},
+
+ handleSave: function(ev) {
+ var tasks = [];
+
+ document.getElementById('maincontent')
+ .querySelectorAll('.cbi-map').forEach(function(map) {
+ tasks.push(L.dom.callClassMethod(map, 'save'));
+ });
+
+ return Promise.all(tasks);
+ },
+
+ handleSaveApply: function(ev) {
+ return this.handleSave(ev).then(function() {
+ L.ui.changes.apply(true);
+ });
+ },
+
+ handleReset: function(ev) {
+ var tasks = [];
+
+ document.getElementById('maincontent')
+ .querySelectorAll('.cbi-map').forEach(function(map) {
+ tasks.push(L.dom.callClassMethod(map, 'reset'));
+ });
+
+ return Promise.all(tasks);
+ },
+
+ addFooter: function() {
+ var footer = E([]),
+ mc = document.getElementById('maincontent');
+
+ if (mc.querySelector('.cbi-map')) {
+ footer.appendChild(E('div', { 'class': 'cbi-page-actions' }, [
+ E('input', {
+ 'class': 'cbi-button cbi-button-apply',
+ 'type': 'button',
+ 'value': _('Save & Apply'),
+ 'click': L.bind(this.handleSaveApply, this)
+ }), ' ',
+ E('input', {
+ 'class': 'cbi-button cbi-button-save',
+ 'type': 'submit',
+ 'value': _('Save'),
+ 'click': L.bind(this.handleSave, this)
+ }), ' ',
+ E('input', {
+ 'class': 'cbi-button cbi-button-reset',
+ 'type': 'button',
+ 'value': _('Reset'),
+ 'click': L.bind(this.handleReset, this)
+ })
+ ]));
+ }
+
+ return footer;
+ }
+ })
+ });
+
+ var XHR = Class.extend({
+ __name__: 'LuCI.XHR',
+ __init__: function() {
+ if (window.console && console.debug)
+ console.debug('Direct use XHR() is deprecated, please use L.Request instead');
+ },
+
+ _response: function(cb, res, json, duration) {
+ if (this.active)
+ cb(res, json, duration);
+ delete this.active;
+ },
+
+ get: function(url, data, callback, timeout) {
+ this.active = true;
+ L.get(url, data, this._response.bind(this, callback), timeout);
+ },
+
+ post: function(url, data, callback, timeout) {
+ this.active = true;
+ L.post(url, data, this._response.bind(this, callback), timeout);
+ },
+ cancel: function() { delete this.active },
+ busy: function() { return (this.active === true) },
+ abort: function() {},
+ send_form: function() { L.error('InternalError', 'Not implemented') },
+ });
+
+ XHR.get = function() { return window.L.get.apply(window.L, arguments) };
+ XHR.post = function() { return window.L.post.apply(window.L, arguments) };
+ XHR.poll = function() { return window.L.poll.apply(window.L, arguments) };
+ XHR.stop = Request.poll.remove.bind(Request.poll);
+ XHR.halt = Request.poll.stop.bind(Request.poll);
+ XHR.run = Request.poll.start.bind(Request.poll);
+ XHR.running = Request.poll.active.bind(Request.poll);
+
+ window.XHR = XHR;
window.LuCI = LuCI;
})(window, document);
diff --git a/modules/luci-base/htdocs/luci-static/resources/network.js b/modules/luci-base/htdocs/luci-static/resources/network.js
new file mode 100644
index 0000000000..d3d9a1cf57
--- /dev/null
+++ b/modules/luci-base/htdocs/luci-static/resources/network.js
@@ -0,0 +1,2085 @@
+'use strict';
+'require uci';
+'require rpc';
+'require validation';
+
+var proto_errors = {
+ CONNECT_FAILED: _('Connection attempt failed'),
+ INVALID_ADDRESS: _('IP address in invalid'),
+ INVALID_GATEWAY: _('Gateway address is invalid'),
+ INVALID_LOCAL_ADDRESS: _('Local IP address is invalid'),
+ MISSING_ADDRESS: _('IP address is missing'),
+ MISSING_PEER_ADDRESS: _('Peer address is missing'),
+ NO_DEVICE: _('Network device is not present'),
+ NO_IFACE: _('Unable to determine device name'),
+ NO_IFNAME: _('Unable to determine device name'),
+ NO_WAN_ADDRESS: _('Unable to determine external IP address'),
+ NO_WAN_LINK: _('Unable to determine upstream interface'),
+ PEER_RESOLVE_FAIL: _('Unable to resolve peer host name'),
+ PIN_FAILED: _('PIN code rejected')
+};
+
+var iface_patterns_ignore = [
+ /^wmaster\d+/,
+ /^wifi\d+/,
+ /^hwsim\d+/,
+ /^imq\d+/,
+ /^ifb\d+/,
+ /^mon\.wlan\d+/,
+ /^sit\d+/,
+ /^gre\d+/,
+ /^gretap\d+/,
+ /^ip6gre\d+/,
+ /^ip6tnl\d+/,
+ /^tunl\d+/,
+ /^lo$/
+];
+
+var iface_patterns_wireless = [
+ /^wlan\d+/,
+ /^wl\d+/,
+ /^ath\d+/,
+ /^\w+\.network\d+/
+];
+
+var iface_patterns_virtual = [ ];
+
+var callNetworkWirelessStatus = rpc.declare({
+ object: 'network.wireless',
+ method: 'status'
+});
+
+var callLuciNetdevs = rpc.declare({
+ object: 'luci',
+ method: 'netdevs'
+});
+
+var callLuciIfaddrs = rpc.declare({
+ object: 'luci',
+ method: 'ifaddrs',
+ expect: { result: [] }
+});
+
+var callLuciBoardjson = rpc.declare({
+ object: 'luci',
+ method: 'boardjson'
+});
+
+var callIwinfoInfo = rpc.declare({
+ object: 'iwinfo',
+ method: 'info',
+ params: [ 'device' ]
+});
+
+var callNetworkInterfaceStatus = rpc.declare({
+ object: 'network.interface',
+ method: 'dump',
+ expect: { 'interface': [] }
+});
+
+var callNetworkDeviceStatus = rpc.declare({
+ object: 'network.device',
+ method: 'status',
+ expect: { '': {} }
+});
+
+var _cache = {},
+ _state = null,
+ _protocols = {};
+
+function getWifiState() {
+ if (_cache.wifi == null)
+ return callNetworkWirelessStatus().then(function(state) {
+ if (!L.isObject(state))
+ throw !1;
+ return (_cache.wifi = state);
+ }).catch(function() {
+ return (_cache.wifi = {});
+ });
+
+ return Promise.resolve(_cache.wifi);
+}
+
+function getInterfaceState() {
+ if (_cache.interfacedump == null)
+ return callNetworkInterfaceStatus().then(function(state) {
+ if (!Array.isArray(state))
+ throw !1;
+ return (_cache.interfacedump = state);
+ }).catch(function() {
+ return (_cache.interfacedump = []);
+ });
+
+ return Promise.resolve(_cache.interfacedump);
+}
+
+function getDeviceState() {
+ if (_cache.devicedump == null)
+ return callNetworkDeviceStatus().then(function(state) {
+ if (!L.isObject(state))
+ throw !1;
+ return (_cache.devicedump = state);
+ }).catch(function() {
+ return (_cache.devicedump = {});
+ });
+
+ return Promise.resolve(_cache.devicedump);
+}
+
+function getIfaddrState() {
+ if (_cache.ifaddrs == null)
+ return callLuciIfaddrs().then(function(addrs) {
+ if (!Array.isArray(addrs))
+ throw !1;
+ return (_cache.ifaddrs = addrs);
+ }).catch(function() {
+ return (_cache.ifaddrs = []);
+ });
+
+ return Promise.resolve(_cache.ifaddrs);
+}
+
+function getNetdevState() {
+ if (_cache.devices == null)
+ return callLuciNetdevs().then(function(state) {
+ if (!L.isObject(state))
+ throw !1;
+ return (_cache.devices = state);
+ }).catch(function() {
+ return (_cache.devices = {});
+ });
+
+ return Promise.resolve(_cache.devices);
+}
+
+function getBoardState() {
+ if (_cache.board == null)
+ return callLuciBoardjson().then(function(state) {
+ if (!L.isObject(state))
+ throw !1;
+ return (_cache.board = state);
+ }).catch(function() {
+ return (_cache.board = {});
+ });
+
+ return Promise.resolve(_cache.board);
+}
+
+function getWifiStateBySid(sid) {
+ var s = uci.get('wireless', sid);
+
+ if (s != null && s['.type'] == 'wifi-iface') {
+ for (var radioname in _cache.wifi) {
+ for (var i = 0; i < _cache.wifi[radioname].interfaces.length; i++) {
+ var netstate = _cache.wifi[radioname].interfaces[i];
+
+ if (typeof(netstate.section) != 'string')
+ continue;
+
+ var s2 = uci.get('wireless', netstate.section);
+
+ if (s2 != null && s['.type'] == s2['.type'] && s['.name'] == s2['.name'])
+ return [ radioname, _cache.wifi[radioname], netstate ];
+ }
+ }
+ }
+
+ return null;
+}
+
+function getWifiStateByIfname(ifname) {
+ for (var radioname in _cache.wifi) {
+ for (var i = 0; i < _cache.wifi[radioname].interfaces.length; i++) {
+ var netstate = _cache.wifi[radioname].interfaces[i];
+
+ if (typeof(netstate.ifname) != 'string')
+ continue;
+
+ if (netstate.ifname == ifname)
+ return [ radioname, _cache.wifi[radioname], netstate ];
+ }
+ }
+
+ return null;
+}
+
+function isWifiIfname(ifname) {
+ for (var i = 0; i < iface_patterns_wireless.length; i++)
+ if (iface_patterns_wireless[i].test(ifname))
+ return true;
+
+ return false;
+}
+
+function getWifiIwinfoByIfname(ifname, forcePhyOnly) {
+ var tasks = [ callIwinfoInfo(ifname) ];
+
+ if (!forcePhyOnly)
+ tasks.push(getNetdevState());
+
+ return Promise.all(tasks).then(function(info) {
+ var iwinfo = info[0],
+ devstate = info[1],
+ phyonly = forcePhyOnly || !devstate[ifname] || (devstate[ifname].type != 1);
+
+ if (L.isObject(iwinfo)) {
+ if (phyonly) {
+ delete iwinfo.bitrate;
+ delete iwinfo.quality;
+ delete iwinfo.quality_max;
+ delete iwinfo.mode;
+ delete iwinfo.ssid;
+ delete iwinfo.bssid;
+ delete iwinfo.encryption;
+ }
+
+ iwinfo.ifname = ifname;
+ }
+
+ return iwinfo;
+ }).catch(function() {
+ return null;
+ });
+}
+
+function getWifiSidByNetid(netid) {
+ var m = /^(\w+)\.network(\d+)$/.exec(netid);
+ if (m) {
+ var sections = uci.sections('wireless', 'wifi-iface');
+ for (var i = 0, n = 0; i < sections.length; i++) {
+ if (sections[i].device != m[1])
+ continue;
+
+ if (++n == +m[2])
+ return sections[i]['.name'];
+ }
+ }
+
+ return null;
+}
+
+function getWifiSidByIfname(ifname) {
+ var sid = getWifiSidByNetid(ifname);
+
+ if (sid != null)
+ return sid;
+
+ var res = getWifiStateByIfname(ifname);
+
+ if (res != null && L.isObject(res[2]) && typeof(res[2].section) == 'string')
+ return res[2].section;
+
+ return null;
+}
+
+function getWifiNetidBySid(sid) {
+ var s = uci.get('wireless', sid);
+ if (s != null && s['.type'] == 'wifi-iface') {
+ var radioname = s.device;
+ if (typeof(s.device) == 'string') {
+ var i = 0, netid = null, sections = uci.sections('wireless', 'wifi-iface');
+ for (var i = 0, n = 0; i < sections.length; i++) {
+ if (sections[i].device != s.device)
+ continue;
+
+ n++;
+
+ if (sections[i]['.name'] != s['.name'])
+ continue;
+
+ return [ '%s.network%d'.format(s.device, n), s.device ];
+ }
+
+ }
+ }
+
+ return null;
+}
+
+function getWifiNetidByNetname(name) {
+ var sections = uci.sections('wireless', 'wifi-iface');
+ for (var i = 0; i < sections.length; i++) {
+ if (typeof(sections[i].network) != 'string')
+ continue;
+
+ var nets = sections[i].network.split(/\s+/);
+ for (var j = 0; j < nets.length; j++) {
+ if (nets[j] != name)
+ continue;
+
+ return getWifiNetidBySid(sections[i]['.name']);
+ }
+ }
+
+ return null;
+}
+
+function isVirtualIfname(ifname) {
+ for (var i = 0; i < iface_patterns_virtual.length; i++)
+ if (iface_patterns_virtual[i].test(ifname))
+ return true;
+
+ return false;
+}
+
+function isIgnoredIfname(ifname) {
+ for (var i = 0; i < iface_patterns_ignore.length; i++)
+ if (iface_patterns_ignore[i].test(ifname))
+ return true;
+
+ return false;
+}
+
+function appendValue(config, section, option, value) {
+ var values = uci.get(config, section, option),
+ isArray = Array.isArray(values),
+ rv = false;
+
+ if (isArray == false)
+ values = String(values || '').split(/\s+/);
+
+ if (values.indexOf(value) == -1) {
+ values.push(value);
+ rv = true;
+ }
+
+ uci.set(config, section, option, isArray ? values : values.join(' '));
+
+ return rv;
+}
+
+function removeValue(config, section, option, value) {
+ var values = uci.get(config, section, option),
+ isArray = Array.isArray(values),
+ rv = false;
+
+ if (isArray == false)
+ values = String(values || '').split(/\s+/);
+
+ for (var i = values.length - 1; i >= 0; i--) {
+ if (values[i] == value) {
+ values.splice(i, 1);
+ rv = true;
+ }
+ }
+
+ if (values.length > 0)
+ uci.set(config, section, option, isArray ? values : values.join(' '));
+ else
+ uci.unset(config, section, option);
+
+ return rv;
+}
+
+function prefixToMask(bits, v6) {
+ var w = v6 ? 128 : 32,
+ m = [];
+
+ if (bits > w)
+ return null;
+
+ for (var i = 0; i < w / 16; i++) {
+ var b = Math.min(16, bits);
+ m.push((0xffff << (16 - b)) & 0xffff);
+ bits -= b;
+ }
+
+ if (v6)
+ return String.prototype.format.apply('%x:%x:%x:%x:%x:%x:%x:%x', m).replace(/:0(?::0)+$/, '::');
+ else
+ return '%d.%d.%d.%d'.format(m[0] >>> 8, m[0] & 0xff, m[1] >>> 8, m[1] & 0xff);
+}
+
+function maskToPrefix(mask, v6) {
+ var m = v6 ? validation.parseIPv6(mask) : validation.parseIPv4(mask);
+
+ if (!m)
+ return null;
+
+ var bits = 0;
+
+ for (var i = 0, z = false; i < m.length; i++) {
+ z = z || !m[i];
+
+ while (!z && (m[i] & (v6 ? 0x8000 : 0x80))) {
+ m[i] = (m[i] << 1) & (v6 ? 0xffff : 0xff);
+ bits++;
+ }
+
+ if (m[i])
+ return null;
+ }
+
+ return bits;
+}
+
+function initNetworkState() {
+ if (_state == null)
+ return (_state = Promise.all([
+ getInterfaceState(), getDeviceState(), getBoardState(),
+ getWifiState(), getIfaddrState(), getNetdevState(),
+ uci.load('network'), uci.load('wireless'), uci.load('luci')
+ ]).finally(function() {
+ var ifaddrs = _cache.ifaddrs,
+ devices = _cache.devices,
+ board = _cache.board,
+ s = { isTunnel: {}, isBridge: {}, isSwitch: {}, isWifi: {}, interfaces: {}, bridges: {}, switches: {} };
+
+ for (var i = 0, a; (a = ifaddrs[i]) != null; i++) {
+ var name = a.name.replace(/:.+$/, '');
+
+ if (isVirtualIfname(name))
+ s.isTunnel[name] = true;
+
+ if (s.isTunnel[name] || !(isIgnoredIfname(name) || isVirtualIfname(name))) {
+ s.interfaces[name] = s.interfaces[name] || {
+ idx: a.ifindex || i,
+ name: name,
+ rawname: a.name,
+ flags: [],
+ ipaddrs: [],
+ ip6addrs: []
+ };
+
+ if (a.family == 'packet') {
+ s.interfaces[name].flags = a.flags;
+ s.interfaces[name].stats = a.data;
+ s.interfaces[name].macaddr = a.addr;
+ }
+ else if (a.family == 'inet') {
+ s.interfaces[name].ipaddrs.push(a.addr + '/' + a.netmask);
+ }
+ else if (a.family == 'inet6') {
+ s.interfaces[name].ip6addrs.push(a.addr + '/' + a.netmask);
+ }
+ }
+ }
+
+ for (var devname in devices) {
+ var dev = devices[devname];
+
+ if (dev.bridge) {
+ var b = {
+ name: devname,
+ id: dev.id,
+ stp: dev.stp,
+ ifnames: []
+ };
+
+ for (var i = 0; dev.ports && i < dev.ports.length; i++) {
+ var subdev = s.interfaces[dev.ports[i]];
+
+ if (subdev == null)
+ continue;
+
+ b.ifnames.push(subdev);
+ subdev.bridge = b;
+ }
+
+ s.bridges[devname] = b;
+ }
+ }
+
+ if (L.isObject(board.switch)) {
+ for (var switchname in board.switch) {
+ var layout = board.switch[switchname],
+ netdevs = {},
+ nports = {},
+ ports = [],
+ pnum = null,
+ role = null;
+
+ if (L.isObject(layout) && Array.isArray(layout.ports)) {
+ for (var i = 0, port; (port = layout.ports[i]) != null; i++) {
+ if (typeof(port) == 'object' && typeof(port.num) == 'number' &&
+ (typeof(port.role) == 'string' || typeof(port.device) == 'string')) {
+ var spec = {
+ num: port.num,
+ role: port.role || 'cpu',
+ index: (port.index != null) ? port.index : port.num
+ };
+
+ if (port.device != null) {
+ spec.device = port.device;
+ spec.tagged = spec.need_tag;
+ netdevs[port.num] = port.device;
+ }
+
+ ports.push(spec);
+
+ if (port.role != null)
+ nports[port.role] = (nports[port.role] || 0) + 1;
+ }
+ }
+
+ ports.sort(function(a, b) {
+ if (a.role != b.role)
+ return (a.role < b.role) ? -1 : 1;
+
+ return (a.index - b.index);
+ });
+
+ for (var i = 0, port; (port = ports[i]) != null; i++) {
+ if (port.role != role) {
+ role = port.role;
+ pnum = 1;
+ }
+
+ if (role == 'cpu')
+ port.label = 'CPU (%s)'.format(port.device);
+ else if (nports[role] > 1)
+ port.label = '%s %d'.format(role.toUpperCase(), pnum++);
+ else
+ port.label = role.toUpperCase();
+
+ delete port.role;
+ delete port.index;
+ }
+
+ s.switches[switchname] = {
+ ports: ports,
+ netdevs: netdevs
+ };
+ }
+ }
+ }
+
+ return (_state = s);
+ }));
+
+ return Promise.resolve(_state);
+}
+
+function ifnameOf(obj) {
+ if (obj instanceof Interface)
+ return obj.name();
+ else if (obj instanceof Protocol)
+ return obj.ifname();
+ else if (typeof(obj) == 'string')
+ return obj.replace(/:.+$/, '');
+
+ return null;
+}
+
+function networkSort(a, b) {
+ return a.getName() > b.getName();
+}
+
+function deviceSort(a, b) {
+ var typeWeigth = { wifi: 2, alias: 3 },
+ weightA = typeWeigth[a.getType()] || 1,
+ weightB = typeWeigth[b.getType()] || 1;
+
+ if (weightA != weightB)
+ return weightA - weightB;
+
+ return a.getName() > b.getName();
+}
+
+
+var Network, Protocol, Device, WifiDevice, WifiNetwork;
+
+Network = L.Class.extend({
+ getProtocol: function(protoname, netname) {
+ var v = _protocols[protoname];
+ if (v != null)
+ return v(netname || '__dummy__');
+
+ return null;
+ },
+
+ getProtocols: function() {
+ var rv = [];
+
+ for (var protoname in _protocols)
+ rv.push(_protocols[protoname]('__dummy__'));
+
+ return rv;
+ },
+
+ registerProtocol: function(protoname, methods) {
+ var proto = Protocol.extend(Object.assign({}, methods, {
+ __init__: function(name) {
+ this.sid = name;
+ },
+
+ proto: function() {
+ return protoname;
+ }
+ }));
+
+ _protocols[protoname] = proto;
+
+ return proto;
+ },
+
+ registerPatternVirtual: function(pat) {
+ iface_patterns_virtual.push(pat);
+ },
+
+ registerErrorCode: function(code, message) {
+ if (typeof(code) == 'string' &&
+ typeof(message) == 'string' &&
+ proto_errors.hasOwnProperty(code)) {
+ proto_errors[code] = message;
+ return true;
+ }
+
+ return false;
+ },
+
+ addNetwork: function(name, options) {
+ return this.getNetwork(name).then(L.bind(function(existingNetwork) {
+ if (name != null && /^[a-zA-Z0-9_]+$/.test(name) && existingNetwork == null) {
+ var sid = uci.add('network', 'interface', name);
+
+ if (sid != null) {
+ if (L.isObject(options))
+ for (var key in options)
+ if (options.hasOwnProperty(key))
+ uci.set('network', sid, key, options[key]);
+
+ return this.instantiateNetwork(sid);
+ }
+ }
+ else if (existingNetwork != null && existingNetwork.isEmpty()) {
+ if (L.isObject(options))
+ for (var key in options)
+ if (options.hasOwnProperty(key))
+ existingNetwork.set(key, options[key]);
+
+ return existingNetwork;
+ }
+ }, this));
+ },
+
+ getNetwork: function(name) {
+ return initNetworkState().then(L.bind(function() {
+ var section = (name != null) ? uci.get('network', name) : null;
+
+ if (section != null && section['.type'] == 'interface') {
+ return this.instantiateNetwork(name);
+ }
+ else if (name != null) {
+ for (var i = 0; i < _cache.interfacedump.length; i++)
+ if (_cache.interfacedump[i].interface == name)
+ return this.instantiateNetwork(name, _cache.interfacedump[i].proto);
+ }
+
+ return null;
+ }, this));
+ },
+
+ getNetworks: function() {
+ return initNetworkState().then(L.bind(function() {
+ var uciInterfaces = uci.sections('network', 'interface'),
+ networks = {};
+
+ for (var i = 0; i < uciInterfaces.length; i++)
+ networks[uciInterfaces[i]['.name']] = this.instantiateNetwork(uciInterfaces[i]['.name']);
+
+ for (var i = 0; i < _cache.interfacedump.length; i++)
+ if (networks[_cache.interfacedump[i].interface] == null)
+ networks[_cache.interfacedump[i].interface] =
+ this.instantiateNetwork(_cache.interfacedump[i].interface, _cache.interfacedump[i].proto);
+
+ var rv = [];
+
+ for (var network in networks)
+ if (networks.hasOwnProperty(network))
+ rv.push(networks[network]);
+
+ rv.sort(networkSort);
+
+ return rv;
+ }, this));
+ },
+
+ deleteNetwork: function(name) {
+ return Promise.all([ L.require('firewall').catch(function() { return null }), initNetworkState() ]).then(function() {
+ var uciInterface = uci.get('network', name);
+
+ if (uciInterface != null && uciInterface['.type'] == 'interface') {
+ uci.remove('network', name);
+
+ uci.sections('luci', 'ifstate', function(s) {
+ if (s.interface == name)
+ uci.remove('luci', s['.name']);
+ });
+
+ uci.sections('network', 'alias', function(s) {
+ if (s.interface == name)
+ uci.remove('network', s['.name']);
+ });
+
+ uci.sections('network', 'route', function(s) {
+ if (s.interface == name)
+ uci.remove('network', s['.name']);
+ });
+
+ uci.sections('network', 'route6', function(s) {
+ if (s.interface == name)
+ uci.remove('network', s['.name']);
+ });
+
+ uci.sections('wireless', 'wifi-iface', function(s) {
+ var networks = L.toArray(s.network).filter(function(network) { return network != name });
+
+ if (networks.length > 0)
+ uci.set('wireless', s['.name'], 'network', networks.join(' '));
+ else
+ uci.unset('wireless', s['.name'], 'network');
+ });
+
+ if (L.firewall)
+ return L.firewall.deleteNetwork(name).then(function() { return true });
+
+ return true;
+ }
+
+ return false;
+ });
+ },
+
+ renameNetwork: function(oldName, newName) {
+ return initNetworkState().then(function() {
+ if (newName == null || !/^[a-zA-Z0-9_]+$/.test(newName) || uci.get('network', newName) != null)
+ return false;
+
+ var oldNetwork = uci.get('network', oldName);
+
+ if (oldNetwork == null || oldNetwork['.type'] != 'interface')
+ return false;
+
+ var sid = uci.add('network', 'interface', newName);
+
+ for (var key in oldNetwork)
+ if (oldNetwork.hasOwnProperty(key) && key.charAt(0) != '.')
+ uci.set('network', sid, key, oldNetwork[key]);
+
+ uci.sections('luci', 'ifstate', function(s) {
+ if (s.interface == oldName)
+ uci.set('luci', s['.name'], 'interface', newName);
+ });
+
+ uci.sections('network', 'alias', function(s) {
+ if (s.interface == oldName)
+ uci.set('network', s['.name'], 'interface', newName);
+ });
+
+ uci.sections('network', 'route', function(s) {
+ if (s.interface == oldName)
+ uci.set('network', s['.name'], 'interface', newName);
+ });
+
+ uci.sections('network', 'route6', function(s) {
+ if (s.interface == oldName)
+ uci.set('network', s['.name'], 'interface', newName);
+ });
+
+ uci.sections('wireless', 'wifi-iface', function(s) {
+ var networks = L.toArray(s.network).map(function(network) { return (network == oldName ? newName : network) });
+
+ if (networks.length > 0)
+ uci.set('wireless', s['.name'], 'network', networks.join(' '));
+ });
+
+ uci.remove('network', oldName);
+
+ return true;
+ });
+ },
+
+ getDevice: function(name) {
+ return initNetworkState().then(L.bind(function() {
+ if (name == null)
+ return null;
+
+ if (_state.interfaces.hasOwnProperty(name) || isWifiIfname(name))
+ return this.instantiateDevice(name);
+
+ var netid = getWifiNetidBySid(name);
+ if (netid != null)
+ return this.instantiateDevice(netid[0]);
+
+ return null;
+ }, this));
+ },
+
+ getDevices: function() {
+ return initNetworkState().then(L.bind(function() {
+ var devices = {};
+
+ /* find simple devices */
+ var uciInterfaces = uci.sections('network', 'interface');
+ for (var i = 0; i < uciInterfaces.length; i++) {
+ var ifnames = L.toArray(uciInterfaces[i].ifname);
+
+ for (var j = 0; j < ifnames.length; j++) {
+ if (ifnames[j].charAt(0) == '@')
+ continue;
+
+ if (isIgnoredIfname(ifnames[j]) || isVirtualIfname(ifnames[j]) || isWifiIfname(ifnames[j]))
+ continue;
+
+ devices[ifnames[j]] = this.instantiateDevice(ifnames[j]);
+ }
+ }
+
+ for (var ifname in _state.interfaces) {
+ if (devices.hasOwnProperty(ifname))
+ continue;
+
+ if (isIgnoredIfname(ifname) || isVirtualIfname(ifname) || isWifiIfname(ifname))
+ continue;
+
+ devices[ifname] = this.instantiateDevice(ifname);
+ }
+
+ /* find VLAN devices */
+ var uciSwitchVLANs = uci.sections('network', 'switch_vlan');
+ for (var i = 0; i < uciSwitchVLANs.length; i++) {
+ if (typeof(uciSwitchVLANs[i].ports) != 'string' ||
+ typeof(uciSwitchVLANs[i].device) != 'string' ||
+ !_state.switches.hasOwnProperty(uciSwitchVLANs[i].device))
+ continue;
+
+ var ports = uciSwitchVLANs[i].ports.split(/\s+/);
+ for (var j = 0; j < ports.length; j++) {
+ var m = ports[j].match(/^(\d+)([tu]?)$/);
+ if (m == null)
+ continue;
+
+ var netdev = _state.switches[uciSwitchVLANs[i].device].netdevs[m[1]];
+ if (netdev == null)
+ continue;
+
+ if (!devices.hasOwnProperty(netdev))
+ devices[netdev] = this.instantiateDevice(netdev);
+
+ _state.isSwitch[netdev] = true;
+
+ if (m[2] != 't')
+ continue;
+
+ var vid = uciSwitchVLANs[i].vid || uciSwitchVLANs[i].vlan;
+ vid = (vid != null ? +vid : null);
+
+ if (vid == null || vid < 0 || vid > 4095)
+ continue;
+
+ var vlandev = '%s.%d'.format(netdev, vid);
+
+ if (!devices.hasOwnProperty(vlandev))
+ devices[vlandev] = this.instantiateDevice(vlandev);
+
+ _state.isSwitch[vlandev] = true;
+ }
+ }
+
+ /* find wireless interfaces */
+ var uciWifiIfaces = uci.sections('wireless', 'wifi-iface'),
+ networkCount = {};
+
+ for (var i = 0; i < uciWifiIfaces.length; i++) {
+ if (typeof(uciWifiIfaces[i].device) != 'string')
+ continue;
+
+ networkCount[uciWifiIfaces[i].device] = (networkCount[uciWifiIfaces[i].device] || 0) + 1;
+
+ var netid = '%s.network%d'.format(uciWifiIfaces[i].device, networkCount[uciWifiIfaces[i].device]);
+
+ devices[netid] = this.instantiateDevice(netid);
+ }
+
+ var rv = [];
+
+ for (var netdev in devices)
+ if (devices.hasOwnProperty(netdev))
+ rv.push(devices[netdev]);
+
+ rv.sort(deviceSort);
+
+ return rv;
+ }, this));
+ },
+
+ isIgnoredDevice: function(name) {
+ return isIgnoredIfname(name);
+ },
+
+ getWifiDevice: function(devname) {
+ return Promise.all([ getWifiIwinfoByIfname(devname, true), initNetworkState() ]).then(L.bind(function(res) {
+ var existingDevice = uci.get('wireless', devname);
+
+ if (existingDevice == null || existingDevice['.type'] != 'wifi-device')
+ return null;
+
+ return this.instantiateWifiDevice(devname, res[0]);
+ }, this));
+ },
+
+ getWifiDevices: function() {
+ var deviceNames = [];
+
+ return initNetworkState().then(L.bind(function() {
+ var uciWifiDevices = uci.sections('wireless', 'wifi-device'),
+ tasks = [];
+
+ for (var i = 0; i < uciWifiDevices.length; i++) {
+ tasks.push(callIwinfoInfo(uciWifiDevices['.name'], true));
+ deviceNames.push(uciWifiDevices['.name']);
+ }
+
+ return Promise.all(tasks);
+ }, this)).then(L.bind(function(iwinfos) {
+ var rv = [];
+
+ for (var i = 0; i < deviceNames.length; i++)
+ if (L.isObject(iwinfos[i]))
+ rv.push(this.instantiateWifiDevice(deviceNames[i], iwinfos[i]));
+
+ rv.sort(function(a, b) { return a.getName() < b.getName() });
+
+ return rv;
+ }, this));
+ },
+
+ getWifiNetwork: function(netname) {
+ var sid, res, netid, radioname, radiostate, netstate;
+
+ return initNetworkState().then(L.bind(function() {
+ sid = getWifiSidByNetid(netname);
+
+ if (sid != null) {
+ res = getWifiStateBySid(sid);
+ netid = netname;
+ radioname = res ? res[0] : null;
+ radiostate = res ? res[1] : null;
+ netstate = res ? res[2] : null;
+ }
+ else {
+ res = getWifiStateByIfname(netname);
+
+ if (res != null) {
+ radioname = res[0];
+ radiostate = res[1];
+ netstate = res[2];
+ sid = netstate.section;
+ netid = L.toArray(getWifiNetidBySid(sid))[0];
+ }
+ else {
+ res = getWifiStateBySid(netname);
+
+ if (res != null) {
+ radioname = res[0];
+ radiostate = res[1];
+ netstate = res[2];
+ sid = netname;
+ netid = L.toArray(getWifiNetidBySid(sid))[0];
+ }
+ else {
+ res = getWifiNetidBySid(netname);
+
+ if (res != null) {
+ netid = res[0];
+ radioname = res[1];
+ sid = netname;
+ }
+ }
+ }
+ }
+
+ return (netstate ? getWifiIwinfoByIfname(netstate.ifname) : Promise.reject())
+ .catch(function() { return radioname ? getWifiIwinfoByIfname(radioname) : Promise.reject() })
+ .catch(function() { return Promise.resolve({ ifname: netid || sid || netname }) });
+ }, this)).then(L.bind(function(iwinfo) {
+ return this.instantiateWifiNetwork(sid || netname, radioname, radiostate, netid, netstate, iwinfo);
+ }, this));
+ },
+
+ addWifiNetwork: function(options) {
+ return initNetworkState().then(L.bind(function() {
+ if (options == null ||
+ typeof(options) != 'object' ||
+ typeof(options.device) != 'string')
+ return null;
+
+ var existingDevice = uci.get('wireless', options.device);
+ if (existingDevice == null || existingDevice['.type'] != 'wifi-device')
+ return null;
+
+ var sid = uci.add('wireless', 'wifi-iface');
+ for (var key in options)
+ if (options.hasOwnProperty(key))
+ uci.set('wireless', sid, key, options[key]);
+
+ var radioname = existingDevice['.name'],
+ netid = getWifiNetidBySid(sid) || [];
+
+ return this.instantiateWifiNetwork(sid, radioname, _cache.wifi[radioname], netid[0], null, { ifname: netid });
+ }, this));
+ },
+
+ deleteWifiNetwork: function(netname) {
+ return initNetworkState().then(L.bind(function() {
+ var sid = getWifiSidByIfname(netname);
+
+ if (sid == null)
+ return false;
+
+ uci.remove('wireless', sid);
+ return true;
+ }, this));
+ },
+
+ getStatusByRoute: function(addr, mask) {
+ return initNetworkState().then(L.bind(function() {
+ var rv = [];
+
+ for (var i = 0; i < _state.interfacedump.length; i++) {
+ if (!Array.isArray(_state.interfacedump[i].route))
+ continue;
+
+ for (var j = 0; j < _state.interfacedump[i].route.length; j++) {
+ if (typeof(_state.interfacedump[i].route[j]) != 'object' ||
+ typeof(_state.interfacedump[i].route[j].target) != 'string' ||
+ typeof(_state.interfacedump[i].route[j].mask) != 'number')
+ continue;
+
+ if (_state.interfacedump[i].route[j].table)
+ continue;
+
+ rv.push(_state.interfacedump[i]);
+ }
+ }
+
+ return rv;
+ }, this));
+ },
+
+ getStatusByAddress: function(addr) {
+ return initNetworkState().then(L.bind(function() {
+ var rv = [];
+
+ for (var i = 0; i < _state.interfacedump.length; i++) {
+ if (Array.isArray(_state.interfacedump[i]['ipv4-address']))
+ for (var j = 0; j < _state.interfacedump[i]['ipv4-address'].length; j++)
+ if (typeof(_state.interfacedump[i]['ipv4-address'][j]) == 'object' &&
+ _state.interfacedump[i]['ipv4-address'][j].address == addr)
+ return _state.interfacedump[i];
+
+ if (Array.isArray(_state.interfacedump[i]['ipv6-address']))
+ for (var j = 0; j < _state.interfacedump[i]['ipv6-address'].length; j++)
+ if (typeof(_state.interfacedump[i]['ipv6-address'][j]) == 'object' &&
+ _state.interfacedump[i]['ipv6-address'][j].address == addr)
+ return _state.interfacedump[i];
+
+ if (Array.isArray(_state.interfacedump[i]['ipv6-prefix-assignment']))
+ for (var j = 0; j < _state.interfacedump[i]['ipv6-prefix-assignment'].length; j++)
+ if (typeof(_state.interfacedump[i]['ipv6-prefix-assignment'][j]) == 'object' &&
+ typeof(_state.interfacedump[i]['ipv6-prefix-assignment'][j]['local-address']) == 'object' &&
+ _state.interfacedump[i]['ipv6-prefix-assignment'][j]['local-address'].address == addr)
+ return _state.interfacedump[i];
+ }
+
+ return null;
+ }, this));
+ },
+
+ getWANNetworks: function() {
+ return this.getStatusByRoute('0.0.0.0', 0).then(L.bind(function(statuses) {
+ var rv = [];
+
+ for (var i = 0; i < statuses.length; i++)
+ rv.push(this.instantiateNetwork(statuses[i].interface, statuses[i].proto));
+
+ return rv;
+ }, this));
+ },
+
+ getWAN6Networks: function() {
+ return this.getStatusByRoute('::', 0).then(L.bind(function(statuses) {
+ var rv = [];
+
+ for (var i = 0; i < statuses.length; i++)
+ rv.push(this.instantiateNetwork(statuses[i].interface, statuses[i].proto));
+
+ return rv;
+ }, this));
+ },
+
+ getSwitchTopologies: function() {
+ return initNetworkState().then(function() {
+ return _state.switches;
+ });
+ },
+
+ instantiateNetwork: function(name, proto) {
+ if (name == null)
+ return null;
+
+ proto = (proto == null ? uci.get('network', name, 'proto') : proto);
+
+ var protoClass = _protocols[proto] || Protocol;
+ return new protoClass(name);
+ },
+
+ instantiateDevice: function(name, network) {
+ return new Device(name, network);
+ },
+
+ instantiateWifiDevice: function(radioname, iwinfo) {
+ return new WifiDevice(radioname, iwinfo);
+ },
+
+ instantiateWifiNetwork: function(sid, radioname, radiostate, netid, netstate, iwinfo) {
+ return new WifiNetwork(sid, radioname, radiostate, netid, netstate, iwinfo);
+ }
+});
+
+Protocol = L.Class.extend({
+ __init__: function(name) {
+ this.sid = name;
+ },
+
+ _get: function(opt) {
+ var val = uci.get('network', this.sid, opt);
+
+ if (Array.isArray(val))
+ return val.join(' ');
+
+ return val || '';
+ },
+
+ _ubus: function(field) {
+ for (var i = 0; i < _cache.interfacedump.length; i++) {
+ if (_cache.interfacedump[i].interface != this.sid)
+ continue;
+
+ return (field != null ? _cache.interfacedump[i][field] : _cache.interfacedump[i]);
+ }
+ },
+
+ get: function(opt) {
+ return uci.get('network', this.sid, opt);
+ },
+
+ set: function(opt, val) {
+ return uci.set('network', this.sid, opt, val);
+ },
+
+ getIfname: function() {
+ var ifname;
+
+ if (this.isFloating())
+ ifname = this._ubus('l3_device');
+ else
+ ifname = this._ubus('device');
+
+ if (ifname != null)
+ return ifname;
+
+ var res = getWifiNetidByNetname(this.sid);
+ return (res != null ? res[0] : null);
+ },
+
+ getProtocol: function() {
+ return 'none';
+ },
+
+ getI18n: function() {
+ switch (this.getProtocol()) {
+ case 'none': return _('Unmanaged');
+ case 'static': return _('Static address');
+ case 'dhcp': return _('DHCP client');
+ default: return _('Unknown');
+ }
+ },
+
+ getType: function() {
+ return this._get('type');
+ },
+
+ getName: function() {
+ return this.sid;
+ },
+
+ getUptime: function() {
+ return this._ubus('uptime') || 0;
+ },
+
+ getExpiry: function() {
+ var u = this._ubus('uptime'),
+ d = this._ubus('data');
+
+ if (typeof(u) == 'number' && d != null &&
+ typeof(d) == 'object' && typeof(d.leasetime) == 'number') {
+ var r = d.leasetime - (u % d.leasetime);
+ return (r > 0 ? r : 0);
+ }
+
+ return -1;
+ },
+
+ getMetric: function() {
+ return this._ubus('metric') || 0;
+ },
+
+ getZoneName: function() {
+ var d = this._ubus('data');
+
+ if (L.isObject(d) && typeof(d.zone) == 'string')
+ return d.zone;
+
+ return null;
+ },
+
+ getIPAddr: function() {
+ var addrs = this._ubus('ipv4-address');
+ return ((Array.isArray(addrs) && addrs.length) ? addrs[0].address : null);
+ },
+
+ getIPAddrs: function() {
+ var addrs = this._ubus('ipv4-address'),
+ rv = [];
+
+ if (Array.isArray(addrs))
+ for (var i = 0; i < addrs.length; i++)
+ rv.push('%s/%d'.format(addrs[i].address, addrs[i].mask));
+
+ return rv;
+ },
+
+ getNetmask: function() {
+ var addrs = this._ubus('ipv4-address');
+ if (Array.isArray(addrs) && addrs.length)
+ return prefixToMask(addrs[0].mask, false);
+ },
+
+ getGatewayAddr: function() {
+ var routes = this._ubus('route');
+
+ if (Array.isArray(routes))
+ for (var i = 0; i < routes.length; i++)
+ if (typeof(routes[i]) == 'object' &&
+ routes[i].target == '0.0.0.0' &&
+ routes[i].mask == 0)
+ return routes[i].nexthop;
+
+ return null;
+ },
+
+ getDNSAddrs: function() {
+ var addrs = this._ubus('dns-server'),
+ rv = [];
+
+ if (Array.isArray(addrs))
+ for (var i = 0; i < addrs.length; i++)
+ if (!/:/.test(addrs[i]))
+ rv.push(addrs[i]);
+
+ return rv;
+ },
+
+ getIP6Addr: function() {
+ var addrs = this._ubus('ipv6-address');
+
+ if (Array.isArray(addrs) && L.isObject(addrs[0]))
+ return '%s/%d'.format(addrs[0].address, addrs[0].mask);
+
+ addrs = this._ubus('ipv6-prefix-assignment');
+
+ if (Array.isArray(addrs) && L.isObject(addrs[0]) && L.isObject(addrs[0]['local-address']))
+ return '%s/%d'.format(addrs[0]['local-address'].address, addrs[0]['local-address'].mask);
+
+ return null;
+ },
+
+ getIP6Addrs: function() {
+ var addrs = this._ubus('ipv6-address'),
+ rv = [];
+
+ if (Array.isArray(addrs))
+ for (var i = 0; i < addrs.length; i++)
+ if (L.isObject(addrs[i]))
+ rv.push('%s/%d'.format(addrs[i].address, addrs[i].mask));
+
+ addrs = this._ubus('ipv6-prefix-assignment');
+
+ if (Array.isArray(addrs))
+ for (var i = 0; i < addrs.length; i++)
+ if (L.isObject(addrs[i]) && L.isObject(addrs[i]['local-address']))
+ rv.push('%s/%d'.format(addrs[i]['local-address'].address, addrs[i]['local-address'].mask));
+
+ return rv;
+ },
+
+ getDNS6Addrs: function() {
+ var addrs = this._ubus('dns-server'),
+ rv = [];
+
+ if (Array.isArray(addrs))
+ for (var i = 0; i < addrs.length; i++)
+ if (/:/.test(addrs[i]))
+ rv.push(addrs[i]);
+
+ return rv;
+ },
+
+ getIP6Prefix: function() {
+ var prefixes = this._ubus('ipv6-prefix');
+
+ if (Array.isArray(prefixes) && L.isObject(prefixes[0]))
+ return '%s/%d'.format(prefixes[0].address, prefixes[0].mask);
+
+ return null;
+ },
+
+ getErrors: function() {
+ var errors = this._ubus('errors'),
+ rv = null;
+
+ if (Array.isArray(errors)) {
+ for (var i = 0; i < errors.length; i++) {
+ if (!L.isObject(errors[i]) || typeof(errors[i].code) != 'string')
+ continue;
+
+ rv = rv || [];
+ rv.push(proto_errors[errors[i].code] || _('Unknown error (%s)').format(errors[i].code));
+ }
+ }
+
+ return rv;
+ },
+
+ isBridge: function() {
+ return (!this.isVirtual() && this.getType() == 'bridge');
+ },
+
+ getOpkgPackage: function() {
+ return null;
+ },
+
+ isInstalled: function() {
+ return true;
+ },
+
+ isVirtual: function() {
+ return false;
+ },
+
+ isFloating: function() {
+ return false;
+ },
+
+ isDynamic: function() {
+ return (this._ubus('dynamic') == true);
+ },
+
+ isAlias: function() {
+ var ifnames = L.toArray(uci.get('network', this.sid, 'ifname')),
+ parent = null;
+
+ for (var i = 0; i < ifnames.length; i++)
+ if (ifnames[i].charAt(0) == '@')
+ parent = ifnames[i].substr(1);
+ else if (parent != null)
+ parent = null;
+
+ return parent;
+ },
+
+ isEmpty: function() {
+ if (this.isFloating())
+ return false;
+
+ var empty = true,
+ ifname = this._get('ifname');
+
+ if (ifname != null && ifname.match(/\S+/))
+ empty = false;
+
+ if (empty == true && getWifiNetidBySid(this.sid) != null)
+ empty = false;
+
+ return empty;
+ },
+
+ isUp: function() {
+ return (this._ubus('up') == true);
+ },
+
+ addDevice: function(ifname) {
+ ifname = ifnameOf(ifname);
+
+ if (ifname == null || this.isFloating())
+ return false;
+
+ var wif = getWifiSidByIfname(ifname);
+
+ if (wif != null)
+ return appendValue('wireless', wif, 'network', this.sid);
+
+ return appendValue('network', this.sid, 'ifname', ifname);
+ },
+
+ deleteDevice: function(ifname) {
+ var rv = false;
+
+ ifname = ifnameOf(ifname);
+
+ if (ifname == null || this.isFloating())
+ return false;
+
+ var wif = getWifiSidByIfname(ifname);
+
+ if (wif != null)
+ rv = removeValue('wireless', wif, 'network', this.sid);
+
+ if (removeValue('network', this.sid, 'ifname', ifname))
+ rv = true;
+
+ return rv;
+ },
+
+ getDevice: function() {
+ if (this.isVirtual()) {
+ var ifname = '%s-%s'.format(this.getProtocol(), this.sid);
+ _state.isTunnel[this.getProtocol() + '-' + this.sid] = true;
+ return L.network.instantiateDevice(ifname, this);
+ }
+ else if (this.isBridge()) {
+ var ifname = 'br-%s'.format(this.sid);
+ _state.isBridge[ifname] = true;
+ return new Device(ifname, this);
+ }
+ else {
+ var ifname = this._ubus('l3_device') || this._ubus('device');
+
+ if (ifname != null)
+ return L.network.instantiateDevice(ifname, this);
+
+ var ifnames = L.toArray(uci.get('network', this.sid, 'ifname'));
+
+ for (var i = 0; i < ifnames.length; i++) {
+ var m = ifnames[i].match(/^([^:/]+)/);
+ return ((m && m[1]) ? L.network.instantiateDevice(m[1], this) : null);
+ }
+
+ ifname = getWifiNetidByNetname(this.sid);
+
+ return (ifname != null ? L.network.instantiateDevice(ifname[0], this) : null);
+ }
+ },
+
+ getDevices: function() {
+ var rv = [];
+
+ 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 m = ifnames[i].match(/^([^:/]+)/);
+ if (m != null)
+ rv.push(L.network.instantiateDevice(m[1], this));
+ }
+
+ var uciWifiIfaces = uci.sections('wireless', 'wifi-iface');
+
+ for (var i = 0; i < uciWifiIfaces.length; i++) {
+ if (typeof(uciWifiIfaces[i].device) != 'string')
+ continue;
+
+ var networks = L.toArray(uciWifiIfaces[i].network);
+
+ for (var j = 0; j < networks.length; j++) {
+ if (networks[j] != this.sid)
+ continue;
+
+ var netid = getWifiNetidBySid(uciWifiIfaces[i]['.name']);
+
+ if (netid != null)
+ rv.push(L.network.instantiateDevice(netid[0], this));
+ }
+ }
+
+ rv.sort(deviceSort);
+
+ return rv;
+ },
+
+ containsDevice: function(ifname) {
+ ifname = ifnameOf(ifname);
+
+ if (ifname == null)
+ return false;
+ else if (this.isVirtual() && '%s-%s'.format(this.getProtocol(), this.sid) == ifname)
+ return true;
+ else if (this.isBridge() && 'br-%s'.format(this.sid) == ifname)
+ 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)
+ return true;
+ }
+
+ var wif = getWifiSidByIfname(ifname);
+
+ if (wif != null) {
+ var networks = L.toArray(uci.get('wireless', wif, 'network'));
+
+ for (var i = 0; i < networks.length; i++)
+ if (networks[i] == this.sid)
+ return true;
+ }
+
+ return false;
+ }
+});
+
+Device = L.Class.extend({
+ __init__: function(ifname, network) {
+ var wif = getWifiSidByIfname(ifname);
+
+ 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.ifname = this.ifname || ifname;
+ this.dev = _state.interfaces[this.ifname];
+ this.network = network;
+ },
+
+ _ubus: function(field) {
+ var dump = _cache.devicedump[this.ifname] || {};
+
+ return (field != null ? dump[field] : dump);
+ },
+
+ getName: function() {
+ return (this.wif != null ? this.wif.getIfname() : this.ifname);
+ },
+
+ getMAC: function() {
+ return this._ubus('macaddr');
+ },
+
+ getIPAddrs: function() {
+ var addrs = (this.dev != null ? this.dev.ipaddrs : null);
+ return (Array.isArray(addrs) ? addrs : []);
+ },
+
+ getIP6Addrs: function() {
+ var addrs = (this.dev != null ? this.dev.ip6addrs : null);
+ return (Array.isArray(addrs) ? addrs : []);
+ },
+
+ getType: function() {
+ if (this.ifname != null && this.ifname.charAt(0) == '@')
+ return 'alias';
+ else if (this.wif != null || isWifiIfname(this.ifname))
+ return 'wifi';
+ else if (_state.isBridge[this.ifname])
+ return 'bridge';
+ else if (_state.isTunnel[this.ifname])
+ return 'tunnel';
+ else if (this.ifname.indexOf('.') > -1)
+ return 'vlan';
+ else if (_state.isSwitch[this.ifname])
+ return 'switch';
+ else
+ return 'ethernet';
+ },
+
+ getShortName: function() {
+ if (this.wif != null)
+ return this.wif.getShortName();
+
+ return this.ifname;
+ },
+
+ getI18n: function() {
+ if (this.wif != null) {
+ return '%s: %s "%s"'.format(
+ _('Wireless Network'),
+ this.wif.getActiveMode(),
+ this.wif.getActiveSSID() || this.wif.getActiveBSSID() || this.wif.getID() || '?');
+ }
+
+ return '%s: "%s"'.format(this.getTypeI18n(), this.getName());
+ },
+
+ getTypeI18n: function() {
+ switch (this.getType()) {
+ case 'alias':
+ return _('Alias Interface');
+
+ case 'wifi':
+ return _('Wireless Adapter');
+
+ case 'bridge':
+ return _('Bridge');
+
+ case 'switch':
+ return _('Ethernet Switch');
+
+ case 'vlan':
+ return (_state.isSwitch[this.ifname] ? _('Switch VLAN') : _('Software VLAN'));
+
+ case 'tunnel':
+ return _('Tunnel Interface');
+
+ default:
+ return _('Ethernet Adapter');
+ }
+ },
+
+ getPorts: function() {
+ var br = _state.bridges[this.ifname],
+ rv = [];
+
+ if (br == null || !Array.isArray(br.ifnames))
+ return null;
+
+ for (var i = 0; i < br.ifnames.length; i++)
+ rv.push(L.network.instantiateDevice(br.ifnames[i]));
+
+ return rv;
+ },
+
+ getBridgeID: function() {
+ var br = _state.bridges[this.ifname];
+ return (br != null ? br.id : null);
+ },
+
+ getBridgeSTP: function() {
+ var br = _state.bridges[this.ifname];
+ return (br != null ? !!br.stp : false);
+ },
+
+ isUp: function() {
+ var up = this._ubus('up');
+
+ if (up == null)
+ up = (this.getType() == 'alias');
+
+ return up;
+ },
+
+ isBridge: function() {
+ return (this.getType() == 'bridge');
+ },
+
+ isBridgePort: function() {
+ return (this.dev != null && this.dev.bridge != null);
+ },
+
+ getTXBytes: function() {
+ var stat = this._ubus('statistics');
+ return (stat != null ? stat.tx_bytes || 0 : 0);
+ },
+
+ getRXBytes: function() {
+ var stat = this._ubus('statistics');
+ return (stat != null ? stat.rx_bytes || 0 : 0);
+ },
+
+ getTXPackets: function() {
+ var stat = this._ubus('statistics');
+ return (stat != null ? stat.tx_packets || 0 : 0);
+ },
+
+ getRXPackets: function() {
+ var stat = this._ubus('statistics');
+ return (stat != null ? stat.rx_packets || 0 : 0);
+ },
+
+ getNetwork: function() {
+ return this.getNetworks()[0];
+ },
+
+ getNetworks: function() {
+ if (this.networks == null) {
+ this.networks = [];
+
+ var networks = L.network.getNetworks();
+
+ for (var i = 0; i < networks.length; i++)
+ if (networks[i].containsDevice(this.ifname) || networks[i].getIfname() == this.ifname)
+ this.networks.push(networks[i]);
+
+ this.networks.sort(networkSort);
+ }
+
+ return this.networks;
+ },
+
+ getWifiNetwork: function() {
+ return (this.wif != null ? this.wif : null);
+ }
+});
+
+WifiDevice = L.Class.extend({
+ __init__: function(name, iwinfo) {
+ var uciWifiDevice = uci.get('wireless', name);
+
+ if (uciWifiDevice != null &&
+ uciWifiDevice['.type'] == 'wifi-device' &&
+ uciWifiDevice['.name'] != null) {
+ this.sid = uciWifiDevice['.name'];
+ this.iwinfo = iwinfo;
+ }
+
+ this.sid = this.sid || name;
+ this.iwinfo = this.iwinfo || { ifname: this.sid };
+ },
+
+ get: function(opt) {
+ return uci.get('wireless', this.sid, opt);
+ },
+
+ set: function(opt, value) {
+ return uci.set('wireless', this.sid, opt, value);
+ },
+
+ getName: function() {
+ return this.sid;
+ },
+
+ getHWModes: function() {
+ if (L.isObject(this.iwinfo.hwmodelist))
+ for (var k in this.iwinfo.hwmodelist)
+ return this.iwinfo.hwmodelist;
+
+ return { b: true, g: true };
+ },
+
+ getI18n: function() {
+ var type = this.iwinfo.hardware_name || 'Generic';
+
+ if (this.iwinfo.type == 'wl')
+ type = 'Broadcom';
+
+ var hwmodes = this.getHWModes(),
+ modestr = '';
+
+ if (hwmodes.a) modestr += 'a';
+ if (hwmodes.b) modestr += 'b';
+ if (hwmodes.g) modestr += 'g';
+ if (hwmodes.n) modestr += 'n';
+ if (hwmodes.ad) modestr += 'ac';
+
+ return '%s 802.11%s Wireless Controller (%s)'.format(type, modestr, this.getName());
+ },
+
+ isUp: function() {
+ if (L.isObject(_cache.wifi[this.sid]))
+ return (_cache.wifi[this.sid].up == true);
+
+ return false;
+ },
+
+ getWifiNetwork: function(network) {
+ return L.network.getWifiNetwork(network).then(L.bind(function(networkInstance) {
+ var uciWifiIface = (networkInstance.sid ? uci.get('wireless', networkInstance.sid) : null);
+
+ if (uciWifiIface == null || uciWifiIface['.type'] != 'wifi-iface' || uciWifiIface.device != this.sid)
+ return Promise.reject();
+
+ return networkInstance;
+ }, this));
+ },
+
+ getWifiNetworks: function() {
+ var uciWifiIfaces = uci.sections('wireless', 'wifi-iface'),
+ tasks = [];
+
+ for (var i = 0; i < uciWifiIfaces.length; i++)
+ if (uciWifiIfaces[i].device == this.sid)
+ tasks.push(L.network.getWifiNetwork(uciWifiIfaces[i]['.name']));
+
+ return Promise.all(tasks);
+ },
+
+ addWifiNetwork: function(options) {
+ if (!L.isObject(options))
+ options = {};
+
+ options.device = this.sid;
+
+ return L.network.addWifiNetwork(options);
+ },
+
+ deleteWifiNetwork: function(network) {
+ var sid = null;
+
+ if (network instanceof WifiNetwork) {
+ sid = network.sid;
+ }
+ else {
+ var uciWifiIface = uci.get('wireless', network);
+
+ if (uciWifiIface == null || uciWifiIface['.type'] != 'wifi-iface')
+ sid = getWifiSidByIfname(network);
+ }
+
+ if (sid == null || uci.get('wireless', sid, 'device') != this.sid)
+ return Promise.resolve(false);
+
+ uci.delete('wireless', network);
+
+ return Promise.resolve(true);
+ }
+});
+
+WifiNetwork = L.Class.extend({
+ __init__: function(sid, radioname, radiostate, netid, netstate, iwinfo) {
+ this.sid = sid;
+ this.wdev = iwinfo.ifname;
+ this.iwinfo = iwinfo;
+ this.netid = netid;
+ this._ubusdata = {
+ radio: radioname,
+ dev: radiostate,
+ net: netstate
+ };
+ },
+
+ ubus: function(/* ... */) {
+ var v = this._ubusdata;
+
+ for (var i = 0; i < arguments.length; i++)
+ if (L.isObject(v))
+ v = v[arguments[i]];
+ else
+ return null;
+
+ return v;
+ },
+
+ get: function(opt) {
+ return uci.get('wireless', this.sid, opt);
+ },
+
+ set: function(opt, value) {
+ return uci.set('wireless', this.sid, opt, value);
+ },
+
+ getMode: function() {
+ return this.ubus('net', 'config', 'mode') || this.get('mode') || 'ap';
+ },
+
+ getSSID: function() {
+ return this.ubus('net', 'config', 'ssid') || this.get('ssid');
+ },
+
+ getBSSID: function() {
+ return this.ubus('net', 'config', 'bssid') || this.get('bssid');
+ },
+
+ getNetworkNames: function() {
+ return L.toArray(this.ubus('net', 'config', 'network') || this.get('network'));
+ },
+
+ getID: function() {
+ return this.netid;
+ },
+
+ getName: function() {
+ return this.sid;
+ },
+
+ getIfname: function() {
+ var ifname = this.ubus('net', 'ifname') || this.iwinfo.ifname;
+
+ if (ifname == null || ifname.match(/^(wifi|radio)\d/))
+ ifname = this.netid;
+
+ return ifname;
+ },
+
+ getWifiDevice: function() {
+ var radioname = this.ubus('radio') || this.get('device');
+
+ if (radioname == null)
+ return Promise.reject();
+
+ return L.network.getWifiDevice(radioname);
+ },
+
+ isUp: function() {
+ var device = this.getDevice();
+
+ if (device == null)
+ return false;
+
+ return device.isUp();
+ },
+
+ getActiveMode: function() {
+ var mode = this.iwinfo.mode || this.ubus('net', 'config', 'mode') || this.get('mode') || 'ap';
+
+ switch (mode) {
+ case 'ap': return 'Master';
+ case 'sta': return 'Client';
+ case 'adhoc': return 'Ad-Hoc';
+ case 'mesh': return 'Mesh';
+ case 'monitor': return 'Monitor';
+ default: return mode;
+ }
+ },
+
+ getActiveModeI18n: function() {
+ var mode = this.getActiveMode();
+
+ switch (mode) {
+ case 'Master': return _('Master');
+ case 'Client': return _('Client');
+ case 'Ad-Hoc': return _('Ad-Hoc');
+ case 'Mash': return _('Mesh');
+ case 'Monitor': return _('Monitor');
+ default: return mode;
+ }
+ },
+
+ getActiveSSID: function() {
+ return this.iwinfo.ssid || this.ubus('net', 'config', 'ssid') || this.get('ssid');
+ },
+
+ getActiveBSSID: function() {
+ return this.iwinfo.bssid || this.ubus('net', 'config', 'bssid') || this.get('bssid');
+ },
+
+ getActiveEncryption: function() {
+ var encryption = this.iwinfo.encryption;
+
+ return (L.isObject(encryption) ? encryption.description || '-' : '-');
+ },
+
+ getAssocList: function() {
+ // XXX tbd
+ },
+
+ getFrequency: function() {
+ var freq = this.iwinfo.frequency;
+
+ if (freq != null && freq > 0)
+ return '%.03f'.format(freq / 1000);
+
+ return null;
+ },
+
+ getBitRate: function() {
+ var rate = this.iwinfo.bitrate;
+
+ if (rate != null && rate > 0)
+ return (rate / 1000);
+
+ return null;
+ },
+
+ getChannel: function() {
+ return this.iwinfo.channel || this.ubus('dev', 'config', 'channel') || this.get('channel');
+ },
+
+ getSignal: function() {
+ return this.iwinfo.signal || 0;
+ },
+
+ getNoise: function() {
+ return this.iwinfo.noise || 0;
+ },
+
+ getCountryCode: function() {
+ return this.iwinfo.country || this.ubus('dev', 'config', 'country') || '00';
+ },
+
+ getTXPower: function() {
+ var pwr = this.iwinfo.txpower || 0;
+ return (pwr + this.getTXPowerOffset());
+ },
+
+ getTXPowerOffset: function() {
+ return this.iwinfo.txpower_offset || 0;
+ },
+
+ getSignalLevel: function(signal, noise) {
+ if (this.getActiveBSSID() == '00:00:00:00:00:00')
+ return -1;
+
+ signal = signal || this.getSignal();
+ noise = noise || this.getNoise();
+
+ if (signal < 0 && noise < 0) {
+ var snr = -1 * (noise - signal);
+ return Math.floor(snr / 5);
+ }
+
+ return 0;
+ },
+
+ getSignalPercent: function() {
+ var qc = this.iwinfo.quality || 0,
+ qm = this.iwinfo.quality_max || 0;
+
+ if (qc > 0 && qm > 0)
+ return Math.floor((100 / qm) * qc);
+
+ return 0;
+ },
+
+ getShortName: function() {
+ return '%s "%s"'.format(
+ this.getActiveModeI18n(),
+ this.getActiveSSID() || this.getActiveBSSID() || this.getID());
+ },
+
+ getI18n: function() {
+ return '%s: %s "%s" (%s)'.format(
+ _('Wireless Network'),
+ this.getActiveModeI18n(),
+ this.getActiveSSID() || this.getActiveBSSID() || this.getID(),
+ this.getIfname());
+ },
+
+ getNetwork: function() {
+ return this.getNetworks()[0];
+ },
+
+ getNetworks: function() {
+ var networkNames = this.getNetworkNames(),
+ networks = [];
+
+ for (var i = 0; i < networkNames.length; i++) {
+ var uciInterface = uci.get('network', networkNames[i]);
+
+ if (uciInterface == null || uciInterface['.type'] != 'interface')
+ continue;
+
+ networks.push(L.network.instantiateNetwork(networkNames[i]));
+ }
+
+ networks.sort(networkSort);
+
+ return networks;
+ },
+
+ getDevice: function() {
+ return L.network.instantiateDevice(this.getIfname());
+ }
+});
+
+return Network;
diff --git a/modules/luci-base/htdocs/luci-static/resources/promis.min.js b/modules/luci-base/htdocs/luci-static/resources/promis.min.js
new file mode 100644
index 0000000000..ff71b6999c
--- /dev/null
+++ b/modules/luci-base/htdocs/luci-static/resources/promis.min.js
@@ -0,0 +1,5 @@
+/* Licensed under the BSD license. Copyright 2014 - Bram Stein. All rights reserved.
+ * https://github.com/bramstein/promis */
+(function(){'use strict';var f,g=[];function l(a){g.push(a);1==g.length&&f()}function m(){for(;g.length;)g[0](),g.shift()}f=function(){setTimeout(m)};function n(a){this.a=p;this.b=void 0;this.f=[];var b=this;try{a(function(a){q(b,a)},function(a){r(b,a)})}catch(c){r(b,c)}}var p=2;function t(a){return new n(function(b,c){c(a)})}function u(a){return new n(function(b){b(a)})}function q(a,b){if(a.a==p){if(b==a)throw new TypeError;var c=!1;try{var d=b&&b.then;if(null!=b&&"object"==typeof b&&"function"==typeof d){d.call(b,function(b){c||q(a,b);c=!0},function(b){c||r(a,b);c=!0});return}}catch(e){c||r(a,e);return}a.a=0;a.b=b;v(a)}}
+function r(a,b){if(a.a==p){if(b==a)throw new TypeError;a.a=1;a.b=b;v(a)}}function v(a){l(function(){if(a.a!=p)for(;a.f.length;){var b=a.f.shift(),c=b[0],d=b[1],e=b[2],b=b[3];try{0==a.a?"function"==typeof c?e(c.call(void 0,a.b)):e(a.b):1==a.a&&("function"==typeof d?e(d.call(void 0,a.b)):b(a.b))}catch(h){b(h)}}})}n.prototype.g=function(a){return this.c(void 0,a)};n.prototype.c=function(a,b){var c=this;return new n(function(d,e){c.f.push([a,b,d,e]);v(c)})};
+function w(a){return new n(function(b,c){function d(c){return function(d){h[c]=d;e+=1;e==a.length&&b(h)}}var e=0,h=[];0==a.length&&b(h);for(var k=0;k<a.length;k+=1)u(a[k]).c(d(k),c)})}function x(a){return new n(function(b,c){for(var d=0;d<a.length;d+=1)u(a[d]).c(b,c)})};window.Promise||(window.Promise=n,window.Promise.resolve=u,window.Promise.reject=t,window.Promise.race=x,window.Promise.all=w,window.Promise.prototype.then=n.prototype.c,window.Promise.prototype["catch"]=n.prototype.g,window.Promise.prototype.finally=function(a){return this.c(a,a)});}());
diff --git a/modules/luci-base/htdocs/luci-static/resources/rpc.js b/modules/luci-base/htdocs/luci-static/resources/rpc.js
new file mode 100644
index 0000000000..e12c2f77ee
--- /dev/null
+++ b/modules/luci-base/htdocs/luci-static/resources/rpc.js
@@ -0,0 +1,160 @@
+'use strict';
+
+var rpcRequestID = 1,
+ rpcSessionID = L.env.sessionid || '00000000000000000000000000000000',
+ rpcBaseURL = L.url('admin/ubus');
+
+return L.Class.extend({
+ call: function(req, cb) {
+ var q = '';
+
+ if (Array.isArray(req)) {
+ if (req.length == 0)
+ return Promise.resolve([]);
+
+ for (var i = 0; i < req.length; i++)
+ q += '%s%s.%s'.format(
+ q ? ';' : '/',
+ req[i].params[1],
+ req[i].params[2]
+ );
+ }
+ else {
+ q += '/%s.%s'.format(req.params[1], req.params[2]);
+ }
+
+ return L.Request.post(rpcBaseURL + q, req, {
+ timeout: (L.env.rpctimeout || 5) * 1000,
+ credentials: true
+ }).then(cb);
+ },
+
+ handleListReply: function(req, msg) {
+ var list = msg.result;
+
+ /* verify message frame */
+ if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id || !Array.isArray(list))
+ list = [ ];
+
+ req.resolve(list);
+ },
+
+ handleCallReply: function(req, res) {
+ var type = Object.prototype.toString,
+ msg = null;
+
+ if (!res.ok)
+ L.error('RPCError', 'RPC call failed with HTTP error %d: %s',
+ res.status, res.statusText || '?');
+
+ msg = res.json();
+
+ /* fetch response attribute and verify returned type */
+ var ret = undefined;
+
+ /* verify message frame */
+ if (typeof(msg) == 'object' && msg.jsonrpc == '2.0') {
+ if (typeof(msg.error) == 'object' && msg.error.code && msg.error.message)
+ req.reject(new Error('RPC call failed with error %d: %s'
+ .format(msg.error.code, msg.error.message || '?')));
+ else if (Array.isArray(msg.result) && msg.result[0] == 0)
+ ret = (msg.result.length > 1) ? msg.result[1] : msg.result[0];
+ }
+ else {
+ req.reject(new Error('Invalid message frame received'));
+ }
+
+ if (req.expect) {
+ for (var key in req.expect) {
+ if (ret != null && key != '')
+ ret = ret[key];
+
+ if (ret == null || type.call(ret) != type.call(req.expect[key]))
+ ret = req.expect[key];
+
+ break;
+ }
+ }
+
+ /* apply filter */
+ if (typeof(req.filter) == 'function') {
+ req.priv[0] = ret;
+ req.priv[1] = req.params;
+ ret = req.filter.apply(this, req.priv);
+ }
+
+ req.resolve(ret);
+ },
+
+ list: function() {
+ var msg = {
+ jsonrpc: '2.0',
+ id: rpcRequestID++,
+ method: 'list',
+ params: arguments.length ? this.varargs(arguments) : undefined
+ };
+
+ return this.call(msg, this.handleListReply);
+ },
+
+ declare: function(options) {
+ return Function.prototype.bind.call(function(rpc, options) {
+ var args = this.varargs(arguments, 2);
+ return new Promise(function(resolveFn, rejectFn) {
+ /* build parameter object */
+ var p_off = 0;
+ var params = { };
+ if (Array.isArray(options.params))
+ for (p_off = 0; p_off < options.params.length; p_off++)
+ params[options.params[p_off]] = args[p_off];
+
+ /* all remaining arguments are private args */
+ var priv = [ undefined, undefined ];
+ for (; p_off < args.length; p_off++)
+ priv.push(args[p_off]);
+
+ /* store request info */
+ var req = {
+ expect: options.expect,
+ filter: options.filter,
+ resolve: resolveFn,
+ reject: rejectFn,
+ params: params,
+ priv: priv
+ };
+
+ /* build message object */
+ var msg = {
+ jsonrpc: '2.0',
+ id: rpcRequestID++,
+ method: 'call',
+ params: [
+ rpcSessionID,
+ options.object,
+ options.method,
+ params
+ ]
+ };
+
+ /* call rpc */
+ rpc.call(msg, rpc.handleCallReply.bind(rpc, req));
+ });
+ }, this, this, options);
+ },
+
+ getSessionID: function() {
+ return rpcSessionID;
+ },
+
+ setSessionID: function(sid) {
+ rpcSessionID = sid;
+ },
+
+ getBaseURL: function() {
+ return rpcBaseURL;
+ },
+
+ setBaseURL: function(url) {
+ rpcBaseURL = url;
+ }
+});
diff --git a/modules/luci-base/htdocs/luci-static/resources/tools/prng.js b/modules/luci-base/htdocs/luci-static/resources/tools/prng.js
new file mode 100644
index 0000000000..752dc75ce8
--- /dev/null
+++ b/modules/luci-base/htdocs/luci-static/resources/tools/prng.js
@@ -0,0 +1,93 @@
+'use strict';
+
+var s = [0x0000, 0x0000, 0x0000, 0x0000];
+
+function mul(a, b) {
+ var r = [0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000];
+
+ for (var j = 0; j < 4; j++) {
+ var k = 0;
+ for (var i = 0; i < 4; i++) {
+ var t = a[i] * b[j] + r[i+j] + k;
+ r[i+j] = t & 0xffff;
+ k = t >>> 16;
+ }
+ r[j+4] = k;
+ }
+
+ r.length = 4;
+
+ return r;
+}
+
+function add(a, n) {
+ var r = [0x0000, 0x0000, 0x0000, 0x0000],
+ k = n;
+
+ for (var i = 0; i < 4; i++) {
+ var t = a[i] + k;
+ r[i] = t & 0xffff;
+ k = t >>> 16;
+ }
+
+ return r;
+}
+
+function shr(a, n) {
+ var r = [a[0], a[1], a[2], a[3], 0x0000],
+ i = 4,
+ k = 0;
+
+ for (; n > 16; n -= 16, i--)
+ for (var j = 0; j < 4; j++)
+ r[j] = r[j+1];
+
+ for (; i > 0; i--) {
+ var s = r[i-1];
+ r[i-1] = (s >>> n) | k;
+ k = ((s & ((1 << n) - 1)) << (16 - n));
+ }
+
+ r.length = 4;
+
+ return r;
+}
+
+return L.Class.extend({
+ seed: function(n) {
+ n = (n - 1)|0;
+ s[0] = n & 0xffff;
+ s[1] = n >>> 16;
+ s[2] = 0;
+ s[3] = 0;
+ },
+
+ int: function() {
+ s = mul(s, [0x7f2d, 0x4c95, 0xf42d, 0x5851]);
+ s = add(s, 1);
+
+ var r = shr(s, 33);
+ return (r[1] << 16) | r[0];
+ },
+
+ get: function() {
+ var r = (this.int() % 0x7fffffff) / 0x7fffffff, l, u;
+
+ switch (arguments.length) {
+ case 0:
+ return r;
+
+ case 1:
+ l = 1;
+ u = arguments[0]|0;
+ break;
+
+ case 2:
+ l = arguments[0]|0;
+ u = arguments[1]|0;
+ break;
+ }
+
+ return Math.floor(r * (u - l + 1)) + l;
+ }
+});
diff --git a/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js b/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js
new file mode 100644
index 0000000000..39e5aa1655
--- /dev/null
+++ b/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js
@@ -0,0 +1,535 @@
+'use strict';
+'require ui';
+'require form';
+'require network';
+'require firewall';
+
+var CBIZoneSelect = form.ListValue.extend({
+ __name__: 'CBI.ZoneSelect',
+
+ load: function(section_id) {
+ return Promise.all([ firewall.getZones(), network.getNetworks() ]).then(L.bind(function(zn) {
+ this.zones = zn[0];
+ this.networks = zn[1];
+
+ return this.super('load', section_id);
+ }, this));
+ },
+
+ filter: function(section_id, value) {
+ return true;
+ },
+
+ lookupZone: function(name) {
+ return this.zones.filter(function(zone) { return zone.getName() == name })[0];
+ },
+
+ lookupNetwork: function(name) {
+ return this.networks.filter(function(network) { return network.getName() == name })[0];
+ },
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
+ isOutputOnly = false,
+ choices = {};
+
+ if (this.option == 'dest') {
+ for (var i = 0; i < this.section.children.length; i++) {
+ var opt = this.section.children[i];
+ if (opt.option == 'src') {
+ var val = opt.cfgvalue(section_id) || opt.default;
+ isOutputOnly = (val == null || val == '');
+ break;
+ }
+ }
+
+ this.title = isOutputOnly ? _('Output zone') : _('Destination zone');
+ }
+
+ if (this.allowlocal) {
+ choices[''] = E('span', {
+ 'class': 'zonebadge',
+ 'style': 'background-color:' + firewall.getColorForName(null)
+ }, [
+ E('strong', _('Device')),
+ (this.allowany || this.allowlocal)
+ ? ' (%s)'.format(this.option != 'dest' ? _('output') : _('input')) : ''
+ ]);
+ }
+ else if (!this.multiple && (this.rmempty || this.optional)) {
+ choices[''] = E('span', {
+ 'class': 'zonebadge',
+ 'style': 'background-color:' + firewall.getColorForName(null)
+ }, E('em', _('unspecified')));
+ }
+
+ if (this.allowany) {
+ choices['*'] = E('span', {
+ 'class': 'zonebadge',
+ 'style': 'background-color:' + firewall.getColorForName(null)
+ }, [
+ E('strong', _('Any zone')),
+ (this.allowany && this.allowlocal && !isOutputOnly) ? ' (%s)'.format(_('forward')) : ''
+ ]);
+ }
+
+ for (var i = 0; i < this.zones.length; i++) {
+ var zone = this.zones[i],
+ name = zone.getName(),
+ networks = zone.getNetworks(),
+ ifaces = [];
+
+ if (!this.filter(section_id, name))
+ continue;
+
+ for (var j = 0; j < networks.length; j++) {
+ var network = this.lookupNetwork(networks[j]);
+
+ if (!network)
+ continue;
+
+ var span = E('span', {
+ 'class': 'ifacebadge' + (network.getName() == this.network ? ' ifacebadge-active' : '')
+ }, network.getName() + ': ');
+
+ var devices = network.isBridge() ? network.getDevices() : L.toArray(network.getDevice());
+
+ for (var k = 0; k < devices.length; k++) {
+ span.appendChild(E('img', {
+ 'title': devices[k].getI18n(),
+ 'src': L.resource('icons/%s%s.png'.format(devices[k].getType(), devices[k].isUp() ? '' : '_disabled'))
+ }));
+ }
+
+ if (!devices.length)
+ span.appendChild(E('em', _('(empty)')));
+
+ ifaces.push(span);
+ }
+
+ if (!ifaces.length)
+ ifaces.push(E('em', _('(empty)')));
+
+ choices[name] = E('span', {
+ 'class': 'zonebadge',
+ 'style': 'background-color:' + zone.getColor()
+ }, [ E('strong', name) ].concat(ifaces));
+ }
+
+ var widget = new ui.Dropdown(values, choices, {
+ id: this.cbid(section_id),
+ sort: true,
+ multiple: this.multiple,
+ optional: this.optional || this.rmempty,
+ select_placeholder: E('em', _('unspecified')),
+ display_items: this.display_size || this.size || 3,
+ dropdown_items: this.dropdown_size || this.size || 5,
+ validate: L.bind(this.validate, this, section_id),
+ create: !this.nocreate,
+ create_markup: '' +
+ '<li data-value="{{value}}">' +
+ '<span class="zonebadge" style="background:repeating-linear-gradient(45deg,rgba(204,204,204,0.5),rgba(204,204,204,0.5) 5px,rgba(255,255,255,0.5) 5px,rgba(255,255,255,0.5) 10px)">' +
+ '<strong>{{value}}:</strong> <em>('+_('create')+')</em>' +
+ '</span>' +
+ '</li>'
+ });
+
+ var elem = widget.render();
+
+ if (this.option == 'src') {
+ elem.addEventListener('cbi-dropdown-change', L.bind(function(ev) {
+ var opt = this.map.lookupOption('dest', section_id),
+ val = ev.detail.instance.getValue();
+
+ if (opt == null)
+ return;
+
+ var cbid = opt[0].cbid(section_id),
+ label = document.querySelector('label[for="widget.%s"]'.format(cbid)),
+ node = document.getElementById(cbid);
+
+ L.dom.content(label, val == '' ? _('Output zone') : _('Destination zone'));
+
+ if (val == '') {
+ if (L.dom.callClassMethod(node, 'getValue') == '')
+ L.dom.callClassMethod(node, 'setValue', '*');
+
+ var emptyval = node.querySelector('[data-value=""]'),
+ anyval = node.querySelector('[data-value="*"]');
+
+ L.dom.content(anyval.querySelector('span'), E('strong', _('Any zone')));
+
+ if (emptyval != null)
+ emptyval.parentNode.removeChild(emptyval);
+ }
+ else {
+ var anyval = node.querySelector('[data-value="*"]'),
+ emptyval = node.querySelector('[data-value=""]');
+
+ if (emptyval == null) {
+ emptyval = anyval.cloneNode(true);
+ emptyval.removeAttribute('display');
+ emptyval.removeAttribute('selected');
+ emptyval.setAttribute('data-value', '');
+ }
+
+ L.dom.content(emptyval.querySelector('span'), [
+ E('strong', _('Device')), ' (%s)'.format(_('input'))
+ ]);
+
+ L.dom.content(anyval.querySelector('span'), [
+ E('strong', _('Any zone')), ' (%s)'.format(_('forward'))
+ ]);
+
+ anyval.parentNode.insertBefore(emptyval, anyval);
+ }
+
+ }, this));
+ }
+ else if (isOutputOnly) {
+ var emptyval = elem.querySelector('[data-value=""]');
+ emptyval.parentNode.removeChild(emptyval);
+ }
+
+ return elem;
+ },
+});
+
+var CBIZoneForwards = form.DummyValue.extend({
+ __name__: 'CBI.ZoneForwards',
+
+ load: function(section_id) {
+ return Promise.all([
+ firewall.getDefaults(),
+ firewall.getZones(),
+ network.getNetworks(),
+ network.getDevices()
+ ]).then(L.bind(function(dznd) {
+ this.defaults = dznd[0];
+ this.zones = dznd[1];
+ this.networks = dznd[2];
+ this.devices = dznd[3];
+
+ return this.super('load', section_id);
+ }, this));
+ },
+
+ renderZone: function(zone) {
+ var name = zone.getName(),
+ networks = zone.getNetworks(),
+ devices = zone.getDevices(),
+ subnets = zone.getSubnets(),
+ ifaces = [];
+
+ for (var j = 0; j < networks.length; j++) {
+ var network = this.networks.filter(function(net) { return net.getName() == networks[j] })[0];
+
+ if (!network)
+ continue;
+
+ var span = E('span', {
+ 'class': 'ifacebadge' + (network.getName() == this.network ? ' ifacebadge-active' : '')
+ }, network.getName() + ': ');
+
+ var subdevs = network.isBridge() ? network.getDevices() : L.toArray(network.getDevice());
+
+ for (var k = 0; k < subdevs.length && subdevs[k]; k++) {
+ span.appendChild(E('img', {
+ 'title': subdevs[k].getI18n(),
+ 'src': L.resource('icons/%s%s.png'.format(subdevs[k].getType(), subdevs[k].isUp() ? '' : '_disabled'))
+ }));
+ }
+
+ if (!subdevs.length)
+ span.appendChild(E('em', _('(empty)')));
+
+ ifaces.push(span);
+ }
+
+ for (var i = 0; i < devices.length; i++) {
+ var device = this.devices.filter(function(dev) { return dev.getName() == devices[i] })[0],
+ title = device ? device.getI18n() : _('Absent Interface'),
+ type = device ? device.getType() : 'ethernet',
+ up = device ? device.isUp() : false;
+
+ ifaces.push(E('span', { 'class': 'ifacebadge' }, [
+ E('img', {
+ 'title': title,
+ 'src': L.resource('icons/%s%s.png'.format(type, up ? '' : '_disabled'))
+ }),
+ device ? device.getName() : devices[i]
+ ]));
+ }
+
+ if (subnets.length > 0)
+ ifaces.push(E('span', { 'class': 'ifacebadge' }, [ '{ %s }'.format(subnets.join('; ')) ]));
+
+ if (!ifaces.length)
+ ifaces.push(E('span', { 'class': 'ifacebadge' }, E('em', _('(empty)'))));
+
+ return E('label', {
+ 'class': 'zonebadge cbi-tooltip-container',
+ 'style': 'background-color:' + zone.getColor()
+ }, [
+ E('strong', name),
+ E('div', { 'class': 'cbi-tooltip' }, ifaces)
+ ]);
+ },
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var value = (cfgvalue != null) ? cfgvalue : this.default,
+ zone = this.zones.filter(function(z) { return z.getName() == value })[0];
+
+ if (!zone)
+ return E([]);
+
+ var forwards = zone.getForwardingsBy('src'),
+ dzones = [];
+
+ for (var i = 0; i < forwards.length; i++) {
+ var dzone = forwards[i].getDestinationZone();
+
+ if (!dzone)
+ continue;
+
+ dzones.push(this.renderZone(dzone));
+ }
+
+ if (!dzones.length)
+ dzones.push(E('label', { 'class': 'zonebadge zonebadge-empty' },
+ E('strong', this.defaults.getForward())));
+
+ return E('div', { 'class': 'zone-forwards' }, [
+ E('div', { 'class': 'zone-src' }, this.renderZone(zone)),
+ E('span', '⇒'),
+ E('div', { 'class': 'zone-dest' }, dzones)
+ ]);
+ },
+});
+
+var CBINetworkSelect = form.ListValue.extend({
+ __name__: 'CBI.NetworkSelect',
+
+ load: function(section_id) {
+ return network.getNetworks().then(L.bind(function(networks) {
+ this.networks = networks;
+
+ return this.super('load', section_id);
+ }, this));
+ },
+
+ filter: function(section_id, value) {
+ return true;
+ },
+
+ renderIfaceBadge: function(network) {
+ var span = E('span', { 'class': 'ifacebadge' }, network.getName() + ': '),
+ devices = network.isBridge() ? network.getDevices() : L.toArray(network.getDevice());
+
+ for (var j = 0; j < devices.length && devices[j]; j++) {
+ span.appendChild(E('img', {
+ 'title': devices[j].getI18n(),
+ 'src': L.resource('icons/%s%s.png'.format(devices[j].getType(), devices[j].isUp() ? '' : '_disabled'))
+ }));
+ }
+
+ if (!devices.length) {
+ span.appendChild(E('em', { 'class': 'hide-close' }, _('(no interfaces attached)')));
+ span.appendChild(E('em', { 'class': 'hide-open' }, '-'));
+ }
+
+ return span;
+ },
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
+ choices = {},
+ checked = {};
+
+ for (var i = 0; i < values.length; i++)
+ checked[values[i]] = true;
+
+ values = [];
+
+ if (!this.multiple && (this.rmempty || this.optional))
+ choices[''] = E('em', _('unspecified'));
+
+ for (var i = 0; i < this.networks.length; i++) {
+ var network = this.networks[i],
+ name = network.getName();
+
+ if (name == 'loopback' || !this.filter(section_id, name))
+ continue;
+
+ if (this.novirtual && network.isVirtual())
+ continue;
+
+ if (checked[name])
+ values.push(name);
+
+ choices[name] = this.renderIfaceBadge(network);
+ }
+
+ var widget = new ui.Dropdown(this.multiple ? values : values[0], choices, {
+ id: this.cbid(section_id),
+ sort: true,
+ multiple: this.multiple,
+ optional: this.optional || this.rmempty,
+ select_placeholder: E('em', _('unspecified')),
+ display_items: this.display_size || this.size || 3,
+ dropdown_items: this.dropdown_size || this.size || 5,
+ validate: L.bind(this.validate, this, section_id),
+ create: !this.nocreate,
+ create_markup: '' +
+ '<li data-value="{{value}}">' +
+ '<span class="ifacebadge" style="background:repeating-linear-gradient(45deg,rgba(204,204,204,0.5),rgba(204,204,204,0.5) 5px,rgba(255,255,255,0.5) 5px,rgba(255,255,255,0.5) 10px)">' +
+ '{{value}}: <em>('+_('create')+')</em>' +
+ '</span>' +
+ '</li>'
+ });
+
+ return widget.render();
+ },
+
+ textvalue: function(section_id) {
+ var cfgvalue = this.cfgvalue(section_id),
+ values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
+ rv = E([]);
+
+ for (var i = 0; i < (this.networks || []).length; i++) {
+ var network = this.networks[i],
+ name = network.getName();
+
+ if (values.indexOf(name) == -1)
+ continue;
+
+ if (rv.length)
+ L.dom.append(rv, ' ');
+
+ L.dom.append(rv, this.renderIfaceBadge(network));
+ }
+
+ if (!rv.firstChild)
+ rv.appendChild(E('em', _('unspecified')));
+
+ return rv;
+ },
+});
+
+var CBIDeviceSelect = form.ListValue.extend({
+ __name__: 'CBI.DeviceSelect',
+
+ load: function(section_id) {
+ return network.getDevices().then(L.bind(function(devices) {
+ this.devices = devices;
+
+ return this.super('load', section_id);
+ }, this));
+ },
+
+ filter: function(section_id, value) {
+ return true;
+ },
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
+ choices = {},
+ checked = {},
+ order = [];
+
+ for (var i = 0; i < values.length; i++)
+ checked[values[i]] = true;
+
+ values = [];
+
+ if (!this.multiple && (this.rmempty || this.optional))
+ choices[''] = E('em', _('unspecified'));
+
+ for (var i = 0; i < this.devices.length; i++) {
+ var device = this.devices[i],
+ name = device.getName(),
+ type = device.getType();
+
+ if (name == 'lo' || name == this.exclude || !this.filter(section_id, name))
+ continue;
+
+ if (this.noaliases && type == 'alias')
+ continue;
+
+ if (this.nobridges && type == 'bridge')
+ continue;
+
+ if (this.noinactive && device.isUp() == false)
+ continue;
+
+ var item = E([
+ E('img', {
+ 'title': device.getI18n(),
+ 'src': L.resource('icons/%s%s.png'.format(type, device.isUp() ? '' : '_disabled'))
+ }),
+ E('span', { 'class': 'hide-open' }, [ name ]),
+ E('span', { 'class': 'hide-close'}, [ device.getI18n() ])
+ ]);
+
+ var networks = device.getNetworks();
+
+ if (networks.length > 0)
+ L.dom.append(item.lastChild, [ ' (', networks.join(', '), ')' ]);
+
+ if (checked[name])
+ values.push(name);
+
+ choices[name] = item;
+ order.push(name);
+ }
+
+ if (!this.nocreate) {
+ var keys = Object.keys(checked).sort();
+
+ for (var i = 0; i < keys.length; i++) {
+ if (choices.hasOwnProperty(keys[i]))
+ continue;
+
+ choices[keys[i]] = E([
+ E('img', {
+ 'title': _('Absent Interface'),
+ 'src': L.resource('icons/ethernet_disabled.png')
+ }),
+ E('span', { 'class': 'hide-open' }, [ keys[i] ]),
+ E('span', { 'class': 'hide-close'}, [ '%s: "%h"'.format(_('Absent Interface'), keys[i]) ])
+ ]);
+
+ values.push(keys[i]);
+ order.push(keys[i]);
+ }
+ }
+
+ var widget = new ui.Dropdown(this.multiple ? values : values[0], choices, {
+ id: this.cbid(section_id),
+ sort: order,
+ multiple: this.multiple,
+ optional: this.optional || this.rmempty,
+ select_placeholder: E('em', _('unspecified')),
+ display_items: this.display_size || this.size || 3,
+ dropdown_items: this.dropdown_size || this.size || 5,
+ validate: L.bind(this.validate, this, section_id),
+ create: !this.nocreate,
+ create_markup: '' +
+ '<li data-value="{{value}}">' +
+ '<img title="'+_('Custom Interface')+': &quot;{{value}}&quot;" src="'+L.resource('icons/ethernet_disabled.png')+'" />' +
+ '<span class="hide-open">{{value}}</span>' +
+ '<span class="hide-close">'+_('Custom Interface')+': "{{value}}"</span>' +
+ '</li>'
+ });
+
+ return widget.render();
+ },
+});
+
+
+return L.Class.extend({
+ ZoneSelect: CBIZoneSelect,
+ ZoneForwards: CBIZoneForwards,
+ NetworkSelect: CBINetworkSelect,
+ DeviceSelect: CBIDeviceSelect,
+});
diff --git a/modules/luci-base/htdocs/luci-static/resources/uci.js b/modules/luci-base/htdocs/luci-static/resources/uci.js
new file mode 100644
index 0000000000..17f11eecb8
--- /dev/null
+++ b/modules/luci-base/htdocs/luci-static/resources/uci.js
@@ -0,0 +1,540 @@
+'use strict';
+'require rpc';
+
+return L.Class.extend({
+ __init__: function() {
+ this.state = {
+ newidx: 0,
+ values: { },
+ creates: { },
+ changes: { },
+ deletes: { },
+ reorder: { }
+ };
+
+ this.loaded = {};
+ },
+
+ callLoad: rpc.declare({
+ object: 'uci',
+ method: 'get',
+ params: [ 'config' ],
+ expect: { values: { } }
+ }),
+
+ callOrder: rpc.declare({
+ object: 'uci',
+ method: 'order',
+ params: [ 'config', 'sections' ]
+ }),
+
+ callAdd: rpc.declare({
+ object: 'uci',
+ method: 'add',
+ params: [ 'config', 'type', 'name', 'values' ],
+ expect: { section: '' }
+ }),
+
+ callSet: rpc.declare({
+ object: 'uci',
+ method: 'set',
+ params: [ 'config', 'section', 'values' ]
+ }),
+
+ callDelete: rpc.declare({
+ object: 'uci',
+ method: 'delete',
+ params: [ 'config', 'section', 'options' ]
+ }),
+
+ callApply: rpc.declare({
+ object: 'uci',
+ method: 'apply',
+ params: [ 'timeout', 'rollback' ]
+ }),
+
+ callConfirm: rpc.declare({
+ object: 'uci',
+ method: 'confirm'
+ }),
+
+ createSID: function(conf) {
+ var v = this.state.values,
+ n = this.state.creates,
+ sid;
+
+ do {
+ sid = "new%06x".format(Math.random() * 0xFFFFFF);
+ } while ((n[conf] && n[conf][sid]) || (v[conf] && v[conf][sid]));
+
+ return sid;
+ },
+
+ resolveSID: function(conf, sid) {
+ if (typeof(sid) != 'string')
+ return sid;
+
+ var m = /^@([a-zA-Z0-9_-]+)\[(-?[0-9]+)\]$/.exec(sid);
+
+ if (m) {
+ var type = m[1],
+ pos = +m[2],
+ sections = this.sections(conf, type),
+ section = sections[pos >= 0 ? pos : sections.length + pos];
+
+ return section ? section['.name'] : null;
+ }
+
+ return sid;
+ },
+
+ reorderSections: function() {
+ var v = this.state.values,
+ n = this.state.creates,
+ r = this.state.reorder,
+ tasks = [];
+
+ if (Object.keys(r).length === 0)
+ return Promise.resolve();
+
+ /*
+ gather all created and existing sections, sort them according
+ to their index value and issue an uci order call
+ */
+ for (var c in r) {
+ var o = [ ];
+
+ if (n[c])
+ for (var s in n[c])
+ o.push(n[c][s]);
+
+ for (var s in v[c])
+ o.push(v[c][s]);
+
+ if (o.length > 0) {
+ o.sort(function(a, b) {
+ return (a['.index'] - b['.index']);
+ });
+
+ var sids = [ ];
+
+ for (var i = 0; i < o.length; i++)
+ sids.push(o[i]['.name']);
+
+ tasks.push(this.callOrder(c, sids));
+ }
+ }
+
+ this.state.reorder = { };
+ return Promise.all(tasks);
+ },
+
+ loadPackage: function(packageName) {
+ if (this.loaded[packageName] == null)
+ return (this.loaded[packageName] = this.callLoad(packageName));
+
+ return Promise.resolve(this.loaded[packageName]);
+ },
+
+ load: function(packages) {
+ var self = this,
+ pkgs = [ ],
+ tasks = [];
+
+ if (!Array.isArray(packages))
+ packages = [ packages ];
+
+ for (var i = 0; i < packages.length; i++)
+ if (!self.state.values[packages[i]]) {
+ pkgs.push(packages[i]);
+ tasks.push(self.loadPackage(packages[i]));
+ }
+
+ return Promise.all(tasks).then(function(responses) {
+ for (var i = 0; i < responses.length; i++)
+ self.state.values[pkgs[i]] = responses[i];
+
+ if (responses.length)
+ document.dispatchEvent(new CustomEvent('uci-loaded'));
+
+ return pkgs;
+ });
+ },
+
+ unload: function(packages) {
+ if (!Array.isArray(packages))
+ packages = [ packages ];
+
+ for (var i = 0; i < packages.length; i++) {
+ delete this.state.values[packages[i]];
+ delete this.state.creates[packages[i]];
+ delete this.state.changes[packages[i]];
+ delete this.state.deletes[packages[i]];
+
+ delete this.loaded[packages[i]];
+ }
+ },
+
+ add: function(conf, type, name) {
+ var n = this.state.creates,
+ sid = name || this.createSID(conf);
+
+ if (!n[conf])
+ n[conf] = { };
+
+ n[conf][sid] = {
+ '.type': type,
+ '.name': sid,
+ '.create': name,
+ '.anonymous': !name,
+ '.index': 1000 + this.state.newidx++
+ };
+
+ return sid;
+ },
+
+ remove: function(conf, sid) {
+ var n = this.state.creates,
+ c = this.state.changes,
+ d = this.state.deletes;
+
+ /* requested deletion of a just created section */
+ if (n[conf] && n[conf][sid]) {
+ delete n[conf][sid];
+ }
+ else {
+ if (c[conf])
+ delete c[conf][sid];
+
+ if (!d[conf])
+ d[conf] = { };
+
+ d[conf][sid] = true;
+ }
+ },
+
+ sections: function(conf, type, cb) {
+ var sa = [ ],
+ v = this.state.values[conf],
+ n = this.state.creates[conf],
+ c = this.state.changes[conf],
+ d = this.state.deletes[conf];
+
+ if (!v)
+ return sa;
+
+ 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));
+
+ if (n)
+ for (var s in n)
+ if (!type || n[s]['.type'] == type)
+ sa.push(Object.assign({ }, n[s]));
+
+ sa.sort(function(a, b) {
+ return a['.index'] - b['.index'];
+ });
+
+ for (var i = 0; i < sa.length; i++)
+ sa[i]['.index'] = i;
+
+ if (typeof(cb) == 'function')
+ for (var i = 0; i < sa.length; i++)
+ cb.call(this, sa[i], sa[i]['.name']);
+
+ return sa;
+ },
+
+ get: function(conf, sid, opt) {
+ var v = this.state.values,
+ n = this.state.creates,
+ c = this.state.changes,
+ d = this.state.deletes;
+
+ sid = this.resolveSID(conf, sid);
+
+ if (sid == null)
+ return null;
+
+ /* requested option in a just created section */
+ if (n[conf] && n[conf][sid]) {
+ if (!n[conf])
+ return undefined;
+
+ if (opt == null)
+ return n[conf][sid];
+
+ return n[conf][sid][opt];
+ }
+
+ /* 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;
+ }
+
+ /* check whether option was changed */
+ if (c[conf] && c[conf][sid] && c[conf][sid][opt] != null)
+ return c[conf][sid][opt];
+
+ /* return base value */
+ if (v[conf] && v[conf][sid])
+ return v[conf][sid][opt];
+
+ return undefined;
+ }
+
+ /* requested an entire section */
+ if (v[conf])
+ return v[conf][sid];
+
+ return undefined;
+ },
+
+ set: function(conf, sid, opt, val) {
+ var v = this.state.values,
+ n = this.state.creates,
+ c = this.state.changes,
+ d = this.state.deletes;
+
+ sid = this.resolveSID(conf, sid);
+
+ if (sid == null || opt == null || opt.charAt(0) == '.')
+ return;
+
+ if (n[conf] && n[conf][sid]) {
+ if (val != null)
+ n[conf][sid][opt] = val;
+ else
+ delete n[conf][sid][opt];
+ }
+ else if (val != null && val !== '') {
+ /* do not set within deleted section */
+ if (d[conf] && d[conf][sid] === true)
+ return;
+
+ /* only set in existing sections */
+ if (!v[conf] || !v[conf][sid])
+ return;
+
+ if (!c[conf])
+ c[conf] = {};
+
+ if (!c[conf][sid])
+ c[conf][sid] = {};
+
+ /* undelete option */
+ if (d[conf] && d[conf][sid])
+ d[conf][sid] = d[conf][sid].filter(function(o) { return o !== 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;
+
+ if (!d[conf])
+ d[conf] = { };
+
+ if (!d[conf][sid])
+ d[conf][sid] = [ ];
+
+ if (d[conf][sid] !== true)
+ d[conf][sid].push(opt);
+ }
+ },
+
+ unset: function(conf, sid, opt) {
+ return this.set(conf, sid, opt, null);
+ },
+
+ get_first: function(conf, type, opt) {
+ var sid = null;
+
+ this.sections(conf, type, function(s) {
+ if (sid == null)
+ sid = s['.name'];
+ });
+
+ return this.get(conf, sid, opt);
+ },
+
+ set_first: function(conf, type, opt, val) {
+ var sid = null;
+
+ this.sections(conf, type, function(s) {
+ if (sid == null)
+ sid = s['.name'];
+ });
+
+ return this.set(conf, sid, opt, val);
+ },
+
+ unset_first: function(conf, type, opt) {
+ return this.set_first(conf, type, opt, null);
+ },
+
+ move: function(conf, sid1, sid2, after) {
+ var sa = this.sections(conf),
+ s1 = null, s2 = null;
+
+ sid1 = this.resolveSID(conf, sid1);
+ sid2 = this.resolveSID(conf, sid2);
+
+ for (var i = 0; i < sa.length; i++) {
+ if (sa[i]['.name'] != sid1)
+ continue;
+
+ s1 = sa[i];
+ sa.splice(i, 1);
+ break;
+ }
+
+ if (s1 == null)
+ return false;
+
+ if (sid2 == null) {
+ sa.push(s1);
+ }
+ else {
+ for (var i = 0; i < sa.length; i++) {
+ if (sa[i]['.name'] != sid2)
+ continue;
+
+ s2 = sa[i];
+ sa.splice(i + !!after, 0, s1);
+ break;
+ }
+
+ if (s2 == null)
+ return false;
+ }
+
+ for (var i = 0; i < sa.length; i++)
+ this.get(conf, sa[i]['.name'])['.index'] = i;
+
+ this.state.reorder[conf] = true;
+
+ return true;
+ },
+
+ save: function() {
+ var v = this.state.values,
+ n = this.state.creates,
+ c = this.state.changes,
+ d = this.state.deletes,
+ r = this.state.reorder,
+ self = this,
+ snew = [ ],
+ pkgs = { },
+ tasks = [];
+
+ if (n)
+ for (var conf in n) {
+ for (var sid in n[conf]) {
+ var r = {
+ config: conf,
+ values: { }
+ };
+
+ for (var k in n[conf][sid]) {
+ if (k == '.type')
+ r.type = n[conf][sid][k];
+ else if (k == '.create')
+ r.name = n[conf][sid][k];
+ else if (k.charAt(0) != '.')
+ r.values[k] = n[conf][sid][k];
+ }
+
+ snew.push(n[conf][sid]);
+ tasks.push(self.callAdd(r.config, r.type, r.name, r.values));
+ }
+
+ pkgs[conf] = true;
+ }
+
+ if (c)
+ for (var conf in c) {
+ for (var sid in c[conf])
+ tasks.push(self.callSet(conf, sid, c[conf][sid]));
+
+ pkgs[conf] = true;
+ }
+
+ if (d)
+ 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));
+ }
+
+ pkgs[conf] = true;
+ }
+
+ if (r)
+ for (var conf in r)
+ pkgs[conf] = true;
+
+ return Promise.all(tasks).then(function(responses) {
+ /*
+ array "snew" holds references to the created uci sections,
+ use it to assign the returned names of the new sections
+ */
+ for (var i = 0; i < snew.length; i++)
+ snew[i]['.name'] = responses[i];
+
+ return self.reorderSections();
+ }).then(function() {
+ pkgs = Object.keys(pkgs);
+
+ self.unload(pkgs);
+
+ return self.load(pkgs);
+ });
+ },
+
+ apply: function(timeout) {
+ var self = this,
+ date = new Date();
+
+ if (typeof(timeout) != 'number' || timeout < 1)
+ timeout = 10;
+
+ return self.callApply(timeout, true).then(function(rv) {
+ if (rv != 0)
+ return Promise.reject(rv);
+
+ var try_deadline = date.getTime() + 1000 * timeout;
+ var try_confirm = function() {
+ return self.callConfirm().then(function(rv) {
+ if (rv != 0) {
+ if (date.getTime() < try_deadline)
+ window.setTimeout(try_confirm, 250);
+ else
+ return Promise.reject(rv);
+ }
+
+ return rv;
+ });
+ };
+
+ window.setTimeout(try_confirm, 1000);
+ });
+ },
+
+ changes: rpc.declare({
+ object: 'uci',
+ method: 'changes',
+ expect: { changes: { } }
+ })
+});
diff --git a/modules/luci-base/htdocs/luci-static/resources/ui.js b/modules/luci-base/htdocs/luci-static/resources/ui.js
new file mode 100644
index 0000000000..29233dec02
--- /dev/null
+++ b/modules/luci-base/htdocs/luci-static/resources/ui.js
@@ -0,0 +1,2069 @@
+'use strict';
+'require uci';
+'require validation';
+
+var modalDiv = null,
+ tooltipDiv = null,
+ tooltipTimeout = null;
+
+var UIElement = L.Class.extend({
+ getValue: function() {
+ if (L.dom.matches(this.node, 'select') || L.dom.matches(this.node, 'input'))
+ return this.node.value;
+
+ return null;
+ },
+
+ setValue: function(value) {
+ if (L.dom.matches(this.node, 'select') || L.dom.matches(this.node, 'input'))
+ this.node.value = value;
+ },
+
+ isValid: function() {
+ return (this.validState !== false);
+ },
+
+ triggerValidation: function() {
+ if (typeof(this.vfunc) != 'function')
+ return false;
+
+ var wasValid = this.isValid();
+
+ this.vfunc();
+
+ return (wasValid != this.isValid());
+ },
+
+ registerEvents: function(targetNode, synevent, events) {
+ var dispatchFn = L.bind(function(ev) {
+ this.node.dispatchEvent(new CustomEvent(synevent, { bubbles: true }));
+ }, this);
+
+ for (var i = 0; i < events.length; i++)
+ targetNode.addEventListener(events[i], dispatchFn);
+ },
+
+ setUpdateEvents: function(targetNode /*, ... */) {
+ var datatype = this.options.datatype,
+ optional = this.options.hasOwnProperty('optional') ? this.options.optional : true,
+ validate = this.options.validate,
+ events = this.varargs(arguments, 1);
+
+ this.registerEvents(targetNode, 'widget-update', events);
+
+ if (!datatype && !validate)
+ return;
+
+ this.vfunc = L.ui.addValidator.apply(L.ui, [
+ targetNode, datatype || 'string',
+ optional, validate
+ ].concat(events));
+
+ this.node.addEventListener('validation-success', L.bind(function(ev) {
+ this.validState = true;
+ }, this));
+
+ this.node.addEventListener('validation-failure', L.bind(function(ev) {
+ this.validState = false;
+ }, this));
+ },
+
+ setChangeEvents: function(targetNode /*, ... */) {
+ this.registerEvents(targetNode, 'widget-change', this.varargs(arguments, 1));
+ }
+});
+
+var UITextfield = UIElement.extend({
+ __init__: function(value, options) {
+ this.value = value;
+ this.options = Object.assign({
+ optional: true,
+ password: false
+ }, options);
+ },
+
+ render: function() {
+ var frameEl = E('div', { 'id': this.options.id });
+
+ if (this.options.password) {
+ frameEl.classList.add('nowrap');
+ frameEl.appendChild(E('input', {
+ 'type': 'password',
+ 'style': 'position:absolute; left:-100000px',
+ 'aria-hidden': true,
+ 'tabindex': -1,
+ 'name': this.options.name ? 'password.%s'.format(this.options.name) : null
+ }));
+ }
+
+ frameEl.appendChild(E('input', {
+ 'id': this.options.id ? 'widget.' + this.options.id : null,
+ 'name': this.options.name,
+ 'type': this.options.password ? 'password' : 'text',
+ 'class': this.options.password ? 'cbi-input-password' : 'cbi-input-text',
+ 'readonly': this.options.readonly ? '' : null,
+ 'maxlength': this.options.maxlength,
+ 'placeholder': this.options.placeholder,
+ 'value': this.value,
+ }));
+
+ if (this.options.password)
+ frameEl.appendChild(E('button', {
+ 'class': 'cbi-button cbi-button-neutral',
+ 'title': _('Reveal/hide password'),
+ 'aria-label': _('Reveal/hide password'),
+ 'click': function(ev) {
+ var e = this.previousElementSibling;
+ e.type = (e.type === 'password') ? 'text' : 'password';
+ ev.preventDefault();
+ }
+ }, '∗'));
+
+ return this.bind(frameEl);
+ },
+
+ bind: function(frameEl) {
+ var inputEl = frameEl.childNodes[+!!this.options.password];
+
+ this.node = frameEl;
+
+ this.setUpdateEvents(inputEl, 'keyup', 'blur');
+ this.setChangeEvents(inputEl, 'change');
+
+ L.dom.bindClassInstance(frameEl, this);
+
+ return frameEl;
+ },
+
+ getValue: function() {
+ var inputEl = this.node.childNodes[+!!this.options.password];
+ return inputEl.value;
+ },
+
+ setValue: function(value) {
+ var inputEl = this.node.childNodes[+!!this.options.password];
+ inputEl.value = value;
+ }
+});
+
+var UICheckbox = UIElement.extend({
+ __init__: function(value, options) {
+ this.value = value;
+ this.options = Object.assign({
+ value_enabled: '1',
+ value_disabled: '0'
+ }, options);
+ },
+
+ render: function() {
+ var frameEl = E('div', {
+ 'id': this.options.id,
+ 'class': 'cbi-checkbox'
+ });
+
+ if (this.options.hiddenname)
+ frameEl.appendChild(E('input', {
+ 'type': 'hidden',
+ 'name': this.options.hiddenname,
+ 'value': 1
+ }));
+
+ frameEl.appendChild(E('input', {
+ 'id': this.options.id ? 'widget.' + this.options.id : null,
+ 'name': this.options.name,
+ 'type': 'checkbox',
+ 'value': this.options.value_enabled,
+ 'checked': (this.value == this.options.value_enabled) ? '' : null
+ }));
+
+ return this.bind(frameEl);
+ },
+
+ bind: function(frameEl) {
+ this.node = frameEl;
+
+ this.setUpdateEvents(frameEl.lastElementChild, 'click', 'blur');
+ this.setChangeEvents(frameEl.lastElementChild, 'change');
+
+ L.dom.bindClassInstance(frameEl, this);
+
+ return frameEl;
+ },
+
+ isChecked: function() {
+ return this.node.lastElementChild.checked;
+ },
+
+ getValue: function() {
+ return this.isChecked()
+ ? this.options.value_enabled
+ : this.options.value_disabled;
+ },
+
+ setValue: function(value) {
+ this.node.lastElementChild.checked = (value == this.options.value_enabled);
+ }
+});
+
+var UISelect = UIElement.extend({
+ __init__: function(value, choices, options) {
+ if (typeof(choices) != 'object')
+ choices = {};
+
+ if (!Array.isArray(value))
+ value = (value != null && value != '') ? [ value ] : [];
+
+ if (!options.multiple && value.length > 1)
+ value.length = 1;
+
+ this.values = value;
+ this.choices = choices;
+ this.options = Object.assign({
+ multiple: false,
+ widget: 'select',
+ orientation: 'horizontal'
+ }, options);
+
+ if (this.choices.hasOwnProperty(''))
+ this.options.optional = true;
+ },
+
+ render: function() {
+ var frameEl = E('div', { 'id': this.options.id }),
+ keys = Object.keys(this.choices);
+
+ if (this.options.sort === true)
+ keys.sort();
+ else if (Array.isArray(this.options.sort))
+ keys = this.options.sort;
+
+ if (this.options.widget == 'select') {
+ frameEl.appendChild(E('select', {
+ 'id': this.options.id ? 'widget.' + this.options.id : null,
+ 'name': this.options.name,
+ 'size': this.options.size,
+ 'class': 'cbi-input-select',
+ 'multiple': this.options.multiple ? '' : null
+ }));
+
+ if (this.options.optional)
+ frameEl.lastChild.appendChild(E('option', {
+ 'value': '',
+ 'selected': (this.values.length == 0 || this.values[0] == '') ? '' : null
+ }, this.choices[''] || this.options.placeholder || _('-- Please choose --')));
+
+ for (var i = 0; i < keys.length; i++) {
+ if (keys[i] == null || keys[i] == '')
+ continue;
+
+ frameEl.lastChild.appendChild(E('option', {
+ 'value': keys[i],
+ 'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
+ }, this.choices[keys[i]] || keys[i]));
+ }
+ }
+ else {
+ var brEl = (this.options.orientation === 'horizontal') ? document.createTextNode(' ') : E('br');
+
+ for (var i = 0; i < keys.length; i++) {
+ frameEl.appendChild(E('label', {}, [
+ E('input', {
+ 'id': this.options.id ? 'widget.' + this.options.id : null,
+ 'name': this.options.id || this.options.name,
+ 'type': this.options.multiple ? 'checkbox' : 'radio',
+ 'class': this.options.multiple ? 'cbi-input-checkbox' : 'cbi-input-radio',
+ 'value': keys[i],
+ 'checked': (this.values.indexOf(keys[i]) > -1) ? '' : null
+ }),
+ this.choices[keys[i]] || keys[i]
+ ]));
+
+ if (i + 1 == this.options.size)
+ frameEl.appendChild(brEl);
+ }
+ }
+
+ return this.bind(frameEl);
+ },
+
+ bind: function(frameEl) {
+ this.node = frameEl;
+
+ if (this.options.widget == 'select') {
+ this.setUpdateEvents(frameEl.firstChild, 'change', 'click', 'blur');
+ this.setChangeEvents(frameEl.firstChild, 'change');
+ }
+ else {
+ var radioEls = frameEl.querySelectorAll('input[type="radio"]');
+ for (var i = 0; i < radioEls.length; i++) {
+ this.setUpdateEvents(radioEls[i], 'change', 'click', 'blur');
+ this.setChangeEvents(radioEls[i], 'change', 'click', 'blur');
+ }
+ }
+
+ L.dom.bindClassInstance(frameEl, this);
+
+ return frameEl;
+ },
+
+ getValue: function() {
+ if (this.options.widget == 'select')
+ return this.node.firstChild.value;
+
+ var radioEls = frameEl.querySelectorAll('input[type="radio"]');
+ for (var i = 0; i < radioEls.length; i++)
+ if (radioEls[i].checked)
+ return radioEls[i].value;
+
+ return null;
+ },
+
+ setValue: function(value) {
+ if (this.options.widget == 'select') {
+ if (value == null)
+ value = '';
+
+ for (var i = 0; i < this.node.firstChild.options.length; i++)
+ this.node.firstChild.options[i].selected = (this.node.firstChild.options[i].value == value);
+
+ return;
+ }
+
+ var radioEls = frameEl.querySelectorAll('input[type="radio"]');
+ for (var i = 0; i < radioEls.length; i++)
+ radioEls[i].checked = (radioEls[i].value == value);
+ }
+});
+
+var UIDropdown = UIElement.extend({
+ __init__: function(value, choices, options) {
+ if (typeof(choices) != 'object')
+ choices = {};
+
+ if (!Array.isArray(value))
+ this.values = (value != null && value != '') ? [ value ] : [];
+ else
+ this.values = value;
+
+ this.choices = choices;
+ this.options = Object.assign({
+ sort: true,
+ multiple: Array.isArray(value),
+ optional: true,
+ select_placeholder: _('-- Please choose --'),
+ custom_placeholder: _('-- custom --'),
+ display_items: 3,
+ dropdown_items: -1,
+ create: false,
+ create_query: '.create-item-input',
+ create_template: 'script[type="item-template"]'
+ }, options);
+ },
+
+ render: function() {
+ var sb = E('div', {
+ 'id': this.options.id,
+ 'class': 'cbi-dropdown',
+ 'multiple': this.options.multiple ? '' : null,
+ 'optional': this.options.optional ? '' : null,
+ }, E('ul'));
+
+ var keys = Object.keys(this.choices);
+
+ if (this.options.sort === true)
+ keys.sort();
+ else if (Array.isArray(this.options.sort))
+ keys = this.options.sort;
+
+ if (this.options.create)
+ for (var i = 0; i < this.values.length; i++)
+ if (!this.choices.hasOwnProperty(this.values[i]))
+ keys.push(this.values[i]);
+
+ for (var i = 0; i < keys.length; i++)
+ sb.lastElementChild.appendChild(E('li', {
+ 'data-value': keys[i],
+ 'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
+ }, this.choices[keys[i]] || keys[i]));
+
+ if (this.options.create) {
+ var createEl = E('input', {
+ 'type': 'text',
+ 'class': 'create-item-input',
+ 'readonly': this.options.readonly ? '' : null,
+ 'maxlength': this.options.maxlength,
+ 'placeholder': this.options.custom_placeholder || this.options.placeholder
+ });
+
+ if (this.options.datatype)
+ L.ui.addValidator(createEl, this.options.datatype,
+ true, null, 'blur', 'keyup');
+
+ sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, createEl));
+ }
+
+ if (this.options.create_markup)
+ sb.appendChild(E('script', { type: 'item-template' },
+ this.options.create_markup));
+
+ return this.bind(sb);
+ },
+
+ bind: function(sb) {
+ var o = this.options;
+
+ o.multiple = sb.hasAttribute('multiple');
+ o.optional = sb.hasAttribute('optional');
+ o.placeholder = sb.getAttribute('placeholder') || o.placeholder;
+ o.display_items = parseInt(sb.getAttribute('display-items') || o.display_items);
+ o.dropdown_items = parseInt(sb.getAttribute('dropdown-items') || o.dropdown_items);
+ o.create_query = sb.getAttribute('item-create') || o.create_query;
+ o.create_template = sb.getAttribute('item-template') || o.create_template;
+
+ var ul = sb.querySelector('ul'),
+ more = sb.appendChild(E('span', { class: 'more', tabindex: -1 }, '···')),
+ open = sb.appendChild(E('span', { class: 'open', tabindex: -1 }, '▾')),
+ canary = sb.appendChild(E('div')),
+ create = sb.querySelector(this.options.create_query),
+ ndisplay = this.options.display_items,
+ n = 0;
+
+ if (this.options.multiple) {
+ var items = ul.querySelectorAll('li');
+
+ for (var i = 0; i < items.length; i++) {
+ this.transformItem(sb, items[i]);
+
+ if (items[i].hasAttribute('selected') && ndisplay-- > 0)
+ items[i].setAttribute('display', n++);
+ }
+ }
+ else {
+ if (this.options.optional && !ul.querySelector('li[data-value=""]')) {
+ var placeholder = E('li', { placeholder: '' },
+ this.options.select_placeholder || this.options.placeholder);
+
+ ul.firstChild
+ ? ul.insertBefore(placeholder, ul.firstChild)
+ : ul.appendChild(placeholder);
+ }
+
+ var items = ul.querySelectorAll('li'),
+ sel = sb.querySelectorAll('[selected]');
+
+ sel.forEach(function(s) {
+ s.removeAttribute('selected');
+ });
+
+ var s = sel[0] || items[0];
+ if (s) {
+ s.setAttribute('selected', '');
+ s.setAttribute('display', n++);
+ }
+
+ ndisplay--;
+ }
+
+ this.saveValues(sb, ul);
+
+ ul.setAttribute('tabindex', -1);
+ sb.setAttribute('tabindex', 0);
+
+ if (ndisplay < 0)
+ sb.setAttribute('more', '')
+ else
+ sb.removeAttribute('more');
+
+ if (ndisplay == this.options.display_items)
+ sb.setAttribute('empty', '')
+ else
+ sb.removeAttribute('empty');
+
+ L.dom.content(more, (ndisplay == this.options.display_items)
+ ? (this.options.select_placeholder || this.options.placeholder) : '···');
+
+
+ sb.addEventListener('click', this.handleClick.bind(this));
+ sb.addEventListener('keydown', this.handleKeydown.bind(this));
+ sb.addEventListener('cbi-dropdown-close', this.handleDropdownClose.bind(this));
+ sb.addEventListener('cbi-dropdown-select', this.handleDropdownSelect.bind(this));
+
+ if ('ontouchstart' in window) {
+ sb.addEventListener('touchstart', function(ev) { ev.stopPropagation(); });
+ window.addEventListener('touchstart', this.closeAllDropdowns);
+ }
+ else {
+ sb.addEventListener('mouseover', this.handleMouseover.bind(this));
+ sb.addEventListener('focus', this.handleFocus.bind(this));
+
+ canary.addEventListener('focus', this.handleCanaryFocus.bind(this));
+
+ window.addEventListener('mouseover', this.setFocus);
+ window.addEventListener('click', this.closeAllDropdowns);
+ }
+
+ if (create) {
+ create.addEventListener('keydown', this.handleCreateKeydown.bind(this));
+ create.addEventListener('focus', this.handleCreateFocus.bind(this));
+ create.addEventListener('blur', this.handleCreateBlur.bind(this));
+
+ var li = findParent(create, 'li');
+
+ li.setAttribute('unselectable', '');
+ li.addEventListener('click', this.handleCreateClick.bind(this));
+ }
+
+ this.node = sb;
+
+ this.setUpdateEvents(sb, 'cbi-dropdown-open', 'cbi-dropdown-close');
+ this.setChangeEvents(sb, 'cbi-dropdown-change', 'cbi-dropdown-close');
+
+ L.dom.bindClassInstance(sb, this);
+
+ return sb;
+ },
+
+ openDropdown: function(sb) {
+ var st = window.getComputedStyle(sb, null),
+ ul = sb.querySelector('ul'),
+ li = ul.querySelectorAll('li'),
+ fl = findParent(sb, '.cbi-value-field'),
+ sel = ul.querySelector('[selected]'),
+ rect = sb.getBoundingClientRect(),
+ items = Math.min(this.options.dropdown_items, li.length);
+
+ document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
+ s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
+ });
+
+ sb.setAttribute('open', '');
+
+ var pv = ul.cloneNode(true);
+ pv.classList.add('preview');
+
+ if (fl)
+ fl.classList.add('cbi-dropdown-open');
+
+ if ('ontouchstart' in window) {
+ var vpWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
+ vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0),
+ scrollFrom = window.pageYOffset,
+ scrollTo = scrollFrom + rect.top - vpHeight * 0.5,
+ start = null;
+
+ ul.style.top = sb.offsetHeight + 'px';
+ ul.style.left = -rect.left + 'px';
+ ul.style.right = (rect.right - vpWidth) + 'px';
+ ul.style.maxHeight = (vpHeight * 0.5) + 'px';
+ ul.style.WebkitOverflowScrolling = 'touch';
+
+ var scrollStep = function(timestamp) {
+ if (!start) {
+ start = timestamp;
+ ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
+ }
+
+ var duration = Math.max(timestamp - start, 1);
+ if (duration < 100) {
+ document.body.scrollTop = scrollFrom + (scrollTo - scrollFrom) * (duration / 100);
+ window.requestAnimationFrame(scrollStep);
+ }
+ else {
+ document.body.scrollTop = scrollTo;
+ }
+ };
+
+ window.requestAnimationFrame(scrollStep);
+ }
+ else {
+ ul.style.maxHeight = '1px';
+ ul.style.top = ul.style.bottom = '';
+
+ window.requestAnimationFrame(function() {
+ var itemHeight = li[Math.max(0, li.length - 2)].getBoundingClientRect().height,
+ fullHeight = 0,
+ spaceAbove = rect.top,
+ spaceBelow = window.innerHeight - rect.height - rect.top;
+
+ for (var 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';
+ }
+ else if (fullHeight <= spaceAbove) {
+ ul.style.bottom = rect.height + 'px';
+ ul.style.maxHeight = spaceAbove + 'px';
+ }
+ else if (spaceBelow >= spaceAbove) {
+ ul.style.top = rect.height + 'px';
+ ul.style.maxHeight = (spaceBelow - (spaceBelow % itemHeight)) + 'px';
+ }
+ else {
+ ul.style.bottom = rect.height + 'px';
+ ul.style.maxHeight = (spaceAbove - (spaceAbove % itemHeight)) + 'px';
+ }
+
+ ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
+ });
+ }
+
+ var cboxes = ul.querySelectorAll('[selected] input[type="checkbox"]');
+ for (var i = 0; i < cboxes.length; i++) {
+ cboxes[i].checked = true;
+ cboxes[i].disabled = (cboxes.length == 1 && !this.options.optional);
+ };
+
+ ul.classList.add('dropdown');
+
+ sb.insertBefore(pv, ul.nextElementSibling);
+
+ li.forEach(function(l) {
+ l.setAttribute('tabindex', 0);
+ });
+
+ sb.lastElementChild.setAttribute('tabindex', 0);
+
+ this.setFocus(sb, sel || li[0], true);
+ },
+
+ closeDropdown: function(sb, no_focus) {
+ if (!sb.hasAttribute('open'))
+ return;
+
+ var pv = sb.querySelector('ul.preview'),
+ ul = sb.querySelector('ul.dropdown'),
+ li = ul.querySelectorAll('li'),
+ fl = findParent(sb, '.cbi-value-field');
+
+ li.forEach(function(l) { l.removeAttribute('tabindex'); });
+ sb.lastElementChild.removeAttribute('tabindex');
+
+ sb.removeChild(pv);
+ sb.removeAttribute('open');
+ sb.style.width = sb.style.height = '';
+
+ ul.classList.remove('dropdown');
+ ul.style.top = ul.style.bottom = ul.style.maxHeight = '';
+
+ if (fl)
+ fl.classList.remove('cbi-dropdown-open');
+
+ if (!no_focus)
+ this.setFocus(sb, sb);
+
+ this.saveValues(sb, ul);
+ },
+
+ toggleItem: function(sb, li, force_state) {
+ if (li.hasAttribute('unselectable'))
+ return;
+
+ if (this.options.multiple) {
+ var cbox = li.querySelector('input[type="checkbox"]'),
+ items = li.parentNode.querySelectorAll('li'),
+ label = sb.querySelector('ul.preview'),
+ sel = li.parentNode.querySelectorAll('[selected]').length,
+ more = sb.querySelector('.more'),
+ ndisplay = this.options.display_items,
+ n = 0;
+
+ if (li.hasAttribute('selected')) {
+ if (force_state !== true) {
+ if (sel > 1 || this.options.optional) {
+ li.removeAttribute('selected');
+ cbox.checked = cbox.disabled = false;
+ sel--;
+ }
+ else {
+ cbox.disabled = true;
+ }
+ }
+ }
+ else {
+ if (force_state !== false) {
+ li.setAttribute('selected', '');
+ cbox.checked = true;
+ cbox.disabled = false;
+ sel++;
+ }
+ }
+
+ while (label && label.firstElementChild)
+ label.removeChild(label.firstElementChild);
+
+ for (var i = 0; i < items.length; i++) {
+ items[i].removeAttribute('display');
+ if (items[i].hasAttribute('selected')) {
+ if (ndisplay-- > 0) {
+ items[i].setAttribute('display', n++);
+ if (label)
+ label.appendChild(items[i].cloneNode(true));
+ }
+ var c = items[i].querySelector('input[type="checkbox"]');
+ if (c)
+ c.disabled = (sel == 1 && !this.options.optional);
+ }
+ }
+
+ if (ndisplay < 0)
+ sb.setAttribute('more', '');
+ else
+ sb.removeAttribute('more');
+
+ if (ndisplay === this.options.display_items)
+ sb.setAttribute('empty', '');
+ else
+ sb.removeAttribute('empty');
+
+ L.dom.content(more, (ndisplay === this.options.display_items)
+ ? (this.options.select_placeholder || this.options.placeholder) : '···');
+ }
+ else {
+ var sel = li.parentNode.querySelector('[selected]');
+ if (sel) {
+ sel.removeAttribute('display');
+ sel.removeAttribute('selected');
+ }
+
+ li.setAttribute('display', 0);
+ li.setAttribute('selected', '');
+
+ this.closeDropdown(sb, true);
+ }
+
+ this.saveValues(sb, li.parentNode);
+ },
+
+ transformItem: function(sb, li) {
+ var cbox = E('form', {}, E('input', { type: 'checkbox', tabindex: -1, onclick: 'event.preventDefault()' })),
+ label = E('label');
+
+ while (li.firstChild)
+ label.appendChild(li.firstChild);
+
+ li.appendChild(cbox);
+ li.appendChild(label);
+ },
+
+ saveValues: function(sb, ul) {
+ var sel = ul.querySelectorAll('li[selected]'),
+ div = sb.lastElementChild,
+ name = this.options.name,
+ strval = '',
+ values = [];
+
+ while (div.lastElementChild)
+ div.removeChild(div.lastElementChild);
+
+ sel.forEach(function (s) {
+ if (s.hasAttribute('placeholder'))
+ return;
+
+ var v = {
+ text: s.innerText,
+ value: s.hasAttribute('data-value') ? s.getAttribute('data-value') : s.innerText,
+ element: s
+ };
+
+ div.appendChild(E('input', {
+ type: 'hidden',
+ name: name,
+ value: v.value
+ }));
+
+ values.push(v);
+
+ strval += strval.length ? ' ' + v.value : v.value;
+ });
+
+ var detail = {
+ instance: this,
+ element: sb
+ };
+
+ if (this.options.multiple)
+ detail.values = values;
+ else
+ detail.value = values.length ? values[0] : null;
+
+ sb.value = strval;
+
+ sb.dispatchEvent(new CustomEvent('cbi-dropdown-change', {
+ bubbles: true,
+ detail: detail
+ }));
+ },
+
+ setValues: function(sb, values) {
+ var ul = sb.querySelector('ul');
+
+ if (this.options.create) {
+ for (var value in values) {
+ this.createItems(sb, value);
+
+ if (!this.options.multiple)
+ break;
+ }
+ }
+
+ if (this.options.multiple) {
+ var lis = ul.querySelectorAll('li[data-value]');
+ for (var i = 0; i < lis.length; i++) {
+ var value = lis[i].getAttribute('data-value');
+ if (values === null || !(value in values))
+ this.toggleItem(sb, lis[i], false);
+ else
+ this.toggleItem(sb, lis[i], true);
+ }
+ }
+ else {
+ var ph = ul.querySelector('li[placeholder]');
+ if (ph)
+ this.toggleItem(sb, ph);
+
+ var lis = ul.querySelectorAll('li[data-value]');
+ for (var i = 0; i < lis.length; i++) {
+ var value = lis[i].getAttribute('data-value');
+ if (values !== null && (value in values))
+ this.toggleItem(sb, lis[i]);
+ }
+ }
+ },
+
+ setFocus: function(sb, elem, scroll) {
+ if (sb && sb.hasAttribute && sb.hasAttribute('locked-in'))
+ return;
+
+ if (sb.target && findParent(sb.target, 'ul.dropdown'))
+ return;
+
+ document.querySelectorAll('.focus').forEach(function(e) {
+ if (!matchesElem(e, 'input')) {
+ e.classList.remove('focus');
+ e.blur();
+ }
+ });
+
+ if (elem) {
+ elem.focus();
+ elem.classList.add('focus');
+
+ if (scroll)
+ elem.parentNode.scrollTop = elem.offsetTop - elem.parentNode.offsetTop;
+ }
+ },
+
+ createItems: function(sb, value) {
+ var sbox = this,
+ val = (value || '').trim(),
+ ul = sb.querySelector('ul');
+
+ if (!sbox.options.multiple)
+ val = val.length ? [ val ] : [];
+ else
+ val = val.length ? val.split(/\s+/) : [];
+
+ val.forEach(function(item) {
+ var new_item = null;
+
+ ul.childNodes.forEach(function(li) {
+ if (li.getAttribute && li.getAttribute('data-value') === item)
+ new_item = li;
+ });
+
+ if (!new_item) {
+ var markup,
+ tpl = sb.querySelector(sbox.options.create_template);
+
+ if (tpl)
+ markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^<!--|-->$/, '').trim();
+ else
+ markup = '<li data-value="{{value}}">{{value}}</li>';
+
+ new_item = E(markup.replace(/{{value}}/g, '%h'.format(item)));
+
+ if (sbox.options.multiple) {
+ sbox.transformItem(sb, new_item);
+ }
+ else {
+ var old = ul.querySelector('li[created]');
+ if (old)
+ ul.removeChild(old);
+
+ new_item.setAttribute('created', '');
+ }
+
+ new_item = ul.insertBefore(new_item, ul.lastElementChild);
+ }
+
+ sbox.toggleItem(sb, new_item, true);
+ sbox.setFocus(sb, new_item, true);
+ });
+ },
+
+ closeAllDropdowns: function() {
+ document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
+ s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
+ });
+ },
+
+ handleClick: function(ev) {
+ var sb = ev.currentTarget;
+
+ if (!sb.hasAttribute('open')) {
+ if (!matchesElem(ev.target, 'input'))
+ this.openDropdown(sb);
+ }
+ else {
+ var li = findParent(ev.target, 'li');
+ if (li && li.parentNode.classList.contains('dropdown'))
+ this.toggleItem(sb, li);
+ else if (li && li.parentNode.classList.contains('preview'))
+ this.closeDropdown(sb);
+ else if (matchesElem(ev.target, 'span.open, span.more'))
+ this.closeDropdown(sb);
+ }
+
+ ev.preventDefault();
+ ev.stopPropagation();
+ },
+
+ handleKeydown: function(ev) {
+ var sb = ev.currentTarget;
+
+ if (matchesElem(ev.target, 'input'))
+ return;
+
+ if (!sb.hasAttribute('open')) {
+ switch (ev.keyCode) {
+ case 37:
+ case 38:
+ case 39:
+ case 40:
+ this.openDropdown(sb);
+ ev.preventDefault();
+ }
+ }
+ else {
+ var active = findParent(document.activeElement, 'li');
+
+ switch (ev.keyCode) {
+ case 27:
+ this.closeDropdown(sb);
+ break;
+
+ case 13:
+ if (active) {
+ if (!active.hasAttribute('selected'))
+ this.toggleItem(sb, active);
+ this.closeDropdown(sb);
+ ev.preventDefault();
+ }
+ break;
+
+ case 32:
+ if (active) {
+ this.toggleItem(sb, active);
+ ev.preventDefault();
+ }
+ break;
+
+ case 38:
+ if (active && active.previousElementSibling) {
+ this.setFocus(sb, active.previousElementSibling);
+ ev.preventDefault();
+ }
+ break;
+
+ case 40:
+ if (active && active.nextElementSibling) {
+ this.setFocus(sb, active.nextElementSibling);
+ ev.preventDefault();
+ }
+ break;
+ }
+ }
+ },
+
+ handleDropdownClose: function(ev) {
+ var sb = ev.currentTarget;
+
+ this.closeDropdown(sb, true);
+ },
+
+ handleDropdownSelect: function(ev) {
+ var sb = ev.currentTarget,
+ li = findParent(ev.target, 'li');
+
+ if (!li)
+ return;
+
+ this.toggleItem(sb, li);
+ this.closeDropdown(sb, true);
+ },
+
+ handleMouseover: function(ev) {
+ var sb = ev.currentTarget;
+
+ if (!sb.hasAttribute('open'))
+ return;
+
+ var li = findParent(ev.target, 'li');
+
+ if (li && li.parentNode.classList.contains('dropdown'))
+ this.setFocus(sb, li);
+ },
+
+ handleFocus: function(ev) {
+ var sb = ev.currentTarget;
+
+ document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
+ if (s !== sb || sb.hasAttribute('open'))
+ s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
+ });
+ },
+
+ handleCanaryFocus: function(ev) {
+ this.closeDropdown(ev.currentTarget.parentNode);
+ },
+
+ handleCreateKeydown: function(ev) {
+ var input = ev.currentTarget,
+ sb = findParent(input, '.cbi-dropdown');
+
+ switch (ev.keyCode) {
+ case 13:
+ ev.preventDefault();
+
+ if (input.classList.contains('cbi-input-invalid'))
+ return;
+
+ this.createItems(sb, input.value);
+ input.value = '';
+ input.blur();
+ break;
+ }
+ },
+
+ handleCreateFocus: function(ev) {
+ var input = ev.currentTarget,
+ cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
+ sb = findParent(input, '.cbi-dropdown');
+
+ if (cbox)
+ cbox.checked = true;
+
+ sb.setAttribute('locked-in', '');
+ },
+
+ handleCreateBlur: function(ev) {
+ var input = ev.currentTarget,
+ cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
+ sb = findParent(input, '.cbi-dropdown');
+
+ if (cbox)
+ cbox.checked = false;
+
+ sb.removeAttribute('locked-in');
+ },
+
+ handleCreateClick: function(ev) {
+ ev.currentTarget.querySelector(this.options.create_query).focus();
+ },
+
+ setValue: function(values) {
+ if (this.options.multiple) {
+ if (!Array.isArray(values))
+ values = (values != null && values != '') ? [ values ] : [];
+
+ var v = {};
+
+ for (var i = 0; i < values.length; i++)
+ v[values[i]] = true;
+
+ this.setValues(this.node, v);
+ }
+ else {
+ var v = {};
+
+ if (values != null) {
+ if (Array.isArray(values))
+ v[values[0]] = true;
+ else
+ v[values] = true;
+ }
+
+ this.setValues(this.node, v);
+ }
+ },
+
+ getValue: function() {
+ var div = this.node.lastElementChild,
+ h = div.querySelectorAll('input[type="hidden"]'),
+ v = [];
+
+ for (var i = 0; i < h.length; i++)
+ v.push(h[i].value);
+
+ return this.options.multiple ? v : v[0];
+ }
+});
+
+var UICombobox = UIDropdown.extend({
+ __init__: function(value, choices, options) {
+ this.super('__init__', [ value, choices, Object.assign({
+ select_placeholder: _('-- Please choose --'),
+ custom_placeholder: _('-- custom --'),
+ dropdown_items: -1,
+ sort: true
+ }, options, {
+ multiple: false,
+ create: true,
+ optional: true
+ }) ]);
+ }
+});
+
+var UIDynamicList = UIElement.extend({
+ __init__: function(values, choices, options) {
+ if (!Array.isArray(values))
+ values = (values != null && values != '') ? [ values ] : [];
+
+ if (typeof(choices) != 'object')
+ choices = null;
+
+ this.values = values;
+ this.choices = choices;
+ this.options = Object.assign({}, options, {
+ multiple: false,
+ optional: true
+ });
+ },
+
+ render: function() {
+ var dl = E('div', {
+ 'id': this.options.id,
+ 'class': 'cbi-dynlist'
+ }, E('div', { 'class': 'add-item' }));
+
+ if (this.choices) {
+ var cbox = new UICombobox(null, this.choices, this.options);
+ dl.lastElementChild.appendChild(cbox.render());
+ }
+ else {
+ var inputEl = E('input', {
+ 'id': this.options.id ? 'widget.' + this.options.id : null,
+ 'type': 'text',
+ 'class': 'cbi-input-text',
+ 'placeholder': this.options.placeholder
+ });
+
+ dl.lastElementChild.appendChild(inputEl);
+ dl.lastElementChild.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+'));
+
+ if (this.options.datatype)
+ L.ui.addValidator(inputEl, this.options.datatype,
+ true, null, 'blur', 'keyup');
+ }
+
+ for (var i = 0; i < this.values.length; i++)
+ this.addItem(dl, this.values[i],
+ this.choices ? this.choices[this.values[i]] : null);
+
+ return this.bind(dl);
+ },
+
+ bind: function(dl) {
+ dl.addEventListener('click', L.bind(this.handleClick, this));
+ dl.addEventListener('keydown', L.bind(this.handleKeydown, this));
+ dl.addEventListener('cbi-dropdown-change', L.bind(this.handleDropdownChange, this));
+
+ this.node = dl;
+
+ this.setUpdateEvents(dl, 'cbi-dynlist-change');
+ this.setChangeEvents(dl, 'cbi-dynlist-change');
+
+ L.dom.bindClassInstance(dl, this);
+
+ return dl;
+ },
+
+ addItem: function(dl, value, text, flash) {
+ var exists = false,
+ new_item = E('div', { 'class': flash ? 'item flash' : 'item', 'tabindex': 0 }, [
+ E('span', {}, text || value),
+ E('input', {
+ 'type': 'hidden',
+ 'name': this.options.name,
+ 'value': value })]);
+
+ dl.querySelectorAll('.item, .add-item').forEach(function(item) {
+ if (exists)
+ return;
+
+ var hidden = item.querySelector('input[type="hidden"]');
+
+ if (hidden && hidden.parentNode !== item)
+ hidden = null;
+
+ if (hidden && hidden.value === value)
+ exists = true;
+ else if (!hidden || hidden.value >= value)
+ exists = !!item.parentNode.insertBefore(new_item, item);
+ });
+
+ dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
+ bubbles: true,
+ detail: {
+ instance: this,
+ element: dl,
+ value: value,
+ add: true
+ }
+ }));
+ },
+
+ removeItem: function(dl, item) {
+ var value = item.querySelector('input[type="hidden"]').value;
+ var sb = dl.querySelector('.cbi-dropdown');
+ if (sb)
+ sb.querySelectorAll('ul > li').forEach(function(li) {
+ if (li.getAttribute('data-value') === value) {
+ if (li.hasAttribute('dynlistcustom'))
+ li.parentNode.removeChild(li);
+ else
+ li.removeAttribute('unselectable');
+ }
+ });
+
+ item.parentNode.removeChild(item);
+
+ dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
+ bubbles: true,
+ detail: {
+ instance: this,
+ element: dl,
+ value: value,
+ remove: true
+ }
+ }));
+ },
+
+ handleClick: function(ev) {
+ var dl = ev.currentTarget,
+ item = findParent(ev.target, '.item');
+
+ if (item) {
+ this.removeItem(dl, item);
+ }
+ else if (matchesElem(ev.target, '.cbi-button-add')) {
+ var input = ev.target.previousElementSibling;
+ if (input.value.length && !input.classList.contains('cbi-input-invalid')) {
+ this.addItem(dl, input.value, null, true);
+ input.value = '';
+ }
+ }
+ },
+
+ handleDropdownChange: function(ev) {
+ var dl = ev.currentTarget,
+ sbIn = ev.detail.instance,
+ sbEl = ev.detail.element,
+ sbVal = ev.detail.value;
+
+ if (sbVal === null)
+ return;
+
+ sbIn.setValues(sbEl, null);
+ sbVal.element.setAttribute('unselectable', '');
+
+ if (sbVal.element.hasAttribute('created')) {
+ sbVal.element.removeAttribute('created');
+ sbVal.element.setAttribute('dynlistcustom', '');
+ }
+
+ this.addItem(dl, sbVal.value, sbVal.text, true);
+ },
+
+ handleKeydown: function(ev) {
+ var dl = ev.currentTarget,
+ item = findParent(ev.target, '.item');
+
+ if (item) {
+ switch (ev.keyCode) {
+ case 8: /* backspace */
+ if (item.previousElementSibling)
+ item.previousElementSibling.focus();
+
+ this.removeItem(dl, item);
+ break;
+
+ case 46: /* delete */
+ if (item.nextElementSibling) {
+ if (item.nextElementSibling.classList.contains('item'))
+ item.nextElementSibling.focus();
+ else
+ item.nextElementSibling.firstElementChild.focus();
+ }
+
+ this.removeItem(dl, item);
+ break;
+ }
+ }
+ else if (matchesElem(ev.target, '.cbi-input-text')) {
+ switch (ev.keyCode) {
+ case 13: /* enter */
+ if (ev.target.value.length && !ev.target.classList.contains('cbi-input-invalid')) {
+ this.addItem(dl, ev.target.value, null, true);
+ ev.target.value = '';
+ ev.target.blur();
+ ev.target.focus();
+ }
+
+ ev.preventDefault();
+ break;
+ }
+ }
+ },
+
+ getValue: function() {
+ var items = this.node.querySelectorAll('.item > input[type="hidden"]'),
+ input = this.node.querySelector('.add-item > input[type="text"]'),
+ v = [];
+
+ for (var i = 0; i < items.length; i++)
+ v.push(items[i].value);
+
+ if (input && input.value != null && input.value.match(/\S/) &&
+ input.classList.contains('cbi-input-invalid') == false &&
+ v.filter(function(s) { return s == input.value }).length == 0)
+ v.push(input.value);
+
+ return v;
+ },
+
+ setValue: function(values) {
+ if (!Array.isArray(values))
+ values = (values != null && values != '') ? [ values ] : [];
+
+ var items = this.node.querySelectorAll('.item');
+
+ for (var i = 0; i < items.length; i++)
+ if (items[i].parentNode === this.node)
+ this.removeItem(this.node, items[i]);
+
+ for (var i = 0; i < values.length; i++)
+ this.addItem(this.node, values[i],
+ this.choices ? this.choices[values[i]] : null);
+ }
+});
+
+var UIHiddenfield = UIElement.extend({
+ __init__: function(value, options) {
+ this.value = value;
+ this.options = Object.assign({
+
+ }, options);
+ },
+
+ render: function() {
+ var hiddenEl = E('input', {
+ 'id': this.options.id,
+ 'type': 'hidden',
+ 'value': this.value
+ });
+
+ return this.bind(hiddenEl);
+ },
+
+ bind: function(hiddenEl) {
+ this.node = hiddenEl;
+
+ L.dom.bindClassInstance(hiddenEl, this);
+
+ return hiddenEl;
+ },
+
+ getValue: function() {
+ return this.node.value;
+ },
+
+ setValue: function(value) {
+ this.node.value = value;
+ }
+});
+
+
+return L.Class.extend({
+ __init__: function() {
+ modalDiv = document.body.appendChild(
+ L.dom.create('div', { id: 'modal_overlay' },
+ L.dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true })));
+
+ tooltipDiv = document.body.appendChild(
+ L.dom.create('div', { class: 'cbi-tooltip' }));
+
+ /* setup old aliases */
+ L.showModal = this.showModal;
+ L.hideModal = this.hideModal;
+ L.showTooltip = this.showTooltip;
+ L.hideTooltip = this.hideTooltip;
+ L.itemlist = this.itemlist;
+
+ document.addEventListener('mouseover', this.showTooltip.bind(this), true);
+ document.addEventListener('mouseout', this.hideTooltip.bind(this), true);
+ document.addEventListener('focus', this.showTooltip.bind(this), true);
+ document.addEventListener('blur', this.hideTooltip.bind(this), true);
+
+ document.addEventListener('luci-loaded', this.tabs.init.bind(this.tabs));
+ document.addEventListener('luci-loaded', this.changes.init.bind(this.changes));
+ document.addEventListener('uci-loaded', this.changes.init.bind(this.changes));
+ },
+
+ /* Modal dialog */
+ showModal: function(title, children /* , ... */) {
+ var dlg = modalDiv.firstElementChild;
+
+ dlg.setAttribute('class', 'modal');
+
+ for (var i = 2; i < arguments.length; i++)
+ dlg.classList.add(arguments[i]);
+
+ L.dom.content(dlg, L.dom.create('h4', {}, title));
+ L.dom.append(dlg, children);
+
+ document.body.classList.add('modal-overlay-active');
+
+ return dlg;
+ },
+
+ hideModal: function() {
+ document.body.classList.remove('modal-overlay-active');
+ },
+
+ /* Tooltip */
+ showTooltip: function(ev) {
+ var target = findParent(ev.target, '[data-tooltip]');
+
+ if (!target)
+ return;
+
+ if (tooltipTimeout !== null) {
+ window.clearTimeout(tooltipTimeout);
+ tooltipTimeout = null;
+ }
+
+ var rect = target.getBoundingClientRect(),
+ x = rect.left + window.pageXOffset,
+ y = rect.top + rect.height + window.pageYOffset;
+
+ tooltipDiv.className = 'cbi-tooltip';
+ tooltipDiv.innerHTML = '▲ ';
+ tooltipDiv.firstChild.data += target.getAttribute('data-tooltip');
+
+ if (target.hasAttribute('data-tooltip-style'))
+ tooltipDiv.classList.add(target.getAttribute('data-tooltip-style'));
+
+ if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) {
+ y -= (tooltipDiv.offsetHeight + target.offsetHeight);
+ tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2);
+ }
+
+ tooltipDiv.style.top = y + 'px';
+ tooltipDiv.style.left = x + 'px';
+ tooltipDiv.style.opacity = 1;
+
+ tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', {
+ bubbles: true,
+ detail: { target: target }
+ }));
+ },
+
+ hideTooltip: function(ev) {
+ if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv ||
+ tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget))
+ return;
+
+ if (tooltipTimeout !== null) {
+ window.clearTimeout(tooltipTimeout);
+ tooltipTimeout = null;
+ }
+
+ tooltipDiv.style.opacity = 0;
+ tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
+
+ tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
+ },
+
+ /* Widget helper */
+ itemlist: function(node, items, separators) {
+ var children = [];
+
+ if (!Array.isArray(separators))
+ separators = [ separators || E('br') ];
+
+ for (var i = 0; i < items.length; i += 2) {
+ if (items[i+1] !== null && items[i+1] !== undefined) {
+ var sep = separators[(i/2) % separators.length],
+ cld = [];
+
+ children.push(E('span', { class: 'nowrap' }, [
+ items[i] ? E('strong', items[i] + ': ') : '',
+ items[i+1]
+ ]));
+
+ if ((i+2) < items.length)
+ children.push(L.dom.elem(sep) ? sep.cloneNode(true) : sep);
+ }
+ }
+
+ L.dom.content(node, children);
+
+ return node;
+ },
+
+ /* Tabs */
+ tabs: L.Class.singleton({
+ init: function() {
+ var groups = [], prevGroup = null, currGroup = null;
+
+ document.querySelectorAll('[data-tab]').forEach(function(tab) {
+ var parent = tab.parentNode;
+
+ if (!parent.hasAttribute('data-tab-group'))
+ parent.setAttribute('data-tab-group', groups.length);
+
+ currGroup = +parent.getAttribute('data-tab-group');
+
+ if (currGroup !== prevGroup) {
+ prevGroup = currGroup;
+
+ if (!groups[currGroup])
+ groups[currGroup] = [];
+ }
+
+ groups[currGroup].push(tab);
+ });
+
+ for (var i = 0; i < groups.length; i++)
+ this.initTabGroup(groups[i]);
+
+ document.addEventListener('dependency-update', this.updateTabs.bind(this));
+
+ this.updateTabs();
+
+ if (!groups.length)
+ this.setActiveTabId(-1, -1);
+ },
+
+ initTabGroup: function(panes) {
+ if (typeof(panes) != 'object' || !('length' in panes) || panes.length === 0)
+ return;
+
+ var menu = E('ul', { 'class': 'cbi-tabmenu' }),
+ group = panes[0].parentNode,
+ groupId = +group.getAttribute('data-tab-group'),
+ selected = null;
+
+ for (var i = 0, pane; pane = panes[i]; i++) {
+ var name = pane.getAttribute('data-tab'),
+ title = pane.getAttribute('data-tab-title'),
+ active = pane.getAttribute('data-tab-active') === 'true';
+
+ menu.appendChild(E('li', {
+ 'class': active ? 'cbi-tab' : 'cbi-tab-disabled',
+ 'data-tab': name
+ }, E('a', {
+ 'href': '#',
+ 'click': this.switchTab.bind(this)
+ }, title)));
+
+ if (active)
+ selected = i;
+ }
+
+ group.parentNode.insertBefore(menu, group);
+
+ if (selected === null) {
+ selected = this.getActiveTabId(groupId);
+
+ if (selected < 0 || selected >= panes.length || L.dom.isEmpty(panes[selected])) {
+ for (var i = 0; i < panes.length; i++) {
+ if (!L.dom.isEmpty(panes[i])) {
+ selected = i;
+ break;
+ }
+ }
+ }
+
+ menu.childNodes[selected].classList.add('cbi-tab');
+ menu.childNodes[selected].classList.remove('cbi-tab-disabled');
+ panes[selected].setAttribute('data-tab-active', 'true');
+
+ this.setActiveTabId(groupId, selected);
+ }
+ },
+
+ getActiveTabState: function() {
+ var page = document.body.getAttribute('data-page');
+
+ try {
+ var val = JSON.parse(window.sessionStorage.getItem('tab'));
+ if (val.page === page && Array.isArray(val.groups))
+ return val;
+ }
+ catch(e) {}
+
+ window.sessionStorage.removeItem('tab');
+ return { page: page, groups: [] };
+ },
+
+ getActiveTabId: function(groupId) {
+ return +this.getActiveTabState().groups[groupId] || 0;
+ },
+
+ setActiveTabId: function(groupId, tabIndex) {
+ try {
+ var state = this.getActiveTabState();
+ state.groups[groupId] = tabIndex;
+
+ window.sessionStorage.setItem('tab', JSON.stringify(state));
+ }
+ catch (e) { return false; }
+
+ return true;
+ },
+
+ updateTabs: function(ev, root) {
+ (root || document).querySelectorAll('[data-tab-title]').forEach(function(pane) {
+ var menu = pane.parentNode.previousElementSibling,
+ tab = menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))),
+ n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
+
+ if (L.dom.isEmpty(pane)) {
+ tab.style.display = 'none';
+ tab.classList.remove('flash');
+ }
+ else if (tab.style.display === 'none') {
+ tab.style.display = '';
+ requestAnimationFrame(function() { tab.classList.add('flash') });
+ }
+
+ if (n_errors) {
+ tab.setAttribute('data-errors', n_errors);
+ tab.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors));
+ tab.setAttribute('data-tooltip-style', 'error');
+ }
+ else {
+ tab.removeAttribute('data-errors');
+ tab.removeAttribute('data-tooltip');
+ }
+ });
+ },
+
+ switchTab: function(ev) {
+ var tab = ev.target.parentNode,
+ name = tab.getAttribute('data-tab'),
+ menu = tab.parentNode,
+ group = menu.nextElementSibling,
+ groupId = +group.getAttribute('data-tab-group'),
+ index = 0;
+
+ ev.preventDefault();
+
+ if (!tab.classList.contains('cbi-tab-disabled'))
+ return;
+
+ menu.querySelectorAll('[data-tab]').forEach(function(tab) {
+ tab.classList.remove('cbi-tab');
+ tab.classList.remove('cbi-tab-disabled');
+ tab.classList.add(
+ tab.getAttribute('data-tab') === name ? 'cbi-tab' : 'cbi-tab-disabled');
+ });
+
+ group.childNodes.forEach(function(pane) {
+ if (L.dom.matches(pane, '[data-tab]')) {
+ if (pane.getAttribute('data-tab') === name) {
+ pane.setAttribute('data-tab-active', 'true');
+ L.ui.tabs.setActiveTabId(groupId, index);
+ }
+ else {
+ pane.setAttribute('data-tab-active', 'false');
+ }
+
+ index++;
+ }
+ });
+ }
+ }),
+
+ /* UCI Changes */
+ changes: L.Class.singleton({
+ init: function() {
+ if (!L.env.sessionid)
+ return;
+
+ return uci.changes().then(L.bind(this.renderChangeIndicator, this));
+ },
+
+ setIndicator: function(n) {
+ var i = document.querySelector('.uci_change_indicator');
+ if (i == null) {
+ var poll = document.getElementById('xhr_poll_status');
+ i = poll.parentNode.insertBefore(E('a', {
+ 'href': '#',
+ 'class': 'uci_change_indicator label notice',
+ 'click': L.bind(this.displayChanges, this)
+ }), poll);
+ }
+
+ if (n > 0) {
+ L.dom.content(i, [ _('Unsaved Changes'), ': ', n ]);
+ i.classList.add('flash');
+ i.style.display = '';
+ }
+ else {
+ i.classList.remove('flash');
+ i.style.display = 'none';
+ }
+ },
+
+ renderChangeIndicator: function(changes) {
+ var n_changes = 0;
+
+ for (var config in changes)
+ if (changes.hasOwnProperty(config))
+ n_changes += changes[config].length;
+
+ this.changes = changes;
+ this.setIndicator(n_changes);
+ },
+
+ changeTemplates: {
+ 'add-3': '<ins>uci add %0 <strong>%3</strong> # =%2</ins>',
+ 'set-3': '<ins>uci set %0.<strong>%2</strong>=%3</ins>',
+ 'set-4': '<var><ins>uci set %0.%2.%3=<strong>%4</strong></ins></var>',
+ 'remove-2': '<del>uci del %0.<strong>%2</strong></del>',
+ 'remove-3': '<var><del>uci del %0.%2.<strong>%3</strong></del></var>',
+ 'order-3': '<var>uci reorder %0.%2=<strong>%3</strong></var>',
+ 'list-add-4': '<var><ins>uci add_list %0.%2.%3=<strong>%4</strong></ins></var>',
+ 'list-del-4': '<var><del>uci del_list %0.%2.%3=<strong>%4</strong></del></var>',
+ 'rename-3': '<var>uci rename %0.%2=<strong>%3</strong></var>',
+ 'rename-4': '<var>uci rename %0.%2.%3=<strong>%4</strong></var>'
+ },
+
+ displayChanges: function() {
+ var list = E('div', { 'class': 'uci-change-list' }),
+ dlg = L.ui.showModal(_('Configuration') + ' / ' + _('Changes'), [
+ E('div', { 'class': 'cbi-section' }, [
+ E('strong', _('Legend:')),
+ E('div', { 'class': 'uci-change-legend' }, [
+ E('div', { 'class': 'uci-change-legend-label' }, [
+ E('ins', '&#160;'), ' ', _('Section added') ]),
+ E('div', { 'class': 'uci-change-legend-label' }, [
+ E('del', '&#160;'), ' ', _('Section removed') ]),
+ E('div', { 'class': 'uci-change-legend-label' }, [
+ E('var', {}, E('ins', '&#160;')), ' ', _('Option changed') ]),
+ E('div', { 'class': 'uci-change-legend-label' }, [
+ E('var', {}, E('del', '&#160;')), ' ', _('Option removed') ])]),
+ E('br'), list,
+ E('div', { 'class': 'right' }, [
+ E('input', {
+ 'type': 'button',
+ 'class': 'btn',
+ 'click': L.ui.hideModal,
+ 'value': _('Dismiss')
+ }), ' ',
+ E('input', {
+ 'type': 'button',
+ 'class': 'cbi-button cbi-button-positive important',
+ 'click': L.bind(this.apply, this, true),
+ 'value': _('Save & Apply')
+ }), ' ',
+ E('input', {
+ 'type': 'button',
+ 'class': 'cbi-button cbi-button-reset',
+ 'click': L.bind(this.revert, this),
+ 'value': _('Revert')
+ })])])
+ ]);
+
+ for (var config in this.changes) {
+ if (!this.changes.hasOwnProperty(config))
+ continue;
+
+ list.appendChild(E('h5', '# /etc/config/%s'.format(config)));
+
+ for (var i = 0, added = null; i < this.changes[config].length; i++) {
+ var chg = this.changes[config][i],
+ tpl = this.changeTemplates['%s-%d'.format(chg[0], chg.length)];
+
+ list.appendChild(E(tpl.replace(/%([01234])/g, function(m0, m1) {
+ switch (+m1) {
+ case 0:
+ return config;
+
+ case 2:
+ if (added != null && chg[1] == added[0])
+ return '@' + added[1] + '[-1]';
+ else
+ return chg[1];
+
+ case 4:
+ return "'%h'".format(chg[3].replace(/'/g, "'\"'\"'"));
+
+ default:
+ return chg[m1-1];
+ }
+ })));
+
+ if (chg[0] == 'add')
+ added = [ chg[1], chg[2] ];
+ }
+ }
+
+ list.appendChild(E('br'));
+ dlg.classList.add('uci-dialog');
+ },
+
+ displayStatus: function(type, content) {
+ if (type) {
+ var message = L.ui.showModal('', '');
+
+ message.classList.add('alert-message');
+ DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
+
+ if (content)
+ L.dom.content(message, content);
+
+ if (!this.was_polling) {
+ this.was_polling = L.Request.poll.active();
+ L.Request.poll.stop();
+ }
+ }
+ else {
+ L.ui.hideModal();
+
+ if (this.was_polling)
+ L.Request.poll.start();
+ }
+ },
+
+ rollback: function(checked) {
+ if (checked) {
+ this.displayStatus('warning spinning',
+ E('p', _('Failed to confirm apply within %ds, waiting for rollback…')
+ .format(L.env.apply_rollback)));
+
+ var call = function(r, data, duration) {
+ if (r.status === 204) {
+ L.ui.changes.displayStatus('warning', [
+ E('h4', _('Configuration has been rolled back!')),
+ E('p', _('The device could not be reached within %d seconds after applying the pending changes, which caused the configuration to be rolled back for safety reasons. If you believe that the configuration changes are correct nonetheless, perform an unchecked configuration apply. Alternatively, you can dismiss this warning and edit changes before attempting to apply again, or revert all pending changes to keep the currently working configuration state.').format(L.env.apply_rollback)),
+ E('div', { 'class': 'right' }, [
+ E('input', {
+ 'type': 'button',
+ 'class': 'btn',
+ 'click': L.bind(L.ui.changes.displayStatus, L.ui.changes, false),
+ 'value': _('Dismiss')
+ }), ' ',
+ E('input', {
+ 'type': 'button',
+ 'class': 'btn cbi-button-action important',
+ 'click': L.bind(L.ui.changes.revert, L.ui.changes),
+ 'value': _('Revert changes')
+ }), ' ',
+ E('input', {
+ 'type': 'button',
+ 'class': 'btn cbi-button-negative important',
+ 'click': L.bind(L.ui.changes.apply, L.ui.changes, false),
+ 'value': _('Apply unchecked')
+ })
+ ])
+ ]);
+
+ return;
+ }
+
+ var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
+ window.setTimeout(function() {
+ L.Request.request(L.url('admin/uci/confirm'), {
+ method: 'post',
+ timeout: L.env.apply_timeout * 1000,
+ query: { sid: L.env.sessionid, token: L.env.token }
+ }).then(call);
+ }, delay);
+ };
+
+ call({ status: 0 });
+ }
+ else {
+ this.displayStatus('warning', [
+ E('h4', _('Device unreachable!')),
+ E('p', _('Could not regain access to the device after applying the configuration changes. You might need to reconnect if you modified network related settings such as the IP address or wireless security credentials.'))
+ ]);
+ }
+ },
+
+ confirm: function(checked, deadline, override_token) {
+ var tt;
+ var ts = Date.now();
+
+ this.displayStatus('notice');
+
+ if (override_token)
+ this.confirm_auth = { token: override_token };
+
+ var call = function(r, data, duration) {
+ if (Date.now() >= deadline) {
+ window.clearTimeout(tt);
+ L.ui.changes.rollback(checked);
+ return;
+ }
+ else if (r && (r.status === 200 || r.status === 204)) {
+ document.dispatchEvent(new CustomEvent('uci-applied'));
+
+ L.ui.changes.setIndicator(0);
+ L.ui.changes.displayStatus('notice',
+ E('p', _('Configuration has been applied.')));
+
+ window.clearTimeout(tt);
+ window.setTimeout(function() {
+ //L.ui.changes.displayStatus(false);
+ window.location = window.location.href.split('#')[0];
+ }, L.env.apply_display * 1000);
+
+ return;
+ }
+
+ var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
+ window.setTimeout(function() {
+ L.Request.request(L.url('admin/uci/confirm'), {
+ method: 'post',
+ timeout: L.env.apply_timeout * 1000,
+ query: L.ui.changes.confirm_auth
+ }).then(call, call);
+ }, delay);
+ };
+
+ var tick = function() {
+ var now = Date.now();
+
+ L.ui.changes.displayStatus('notice spinning',
+ E('p', _('Waiting for configuration to get applied… %ds')
+ .format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
+
+ if (now >= deadline)
+ return;
+
+ tt = window.setTimeout(tick, 1000 - (now - ts));
+ ts = now;
+ };
+
+ tick();
+
+ /* wait a few seconds for the settings to become effective */
+ window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
+ },
+
+ apply: function(checked) {
+ this.displayStatus('notice spinning',
+ E('p', _('Starting configuration apply…')));
+
+ L.Request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
+ method: 'post',
+ query: { sid: L.env.sessionid, token: L.env.token }
+ }).then(function(r) {
+ if (r.status === (checked ? 200 : 204)) {
+ var tok = null; try { tok = r.json(); } catch(e) {}
+ if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
+ L.ui.changes.confirm_auth = tok;
+
+ L.ui.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
+ }
+ else if (checked && r.status === 204) {
+ L.ui.changes.displayStatus('notice',
+ E('p', _('There are no changes to apply')));
+
+ window.setTimeout(function() {
+ L.ui.changes.displayStatus(false);
+ }, L.env.apply_display * 1000);
+ }
+ else {
+ L.ui.changes.displayStatus('warning',
+ E('p', _('Apply request failed with status <code>%h</code>')
+ .format(r.responseText || r.statusText || r.status)));
+
+ window.setTimeout(function() {
+ L.ui.changes.displayStatus(false);
+ }, L.env.apply_display * 1000);
+ }
+ });
+ },
+
+ revert: function() {
+ this.displayStatus('notice spinning',
+ E('p', _('Reverting configuration…')));
+
+ L.Request.request(L.url('admin/uci/revert'), {
+ method: 'post',
+ query: { sid: L.env.sessionid, token: L.env.token }
+ }).then(function(r) {
+ if (r.status === 200) {
+ document.dispatchEvent(new CustomEvent('uci-reverted'));
+
+ L.ui.changes.setIndicator(0);
+ L.ui.changes.displayStatus('notice',
+ E('p', _('Changes have been reverted.')));
+
+ window.setTimeout(function() {
+ //L.ui.changes.displayStatus(false);
+ window.location = window.location.href.split('#')[0];
+ }, L.env.apply_display * 1000);
+ }
+ else {
+ L.ui.changes.displayStatus('warning',
+ E('p', _('Revert request failed with status <code>%h</code>')
+ .format(r.statusText || r.status)));
+
+ window.setTimeout(function() {
+ L.ui.changes.displayStatus(false);
+ }, L.env.apply_display * 1000);
+ }
+ });
+ }
+ }),
+
+ addValidator: function(field, type, optional, vfunc /*, ... */) {
+ if (type == null)
+ return;
+
+ var events = this.varargs(arguments, 3);
+ if (events.length == 0)
+ events.push('blur', 'keyup');
+
+ try {
+ var cbiValidator = L.validation.create(field, type, optional, vfunc),
+ validatorFn = cbiValidator.validate.bind(cbiValidator);
+
+ for (var i = 0; i < events.length; i++)
+ field.addEventListener(events[i], validatorFn);
+
+ validatorFn();
+
+ return validatorFn;
+ }
+ catch (e) { }
+ },
+
+ /* Widgets */
+ Textfield: UITextfield,
+ Checkbox: UICheckbox,
+ Select: UISelect,
+ Dropdown: UIDropdown,
+ DynamicList: UIDynamicList,
+ Combobox: UICombobox,
+ Hiddenfield: UIHiddenfield
+});
diff --git a/modules/luci-base/htdocs/luci-static/resources/validation.js b/modules/luci-base/htdocs/luci-static/resources/validation.js
new file mode 100644
index 0000000000..ca544cb15d
--- /dev/null
+++ b/modules/luci-base/htdocs/luci-static/resources/validation.js
@@ -0,0 +1,568 @@
+'use strict';
+
+var Validator = L.Class.extend({
+ __name__: 'Validation',
+
+ __init__: function(field, type, optional, vfunc, validatorFactory) {
+ this.field = field;
+ this.optional = optional;
+ this.vfunc = vfunc;
+ this.vstack = validatorFactory.compile(type);
+ this.factory = validatorFactory;
+ },
+
+ assert: function(condition, message) {
+ if (!condition) {
+ this.field.classList.add('cbi-input-invalid');
+ this.error = message;
+ return false;
+ }
+
+ this.field.classList.remove('cbi-input-invalid');
+ this.error = null;
+ return true;
+ },
+
+ apply: function(name, value, args) {
+ var func;
+
+ if (typeof(name) === 'function')
+ func = name;
+ else if (typeof(this.factory.types[name]) === 'function')
+ func = this.factory.types[name];
+ else
+ return false;
+
+ if (value != null)
+ this.value = value;
+
+ return func.apply(this, args);
+ },
+
+ validate: function() {
+ /* element is detached */
+ if (!findParent(this.field, 'body') && !findParent(this.field, '[data-field]'))
+ return true;
+
+ this.field.classList.remove('cbi-input-invalid');
+ this.value = (this.field.value != null) ? this.field.value : '';
+ this.error = null;
+
+ var valid;
+
+ if (this.value.length === 0)
+ valid = this.assert(this.optional, _('non-empty value'));
+ else
+ valid = this.vstack[0].apply(this, this.vstack[1]);
+
+ if (valid !== true) {
+ this.field.setAttribute('data-tooltip', _('Expecting: %s').format(this.error));
+ this.field.setAttribute('data-tooltip-style', 'error');
+ this.field.dispatchEvent(new CustomEvent('validation-failure', { bubbles: true }));
+ return false;
+ }
+
+ if (typeof(this.vfunc) == 'function')
+ valid = this.vfunc(this.value);
+
+ if (valid !== true) {
+ this.assert(false, valid);
+ this.field.setAttribute('data-tooltip', valid);
+ this.field.setAttribute('data-tooltip-style', 'error');
+ this.field.dispatchEvent(new CustomEvent('validation-failure', { bubbles: true }));
+ return false;
+ }
+
+ this.field.removeAttribute('data-tooltip');
+ this.field.removeAttribute('data-tooltip-style');
+ this.field.dispatchEvent(new CustomEvent('validation-success', { bubbles: true }));
+ return true;
+ },
+
+});
+
+var ValidatorFactory = L.Class.extend({
+ __name__: 'ValidatorFactory',
+
+ create: function(field, type, optional, vfunc) {
+ return new Validator(field, type, optional, vfunc, this);
+ },
+
+ compile: function(code) {
+ var pos = 0;
+ var esc = false;
+ var depth = 0;
+ var stack = [ ];
+
+ code += ',';
+
+ for (var i = 0; i < code.length; i++) {
+ if (esc) {
+ esc = false;
+ continue;
+ }
+
+ switch (code.charCodeAt(i))
+ {
+ case 92:
+ esc = true;
+ break;
+
+ case 40:
+ case 44:
+ if (depth <= 0) {
+ if (pos < i) {
+ var label = code.substring(pos, i);
+ label = label.replace(/\\(.)/g, '$1');
+ label = label.replace(/^[ \t]+/g, '');
+ label = label.replace(/[ \t]+$/g, '');
+
+ if (label && !isNaN(label)) {
+ stack.push(parseFloat(label));
+ }
+ else if (label.match(/^(['"]).*\1$/)) {
+ stack.push(label.replace(/^(['"])(.*)\1$/, '$2'));
+ }
+ else if (typeof this.types[label] == 'function') {
+ stack.push(this.types[label]);
+ stack.push(null);
+ }
+ else {
+ L.raise('SyntaxError', 'Unhandled token "%s"', label);
+ }
+ }
+
+ pos = i+1;
+ }
+
+ depth += (code.charCodeAt(i) == 40);
+ break;
+
+ case 41:
+ if (--depth <= 0) {
+ if (typeof stack[stack.length-2] != 'function')
+ L.raise('SyntaxError', 'Argument list follows non-function');
+
+ stack[stack.length-1] = this.compile(code.substring(pos, i));
+ pos = i+1;
+ }
+
+ break;
+ }
+ }
+
+ return stack;
+ },
+
+ parseInteger: function(x) {
+ return (/^-?\d+$/.test(x) ? +x : NaN);
+ },
+
+ parseDecimal: function(x) {
+ return (/^-?\d+(?:\.\d+)?$/.test(x) ? +x : NaN);
+ },
+
+ parseIPv4: function(x) {
+ if (!x.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/))
+ return null;
+
+ if (RegExp.$1 > 255 || RegExp.$2 > 255 || RegExp.$3 > 255 || RegExp.$4 > 255)
+ return null;
+
+ return [ +RegExp.$1, +RegExp.$2, +RegExp.$3, +RegExp.$4 ];
+ },
+
+ parseIPv6: function(x) {
+ if (x.match(/^([a-fA-F0-9:]+):(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/)) {
+ var v6 = RegExp.$1, v4 = this.parseIPv4(RegExp.$2);
+
+ if (!v4)
+ return null;
+
+ x = v6 + ':' + (v4[0] * 256 + v4[1]).toString(16)
+ + ':' + (v4[2] * 256 + v4[3]).toString(16);
+ }
+
+ if (!x.match(/^[a-fA-F0-9:]+$/))
+ return null;
+
+ var prefix_suffix = x.split(/::/);
+
+ if (prefix_suffix.length > 2)
+ return null;
+
+ var prefix = (prefix_suffix[0] || '0').split(/:/);
+ var suffix = prefix_suffix.length > 1 ? (prefix_suffix[1] || '0').split(/:/) : [];
+
+ if (suffix.length ? (prefix.length + suffix.length > 7)
+ : ((prefix_suffix.length < 2 && prefix.length < 8) || prefix.length > 8))
+ return null;
+
+ var i, word;
+ var words = [];
+
+ for (i = 0, word = parseInt(prefix[0], 16); i < prefix.length; word = parseInt(prefix[++i], 16))
+ if (prefix[i].length <= 4 && !isNaN(word) && word <= 0xFFFF)
+ words.push(word);
+ else
+ return null;
+
+ for (i = 0; i < (8 - prefix.length - suffix.length); i++)
+ words.push(0);
+
+ for (i = 0, word = parseInt(suffix[0], 16); i < suffix.length; word = parseInt(suffix[++i], 16))
+ if (suffix[i].length <= 4 && !isNaN(word) && word <= 0xFFFF)
+ words.push(word);
+ else
+ return null;
+
+ return words;
+ },
+
+ types: {
+ integer: function() {
+ return this.assert(this.factory.parseInteger(this.value) !== NaN, _('valid integer value'));
+ },
+
+ uinteger: function() {
+ return this.assert(this.factory.parseInteger(this.value) >= 0, _('positive integer value'));
+ },
+
+ float: function() {
+ return this.assert(this.factory.parseDecimal(this.value) !== NaN, _('valid decimal value'));
+ },
+
+ ufloat: function() {
+ return this.assert(this.factory.parseDecimal(this.value) >= 0, _('positive decimal value'));
+ },
+
+ ipaddr: function(nomask) {
+ return this.assert(this.apply('ip4addr', null, [nomask]) || this.apply('ip6addr', null, [nomask]),
+ nomask ? _('valid IP address') : _('valid IP address or prefix'));
+ },
+
+ ip4addr: function(nomask) {
+ var re = nomask ? /^(\d+\.\d+\.\d+\.\d+)$/ : /^(\d+\.\d+\.\d+\.\d+)(?:\/(\d+\.\d+\.\d+\.\d+)|\/(\d{1,2}))?$/,
+ m = this.value.match(re);
+
+ return this.assert(m && this.factory.parseIPv4(m[1]) && (m[2] ? this.factory.parseIPv4(m[2]) : (m[3] ? this.apply('ip4prefix', m[3]) : true)),
+ nomask ? _('valid IPv4 address') : _('valid IPv4 address or network'));
+ },
+
+ ip6addr: function(nomask) {
+ var re = nomask ? /^([0-9a-fA-F:.]+)$/ : /^([0-9a-fA-F:.]+)(?:\/(\d{1,3}))?$/,
+ m = this.value.match(re);
+
+ return this.assert(m && this.factory.parseIPv6(m[1]) && (m[2] ? this.apply('ip6prefix', m[2]) : true),
+ nomask ? _('valid IPv6 address') : _('valid IPv6 address or prefix'));
+ },
+
+ ip4prefix: function() {
+ return this.assert(!isNaN(this.value) && this.value >= 0 && this.value <= 32,
+ _('valid IPv4 prefix value (0-32)'));
+ },
+
+ ip6prefix: function() {
+ return this.assert(!isNaN(this.value) && this.value >= 0 && this.value <= 128,
+ _('valid IPv6 prefix value (0-128)'));
+ },
+
+ cidr: function() {
+ return this.assert(this.apply('cidr4') || this.apply('cidr6'), _('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'));
+ },
+
+ 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'));
+ },
+
+ ipnet4: function() {
+ var m = this.value.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
+ return this.assert(m && this.factory.parseIPv4(m[1]) && this.factory.parseIPv4(m[2]), _('IPv4 network in address/netmask notation'));
+ },
+
+ ipnet6: function() {
+ var m = this.value.match(/^([0-9a-fA-F:.]+)\/([0-9a-fA-F:.]+)$/);
+ return this.assert(m && this.factory.parseIPv6(m[1]) && this.factory.parseIPv6(m[2]), _('IPv6 network in address/netmask notation'));
+ },
+
+ ip6hostid: function() {
+ if (this.value == "eui64" || this.value == "random")
+ return true;
+
+ var v6 = this.factory.parseIPv6(this.value);
+ 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'),
+ _('valid network in address/netmask notation'));
+ },
+
+ ipmask4: function() {
+ return this.assert(this.apply('cidr4') || this.apply('ipnet4') || this.apply('ip4addr'),
+ _('valid IPv4 network'));
+ },
+
+ ipmask6: function() {
+ return this.assert(this.apply('cidr6') || this.apply('ipnet6') || this.apply('ip6addr'),
+ _('valid IPv6 network'));
+ },
+
+ port: function() {
+ var p = this.factory.parseInteger(this.value);
+ return this.assert(p >= 0 && p <= 65535, _('valid port value'));
+ },
+
+ portrange: function() {
+ if (this.value.match(/^(\d+)-(\d+)$/)) {
+ var p1 = +RegExp.$1;
+ var p2 = +RegExp.$2;
+ return this.assert(p1 <= p2 && p2 <= 65535,
+ _('valid port or port range (port1-port2)'));
+ }
+
+ return this.assert(this.apply('port'), _('valid port or port range (port1-port2)'));
+ },
+
+ macaddr: function() {
+ return this.assert(this.value.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null,
+ _('valid MAC address'));
+ },
+
+ host: function(ipv4only) {
+ return this.assert(this.apply('hostname') || this.apply(ipv4only == 1 ? 'ip4addr' : 'ipaddr'),
+ _('valid hostname or IP address'));
+ },
+
+ hostname: function(strict) {
+ if (this.value.length <= 253)
+ return this.assert(
+ (this.value.match(/^[a-zA-Z0-9_]+$/) != null ||
+ (this.value.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) &&
+ this.value.match(/[^0-9.]/))) &&
+ (!strict || !this.value.match(/^_/)),
+ _('valid hostname'));
+
+ return this.assert(false, _('valid hostname'));
+ },
+
+ network: function() {
+ return this.assert(this.apply('uciname') || this.apply('host'),
+ _('valid UCI identifier, hostname or IP address'));
+ },
+
+ hostport: function(ipv4only) {
+ var hp = this.value.split(/:/);
+ return this.assert(hp.length == 2 && this.apply('host', hp[0], [ipv4only]) && this.apply('port', hp[1]),
+ _('valid host:port'));
+ },
+
+ ip4addrport: function() {
+ var hp = this.value.split(/:/);
+ return this.assert(hp.length == 2 && this.apply('ip4addr', hp[0], [true]) && this.apply('port', hp[1]),
+ _('valid IPv4 address:port'));
+ },
+
+ ipaddrport: function(bracket) {
+ var m4 = this.value.match(/^([^\[\]:]+):(\d+)$/),
+ m6 = this.value.match((bracket == 1) ? /^\[(.+)\]:(\d+)$/ : /^([^\[\]]+):(\d+)$/);
+
+ if (m4)
+ return this.assert(this.apply('ip4addr', m4[1], [true]) && this.apply('port', m4[2]),
+ _('valid address:port'));
+
+ return this.assert(m6 && this.apply('ip6addr', m6[1], [true]) && this.apply('port', m6[2]),
+ _('valid address:port'));
+ },
+
+ wpakey: function() {
+ var v = this.value;
+
+ if (v.length == 64)
+ return this.assert(v.match(/^[a-fA-F0-9]{64}$/), _('valid hexadecimal WPA key'));
+
+ return this.assert((v.length >= 8) && (v.length <= 63), _('key between 8 and 63 characters'));
+ },
+
+ wepkey: function() {
+ var v = this.value;
+
+ if (v.substr(0, 2) === 's:')
+ v = v.substr(2);
+
+ if ((v.length == 10) || (v.length == 26))
+ return this.assert(v.match(/^[a-fA-F0-9]{10,26}$/), _('valid hexadecimal WEP key'));
+
+ return this.assert((v.length === 5) || (v.length === 13), _('key with either 5 or 13 characters'));
+ },
+
+ uciname: function() {
+ return this.assert(this.value.match(/^[a-zA-Z0-9_]+$/), _('valid UCI identifier'));
+ },
+
+ range: function(min, max) {
+ var val = this.factory.parseDecimal(this.value);
+ return this.assert(val >= +min && val <= +max, _('value between %f and %f').format(min, max));
+ },
+
+ min: function(min) {
+ return this.assert(this.factory.parseDecimal(this.value) >= +min, _('value greater or equal to %f').format(min));
+ },
+
+ max: function(max) {
+ return this.assert(this.factory.parseDecimal(this.value) <= +max, _('value smaller or equal to %f').format(max));
+ },
+
+ rangelength: function(min, max) {
+ var val = '' + this.value;
+ return this.assert((val.length >= +min) && (val.length <= +max),
+ _('value between %d and %d characters').format(min, max));
+ },
+
+ minlength: function(min) {
+ return this.assert((''+this.value).length >= +min,
+ _('value with at least %d characters').format(min));
+ },
+
+ maxlength: function(max) {
+ return this.assert((''+this.value).length <= +max,
+ _('value with at most %d characters').format(max));
+ },
+
+ or: function() {
+ var errors = [];
+
+ for (var i = 0; i < arguments.length; i += 2) {
+ if (typeof arguments[i] != 'function') {
+ if (arguments[i] == this.value)
+ return this.assert(true);
+ errors.push('"%s"'.format(arguments[i]));
+ i--;
+ }
+ else if (arguments[i].apply(this, arguments[i+1])) {
+ return this.assert(true);
+ }
+ else {
+ errors.push(this.error);
+ }
+ }
+
+ var t = _('One of the following: %s');
+
+ return this.assert(false, t.format('\n - ' + errors.join('\n - ')));
+ },
+
+ and: function() {
+ for (var i = 0; i < arguments.length; i += 2) {
+ if (typeof arguments[i] != 'function') {
+ if (arguments[i] != this.value)
+ return this.assert(false, '"%s"'.format(arguments[i]));
+ i--;
+ }
+ else if (!arguments[i].apply(this, arguments[i+1])) {
+ return this.assert(false, this.error);
+ }
+ }
+
+ return this.assert(true);
+ },
+
+ neg: function() {
+ this.value = this.value.replace(/^[ \t]*![ \t]*/, '');
+
+ if (arguments[0].apply(this, arguments[1]))
+ return this.assert(true);
+
+ return this.assert(false, _('Potential negation of: %s').format(this.error));
+ },
+
+ list: function(subvalidator, subargs) {
+ this.field.setAttribute('data-is-list', 'true');
+
+ var tokens = this.value.match(/[^ \t]+/g);
+ for (var i = 0; i < tokens.length; i++)
+ if (!this.apply(subvalidator, tokens[i], subargs))
+ return this.assert(false, this.error);
+
+ return this.assert(true);
+ },
+
+ phonedigit: function() {
+ return this.assert(this.value.match(/^[0-9\*#!\.]+$/),
+ _('valid phone digit (0-9, "*", "#", "!" or ".")'));
+ },
+
+ timehhmmss: function() {
+ return this.assert(this.value.match(/^[0-6][0-9]:[0-6][0-9]:[0-6][0-9]$/),
+ _('valid time (HH:MM:SS)'));
+ },
+
+ dateyyyymmdd: function() {
+ if (this.value.match(/^(\d\d\d\d)-(\d\d)-(\d\d)/)) {
+ var year = +RegExp.$1,
+ month = +RegExp.$2,
+ day = +RegExp.$3,
+ days_in_month = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
+
+ var is_leap_year = function(year) {
+ return ((!(year % 4) && (year % 100)) || !(year % 400));
+ }
+
+ var get_days_in_month = function(month, year) {
+ return (month === 2 && is_leap_year(year)) ? 29 : days_in_month[month - 1];
+ }
+
+ /* Firewall rules in the past don't make sense */
+ return this.assert(year >= 2015 && month && month <= 12 && day && day <= get_days_in_month(month, year),
+ _('valid date (YYYY-MM-DD)'));
+
+ }
+
+ return this.assert(false, _('valid date (YYYY-MM-DD)'));
+ },
+
+ unique: function(subvalidator, subargs) {
+ var ctx = this,
+ option = findParent(ctx.field, '[data-type][data-name]'),
+ section = findParent(option, '.cbi-section'),
+ query = '[data-type="%s"][data-name="%s"]'.format(option.getAttribute('data-type'), option.getAttribute('data-name')),
+ unique = true;
+
+ section.querySelectorAll(query).forEach(function(sibling) {
+ if (sibling === option)
+ return;
+
+ var input = sibling.querySelector('[data-type]'),
+ values = input ? (input.getAttribute('data-is-list') ? input.value.match(/[^ \t]+/g) : [ input.value ]) : null;
+
+ if (values !== null && values.indexOf(ctx.value) !== -1)
+ unique = false;
+ });
+
+ if (!unique)
+ return this.assert(false, _('unique value'));
+
+ if (typeof(subvalidator) === 'function')
+ return this.apply(subvalidator, null, subargs);
+
+ return this.assert(true);
+ },
+
+ hexstring: function() {
+ return this.assert(this.value.match(/^([a-f0-9][a-f0-9]|[A-F0-9][A-F0-9])+$/),
+ _('hexadecimal encoded value'));
+ },
+
+ string: function() {
+ return true;
+ }
+ }
+});
+
+return ValidatorFactory;
diff --git a/modules/luci-base/htdocs/luci-static/resources/xhr.js b/modules/luci-base/htdocs/luci-static/resources/xhr.js
index 3133898b5e..10bc88e1f4 100644
--- a/modules/luci-base/htdocs/luci-static/resources/xhr.js
+++ b/modules/luci-base/htdocs/luci-static/resources/xhr.js
@@ -1,250 +1 @@
-/*
- * xhr.js - XMLHttpRequest helper class
- * (c) 2008-2018 Jo-Philipp Wich <jo@mein.io>
- */
-
-XHR.prototype = {
- _encode: function(obj) {
- obj = obj ? obj : { };
- obj['_'] = Math.random();
-
- if (typeof obj == 'object') {
- var code = '';
- var self = this;
-
- for (var k in obj)
- code += (code ? '&' : '') +
- k + '=' + encodeURIComponent(obj[k]);
-
- return code;
- }
-
- return obj;
- },
-
- _response: function(callback, ts) {
- if (this._xmlHttp.readyState !== 4)
- return;
-
- var status = this._xmlHttp.status,
- login = this._xmlHttp.getResponseHeader("X-LuCI-Login-Required"),
- type = this._xmlHttp.getResponseHeader("Content-Type"),
- json = null;
-
- if (status === 403 && login === 'yes') {
- XHR.halt();
-
- showModal(_('Session expired'), [
- E('div', { class: 'alert-message warning' },
- _('A new login is required since the authentication session expired.')),
- E('div', { class: 'right' },
- E('div', {
- class: 'btn primary',
- click: function() {
- var loc = window.location;
- window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
- }
- }, _('To login…')))
- ]);
- }
- else if (type && type.toLowerCase().match(/^application\/json\b/)) {
- try {
- json = JSON.parse(this._xmlHttp.responseText);
- }
- catch(e) {
- json = null;
- }
- }
-
- callback(this._xmlHttp, json, Date.now() - ts);
- },
-
- busy: function() {
- if (!this._xmlHttp)
- return false;
-
- switch (this._xmlHttp.readyState)
- {
- case 1:
- case 2:
- case 3:
- return true;
-
- default:
- return false;
- }
- },
-
- abort: function() {
- if (this.busy())
- this._xmlHttp.abort();
- },
-
- get: function(url, data, callback, timeout) {
- this._xmlHttp = new XMLHttpRequest();
-
- var xhr = this._xmlHttp,
- code = this._encode(data);
-
- url = location.protocol + '//' + location.host + url;
-
- if (code)
- if (url.substr(url.length-1,1) == '&')
- url += code;
- else
- url += '?' + code;
-
- xhr.open('GET', url, true);
-
- if (!isNaN(timeout))
- xhr.timeout = timeout;
-
- xhr.onreadystatechange = this._response.bind(this, callback, Date.now());
- xhr.send(null);
- },
-
- post: function(url, data, callback, timeout) {
- this._xmlHttp = new XMLHttpRequest();
-
- var xhr = this._xmlHttp,
- code = this._encode(data);
-
- xhr.open('POST', url, true);
-
- if (!isNaN(timeout))
- xhr.timeout = timeout;
-
- xhr.onreadystatechange = this._response.bind(this, callback, Date.now());
- xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
- xhr.send(code);
- },
-
- cancel: function() {
- this._xmlHttp.onreadystatechange = function() {};
- this._xmlHttp.abort();
- },
-
- send_form: function(form, callback, extra_values) {
- var code = '';
-
- for (var i = 0; i < form.elements.length; i++) {
- var e = form.elements[i];
-
- if (e.options) {
- code += (code ? '&' : '') +
- form.elements[i].name + '=' + encodeURIComponent(
- e.options[e.selectedIndex].value
- );
- }
- else if (e.length) {
- for (var j = 0; j < e.length; j++)
- if (e[j].name) {
- code += (code ? '&' : '') +
- e[j].name + '=' + encodeURIComponent(e[j].value);
- }
- }
- else {
- code += (code ? '&' : '') +
- e.name + '=' + encodeURIComponent(e.value);
- }
- }
-
- if (typeof extra_values == 'object')
- for (var key in extra_values)
- code += (code ? '&' : '') +
- key + '=' + encodeURIComponent(extra_values[key]);
-
- return (form.method == 'get'
- ? this.get(form.getAttribute('action'), code, callback)
- : this.post(form.getAttribute('action'), code, callback));
- }
-}
-
-XHR.get = function(url, data, callback) {
- (new XHR()).get(url, data, callback);
-}
-
-XHR.post = function(url, data, callback) {
- (new XHR()).post(url, data, callback);
-}
-
-XHR.poll = function(interval, url, data, callback, post) {
- if (isNaN(interval) || interval <= 0)
- interval = L.env.pollinterval;
-
- if (!XHR._q) {
- XHR._t = 0;
- XHR._q = [ ];
- XHR._r = function() {
- for (var i = 0, e = XHR._q[0]; i < XHR._q.length; e = XHR._q[++i])
- {
- if (!(XHR._t % e.interval) && !e.xhr.busy())
- e.xhr[post ? 'post' : 'get'](e.url, e.data, e.callback, e.interval * 1000 * 5 - 5);
- }
-
- XHR._t++;
- };
- }
-
- var e = {
- interval: interval,
- callback: callback,
- url: url,
- data: data,
- xhr: new XHR()
- };
-
- XHR._q.push(e);
-
- return e;
-}
-
-XHR.stop = function(e) {
- for (var i = 0; XHR._q && XHR._q[i]; i++) {
- if (XHR._q[i] === e) {
- e.xhr.cancel();
- XHR._q.splice(i, 1);
- return true;
- }
- }
-
- return false;
-}
-
-XHR.halt = function() {
- if (XHR._i) {
- /* show & set poll indicator */
- try {
- document.getElementById('xhr_poll_status').style.display = '';
- document.getElementById('xhr_poll_status_on').style.display = 'none';
- document.getElementById('xhr_poll_status_off').style.display = '';
- } catch(e) { }
-
- window.clearInterval(XHR._i);
- XHR._i = null;
- }
-}
-
-XHR.run = function() {
- if (XHR._r && !XHR._i) {
- /* show & set poll indicator */
- try {
- document.getElementById('xhr_poll_status').style.display = '';
- document.getElementById('xhr_poll_status_on').style.display = '';
- document.getElementById('xhr_poll_status_off').style.display = 'none';
- } catch(e) { }
-
- /* kick first round manually to prevent one second lag when setting up
- * the poll interval */
- XHR._r();
- XHR._i = window.setInterval(XHR._r, 1000);
- }
-}
-
-XHR.running = function() {
- return !!(XHR._r && XHR._i);
-}
-
-function XHR() {}
-
-document.addEventListener('DOMContentLoaded', XHR.run);
+/* replaced by luci.js */