diff options
author | Jo-Philipp Wich <jow@openwrt.org> | 2015-01-05 19:17:55 +0100 |
---|---|---|
committer | Jo-Philipp Wich <jow@openwrt.org> | 2015-01-05 19:17:55 +0100 |
commit | e28f8f6a5a654a86dc6840f8bb1b16b888e24430 (patch) | |
tree | d05bc269448c558972a4482d316e63e759a9c414 /luci2 | |
parent | 863e738b445e2e53f8d84dd12535781b2d6fc081 (diff) | |
download | luci2-ui-e28f8f6a5a654a86dc6840f8bb1b16b888e24430.tar.gz |
luci2: split into submodules
Signed-off-by: Jo-Philipp Wich <jow@openwrt.org>
Diffstat (limited to 'luci2')
-rw-r--r-- | luci2/htdocs/luci2.html | 2 | ||||
-rw-r--r-- | luci2/htdocs/luci2/cbi.js | 3200 | ||||
-rw-r--r-- | luci2/htdocs/luci2/firewall.js | 50 | ||||
-rw-r--r-- | luci2/htdocs/luci2/luci2.js | 7036 | ||||
-rw-r--r-- | luci2/htdocs/luci2/network.js | 1447 | ||||
-rw-r--r-- | luci2/htdocs/luci2/rpc.js | 188 | ||||
-rw-r--r-- | luci2/htdocs/luci2/session.js | 78 | ||||
-rw-r--r-- | luci2/htdocs/luci2/system.js | 82 | ||||
-rw-r--r-- | luci2/htdocs/luci2/uci.js | 532 | ||||
-rw-r--r-- | luci2/htdocs/luci2/ui.js | 1324 | ||||
-rw-r--r-- | luci2/htdocs/luci2/wireless.js | 156 |
11 files changed, 7067 insertions, 7028 deletions
diff --git a/luci2/htdocs/luci2.html b/luci2/htdocs/luci2.html index de5e037..47c74c7 100644 --- a/luci2/htdocs/luci2.html +++ b/luci2/htdocs/luci2.html @@ -24,7 +24,7 @@ var L = new LuCI2(); L.ui.login().then(function() { - L.ui.init(); + L.ui.load(); }); }); </script> diff --git a/luci2/htdocs/luci2/cbi.js b/luci2/htdocs/luci2/cbi.js new file mode 100644 index 0000000..cfc677d --- /dev/null +++ b/luci2/htdocs/luci2/cbi.js @@ -0,0 +1,3200 @@ +(function() { + var type = function(f, l) + { + f.message = l; + return f; + }; + + var cbi_class = { + validation: { + i18n: function(msg) + { + L.cbi.validation.message = L.tr(msg); + }, + + compile: function(code) + { + var pos = 0; + var esc = false; + var depth = 0; + var types = L.cbi.validation.types; + 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 types[label] == 'function') + { + stack.push(types[label]); + stack.push([ ]); + } + 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] = + L.cbi.validation.compile(code.substring(pos, i)); + + pos = i+1; + } + break; + } + } + + return stack; + } + } + }; + + var validation = cbi_class.validation; + + validation.types = { + 'integer': function() + { + if (this.match(/^-?[0-9]+$/) != null) + return true; + + validation.i18n('Must be a valid integer'); + return false; + }, + + 'uinteger': function() + { + if (validation.types['integer'].apply(this) && (this >= 0)) + return true; + + validation.i18n('Must be a positive integer'); + return false; + }, + + 'float': function() + { + if (!isNaN(parseFloat(this))) + return true; + + validation.i18n('Must be a valid number'); + return false; + }, + + 'ufloat': function() + { + if (validation.types['float'].apply(this) && (this >= 0)) + return true; + + validation.i18n('Must be a positive number'); + return false; + }, + + 'ipaddr': function() + { + if (L.parseIPv4(this) || L.parseIPv6(this)) + return true; + + validation.i18n('Must be a valid IP address'); + return false; + }, + + 'ip4addr': function() + { + if (L.parseIPv4(this)) + return true; + + validation.i18n('Must be a valid IPv4 address'); + return false; + }, + + 'ip6addr': function() + { + if (L.parseIPv6(this)) + return true; + + validation.i18n('Must be a valid IPv6 address'); + return false; + }, + + 'netmask4': function() + { + if (L.isNetmask(L.parseIPv4(this))) + return true; + + validation.i18n('Must be a valid IPv4 netmask'); + return false; + }, + + 'netmask6': function() + { + if (L.isNetmask(L.parseIPv6(this))) + return true; + + validation.i18n('Must be a valid IPv6 netmask6'); + return false; + }, + + 'cidr4': function() + { + if (this.match(/^([0-9.]+)\/(\d{1,2})$/)) + if (RegExp.$2 <= 32 && L.parseIPv4(RegExp.$1)) + return true; + + validation.i18n('Must be a valid IPv4 prefix'); + return false; + }, + + 'cidr6': function() + { + if (this.match(/^([a-fA-F0-9:.]+)\/(\d{1,3})$/)) + if (RegExp.$2 <= 128 && L.parseIPv6(RegExp.$1)) + return true; + + validation.i18n('Must be a valid IPv6 prefix'); + return false; + }, + + 'ipmask4': function() + { + if (this.match(/^([0-9.]+)\/([0-9.]+)$/)) + { + var addr = RegExp.$1, mask = RegExp.$2; + if (L.parseIPv4(addr) && L.isNetmask(L.parseIPv4(mask))) + return true; + } + + validation.i18n('Must be a valid IPv4 address/netmask pair'); + return false; + }, + + 'ipmask6': function() + { + if (this.match(/^([a-fA-F0-9:.]+)\/([a-fA-F0-9:.]+)$/)) + { + var addr = RegExp.$1, mask = RegExp.$2; + if (L.parseIPv6(addr) && L.isNetmask(L.parseIPv6(mask))) + return true; + } + + validation.i18n('Must be a valid IPv6 address/netmask pair'); + return false; + }, + + 'port': function() + { + if (validation.types['integer'].apply(this) && + (this >= 0) && (this <= 65535)) + return true; + + validation.i18n('Must be a valid port number'); + return false; + }, + + 'portrange': function() + { + if (this.match(/^(\d+)-(\d+)$/)) + { + var p1 = RegExp.$1; + var p2 = RegExp.$2; + + if (validation.types['port'].apply(p1) && + validation.types['port'].apply(p2) && + (parseInt(p1) <= parseInt(p2))) + return true; + } + else if (validation.types['port'].apply(this)) + { + return true; + } + + validation.i18n('Must be a valid port range'); + return false; + }, + + 'macaddr': function() + { + if (this.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null) + return true; + + validation.i18n('Must be a valid MAC address'); + return false; + }, + + 'host': function() + { + if (validation.types['hostname'].apply(this) || + validation.types['ipaddr'].apply(this)) + return true; + + validation.i18n('Must be a valid hostname or IP address'); + return false; + }, + + 'hostname': function() + { + if ((this.length <= 253) && + ((this.match(/^[a-zA-Z0-9]+$/) != null || + (this.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) && + this.match(/[^0-9.]/))))) + return true; + + validation.i18n('Must be a valid host name'); + return false; + }, + + 'network': function() + { + if (validation.types['uciname'].apply(this) || + validation.types['host'].apply(this)) + return true; + + validation.i18n('Must be a valid network name'); + return false; + }, + + 'wpakey': function() + { + var v = this; + + if ((v.length == 64) + ? (v.match(/^[a-fA-F0-9]{64}$/) != null) + : ((v.length >= 8) && (v.length <= 63))) + return true; + + validation.i18n('Must be a valid WPA key'); + return false; + }, + + 'wepkey': function() + { + var v = this; + + if (v.substr(0,2) == 's:') + v = v.substr(2); + + if (((v.length == 10) || (v.length == 26)) + ? (v.match(/^[a-fA-F0-9]{10,26}$/) != null) + : ((v.length == 5) || (v.length == 13))) + return true; + + validation.i18n('Must be a valid WEP key'); + return false; + }, + + 'uciname': function() + { + if (this.match(/^[a-zA-Z0-9_]+$/) != null) + return true; + + validation.i18n('Must be a valid UCI identifier'); + return false; + }, + + 'range': function(min, max) + { + var val = parseFloat(this); + + if (validation.types['integer'].apply(this) && + !isNaN(min) && !isNaN(max) && ((val >= min) && (val <= max))) + return true; + + validation.i18n('Must be a number between %d and %d'); + return false; + }, + + 'min': function(min) + { + var val = parseFloat(this); + + if (validation.types['integer'].apply(this) && + !isNaN(min) && !isNaN(val) && (val >= min)) + return true; + + validation.i18n('Must be a number greater or equal to %d'); + return false; + }, + + 'max': function(max) + { + var val = parseFloat(this); + + if (validation.types['integer'].apply(this) && + !isNaN(max) && !isNaN(val) && (val <= max)) + return true; + + validation.i18n('Must be a number lower or equal to %d'); + return false; + }, + + 'rangelength': function(min, max) + { + var val = '' + this; + + if (!isNaN(min) && !isNaN(max) && + (val.length >= min) && (val.length <= max)) + return true; + + if (min != max) + validation.i18n('Must be between %d and %d characters'); + else + validation.i18n('Must be %d characters'); + return false; + }, + + 'minlength': function(min) + { + var val = '' + this; + + if (!isNaN(min) && (val.length >= min)) + return true; + + validation.i18n('Must be at least %d characters'); + return false; + }, + + 'maxlength': function(max) + { + var val = '' + this; + + if (!isNaN(max) && (val.length <= max)) + return true; + + validation.i18n('Must be at most %d characters'); + return false; + }, + + 'or': function() + { + var msgs = [ ]; + + for (var i = 0; i < arguments.length; i += 2) + { + delete validation.message; + + if (typeof(arguments[i]) != 'function') + { + if (arguments[i] == this) + return true; + i--; + } + else if (arguments[i].apply(this, arguments[i+1])) + { + return true; + } + + if (validation.message) + msgs.push(validation.message.format.apply(validation.message, arguments[i+1])); + } + + validation.message = msgs.join( L.tr(' - or - ')); + return false; + }, + + 'and': function() + { + var msgs = [ ]; + + for (var i = 0; i < arguments.length; i += 2) + { + delete validation.message; + + if (typeof arguments[i] != 'function') + { + if (arguments[i] != this) + return false; + i--; + } + else if (!arguments[i].apply(this, arguments[i+1])) + { + return false; + } + + if (validation.message) + msgs.push(validation.message.format.apply(validation.message, arguments[i+1])); + } + + validation.message = msgs.join(', '); + return true; + }, + + 'neg': function() + { + return validation.types['or'].apply( + this.replace(/^[ \t]*![ \t]*/, ''), arguments); + }, + + 'list': function(subvalidator, subargs) + { + if (typeof subvalidator != 'function') + return false; + + var tokens = this.match(/[^ \t]+/g); + for (var i = 0; i < tokens.length; i++) + if (!subvalidator.apply(tokens[i], subargs)) + return false; + + return true; + }, + + 'phonedigit': function() + { + if (this.match(/^[0-9\*#!\.]+$/) != null) + return true; + + validation.i18n('Must be a valid phone number digit'); + return false; + }, + + 'string': function() + { + return true; + } + }; + + cbi_class.AbstractValue = L.ui.AbstractWidget.extend({ + init: function(name, options) + { + this.name = name; + this.instance = { }; + this.dependencies = [ ]; + this.rdependency = { }; + + this.options = L.defaults(options, { + placeholder: '', + datatype: 'string', + optional: false, + keep: true + }); + }, + + id: function(sid) + { + return this.ownerSection.id('field', sid || '__unknown__', this.name); + }, + + render: function(sid, condensed) + { + var i = this.instance[sid] = { }; + + i.top = $('<div />') + .addClass('luci2-field'); + + if (!condensed) + { + i.top.addClass('form-group'); + + if (typeof(this.options.caption) == 'string') + $('<label />') + .addClass('col-lg-2 control-label') + .attr('for', this.id(sid)) + .text(this.options.caption) + .appendTo(i.top); + } + + i.error = $('<div />') + .hide() + .addClass('luci2-field-error label label-danger'); + + i.widget = $('<div />') + .addClass('luci2-field-widget') + .append(this.widget(sid)) + .append(i.error) + .appendTo(i.top); + + if (!condensed) + { + i.widget.addClass('col-lg-5'); + + $('<div />') + .addClass('col-lg-5') + .text((typeof(this.options.description) == 'string') ? this.options.description : '') + .appendTo(i.top); + } + + return i.top; + }, + + active: function(sid) + { + return (this.instance[sid] && !this.instance[sid].disabled); + }, + + ucipath: function(sid) + { + return { + config: (this.options.uci_package || this.ownerMap.uci_package), + section: (this.options.uci_section || sid), + option: (this.options.uci_option || this.name) + }; + }, + + ucivalue: function(sid) + { + var uci = this.ucipath(sid); + var val = this.ownerMap.get(uci.config, uci.section, uci.option); + + if (typeof(val) == 'undefined') + return this.options.initial; + + return val; + }, + + formvalue: function(sid) + { + var v = $('#' + this.id(sid)).val(); + return (v === '') ? undefined : v; + }, + + textvalue: function(sid) + { + var v = this.formvalue(sid); + + if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length)) + v = this.ucivalue(sid); + + if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length)) + v = this.options.placeholder; + + if (typeof(v) == 'undefined' || v === '') + return undefined; + + if (typeof(v) == 'string' && $.isArray(this.choices)) + { + for (var i = 0; i < this.choices.length; i++) + if (v === this.choices[i][0]) + return this.choices[i][1]; + } + else if (v === true) + return L.tr('yes'); + else if (v === false) + return L.tr('no'); + else if ($.isArray(v)) + return v.join(', '); + + return v; + }, + + changed: function(sid) + { + var a = this.ucivalue(sid); + var b = this.formvalue(sid); + + if (typeof(a) != typeof(b)) + return true; + + if ($.isArray(a)) + { + if (a.length != b.length) + return true; + + for (var i = 0; i < a.length; i++) + if (a[i] != b[i]) + return true; + + return false; + } + else if ($.isPlainObject(a)) + { + for (var k in a) + if (!(k in b)) + return true; + + for (var k in b) + if (!(k in a) || a[k] !== b[k]) + return true; + + return false; + } + + return (a != b); + }, + + save: function(sid) + { + var uci = this.ucipath(sid); + + if (this.instance[sid].disabled) + { + if (!this.options.keep) + return this.ownerMap.set(uci.config, uci.section, uci.option, undefined); + + return false; + } + + var chg = this.changed(sid); + var val = this.formvalue(sid); + + if (chg) + this.ownerMap.set(uci.config, uci.section, uci.option, val); + + return chg; + }, + + findSectionID: function($elem) + { + return this.ownerSection.findParentSectionIDs($elem)[0]; + }, + + setError: function($elem, msg, msgargs) + { + var $field = $elem.parents('.luci2-field:first'); + var $error = $field.find('.luci2-field-error:first'); + + if (typeof(msg) == 'string' && msg.length > 0) + { + $field.addClass('luci2-form-error'); + $elem.parent().addClass('has-error'); + + $error.text(msg.format.apply(msg, msgargs)).show(); + $field.trigger('validate'); + + return false; + } + else + { + $elem.parent().removeClass('has-error'); + + var $other_errors = $field.find('.has-error'); + if ($other_errors.length == 0) + { + $field.removeClass('luci2-form-error'); + $error.text('').hide(); + $field.trigger('validate'); + + return true; + } + + return false; + } + }, + + handleValidate: function(ev) + { + var $elem = $(this); + + var d = ev.data; + var rv = true; + var val = $elem.val(); + var vstack = d.vstack; + + if (vstack && typeof(vstack[0]) == 'function') + { + delete validation.message; + + if ((val.length == 0 && !d.opt)) + { + rv = d.self.setError($elem, L.tr('Field must not be empty')); + } + else if (val.length > 0 && !vstack[0].apply(val, vstack[1])) + { + rv = d.self.setError($elem, validation.message, vstack[1]); + } + else + { + rv = d.self.setError($elem); + } + } + + if (rv) + { + var sid = d.self.findSectionID($elem); + + for (var field in d.self.rdependency) + { + d.self.rdependency[field].toggle(sid); + d.self.rdependency[field].validate(sid); + } + + d.self.ownerSection.tabtoggle(sid); + } + + return rv; + }, + + attachEvents: function(sid, elem) + { + var evdata = { + self: this, + opt: this.options.optional + }; + + if (this.events) + for (var evname in this.events) + elem.on(evname, evdata, this.events[evname]); + + if (typeof(this.options.datatype) == 'undefined' && $.isEmptyObject(this.rdependency)) + return elem; + + var vstack; + if (typeof(this.options.datatype) == 'string') + { + try { + evdata.vstack = L.cbi.validation.compile(this.options.datatype); + } catch(e) { }; + } + else if (typeof(this.options.datatype) == 'function') + { + var vfunc = this.options.datatype; + evdata.vstack = [ function(elem) { + var rv = vfunc(this, elem); + if (rv !== true) + validation.message = rv; + return (rv === true); + }, [ elem ] ]; + } + + if (elem.prop('tagName') == 'SELECT') + { + elem.change(evdata, this.handleValidate); + } + else if (elem.prop('tagName') == 'INPUT' && elem.attr('type') == 'checkbox') + { + elem.click(evdata, this.handleValidate); + elem.blur(evdata, this.handleValidate); + } + else + { + elem.keyup(evdata, this.handleValidate); + elem.blur(evdata, this.handleValidate); + } + + elem.addClass('luci2-field-validate') + .on('validate', evdata, this.handleValidate); + + return elem; + }, + + validate: function(sid) + { + var i = this.instance[sid]; + + i.widget.find('.luci2-field-validate').trigger('validate'); + + return (i.disabled || i.error.text() == ''); + }, + + depends: function(d, v, add) + { + var dep; + + if ($.isArray(d)) + { + dep = { }; + for (var i = 0; i < d.length; i++) + { + if (typeof(d[i]) == 'string') + dep[d[i]] = true; + else if (d[i] instanceof L.cbi.AbstractValue) + dep[d[i].name] = true; + } + } + else if (d instanceof L.cbi.AbstractValue) + { + dep = { }; + dep[d.name] = (typeof(v) == 'undefined') ? true : v; + } + else if (typeof(d) == 'object') + { + dep = d; + } + else if (typeof(d) == 'string') + { + dep = { }; + dep[d] = (typeof(v) == 'undefined') ? true : v; + } + + if (!dep || $.isEmptyObject(dep)) + return this; + + for (var field in dep) + { + var f = this.ownerSection.fields[field]; + if (f) + f.rdependency[this.name] = this; + else + delete dep[field]; + } + + if ($.isEmptyObject(dep)) + return this; + + if (!add || !this.dependencies.length) + this.dependencies.push(dep); + else + for (var i = 0; i < this.dependencies.length; i++) + $.extend(this.dependencies[i], dep); + + return this; + }, + + toggle: function(sid) + { + var d = this.dependencies; + var i = this.instance[sid]; + + if (!d.length) + return true; + + for (var n = 0; n < d.length; n++) + { + var rv = true; + + for (var field in d[n]) + { + var val = this.ownerSection.fields[field].formvalue(sid); + var cmp = d[n][field]; + + if (typeof(cmp) == 'boolean') + { + if (cmp == (typeof(val) == 'undefined' || val === '' || val === false)) + { + rv = false; + break; + } + } + else if (typeof(cmp) == 'string' || typeof(cmp) == 'number') + { + if (val != cmp) + { + rv = false; + break; + } + } + else if (typeof(cmp) == 'function') + { + if (!cmp(val)) + { + rv = false; + break; + } + } + else if (cmp instanceof RegExp) + { + if (!cmp.test(val)) + { + rv = false; + break; + } + } + } + + if (rv) + { + if (i.disabled) + { + i.disabled = false; + i.top.removeClass('luci2-field-disabled'); + i.top.fadeIn(); + } + + return true; + } + } + + if (!i.disabled) + { + i.disabled = true; + i.top.is(':visible') ? i.top.fadeOut() : i.top.hide(); + i.top.addClass('luci2-field-disabled'); + } + + return false; + } + }); + + cbi_class.CheckboxValue = cbi_class.AbstractValue.extend({ + widget: function(sid) + { + var o = this.options; + + if (typeof(o.enabled) == 'undefined') o.enabled = '1'; + if (typeof(o.disabled) == 'undefined') o.disabled = '0'; + + var i = $('<input />') + .attr('id', this.id(sid)) + .attr('type', 'checkbox') + .prop('checked', this.ucivalue(sid)); + + return $('<div />') + .addClass('checkbox') + .append(this.attachEvents(sid, i)); + }, + + ucivalue: function(sid) + { + var v = this.callSuper('ucivalue', sid); + + if (typeof(v) == 'boolean') + return v; + + return (v == this.options.enabled); + }, + + formvalue: function(sid) + { + var v = $('#' + this.id(sid)).prop('checked'); + + if (typeof(v) == 'undefined') + return !!this.options.initial; + + return v; + }, + + save: function(sid) + { + var uci = this.ucipath(sid); + + if (this.instance[sid].disabled) + { + if (!this.options.keep) + return this.ownerMap.set(uci.config, uci.section, uci.option, undefined); + + return false; + } + + var chg = this.changed(sid); + var val = this.formvalue(sid); + + if (chg) + { + if (this.options.optional && val == this.options.initial) + this.ownerMap.set(uci.config, uci.section, uci.option, undefined); + else + this.ownerMap.set(uci.config, uci.section, uci.option, val ? this.options.enabled : this.options.disabled); + } + + return chg; + } + }); + + cbi_class.InputValue = cbi_class.AbstractValue.extend({ + widget: function(sid) + { + var i = $('<input />') + .addClass('form-control') + .attr('id', this.id(sid)) + .attr('type', 'text') + .attr('placeholder', this.options.placeholder) + .val(this.ucivalue(sid)); + + return this.attachEvents(sid, i); + } + }); + + cbi_class.PasswordValue = cbi_class.AbstractValue.extend({ + widget: function(sid) + { + var i = $('<input />') + .addClass('form-control') + .attr('id', this.id(sid)) + .attr('type', 'password') + .attr('placeholder', this.options.placeholder) + .val(this.ucivalue(sid)); + + var t = $('<span />') + .addClass('input-group-btn') + .append(L.ui.button(L.tr('Reveal'), 'default') + .click(function(ev) { + var b = $(this); + var i = b.parent().prev(); + var t = i.attr('type'); + b.text(t == 'password' ? L.tr('Hide') : L.tr('Reveal')); + i.attr('type', (t == 'password') ? 'text' : 'password'); + b = i = t = null; + })); + + this.attachEvents(sid, i); + + return $('<div />') + .addClass('input-group') + .append(i) + .append(t); + } + }); + + cbi_class.ListValue = cbi_class.AbstractValue.extend({ + widget: function(sid) + { + var s = $('<select />') + .addClass('form-control'); + + if (this.options.optional && !this.has_empty) + $('<option />') + .attr('value', '') + .text(L.tr('-- Please choose --')) + .appendTo(s); + + if (this.choices) + for (var i = 0; i < this.choices.length; i++) + $('<option />') + .attr('value', this.choices[i][0]) + .text(this.choices[i][1]) + .appendTo(s); + + s.attr('id', this.id(sid)).val(this.ucivalue(sid)); + + return this.attachEvents(sid, s); + }, + + value: function(k, v) + { + if (!this.choices) + this.choices = [ ]; + + if (k == '') + this.has_empty = true; + + this.choices.push([k, v || k]); + return this; + } + }); + + cbi_class.MultiValue = cbi_class.ListValue.extend({ + widget: function(sid) + { + var v = this.ucivalue(sid); + var t = $('<div />').attr('id', this.id(sid)); + + if (!$.isArray(v)) + v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ]; + + var s = { }; + for (var i = 0; i < v.length; i++) + s[v[i]] = true; + + if (this.choices) + for (var i = 0; i < this.choices.length; i++) + { + $('<label />') + .addClass('checkbox') + .append($('<input />') + .attr('type', 'checkbox') + .attr('value', this.choices[i][0]) + .prop('checked', s[this.choices[i][0]])) + .append(this.choices[i][1]) + .appendTo(t); + } + + return t; + }, + + formvalue: function(sid) + { + var rv = [ ]; + var fields = $('#' + this.id(sid) + ' > label > input'); + + for (var i = 0; i < fields.length; i++) + if (fields[i].checked) + rv.push(fields[i].getAttribute('value')); + + return rv; + }, + + textvalue: function(sid) + { + var v = this.formvalue(sid); + var c = { }; + + if (this.choices) + for (var i = 0; i < this.choices.length; i++) + c[this.choices[i][0]] = this.choices[i][1]; + + var t = [ ]; + + for (var i = 0; i < v.length; i++) + t.push(c[v[i]] || v[i]); + + return t.join(', '); + } + }); + + cbi_class.ComboBox = cbi_class.AbstractValue.extend({ + _change: function(ev) + { + var s = ev.target; + var self = ev.data.self; + + if (s.selectedIndex == (s.options.length - 1)) + { + ev.data.select.hide(); + ev.data.input.show().focus(); + ev.data.input.val(''); + } + else if (self.options.optional && s.selectedIndex == 0) + { + ev.data.input.val(''); + } + else + { + ev.data.input.val(ev.data.select.val()); + } + + ev.stopPropagation(); + }, + + _blur: function(ev) + { + var seen = false; + var val = this.value; + var self = ev.data.self; + + ev.data.select.empty(); + + if (self.options.optional && !self.has_empty) + $('<option />') + .attr('value', '') + .text(L.tr('-- please choose --')) + .appendTo(ev.data.select); + + if (self.choices) + for (var i = 0; i < self.choices.length; i++) + { + if (self.choices[i][0] == val) + seen = true; + + $('<option />') + .attr('value', self.choices[i][0]) + .text(self.choices[i][1]) + .appendTo(ev.data.select); + } + + if (!seen && val != '') + $('<option />') + .attr('value', val) + .text(val) + .appendTo(ev.data.select); + + $('<option />') + .attr('value', ' ') + .text(L.tr('-- custom --')) + .appendTo(ev.data.select); + + ev.data.input.hide(); + ev.data.select.val(val).show().blur(); + }, + + _enter: function(ev) + { + if (ev.which != 13) + return true; + + ev.preventDefault(); + ev.data.self._blur(ev); + return false; + }, + + widget: function(sid) + { + var d = $('<div />') + .attr('id', this.id(sid)); + + var t = $('<input />') + .addClass('form-control') + .attr('type', 'text') + .hide() + .appendTo(d); + + var s = $('<select />') + .addClass('form-control') + .appendTo(d); + + var evdata = { + self: this, + input: t, + select: s + }; + + s.change(evdata, this._change); + t.blur(evdata, this._blur); + t.keydown(evdata, this._enter); + + t.val(this.ucivalue(sid)); + t.blur(); + + this.attachEvents(sid, t); + this.attachEvents(sid, s); + + return d; + }, + + value: function(k, v) + { + if (!this.choices) + this.choices = [ ]; + + if (k == '') + this.has_empty = true; + + this.choices.push([k, v || k]); + return this; + }, + + formvalue: function(sid) + { + var v = $('#' + this.id(sid)).children('input').val(); + return (v == '') ? undefined : v; + } + }); + + cbi_class.DynamicList = cbi_class.ComboBox.extend({ + _redraw: function(focus, add, del, s) + { + var v = s.values || [ ]; + delete s.values; + + $(s.parent).children('div.input-group').children('input').each(function(i) { + if (i != del) + v.push(this.value || ''); + }); + + $(s.parent).empty(); + + if (add >= 0) + { + focus = add + 1; + v.splice(focus, 0, ''); + } + else if (v.length == 0) + { + focus = 0; + v.push(''); + } + + for (var i = 0; i < v.length; i++) + { + var evdata = { + sid: s.sid, + self: s.self, + parent: s.parent, + index: i, + remove: ((i+1) < v.length) + }; + + var btn; + if (evdata.remove) + btn = L.ui.button('–', 'danger').click(evdata, this._btnclick); + else + btn = L.ui.button('+', 'success').click(evdata, this._btnclick); + + if (this.choices) + { + var txt = $('<input />') + .addClass('form-control') + .attr('type', 'text') + .hide(); + + var sel = $('<select />') + .addClass('form-control'); + + $('<div />') + .addClass('input-group') + .append(txt) + .append(sel) + .append($('<span />') + .addClass('input-group-btn') + .append(btn)) + .appendTo(s.parent); + + evdata.input = this.attachEvents(s.sid, txt); + evdata.select = this.attachEvents(s.sid, sel); + + sel.change(evdata, this._change); + txt.blur(evdata, this._blur); + txt.keydown(evdata, this._keydown); + + txt.val(v[i]); + txt.blur(); + + if (i == focus || -(i+1) == focus) + sel.focus(); + + sel = txt = null; + } + else + { + var f = $('<input />') + .attr('type', 'text') + .attr('index', i) + .attr('placeholder', (i == 0) ? this.options.placeholder : '') + .addClass('form-control') + .keydown(evdata, this._keydown) + .keypress(evdata, this._keypress) + .val(v[i]); + + $('<div />') + .addClass('input-group') + .append(f) + .append($('<span />') + .addClass('input-group-btn') + .append(btn)) + .appendTo(s.parent); + + if (i == focus) + { + f.focus(); + } + else if (-(i+1) == focus) + { + f.focus(); + + /* force cursor to end */ + var val = f.val(); + f.val(' '); + f.val(val); + } + + evdata.input = this.attachEvents(s.sid, f); + + f = null; + } + + evdata = null; + } + + s = null; + }, + + _keypress: function(ev) + { + switch (ev.which) + { + /* backspace, delete */ + case 8: + case 46: + if (ev.data.input.val() == '') + { + ev.preventDefault(); + return false; + } + + return true; + + /* enter, arrow up, arrow down */ + case 13: + case 38: + case 40: + ev.preventDefault(); + return false; + } + + return true; + }, + + _keydown: function(ev) + { + var input = ev.data.input; + + switch (ev.which) + { + /* backspace, delete */ + case 8: + case 46: + if (input.val().length == 0) + { + ev.preventDefault(); + + var index = ev.data.index; + var focus = index; + + if (ev.which == 8) + focus = -focus; + + ev.data.self._redraw(focus, -1, index, ev.data); + return false; + } + + break; + + /* enter */ + case 13: + ev.data.self._redraw(NaN, ev.data.index, -1, ev.data); + break; + + /* arrow up */ + case 38: + var prev = input.parent().prevAll('div.input-group:first').children('input'); + if (prev.is(':visible')) + prev.focus(); + else + prev.next('select').focus(); + break; + + /* arrow down */ + case 40: + var next = input.parent().nextAll('div.input-group:first').children('input'); + if (next.is(':visible')) + next.focus(); + else + next.next('select').focus(); + break; + } + + return true; + }, + + _btnclick: function(ev) + { + if (!this.getAttribute('disabled')) + { + if (ev.data.remove) + { + var index = ev.data.index; + ev.data.self._redraw(-index, -1, index, ev.data); + } + else + { + ev.data.self._redraw(NaN, ev.data.index, -1, ev.data); + } + } + + return false; + }, + + widget: function(sid) + { + this.options.optional = true; + + var v = this.ucivalue(sid); + + if (!$.isArray(v)) + v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ]; + + var d = $('<div />') + .attr('id', this.id(sid)) + .addClass('cbi-input-dynlist'); + + this._redraw(NaN, -1, -1, { + self: this, + parent: d[0], + values: v, + sid: sid + }); + + return d; + }, + + ucivalue: function(sid) + { + var v = this.callSuper('ucivalue', sid); + + if (!$.isArray(v)) + v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ]; + + return v; + }, + + formvalue: function(sid) + { + var rv = [ ]; + var fields = $('#' + this.id(sid) + ' input'); + + for (var i = 0; i < fields.length; i++) + if (typeof(fields[i].value) == 'string' && fields[i].value.length) + rv.push(fields[i].value); + + return rv; + } + }); + + cbi_class.DummyValue = cbi_class.AbstractValue.extend({ + widget: function(sid) + { + return $('<div />') + .addClass('form-control-static') + .attr('id', this.id(sid)) + .html(this.ucivalue(sid) || this.label('placeholder')); + }, + + formvalue: function(sid) + { + return this.ucivalue(sid); + } + }); + + cbi_class.ButtonValue = cbi_class.AbstractValue.extend({ + widget: function(sid) + { + this.options.optional = true; + + var btn = $('<button />') + .addClass('btn btn-default') + .attr('id', this.id(sid)) + .attr('type', 'button') + .text(this.label('text')); + + return this.attachEvents(sid, btn); + } + }); + + cbi_class.NetworkList = cbi_class.AbstractValue.extend({ + load: function(sid) + { + return L.network.load(); + }, + + _device_icon: function(dev) + { + return $('<img />') + .attr('src', dev.icon()) + .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?')); + }, + + widget: function(sid) + { + var id = this.id(sid); + var ul = $('<ul />') + .attr('id', id) + .addClass('list-unstyled'); + + var itype = this.options.multiple ? 'checkbox' : 'radio'; + var value = this.ucivalue(sid); + var check = { }; + + if (!this.options.multiple) + check[value] = true; + else + for (var i = 0; i < value.length; i++) + check[value[i]] = true; + + var interfaces = L.network.getInterfaces(); + + for (var i = 0; i < interfaces.length; i++) + { + var iface = interfaces[i]; + + $('<li />') + .append($('<label />') + .addClass(itype + ' inline') + .append(this.attachEvents(sid, $('<input />') + .attr('name', itype + id) + .attr('type', itype) + .attr('value', iface.name()) + .prop('checked', !!check[iface.name()]))) + .append(iface.renderBadge())) + .appendTo(ul); + } + + if (!this.options.multiple) + { + $('<li />') + .append($('<label />') + .addClass(itype + ' inline text-muted') + .append(this.attachEvents(sid, $('<input />') + .attr('name', itype + id) + .attr('type', itype) + .attr('value', '') + .prop('checked', $.isEmptyObject(check)))) + .append(L.tr('unspecified'))) + .appendTo(ul); + } + + return ul; + }, + + ucivalue: function(sid) + { + var v = this.callSuper('ucivalue', sid); + + if (!this.options.multiple) + { + if ($.isArray(v)) + { + return v[0]; + } + else if (typeof(v) == 'string') + { + v = v.match(/\S+/); + return v ? v[0] : undefined; + } + + return v; + } + else + { + if (typeof(v) == 'string') + v = v.match(/\S+/g); + + return v || [ ]; + } + }, + + formvalue: function(sid) + { + var inputs = $('#' + this.id(sid) + ' input'); + + if (!this.options.multiple) + { + for (var i = 0; i < inputs.length; i++) + if (inputs[i].checked && inputs[i].value !== '') + return inputs[i].value; + + return undefined; + } + + var rv = [ ]; + + for (var i = 0; i < inputs.length; i++) + if (inputs[i].checked) + rv.push(inputs[i].value); + + return rv.length ? rv : undefined; + } + }); + + cbi_class.DeviceList = cbi_class.NetworkList.extend({ + handleFocus: function(ev) + { + var self = ev.data.self; + var input = $(this); + + input.parent().prev().prop('checked', true); + }, + + handleBlur: function(ev) + { + ev.which = 10; + ev.data.self.handleKeydown.call(this, ev); + }, + + handleKeydown: function(ev) + { + if (ev.which != 10 && ev.which != 13) + return; + + var sid = ev.data.sid; + var self = ev.data.self; + var input = $(this); + var ifnames = L.toArray(input.val()); + + if (!ifnames.length) + return; + + L.network.createDevice(ifnames[0]); + + self._redraw(sid, $('#' + self.id(sid)), ifnames[0]); + }, + + load: function(sid) + { + return L.network.load(); + }, + + _redraw: function(sid, ul, sel) + { + var id = ul.attr('id'); + var devs = L.network.getDevices(); + var iface = L.network.getInterface(sid); + var itype = this.options.multiple ? 'checkbox' : 'radio'; + var check = { }; + + if (!sel) + { + for (var i = 0; i < devs.length; i++) + if (devs[i].isInNetwork(iface)) + check[devs[i].name()] = true; + } + else + { + if (this.options.multiple) + check = L.toObject(this.formvalue(sid)); + + check[sel] = true; + } + + ul.empty(); + + for (var i = 0; i < devs.length; i++) + { + var dev = devs[i]; + + if (dev.isBridge() && this.options.bridges === false) + continue; + + if (!dev.isBridgeable() && this.options.multiple) + continue; + + var badge = $('<span />') + .addClass('badge') + .append($('<img />').attr('src', dev.icon())) + .append(' %s: %s'.format(dev.name(), dev.description())); + + //var ifcs = dev.getInterfaces(); + //if (ifcs.length) + //{ + // for (var j = 0; j < ifcs.length; j++) + // badge.append((j ? ', ' : ' (') + ifcs[j].name()); + // + // badge.append(')'); + //} + + $('<li />') + .append($('<label />') + .addClass(itype + ' inline') + .append($('<input />') + .attr('name', itype + id) + .attr('type', itype) + .attr('value', dev.name()) + .prop('checked', !!check[dev.name()])) + .append(badge)) + .appendTo(ul); + } + + + $('<li />') + .append($('<label />') + .attr('for', 'custom' + id) + .addClass(itype + ' inline') + .append($('<input />') + .attr('name', itype + id) + .attr('type', itype) + .attr('value', '')) + .append($('<span />') + .addClass('badge') + .append($('<input />') + .attr('id', 'custom' + id) + .attr('type', 'text') + .attr('placeholder', L.tr('Custom device …')) + .on('focus', { self: this, sid: sid }, this.handleFocus) + .on('blur', { self: this, sid: sid }, this.handleBlur) + .on('keydown', { self: this, sid: sid }, this.handleKeydown)))) + .appendTo(ul); + + if (!this.options.multiple) + { + $('<li />') + .append($('<label />') + .addClass(itype + ' inline text-muted') + .append($('<input />') + .attr('name', itype + id) + .attr('type', itype) + .attr('value', '') + .prop('checked', $.isEmptyObject(check))) + .append(L.tr('unspecified'))) + .appendTo(ul); + } + }, + + widget: function(sid) + { + var id = this.id(sid); + var ul = $('<ul />') + .attr('id', id) + .addClass('list-unstyled'); + + this._redraw(sid, ul); + + return ul; + }, + + save: function(sid) + { + if (this.instance[sid].disabled) + return; + + var ifnames = this.formvalue(sid); + //if (!ifnames) + // return; + + var iface = L.network.getInterface(sid); + if (!iface) + return; + + iface.setDevices($.isArray(ifnames) ? ifnames : [ ifnames ]); + } + }); + + + cbi_class.AbstractSection = L.ui.AbstractWidget.extend({ + id: function() + { + var s = [ arguments[0], this.ownerMap.uci_package, this.uci_type ]; + + for (var i = 1; i < arguments.length && typeof(arguments[i]) == 'string'; i++) + s.push(arguments[i].replace(/\./g, '_')); + + return s.join('_'); + }, + + option: function(widget, name, options) + { + if (this.tabs.length == 0) + this.tab({ id: '__default__', selected: true }); + + return this.taboption('__default__', widget, name, options); + }, + + tab: function(options) + { + if (options.selected) + this.tabs.selected = this.tabs.length; + + this.tabs.push({ + id: options.id, + caption: options.caption, + description: options.description, + fields: [ ], + li: { } + }); + }, + + taboption: function(tabid, widget, name, options) + { + var tab; + for (var i = 0; i < this.tabs.length; i++) + { + if (this.tabs[i].id == tabid) + { + tab = this.tabs[i]; + break; + } + } + + if (!tab) + throw 'Cannot append to unknown tab ' + tabid; + + var w = widget ? new widget(name, options) : null; + + if (!(w instanceof L.cbi.AbstractValue)) + throw 'Widget must be an instance of AbstractValue'; + + w.ownerSection = this; + w.ownerMap = this.ownerMap; + + this.fields[name] = w; + tab.fields.push(w); + + return w; + }, + + tabtoggle: function(sid) + { + for (var i = 0; i < this.tabs.length; i++) + { + var tab = this.tabs[i]; + var elem = $('#' + this.id('nodetab', sid, tab.id)); + var empty = true; + + for (var j = 0; j < tab.fields.length; j++) + { + if (tab.fields[j].active(sid)) + { + empty = false; + break; + } + } + + if (empty && elem.is(':visible')) + elem.fadeOut(); + else if (!empty) + elem.fadeIn(); + } + }, + + validate: function(parent_sid) + { + var s = this.getUCISections(parent_sid); + var n = 0; + + for (var i = 0; i < s.length; i++) + { + var $item = $('#' + this.id('sectionitem', s[i]['.name'])); + + $item.find('.luci2-field-validate').trigger('validate'); + n += $item.find('.luci2-field.luci2-form-error').not('.luci2-field-disabled').length; + } + + return (n == 0); + }, + + load: function(parent_sid) + { + var deferreds = [ ]; + + var s = this.getUCISections(parent_sid); + for (var i = 0; i < s.length; i++) + { + for (var f in this.fields) + { + if (typeof(this.fields[f].load) != 'function') + continue; + + var rv = this.fields[f].load(s[i]['.name']); + if (L.isDeferred(rv)) + deferreds.push(rv); + } + + for (var j = 0; j < this.subsections.length; j++) + { + var rv = this.subsections[j].load(s[i]['.name']); + deferreds.push.apply(deferreds, rv); + } + } + + return deferreds; + }, + + save: function(parent_sid) + { + var deferreds = [ ]; + var s = this.getUCISections(parent_sid); + + for (i = 0; i < s.length; i++) + { + if (!this.options.readonly) + { + for (var f in this.fields) + { + if (typeof(this.fields[f].save) != 'function') + continue; + + var rv = this.fields[f].save(s[i]['.name']); + if (L.isDeferred(rv)) + deferreds.push(rv); + } + } + + for (var j = 0; j < this.subsections.length; j++) + { + var rv = this.subsections[j].save(s[i]['.name']); + deferreds.push.apply(deferreds, rv); + } + } + + return deferreds; + }, + + teaser: function(sid) + { + var tf = this.teaser_fields; + + if (!tf) + { + tf = this.teaser_fields = [ ]; + + if ($.isArray(this.options.teasers)) + { + for (var i = 0; i < this.options.teasers.length; i++) + { + var f = this.options.teasers[i]; + if (f instanceof L.cbi.AbstractValue) + tf.push(f); + else if (typeof(f) == 'string' && this.fields[f] instanceof L.cbi.AbstractValue) + tf.push(this.fields[f]); + } + } + else + { + for (var i = 0; tf.length <= 5 && i < this.tabs.length; i++) + for (var j = 0; tf.length <= 5 && j < this.tabs[i].fields.length; j++) + tf.push(this.tabs[i].fields[j]); + } + } + + var t = ''; + + for (var i = 0; i < tf.length; i++) + { + if (tf[i].instance[sid] && tf[i].instance[sid].disabled) + continue; + + var n = tf[i].options.caption || tf[i].name; + var v = tf[i].textvalue(sid); + + if (typeof(v) == 'undefined') + continue; + + t = t + '%s%s: <strong>%s</strong>'.format(t ? ' | ' : '', n, v); + } + + return t; + }, + + findAdditionalUCIPackages: function() + { + var packages = [ ]; + + for (var i = 0; i < this.tabs.length; i++) + for (var j = 0; j < this.tabs[i].fields.length; j++) + if (this.tabs[i].fields[j].options.uci_package) + packages.push(this.tabs[i].fields[j].options.uci_package); + + return packages; + }, + + findParentSectionIDs: function($elem) + { + var rv = [ ]; + var $parents = $elem.parents('.luci2-section-item'); + + for (var i = 0; i < $parents.length; i++) + rv.push($parents[i].getAttribute('data-luci2-sid')); + + return rv; + } + }); + + cbi_class.TypedSection = cbi_class.AbstractSection.extend({ + init: function(uci_type, options) + { + this.uci_type = uci_type; + this.options = options; + this.tabs = [ ]; + this.fields = { }; + this.subsections = [ ]; + this.active_panel = { }; + this.active_tab = { }; + + this.instance = { }; + }, + + filter: function(section, parent_sid) + { + return true; + }, + + sort: function(section1, section2) + { + return 0; + }, + + subsection: function(widget, uci_type, options) + { + var w = widget ? new widget(uci_type, options) : null; + + if (!(w instanceof L.cbi.AbstractSection)) + throw 'Widget must be an instance of AbstractSection'; + + w.ownerSection = this; + w.ownerMap = this.ownerMap; + w.index = this.subsections.length; + + this.subsections.push(w); + return w; + }, + + getUCISections: function(parent_sid) + { + var s1 = L.uci.sections(this.ownerMap.uci_package); + var s2 = [ ]; + + for (var i = 0; i < s1.length; i++) + if (s1[i]['.type'] == this.uci_type) + if (this.filter(s1[i], parent_sid)) + s2.push(s1[i]); + + s2.sort(this.sort); + + return s2; + }, + + add: function(name, parent_sid) + { + return this.ownerMap.add(this.ownerMap.uci_package, this.uci_type, name); + }, + + remove: function(sid, parent_sid) + { + return this.ownerMap.remove(this.ownerMap.uci_package, sid); + }, + + handleAdd: function(ev) + { + var addb = $(this); + var name = undefined; + var self = ev.data.self; + var sid = self.findParentSectionIDs(addb)[0]; + + if (addb.prev().prop('nodeName') == 'INPUT') + name = addb.prev().val(); + + if (addb.prop('disabled') || name === '') + return; + + L.ui.saveScrollTop(); + + self.setPanelIndex(sid, -1); + self.ownerMap.save(); + + ev.data.sid = self.add(name, sid); + ev.data.type = self.uci_type; + ev.data.name = name; + + self.trigger('add', ev); + + self.ownerMap.redraw(); + + L.ui.restoreScrollTop(); + }, + + handleRemove: function(ev) + { + var self = ev.data.self; + var sids = self.findParentSectionIDs($(this)); + + if (sids.length) + { + L.ui.saveScrollTop(); + + ev.sid = sids[0]; + ev.parent_sid = sids[1]; + + self.trigger('remove', ev); + + self.ownerMap.save(); + self.remove(ev.sid, ev.parent_sid); + self.ownerMap.redraw(); + + L.ui.restoreScrollTop(); + } + + ev.stopPropagation(); + }, + + handleSID: function(ev) + { + var self = ev.data.self; + var text = $(this); + var addb = text.next(); + var errt = addb.next(); + var name = text.val(); + + if (!/^[a-zA-Z0-9_]*$/.test(name)) + { + errt.text(L.tr('Invalid section name')).show(); + text.addClass('error'); + addb.prop('disabled', true); + return false; + } + + if (L.uci.get(self.ownerMap.uci_package, name)) + { + errt.text(L.tr('Name already used')).show(); + text.addClass('error'); + addb.prop('disabled', true); + return false; + } + + errt.text('').hide(); + text.removeClass('error'); + addb.prop('disabled', false); + return true; + }, + + handleTab: function(ev) + { + var self = ev.data.self; + var $tab = $(this); + var sid = self.findParentSectionIDs($tab)[0]; + + self.active_tab[sid] = $tab.parent().index(); + }, + + handleTabValidate: function(ev) + { + var $pane = $(ev.delegateTarget); + var $badge = $pane.parent() + .children('.nav-tabs') + .children('li') + .eq($pane.index() - 1) // item #1 is the <ul> + .find('.badge:first'); + + var err_count = $pane.find('.luci2-field.luci2-form-error').not('.luci2-field-disabled').length; + if (err_count > 0) + $badge + .text(err_count) + .attr('title', L.trp('1 Error', '%d Errors', err_count).format(err_count)) + .show(); + else + $badge.hide(); + }, + + handlePanelValidate: function(ev) + { + var $elem = $(this); + var $badge = $elem + .prevAll('.luci2-section-header:first') + .children('.luci2-section-teaser') + .find('.badge:first'); + + var err_count = $elem.find('.luci2-field.luci2-form-error').not('.luci2-field-disabled').length; + if (err_count > 0) + $badge + .text(err_count) + .attr('title', L.trp('1 Error', '%d Errors', err_count).format(err_count)) + .show(); + else + $badge.hide(); + }, + + handlePanelCollapse: function(ev) + { + var self = ev.data.self; + + var $items = $(ev.delegateTarget).children('.luci2-section-item'); + + var $this_panel = $(ev.target); + var $this_teaser = $this_panel.prevAll('.luci2-section-header:first').children('.luci2-section-teaser'); + + var $prev_panel = $items.children('.luci2-section-panel.in'); + var $prev_teaser = $prev_panel.prevAll('.luci2-section-header:first').children('.luci2-section-teaser'); + + var sids = self.findParentSectionIDs($prev_panel); + + self.setPanelIndex(sids[1], $this_panel.parent().index()); + + $prev_panel + .removeClass('in') + .addClass('collapse'); + + $prev_teaser + .show() + .children('span:last') + .empty() + .append(self.teaser(sids[0])); + + $this_teaser + .hide(); + + ev.stopPropagation(); + }, + + handleSort: function(ev) + { + var self = ev.data.self; + + var $item = $(this).parents('.luci2-section-item:first'); + var $next = ev.data.up ? $item.prev() : $item.next(); + + if ($item.length && $next.length) + { + var cur_sid = $item.attr('data-luci2-sid'); + var new_sid = $next.attr('data-luci2-sid'); + + L.uci.swap(self.ownerMap.uci_package, cur_sid, new_sid); + + self.ownerMap.save(); + self.ownerMap.redraw(); + } + + ev.stopPropagation(); + }, + + getPanelIndex: function(parent_sid) + { + return (this.active_panel[parent_sid || '__top__'] || 0); + }, + + setPanelIndex: function(parent_sid, new_index) + { + if (typeof(new_index) == 'number') + this.active_panel[parent_sid || '__top__'] = new_index; + }, + + renderAdd: function() + { + if (!this.options.addremove) + return null; + + var text = L.tr('Add section'); + var ttip = L.tr('Create new section...'); + + if ($.isArray(this.options.add_caption)) + text = this.options.add_caption[0], ttip = this.options.add_caption[1]; + else if (typeof(this.options.add_caption) == 'string') + text = this.options.add_caption, ttip = ''; + + var add = $('<div />'); + + if (this.options.anonymous === false) + { + $('<input />') + .addClass('cbi-input-text') + .attr('type', 'text') + .attr('placeholder', ttip) + .blur({ self: this }, this.handleSID) + .keyup({ self: this }, this.handleSID) + .appendTo(add); + + $('<img />') + .attr('src', L.globals.resource + '/icons/cbi/add.gif') + .attr('title', text) + .addClass('cbi-button') + .click({ self: this }, this.handleAdd) + .appendTo(add); + + $('<div />') + .addClass('cbi-value-error') + .hide() + .appendTo(add); + } + else + { + L.ui.button(text, 'success', ttip) + .click({ self: this }, this.handleAdd) + .appendTo(add); + } + + return add; + }, + + renderRemove: function(index) + { + if (!this.options.addremove) + return null; + + var text = L.tr('Remove'); + var ttip = L.tr('Remove this section'); + + if ($.isArray(this.options.remove_caption)) + text = this.options.remove_caption[0], ttip = this.options.remove_caption[1]; + else if (typeof(this.options.remove_caption) == 'string') + text = this.options.remove_caption, ttip = ''; + + return L.ui.button(text, 'danger', ttip) + .click({ self: this, index: index }, this.handleRemove); + }, + + renderSort: function(index) + { + if (!this.options.sortable) + return null; + + var b1 = L.ui.button('↑', 'info', L.tr('Move up')) + .click({ self: this, index: index, up: true }, this.handleSort); + + var b2 = L.ui.button('↓', 'info', L.tr('Move down')) + .click({ self: this, index: index, up: false }, this.handleSort); + + return b1.add(b2); + }, + + renderCaption: function() + { + return $('<h3 />') + .addClass('panel-title') + .append(this.label('caption') || this.uci_type); + }, + + renderDescription: function() + { + var text = this.label('description'); + + if (text) + return $('<div />') + .addClass('luci2-section-description') + .text(text); + + return null; + }, + + renderTeaser: function(sid, index) + { + if (this.options.collabsible || this.ownerMap.options.collabsible) + { + return $('<div />') + .attr('id', this.id('teaser', sid)) + .addClass('luci2-section-teaser well well-sm') + .append($('<span />') + .addClass('badge')) + .append($('<span />')); + } + + return null; + }, + + renderHead: function(condensed) + { + if (condensed) + return null; + + return $('<div />') + .addClass('panel-heading') + .append(this.renderCaption()) + .append(this.renderDescription()); + }, + + renderTabDescription: function(sid, index, tab_index) + { + var tab = this.tabs[tab_index]; + + if (typeof(tab.description) == 'string') + { + return $('<div />') + .addClass('cbi-tab-descr') + .text(tab.description); + } + + return null; + }, + + renderTabHead: function(sid, index, tab_index) + { + var tab = this.tabs[tab_index]; + var cur = this.active_tab[sid] || 0; + + var tabh = $('<li />') + .append($('<a />') + .attr('id', this.id('nodetab', sid, tab.id)) + .attr('href', '#' + this.id('node', sid, tab.id)) + .attr('data-toggle', 'tab') + .text((tab.caption ? tab.caption.format(tab.id) : tab.id) + ' ') + .append($('<span />') + .addClass('badge')) + .on('shown.bs.tab', { self: this, sid: sid }, this.handleTab)); + + if (cur == tab_index) + tabh.addClass('active'); + + if (!tab.fields.length) + tabh.hide(); + + return tabh; + }, + + renderTabBody: function(sid, index, tab_index) + { + var tab = this.tabs[tab_index]; + var cur = this.active_tab[sid] || 0; + + var tabb = $('<div />') + .addClass('tab-pane') + .attr('id', this.id('node', sid, tab.id)) + .append(this.renderTabDescription(sid, index, tab_index)) + .on('validate', this.handleTabValidate); + + if (cur == tab_index) + tabb.addClass('active'); + + for (var i = 0; i < tab.fields.length; i++) + tabb.append(tab.fields[i].render(sid)); + + return tabb; + }, + + renderPanelHead: function(sid, index, parent_sid) + { + var head = $('<div />') + .addClass('luci2-section-header') + .append(this.renderTeaser(sid, index)) + .append($('<div />') + .addClass('btn-group') + .append(this.renderSort(index)) + .append(this.renderRemove(index))); + + if (this.options.collabsible) + { + head.attr('data-toggle', 'collapse') + .attr('data-parent', this.id('sectiongroup', parent_sid)) + .attr('data-target', '#' + this.id('panel', sid)); + } + + return head; + }, + + renderPanelBody: function(sid, index, parent_sid) + { + var body = $('<div />') + .attr('id', this.id('panel', sid)) + .addClass('luci2-section-panel') + .on('validate', this.handlePanelValidate); + + if (this.options.collabsible || this.ownerMap.options.collabsible) + { + body.addClass('panel-collapse collapse'); + + if (index == this.getPanelIndex(parent_sid)) + body.addClass('in'); + } + + var tab_heads = $('<ul />') + .addClass('nav nav-tabs'); + + var tab_bodies = $('<div />') + .addClass('form-horizontal tab-content') + .append(tab_heads); + + for (var j = 0; j < this.tabs.length; j++) + { + tab_heads.append(this.renderTabHead(sid, index, j)); + tab_bodies.append(this.renderTabBody(sid, index, j)); + } + + body.append(tab_bodies); + + if (this.tabs.length <= 1) + tab_heads.hide(); + + for (var i = 0; i < this.subsections.length; i++) + body.append(this.subsections[i].render(false, sid)); + + return body; + }, + + renderBody: function(condensed, parent_sid) + { + var s = this.getUCISections(parent_sid); + var n = this.getPanelIndex(parent_sid); + + if (n < 0) + this.setPanelIndex(parent_sid, n + s.length); + else if (n >= s.length) + this.setPanelIndex(parent_sid, s.length - 1); + + var body = $('<ul />') + .addClass('luci2-section-group list-group'); + + if (this.options.collabsible) + { + body.attr('id', this.id('sectiongroup', parent_sid)) + .on('show.bs.collapse', { self: this }, this.handlePanelCollapse); + } + + if (s.length == 0) + { + body.append($('<li />') + .addClass('list-group-item text-muted') + .text(this.label('placeholder') || L.tr('There are no entries defined yet.'))) + } + + for (var i = 0; i < s.length; i++) + { + var sid = s[i]['.name']; + var inst = this.instance[sid] = { tabs: [ ] }; + + body.append($('<li />') + .addClass('luci2-section-item list-group-item') + .attr('id', this.id('sectionitem', sid)) + .attr('data-luci2-sid', sid) + .append(this.renderPanelHead(sid, i, parent_sid)) + .append(this.renderPanelBody(sid, i, parent_sid))); + } + + return body; + }, + + render: function(condensed, parent_sid) + { + this.instance = { }; + + var panel = $('<div />') + .addClass('panel panel-default') + .append(this.renderHead(condensed)) + .append(this.renderBody(condensed, parent_sid)); + + if (this.options.addremove) + panel.append($('<div />') + .addClass('panel-footer') + .append(this.renderAdd())); + + return panel; + }, + + finish: function(parent_sid) + { + var s = this.getUCISections(parent_sid); + + for (var i = 0; i < s.length; i++) + { + var sid = s[i]['.name']; + + if (i != this.getPanelIndex(parent_sid)) + $('#' + this.id('teaser', sid)).children('span:last') + .append(this.teaser(sid)); + else + $('#' + this.id('teaser', sid)) + .hide(); + + for (var j = 0; j < this.subsections.length; j++) + this.subsections[j].finish(sid); + } + } + }); + + cbi_class.TableSection = cbi_class.TypedSection.extend({ + renderTableHead: function() + { + var thead = $('<thead />') + .append($('<tr />') + .addClass('cbi-section-table-titles')); + + for (var j = 0; j < this.tabs[0].fields.length; j++) + thead.children().append($('<th />') + .addClass('cbi-section-table-cell') + .css('width', this.tabs[0].fields[j].options.width || '') + .append(this.tabs[0].fields[j].label('caption'))); + + if (this.options.addremove !== false || this.options.sortable) + thead.children().append($('<th />') + .addClass('cbi-section-table-cell') + .text(' ')); + + return thead; + }, + + renderTableRow: function(sid, index) + { + var row = $('<tr />') + .addClass('luci2-section-item') + .attr('id', this.id('sectionitem', sid)) + .attr('data-luci2-sid', sid); + + for (var j = 0; j < this.tabs[0].fields.length; j++) + { + row.append($('<td />') + .css('width', this.tabs[0].fields[j].options.width || '') + .append(this.tabs[0].fields[j].render(sid, true))); + } + + if (this.options.addremove !== false || this.options.sortable) + { + row.append($('<td />') + .css('width', '1%') + .addClass('text-right') + .append($('<div />') + .addClass('btn-group') + .append(this.renderSort(index)) + .append(this.renderRemove(index)))); + } + + return row; + }, + + renderTableBody: function(parent_sid) + { + var s = this.getUCISections(parent_sid); + + var tbody = $('<tbody />'); + + if (s.length == 0) + { + var cols = this.tabs[0].fields.length; + + if (this.options.addremove !== false || this.options.sortable) + cols++; + + tbody.append($('<tr />') + .append($('<td />') + .addClass('text-muted') + .attr('colspan', cols) + .text(this.label('placeholder') || L.tr('There are no entries defined yet.')))); + } + + for (var i = 0; i < s.length; i++) + { + var sid = s[i]['.name']; + var inst = this.instance[sid] = { tabs: [ ] }; + + tbody.append(this.renderTableRow(sid, i)); + } + + return tbody; + }, + + renderBody: function(condensed, parent_sid) + { + return $('<table />') + .addClass('table table-condensed table-hover') + .append(this.renderTableHead()) + .append(this.renderTableBody(parent_sid)); + } + }); + + cbi_class.NamedSection = cbi_class.TypedSection.extend({ + getUCISections: function(cb) + { + var sa = [ ]; + var sl = L.uci.sections(this.ownerMap.uci_package); + + for (var i = 0; i < sl.length; i++) + if (sl[i]['.name'] == this.uci_type) + { + sa.push(sl[i]); + break; + } + + if (typeof(cb) == 'function' && sa.length > 0) + cb.call(this, sa[0]); + + return sa; + } + }); + + cbi_class.SingleSection = cbi_class.NamedSection.extend({ + render: function() + { + this.instance = { }; + this.instance[this.uci_type] = { tabs: [ ] }; + + return $('<div />') + .addClass('luci2-section-item') + .attr('id', this.id('sectionitem', this.uci_type)) + .attr('data-luci2-sid', this.uci_type) + .append(this.renderPanelBody(this.uci_type, 0)); + } + }); + + cbi_class.DummySection = cbi_class.TypedSection.extend({ + getUCISections: function(cb) + { + if (typeof(cb) == 'function') + cb.apply(this, [ { '.name': this.uci_type } ]); + + return [ { '.name': this.uci_type } ]; + } + }); + + cbi_class.Map = L.ui.AbstractWidget.extend({ + init: function(uci_package, options) + { + var self = this; + + this.uci_package = uci_package; + this.sections = [ ]; + this.options = L.defaults(options, { + save: function() { }, + prepare: function() { } + }); + }, + + loadCallback: function() + { + var deferreds = [ L.deferrable(this.options.prepare.call(this)) ]; + + for (var i = 0; i < this.sections.length; i++) + { + var rv = this.sections[i].load(); + deferreds.push.apply(deferreds, rv); + } + + return $.when.apply($, deferreds); + }, + + load: function() + { + var self = this; + var packages = [ this.uci_package ]; + + for (var i = 0; i < this.sections.length; i++) + packages.push.apply(packages, this.sections[i].findAdditionalUCIPackages()); + + for (var i = 0; i < packages.length; i++) + if (!L.uci.writable(packages[i])) + { + this.options.readonly = true; + break; + } + + return L.uci.load(packages).then(function() { + return self.loadCallback(); + }); + }, + + handleTab: function(ev) + { + ev.data.self.active_tab = $(ev.target).parent().index(); + }, + + handleApply: function(ev) + { + var self = ev.data.self; + + self.trigger('apply', ev); + }, + + handleSave: function(ev) + { + var self = ev.data.self; + + self.send().then(function() { + self.trigger('save', ev); + }); + }, + + handleReset: function(ev) + { + var self = ev.data.self; + + self.trigger('reset', ev); + self.reset(); + }, + + renderTabHead: function(tab_index) + { + var section = this.sections[tab_index]; + var cur = this.active_tab || 0; + + var tabh = $('<li />') + .append($('<a />') + .attr('id', section.id('sectiontab')) + .attr('href', '#' + section.id('section')) + .attr('data-toggle', 'tab') + .text(section.label('caption') + ' ') + .append($('<span />') + .addClass('badge')) + .on('shown.bs.tab', { self: this }, this.handleTab)); + + if (cur == tab_index) + tabh.addClass('active'); + + return tabh; + }, + + renderTabBody: function(tab_index) + { + var section = this.sections[tab_index]; + var desc = section.label('description'); + var cur = this.active_tab || 0; + + var tabb = $('<div />') + .addClass('tab-pane') + .attr('id', section.id('section')); + + if (cur == tab_index) + tabb.addClass('active'); + + if (desc) + tabb.append($('<p />') + .text(desc)); + + var s = section.render(this.options.tabbed); + + if (this.options.readonly || section.options.readonly) + s.find('input, select, button, img.cbi-button').attr('disabled', true); + + tabb.append(s); + + return tabb; + }, + + renderBody: function() + { + var tabs = $('<ul />') + .addClass('nav nav-tabs'); + + var body = $('<div />') + .append(tabs); + + for (var i = 0; i < this.sections.length; i++) + { + tabs.append(this.renderTabHead(i)); + body.append(this.renderTabBody(i)); + } + + if (this.options.tabbed) + body.addClass('tab-content'); + else + tabs.hide(); + + return body; + }, + + renderFooter: function() + { + var evdata = { + self: this + }; + + return $('<div />') + .addClass('panel panel-default panel-body text-right') + .append($('<div />') + .addClass('btn-group') + .append(L.ui.button(L.tr('Save & Apply'), 'primary') + .click(evdata, this.handleApply)) + .append(L.ui.button(L.tr('Save'), 'default') + .click(evdata, this.handleSave)) + .append(L.ui.button(L.tr('Reset'), 'default') + .click(evdata, this.handleReset))); + }, + + render: function() + { + var map = $('<form />'); + + if (typeof(this.options.caption) == 'string') + map.append($('<h2 />') + .text(this.options.caption)); + + if (typeof(this.options.description) == 'string') + map.append($('<p />') + .text(this.options.description)); + + map.append(this.renderBody()); + + if (this.options.pageaction !== false) + map.append(this.renderFooter()); + + return map; + }, + + finish: function() + { + for (var i = 0; i < this.sections.length; i++) + this.sections[i].finish(); + + this.validate(); + }, + + redraw: function() + { + this.target.hide().empty().append(this.render()); + this.finish(); + this.target.show(); + }, + + section: function(widget, uci_type, options) + { + var w = widget ? new widget(uci_type, options) : null; + + if (!(w instanceof L.cbi.AbstractSection)) + throw 'Widget must be an instance of AbstractSection'; + + w.ownerMap = this; + w.index = this.sections.length; + + this.sections.push(w); + return w; + }, + + add: function(conf, type, name) + { + return L.uci.add(conf, type, name); + }, + + remove: function(conf, sid) + { + return L.uci.remove(conf, sid); + }, + + get: function(conf, sid, opt) + { + return L.uci.get(conf, sid, opt); + }, + + set: function(conf, sid, opt, val) + { + return L.uci.set(conf, sid, opt, val); + }, + + validate: function() + { + var rv = true; + + for (var i = 0; i < this.sections.length; i++) + { + if (!this.sections[i].validate()) + rv = false; + } + + return rv; + }, + + save: function() + { + var self = this; + + if (self.options.readonly) + return L.deferrable(); + + var deferreds = [ ]; + + for (var i = 0; i < self.sections.length; i++) + { + var rv = self.sections[i].save(); + deferreds.push.apply(deferreds, rv); + } + + return $.when.apply($, deferreds).then(function() { + return L.deferrable(self.options.save.call(self)); + }); + }, + + send: function() + { + if (!this.validate()) + return L.deferrable(); + + var self = this; + + L.ui.saveScrollTop(); + L.ui.loading(true); + + return this.save().then(function() { + return L.uci.save(); + }).then(function() { + return L.ui.updateChanges(); + }).then(function() { + return self.load(); + }).then(function() { + self.redraw(); + self = null; + + L.ui.loading(false); + L.ui.restoreScrollTop(); + }); + }, + + revert: function() + { + var packages = [ this.uci_package ]; + + for (var i = 0; i < this.sections.length; i++) + packages.push.apply(packages, this.sections[i].findAdditionalUCIPackages()); + + L.uci.unload(packages); + }, + + reset: function() + { + var self = this; + + self.revert(); + + return self.insertInto(self.target); + }, + + insertInto: function(id) + { + var self = this; + self.target = $(id); + + L.ui.loading(true); + self.target.hide(); + + return self.load().then(function() { + self.target.empty().append(self.render()); + self.finish(); + self.target.show(); + self = null; + L.ui.loading(false); + }); + } + }); + + cbi_class.Modal = cbi_class.Map.extend({ + handleApply: function(ev) + { + var self = ev.data.self; + + self.trigger('apply', ev); + }, + + handleSave: function(ev) + { + var self = ev.data.self; + + self.send().then(function() { + self.trigger('save', ev); + self.close(); + }); + }, + + handleReset: function(ev) + { + var self = ev.data.self; + + self.trigger('close', ev); + self.revert(); + self.close(); + }, + + renderFooter: function() + { + var evdata = { + self: this + }; + + return $('<div />') + .addClass('btn-group') + .append(L.ui.button(L.tr('Save & Apply'), 'primary') + .click(evdata, this.handleApply)) + .append(L.ui.button(L.tr('Save'), 'default') + .click(evdata, this.handleSave)) + .append(L.ui.button(L.tr('Cancel'), 'default') + .click(evdata, this.handleReset)); + }, + + render: function() + { + var modal = L.ui.dialog(this.label('caption'), null, { wide: true }); + var map = $('<form />'); + + var desc = this.label('description'); + if (desc) + map.append($('<p />').text(desc)); + + map.append(this.renderBody()); + + modal.find('.modal-body').append(map); + modal.find('.modal-footer').append(this.renderFooter()); + + return modal; + }, + + redraw: function() + { + this.render(); + this.finish(); + }, + + show: function() + { + var self = this; + + L.ui.loading(true); + + return self.load().then(function() { + self.render(); + self.finish(); + + L.ui.loading(false); + }); + }, + + close: function() + { + L.ui.dialog(false); + } + }); + + return Class.extend(cbi_class); +})(); diff --git a/luci2/htdocs/luci2/firewall.js b/luci2/htdocs/luci2/firewall.js new file mode 100644 index 0000000..7bd0190 --- /dev/null +++ b/luci2/htdocs/luci2/firewall.js @@ -0,0 +1,50 @@ +Class.extend({ + getZoneColor: function(zone) + { + if ($.isPlainObject(zone)) + zone = zone.name; + + if (zone == 'lan') + return '#90f090'; + else if (zone == 'wan') + return '#f09090'; + + for (var i = 0, hash = 0; + i < zone.length; + hash = zone.charCodeAt(i++) + ((hash << 5) - hash)); + + for (var i = 0, color = '#'; + i < 3; + color += ('00' + ((hash >> i++ * 8) & 0xFF).tostring(16)).slice(-2)); + + return color; + }, + + findZoneByNetwork: function(network) + { + var self = this; + var zone = undefined; + + return L.uci.sections('firewall', 'zone', function(z) { + if (!z.name || !z.network) + return; + + if (!$.isArray(z.network)) + z.network = z.network.split(/\s+/); + + for (var i = 0; i < z.network.length; i++) + { + if (z.network[i] == network) + { + zone = z; + break; + } + } + }).then(function() { + if (zone) + zone.color = self.getZoneColor(zone); + + return zone; + }); + } +}); diff --git a/luci2/htdocs/luci2/luci2.js b/luci2/htdocs/luci2/luci2.js index 432248c..3e5af4a 100644 --- a/luci2/htdocs/luci2/luci2.js +++ b/luci2/htdocs/luci2/luci2.js @@ -693,7031 +693,13 @@ function LuCI2() sid: '00000000000000000000000000000000' }; - this.rpc = { - - _id: 1, - _batch: undefined, - _requests: { }, - - _call: function(req, cb) - { - return $.ajax('/ubus', { - cache: false, - contentType: 'application/json', - data: JSON.stringify(req), - dataType: 'json', - type: 'POST', - timeout: L.globals.timeout, - _rpc_req: req - }).then(cb, cb); - }, - - _list_cb: function(msg) - { - var list = msg.result; - - /* verify message frame */ - if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id || !$.isArray(list)) - list = [ ]; - - return $.Deferred().resolveWith(this, [ list ]); - }, - - _call_cb: function(msg) - { - var data = [ ]; - var type = Object.prototype.toString; - var reqs = this._rpc_req; - - if (!$.isArray(reqs)) - { - msg = [ msg ]; - reqs = [ reqs ]; - } - - for (var i = 0; i < msg.length; i++) - { - /* fetch related request info */ - var req = L.rpc._requests[reqs[i].id]; - if (typeof(req) != 'object') - throw 'No related request for JSON response'; - - /* fetch response attribute and verify returned type */ - var ret = undefined; - - /* verify message frame */ - if (typeof(msg[i]) == 'object' && msg[i].jsonrpc == '2.0') - if ($.isArray(msg[i].result) && msg[i].result[0] == 0) - ret = (msg[i].result.length > 1) ? msg[i].result[1] : msg[i].result[0]; - - if (req.expect) - { - for (var key in req.expect) - { - if (typeof(ret) != 'undefined' && key != '') - ret = ret[key]; - - if (typeof(ret) == 'undefined' || 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(L.rpc, req.priv); - } - - /* store response data */ - if (typeof(req.index) == 'number') - data[req.index] = ret; - else - data = ret; - - /* delete request object */ - delete L.rpc._requests[reqs[i].id]; - } - - return $.Deferred().resolveWith(this, [ data ]); - }, - - list: function() - { - var params = [ ]; - for (var i = 0; i < arguments.length; i++) - params[i] = arguments[i]; - - var msg = { - jsonrpc: '2.0', - id: this._id++, - method: 'list', - params: (params.length > 0) ? params : undefined - }; - - return this._call(msg, this._list_cb); - }, - - batch: function() - { - if (!$.isArray(this._batch)) - this._batch = [ ]; - }, - - flush: function() - { - if (!$.isArray(this._batch)) - return L.deferrable([ ]); - - var req = this._batch; - delete this._batch; - - /* call rpc */ - return this._call(req, this._call_cb); - }, - - declare: function(options) - { - var _rpc = this; - - return function() { - /* build parameter object */ - var p_off = 0; - var params = { }; - if ($.isArray(options.params)) - for (p_off = 0; p_off < options.params.length; p_off++) - params[options.params[p_off]] = arguments[p_off]; - - /* all remaining arguments are private args */ - var priv = [ undefined, undefined ]; - for (; p_off < arguments.length; p_off++) - priv.push(arguments[p_off]); - - /* store request info */ - var req = _rpc._requests[_rpc._id] = { - expect: options.expect, - filter: options.filter, - params: params, - priv: priv - }; - - /* build message object */ - var msg = { - jsonrpc: '2.0', - id: _rpc._id++, - method: 'call', - params: [ - L.globals.sid, - options.object, - options.method, - params - ] - }; - - /* when a batch is in progress then store index in request data - * and push message object onto the stack */ - if ($.isArray(_rpc._batch)) - { - req.index = _rpc._batch.push(msg) - 1; - return L.deferrable(msg); - } - - /* call rpc */ - return _rpc._call(msg, _rpc._call_cb); - }; - } - }; - - this.UCIContext = Class.extend({ - - init: function() - { - this.state = { - newidx: 0, - values: { }, - creates: { }, - changes: { }, - deletes: { }, - reorder: { } - }; - }, - - callLoad: L.rpc.declare({ - object: 'uci', - method: 'get', - params: [ 'config' ], - expect: { values: { } } - }), - - callOrder: L.rpc.declare({ - object: 'uci', - method: 'order', - params: [ 'config', 'sections' ] - }), - - callAdd: L.rpc.declare({ - object: 'uci', - method: 'add', - params: [ 'config', 'type', 'name', 'values' ], - expect: { section: '' } - }), - - callSet: L.rpc.declare({ - object: 'uci', - method: 'set', - params: [ 'config', 'section', 'values' ] - }), - - callDelete: L.rpc.declare({ - object: 'uci', - method: 'delete', - params: [ 'config', 'section', 'options' ] - }), - - callApply: L.rpc.declare({ - object: 'uci', - method: 'apply', - params: [ 'timeout', 'rollback' ] - }), - - callConfirm: L.rpc.declare({ - object: 'uci', - method: 'confirm' - }), - - createSID: function(conf) - { - var v = this.state.values; - var n = this.state.creates; - var sid; - - do { - sid = "new%06x".format(Math.random() * 0xFFFFFF); - } while ((n[conf] && n[conf][sid]) || (v[conf] && v[conf][sid])); - - return sid; - }, - - reorderSections: function() - { - var v = this.state.values; - var n = this.state.creates; - var r = this.state.reorder; - - if ($.isEmptyObject(r)) - return L.deferrable(); - - L.rpc.batch(); - - /* - 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']); - - this.callOrder(c, sids); - } - } - - this.state.reorder = { }; - return L.rpc.flush(); - }, - - load: function(packages) - { - var self = this; - var seen = { }; - var pkgs = [ ]; - - if (!$.isArray(packages)) - packages = [ packages ]; - - L.rpc.batch(); - - for (var i = 0; i < packages.length; i++) - if (!seen[packages[i]] && !self.state.values[packages[i]]) - { - pkgs.push(packages[i]); - seen[packages[i]] = true; - self.callLoad(packages[i]); - } - - return L.rpc.flush().then(function(responses) { - for (var i = 0; i < responses.length; i++) - self.state.values[pkgs[i]] = responses[i]; - - return pkgs; - }); - }, - - unload: function(packages) - { - if (!$.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]]; - } - }, - - add: function(conf, type, name) - { - var n = this.state.creates; - var 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; - var c = this.state.changes; - var 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 = [ ]; - var v = this.state.values[conf]; - var n = this.state.creates[conf]; - var c = this.state.changes[conf]; - var 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($.extend({ }, v[s], c ? c[s] : undefined)); - - if (n) - for (var s in n) - if (!type || n[s]['.type'] == type) - sa.push(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; - var n = this.state.creates; - var c = this.state.changes; - var d = this.state.deletes; - - if (typeof(sid) == 'undefined') - return undefined; - - /* requested option in a just created section */ - if (n[conf] && n[conf][sid]) - { - if (!n[conf]) - return undefined; - - if (typeof(opt) == 'undefined') - return n[conf][sid]; - - return n[conf][sid][opt]; - } - - /* requested an option value */ - if (typeof(opt) != 'undefined') - { - /* 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] && typeof(c[conf][sid][opt]) != 'undefined') - 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; - var n = this.state.creates; - var c = this.state.changes; - var d = this.state.deletes; - - if (typeof(sid) == 'undefined' || - typeof(opt) == 'undefined' || - opt.charAt(0) == '.') - return; - - if (n[conf] && n[conf][sid]) - { - if (typeof(val) != 'undefined') - n[conf][sid][opt] = val; - else - delete n[conf][sid][opt]; - } - else if (typeof(val) != 'undefined') - { - /* 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] = L.filterArray(d[conf][sid], opt); - - c[conf][sid][opt] = val; - } - else - { - /* only delete in existing sections */ - if (!v[conf] || !v[conf][sid]) - 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, undefined); - }, - - get_first: function(conf, type, opt) - { - var sid = undefined; - - L.uci.sections(conf, type, function(s) { - if (typeof(sid) != 'string') - sid = s['.name']; - }); - - return this.get(conf, sid, opt); - }, - - set_first: function(conf, type, opt, val) - { - var sid = undefined; - - L.uci.sections(conf, type, function(s) { - if (typeof(sid) != 'string') - sid = s['.name']; - }); - - return this.set(conf, sid, opt, val); - }, - - unset_first: function(conf, type, opt) - { - return this.set_first(conf, type, opt, undefined); - }, - - swap: function(conf, sid1, sid2) - { - var s1 = this.get(conf, sid1); - var s2 = this.get(conf, sid2); - var n1 = s1 ? s1['.index'] : NaN; - var n2 = s2 ? s2['.index'] : NaN; - - if (isNaN(n1) || isNaN(n2)) - return false; - - s1['.index'] = n2; - s2['.index'] = n1; - - this.state.reorder[conf] = true; - - return true; - }, - - save: function() - { - L.rpc.batch(); - - var v = this.state.values; - var n = this.state.creates; - var c = this.state.changes; - var d = this.state.deletes; - - var self = this; - var snew = [ ]; - var pkgs = { }; - - 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]); - - 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]) - 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]; - self.callDelete(conf, sid, (o === true) ? undefined : o); - } - - pkgs[conf] = true; - } - - return L.rpc.flush().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 = L.toArray(pkgs); - - self.unload(pkgs); - - return self.load(pkgs); - }); - }, - - apply: function(timeout) - { - var self = this; - var date = new Date(); - var deferred = $.Deferred(); - - if (typeof(timeout) != 'number' || timeout < 1) - timeout = 10; - - self.callApply(timeout, true).then(function(rv) { - if (rv != 0) - { - deferred.rejectWith(self, [ rv ]); - return; - } - - 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 - deferred.rejectWith(self, [ rv ]); - - return; - } - - deferred.resolveWith(self, [ rv ]); - }); - }; - - window.setTimeout(try_confirm, 1000); - }); - - return deferred; - }, - - changes: L.rpc.declare({ - object: 'uci', - method: 'changes', - expect: { changes: { } } - }), - - readable: function(conf) - { - return L.session.hasACL('uci', conf, 'read'); - }, - - writable: function(conf) - { - return L.session.hasACL('uci', conf, 'write'); - } - }); - - this.uci = new this.UCIContext(); - - this.wireless = { - listDeviceNames: L.rpc.declare({ - object: 'iwinfo', - method: 'devices', - expect: { 'devices': [ ] }, - filter: function(data) { - data.sort(); - return data; - } - }), - - getDeviceStatus: L.rpc.declare({ - object: 'iwinfo', - method: 'info', - params: [ 'device' ], - expect: { '': { } }, - filter: function(data, params) { - if (!$.isEmptyObject(data)) - { - data['device'] = params['device']; - return data; - } - return undefined; - } - }), - - getAssocList: L.rpc.declare({ - object: 'iwinfo', - method: 'assoclist', - params: [ 'device' ], - expect: { results: [ ] }, - filter: function(data, params) { - for (var i = 0; i < data.length; i++) - data[i]['device'] = params['device']; - - data.sort(function(a, b) { - if (a.bssid < b.bssid) - return -1; - else if (a.bssid > b.bssid) - return 1; - else - return 0; - }); - - return data; - } - }), - - getWirelessStatus: function() { - return this.listDeviceNames().then(function(names) { - L.rpc.batch(); - - for (var i = 0; i < names.length; i++) - L.wireless.getDeviceStatus(names[i]); - - return L.rpc.flush(); - }).then(function(networks) { - var rv = { }; - - var phy_attrs = [ - 'country', 'channel', 'frequency', 'frequency_offset', - 'txpower', 'txpower_offset', 'hwmodes', 'hardware', 'phy' - ]; - - var net_attrs = [ - 'ssid', 'bssid', 'mode', 'quality', 'quality_max', - 'signal', 'noise', 'bitrate', 'encryption' - ]; - - for (var i = 0; i < networks.length; i++) - { - var phy = rv[networks[i].phy] || ( - rv[networks[i].phy] = { networks: [ ] } - ); - - var net = { - device: networks[i].device - }; - - for (var j = 0; j < phy_attrs.length; j++) - phy[phy_attrs[j]] = networks[i][phy_attrs[j]]; - - for (var j = 0; j < net_attrs.length; j++) - net[net_attrs[j]] = networks[i][net_attrs[j]]; - - phy.networks.push(net); - } - - return rv; - }); - }, - - getAssocLists: function() - { - return this.listDeviceNames().then(function(names) { - L.rpc.batch(); - - for (var i = 0; i < names.length; i++) - L.wireless.getAssocList(names[i]); - - return L.rpc.flush(); - }).then(function(assoclists) { - var rv = [ ]; - - for (var i = 0; i < assoclists.length; i++) - for (var j = 0; j < assoclists[i].length; j++) - rv.push(assoclists[i][j]); - - return rv; - }); - }, - - formatEncryption: function(enc) - { - var format_list = function(l, s) - { - var rv = [ ]; - for (var i = 0; i < l.length; i++) - rv.push(l[i].toUpperCase()); - return rv.join(s ? s : ', '); - } - - if (!enc || !enc.enabled) - return L.tr('None'); - - if (enc.wep) - { - if (enc.wep.length == 2) - return L.tr('WEP Open/Shared') + ' (%s)'.format(format_list(enc.ciphers, ', ')); - else if (enc.wep[0] == 'shared') - return L.tr('WEP Shared Auth') + ' (%s)'.format(format_list(enc.ciphers, ', ')); - else - return L.tr('WEP Open System') + ' (%s)'.format(format_list(enc.ciphers, ', ')); - } - else if (enc.wpa) - { - if (enc.wpa.length == 2) - return L.tr('mixed WPA/WPA2') + ' %s (%s)'.format( - format_list(enc.authentication, '/'), - format_list(enc.ciphers, ', ') - ); - else if (enc.wpa[0] == 2) - return 'WPA2 %s (%s)'.format( - format_list(enc.authentication, '/'), - format_list(enc.ciphers, ', ') - ); - else - return 'WPA %s (%s)'.format( - format_list(enc.authentication, '/'), - format_list(enc.ciphers, ', ') - ); - } - - return L.tr('Unknown'); - } - }; - - this.firewall = { - getZoneColor: function(zone) - { - if ($.isPlainObject(zone)) - zone = zone.name; - - if (zone == 'lan') - return '#90f090'; - else if (zone == 'wan') - return '#f09090'; - - for (var i = 0, hash = 0; - i < zone.length; - hash = zone.charCodeAt(i++) + ((hash << 5) - hash)); - - for (var i = 0, color = '#'; - i < 3; - color += ('00' + ((hash >> i++ * 8) & 0xFF).tostring(16)).slice(-2)); - - return color; - }, - - findZoneByNetwork: function(network) - { - var self = this; - var zone = undefined; - - return L.uci.sections('firewall', 'zone', function(z) { - if (!z.name || !z.network) - return; - - if (!$.isArray(z.network)) - z.network = z.network.split(/\s+/); - - for (var i = 0; i < z.network.length; i++) - { - if (z.network[i] == network) - { - zone = z; - break; - } - } - }).then(function() { - if (zone) - zone.color = self.getZoneColor(zone); - - return zone; - }); - } - }; - - this.NetworkModel = { - deviceBlacklist: [ - /^gre[0-9]+$/, - /^gretap[0-9]+$/, - /^ifb[0-9]+$/, - /^ip6tnl[0-9]+$/, - /^sit[0-9]+$/, - /^wlan[0-9]+\.sta[0-9]+$/ - ], - - rpcCacheFunctions: [ - 'protolist', 0, L.rpc.declare({ - object: 'network', - method: 'get_proto_handlers', - expect: { '': { } } - }), - 'ifstate', 1, L.rpc.declare({ - object: 'network.interface', - method: 'dump', - expect: { 'interface': [ ] } - }), - 'devstate', 2, L.rpc.declare({ - object: 'network.device', - method: 'status', - expect: { '': { } } - }), - 'wifistate', 0, L.rpc.declare({ - object: 'network.wireless', - method: 'status', - expect: { '': { } } - }), - 'bwstate', 2, L.rpc.declare({ - object: 'luci2.network.bwmon', - method: 'statistics', - expect: { 'statistics': { } } - }), - 'devlist', 2, L.rpc.declare({ - object: 'luci2.network', - method: 'device_list', - expect: { 'devices': [ ] } - }), - 'swlist', 0, L.rpc.declare({ - object: 'luci2.network', - method: 'switch_list', - expect: { 'switches': [ ] } - }) - ], - - loadProtocolHandler: function(proto) - { - var url = L.globals.resource + '/proto/' + proto + '.js'; - var self = L.NetworkModel; - - var def = $.Deferred(); - - $.ajax(url, { - method: 'GET', - cache: true, - dataType: 'text' - }).then(function(data) { - try { - var protoConstructorSource = ( - '(function(L, $) { ' + - 'return %s' + - '})(L, $);\n\n' + - '//@ sourceURL=%s' - ).format(data, url); - - var protoClass = eval(protoConstructorSource); - - self.protocolHandlers[proto] = new protoClass(); - } - catch(e) { - alert('Unable to instantiate proto "%s": %s'.format(url, e)); - }; - - def.resolve(); - }).fail(function() { - def.resolve(); - }); - - return def; - }, - - loadProtocolHandlers: function() - { - var self = L.NetworkModel; - var deferreds = [ - self.loadProtocolHandler('none') - ]; - - for (var proto in self.rpcCache.protolist) - deferreds.push(self.loadProtocolHandler(proto)); - - return $.when.apply($, deferreds); - }, - - callSwitchInfo: L.rpc.declare({ - object: 'luci2.network', - method: 'switch_info', - params: [ 'switch' ], - expect: { 'info': { } } - }), - - callSwitchInfoCallback: function(responses) { - var self = L.NetworkModel; - var swlist = self.rpcCache.swlist; - var swstate = self.rpcCache.swstate = { }; - - for (var i = 0; i < responses.length; i++) - swstate[swlist[i]] = responses[i]; - }, - - loadCacheCallback: function(level) - { - var self = L.NetworkModel; - var name = '_fetch_cache_cb_' + level; - - return self[name] || ( - self[name] = function(responses) - { - for (var i = 0; i < self.rpcCacheFunctions.length; i += 3) - if (!level || self.rpcCacheFunctions[i + 1] == level) - self.rpcCache[self.rpcCacheFunctions[i]] = responses.shift(); - - if (!level) - { - L.rpc.batch(); - - for (var i = 0; i < self.rpcCache.swlist.length; i++) - self.callSwitchInfo(self.rpcCache.swlist[i]); - - return L.rpc.flush().then(self.callSwitchInfoCallback); - } - - return L.deferrable(); - } - ); - }, - - loadCache: function(level) - { - var self = L.NetworkModel; - - return L.uci.load(['network', 'wireless']).then(function() { - L.rpc.batch(); - - for (var i = 0; i < self.rpcCacheFunctions.length; i += 3) - if (!level || self.rpcCacheFunctions[i + 1] == level) - self.rpcCacheFunctions[i + 2](); - - return L.rpc.flush().then(self.loadCacheCallback(level || 0)); - }); - }, - - isBlacklistedDevice: function(dev) - { - for (var i = 0; i < this.deviceBlacklist.length; i++) - if (dev.match(this.deviceBlacklist[i])) - return true; - - return false; - }, - - sortDevicesCallback: function(a, b) - { - if (a.options.kind < b.options.kind) - return -1; - else if (a.options.kind > b.options.kind) - return 1; - - if (a.options.name < b.options.name) - return -1; - else if (a.options.name > b.options.name) - return 1; - - return 0; - }, - - getDeviceObject: function(ifname) - { - var alias = (ifname.charAt(0) == '@'); - return this.deviceObjects[ifname] || ( - this.deviceObjects[ifname] = { - ifname: ifname, - kind: alias ? 'alias' : 'ethernet', - type: alias ? 0 : 1, - up: false, - changed: { } - } - ); - }, - - getInterfaceObject: function(name) - { - return this.interfaceObjects[name] || ( - this.interfaceObjects[name] = { - name: name, - proto: this.protocolHandlers.none, - changed: { } - } - ); - }, - - loadDevicesCallback: function() - { - var self = L.NetworkModel; - var wificount = { }; - - for (var ifname in self.rpcCache.devstate) - { - if (self.isBlacklistedDevice(ifname)) - continue; - - var dev = self.rpcCache.devstate[ifname]; - var entry = self.getDeviceObject(ifname); - - entry.up = dev.up; - - switch (dev.type) - { - case 'IP tunnel': - entry.kind = 'tunnel'; - break; - - case 'Bridge': - entry.kind = 'bridge'; - //entry.ports = dev['bridge-members'].sort(); - break; - } - } - - for (var i = 0; i < self.rpcCache.devlist.length; i++) - { - var dev = self.rpcCache.devlist[i]; - - if (self.isBlacklistedDevice(dev.device)) - continue; - - var entry = self.getDeviceObject(dev.device); - - entry.up = dev.is_up; - entry.type = dev.type; - - switch (dev.type) - { - case 1: /* Ethernet */ - if (dev.is_bridge) - entry.kind = 'bridge'; - else if (dev.is_tuntap) - entry.kind = 'tunnel'; - else if (dev.is_wireless) - entry.kind = 'wifi'; - break; - - case 512: /* PPP */ - case 768: /* IP-IP Tunnel */ - case 769: /* IP6-IP6 Tunnel */ - case 776: /* IPv6-in-IPv4 */ - case 778: /* GRE over IP */ - entry.kind = 'tunnel'; - break; - } - } - - var net = L.uci.sections('network'); - for (var i = 0; i < net.length; i++) - { - var s = net[i]; - var sid = s['.name']; - - if (s['.type'] == 'device' && s.name) - { - var entry = self.getDeviceObject(s.name); - - switch (s.type) - { - case 'macvlan': - case 'tunnel': - entry.kind = 'tunnel'; - break; - } - - entry.sid = sid; - } - else if (s['.type'] == 'interface' && !s['.anonymous'] && s.ifname) - { - var ifnames = L.toArray(s.ifname); - - for (var j = 0; j < ifnames.length; j++) - self.getDeviceObject(ifnames[j]); - - if (s['.name'] != 'loopback') - { - var entry = self.getDeviceObject('@%s'.format(s['.name'])); - - entry.type = 0; - entry.kind = 'alias'; - entry.sid = sid; - } - } - else if (s['.type'] == 'switch_vlan' && s.device) - { - var sw = self.rpcCache.swstate[s.device]; - var vid = parseInt(s.vid || s.vlan); - var ports = L.toArray(s.ports); - - if (!sw || !ports.length || isNaN(vid)) - continue; - - var ifname = undefined; - - for (var j = 0; j < ports.length; j++) - { - var port = parseInt(ports[j]); - var tag = (ports[j].replace(/[^tu]/g, '') == 't'); - - if (port == sw.cpu_port) - { - // XXX: need a way to map switch to netdev - if (tag) - ifname = 'eth0.%d'.format(vid); - else - ifname = 'eth0'; - - break; - } - } - - if (!ifname) - continue; - - var entry = self.getDeviceObject(ifname); - - entry.kind = 'vlan'; - entry.sid = sid; - entry.vsw = sw; - entry.vid = vid; - } - } - - var wifi = L.uci.sections('wireless'); - for (var i = 0; i < wifi.length; i++) - { - var s = wifi[i]; - var sid = s['.name']; - - if (s['.type'] == 'wifi-iface' && s.device) - { - var r = parseInt(s.device.replace(/^[^0-9]+/, '')); - var n = wificount[s.device] = (wificount[s.device] || 0) + 1; - var id = 'radio%d.network%d'.format(r, n); - var ifname = id; - - if (self.rpcCache.wifistate[s.device]) - { - var ifcs = self.rpcCache.wifistate[s.device].interfaces; - for (var ifc in ifcs) - { - if (ifcs[ifc].section == sid && ifcs[ifc].ifname) - { - ifname = ifcs[ifc].ifname; - break; - } - } - } - - var entry = self.getDeviceObject(ifname); - - entry.kind = 'wifi'; - entry.sid = sid; - entry.wid = id; - entry.wdev = s.device; - entry.wmode = s.mode; - entry.wssid = s.ssid; - entry.wbssid = s.bssid; - } - } - - for (var i = 0; i < net.length; i++) - { - var s = net[i]; - var sid = s['.name']; - - if (s['.type'] == 'interface' && !s['.anonymous'] && s.type == 'bridge') - { - var ifnames = L.toArray(s.ifname); - - for (var ifname in self.deviceObjects) - { - var dev = self.deviceObjects[ifname]; - - if (dev.kind != 'wifi') - continue; - - var wnets = L.toArray(L.uci.get('wireless', dev.sid, 'network')); - if ($.inArray(sid, wnets) > -1) - ifnames.push(ifname); - } - - entry = self.getDeviceObject('br-%s'.format(s['.name'])); - entry.type = 1; - entry.kind = 'bridge'; - entry.sid = sid; - entry.ports = ifnames.sort(); - } - } - }, - - loadInterfacesCallback: function() - { - var self = L.NetworkModel; - var net = L.uci.sections('network'); - - for (var i = 0; i < net.length; i++) - { - var s = net[i]; - var sid = s['.name']; - - if (s['.type'] == 'interface' && !s['.anonymous'] && s.proto) - { - var entry = self.getInterfaceObject(s['.name']); - var proto = self.protocolHandlers[s.proto] || self.protocolHandlers.none; - - var l3dev = undefined; - var l2dev = undefined; - - var ifnames = L.toArray(s.ifname); - - for (var ifname in self.deviceObjects) - { - var dev = self.deviceObjects[ifname]; - - if (dev.kind != 'wifi') - continue; - - var wnets = L.toArray(L.uci.get('wireless', dev.sid, 'network')); - if ($.inArray(entry.name, wnets) > -1) - ifnames.push(ifname); - } - - if (proto.virtual) - l3dev = '%s-%s'.format(s.proto, entry.name); - else if (s.type == 'bridge') - l3dev = 'br-%s'.format(entry.name); - else - l3dev = ifnames[0]; - - if (!proto.virtual && s.type == 'bridge') - l2dev = 'br-%s'.format(entry.name); - else if (!proto.virtual) - l2dev = ifnames[0]; - - entry.proto = proto; - entry.sid = sid; - entry.l3dev = l3dev; - entry.l2dev = l2dev; - } - } - - for (var i = 0; i < self.rpcCache.ifstate.length; i++) - { - var iface = self.rpcCache.ifstate[i]; - var entry = self.getInterfaceObject(iface['interface']); - var proto = self.protocolHandlers[iface.proto] || self.protocolHandlers.none; - - /* this is a virtual interface, either deleted from config but - not applied yet or set up from external tools (6rd) */ - if (!entry.sid) - { - entry.proto = proto; - entry.l2dev = iface.device; - entry.l3dev = iface.l3_device; - } - } - }, - - init: function() - { - var self = this; - - if (self.rpcCache) - return L.deferrable(); - - self.rpcCache = { }; - self.deviceObjects = { }; - self.interfaceObjects = { }; - self.protocolHandlers = { }; - - return self.loadCache() - .then(self.loadProtocolHandlers) - .then(self.loadDevicesCallback) - .then(self.loadInterfacesCallback); - }, - - update: function() - { - delete this.rpcCache; - return this.init(); - }, - - refreshInterfaceStatus: function() - { - return this.loadCache(1).then(this.loadInterfacesCallback); - }, - - refreshDeviceStatus: function() - { - return this.loadCache(2).then(this.loadDevicesCallback); - }, - - refreshStatus: function() - { - return this.loadCache(1) - .then(this.loadCache(2)) - .then(this.loadDevicesCallback) - .then(this.loadInterfacesCallback); - }, - - getDevices: function() - { - var devs = [ ]; - - for (var ifname in this.deviceObjects) - if (ifname != 'lo') - devs.push(new L.NetworkModel.Device(this.deviceObjects[ifname])); - - return devs.sort(this.sortDevicesCallback); - }, - - getDeviceByInterface: function(iface) - { - if (iface instanceof L.NetworkModel.Interface) - iface = iface.name(); - - if (this.interfaceObjects[iface]) - return this.getDevice(this.interfaceObjects[iface].l3dev) || - this.getDevice(this.interfaceObjects[iface].l2dev); - - return undefined; - }, - - getDevice: function(ifname) - { - if (this.deviceObjects[ifname]) - return new L.NetworkModel.Device(this.deviceObjects[ifname]); - - return undefined; - }, - - createDevice: function(name) - { - return new L.NetworkModel.Device(this.getDeviceObject(name)); - }, - - getInterfaces: function() - { - var ifaces = [ ]; - - for (var name in this.interfaceObjects) - if (name != 'loopback') - ifaces.push(this.getInterface(name)); - - ifaces.sort(function(a, b) { - if (a.name() < b.name()) - return -1; - else if (a.name() > b.name()) - return 1; - else - return 0; - }); - - return ifaces; - }, - - getInterfacesByDevice: function(dev) - { - var ifaces = [ ]; - - if (dev instanceof L.NetworkModel.Device) - dev = dev.name(); - - for (var name in this.interfaceObjects) - { - var iface = this.interfaceObjects[name]; - if (iface.l2dev == dev || iface.l3dev == dev) - ifaces.push(this.getInterface(name)); - } - - ifaces.sort(function(a, b) { - if (a.name() < b.name()) - return -1; - else if (a.name() > b.name()) - return 1; - else - return 0; - }); - - return ifaces; - }, - - getInterface: function(iface) - { - if (this.interfaceObjects[iface]) - return new L.NetworkModel.Interface(this.interfaceObjects[iface]); - - return undefined; - }, - - getProtocols: function() - { - var rv = [ ]; - - for (var proto in this.protocolHandlers) - { - var pr = this.protocolHandlers[proto]; - - rv.push({ - name: proto, - description: pr.description, - virtual: pr.virtual, - tunnel: pr.tunnel - }); - } - - return rv.sort(function(a, b) { - if (a.name < b.name) - return -1; - else if (a.name > b.name) - return 1; - else - return 0; - }); - }, - - findWANByAddr: function(ipaddr) - { - for (var i = 0; i < this.rpcCache.ifstate.length; i++) - { - var ifstate = this.rpcCache.ifstate[i]; - - if (!ifstate.route) - continue; - - for (var j = 0; j < ifstate.route.length; j++) - if (ifstate.route[j].mask == 0 && - ifstate.route[j].target == ipaddr && - typeof(ifstate.route[j].table) == 'undefined') - { - return this.getInterface(ifstate['interface']); - } - } - - return undefined; - }, - - findWAN: function() - { - return this.findWANByAddr('0.0.0.0'); - }, - - findWAN6: function() - { - return this.findWANByAddr('::'); - }, - - resolveAlias: function(ifname) - { - if (ifname instanceof L.NetworkModel.Device) - ifname = ifname.name(); - - var dev = this.deviceObjects[ifname]; - var seen = { }; - - while (dev && dev.kind == 'alias') - { - // loop - if (seen[dev.ifname]) - return undefined; - - var ifc = this.interfaceObjects[dev.sid]; - - seen[dev.ifname] = true; - dev = ifc ? this.deviceObjects[ifc.l3dev] : undefined; - } - - return dev ? this.getDevice(dev.ifname) : undefined; - } - }; - - this.NetworkModel.Device = Class.extend({ - wifiModeStrings: { - ap: L.tr('Master'), - sta: L.tr('Client'), - adhoc: L.tr('Ad-Hoc'), - monitor: L.tr('Monitor'), - wds: L.tr('Static WDS') - }, - - getStatus: function(key) - { - var s = L.NetworkModel.rpcCache.devstate[this.options.ifname]; - - if (s) - return key ? s[key] : s; - - return undefined; - }, - - get: function(key) - { - var sid = this.options.sid; - var pkg = (this.options.kind == 'wifi') ? 'wireless' : 'network'; - return L.uci.get(pkg, sid, key); - }, - - set: function(key, val) - { - var sid = this.options.sid; - var pkg = (this.options.kind == 'wifi') ? 'wireless' : 'network'; - return L.uci.set(pkg, sid, key, val); - }, - - init: function() - { - if (typeof(this.options.type) == 'undefined') - this.options.type = 1; - - if (typeof(this.options.kind) == 'undefined') - this.options.kind = 'ethernet'; - - if (typeof(this.options.networks) == 'undefined') - this.options.networks = [ ]; - }, - - name: function() - { - return this.options.ifname; - }, - - description: function() - { - switch (this.options.kind) - { - case 'alias': - return L.tr('Alias for network "%s"').format(this.options.ifname.substring(1)); - - case 'bridge': - return L.tr('Network bridge'); - - case 'ethernet': - return L.tr('Network device'); - - case 'tunnel': - switch (this.options.type) - { - case 1: /* tuntap */ - return L.tr('TAP device'); - - case 512: /* PPP */ - return L.tr('PPP tunnel'); - - case 768: /* IP-IP Tunnel */ - return L.tr('IP-in-IP tunnel'); - - case 769: /* IP6-IP6 Tunnel */ - return L.tr('IPv6-in-IPv6 tunnel'); - - case 776: /* IPv6-in-IPv4 */ - return L.tr('IPv6-over-IPv4 tunnel'); - break; - - case 778: /* GRE over IP */ - return L.tr('GRE-over-IP tunnel'); - - default: - return L.tr('Tunnel device'); - } - - case 'vlan': - return L.tr('VLAN %d on %s').format(this.options.vid, this.options.vsw.model); - - case 'wifi': - var o = this.options; - return L.trc('(Wifi-Mode) "(SSID)" on (radioX)', '%s "%h" on %s').format( - o.wmode ? this.wifiModeStrings[o.wmode] : L.tr('Unknown mode'), - o.wssid || '?', o.wdev - ); - } - - return L.tr('Unknown device'); - }, - - icon: function(up) - { - var kind = this.options.kind; - - if (kind == 'alias') - kind = 'ethernet'; - - if (typeof(up) == 'undefined') - up = this.isUp(); - - return L.globals.resource + '/icons/%s%s.png'.format(kind, up ? '' : '_disabled'); - }, - - isUp: function() - { - var l = L.NetworkModel.rpcCache.devlist; - - for (var i = 0; i < l.length; i++) - if (l[i].device == this.options.ifname) - return (l[i].is_up === true); - - return false; - }, - - isAlias: function() - { - return (this.options.kind == 'alias'); - }, - - isBridge: function() - { - return (this.options.kind == 'bridge'); - }, - - isBridgeable: function() - { - return (this.options.type == 1 && this.options.kind != 'bridge'); - }, - - isWireless: function() - { - return (this.options.kind == 'wifi'); - }, - - isInNetwork: function(net) - { - if (!(net instanceof L.NetworkModel.Interface)) - net = L.NetworkModel.getInterface(net); - - if (net) - { - if (net.options.l3dev == this.options.ifname || - net.options.l2dev == this.options.ifname) - return true; - - var dev = L.NetworkModel.deviceObjects[net.options.l2dev]; - if (dev && dev.kind == 'bridge' && dev.ports) - return ($.inArray(this.options.ifname, dev.ports) > -1); - } - - return false; - }, - - getMTU: function() - { - var dev = L.NetworkModel.rpcCache.devstate[this.options.ifname]; - if (dev && !isNaN(dev.mtu)) - return dev.mtu; - - return undefined; - }, - - getMACAddress: function() - { - if (this.options.type != 1) - return undefined; - - var dev = L.NetworkModel.rpcCache.devstate[this.options.ifname]; - if (dev && dev.macaddr) - return dev.macaddr.toUpperCase(); - - return undefined; - }, - - getInterfaces: function() - { - return L.NetworkModel.getInterfacesByDevice(this.options.name); - }, - - getStatistics: function() - { - var s = this.getStatus('statistics') || { }; - return { - rx_bytes: (s.rx_bytes || 0), - tx_bytes: (s.tx_bytes || 0), - rx_packets: (s.rx_packets || 0), - tx_packets: (s.tx_packets || 0) - }; - }, - - getTrafficHistory: function() - { - var def = new Array(120); - - for (var i = 0; i < 120; i++) - def[i] = 0; - - var h = L.NetworkModel.rpcCache.bwstate[this.options.ifname] || { }; - return { - rx_bytes: (h.rx_bytes || def), - tx_bytes: (h.tx_bytes || def), - rx_packets: (h.rx_packets || def), - tx_packets: (h.tx_packets || def) - }; - }, - - removeFromInterface: function(iface) - { - if (!(iface instanceof L.NetworkModel.Interface)) - iface = L.NetworkModel.getInterface(iface); - - if (!iface) - return; - - var ifnames = L.toArray(iface.get('ifname')); - if ($.inArray(this.options.ifname, ifnames) > -1) - iface.set('ifname', L.filterArray(ifnames, this.options.ifname)); - - if (this.options.kind != 'wifi') - return; - - var networks = L.toArray(this.get('network')); - if ($.inArray(iface.name(), networks) > -1) - this.set('network', L.filterArray(networks, iface.name())); - }, - - attachToInterface: function(iface) - { - if (!(iface instanceof L.NetworkModel.Interface)) - iface = L.NetworkModel.getInterface(iface); - - if (!iface) - return; - - if (this.options.kind != 'wifi') - { - var ifnames = L.toArray(iface.get('ifname')); - if ($.inArray(this.options.ifname, ifnames) < 0) - { - ifnames.push(this.options.ifname); - iface.set('ifname', (ifnames.length > 1) ? ifnames : ifnames[0]); - } - } - else - { - var networks = L.toArray(this.get('network')); - if ($.inArray(iface.name(), networks) < 0) - { - networks.push(iface.name()); - this.set('network', (networks.length > 1) ? networks : networks[0]); - } - } - } - }); - - this.NetworkModel.Interface = Class.extend({ - getStatus: function(key) - { - var s = L.NetworkModel.rpcCache.ifstate; - - for (var i = 0; i < s.length; i++) - if (s[i]['interface'] == this.options.name) - return key ? s[i][key] : s[i]; - - return undefined; - }, - - get: function(key) - { - return L.uci.get('network', this.options.name, key); - }, - - set: function(key, val) - { - return L.uci.set('network', this.options.name, key, val); - }, - - name: function() - { - return this.options.name; - }, - - protocol: function() - { - return (this.get('proto') || 'none'); - }, - - isUp: function() - { - return (this.getStatus('up') === true); - }, - - isVirtual: function() - { - return (typeof(this.options.sid) != 'string'); - }, - - getProtocol: function() - { - var prname = this.get('proto') || 'none'; - return L.NetworkModel.protocolHandlers[prname] || L.NetworkModel.protocolHandlers.none; - }, - - getUptime: function() - { - var uptime = this.getStatus('uptime'); - return isNaN(uptime) ? 0 : uptime; - }, - - getDevice: function(resolveAlias) - { - if (this.options.l3dev) - return L.NetworkModel.getDevice(this.options.l3dev); - - return undefined; - }, - - getPhysdev: function() - { - if (this.options.l2dev) - return L.NetworkModel.getDevice(this.options.l2dev); - - return undefined; - }, - - getSubdevices: function() - { - var rv = [ ]; - var dev = this.options.l2dev ? - L.NetworkModel.deviceObjects[this.options.l2dev] : undefined; - - if (dev && dev.kind == 'bridge' && dev.ports && dev.ports.length) - for (var i = 0; i < dev.ports.length; i++) - rv.push(L.NetworkModel.getDevice(dev.ports[i])); - - return rv; - }, - - getIPv4Addrs: function(mask) - { - var rv = [ ]; - var addrs = this.getStatus('ipv4-address'); - - if (addrs) - for (var i = 0; i < addrs.length; i++) - if (!mask) - rv.push(addrs[i].address); - else - rv.push('%s/%d'.format(addrs[i].address, addrs[i].mask)); - - return rv; - }, - - getIPv6Addrs: function(mask) - { - var rv = [ ]; - var addrs; - - addrs = this.getStatus('ipv6-address'); - - if (addrs) - for (var i = 0; i < addrs.length; i++) - if (!mask) - rv.push(addrs[i].address); - else - rv.push('%s/%d'.format(addrs[i].address, addrs[i].mask)); - - addrs = this.getStatus('ipv6-prefix-assignment'); - - if (addrs) - for (var i = 0; i < addrs.length; i++) - if (!mask) - rv.push('%s1'.format(addrs[i].address)); - else - rv.push('%s1/%d'.format(addrs[i].address, addrs[i].mask)); - - return rv; - }, - - getDNSAddrs: function() - { - var rv = [ ]; - var addrs = this.getStatus('dns-server'); - - if (addrs) - for (var i = 0; i < addrs.length; i++) - rv.push(addrs[i]); - - return rv; - }, - - getIPv4DNS: function() - { - var rv = [ ]; - var dns = this.getStatus('dns-server'); - - if (dns) - for (var i = 0; i < dns.length; i++) - if (dns[i].indexOf(':') == -1) - rv.push(dns[i]); - - return rv; - }, - - getIPv6DNS: function() - { - var rv = [ ]; - var dns = this.getStatus('dns-server'); - - if (dns) - for (var i = 0; i < dns.length; i++) - if (dns[i].indexOf(':') > -1) - rv.push(dns[i]); - - return rv; - }, - - getIPv4Gateway: function() - { - var rt = this.getStatus('route'); - - if (rt) - for (var i = 0; i < rt.length; i++) - if (rt[i].target == '0.0.0.0' && rt[i].mask == 0) - return rt[i].nexthop; - - return undefined; - }, - - getIPv6Gateway: function() - { - var rt = this.getStatus('route'); - - if (rt) - for (var i = 0; i < rt.length; i++) - if (rt[i].target == '::' && rt[i].mask == 0) - return rt[i].nexthop; - - return undefined; - }, - - getStatistics: function() - { - var dev = this.getDevice() || new L.NetworkModel.Device({}); - return dev.getStatistics(); - }, - - getTrafficHistory: function() - { - var dev = this.getDevice() || new L.NetworkModel.Device({}); - return dev.getTrafficHistory(); - }, - - renderBadge: function() - { - var badge = $('<span />') - .addClass('badge') - .text('%s: '.format(this.name())); - - var dev = this.getDevice(); - var subdevs = this.getSubdevices(); - - if (subdevs.length) - for (var j = 0; j < subdevs.length; j++) - badge.append($('<img />') - .attr('src', subdevs[j].icon()) - .attr('title', '%s (%s)'.format(subdevs[j].description(), subdevs[j].name() || '?'))); - else if (dev) - badge.append($('<img />') - .attr('src', dev.icon()) - .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?'))); - else - badge.append($('<em />').text(L.tr('(No devices attached)'))); - - return badge; - }, - - setDevices: function(devs) - { - var dev = this.getPhysdev(); - var old_devs = [ ]; - var changed = false; - - if (dev && dev.isBridge()) - old_devs = this.getSubdevices(); - else if (dev) - old_devs = [ dev ]; - - if (old_devs.length != devs.length) - changed = true; - else - for (var i = 0; i < old_devs.length; i++) - { - var dev = devs[i]; - - if (dev instanceof L.NetworkModel.Device) - dev = dev.name(); - - if (!dev || old_devs[i].name() != dev) - { - changed = true; - break; - } - } - - if (changed) - { - for (var i = 0; i < old_devs.length; i++) - old_devs[i].removeFromInterface(this); - - for (var i = 0; i < devs.length; i++) - { - var dev = devs[i]; - - if (!(dev instanceof L.NetworkModel.Device)) - dev = L.NetworkModel.getDevice(dev); - - if (dev) - dev.attachToInterface(this); - } - } - }, - - changeProtocol: function(proto) - { - var pr = L.NetworkModel.protocolHandlers[proto]; - - if (!pr) - return; - - for (var opt in (this.get() || { })) - { - switch (opt) - { - case 'type': - case 'ifname': - case 'macaddr': - if (pr.virtual) - this.set(opt, undefined); - break; - - case 'auto': - case 'mtu': - break; - - case 'proto': - this.set(opt, pr.protocol); - break; - - default: - this.set(opt, undefined); - break; - } - } - }, - - createForm: function(mapwidget) - { - var self = this; - var proto = self.getProtocol(); - var device = self.getDevice(); - - if (!mapwidget) - mapwidget = L.cbi.Map; - - var map = new mapwidget('network', { - caption: L.tr('Configure "%s"').format(self.name()) - }); - - var section = map.section(L.cbi.SingleSection, self.name(), { - anonymous: true - }); - - section.tab({ - id: 'general', - caption: L.tr('General Settings') - }); - - section.tab({ - id: 'advanced', - caption: L.tr('Advanced Settings') - }); - - section.tab({ - id: 'ipv6', - caption: L.tr('IPv6') - }); - - section.tab({ - id: 'physical', - caption: L.tr('Physical Settings') - }); - - - section.taboption('general', L.cbi.CheckboxValue, 'auto', { - caption: L.tr('Start on boot'), - optional: true, - initial: true - }); - - var pr = section.taboption('general', L.cbi.ListValue, 'proto', { - caption: L.tr('Protocol') - }); - - pr.ucivalue = function(sid) { - return self.get('proto') || 'none'; - }; - - var ok = section.taboption('general', L.cbi.ButtonValue, '_confirm', { - caption: L.tr('Really switch?'), - description: L.tr('Changing the protocol will clear all configuration for this interface!'), - text: L.tr('Change protocol') - }); - - ok.on('click', function(ev) { - self.changeProtocol(pr.formvalue(ev.data.sid)); - self.createForm(mapwidget).show(); - }); - - var protos = L.NetworkModel.getProtocols(); - - for (var i = 0; i < protos.length; i++) - pr.value(protos[i].name, protos[i].description); - - proto.populateForm(section, self); - - if (!proto.virtual) - { - var br = section.taboption('physical', L.cbi.CheckboxValue, 'type', { - caption: L.tr('Network bridge'), - description: L.tr('Merges multiple devices into one logical bridge'), - optional: true, - enabled: 'bridge', - disabled: '', - initial: '' - }); - - section.taboption('physical', L.cbi.DeviceList, '__iface_multi', { - caption: L.tr('Devices'), - multiple: true, - bridges: false - }).depends('type', true); - - section.taboption('physical', L.cbi.DeviceList, '__iface_single', { - caption: L.tr('Device'), - multiple: false, - bridges: true - }).depends('type', false); - - var mac = section.taboption('physical', L.cbi.InputValue, 'macaddr', { - caption: L.tr('Override MAC'), - optional: true, - placeholder: device ? device.getMACAddress() : undefined, - datatype: 'macaddr' - }) - - mac.ucivalue = function(sid) - { - if (device) - return device.get('macaddr'); - - return this.callSuper('ucivalue', sid); - }; - - mac.save = function(sid) - { - if (!this.changed(sid)) - return false; - - if (device) - device.set('macaddr', this.formvalue(sid)); - else - this.callSuper('set', sid); - - return true; - }; - } - - section.taboption('physical', L.cbi.InputValue, 'mtu', { - caption: L.tr('Override MTU'), - optional: true, - placeholder: device ? device.getMTU() : undefined, - datatype: 'range(1, 9000)' - }); - - section.taboption('physical', L.cbi.InputValue, 'metric', { - caption: L.tr('Override Metric'), - optional: true, - placeholder: 0, - datatype: 'uinteger' - }); - - for (var field in section.fields) - { - switch (field) - { - case 'proto': - break; - - case '_confirm': - for (var i = 0; i < protos.length; i++) - if (protos[i].name != (this.get('proto') || 'none')) - section.fields[field].depends('proto', protos[i].name); - break; - - default: - section.fields[field].depends('proto', this.get('proto') || 'none', true); - break; - } - } - - return map; - } - }); - - this.NetworkModel.Protocol = this.NetworkModel.Interface.extend({ - description: '__unknown__', - tunnel: false, - virtual: false, - - populateForm: function(section, iface) - { - - } - }); - - this.system = { - getSystemInfo: L.rpc.declare({ - object: 'system', - method: 'info', - expect: { '': { } } - }), - - getBoardInfo: L.rpc.declare({ - object: 'system', - method: 'board', - expect: { '': { } } - }), - - getDiskInfo: L.rpc.declare({ - object: 'luci2.system', - method: 'diskfree', - expect: { '': { } } - }), - - getInfo: function(cb) - { - L.rpc.batch(); - - this.getSystemInfo(); - this.getBoardInfo(); - this.getDiskInfo(); - - return L.rpc.flush().then(function(info) { - var rv = { }; - - $.extend(rv, info[0]); - $.extend(rv, info[1]); - $.extend(rv, info[2]); - - return rv; - }); - }, - - - initList: L.rpc.declare({ - object: 'luci2.system', - method: 'init_list', - expect: { initscripts: [ ] }, - filter: function(data) { - data.sort(function(a, b) { return (a.start || 0) - (b.start || 0) }); - return data; - } - }), - - initEnabled: function(init, cb) - { - return this.initList().then(function(list) { - for (var i = 0; i < list.length; i++) - if (list[i].name == init) - return !!list[i].enabled; - - return false; - }); - }, - - initRun: L.rpc.declare({ - object: 'luci2.system', - method: 'init_action', - params: [ 'name', 'action' ], - filter: function(data) { - return (data == 0); - } - }), - - initStart: function(init, cb) { return L.system.initRun(init, 'start', cb) }, - initStop: function(init, cb) { return L.system.initRun(init, 'stop', cb) }, - initRestart: function(init, cb) { return L.system.initRun(init, 'restart', cb) }, - initReload: function(init, cb) { return L.system.initRun(init, 'reload', cb) }, - initEnable: function(init, cb) { return L.system.initRun(init, 'enable', cb) }, - initDisable: function(init, cb) { return L.system.initRun(init, 'disable', cb) }, - - - performReboot: L.rpc.declare({ - object: 'luci2.system', - method: 'reboot' - }) - }; - - this.session = { - - login: L.rpc.declare({ - object: 'session', - method: 'login', - params: [ 'username', 'password' ], - expect: { '': { } } - }), - - access: L.rpc.declare({ - object: 'session', - method: 'access', - params: [ 'scope', 'object', 'function' ], - expect: { access: false } - }), - - isAlive: function() - { - return L.session.access('ubus', 'session', 'access'); - }, - - startHeartbeat: function() - { - this._hearbeatInterval = window.setInterval(function() { - L.session.isAlive().then(function(alive) { - if (!alive) - { - L.session.stopHeartbeat(); - L.ui.login(true); - } - - }); - }, L.globals.timeout * 2); - }, - - stopHeartbeat: function() - { - if (typeof(this._hearbeatInterval) != 'undefined') - { - window.clearInterval(this._hearbeatInterval); - delete this._hearbeatInterval; - } - }, - - - aclCache: { }, - - callAccess: L.rpc.declare({ - object: 'session', - method: 'access', - expect: { '': { } } - }), - - callAccessCallback: function(acls) - { - L.session.aclCache = acls; - }, - - updateACLs: function() - { - return L.session.callAccess() - .then(L.session.callAccessCallback); - }, - - hasACL: function(scope, object, func) - { - var acls = L.session.aclCache; - - if (typeof(func) == 'undefined') - return (acls && acls[scope] && acls[scope][object]); - - if (acls && acls[scope] && acls[scope][object]) - for (var i = 0; i < acls[scope][object].length; i++) - if (acls[scope][object][i] == func) - return true; - - return false; - } - }; - - this.ui = { - - saveScrollTop: function() - { - this._scroll_top = $(document).scrollTop(); - }, - - restoreScrollTop: function() - { - if (typeof(this._scroll_top) == 'undefined') - return; - - $(document).scrollTop(this._scroll_top); - - delete this._scroll_top; - }, - - loading: function(enable) - { - var win = $(window); - var body = $('body'); - - var state = L.ui._loading || (L.ui._loading = { - modal: $('<div />') - .css('z-index', 2000) - .addClass('modal fade') - .append($('<div />') - .addClass('modal-dialog') - .append($('<div />') - .addClass('modal-content luci2-modal-loader') - .append($('<div />') - .addClass('modal-body') - .text(L.tr('Loading data…'))))) - .appendTo(body) - .modal({ - backdrop: 'static', - keyboard: false - }) - }); - - state.modal.modal(enable ? 'show' : 'hide'); - }, - - dialog: function(title, content, options) - { - var win = $(window); - var body = $('body'); - - var state = L.ui._dialog || (L.ui._dialog = { - dialog: $('<div />') - .addClass('modal fade') - .append($('<div />') - .addClass('modal-dialog') - .append($('<div />') - .addClass('modal-content') - .append($('<div />') - .addClass('modal-header') - .append('<h4 />') - .addClass('modal-title')) - .append($('<div />') - .addClass('modal-body')) - .append($('<div />') - .addClass('modal-footer') - .append(L.ui.button(L.tr('Close'), 'primary') - .click(function() { - $(this).parents('div.modal').modal('hide'); - }))))) - .appendTo(body) - }); - - if (typeof(options) != 'object') - options = { }; - - if (title === false) - { - state.dialog.modal('hide'); - - return state.dialog; - } - - var cnt = state.dialog.children().children().children('div.modal-body'); - var ftr = state.dialog.children().children().children('div.modal-footer'); - - ftr.empty().show(); - - if (options.style == 'confirm') - { - ftr.append(L.ui.button(L.tr('Ok'), 'primary') - .click(options.confirm || function() { L.ui.dialog(false) })); - - ftr.append(L.ui.button(L.tr('Cancel'), 'default') - .click(options.cancel || function() { L.ui.dialog(false) })); - } - else if (options.style == 'close') - { - ftr.append(L.ui.button(L.tr('Close'), 'primary') - .click(options.close || function() { L.ui.dialog(false) })); - } - else if (options.style == 'wait') - { - ftr.append(L.ui.button(L.tr('Close'), 'primary') - .attr('disabled', true)); - } - - if (options.wide) - { - state.dialog.addClass('wide'); - } - else - { - state.dialog.removeClass('wide'); - } - - state.dialog.find('h4:first').text(title); - state.dialog.modal('show'); - - cnt.empty().append(content); - - return state.dialog; - }, - - upload: function(title, content, options) - { - var state = L.ui._upload || (L.ui._upload = { - form: $('<form />') - .attr('method', 'post') - .attr('action', '/cgi-bin/luci-upload') - .attr('enctype', 'multipart/form-data') - .attr('target', 'cbi-fileupload-frame') - .append($('<p />')) - .append($('<input />') - .attr('type', 'hidden') - .attr('name', 'sessionid')) - .append($('<input />') - .attr('type', 'hidden') - .attr('name', 'filename')) - .append($('<input />') - .attr('type', 'file') - .attr('name', 'filedata') - .addClass('cbi-input-file')) - .append($('<div />') - .css('width', '100%') - .addClass('progress progress-striped active') - .append($('<div />') - .addClass('progress-bar') - .css('width', '100%'))) - .append($('<iframe />') - .addClass('pull-right') - .attr('name', 'cbi-fileupload-frame') - .css('width', '1px') - .css('height', '1px') - .css('visibility', 'hidden')), - - finish_cb: function(ev) { - $(this).off('load'); - - var body = (this.contentDocument || this.contentWindow.document).body; - if (body.firstChild.tagName.toLowerCase() == 'pre') - body = body.firstChild; - - var json; - try { - json = $.parseJSON(body.innerHTML); - } catch(e) { - json = { - message: L.tr('Invalid server response received'), - error: [ -1, L.tr('Invalid data') ] - }; - }; - - if (json.error) - { - L.ui.dialog(L.tr('File upload'), [ - $('<p />').text(L.tr('The file upload failed with the server response below:')), - $('<pre />').addClass('alert-message').text(json.message || json.error[1]), - $('<p />').text(L.tr('In case of network problems try uploading the file again.')) - ], { style: 'close' }); - } - else if (typeof(state.success_cb) == 'function') - { - state.success_cb(json); - } - }, - - confirm_cb: function() { - var f = state.form.find('.cbi-input-file'); - var b = state.form.find('.progress'); - var p = state.form.find('p'); - - if (!f.val()) - return; - - state.form.find('iframe').on('load', state.finish_cb); - state.form.submit(); - - f.hide(); - b.show(); - p.text(L.tr('File upload in progress …')); - - state.form.parent().parent().find('button').prop('disabled', true); - } - }); - - state.form.find('.progress').hide(); - state.form.find('.cbi-input-file').val('').show(); - state.form.find('p').text(content || L.tr('Select the file to upload and press "%s" to proceed.').format(L.tr('Ok'))); - - state.form.find('[name=sessionid]').val(L.globals.sid); - state.form.find('[name=filename]').val(options.filename); - - state.success_cb = options.success; - - L.ui.dialog(title || L.tr('File upload'), state.form, { - style: 'confirm', - confirm: state.confirm_cb - }); - }, - - reconnect: function() - { - var protocols = (location.protocol == 'https:') ? [ 'http', 'https' ] : [ 'http' ]; - var ports = (location.protocol == 'https:') ? [ 80, location.port || 443 ] : [ location.port || 80 ]; - var address = location.hostname.match(/^[A-Fa-f0-9]*:[A-Fa-f0-9:]+$/) ? '[' + location.hostname + ']' : location.hostname; - var images = $(); - var interval, timeout; - - L.ui.dialog( - L.tr('Waiting for device'), [ - $('<p />').text(L.tr('Please stand by while the device is reconfiguring …')), - $('<div />') - .css('width', '100%') - .addClass('progressbar') - .addClass('intermediate') - .append($('<div />') - .css('width', '100%')) - ], { style: 'wait' } - ); - - for (var i = 0; i < protocols.length; i++) - images = images.add($('<img />').attr('url', protocols[i] + '://' + address + ':' + ports[i])); - - //L.network.getNetworkStatus(function(s) { - // for (var i = 0; i < protocols.length; i++) - // { - // for (var j = 0; j < s.length; j++) - // { - // for (var k = 0; k < s[j]['ipv4-address'].length; k++) - // images = images.add($('<img />').attr('url', protocols[i] + '://' + s[j]['ipv4-address'][k].address + ':' + ports[i])); - // - // for (var l = 0; l < s[j]['ipv6-address'].length; l++) - // images = images.add($('<img />').attr('url', protocols[i] + '://[' + s[j]['ipv6-address'][l].address + ']:' + ports[i])); - // } - // } - //}).then(function() { - images.on('load', function() { - var url = this.getAttribute('url'); - L.session.isAlive().then(function(access) { - if (access) - { - window.clearTimeout(timeout); - window.clearInterval(interval); - L.ui.dialog(false); - images = null; - } - else - { - location.href = url; - } - }); - }); - - interval = window.setInterval(function() { - images.each(function() { - this.setAttribute('src', this.getAttribute('url') + L.globals.resource + '/icons/loading.gif?r=' + Math.random()); - }); - }, 5000); - - timeout = window.setTimeout(function() { - window.clearInterval(interval); - images.off('load'); - - L.ui.dialog( - L.tr('Device not responding'), - L.tr('The device was not responding within 180 seconds, you might need to manually reconnect your computer or use SSH to regain access.'), - { style: 'close' } - ); - }, 180000); - //}); - }, - - login: function(invalid) - { - var state = L.ui._login || (L.ui._login = { - form: $('<form />') - .attr('target', '') - .attr('method', 'post') - .append($('<p />') - .addClass('alert-message') - .text(L.tr('Wrong username or password given!'))) - .append($('<p />') - .append($('<label />') - .text(L.tr('Username')) - .append($('<br />')) - .append($('<input />') - .attr('type', 'text') - .attr('name', 'username') - .attr('value', 'root') - .addClass('form-control') - .keypress(function(ev) { - if (ev.which == 10 || ev.which == 13) - state.confirm_cb(); - })))) - .append($('<p />') - .append($('<label />') - .text(L.tr('Password')) - .append($('<br />')) - .append($('<input />') - .attr('type', 'password') - .attr('name', 'password') - .addClass('form-control') - .keypress(function(ev) { - if (ev.which == 10 || ev.which == 13) - state.confirm_cb(); - })))) - .append($('<p />') - .text(L.tr('Enter your username and password above, then click "%s" to proceed.').format(L.tr('Ok')))), - - response_cb: function(response) { - if (!response.ubus_rpc_session) - { - L.ui.login(true); - } - else - { - L.globals.sid = response.ubus_rpc_session; - L.setHash('id', L.globals.sid); - L.session.startHeartbeat(); - L.ui.dialog(false); - state.deferred.resolve(); - } - }, - - confirm_cb: function() { - var u = state.form.find('[name=username]').val(); - var p = state.form.find('[name=password]').val(); - - if (!u) - return; - - L.ui.dialog( - L.tr('Logging in'), [ - $('<p />').text(L.tr('Log in in progress …')), - $('<div />') - .css('width', '100%') - .addClass('progressbar') - .addClass('intermediate') - .append($('<div />') - .css('width', '100%')) - ], { style: 'wait' } - ); - - L.globals.sid = '00000000000000000000000000000000'; - L.session.login(u, p).then(state.response_cb); - } - }); - - if (!state.deferred || state.deferred.state() != 'pending') - state.deferred = $.Deferred(); - - /* try to find sid from hash */ - var sid = L.getHash('id'); - if (sid && sid.match(/^[a-f0-9]{32}$/)) - { - L.globals.sid = sid; - L.session.isAlive().then(function(access) { - if (access) - { - L.session.startHeartbeat(); - state.deferred.resolve(); - } - else - { - L.setHash('id', undefined); - L.ui.login(); - } - }); - - return state.deferred; - } - - if (invalid) - state.form.find('.alert-message').show(); - else - state.form.find('.alert-message').hide(); - - L.ui.dialog(L.tr('Authorization Required'), state.form, { - style: 'confirm', - confirm: state.confirm_cb - }); - - state.form.find('[name=password]').focus(); - - return state.deferred; - }, - - cryptPassword: L.rpc.declare({ - object: 'luci2.ui', - method: 'crypt', - params: [ 'data' ], - expect: { crypt: '' } - }), - - - mergeACLScope: function(acl_scope, scope) - { - if ($.isArray(scope)) - { - for (var i = 0; i < scope.length; i++) - acl_scope[scope[i]] = true; - } - else if ($.isPlainObject(scope)) - { - for (var object_name in scope) - { - if (!$.isArray(scope[object_name])) - continue; - - var acl_object = acl_scope[object_name] || (acl_scope[object_name] = { }); - - for (var i = 0; i < scope[object_name].length; i++) - acl_object[scope[object_name][i]] = true; - } - } - }, - - mergeACLPermission: function(acl_perm, perm) - { - if ($.isPlainObject(perm)) - { - for (var scope_name in perm) - { - var acl_scope = acl_perm[scope_name] || (acl_perm[scope_name] = { }); - L.ui.mergeACLScope(acl_scope, perm[scope_name]); - } - } - }, - - mergeACLGroup: function(acl_group, group) - { - if ($.isPlainObject(group)) - { - if (!acl_group.description) - acl_group.description = group.description; - - if (group.read) - { - var acl_perm = acl_group.read || (acl_group.read = { }); - L.ui.mergeACLPermission(acl_perm, group.read); - } - - if (group.write) - { - var acl_perm = acl_group.write || (acl_group.write = { }); - L.ui.mergeACLPermission(acl_perm, group.write); - } - } - }, - - callACLsCallback: function(trees) - { - var acl_tree = { }; - - for (var i = 0; i < trees.length; i++) - { - if (!$.isPlainObject(trees[i])) - continue; - - for (var group_name in trees[i]) - { - var acl_group = acl_tree[group_name] || (acl_tree[group_name] = { }); - L.ui.mergeACLGroup(acl_group, trees[i][group_name]); - } - } - - return acl_tree; - }, - - callACLs: L.rpc.declare({ - object: 'luci2.ui', - method: 'acls', - expect: { acls: [ ] } - }), - - getAvailableACLs: function() - { - return this.callACLs().then(this.callACLsCallback); - }, - - renderChangeIndicator: function() - { - return $('<ul />') - .addClass('nav navbar-nav navbar-right') - .append($('<li />') - .append($('<a />') - .attr('id', 'changes') - .attr('href', '#') - .append($('<span />') - .addClass('label label-info')))); - }, - - callMenuCallback: function(entries) - { - L.globals.mainMenu = new L.ui.menu(); - L.globals.mainMenu.entries(entries); - - $('#mainmenu') - .empty() - .append(L.globals.mainMenu.render(0, 1)) - .append(L.ui.renderChangeIndicator()); - }, - - callMenu: L.rpc.declare({ - object: 'luci2.ui', - method: 'menu', - expect: { menu: { } } - }), - - renderMainMenu: function() - { - return this.callMenu().then(this.callMenuCallback); - }, - - renderViewMenu: function() - { - $('#viewmenu') - .empty() - .append(L.globals.mainMenu.render(2, 900)); - }, - - renderView: function() - { - var node = arguments[0]; - var name = node.view.split(/\//).join('.'); - var cname = L.toClassName(name); - var views = L.views || (L.views = { }); - var args = [ ]; - - for (var i = 1; i < arguments.length; i++) - args.push(arguments[i]); - - if (L.globals.currentView) - L.globals.currentView.finish(); - - L.ui.renderViewMenu(); - L.setHash('view', node.view); - - if (views[cname] instanceof L.ui.view) - { - L.globals.currentView = views[cname]; - return views[cname].render.apply(views[cname], args); - } - - var url = L.globals.resource + '/view/' + name + '.js'; - - return $.ajax(url, { - method: 'GET', - cache: true, - dataType: 'text' - }).then(function(data) { - try { - var viewConstructorSource = ( - '(function(L, $) { ' + - 'return %s' + - '})(L, $);\n\n' + - '//@ sourceURL=%s' - ).format(data, url); - - var viewConstructor = eval(viewConstructorSource); - - views[cname] = new viewConstructor({ - name: name, - acls: node.write || { } - }); - - L.globals.currentView = views[cname]; - return views[cname].render.apply(views[cname], args); - } - catch(e) { - alert('Unable to instantiate view "%s": %s'.format(url, e)); - }; - - return $.Deferred().resolve(); - }); - }, - - changeView: function() - { - var name = L.getHash('view'); - var node = L.globals.defaultNode; - - if (name && L.globals.mainMenu) - node = L.globals.mainMenu.getNode(name); - - if (node) - { - L.ui.loading(true); - L.ui.renderView(node).then(function() { - L.ui.loading(false); - }); - } - }, - - updateHostname: function() - { - return L.system.getBoardInfo().then(function(info) { - if (info.hostname) - $('#hostname').text(info.hostname); - }); - }, - - updateChanges: function() - { - return L.uci.changes().then(function(changes) { - var n = 0; - var html = ''; - - for (var config in changes) - { - var log = [ ]; - - for (var i = 0; i < changes[config].length; i++) - { - var c = changes[config][i]; - - switch (c[0]) - { - case 'order': - log.push('uci reorder %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2])); - break; - - case 'remove': - if (c.length < 3) - log.push('uci delete %s.<del>%s</del>'.format(config, c[1])); - else - log.push('uci delete %s.%s.<del>%s</del>'.format(config, c[1], c[2])); - break; - - case 'rename': - if (c.length < 4) - log.push('uci rename %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3])); - else - log.push('uci rename %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4])); - break; - - case 'add': - log.push('uci add %s <ins>%s</ins> (= <ins><strong>%s</strong></ins>)'.format(config, c[2], c[1])); - break; - - case 'list-add': - log.push('uci add_list %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4])); - break; - - case 'list-del': - log.push('uci del_list %s.%s.<del>%s=<strong>%s</strong></del>'.format(config, c[1], c[2], c[3], c[4])); - break; - - case 'set': - if (c.length < 4) - log.push('uci set %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2])); - else - log.push('uci set %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4])); - break; - } - } - - html += '<code>/etc/config/%s</code><pre class="uci-changes">%s</pre>'.format(config, log.join('\n')); - n += changes[config].length; - } - - if (n > 0) - $('#changes') - .click(function(ev) { - L.ui.dialog(L.tr('Staged configuration changes'), html, { - style: 'confirm', - confirm: function() { - L.uci.apply().then( - function(code) { alert('Success with code ' + code); }, - function(code) { alert('Error with code ' + code); } - ); - } - }); - ev.preventDefault(); - }) - .children('span') - .show() - .text(L.trcp('Pending configuration changes', '1 change', '%d changes', n).format(n)); - else - $('#changes').children('span').hide(); - }); - }, - - init: function() - { - L.ui.loading(true); - - $.when( - L.session.updateACLs(), - L.ui.updateHostname(), - L.ui.updateChanges(), - L.ui.renderMainMenu(), - L.NetworkModel.init() - ).then(function() { - L.ui.renderView(L.globals.defaultNode).then(function() { - L.ui.loading(false); - }); - - $(window).on('hashchange', function() { - L.ui.changeView(); - }); - }); - }, - - button: function(label, style, title) - { - style = style || 'default'; - - return $('<button />') - .attr('type', 'button') - .attr('title', title ? title : '') - .addClass('btn btn-' + style) - .text(label); - } - }; - - this.ui.AbstractWidget = Class.extend({ - i18n: function(text) { - return text; - }, - - label: function() { - var key = arguments[0]; - var args = [ ]; - - for (var i = 1; i < arguments.length; i++) - args.push(arguments[i]); - - switch (typeof(this.options[key])) - { - case 'undefined': - return ''; - - case 'function': - return this.options[key].apply(this, args); - - default: - return ''.format.apply('' + this.options[key], args); - } - }, - - toString: function() { - return $('<div />').append(this.render()).html(); - }, - - insertInto: function(id) { - return $(id).empty().append(this.render()); - }, - - appendTo: function(id) { - return $(id).append(this.render()); - }, - - on: function(evname, evfunc) - { - var evnames = L.toArray(evname); - - if (!this.events) - this.events = { }; - - for (var i = 0; i < evnames.length; i++) - this.events[evnames[i]] = evfunc; - - return this; - }, - - trigger: function(evname, evdata) - { - if (this.events) - { - var evnames = L.toArray(evname); - - for (var i = 0; i < evnames.length; i++) - if (this.events[evnames[i]]) - this.events[evnames[i]].call(this, evdata); - } - - return this; - } - }); - - this.ui.view = this.ui.AbstractWidget.extend({ - _fetch_template: function() - { - return $.ajax(L.globals.resource + '/template/' + this.options.name + '.htm', { - method: 'GET', - cache: true, - dataType: 'text', - success: function(data) { - data = data.replace(/<%([#:=])?(.+?)%>/g, function(match, p1, p2) { - p2 = p2.replace(/^\s+/, '').replace(/\s+$/, ''); - switch (p1) - { - case '#': - return ''; - - case ':': - return L.tr(p2); - - case '=': - return L.globals[p2] || ''; - - default: - return '(?' + match + ')'; - } - }); - - $('#maincontent').append(data); - } - }); - }, - - execute: function() - { - throw "Not implemented"; - }, - - render: function() - { - var container = $('#maincontent'); - - container.empty(); - - if (this.title) - container.append($('<h2 />').append(this.title)); - - if (this.description) - container.append($('<p />').append(this.description)); - - var self = this; - var args = [ ]; - - for (var i = 0; i < arguments.length; i++) - args.push(arguments[i]); - - return this._fetch_template().then(function() { - return L.deferrable(self.execute.apply(self, args)); - }); - }, - - repeat: function(func, interval) - { - var self = this; - - if (!self._timeouts) - self._timeouts = [ ]; - - var index = self._timeouts.length; - - if (typeof(interval) != 'number') - interval = 5000; - - var setTimer, runTimer; - - setTimer = function() { - if (self._timeouts) - self._timeouts[index] = window.setTimeout(runTimer, interval); - }; - - runTimer = function() { - L.deferrable(func.call(self)).then(setTimer, setTimer); - }; - - runTimer(); - }, - - finish: function() - { - if ($.isArray(this._timeouts)) - { - for (var i = 0; i < this._timeouts.length; i++) - window.clearTimeout(this._timeouts[i]); - - delete this._timeouts; - } - } - }); - - this.ui.menu = this.ui.AbstractWidget.extend({ - init: function() { - this._nodes = { }; - }, - - entries: function(entries) - { - for (var entry in entries) - { - var path = entry.split(/\//); - var node = this._nodes; - - for (i = 0; i < path.length; i++) - { - if (!node.childs) - node.childs = { }; - - if (!node.childs[path[i]]) - node.childs[path[i]] = { }; - - node = node.childs[path[i]]; - } - - $.extend(node, entries[entry]); - } - }, - - sortNodesCallback: function(a, b) - { - var x = a.index || 0; - var y = b.index || 0; - return (x - y); - }, - - firstChildView: function(node) - { - if (node.view) - return node; - - var nodes = [ ]; - for (var child in (node.childs || { })) - nodes.push(node.childs[child]); - - nodes.sort(this.sortNodesCallback); - - for (var i = 0; i < nodes.length; i++) - { - var child = this.firstChildView(nodes[i]); - if (child) - { - for (var key in child) - if (!node.hasOwnProperty(key) && child.hasOwnProperty(key)) - node[key] = child[key]; - - return node; - } - } - - return undefined; - }, - - handleClick: function(ev) - { - L.setHash('view', ev.data); - - ev.preventDefault(); - this.blur(); - }, - - renderNodes: function(childs, level, min, max) - { - var nodes = [ ]; - for (var node in childs) - { - var child = this.firstChildView(childs[node]); - if (child) - nodes.push(childs[node]); - } - - nodes.sort(this.sortNodesCallback); - - var list = $('<ul />'); - - if (level == 0) - list.addClass('nav').addClass('navbar-nav'); - else if (level == 1) - list.addClass('dropdown-menu').addClass('navbar-inverse'); - - for (var i = 0; i < nodes.length; i++) - { - if (!L.globals.defaultNode) - { - var v = L.getHash('view'); - if (!v || v == nodes[i].view) - L.globals.defaultNode = nodes[i]; - } - - var item = $('<li />') - .append($('<a />') - .attr('href', '#') - .text(L.tr(nodes[i].title))) - .appendTo(list); - - if (nodes[i].childs && level < max) - { - item.addClass('dropdown'); - - item.find('a') - .addClass('dropdown-toggle') - .attr('data-toggle', 'dropdown') - .append('<b class="caret"></b>'); - - item.append(this.renderNodes(nodes[i].childs, level + 1)); - } - else - { - item.find('a').click(nodes[i].view, this.handleClick); - } - } - - return list.get(0); - }, - - render: function(min, max) - { - var top = min ? this.getNode(L.globals.defaultNode.view, min) : this._nodes; - return this.renderNodes(top.childs, 0, min, max); - }, - - getNode: function(path, max) - { - var p = path.split(/\//); - var n = this._nodes; - - if (typeof(max) == 'undefined') - max = p.length; - - for (var i = 0; i < max; i++) - { - if (!n.childs[p[i]]) - return undefined; - - n = n.childs[p[i]]; - } - - return n; - } - }); - - this.ui.table = this.ui.AbstractWidget.extend({ - init: function() - { - this._rows = [ ]; - }, - - row: function(values) - { - if ($.isArray(values)) - { - this._rows.push(values); - } - else if ($.isPlainObject(values)) - { - var v = [ ]; - for (var i = 0; i < this.options.columns.length; i++) - { - var col = this.options.columns[i]; - - if (typeof col.key == 'string') - v.push(values[col.key]); - else - v.push(null); - } - this._rows.push(v); - } - }, - - rows: function(rows) - { - for (var i = 0; i < rows.length; i++) - this.row(rows[i]); - }, - - render: function(id) - { - var fieldset = document.createElement('fieldset'); - fieldset.className = 'cbi-section'; - - if (this.options.caption) - { - var legend = document.createElement('legend'); - $(legend).append(this.options.caption); - fieldset.appendChild(legend); - } - - var table = document.createElement('table'); - table.className = 'table table-condensed table-hover'; - - var has_caption = false; - var has_description = false; - - for (var i = 0; i < this.options.columns.length; i++) - if (this.options.columns[i].caption) - { - has_caption = true; - break; - } - else if (this.options.columns[i].description) - { - has_description = true; - break; - } - - if (has_caption) - { - var tr = table.insertRow(-1); - tr.className = 'cbi-section-table-titles'; - - for (var i = 0; i < this.options.columns.length; i++) - { - var col = this.options.columns[i]; - var th = document.createElement('th'); - th.className = 'cbi-section-table-cell'; - - tr.appendChild(th); - - if (col.width) - th.style.width = col.width; - - if (col.align) - th.style.textAlign = col.align; - - if (col.caption) - $(th).append(col.caption); - } - } - - if (has_description) - { - var tr = table.insertRow(-1); - tr.className = 'cbi-section-table-descr'; - - for (var i = 0; i < this.options.columns.length; i++) - { - var col = this.options.columns[i]; - var th = document.createElement('th'); - th.className = 'cbi-section-table-cell'; - - tr.appendChild(th); - - if (col.width) - th.style.width = col.width; - - if (col.align) - th.style.textAlign = col.align; - - if (col.description) - $(th).append(col.description); - } - } - - if (this._rows.length == 0) - { - if (this.options.placeholder) - { - var tr = table.insertRow(-1); - var td = tr.insertCell(-1); - td.className = 'cbi-section-table-cell'; - - td.colSpan = this.options.columns.length; - $(td).append(this.options.placeholder); - } - } - else - { - for (var i = 0; i < this._rows.length; i++) - { - var tr = table.insertRow(-1); - - for (var j = 0; j < this.options.columns.length; j++) - { - var col = this.options.columns[j]; - var td = tr.insertCell(-1); - - var val = this._rows[i][j]; - - if (typeof(val) == 'undefined') - val = col.placeholder; - - if (typeof(val) == 'undefined') - val = ''; - - if (col.width) - td.style.width = col.width; - - if (col.align) - td.style.textAlign = col.align; - - if (typeof col.format == 'string') - $(td).append(col.format.format(val)); - else if (typeof col.format == 'function') - $(td).append(col.format(val, i)); - else - $(td).append(val); - } - } - } - - this._rows = [ ]; - fieldset.appendChild(table); - - return fieldset; - } - }); - - this.ui.progress = this.ui.AbstractWidget.extend({ - render: function() - { - var vn = parseInt(this.options.value) || 0; - var mn = parseInt(this.options.max) || 100; - var pc = Math.floor((100 / mn) * vn); - - var text; - - if (typeof(this.options.format) == 'string') - text = this.options.format.format(this.options.value, this.options.max, pc); - else if (typeof(this.options.format) == 'function') - text = this.options.format(pc); - else - text = '%.2f%%'.format(pc); - - return $('<div />') - .addClass('progress') - .append($('<div />') - .addClass('progress-bar') - .addClass('progress-bar-info') - .css('width', pc + '%')) - .append($('<small />') - .text(text)); - } - }); - - this.ui.devicebadge = this.ui.AbstractWidget.extend({ - render: function() - { - var l2dev = this.options.l2_device || this.options.device; - var l3dev = this.options.l3_device; - var dev = l3dev || l2dev || '?'; - - var span = document.createElement('span'); - span.className = 'badge'; - - if (typeof(this.options.signal) == 'number' || - typeof(this.options.noise) == 'number') - { - var r = 'none'; - if (typeof(this.options.signal) != 'undefined' && - typeof(this.options.noise) != 'undefined') - { - var q = (-1 * (this.options.noise - this.options.signal)) / 5; - if (q < 1) - r = '0'; - else if (q < 2) - r = '0-25'; - else if (q < 3) - r = '25-50'; - else if (q < 4) - r = '50-75'; - else - r = '75-100'; - } - - span.appendChild(document.createElement('img')); - span.lastChild.src = L.globals.resource + '/icons/signal-' + r + '.png'; - - if (r == 'none') - span.title = L.tr('No signal'); - else - span.title = '%s: %d %s / %s: %d %s'.format( - L.tr('Signal'), this.options.signal, L.tr('dBm'), - L.tr('Noise'), this.options.noise, L.tr('dBm') - ); - } - else - { - var type = 'ethernet'; - var desc = L.tr('Ethernet device'); - - if (l3dev != l2dev) - { - type = 'tunnel'; - desc = L.tr('Tunnel interface'); - } - else if (dev.indexOf('br-') == 0) - { - type = 'bridge'; - desc = L.tr('Bridge'); - } - else if (dev.indexOf('.') > 0) - { - type = 'vlan'; - desc = L.tr('VLAN interface'); - } - else if (dev.indexOf('wlan') == 0 || - dev.indexOf('ath') == 0 || - dev.indexOf('wl') == 0) - { - type = 'wifi'; - desc = L.tr('Wireless Network'); - } - - span.appendChild(document.createElement('img')); - span.lastChild.src = L.globals.resource + '/icons/' + type + (this.options.up ? '' : '_disabled') + '.png'; - span.title = desc; - } - - $(span).append(' '); - $(span).append(dev); - - return span; - } - }); - - var type = function(f, l) - { - f.message = l; - return f; - }; - - this.cbi = { - validation: { - i18n: function(msg) - { - L.cbi.validation.message = L.tr(msg); - }, - - compile: function(code) - { - var pos = 0; - var esc = false; - var depth = 0; - var types = L.cbi.validation.types; - 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 types[label] == 'function') - { - stack.push(types[label]); - stack.push([ ]); - } - 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] = - L.cbi.validation.compile(code.substring(pos, i)); - - pos = i+1; - } - break; - } - } - - return stack; - } - } - }; - - var validation = this.cbi.validation; - - validation.types = { - 'integer': function() - { - if (this.match(/^-?[0-9]+$/) != null) - return true; - - validation.i18n('Must be a valid integer'); - return false; - }, - - 'uinteger': function() - { - if (validation.types['integer'].apply(this) && (this >= 0)) - return true; - - validation.i18n('Must be a positive integer'); - return false; - }, - - 'float': function() - { - if (!isNaN(parseFloat(this))) - return true; - - validation.i18n('Must be a valid number'); - return false; - }, - - 'ufloat': function() - { - if (validation.types['float'].apply(this) && (this >= 0)) - return true; - - validation.i18n('Must be a positive number'); - return false; - }, - - 'ipaddr': function() - { - if (L.parseIPv4(this) || L.parseIPv6(this)) - return true; - - validation.i18n('Must be a valid IP address'); - return false; - }, - - 'ip4addr': function() - { - if (L.parseIPv4(this)) - return true; - - validation.i18n('Must be a valid IPv4 address'); - return false; - }, - - 'ip6addr': function() - { - if (L.parseIPv6(this)) - return true; - - validation.i18n('Must be a valid IPv6 address'); - return false; - }, - - 'netmask4': function() - { - if (L.isNetmask(L.parseIPv4(this))) - return true; - - validation.i18n('Must be a valid IPv4 netmask'); - return false; - }, - - 'netmask6': function() - { - if (L.isNetmask(L.parseIPv6(this))) - return true; - - validation.i18n('Must be a valid IPv6 netmask6'); - return false; - }, - - 'cidr4': function() - { - if (this.match(/^([0-9.]+)\/(\d{1,2})$/)) - if (RegExp.$2 <= 32 && L.parseIPv4(RegExp.$1)) - return true; - - validation.i18n('Must be a valid IPv4 prefix'); - return false; - }, - - 'cidr6': function() - { - if (this.match(/^([a-fA-F0-9:.]+)\/(\d{1,3})$/)) - if (RegExp.$2 <= 128 && L.parseIPv6(RegExp.$1)) - return true; - - validation.i18n('Must be a valid IPv6 prefix'); - return false; - }, - - 'ipmask4': function() - { - if (this.match(/^([0-9.]+)\/([0-9.]+)$/)) - { - var addr = RegExp.$1, mask = RegExp.$2; - if (L.parseIPv4(addr) && L.isNetmask(L.parseIPv4(mask))) - return true; - } - - validation.i18n('Must be a valid IPv4 address/netmask pair'); - return false; - }, - - 'ipmask6': function() - { - if (this.match(/^([a-fA-F0-9:.]+)\/([a-fA-F0-9:.]+)$/)) - { - var addr = RegExp.$1, mask = RegExp.$2; - if (L.parseIPv6(addr) && L.isNetmask(L.parseIPv6(mask))) - return true; - } - - validation.i18n('Must be a valid IPv6 address/netmask pair'); - return false; - }, - - 'port': function() - { - if (validation.types['integer'].apply(this) && - (this >= 0) && (this <= 65535)) - return true; - - validation.i18n('Must be a valid port number'); - return false; - }, - - 'portrange': function() - { - if (this.match(/^(\d+)-(\d+)$/)) - { - var p1 = RegExp.$1; - var p2 = RegExp.$2; - - if (validation.types['port'].apply(p1) && - validation.types['port'].apply(p2) && - (parseInt(p1) <= parseInt(p2))) - return true; - } - else if (validation.types['port'].apply(this)) - { - return true; - } - - validation.i18n('Must be a valid port range'); - return false; - }, - - 'macaddr': function() - { - if (this.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null) - return true; - - validation.i18n('Must be a valid MAC address'); - return false; - }, - - 'host': function() - { - if (validation.types['hostname'].apply(this) || - validation.types['ipaddr'].apply(this)) - return true; - - validation.i18n('Must be a valid hostname or IP address'); - return false; - }, - - 'hostname': function() - { - if ((this.length <= 253) && - ((this.match(/^[a-zA-Z0-9]+$/) != null || - (this.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) && - this.match(/[^0-9.]/))))) - return true; - - validation.i18n('Must be a valid host name'); - return false; - }, - - 'network': function() - { - if (validation.types['uciname'].apply(this) || - validation.types['host'].apply(this)) - return true; - - validation.i18n('Must be a valid network name'); - return false; - }, - - 'wpakey': function() - { - var v = this; - - if ((v.length == 64) - ? (v.match(/^[a-fA-F0-9]{64}$/) != null) - : ((v.length >= 8) && (v.length <= 63))) - return true; - - validation.i18n('Must be a valid WPA key'); - return false; - }, - - 'wepkey': function() - { - var v = this; - - if (v.substr(0,2) == 's:') - v = v.substr(2); - - if (((v.length == 10) || (v.length == 26)) - ? (v.match(/^[a-fA-F0-9]{10,26}$/) != null) - : ((v.length == 5) || (v.length == 13))) - return true; - - validation.i18n('Must be a valid WEP key'); - return false; - }, - - 'uciname': function() - { - if (this.match(/^[a-zA-Z0-9_]+$/) != null) - return true; - - validation.i18n('Must be a valid UCI identifier'); - return false; - }, - - 'range': function(min, max) - { - var val = parseFloat(this); - - if (validation.types['integer'].apply(this) && - !isNaN(min) && !isNaN(max) && ((val >= min) && (val <= max))) - return true; - - validation.i18n('Must be a number between %d and %d'); - return false; - }, - - 'min': function(min) - { - var val = parseFloat(this); - - if (validation.types['integer'].apply(this) && - !isNaN(min) && !isNaN(val) && (val >= min)) - return true; - - validation.i18n('Must be a number greater or equal to %d'); - return false; - }, - - 'max': function(max) - { - var val = parseFloat(this); - - if (validation.types['integer'].apply(this) && - !isNaN(max) && !isNaN(val) && (val <= max)) - return true; - - validation.i18n('Must be a number lower or equal to %d'); - return false; - }, - - 'rangelength': function(min, max) - { - var val = '' + this; - - if (!isNaN(min) && !isNaN(max) && - (val.length >= min) && (val.length <= max)) - return true; - - validation.i18n('Must be between %d and %d characters'); - return false; - }, - - 'minlength': function(min) - { - var val = '' + this; - - if (!isNaN(min) && (val.length >= min)) - return true; - - validation.i18n('Must be at least %d characters'); - return false; - }, - - 'maxlength': function(max) - { - var val = '' + this; - - if (!isNaN(max) && (val.length <= max)) - return true; - - validation.i18n('Must be at most %d characters'); - return false; - }, - - 'or': function() - { - var msgs = [ ]; - - for (var i = 0; i < arguments.length; i += 2) - { - delete validation.message; - - if (typeof(arguments[i]) != 'function') - { - if (arguments[i] == this) - return true; - i--; - } - else if (arguments[i].apply(this, arguments[i+1])) - { - return true; - } - - if (validation.message) - msgs.push(validation.message.format.apply(validation.message, arguments[i+1])); - } - - validation.message = msgs.join( L.tr(' - or - ')); - return false; - }, - - 'and': function() - { - var msgs = [ ]; - - for (var i = 0; i < arguments.length; i += 2) - { - delete validation.message; - - if (typeof arguments[i] != 'function') - { - if (arguments[i] != this) - return false; - i--; - } - else if (!arguments[i].apply(this, arguments[i+1])) - { - return false; - } - - if (validation.message) - msgs.push(validation.message.format.apply(validation.message, arguments[i+1])); - } - - validation.message = msgs.join(', '); - return true; - }, - - 'neg': function() - { - return validation.types['or'].apply( - this.replace(/^[ \t]*![ \t]*/, ''), arguments); - }, - - 'list': function(subvalidator, subargs) - { - if (typeof subvalidator != 'function') - return false; - - var tokens = this.match(/[^ \t]+/g); - for (var i = 0; i < tokens.length; i++) - if (!subvalidator.apply(tokens[i], subargs)) - return false; - - return true; - }, - - 'phonedigit': function() - { - if (this.match(/^[0-9\*#!\.]+$/) != null) - return true; - - validation.i18n('Must be a valid phone number digit'); - return false; - }, - - 'string': function() - { - return true; - } - }; - - - this.cbi.AbstractValue = this.ui.AbstractWidget.extend({ - init: function(name, options) - { - this.name = name; - this.instance = { }; - this.dependencies = [ ]; - this.rdependency = { }; - - this.options = L.defaults(options, { - placeholder: '', - datatype: 'string', - optional: false, - keep: true - }); - }, - - id: function(sid) - { - return this.ownerSection.id('field', sid || '__unknown__', this.name); - }, - - render: function(sid, condensed) - { - var i = this.instance[sid] = { }; - - i.top = $('<div />') - .addClass('luci2-field'); - - if (!condensed) - { - i.top.addClass('form-group'); - - if (typeof(this.options.caption) == 'string') - $('<label />') - .addClass('col-lg-2 control-label') - .attr('for', this.id(sid)) - .text(this.options.caption) - .appendTo(i.top); - } - - i.error = $('<div />') - .hide() - .addClass('luci2-field-error label label-danger'); - - i.widget = $('<div />') - .addClass('luci2-field-widget') - .append(this.widget(sid)) - .append(i.error) - .appendTo(i.top); - - if (!condensed) - { - i.widget.addClass('col-lg-5'); - - $('<div />') - .addClass('col-lg-5') - .text((typeof(this.options.description) == 'string') ? this.options.description : '') - .appendTo(i.top); - } - - return i.top; - }, - - active: function(sid) - { - return (this.instance[sid] && !this.instance[sid].disabled); - }, - - ucipath: function(sid) - { - return { - config: (this.options.uci_package || this.ownerMap.uci_package), - section: (this.options.uci_section || sid), - option: (this.options.uci_option || this.name) - }; - }, - - ucivalue: function(sid) - { - var uci = this.ucipath(sid); - var val = this.ownerMap.get(uci.config, uci.section, uci.option); - - if (typeof(val) == 'undefined') - return this.options.initial; - - return val; - }, - - formvalue: function(sid) - { - var v = $('#' + this.id(sid)).val(); - return (v === '') ? undefined : v; - }, - - textvalue: function(sid) - { - var v = this.formvalue(sid); - - if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length)) - v = this.ucivalue(sid); - - if (typeof(v) == 'undefined' || ($.isArray(v) && !v.length)) - v = this.options.placeholder; - - if (typeof(v) == 'undefined' || v === '') - return undefined; - - if (typeof(v) == 'string' && $.isArray(this.choices)) - { - for (var i = 0; i < this.choices.length; i++) - if (v === this.choices[i][0]) - return this.choices[i][1]; - } - else if (v === true) - return L.tr('yes'); - else if (v === false) - return L.tr('no'); - else if ($.isArray(v)) - return v.join(', '); - - return v; - }, - - changed: function(sid) - { - var a = this.ucivalue(sid); - var b = this.formvalue(sid); - - if (typeof(a) != typeof(b)) - return true; - - if ($.isArray(a)) - { - if (a.length != b.length) - return true; - - for (var i = 0; i < a.length; i++) - if (a[i] != b[i]) - return true; - - return false; - } - else if ($.isPlainObject(a)) - { - for (var k in a) - if (!(k in b)) - return true; - - for (var k in b) - if (!(k in a) || a[k] !== b[k]) - return true; - - return false; - } - - return (a != b); - }, - - save: function(sid) - { - var uci = this.ucipath(sid); - - if (this.instance[sid].disabled) - { - if (!this.options.keep) - return this.ownerMap.set(uci.config, uci.section, uci.option, undefined); - - return false; - } - - var chg = this.changed(sid); - var val = this.formvalue(sid); - - if (chg) - this.ownerMap.set(uci.config, uci.section, uci.option, val); - - return chg; - }, - - findSectionID: function($elem) - { - return this.ownerSection.findParentSectionIDs($elem)[0]; - }, - - setError: function($elem, msg, msgargs) - { - var $field = $elem.parents('.luci2-field:first'); - var $error = $field.find('.luci2-field-error:first'); - - if (typeof(msg) == 'string' && msg.length > 0) - { - $field.addClass('luci2-form-error'); - $elem.parent().addClass('has-error'); - - $error.text(msg.format.apply(msg, msgargs)).show(); - $field.trigger('validate'); - - return false; - } - else - { - $elem.parent().removeClass('has-error'); - - var $other_errors = $field.find('.has-error'); - if ($other_errors.length == 0) - { - $field.removeClass('luci2-form-error'); - $error.text('').hide(); - $field.trigger('validate'); - - return true; - } - - return false; - } - }, - - handleValidate: function(ev) - { - var $elem = $(this); - - var d = ev.data; - var rv = true; - var val = $elem.val(); - var vstack = d.vstack; - - if (vstack && typeof(vstack[0]) == 'function') - { - delete validation.message; - - if ((val.length == 0 && !d.opt)) - { - rv = d.self.setError($elem, L.tr('Field must not be empty')); - } - else if (val.length > 0 && !vstack[0].apply(val, vstack[1])) - { - rv = d.self.setError($elem, validation.message, vstack[1]); - } - else - { - rv = d.self.setError($elem); - } - } - - if (rv) - { - var sid = d.self.findSectionID($elem); - - for (var field in d.self.rdependency) - { - d.self.rdependency[field].toggle(sid); - d.self.rdependency[field].validate(sid); - } - - d.self.ownerSection.tabtoggle(sid); - } - - return rv; - }, - - attachEvents: function(sid, elem) - { - var evdata = { - self: this, - opt: this.options.optional - }; - - if (this.events) - for (var evname in this.events) - elem.on(evname, evdata, this.events[evname]); - - if (typeof(this.options.datatype) == 'undefined' && $.isEmptyObject(this.rdependency)) - return elem; - - var vstack; - if (typeof(this.options.datatype) == 'string') - { - try { - evdata.vstack = L.cbi.validation.compile(this.options.datatype); - } catch(e) { }; - } - else if (typeof(this.options.datatype) == 'function') - { - var vfunc = this.options.datatype; - evdata.vstack = [ function(elem) { - var rv = vfunc(this, elem); - if (rv !== true) - validation.message = rv; - return (rv === true); - }, [ elem ] ]; - } - - if (elem.prop('tagName') == 'SELECT') - { - elem.change(evdata, this.handleValidate); - } - else if (elem.prop('tagName') == 'INPUT' && elem.attr('type') == 'checkbox') - { - elem.click(evdata, this.handleValidate); - elem.blur(evdata, this.handleValidate); - } - else - { - elem.keyup(evdata, this.handleValidate); - elem.blur(evdata, this.handleValidate); - } - - elem.addClass('luci2-field-validate') - .on('validate', evdata, this.handleValidate); - - return elem; - }, - - validate: function(sid) - { - var i = this.instance[sid]; - - i.widget.find('.luci2-field-validate').trigger('validate'); - - return (i.disabled || i.error.text() == ''); - }, - - depends: function(d, v, add) - { - var dep; - - if ($.isArray(d)) - { - dep = { }; - for (var i = 0; i < d.length; i++) - { - if (typeof(d[i]) == 'string') - dep[d[i]] = true; - else if (d[i] instanceof L.cbi.AbstractValue) - dep[d[i].name] = true; - } - } - else if (d instanceof L.cbi.AbstractValue) - { - dep = { }; - dep[d.name] = (typeof(v) == 'undefined') ? true : v; - } - else if (typeof(d) == 'object') - { - dep = d; - } - else if (typeof(d) == 'string') - { - dep = { }; - dep[d] = (typeof(v) == 'undefined') ? true : v; - } - - if (!dep || $.isEmptyObject(dep)) - return this; - - for (var field in dep) - { - var f = this.ownerSection.fields[field]; - if (f) - f.rdependency[this.name] = this; - else - delete dep[field]; - } - - if ($.isEmptyObject(dep)) - return this; - - if (!add || !this.dependencies.length) - this.dependencies.push(dep); - else - for (var i = 0; i < this.dependencies.length; i++) - $.extend(this.dependencies[i], dep); - - return this; - }, - - toggle: function(sid) - { - var d = this.dependencies; - var i = this.instance[sid]; - - if (!d.length) - return true; - - for (var n = 0; n < d.length; n++) - { - var rv = true; - - for (var field in d[n]) - { - var val = this.ownerSection.fields[field].formvalue(sid); - var cmp = d[n][field]; - - if (typeof(cmp) == 'boolean') - { - if (cmp == (typeof(val) == 'undefined' || val === '' || val === false)) - { - rv = false; - break; - } - } - else if (typeof(cmp) == 'string' || typeof(cmp) == 'number') - { - if (val != cmp) - { - rv = false; - break; - } - } - else if (typeof(cmp) == 'function') - { - if (!cmp(val)) - { - rv = false; - break; - } - } - else if (cmp instanceof RegExp) - { - if (!cmp.test(val)) - { - rv = false; - break; - } - } - } - - if (rv) - { - if (i.disabled) - { - i.disabled = false; - i.top.removeClass('luci2-field-disabled'); - i.top.fadeIn(); - } - - return true; - } - } - - if (!i.disabled) - { - i.disabled = true; - i.top.is(':visible') ? i.top.fadeOut() : i.top.hide(); - i.top.addClass('luci2-field-disabled'); - } - - return false; - } - }); - - this.cbi.CheckboxValue = this.cbi.AbstractValue.extend({ - widget: function(sid) - { - var o = this.options; - - if (typeof(o.enabled) == 'undefined') o.enabled = '1'; - if (typeof(o.disabled) == 'undefined') o.disabled = '0'; - - var i = $('<input />') - .attr('id', this.id(sid)) - .attr('type', 'checkbox') - .prop('checked', this.ucivalue(sid)); - - return $('<div />') - .addClass('checkbox') - .append(this.attachEvents(sid, i)); - }, - - ucivalue: function(sid) - { - var v = this.callSuper('ucivalue', sid); - - if (typeof(v) == 'boolean') - return v; - - return (v == this.options.enabled); - }, - - formvalue: function(sid) - { - var v = $('#' + this.id(sid)).prop('checked'); - - if (typeof(v) == 'undefined') - return !!this.options.initial; - - return v; - }, - - save: function(sid) - { - var uci = this.ucipath(sid); - - if (this.instance[sid].disabled) - { - if (!this.options.keep) - return this.ownerMap.set(uci.config, uci.section, uci.option, undefined); - - return false; - } - - var chg = this.changed(sid); - var val = this.formvalue(sid); - - if (chg) - { - if (this.options.optional && val == this.options.initial) - this.ownerMap.set(uci.config, uci.section, uci.option, undefined); - else - this.ownerMap.set(uci.config, uci.section, uci.option, val ? this.options.enabled : this.options.disabled); - } - - return chg; - } - }); - - this.cbi.InputValue = this.cbi.AbstractValue.extend({ - widget: function(sid) - { - var i = $('<input />') - .addClass('form-control') - .attr('id', this.id(sid)) - .attr('type', 'text') - .attr('placeholder', this.options.placeholder) - .val(this.ucivalue(sid)); - - return this.attachEvents(sid, i); - } - }); - - this.cbi.PasswordValue = this.cbi.AbstractValue.extend({ - widget: function(sid) - { - var i = $('<input />') - .addClass('form-control') - .attr('id', this.id(sid)) - .attr('type', 'password') - .attr('placeholder', this.options.placeholder) - .val(this.ucivalue(sid)); - - var t = $('<span />') - .addClass('input-group-btn') - .append(L.ui.button(L.tr('Reveal'), 'default') - .click(function(ev) { - var b = $(this); - var i = b.parent().prev(); - var t = i.attr('type'); - b.text(t == 'password' ? L.tr('Hide') : L.tr('Reveal')); - i.attr('type', (t == 'password') ? 'text' : 'password'); - b = i = t = null; - })); - - this.attachEvents(sid, i); - - return $('<div />') - .addClass('input-group') - .append(i) - .append(t); - } - }); - - this.cbi.ListValue = this.cbi.AbstractValue.extend({ - widget: function(sid) - { - var s = $('<select />') - .addClass('form-control'); - - if (this.options.optional && !this.has_empty) - $('<option />') - .attr('value', '') - .text(L.tr('-- Please choose --')) - .appendTo(s); - - if (this.choices) - for (var i = 0; i < this.choices.length; i++) - $('<option />') - .attr('value', this.choices[i][0]) - .text(this.choices[i][1]) - .appendTo(s); - - s.attr('id', this.id(sid)).val(this.ucivalue(sid)); - - return this.attachEvents(sid, s); - }, - - value: function(k, v) - { - if (!this.choices) - this.choices = [ ]; - - if (k == '') - this.has_empty = true; - - this.choices.push([k, v || k]); - return this; - } - }); - - this.cbi.MultiValue = this.cbi.ListValue.extend({ - widget: function(sid) - { - var v = this.ucivalue(sid); - var t = $('<div />').attr('id', this.id(sid)); - - if (!$.isArray(v)) - v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ]; - - var s = { }; - for (var i = 0; i < v.length; i++) - s[v[i]] = true; - - if (this.choices) - for (var i = 0; i < this.choices.length; i++) - { - $('<label />') - .addClass('checkbox') - .append($('<input />') - .attr('type', 'checkbox') - .attr('value', this.choices[i][0]) - .prop('checked', s[this.choices[i][0]])) - .append(this.choices[i][1]) - .appendTo(t); - } - - return t; - }, - - formvalue: function(sid) - { - var rv = [ ]; - var fields = $('#' + this.id(sid) + ' > label > input'); - - for (var i = 0; i < fields.length; i++) - if (fields[i].checked) - rv.push(fields[i].getAttribute('value')); - - return rv; - }, - - textvalue: function(sid) - { - var v = this.formvalue(sid); - var c = { }; - - if (this.choices) - for (var i = 0; i < this.choices.length; i++) - c[this.choices[i][0]] = this.choices[i][1]; - - var t = [ ]; - - for (var i = 0; i < v.length; i++) - t.push(c[v[i]] || v[i]); - - return t.join(', '); - } - }); - - this.cbi.ComboBox = this.cbi.AbstractValue.extend({ - _change: function(ev) - { - var s = ev.target; - var self = ev.data.self; - - if (s.selectedIndex == (s.options.length - 1)) - { - ev.data.select.hide(); - ev.data.input.show().focus(); - ev.data.input.val(''); - } - else if (self.options.optional && s.selectedIndex == 0) - { - ev.data.input.val(''); - } - else - { - ev.data.input.val(ev.data.select.val()); - } - - ev.stopPropagation(); - }, - - _blur: function(ev) - { - var seen = false; - var val = this.value; - var self = ev.data.self; - - ev.data.select.empty(); - - if (self.options.optional && !self.has_empty) - $('<option />') - .attr('value', '') - .text(L.tr('-- please choose --')) - .appendTo(ev.data.select); - - if (self.choices) - for (var i = 0; i < self.choices.length; i++) - { - if (self.choices[i][0] == val) - seen = true; - - $('<option />') - .attr('value', self.choices[i][0]) - .text(self.choices[i][1]) - .appendTo(ev.data.select); - } - - if (!seen && val != '') - $('<option />') - .attr('value', val) - .text(val) - .appendTo(ev.data.select); - - $('<option />') - .attr('value', ' ') - .text(L.tr('-- custom --')) - .appendTo(ev.data.select); - - ev.data.input.hide(); - ev.data.select.val(val).show().blur(); - }, - - _enter: function(ev) - { - if (ev.which != 13) - return true; - - ev.preventDefault(); - ev.data.self._blur(ev); - return false; - }, - - widget: function(sid) - { - var d = $('<div />') - .attr('id', this.id(sid)); - - var t = $('<input />') - .addClass('form-control') - .attr('type', 'text') - .hide() - .appendTo(d); - - var s = $('<select />') - .addClass('form-control') - .appendTo(d); - - var evdata = { - self: this, - input: t, - select: s - }; - - s.change(evdata, this._change); - t.blur(evdata, this._blur); - t.keydown(evdata, this._enter); - - t.val(this.ucivalue(sid)); - t.blur(); - - this.attachEvents(sid, t); - this.attachEvents(sid, s); - - return d; - }, - - value: function(k, v) - { - if (!this.choices) - this.choices = [ ]; - - if (k == '') - this.has_empty = true; - - this.choices.push([k, v || k]); - return this; - }, - - formvalue: function(sid) - { - var v = $('#' + this.id(sid)).children('input').val(); - return (v == '') ? undefined : v; - } - }); - - this.cbi.DynamicList = this.cbi.ComboBox.extend({ - _redraw: function(focus, add, del, s) - { - var v = s.values || [ ]; - delete s.values; - - $(s.parent).children('div.input-group').children('input').each(function(i) { - if (i != del) - v.push(this.value || ''); - }); - - $(s.parent).empty(); - - if (add >= 0) - { - focus = add + 1; - v.splice(focus, 0, ''); - } - else if (v.length == 0) - { - focus = 0; - v.push(''); - } - - for (var i = 0; i < v.length; i++) - { - var evdata = { - sid: s.sid, - self: s.self, - parent: s.parent, - index: i, - remove: ((i+1) < v.length) - }; - - var btn; - if (evdata.remove) - btn = L.ui.button('–', 'danger').click(evdata, this._btnclick); - else - btn = L.ui.button('+', 'success').click(evdata, this._btnclick); - - if (this.choices) - { - var txt = $('<input />') - .addClass('form-control') - .attr('type', 'text') - .hide(); - - var sel = $('<select />') - .addClass('form-control'); - - $('<div />') - .addClass('input-group') - .append(txt) - .append(sel) - .append($('<span />') - .addClass('input-group-btn') - .append(btn)) - .appendTo(s.parent); - - evdata.input = this.attachEvents(s.sid, txt); - evdata.select = this.attachEvents(s.sid, sel); - - sel.change(evdata, this._change); - txt.blur(evdata, this._blur); - txt.keydown(evdata, this._keydown); - - txt.val(v[i]); - txt.blur(); - - if (i == focus || -(i+1) == focus) - sel.focus(); - - sel = txt = null; - } - else - { - var f = $('<input />') - .attr('type', 'text') - .attr('index', i) - .attr('placeholder', (i == 0) ? this.options.placeholder : '') - .addClass('form-control') - .keydown(evdata, this._keydown) - .keypress(evdata, this._keypress) - .val(v[i]); - - $('<div />') - .addClass('input-group') - .append(f) - .append($('<span />') - .addClass('input-group-btn') - .append(btn)) - .appendTo(s.parent); - - if (i == focus) - { - f.focus(); - } - else if (-(i+1) == focus) - { - f.focus(); - - /* force cursor to end */ - var val = f.val(); - f.val(' '); - f.val(val); - } - - evdata.input = this.attachEvents(s.sid, f); - - f = null; - } - - evdata = null; - } - - s = null; - }, - - _keypress: function(ev) - { - switch (ev.which) - { - /* backspace, delete */ - case 8: - case 46: - if (ev.data.input.val() == '') - { - ev.preventDefault(); - return false; - } - - return true; - - /* enter, arrow up, arrow down */ - case 13: - case 38: - case 40: - ev.preventDefault(); - return false; - } - - return true; - }, - - _keydown: function(ev) - { - var input = ev.data.input; - - switch (ev.which) - { - /* backspace, delete */ - case 8: - case 46: - if (input.val().length == 0) - { - ev.preventDefault(); - - var index = ev.data.index; - var focus = index; - - if (ev.which == 8) - focus = -focus; - - ev.data.self._redraw(focus, -1, index, ev.data); - return false; - } - - break; - - /* enter */ - case 13: - ev.data.self._redraw(NaN, ev.data.index, -1, ev.data); - break; - - /* arrow up */ - case 38: - var prev = input.parent().prevAll('div.input-group:first').children('input'); - if (prev.is(':visible')) - prev.focus(); - else - prev.next('select').focus(); - break; - - /* arrow down */ - case 40: - var next = input.parent().nextAll('div.input-group:first').children('input'); - if (next.is(':visible')) - next.focus(); - else - next.next('select').focus(); - break; - } - - return true; - }, - - _btnclick: function(ev) - { - if (!this.getAttribute('disabled')) - { - if (ev.data.remove) - { - var index = ev.data.index; - ev.data.self._redraw(-index, -1, index, ev.data); - } - else - { - ev.data.self._redraw(NaN, ev.data.index, -1, ev.data); - } - } - - return false; - }, - - widget: function(sid) - { - this.options.optional = true; - - var v = this.ucivalue(sid); - - if (!$.isArray(v)) - v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ]; - - var d = $('<div />') - .attr('id', this.id(sid)) - .addClass('cbi-input-dynlist'); - - this._redraw(NaN, -1, -1, { - self: this, - parent: d[0], - values: v, - sid: sid - }); - - return d; - }, - - ucivalue: function(sid) - { - var v = this.callSuper('ucivalue', sid); - - if (!$.isArray(v)) - v = (typeof(v) != 'undefined') ? v.toString().split(/\s+/) : [ ]; - - return v; - }, - - formvalue: function(sid) - { - var rv = [ ]; - var fields = $('#' + this.id(sid) + ' input'); - - for (var i = 0; i < fields.length; i++) - if (typeof(fields[i].value) == 'string' && fields[i].value.length) - rv.push(fields[i].value); - - return rv; - } - }); - - this.cbi.DummyValue = this.cbi.AbstractValue.extend({ - widget: function(sid) - { - return $('<div />') - .addClass('form-control-static') - .attr('id', this.id(sid)) - .html(this.ucivalue(sid) || this.label('placeholder')); - }, - - formvalue: function(sid) - { - return this.ucivalue(sid); - } - }); - - this.cbi.ButtonValue = this.cbi.AbstractValue.extend({ - widget: function(sid) - { - this.options.optional = true; - - var btn = $('<button />') - .addClass('btn btn-default') - .attr('id', this.id(sid)) - .attr('type', 'button') - .text(this.label('text')); - - return this.attachEvents(sid, btn); - } - }); - - this.cbi.NetworkList = this.cbi.AbstractValue.extend({ - load: function(sid) - { - return L.NetworkModel.init(); - }, - - _device_icon: function(dev) - { - return $('<img />') - .attr('src', dev.icon()) - .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?')); - }, - - widget: function(sid) - { - var id = this.id(sid); - var ul = $('<ul />') - .attr('id', id) - .addClass('list-unstyled'); - - var itype = this.options.multiple ? 'checkbox' : 'radio'; - var value = this.ucivalue(sid); - var check = { }; - - if (!this.options.multiple) - check[value] = true; - else - for (var i = 0; i < value.length; i++) - check[value[i]] = true; - - var interfaces = L.NetworkModel.getInterfaces(); - - for (var i = 0; i < interfaces.length; i++) - { - var iface = interfaces[i]; - - $('<li />') - .append($('<label />') - .addClass(itype + ' inline') - .append(this.attachEvents(sid, $('<input />') - .attr('name', itype + id) - .attr('type', itype) - .attr('value', iface.name()) - .prop('checked', !!check[iface.name()]))) - .append(iface.renderBadge())) - .appendTo(ul); - } - - if (!this.options.multiple) - { - $('<li />') - .append($('<label />') - .addClass(itype + ' inline text-muted') - .append(this.attachEvents(sid, $('<input />') - .attr('name', itype + id) - .attr('type', itype) - .attr('value', '') - .prop('checked', $.isEmptyObject(check)))) - .append(L.tr('unspecified'))) - .appendTo(ul); - } - - return ul; - }, - - ucivalue: function(sid) - { - var v = this.callSuper('ucivalue', sid); - - if (!this.options.multiple) - { - if ($.isArray(v)) - { - return v[0]; - } - else if (typeof(v) == 'string') - { - v = v.match(/\S+/); - return v ? v[0] : undefined; - } - - return v; - } - else - { - if (typeof(v) == 'string') - v = v.match(/\S+/g); - - return v || [ ]; - } - }, - - formvalue: function(sid) - { - var inputs = $('#' + this.id(sid) + ' input'); - - if (!this.options.multiple) - { - for (var i = 0; i < inputs.length; i++) - if (inputs[i].checked && inputs[i].value !== '') - return inputs[i].value; - - return undefined; - } - - var rv = [ ]; - - for (var i = 0; i < inputs.length; i++) - if (inputs[i].checked) - rv.push(inputs[i].value); - - return rv.length ? rv : undefined; - } - }); - - this.cbi.DeviceList = this.cbi.NetworkList.extend({ - handleFocus: function(ev) - { - var self = ev.data.self; - var input = $(this); - - input.parent().prev().prop('checked', true); - }, - - handleBlur: function(ev) - { - ev.which = 10; - ev.data.self.handleKeydown.call(this, ev); - }, - - handleKeydown: function(ev) - { - if (ev.which != 10 && ev.which != 13) - return; - - var sid = ev.data.sid; - var self = ev.data.self; - var input = $(this); - var ifnames = L.toArray(input.val()); - - if (!ifnames.length) - return; - - L.NetworkModel.createDevice(ifnames[0]); - - self._redraw(sid, $('#' + self.id(sid)), ifnames[0]); - }, - - load: function(sid) - { - return L.NetworkModel.init(); - }, - - _redraw: function(sid, ul, sel) - { - var id = ul.attr('id'); - var devs = L.NetworkModel.getDevices(); - var iface = L.NetworkModel.getInterface(sid); - var itype = this.options.multiple ? 'checkbox' : 'radio'; - var check = { }; - - if (!sel) - { - for (var i = 0; i < devs.length; i++) - if (devs[i].isInNetwork(iface)) - check[devs[i].name()] = true; - } - else - { - if (this.options.multiple) - check = L.toObject(this.formvalue(sid)); - - check[sel] = true; - } - - ul.empty(); - - for (var i = 0; i < devs.length; i++) - { - var dev = devs[i]; - - if (dev.isBridge() && this.options.bridges === false) - continue; - - if (!dev.isBridgeable() && this.options.multiple) - continue; - - var badge = $('<span />') - .addClass('badge') - .append($('<img />').attr('src', dev.icon())) - .append(' %s: %s'.format(dev.name(), dev.description())); - - //var ifcs = dev.getInterfaces(); - //if (ifcs.length) - //{ - // for (var j = 0; j < ifcs.length; j++) - // badge.append((j ? ', ' : ' (') + ifcs[j].name()); - // - // badge.append(')'); - //} - - $('<li />') - .append($('<label />') - .addClass(itype + ' inline') - .append($('<input />') - .attr('name', itype + id) - .attr('type', itype) - .attr('value', dev.name()) - .prop('checked', !!check[dev.name()])) - .append(badge)) - .appendTo(ul); - } - - - $('<li />') - .append($('<label />') - .attr('for', 'custom' + id) - .addClass(itype + ' inline') - .append($('<input />') - .attr('name', itype + id) - .attr('type', itype) - .attr('value', '')) - .append($('<span />') - .addClass('badge') - .append($('<input />') - .attr('id', 'custom' + id) - .attr('type', 'text') - .attr('placeholder', L.tr('Custom device …')) - .on('focus', { self: this, sid: sid }, this.handleFocus) - .on('blur', { self: this, sid: sid }, this.handleBlur) - .on('keydown', { self: this, sid: sid }, this.handleKeydown)))) - .appendTo(ul); - - if (!this.options.multiple) - { - $('<li />') - .append($('<label />') - .addClass(itype + ' inline text-muted') - .append($('<input />') - .attr('name', itype + id) - .attr('type', itype) - .attr('value', '') - .prop('checked', $.isEmptyObject(check))) - .append(L.tr('unspecified'))) - .appendTo(ul); - } - }, - - widget: function(sid) - { - var id = this.id(sid); - var ul = $('<ul />') - .attr('id', id) - .addClass('list-unstyled'); - - this._redraw(sid, ul); - - return ul; - }, - - save: function(sid) - { - if (this.instance[sid].disabled) - return; - - var ifnames = this.formvalue(sid); - //if (!ifnames) - // return; - - var iface = L.NetworkModel.getInterface(sid); - if (!iface) - return; - - iface.setDevices($.isArray(ifnames) ? ifnames : [ ifnames ]); - } - }); - - - this.cbi.AbstractSection = this.ui.AbstractWidget.extend({ - id: function() - { - var s = [ arguments[0], this.ownerMap.uci_package, this.uci_type ]; - - for (var i = 1; i < arguments.length && typeof(arguments[i]) == 'string'; i++) - s.push(arguments[i].replace(/\./g, '_')); - - return s.join('_'); - }, - - option: function(widget, name, options) - { - if (this.tabs.length == 0) - this.tab({ id: '__default__', selected: true }); - - return this.taboption('__default__', widget, name, options); - }, - - tab: function(options) - { - if (options.selected) - this.tabs.selected = this.tabs.length; - - this.tabs.push({ - id: options.id, - caption: options.caption, - description: options.description, - fields: [ ], - li: { } - }); - }, - - taboption: function(tabid, widget, name, options) - { - var tab; - for (var i = 0; i < this.tabs.length; i++) - { - if (this.tabs[i].id == tabid) - { - tab = this.tabs[i]; - break; - } - } - - if (!tab) - throw 'Cannot append to unknown tab ' + tabid; - - var w = widget ? new widget(name, options) : null; - - if (!(w instanceof L.cbi.AbstractValue)) - throw 'Widget must be an instance of AbstractValue'; - - w.ownerSection = this; - w.ownerMap = this.ownerMap; - - this.fields[name] = w; - tab.fields.push(w); - - return w; - }, - - tabtoggle: function(sid) - { - for (var i = 0; i < this.tabs.length; i++) - { - var tab = this.tabs[i]; - var elem = $('#' + this.id('nodetab', sid, tab.id)); - var empty = true; - - for (var j = 0; j < tab.fields.length; j++) - { - if (tab.fields[j].active(sid)) - { - empty = false; - break; - } - } - - if (empty && elem.is(':visible')) - elem.fadeOut(); - else if (!empty) - elem.fadeIn(); - } - }, - - validate: function(parent_sid) - { - var s = this.getUCISections(parent_sid); - var n = 0; - - for (var i = 0; i < s.length; i++) - { - var $item = $('#' + this.id('sectionitem', s[i]['.name'])); - - $item.find('.luci2-field-validate').trigger('validate'); - n += $item.find('.luci2-field.luci2-form-error').not('.luci2-field-disabled').length; - } - - return (n == 0); - }, - - load: function(parent_sid) - { - var deferreds = [ ]; - - var s = this.getUCISections(parent_sid); - for (var i = 0; i < s.length; i++) - { - for (var f in this.fields) - { - if (typeof(this.fields[f].load) != 'function') - continue; - - var rv = this.fields[f].load(s[i]['.name']); - if (L.isDeferred(rv)) - deferreds.push(rv); - } - - for (var j = 0; j < this.subsections.length; j++) - { - var rv = this.subsections[j].load(s[i]['.name']); - deferreds.push.apply(deferreds, rv); - } - } - - return deferreds; - }, - - save: function(parent_sid) - { - var deferreds = [ ]; - var s = this.getUCISections(parent_sid); - - for (i = 0; i < s.length; i++) - { - if (!this.options.readonly) - { - for (var f in this.fields) - { - if (typeof(this.fields[f].save) != 'function') - continue; - - var rv = this.fields[f].save(s[i]['.name']); - if (L.isDeferred(rv)) - deferreds.push(rv); - } - } - - for (var j = 0; j < this.subsections.length; j++) - { - var rv = this.subsections[j].save(s[i]['.name']); - deferreds.push.apply(deferreds, rv); - } - } - - return deferreds; - }, - - teaser: function(sid) - { - var tf = this.teaser_fields; - - if (!tf) - { - tf = this.teaser_fields = [ ]; - - if ($.isArray(this.options.teasers)) - { - for (var i = 0; i < this.options.teasers.length; i++) - { - var f = this.options.teasers[i]; - if (f instanceof L.cbi.AbstractValue) - tf.push(f); - else if (typeof(f) == 'string' && this.fields[f] instanceof L.cbi.AbstractValue) - tf.push(this.fields[f]); - } - } - else - { - for (var i = 0; tf.length <= 5 && i < this.tabs.length; i++) - for (var j = 0; tf.length <= 5 && j < this.tabs[i].fields.length; j++) - tf.push(this.tabs[i].fields[j]); - } - } - - var t = ''; - - for (var i = 0; i < tf.length; i++) - { - if (tf[i].instance[sid] && tf[i].instance[sid].disabled) - continue; - - var n = tf[i].options.caption || tf[i].name; - var v = tf[i].textvalue(sid); - - if (typeof(v) == 'undefined') - continue; - - t = t + '%s%s: <strong>%s</strong>'.format(t ? ' | ' : '', n, v); - } - - return t; - }, - - findAdditionalUCIPackages: function() - { - var packages = [ ]; - - for (var i = 0; i < this.tabs.length; i++) - for (var j = 0; j < this.tabs[i].fields.length; j++) - if (this.tabs[i].fields[j].options.uci_package) - packages.push(this.tabs[i].fields[j].options.uci_package); - - return packages; - }, - - findParentSectionIDs: function($elem) - { - var rv = [ ]; - var $parents = $elem.parents('.luci2-section-item'); - - for (var i = 0; i < $parents.length; i++) - rv.push($parents[i].getAttribute('data-luci2-sid')); - - return rv; - } - }); - - this.cbi.TypedSection = this.cbi.AbstractSection.extend({ - init: function(uci_type, options) - { - this.uci_type = uci_type; - this.options = options; - this.tabs = [ ]; - this.fields = { }; - this.subsections = [ ]; - this.active_panel = { }; - this.active_tab = { }; - - this.instance = { }; - }, - - filter: function(section, parent_sid) - { - return true; - }, - - sort: function(section1, section2) - { - return 0; - }, - - subsection: function(widget, uci_type, options) - { - var w = widget ? new widget(uci_type, options) : null; - - if (!(w instanceof L.cbi.AbstractSection)) - throw 'Widget must be an instance of AbstractSection'; - - w.ownerSection = this; - w.ownerMap = this.ownerMap; - w.index = this.subsections.length; - - this.subsections.push(w); - return w; - }, - - getUCISections: function(parent_sid) - { - var s1 = L.uci.sections(this.ownerMap.uci_package); - var s2 = [ ]; - - for (var i = 0; i < s1.length; i++) - if (s1[i]['.type'] == this.uci_type) - if (this.filter(s1[i], parent_sid)) - s2.push(s1[i]); - - s2.sort(this.sort); - - return s2; - }, - - add: function(name, parent_sid) - { - return this.ownerMap.add(this.ownerMap.uci_package, this.uci_type, name); - }, - - remove: function(sid, parent_sid) - { - return this.ownerMap.remove(this.ownerMap.uci_package, sid); - }, - - handleAdd: function(ev) - { - var addb = $(this); - var name = undefined; - var self = ev.data.self; - var sid = self.findParentSectionIDs(addb)[0]; - - if (addb.prev().prop('nodeName') == 'INPUT') - name = addb.prev().val(); - - if (addb.prop('disabled') || name === '') - return; - - L.ui.saveScrollTop(); - - self.setPanelIndex(sid, -1); - self.ownerMap.save(); - - ev.data.sid = self.add(name, sid); - ev.data.type = self.uci_type; - ev.data.name = name; - - self.trigger('add', ev); - - self.ownerMap.redraw(); - - L.ui.restoreScrollTop(); - }, - - handleRemove: function(ev) - { - var self = ev.data.self; - var sids = self.findParentSectionIDs($(this)); - - if (sids.length) - { - L.ui.saveScrollTop(); - - ev.sid = sids[0]; - ev.parent_sid = sids[1]; - - self.trigger('remove', ev); - - self.ownerMap.save(); - self.remove(ev.sid, ev.parent_sid); - self.ownerMap.redraw(); - - L.ui.restoreScrollTop(); - } - - ev.stopPropagation(); - }, - - handleSID: function(ev) - { - var self = ev.data.self; - var text = $(this); - var addb = text.next(); - var errt = addb.next(); - var name = text.val(); - - if (!/^[a-zA-Z0-9_]*$/.test(name)) - { - errt.text(L.tr('Invalid section name')).show(); - text.addClass('error'); - addb.prop('disabled', true); - return false; - } - - if (L.uci.get(self.ownerMap.uci_package, name)) - { - errt.text(L.tr('Name already used')).show(); - text.addClass('error'); - addb.prop('disabled', true); - return false; - } - - errt.text('').hide(); - text.removeClass('error'); - addb.prop('disabled', false); - return true; - }, - - handleTab: function(ev) - { - var self = ev.data.self; - var $tab = $(this); - var sid = self.findParentSectionIDs($tab)[0]; - - self.active_tab[sid] = $tab.parent().index(); - }, - - handleTabValidate: function(ev) - { - var $pane = $(ev.delegateTarget); - var $badge = $pane.parent() - .children('.nav-tabs') - .children('li') - .eq($pane.index() - 1) // item #1 is the <ul> - .find('.badge:first'); - - var err_count = $pane.find('.luci2-field.luci2-form-error').not('.luci2-field-disabled').length; - if (err_count > 0) - $badge - .text(err_count) - .attr('title', L.trp('1 Error', '%d Errors', err_count).format(err_count)) - .show(); - else - $badge.hide(); - }, - - handlePanelValidate: function(ev) - { - var $elem = $(this); - var $badge = $elem - .prevAll('.luci2-section-header:first') - .children('.luci2-section-teaser') - .find('.badge:first'); - - var err_count = $elem.find('.luci2-field.luci2-form-error').not('.luci2-field-disabled').length; - if (err_count > 0) - $badge - .text(err_count) - .attr('title', L.trp('1 Error', '%d Errors', err_count).format(err_count)) - .show(); - else - $badge.hide(); - }, - - handlePanelCollapse: function(ev) - { - var self = ev.data.self; - - var $items = $(ev.delegateTarget).children('.luci2-section-item'); - - var $this_panel = $(ev.target); - var $this_teaser = $this_panel.prevAll('.luci2-section-header:first').children('.luci2-section-teaser'); - - var $prev_panel = $items.children('.luci2-section-panel.in'); - var $prev_teaser = $prev_panel.prevAll('.luci2-section-header:first').children('.luci2-section-teaser'); - - var sids = self.findParentSectionIDs($prev_panel); - - self.setPanelIndex(sids[1], $this_panel.parent().index()); - - $prev_panel - .removeClass('in') - .addClass('collapse'); - - $prev_teaser - .show() - .children('span:last') - .empty() - .append(self.teaser(sids[0])); - - $this_teaser - .hide(); - - ev.stopPropagation(); - }, - - handleSort: function(ev) - { - var self = ev.data.self; - - var $item = $(this).parents('.luci2-section-item:first'); - var $next = ev.data.up ? $item.prev() : $item.next(); - - if ($item.length && $next.length) - { - var cur_sid = $item.attr('data-luci2-sid'); - var new_sid = $next.attr('data-luci2-sid'); - - L.uci.swap(self.ownerMap.uci_package, cur_sid, new_sid); - - self.ownerMap.save(); - self.ownerMap.redraw(); - } - - ev.stopPropagation(); - }, - - getPanelIndex: function(parent_sid) - { - return (this.active_panel[parent_sid || '__top__'] || 0); - }, - - setPanelIndex: function(parent_sid, new_index) - { - if (typeof(new_index) == 'number') - this.active_panel[parent_sid || '__top__'] = new_index; - }, - - renderAdd: function() - { - if (!this.options.addremove) - return null; - - var text = L.tr('Add section'); - var ttip = L.tr('Create new section...'); - - if ($.isArray(this.options.add_caption)) - text = this.options.add_caption[0], ttip = this.options.add_caption[1]; - else if (typeof(this.options.add_caption) == 'string') - text = this.options.add_caption, ttip = ''; - - var add = $('<div />'); - - if (this.options.anonymous === false) - { - $('<input />') - .addClass('cbi-input-text') - .attr('type', 'text') - .attr('placeholder', ttip) - .blur({ self: this }, this.handleSID) - .keyup({ self: this }, this.handleSID) - .appendTo(add); - - $('<img />') - .attr('src', L.globals.resource + '/icons/cbi/add.gif') - .attr('title', text) - .addClass('cbi-button') - .click({ self: this }, this.handleAdd) - .appendTo(add); - - $('<div />') - .addClass('cbi-value-error') - .hide() - .appendTo(add); - } - else - { - L.ui.button(text, 'success', ttip) - .click({ self: this }, this.handleAdd) - .appendTo(add); - } - - return add; - }, - - renderRemove: function(index) - { - if (!this.options.addremove) - return null; - - var text = L.tr('Remove'); - var ttip = L.tr('Remove this section'); - - if ($.isArray(this.options.remove_caption)) - text = this.options.remove_caption[0], ttip = this.options.remove_caption[1]; - else if (typeof(this.options.remove_caption) == 'string') - text = this.options.remove_caption, ttip = ''; - - return L.ui.button(text, 'danger', ttip) - .click({ self: this, index: index }, this.handleRemove); - }, - - renderSort: function(index) - { - if (!this.options.sortable) - return null; - - var b1 = L.ui.button('↑', 'info', L.tr('Move up')) - .click({ self: this, index: index, up: true }, this.handleSort); - - var b2 = L.ui.button('↓', 'info', L.tr('Move down')) - .click({ self: this, index: index, up: false }, this.handleSort); - - return b1.add(b2); - }, - - renderCaption: function() - { - return $('<h3 />') - .addClass('panel-title') - .append(this.label('caption') || this.uci_type); - }, - - renderDescription: function() - { - var text = this.label('description'); - - if (text) - return $('<div />') - .addClass('luci2-section-description') - .text(text); - - return null; - }, - - renderTeaser: function(sid, index) - { - if (this.options.collabsible || this.ownerMap.options.collabsible) - { - return $('<div />') - .attr('id', this.id('teaser', sid)) - .addClass('luci2-section-teaser well well-sm') - .append($('<span />') - .addClass('badge')) - .append($('<span />')); - } - - return null; - }, - - renderHead: function(condensed) - { - if (condensed) - return null; - - return $('<div />') - .addClass('panel-heading') - .append(this.renderCaption()) - .append(this.renderDescription()); - }, - - renderTabDescription: function(sid, index, tab_index) - { - var tab = this.tabs[tab_index]; - - if (typeof(tab.description) == 'string') - { - return $('<div />') - .addClass('cbi-tab-descr') - .text(tab.description); - } - - return null; - }, - - renderTabHead: function(sid, index, tab_index) - { - var tab = this.tabs[tab_index]; - var cur = this.active_tab[sid] || 0; - - var tabh = $('<li />') - .append($('<a />') - .attr('id', this.id('nodetab', sid, tab.id)) - .attr('href', '#' + this.id('node', sid, tab.id)) - .attr('data-toggle', 'tab') - .text((tab.caption ? tab.caption.format(tab.id) : tab.id) + ' ') - .append($('<span />') - .addClass('badge')) - .on('shown.bs.tab', { self: this, sid: sid }, this.handleTab)); - - if (cur == tab_index) - tabh.addClass('active'); - - if (!tab.fields.length) - tabh.hide(); - - return tabh; - }, - - renderTabBody: function(sid, index, tab_index) - { - var tab = this.tabs[tab_index]; - var cur = this.active_tab[sid] || 0; - - var tabb = $('<div />') - .addClass('tab-pane') - .attr('id', this.id('node', sid, tab.id)) - .append(this.renderTabDescription(sid, index, tab_index)) - .on('validate', this.handleTabValidate); - - if (cur == tab_index) - tabb.addClass('active'); - - for (var i = 0; i < tab.fields.length; i++) - tabb.append(tab.fields[i].render(sid)); - - return tabb; - }, - - renderPanelHead: function(sid, index, parent_sid) - { - var head = $('<div />') - .addClass('luci2-section-header') - .append(this.renderTeaser(sid, index)) - .append($('<div />') - .addClass('btn-group') - .append(this.renderSort(index)) - .append(this.renderRemove(index))); - - if (this.options.collabsible) - { - head.attr('data-toggle', 'collapse') - .attr('data-parent', this.id('sectiongroup', parent_sid)) - .attr('data-target', '#' + this.id('panel', sid)); - } - - return head; - }, - - renderPanelBody: function(sid, index, parent_sid) - { - var body = $('<div />') - .attr('id', this.id('panel', sid)) - .addClass('luci2-section-panel') - .on('validate', this.handlePanelValidate); - - if (this.options.collabsible || this.ownerMap.options.collabsible) - { - body.addClass('panel-collapse collapse'); - - if (index == this.getPanelIndex(parent_sid)) - body.addClass('in'); - } - - var tab_heads = $('<ul />') - .addClass('nav nav-tabs'); - - var tab_bodies = $('<div />') - .addClass('form-horizontal tab-content') - .append(tab_heads); - - for (var j = 0; j < this.tabs.length; j++) - { - tab_heads.append(this.renderTabHead(sid, index, j)); - tab_bodies.append(this.renderTabBody(sid, index, j)); - } - - body.append(tab_bodies); - - if (this.tabs.length <= 1) - tab_heads.hide(); - - for (var i = 0; i < this.subsections.length; i++) - body.append(this.subsections[i].render(false, sid)); - - return body; - }, - - renderBody: function(condensed, parent_sid) - { - var s = this.getUCISections(parent_sid); - var n = this.getPanelIndex(parent_sid); - - if (n < 0) - this.setPanelIndex(parent_sid, n + s.length); - else if (n >= s.length) - this.setPanelIndex(parent_sid, s.length - 1); - - var body = $('<ul />') - .addClass('luci2-section-group list-group'); - - if (this.options.collabsible) - { - body.attr('id', this.id('sectiongroup', parent_sid)) - .on('show.bs.collapse', { self: this }, this.handlePanelCollapse); - } - - if (s.length == 0) - { - body.append($('<li />') - .addClass('list-group-item text-muted') - .text(this.label('placeholder') || L.tr('There are no entries defined yet.'))) - } - - for (var i = 0; i < s.length; i++) - { - var sid = s[i]['.name']; - var inst = this.instance[sid] = { tabs: [ ] }; - - body.append($('<li />') - .addClass('luci2-section-item list-group-item') - .attr('id', this.id('sectionitem', sid)) - .attr('data-luci2-sid', sid) - .append(this.renderPanelHead(sid, i, parent_sid)) - .append(this.renderPanelBody(sid, i, parent_sid))); - } - - return body; - }, - - render: function(condensed, parent_sid) - { - this.instance = { }; - - var panel = $('<div />') - .addClass('panel panel-default') - .append(this.renderHead(condensed)) - .append(this.renderBody(condensed, parent_sid)); - - if (this.options.addremove) - panel.append($('<div />') - .addClass('panel-footer') - .append(this.renderAdd())); - - return panel; - }, - - finish: function(parent_sid) - { - var s = this.getUCISections(parent_sid); - - for (var i = 0; i < s.length; i++) - { - var sid = s[i]['.name']; - - if (i != this.getPanelIndex(parent_sid)) - $('#' + this.id('teaser', sid)).children('span:last') - .append(this.teaser(sid)); - else - $('#' + this.id('teaser', sid)) - .hide(); - - for (var j = 0; j < this.subsections.length; j++) - this.subsections[j].finish(sid); - } - } - }); - - this.cbi.TableSection = this.cbi.TypedSection.extend({ - renderTableHead: function() - { - var thead = $('<thead />') - .append($('<tr />') - .addClass('cbi-section-table-titles')); - - for (var j = 0; j < this.tabs[0].fields.length; j++) - thead.children().append($('<th />') - .addClass('cbi-section-table-cell') - .css('width', this.tabs[0].fields[j].options.width || '') - .append(this.tabs[0].fields[j].label('caption'))); - - if (this.options.addremove !== false || this.options.sortable) - thead.children().append($('<th />') - .addClass('cbi-section-table-cell') - .text(' ')); - - return thead; - }, - - renderTableRow: function(sid, index) - { - var row = $('<tr />') - .addClass('luci2-section-item') - .attr('id', this.id('sectionitem', sid)) - .attr('data-luci2-sid', sid); - - for (var j = 0; j < this.tabs[0].fields.length; j++) - { - row.append($('<td />') - .css('width', this.tabs[0].fields[j].options.width || '') - .append(this.tabs[0].fields[j].render(sid, true))); - } - - if (this.options.addremove !== false || this.options.sortable) - { - row.append($('<td />') - .css('width', '1%') - .addClass('text-right') - .append($('<div />') - .addClass('btn-group') - .append(this.renderSort(index)) - .append(this.renderRemove(index)))); - } - - return row; - }, - - renderTableBody: function(parent_sid) - { - var s = this.getUCISections(parent_sid); - - var tbody = $('<tbody />'); - - if (s.length == 0) - { - var cols = this.tabs[0].fields.length; - - if (this.options.addremove !== false || this.options.sortable) - cols++; - - tbody.append($('<tr />') - .append($('<td />') - .addClass('text-muted') - .attr('colspan', cols) - .text(this.label('placeholder') || L.tr('There are no entries defined yet.')))); - } - - for (var i = 0; i < s.length; i++) - { - var sid = s[i]['.name']; - var inst = this.instance[sid] = { tabs: [ ] }; - - tbody.append(this.renderTableRow(sid, i)); - } - - return tbody; - }, - - renderBody: function(condensed, parent_sid) - { - return $('<table />') - .addClass('table table-condensed table-hover') - .append(this.renderTableHead()) - .append(this.renderTableBody(parent_sid)); - } - }); - - this.cbi.NamedSection = this.cbi.TypedSection.extend({ - getUCISections: function(cb) - { - var sa = [ ]; - var sl = L.uci.sections(this.ownerMap.uci_package); - - for (var i = 0; i < sl.length; i++) - if (sl[i]['.name'] == this.uci_type) - { - sa.push(sl[i]); - break; - } - - if (typeof(cb) == 'function' && sa.length > 0) - cb.call(this, sa[0]); - - return sa; - } - }); - - this.cbi.SingleSection = this.cbi.NamedSection.extend({ - render: function() - { - this.instance = { }; - this.instance[this.uci_type] = { tabs: [ ] }; - - return $('<div />') - .addClass('luci2-section-item') - .attr('id', this.id('sectionitem', this.uci_type)) - .attr('data-luci2-sid', this.uci_type) - .append(this.renderPanelBody(this.uci_type, 0)); - } - }); - - this.cbi.DummySection = this.cbi.TypedSection.extend({ - getUCISections: function(cb) - { - if (typeof(cb) == 'function') - cb.apply(this, [ { '.name': this.uci_type } ]); - - return [ { '.name': this.uci_type } ]; - } - }); - - this.cbi.Map = this.ui.AbstractWidget.extend({ - init: function(uci_package, options) - { - var self = this; - - this.uci_package = uci_package; - this.sections = [ ]; - this.options = L.defaults(options, { - save: function() { }, - prepare: function() { } - }); - }, - - loadCallback: function() - { - var deferreds = [ L.deferrable(this.options.prepare()) ]; - - for (var i = 0; i < this.sections.length; i++) - { - var rv = this.sections[i].load(); - deferreds.push.apply(deferreds, rv); - } - - return $.when.apply($, deferreds); - }, - - load: function() - { - var self = this; - var packages = [ this.uci_package ]; - - for (var i = 0; i < this.sections.length; i++) - packages.push.apply(packages, this.sections[i].findAdditionalUCIPackages()); - - for (var i = 0; i < packages.length; i++) - if (!L.uci.writable(packages[i])) - { - this.options.readonly = true; - break; - } - - return L.uci.load(packages).then(function() { - return self.loadCallback(); - }); - }, - - handleTab: function(ev) - { - ev.data.self.active_tab = $(ev.target).parent().index(); - }, - - handleApply: function(ev) - { - var self = ev.data.self; - - self.trigger('apply', ev); - }, - - handleSave: function(ev) - { - var self = ev.data.self; - - self.send().then(function() { - self.trigger('save', ev); - }); - }, - - handleReset: function(ev) - { - var self = ev.data.self; - - self.trigger('reset', ev); - self.reset(); - }, - - renderTabHead: function(tab_index) - { - var section = this.sections[tab_index]; - var cur = this.active_tab || 0; - - var tabh = $('<li />') - .append($('<a />') - .attr('id', section.id('sectiontab')) - .attr('href', '#' + section.id('section')) - .attr('data-toggle', 'tab') - .text(section.label('caption') + ' ') - .append($('<span />') - .addClass('badge')) - .on('shown.bs.tab', { self: this }, this.handleTab)); - - if (cur == tab_index) - tabh.addClass('active'); - - return tabh; - }, - - renderTabBody: function(tab_index) - { - var section = this.sections[tab_index]; - var desc = section.label('description'); - var cur = this.active_tab || 0; - - var tabb = $('<div />') - .addClass('tab-pane') - .attr('id', section.id('section')); - - if (cur == tab_index) - tabb.addClass('active'); - - if (desc) - tabb.append($('<p />') - .text(desc)); - - var s = section.render(this.options.tabbed); - - if (this.options.readonly || section.options.readonly) - s.find('input, select, button, img.cbi-button').attr('disabled', true); - - tabb.append(s); - - return tabb; - }, - - renderBody: function() - { - var tabs = $('<ul />') - .addClass('nav nav-tabs'); - - var body = $('<div />') - .append(tabs); - - for (var i = 0; i < this.sections.length; i++) - { - tabs.append(this.renderTabHead(i)); - body.append(this.renderTabBody(i)); - } - - if (this.options.tabbed) - body.addClass('tab-content'); - else - tabs.hide(); - - return body; - }, - - renderFooter: function() - { - var evdata = { - self: this - }; - - return $('<div />') - .addClass('panel panel-default panel-body text-right') - .append($('<div />') - .addClass('btn-group') - .append(L.ui.button(L.tr('Save & Apply'), 'primary') - .click(evdata, this.handleApply)) - .append(L.ui.button(L.tr('Save'), 'default') - .click(evdata, this.handleSave)) - .append(L.ui.button(L.tr('Reset'), 'default') - .click(evdata, this.handleReset))); - }, - - render: function() - { - var map = $('<form />'); - - if (typeof(this.options.caption) == 'string') - map.append($('<h2 />') - .text(this.options.caption)); - - if (typeof(this.options.description) == 'string') - map.append($('<p />') - .text(this.options.description)); - - map.append(this.renderBody()); - - if (this.options.pageaction !== false) - map.append(this.renderFooter()); - - return map; - }, - - finish: function() - { - for (var i = 0; i < this.sections.length; i++) - this.sections[i].finish(); - - this.validate(); - }, - - redraw: function() - { - this.target.hide().empty().append(this.render()); - this.finish(); - this.target.show(); - }, - - section: function(widget, uci_type, options) - { - var w = widget ? new widget(uci_type, options) : null; - - if (!(w instanceof L.cbi.AbstractSection)) - throw 'Widget must be an instance of AbstractSection'; - - w.ownerMap = this; - w.index = this.sections.length; - - this.sections.push(w); - return w; - }, - - add: function(conf, type, name) - { - return L.uci.add(conf, type, name); - }, - - remove: function(conf, sid) - { - return L.uci.remove(conf, sid); - }, - - get: function(conf, sid, opt) - { - return L.uci.get(conf, sid, opt); - }, - - set: function(conf, sid, opt, val) - { - return L.uci.set(conf, sid, opt, val); - }, - - validate: function() - { - var rv = true; - - for (var i = 0; i < this.sections.length; i++) - { - if (!this.sections[i].validate()) - rv = false; - } - - return rv; - }, - - save: function() - { - var self = this; - - if (self.options.readonly) - return L.deferrable(); - - var deferreds = [ ]; - - for (var i = 0; i < self.sections.length; i++) - { - var rv = self.sections[i].save(); - deferreds.push.apply(deferreds, rv); - } - - return $.when.apply($, deferreds).then(function() { - return L.deferrable(self.options.save()); - }); - }, - - send: function() - { - if (!this.validate()) - return L.deferrable(); - - var self = this; - - L.ui.saveScrollTop(); - L.ui.loading(true); - - return this.save().then(function() { - return L.uci.save(); - }).then(function() { - return L.ui.updateChanges(); - }).then(function() { - return self.load(); - }).then(function() { - self.redraw(); - self = null; - - L.ui.loading(false); - L.ui.restoreScrollTop(); - }); - }, - - revert: function() - { - var packages = [ this.uci_package ]; - - for (var i = 0; i < this.sections.length; i++) - packages.push.apply(packages, this.sections[i].findAdditionalUCIPackages()); - - L.uci.unload(packages); - }, - - reset: function() - { - var self = this; - - self.revert(); - - return self.insertInto(self.target); - }, - - insertInto: function(id) - { - var self = this; - self.target = $(id); - - L.ui.loading(true); - self.target.hide(); - - return self.load().then(function() { - self.target.empty().append(self.render()); - self.finish(); - self.target.show(); - self = null; - L.ui.loading(false); - }); - } - }); - - this.cbi.Modal = this.cbi.Map.extend({ - handleApply: function(ev) - { - var self = ev.data.self; - - self.trigger('apply', ev); - }, - - handleSave: function(ev) - { - var self = ev.data.self; - - self.send().then(function() { - self.trigger('save', ev); - self.close(); - }); - }, - - handleReset: function(ev) - { - var self = ev.data.self; - - self.trigger('close', ev); - self.revert(); - self.close(); - }, - - renderFooter: function() - { - var evdata = { - self: this - }; - - return $('<div />') - .addClass('btn-group') - .append(L.ui.button(L.tr('Save & Apply'), 'primary') - .click(evdata, this.handleApply)) - .append(L.ui.button(L.tr('Save'), 'default') - .click(evdata, this.handleSave)) - .append(L.ui.button(L.tr('Cancel'), 'default') - .click(evdata, this.handleReset)); - }, - - render: function() - { - var modal = L.ui.dialog(this.label('caption'), null, { wide: true }); - var map = $('<form />'); - - var desc = this.label('description'); - if (desc) - map.append($('<p />').text(desc)); - - map.append(this.renderBody()); - - modal.find('.modal-body').append(map); - modal.find('.modal-footer').append(this.renderFooter()); - - return modal; - }, - - redraw: function() - { - this.render(); - this.finish(); - }, - - show: function() - { - var self = this; - - L.ui.loading(true); - - return self.load().then(function() { - self.render(); - self.finish(); - - L.ui.loading(false); - }); - }, - - close: function() - { - L.ui.dialog(false); - } - }); + Class.instantiate('luci2.rpc'); + Class.instantiate('luci2.uci'); + Class.instantiate('luci2.network'); + Class.instantiate('luci2.wireless'); + Class.instantiate('luci2.firewall'); + Class.instantiate('luci2.system'); + Class.instantiate('luci2.session'); + Class.instantiate('luci2.ui'); + Class.instantiate('luci2.cbi'); }; diff --git a/luci2/htdocs/luci2/network.js b/luci2/htdocs/luci2/network.js new file mode 100644 index 0000000..64e32e5 --- /dev/null +++ b/luci2/htdocs/luci2/network.js @@ -0,0 +1,1447 @@ +(function() { + var network_class = { + deviceBlacklist: [ + /^gre[0-9]+$/, + /^gretap[0-9]+$/, + /^ifb[0-9]+$/, + /^ip6tnl[0-9]+$/, + /^sit[0-9]+$/, + /^wlan[0-9]+\.sta[0-9]+$/, + /^tunl[0-9]+$/, + /^ip6gre[0-9]+$/ + ], + + rpcCacheFunctions: [ + 'protolist', 0, L.rpc.declare({ + object: 'network', + method: 'get_proto_handlers', + expect: { '': { } } + }), + 'ifstate', 1, L.rpc.declare({ + object: 'network.interface', + method: 'dump', + expect: { 'interface': [ ] } + }), + 'devstate', 2, L.rpc.declare({ + object: 'network.device', + method: 'status', + expect: { '': { } } + }), + 'wifistate', 0, L.rpc.declare({ + object: 'network.wireless', + method: 'status', + expect: { '': { } } + }), + 'bwstate', 2, L.rpc.declare({ + object: 'luci2.network.bwmon', + method: 'statistics', + expect: { 'statistics': { } } + }), + 'devlist', 2, L.rpc.declare({ + object: 'luci2.network', + method: 'device_list', + expect: { 'devices': [ ] } + }), + 'swlist', 0, L.rpc.declare({ + object: 'luci2.network', + method: 'switch_list', + expect: { 'switches': [ ] } + }) + ], + + loadProtocolHandler: function(proto) + { + var url = L.globals.resource + '/proto/' + proto + '.js'; + var self = L.network; + + var def = $.Deferred(); + + $.ajax(url, { + method: 'GET', + cache: true, + dataType: 'text' + }).then(function(data) { + try { + var protoConstructorSource = ( + '(function(L, $) { ' + + 'return %s' + + '})(L, $);\n\n' + + '//@ sourceURL=%s/%s' + ).format(data, window.location.origin, url); + + var protoClass = eval(protoConstructorSource); + + self.protocolHandlers[proto] = new protoClass(); + } + catch(e) { + alert('Unable to instantiate proto "%s": %s'.format(url, e)); + }; + + def.resolve(); + }).fail(function() { + def.resolve(); + }); + + return def; + }, + + loadProtocolHandlers: function() + { + var self = L.network; + var deferreds = [ + self.loadProtocolHandler('none') + ]; + + for (var proto in self.rpcCache.protolist) + deferreds.push(self.loadProtocolHandler(proto)); + + return $.when.apply($, deferreds); + }, + + callSwitchInfo: L.rpc.declare({ + object: 'luci2.network', + method: 'switch_info', + params: [ 'switch' ], + expect: { 'info': { } } + }), + + callSwitchInfoCallback: function(responses) { + var self = L.network; + var swlist = self.rpcCache.swlist; + var swstate = self.rpcCache.swstate = { }; + + for (var i = 0; i < responses.length; i++) + swstate[swlist[i]] = responses[i]; + }, + + loadCacheCallback: function(level) + { + var self = L.network; + var name = '_fetch_cache_cb_' + level; + + return self[name] || ( + self[name] = function(responses) + { + for (var i = 0; i < self.rpcCacheFunctions.length; i += 3) + if (!level || self.rpcCacheFunctions[i + 1] == level) + self.rpcCache[self.rpcCacheFunctions[i]] = responses.shift(); + + if (!level) + { + L.rpc.batch(); + + for (var i = 0; i < self.rpcCache.swlist.length; i++) + self.callSwitchInfo(self.rpcCache.swlist[i]); + + return L.rpc.flush().then(self.callSwitchInfoCallback); + } + + return L.deferrable(); + } + ); + }, + + loadCache: function(level) + { + var self = L.network; + + return L.uci.load(['network', 'wireless']).then(function() { + L.rpc.batch(); + + for (var i = 0; i < self.rpcCacheFunctions.length; i += 3) + if (!level || self.rpcCacheFunctions[i + 1] == level) + self.rpcCacheFunctions[i + 2](); + + return L.rpc.flush().then(self.loadCacheCallback(level || 0)); + }); + }, + + isBlacklistedDevice: function(dev) + { + for (var i = 0; i < this.deviceBlacklist.length; i++) + if (dev.match(this.deviceBlacklist[i])) + return true; + + return false; + }, + + sortDevicesCallback: function(a, b) + { + if (a.options.kind < b.options.kind) + return -1; + else if (a.options.kind > b.options.kind) + return 1; + + if (a.options.name < b.options.name) + return -1; + else if (a.options.name > b.options.name) + return 1; + + return 0; + }, + + getDeviceObject: function(ifname) + { + var alias = (ifname.charAt(0) == '@'); + return this.deviceObjects[ifname] || ( + this.deviceObjects[ifname] = { + ifname: ifname, + kind: alias ? 'alias' : 'ethernet', + type: alias ? 0 : 1, + up: false, + changed: { } + } + ); + }, + + getInterfaceObject: function(name) + { + return this.interfaceObjects[name] || ( + this.interfaceObjects[name] = { + name: name, + proto: this.protocolHandlers.none, + changed: { } + } + ); + }, + + loadDevicesCallback: function() + { + var self = L.network; + var wificount = { }; + + for (var ifname in self.rpcCache.devstate) + { + if (self.isBlacklistedDevice(ifname)) + continue; + + var dev = self.rpcCache.devstate[ifname]; + var entry = self.getDeviceObject(ifname); + + entry.up = dev.up; + + switch (dev.type) + { + case 'IP tunnel': + entry.kind = 'tunnel'; + break; + + case 'Bridge': + entry.kind = 'bridge'; + //entry.ports = dev['bridge-members'].sort(); + break; + } + } + + for (var i = 0; i < self.rpcCache.devlist.length; i++) + { + var dev = self.rpcCache.devlist[i]; + + if (self.isBlacklistedDevice(dev.device)) + continue; + + var entry = self.getDeviceObject(dev.device); + + entry.up = dev.is_up; + entry.type = dev.type; + + switch (dev.type) + { + case 1: /* Ethernet */ + if (dev.is_bridge) + entry.kind = 'bridge'; + else if (dev.is_tuntap) + entry.kind = 'tunnel'; + else if (dev.is_wireless) + entry.kind = 'wifi'; + break; + + case 512: /* PPP */ + case 768: /* IP-IP Tunnel */ + case 769: /* IP6-IP6 Tunnel */ + case 776: /* IPv6-in-IPv4 */ + case 778: /* GRE over IP */ + entry.kind = 'tunnel'; + break; + } + } + + var net = L.uci.sections('network'); + for (var i = 0; i < net.length; i++) + { + var s = net[i]; + var sid = s['.name']; + + if (s['.type'] == 'device' && s.name) + { + var entry = self.getDeviceObject(s.name); + + switch (s.type) + { + case 'macvlan': + case 'tunnel': + entry.kind = 'tunnel'; + break; + } + + entry.sid = sid; + } + else if (s['.type'] == 'interface' && !s['.anonymous'] && s.ifname) + { + var ifnames = L.toArray(s.ifname); + + for (var j = 0; j < ifnames.length; j++) + self.getDeviceObject(ifnames[j]); + + if (s['.name'] != 'loopback') + { + var entry = self.getDeviceObject('@%s'.format(s['.name'])); + + entry.type = 0; + entry.kind = 'alias'; + entry.sid = sid; + } + } + else if (s['.type'] == 'switch_vlan' && s.device) + { + var sw = self.rpcCache.swstate[s.device]; + var vid = parseInt(s.vid || s.vlan); + var ports = L.toArray(s.ports); + + if (!sw || !ports.length || isNaN(vid)) + continue; + + var ifname = undefined; + + for (var j = 0; j < ports.length; j++) + { + var port = parseInt(ports[j]); + var tag = (ports[j].replace(/[^tu]/g, '') == 't'); + + if (port == sw.cpu_port) + { + // XXX: need a way to map switch to netdev + if (tag) + ifname = 'eth0.%d'.format(vid); + else + ifname = 'eth0'; + + break; + } + } + + if (!ifname) + continue; + + var entry = self.getDeviceObject(ifname); + + entry.kind = 'vlan'; + entry.sid = sid; + entry.vsw = sw; + entry.vid = vid; + } + } + + var wifi = L.uci.sections('wireless'); + for (var i = 0, c = 0; i < wifi.length; i++) + { + var s = wifi[i]; + + if (s['.type'] == 'wifi-iface') + { + var sid = '@wifi-iface[%d]'.format(c++); + + if (!s.device) + continue; + + var r = parseInt(s.device.replace(/^[^0-9]+/, '')); + var n = wificount[s.device] = (wificount[s.device] || 0) + 1; + var id = 'radio%d.network%d'.format(r, n); + var ifname = id; + + if (self.rpcCache.wifistate[s.device]) + { + var ifcs = self.rpcCache.wifistate[s.device].interfaces; + for (var ifc in ifcs) + { + if (ifcs[ifc].section == sid && ifcs[ifc].ifname) + { + ifname = ifcs[ifc].ifname; + break; + } + } + } + + var entry = self.getDeviceObject(ifname); + + entry.kind = 'wifi'; + entry.sid = s['.name']; + entry.wid = id; + entry.wdev = s.device; + entry.wmode = s.mode; + entry.wssid = s.ssid; + entry.wbssid = s.bssid; + } + } + + for (var i = 0; i < net.length; i++) + { + var s = net[i]; + var sid = s['.name']; + + if (s['.type'] == 'interface' && !s['.anonymous'] && s.type == 'bridge') + { + var ifnames = L.toArray(s.ifname); + + for (var ifname in self.deviceObjects) + { + var dev = self.deviceObjects[ifname]; + + if (dev.kind != 'wifi') + continue; + + var wnets = L.toArray(L.uci.get('wireless', dev.sid, 'network')); + if ($.inArray(sid, wnets) > -1) + ifnames.push(ifname); + } + + entry = self.getDeviceObject('br-%s'.format(s['.name'])); + entry.type = 1; + entry.kind = 'bridge'; + entry.sid = sid; + entry.ports = ifnames.sort(); + } + } + }, + + loadInterfacesCallback: function() + { + var self = L.network; + var net = L.uci.sections('network'); + + for (var i = 0; i < net.length; i++) + { + var s = net[i]; + var sid = s['.name']; + + if (s['.type'] == 'interface' && !s['.anonymous'] && s.proto) + { + var entry = self.getInterfaceObject(s['.name']); + var proto = self.protocolHandlers[s.proto] || self.protocolHandlers.none; + + var l3dev = undefined; + var l2dev = undefined; + + var ifnames = L.toArray(s.ifname); + + for (var ifname in self.deviceObjects) + { + var dev = self.deviceObjects[ifname]; + + if (dev.kind != 'wifi') + continue; + + var wnets = L.toArray(L.uci.get('wireless', dev.sid, 'network')); + if ($.inArray(entry.name, wnets) > -1) + ifnames.push(ifname); + } + + if (proto.virtual) + l3dev = '%s-%s'.format(s.proto, entry.name); + else if (s.type == 'bridge') + l3dev = 'br-%s'.format(entry.name); + else + l3dev = ifnames[0]; + + if (!proto.virtual && s.type == 'bridge') + l2dev = 'br-%s'.format(entry.name); + else if (!proto.virtual) + l2dev = ifnames[0]; + + entry.proto = proto; + entry.sid = sid; + entry.l3dev = l3dev; + entry.l2dev = l2dev; + } + } + + for (var i = 0; i < self.rpcCache.ifstate.length; i++) + { + var iface = self.rpcCache.ifstate[i]; + var entry = self.getInterfaceObject(iface['interface']); + var proto = self.protocolHandlers[iface.proto] || self.protocolHandlers.none; + + /* this is a virtual interface, either deleted from config but + not applied yet or set up from external tools (6rd) */ + if (!entry.sid) + { + entry.proto = proto; + entry.l2dev = iface.device; + entry.l3dev = iface.l3_device; + } + } + }, + + load: function() + { + var self = this; + + if (self.rpcCache) + return L.deferrable(); + + self.rpcCache = { }; + self.deviceObjects = { }; + self.interfaceObjects = { }; + self.protocolHandlers = { }; + + return self.loadCache() + .then(self.loadProtocolHandlers) + .then(self.loadDevicesCallback) + .then(self.loadInterfacesCallback); + }, + + update: function() + { + delete this.rpcCache; + return this.load(); + }, + + refreshInterfaceStatus: function() + { + return this.loadCache(1).then(this.loadInterfacesCallback); + }, + + refreshDeviceStatus: function() + { + return this.loadCache(2).then(this.loadDevicesCallback); + }, + + refreshStatus: function() + { + return this.loadCache(1) + .then(this.loadCache(2)) + .then(this.loadDevicesCallback) + .then(this.loadInterfacesCallback); + }, + + getDevices: function() + { + var devs = [ ]; + + for (var ifname in this.deviceObjects) + if (ifname != 'lo') + devs.push(new L.network.Device(this.deviceObjects[ifname])); + + return devs.sort(this.sortDevicesCallback); + }, + + getDeviceByInterface: function(iface) + { + if (iface instanceof L.network.Interface) + iface = iface.name(); + + if (this.interfaceObjects[iface]) + return this.getDevice(this.interfaceObjects[iface].l3dev) || + this.getDevice(this.interfaceObjects[iface].l2dev); + + return undefined; + }, + + getDevice: function(ifname) + { + if (this.deviceObjects[ifname]) + return new L.network.Device(this.deviceObjects[ifname]); + + return undefined; + }, + + createDevice: function(name) + { + return new L.network.Device(this.getDeviceObject(name)); + }, + + getInterfaces: function() + { + var ifaces = [ ]; + + for (var name in this.interfaceObjects) + if (name != 'loopback') + ifaces.push(this.getInterface(name)); + + ifaces.sort(function(a, b) { + if (a.name() < b.name()) + return -1; + else if (a.name() > b.name()) + return 1; + else + return 0; + }); + + return ifaces; + }, + + getInterfacesByDevice: function(dev) + { + var ifaces = [ ]; + + if (dev instanceof L.network.Device) + dev = dev.name(); + + for (var name in this.interfaceObjects) + { + var iface = this.interfaceObjects[name]; + if (iface.l2dev == dev || iface.l3dev == dev) + ifaces.push(this.getInterface(name)); + } + + ifaces.sort(function(a, b) { + if (a.name() < b.name()) + return -1; + else if (a.name() > b.name()) + return 1; + else + return 0; + }); + + return ifaces; + }, + + getInterface: function(iface) + { + if (this.interfaceObjects[iface]) + return new L.network.Interface(this.interfaceObjects[iface]); + + return undefined; + }, + + getProtocols: function() + { + var rv = [ ]; + + for (var proto in this.protocolHandlers) + { + var pr = this.protocolHandlers[proto]; + + rv.push({ + name: proto, + description: pr.description, + virtual: pr.virtual, + tunnel: pr.tunnel + }); + } + + return rv.sort(function(a, b) { + if (a.name < b.name) + return -1; + else if (a.name > b.name) + return 1; + else + return 0; + }); + }, + + findWANByAddr: function(ipaddr) + { + for (var i = 0; i < this.rpcCache.ifstate.length; i++) + { + var ifstate = this.rpcCache.ifstate[i]; + + if (!ifstate.route) + continue; + + for (var j = 0; j < ifstate.route.length; j++) + if (ifstate.route[j].mask == 0 && + ifstate.route[j].target == ipaddr && + typeof(ifstate.route[j].table) == 'undefined') + { + return this.getInterface(ifstate['interface']); + } + } + + return undefined; + }, + + findWAN: function() + { + return this.findWANByAddr('0.0.0.0'); + }, + + findWAN6: function() + { + return this.findWANByAddr('::'); + }, + + resolveAlias: function(ifname) + { + if (ifname instanceof L.network.Device) + ifname = ifname.name(); + + var dev = this.deviceObjects[ifname]; + var seen = { }; + + while (dev && dev.kind == 'alias') + { + // loop + if (seen[dev.ifname]) + return undefined; + + var ifc = this.interfaceObjects[dev.sid]; + + seen[dev.ifname] = true; + dev = ifc ? this.deviceObjects[ifc.l3dev] : undefined; + } + + return dev ? this.getDevice(dev.ifname) : undefined; + } + }; + + network_class.Interface = Class.extend({ + getStatus: function(key) + { + var s = L.network.rpcCache.ifstate; + + for (var i = 0; i < s.length; i++) + if (s[i]['interface'] == this.options.name) + return key ? s[i][key] : s[i]; + + return undefined; + }, + + get: function(key) + { + return L.uci.get('network', this.options.name, key); + }, + + set: function(key, val) + { + return L.uci.set('network', this.options.name, key, val); + }, + + name: function() + { + return this.options.name; + }, + + protocol: function() + { + return (this.get('proto') || 'none'); + }, + + isUp: function() + { + return (this.getStatus('up') === true); + }, + + isVirtual: function() + { + return (typeof(this.options.sid) != 'string'); + }, + + getProtocol: function() + { + var prname = this.get('proto') || 'none'; + return L.network.protocolHandlers[prname] || L.network.protocolHandlers.none; + }, + + getUptime: function() + { + var uptime = this.getStatus('uptime'); + return isNaN(uptime) ? 0 : uptime; + }, + + getDevice: function(resolveAlias) + { + if (this.options.l3dev) + return L.network.getDevice(this.options.l3dev); + + return undefined; + }, + + getPhysdev: function() + { + if (this.options.l2dev) + return L.network.getDevice(this.options.l2dev); + + return undefined; + }, + + getSubdevices: function() + { + var rv = [ ]; + var dev = this.options.l2dev ? + L.network.deviceObjects[this.options.l2dev] : undefined; + + if (dev && dev.kind == 'bridge' && dev.ports && dev.ports.length) + for (var i = 0; i < dev.ports.length; i++) + rv.push(L.network.getDevice(dev.ports[i])); + + return rv; + }, + + getIPv4Addrs: function(mask) + { + var rv = [ ]; + var addrs = this.getStatus('ipv4-address'); + + if (addrs) + for (var i = 0; i < addrs.length; i++) + if (!mask) + rv.push(addrs[i].address); + else + rv.push('%s/%d'.format(addrs[i].address, addrs[i].mask)); + + return rv; + }, + + getIPv6Addrs: function(mask) + { + var rv = [ ]; + var addrs; + + addrs = this.getStatus('ipv6-address'); + + if (addrs) + for (var i = 0; i < addrs.length; i++) + if (!mask) + rv.push(addrs[i].address); + else + rv.push('%s/%d'.format(addrs[i].address, addrs[i].mask)); + + addrs = this.getStatus('ipv6-prefix-assignment'); + + if (addrs) + for (var i = 0; i < addrs.length; i++) + if (!mask) + rv.push('%s1'.format(addrs[i].address)); + else + rv.push('%s1/%d'.format(addrs[i].address, addrs[i].mask)); + + return rv; + }, + + getDNSAddrs: function() + { + var rv = [ ]; + var addrs = this.getStatus('dns-server'); + + if (addrs) + for (var i = 0; i < addrs.length; i++) + rv.push(addrs[i]); + + return rv; + }, + + getIPv4DNS: function() + { + var rv = [ ]; + var dns = this.getStatus('dns-server'); + + if (dns) + for (var i = 0; i < dns.length; i++) + if (dns[i].indexOf(':') == -1) + rv.push(dns[i]); + + return rv; + }, + + getIPv6DNS: function() + { + var rv = [ ]; + var dns = this.getStatus('dns-server'); + + if (dns) + for (var i = 0; i < dns.length; i++) + if (dns[i].indexOf(':') > -1) + rv.push(dns[i]); + + return rv; + }, + + getIPv4Gateway: function() + { + var rt = this.getStatus('route'); + + if (rt) + for (var i = 0; i < rt.length; i++) + if (rt[i].target == '0.0.0.0' && rt[i].mask == 0) + return rt[i].nexthop; + + return undefined; + }, + + getIPv6Gateway: function() + { + var rt = this.getStatus('route'); + + if (rt) + for (var i = 0; i < rt.length; i++) + if (rt[i].target == '::' && rt[i].mask == 0) + return rt[i].nexthop; + + return undefined; + }, + + getStatistics: function() + { + var dev = this.getDevice() || new L.network.Device({}); + return dev.getStatistics(); + }, + + getTrafficHistory: function() + { + var dev = this.getDevice() || new L.network.Device({}); + return dev.getTrafficHistory(); + }, + + renderBadge: function() + { + var badge = $('<span />') + .addClass('badge') + .text('%s: '.format(this.name())); + + var dev = this.getDevice(); + var subdevs = this.getSubdevices(); + + if (subdevs.length) + for (var j = 0; j < subdevs.length; j++) + badge.append($('<img />') + .attr('src', subdevs[j].icon()) + .attr('title', '%s (%s)'.format(subdevs[j].description(), subdevs[j].name() || '?'))); + else if (dev) + badge.append($('<img />') + .attr('src', dev.icon()) + .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?'))); + else + badge.append($('<em />').text(L.tr('(No devices attached)'))); + + return badge; + }, + + setDevices: function(devs) + { + var dev = this.getPhysdev(); + var old_devs = [ ]; + var changed = false; + + if (dev && dev.isBridge()) + old_devs = this.getSubdevices(); + else if (dev) + old_devs = [ dev ]; + + if (old_devs.length != devs.length) + changed = true; + else + for (var i = 0; i < old_devs.length; i++) + { + var dev = devs[i]; + + if (dev instanceof L.network.Device) + dev = dev.name(); + + if (!dev || old_devs[i].name() != dev) + { + changed = true; + break; + } + } + + if (changed) + { + for (var i = 0; i < old_devs.length; i++) + old_devs[i].removeFromInterface(this); + + for (var i = 0; i < devs.length; i++) + { + var dev = devs[i]; + + if (!(dev instanceof L.network.Device)) + dev = L.network.getDevice(dev); + + if (dev) + dev.attachToInterface(this); + } + } + }, + + changeProtocol: function(proto) + { + var pr = L.network.protocolHandlers[proto]; + + if (!pr) + return; + + for (var opt in (this.get() || { })) + { + switch (opt) + { + case 'type': + case 'ifname': + case 'macaddr': + if (pr.virtual) + this.set(opt, undefined); + break; + + case 'auto': + case 'mtu': + break; + + case 'proto': + this.set(opt, pr.protocol); + break; + + default: + this.set(opt, undefined); + break; + } + } + }, + + createFormPrepareCallback: function() + { + var map = this; + var iface = map.options.netIface; + var proto = iface.getProtocol(); + var device = iface.getDevice(); + + map.options.caption = L.tr('Configure "%s"').format(iface.name()); + + var section = map.section(L.cbi.SingleSection, iface.name(), { + anonymous: true + }); + + section.tab({ + id: 'general', + caption: L.tr('General Settings') + }); + + section.tab({ + id: 'advanced', + caption: L.tr('Advanced Settings') + }); + + section.tab({ + id: 'ipv6', + caption: L.tr('IPv6') + }); + + section.tab({ + id: 'physical', + caption: L.tr('Physical Settings') + }); + + + section.taboption('general', L.cbi.CheckboxValue, 'auto', { + caption: L.tr('Start on boot'), + optional: true, + initial: true + }); + + var pr = section.taboption('general', L.cbi.ListValue, 'proto', { + caption: L.tr('Protocol') + }); + + pr.ucivalue = function(sid) { + return iface.get('proto') || 'none'; + }; + + var ok = section.taboption('general', L.cbi.ButtonValue, '_confirm', { + caption: L.tr('Really switch?'), + description: L.tr('Changing the protocol will clear all configuration for this interface!'), + text: L.tr('Change protocol') + }); + + ok.on('click', function(ev) { + iface.changeProtocol(pr.formvalue(ev.data.sid)); + iface.createForm(mapwidget).show(); + }); + + var protos = L.network.getProtocols(); + + for (var i = 0; i < protos.length; i++) + pr.value(protos[i].name, protos[i].description); + + proto.populateForm(section, iface); + + if (!proto.virtual) + { + var br = section.taboption('physical', L.cbi.CheckboxValue, 'type', { + caption: L.tr('Network bridge'), + description: L.tr('Merges multiple devices into one logical bridge'), + optional: true, + enabled: 'bridge', + disabled: '', + initial: '' + }); + + section.taboption('physical', L.cbi.DeviceList, '__iface_multi', { + caption: L.tr('Devices'), + multiple: true, + bridges: false + }).depends('type', true); + + section.taboption('physical', L.cbi.DeviceList, '__iface_single', { + caption: L.tr('Device'), + multiple: false, + bridges: true + }).depends('type', false); + + var mac = section.taboption('physical', L.cbi.InputValue, 'macaddr', { + caption: L.tr('Override MAC'), + optional: true, + placeholder: device ? device.getMACAddress() : undefined, + datatype: 'macaddr' + }) + + mac.ucivalue = function(sid) + { + if (device) + return device.get('macaddr'); + + return this.callSuper('ucivalue', sid); + }; + + mac.save = function(sid) + { + if (!this.changed(sid)) + return false; + + if (device) + device.set('macaddr', this.formvalue(sid)); + else + this.callSuper('set', sid); + + return true; + }; + } + + section.taboption('physical', L.cbi.InputValue, 'mtu', { + caption: L.tr('Override MTU'), + optional: true, + placeholder: device ? device.getMTU() : undefined, + datatype: 'range(1, 9000)' + }); + + section.taboption('physical', L.cbi.InputValue, 'metric', { + caption: L.tr('Override Metric'), + optional: true, + placeholder: 0, + datatype: 'uinteger' + }); + + for (var field in section.fields) + { + switch (field) + { + case 'proto': + break; + + case '_confirm': + for (var i = 0; i < protos.length; i++) + if (protos[i].name != proto.protocol) + section.fields[field].depends('proto', protos[i].name); + break; + + default: + section.fields[field].depends('proto', proto.protocol, true); + break; + } + } + }, + + createForm: function(mapwidget) + { + var self = this; + + if (!mapwidget) + mapwidget = L.cbi.Map; + + var map = new mapwidget('network', { + prepare: self.createFormPrepareCallback, + netIface: self + }); + + return map; + } + }); + + network_class.Device = Class.extend({ + wifiModeStrings: { + ap: L.tr('Master'), + sta: L.tr('Client'), + adhoc: L.tr('Ad-Hoc'), + monitor: L.tr('Monitor'), + wds: L.tr('Static WDS') + }, + + getStatus: function(key) + { + var s = L.network.rpcCache.devstate[this.options.ifname]; + + if (s) + return key ? s[key] : s; + + return undefined; + }, + + get: function(key) + { + var sid = this.options.sid; + var pkg = (this.options.kind == 'wifi') ? 'wireless' : 'network'; + return L.uci.get(pkg, sid, key); + }, + + set: function(key, val) + { + var sid = this.options.sid; + var pkg = (this.options.kind == 'wifi') ? 'wireless' : 'network'; + return L.uci.set(pkg, sid, key, val); + }, + + init: function() + { + if (typeof(this.options.type) == 'undefined') + this.options.type = 1; + + if (typeof(this.options.kind) == 'undefined') + this.options.kind = 'ethernet'; + + if (typeof(this.options.networks) == 'undefined') + this.options.networks = [ ]; + }, + + name: function() + { + return this.options.ifname; + }, + + description: function() + { + switch (this.options.kind) + { + case 'alias': + return L.tr('Alias for network "%s"').format(this.options.ifname.substring(1)); + + case 'bridge': + return L.tr('Network bridge'); + + case 'ethernet': + return L.tr('Network device'); + + case 'tunnel': + switch (this.options.type) + { + case 1: /* tuntap */ + return L.tr('TAP device'); + + case 512: /* PPP */ + return L.tr('PPP tunnel'); + + case 768: /* IP-IP Tunnel */ + return L.tr('IP-in-IP tunnel'); + + case 769: /* IP6-IP6 Tunnel */ + return L.tr('IPv6-in-IPv6 tunnel'); + + case 776: /* IPv6-in-IPv4 */ + return L.tr('IPv6-over-IPv4 tunnel'); + break; + + case 778: /* GRE over IP */ + return L.tr('GRE-over-IP tunnel'); + + default: + return L.tr('Tunnel device'); + } + + case 'vlan': + return L.tr('VLAN %d on %s').format(this.options.vid, this.options.vsw.model); + + case 'wifi': + var o = this.options; + return L.trc('(Wifi-Mode) "(SSID)" on (radioX)', '%s "%h" on %s').format( + o.wmode ? this.wifiModeStrings[o.wmode] : L.tr('Unknown mode'), + o.wssid || '?', o.wdev + ); + } + + return L.tr('Unknown device'); + }, + + icon: function(up) + { + var kind = this.options.kind; + + if (kind == 'alias') + kind = 'ethernet'; + + if (typeof(up) == 'undefined') + up = this.isUp(); + + return L.globals.resource + '/icons/%s%s.png'.format(kind, up ? '' : '_disabled'); + }, + + isUp: function() + { + var l = L.network.rpcCache.devlist; + + for (var i = 0; i < l.length; i++) + if (l[i].device == this.options.ifname) + return (l[i].is_up === true); + + return false; + }, + + isAlias: function() + { + return (this.options.kind == 'alias'); + }, + + isBridge: function() + { + return (this.options.kind == 'bridge'); + }, + + isBridgeable: function() + { + return (this.options.type == 1 && this.options.kind != 'bridge'); + }, + + isWireless: function() + { + return (this.options.kind == 'wifi'); + }, + + isInNetwork: function(net) + { + if (!(net instanceof L.network.Interface)) + net = L.network.getInterface(net); + + if (net) + { + if (net.options.l3dev == this.options.ifname || + net.options.l2dev == this.options.ifname) + return true; + + var dev = L.network.deviceObjects[net.options.l2dev]; + if (dev && dev.kind == 'bridge' && dev.ports) + return ($.inArray(this.options.ifname, dev.ports) > -1); + } + + return false; + }, + + getMTU: function() + { + var dev = L.network.rpcCache.devstate[this.options.ifname]; + if (dev && !isNaN(dev.mtu)) + return dev.mtu; + + return undefined; + }, + + getMACAddress: function() + { + if (this.options.type != 1) + return undefined; + + var dev = L.network.rpcCache.devstate[this.options.ifname]; + if (dev && dev.macaddr) + return dev.macaddr.toUpperCase(); + + return undefined; + }, + + getInterfaces: function() + { + return L.network.getInterfacesByDevice(this.options.name); + }, + + getStatistics: function() + { + var s = this.getStatus('statistics') || { }; + return { + rx_bytes: (s.rx_bytes || 0), + tx_bytes: (s.tx_bytes || 0), + rx_packets: (s.rx_packets || 0), + tx_packets: (s.tx_packets || 0) + }; + }, + + getTrafficHistory: function() + { + var def = new Array(120); + + for (var i = 0; i < 120; i++) + def[i] = 0; + + var h = L.network.rpcCache.bwstate[this.options.ifname] || { }; + return { + rx_bytes: (h.rx_bytes || def), + tx_bytes: (h.tx_bytes || def), + rx_packets: (h.rx_packets || def), + tx_packets: (h.tx_packets || def) + }; + }, + + removeFromInterface: function(iface) + { + if (!(iface instanceof L.network.Interface)) + iface = L.network.getInterface(iface); + + if (!iface) + return; + + var ifnames = L.toArray(iface.get('ifname')); + if ($.inArray(this.options.ifname, ifnames) > -1) + iface.set('ifname', L.filterArray(ifnames, this.options.ifname)); + + if (this.options.kind != 'wifi') + return; + + var networks = L.toArray(this.get('network')); + if ($.inArray(iface.name(), networks) > -1) + this.set('network', L.filterArray(networks, iface.name())); + }, + + attachToInterface: function(iface) + { + if (!(iface instanceof L.network.Interface)) + iface = L.network.getInterface(iface); + + if (!iface) + return; + + if (this.options.kind != 'wifi') + { + var ifnames = L.toArray(iface.get('ifname')); + if ($.inArray(this.options.ifname, ifnames) < 0) + { + ifnames.push(this.options.ifname); + iface.set('ifname', (ifnames.length > 1) ? ifnames : ifnames[0]); + } + } + else + { + var networks = L.toArray(this.get('network')); + if ($.inArray(iface.name(), networks) < 0) + { + networks.push(iface.name()); + this.set('network', (networks.length > 1) ? networks : networks[0]); + } + } + } + }); + + network_class.Protocol = network_class.Interface.extend({ + description: '__unknown__', + tunnel: false, + virtual: false, + + populateForm: function(section, iface) + { + + } + }); + + return Class.extend(network_class); +})(); diff --git a/luci2/htdocs/luci2/rpc.js b/luci2/htdocs/luci2/rpc.js new file mode 100644 index 0000000..0aa0612 --- /dev/null +++ b/luci2/htdocs/luci2/rpc.js @@ -0,0 +1,188 @@ +Class.extend({ + _id: 1, + _batch: undefined, + _requests: { }, + + _call: function(req, cb) + { + var q = ''; + + if ($.isArray(req)) + 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 $.ajax('/ubus' + q, { + cache: false, + contentType: 'application/json', + data: JSON.stringify(req), + dataType: 'json', + type: 'POST', + timeout: L.globals.timeout, + _rpc_req: req + }).then(cb, cb); + }, + + _list_cb: function(msg) + { + var list = msg.result; + + /* verify message frame */ + if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id || !$.isArray(list)) + list = [ ]; + + return $.Deferred().resolveWith(this, [ list ]); + }, + + _call_cb: function(msg) + { + var data = [ ]; + var type = Object.prototype.toString; + var reqs = this._rpc_req; + + if (!$.isArray(reqs)) + { + msg = [ msg ]; + reqs = [ reqs ]; + } + + for (var i = 0; i < msg.length; i++) + { + /* fetch related request info */ + var req = L.rpc._requests[reqs[i].id]; + if (typeof(req) != 'object') + throw 'No related request for JSON response'; + + /* fetch response attribute and verify returned type */ + var ret = undefined; + + /* verify message frame */ + if (typeof(msg[i]) == 'object' && msg[i].jsonrpc == '2.0') + if ($.isArray(msg[i].result) && msg[i].result[0] == 0) + ret = (msg[i].result.length > 1) ? msg[i].result[1] : msg[i].result[0]; + + if (req.expect) + { + for (var key in req.expect) + { + if (typeof(ret) != 'undefined' && key != '') + ret = ret[key]; + + if (typeof(ret) == 'undefined' || 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(L.rpc, req.priv); + } + + /* store response data */ + if (typeof(req.index) == 'number') + data[req.index] = ret; + else + data = ret; + + /* delete request object */ + delete L.rpc._requests[reqs[i].id]; + } + + return $.Deferred().resolveWith(this, [ data ]); + }, + + list: function() + { + var params = [ ]; + for (var i = 0; i < arguments.length; i++) + params[i] = arguments[i]; + + var msg = { + jsonrpc: '2.0', + id: this._id++, + method: 'list', + params: (params.length > 0) ? params : undefined + }; + + return this._call(msg, this._list_cb); + }, + + batch: function() + { + if (!$.isArray(this._batch)) + this._batch = [ ]; + }, + + flush: function() + { + if (!$.isArray(this._batch)) + return L.deferrable([ ]); + + var req = this._batch; + delete this._batch; + + /* call rpc */ + return this._call(req, this._call_cb); + }, + + declare: function(options) + { + var _rpc = this; + + return function() { + /* build parameter object */ + var p_off = 0; + var params = { }; + if ($.isArray(options.params)) + for (p_off = 0; p_off < options.params.length; p_off++) + params[options.params[p_off]] = arguments[p_off]; + + /* all remaining arguments are private args */ + var priv = [ undefined, undefined ]; + for (; p_off < arguments.length; p_off++) + priv.push(arguments[p_off]); + + /* store request info */ + var req = _rpc._requests[_rpc._id] = { + expect: options.expect, + filter: options.filter, + params: params, + priv: priv + }; + + /* build message object */ + var msg = { + jsonrpc: '2.0', + id: _rpc._id++, + method: 'call', + params: [ + L.globals.sid, + options.object, + options.method, + params + ] + }; + + /* when a batch is in progress then store index in request data + * and push message object onto the stack */ + if ($.isArray(_rpc._batch)) + { + req.index = _rpc._batch.push(msg) - 1; + return L.deferrable(msg); + } + + /* call rpc */ + return _rpc._call(msg, _rpc._call_cb); + }; + } +}); diff --git a/luci2/htdocs/luci2/session.js b/luci2/htdocs/luci2/session.js new file mode 100644 index 0000000..9e5b435 --- /dev/null +++ b/luci2/htdocs/luci2/session.js @@ -0,0 +1,78 @@ +Class.extend({ + login: L.rpc.declare({ + object: 'session', + method: 'login', + params: [ 'username', 'password' ], + expect: { '': { } } + }), + + access: L.rpc.declare({ + object: 'session', + method: 'access', + params: [ 'scope', 'object', 'function' ], + expect: { access: false } + }), + + isAlive: function() + { + return L.session.access('ubus', 'session', 'access'); + }, + + startHeartbeat: function() + { + this._hearbeatInterval = window.setInterval(function() { + L.session.isAlive().then(function(alive) { + if (!alive) + { + L.session.stopHeartbeat(); + L.ui.login(true); + } + + }); + }, L.globals.timeout * 2); + }, + + stopHeartbeat: function() + { + if (typeof(this._hearbeatInterval) != 'undefined') + { + window.clearInterval(this._hearbeatInterval); + delete this._hearbeatInterval; + } + }, + + + aclCache: { }, + + callAccess: L.rpc.declare({ + object: 'session', + method: 'access', + expect: { '': { } } + }), + + callAccessCallback: function(acls) + { + L.session.aclCache = acls; + }, + + updateACLs: function() + { + return L.session.callAccess() + .then(L.session.callAccessCallback); + }, + + hasACL: function(scope, object, func) + { + var acls = L.session.aclCache; + + if (typeof(func) == 'undefined') + return (acls && acls[scope] && acls[scope][object]); + + if (acls && acls[scope] && acls[scope][object]) + for (var i = 0; i < acls[scope][object].length; i++) + if (acls[scope][object][i] == func) + return true; + + return false; + } +}); diff --git a/luci2/htdocs/luci2/system.js b/luci2/htdocs/luci2/system.js new file mode 100644 index 0000000..f84d61f --- /dev/null +++ b/luci2/htdocs/luci2/system.js @@ -0,0 +1,82 @@ +Class.extend({ + getSystemInfo: L.rpc.declare({ + object: 'system', + method: 'info', + expect: { '': { } } + }), + + getBoardInfo: L.rpc.declare({ + object: 'system', + method: 'board', + expect: { '': { } } + }), + + getDiskInfo: L.rpc.declare({ + object: 'luci2.system', + method: 'diskfree', + expect: { '': { } } + }), + + getInfo: function(cb) + { + L.rpc.batch(); + + this.getSystemInfo(); + this.getBoardInfo(); + this.getDiskInfo(); + + return L.rpc.flush().then(function(info) { + var rv = { }; + + $.extend(rv, info[0]); + $.extend(rv, info[1]); + $.extend(rv, info[2]); + + return rv; + }); + }, + + + initList: L.rpc.declare({ + object: 'luci2.system', + method: 'init_list', + expect: { initscripts: [ ] }, + filter: function(data) { + data.sort(function(a, b) { return (a.start || 0) - (b.start || 0) }); + return data; + } + }), + + initEnabled: function(init, cb) + { + return this.initList().then(function(list) { + for (var i = 0; i < list.length; i++) + if (list[i].name == init) + return !!list[i].enabled; + + return false; + }); + }, + + initRun: L.rpc.declare({ + object: 'luci2.system', + method: 'init_action', + params: [ 'name', 'action' ], + filter: function(data) { + return (data == 0); + } + }), + + initStart: function(init, cb) { return L.system.initRun(init, 'start', cb) }, + initStop: function(init, cb) { return L.system.initRun(init, 'stop', cb) }, + initRestart: function(init, cb) { return L.system.initRun(init, 'restart', cb) }, + initReload: function(init, cb) { return L.system.initRun(init, 'reload', cb) }, + initEnable: function(init, cb) { return L.system.initRun(init, 'enable', cb) }, + initDisable: function(init, cb) { return L.system.initRun(init, 'disable', cb) }, + + + performReboot: L.rpc.declare({ + object: 'luci2.system', + method: 'reboot' + }) +}); diff --git a/luci2/htdocs/luci2/uci.js b/luci2/htdocs/luci2/uci.js new file mode 100644 index 0000000..2056ce9 --- /dev/null +++ b/luci2/htdocs/luci2/uci.js @@ -0,0 +1,532 @@ +Class.extend({ + init: function() + { + this.state = { + newidx: 0, + values: { }, + creates: { }, + changes: { }, + deletes: { }, + reorder: { } + }; + }, + + callLoad: L.rpc.declare({ + object: 'uci', + method: 'get', + params: [ 'config' ], + expect: { values: { } } + }), + + callOrder: L.rpc.declare({ + object: 'uci', + method: 'order', + params: [ 'config', 'sections' ] + }), + + callAdd: L.rpc.declare({ + object: 'uci', + method: 'add', + params: [ 'config', 'type', 'name', 'values' ], + expect: { section: '' } + }), + + callSet: L.rpc.declare({ + object: 'uci', + method: 'set', + params: [ 'config', 'section', 'values' ] + }), + + callDelete: L.rpc.declare({ + object: 'uci', + method: 'delete', + params: [ 'config', 'section', 'options' ] + }), + + callApply: L.rpc.declare({ + object: 'uci', + method: 'apply', + params: [ 'timeout', 'rollback' ] + }), + + callConfirm: L.rpc.declare({ + object: 'uci', + method: 'confirm' + }), + + createSID: function(conf) + { + var v = this.state.values; + var n = this.state.creates; + var sid; + + do { + sid = "new%06x".format(Math.random() * 0xFFFFFF); + } while ((n[conf] && n[conf][sid]) || (v[conf] && v[conf][sid])); + + return sid; + }, + + reorderSections: function() + { + var v = this.state.values; + var n = this.state.creates; + var r = this.state.reorder; + + if ($.isEmptyObject(r)) + return L.deferrable(); + + L.rpc.batch(); + + /* + 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']); + + this.callOrder(c, sids); + } + } + + this.state.reorder = { }; + return L.rpc.flush(); + }, + + load: function(packages) + { + var self = this; + var seen = { }; + var pkgs = [ ]; + + if (!$.isArray(packages)) + packages = [ packages ]; + + L.rpc.batch(); + + for (var i = 0; i < packages.length; i++) + if (!seen[packages[i]] && !self.state.values[packages[i]]) + { + pkgs.push(packages[i]); + seen[packages[i]] = true; + self.callLoad(packages[i]); + } + + return L.rpc.flush().then(function(responses) { + for (var i = 0; i < responses.length; i++) + self.state.values[pkgs[i]] = responses[i]; + + return pkgs; + }); + }, + + unload: function(packages) + { + if (!$.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]]; + } + }, + + add: function(conf, type, name) + { + var n = this.state.creates; + var 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; + var c = this.state.changes; + var 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 = [ ]; + var v = this.state.values[conf]; + var n = this.state.creates[conf]; + var c = this.state.changes[conf]; + var 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($.extend({ }, v[s], c ? c[s] : undefined)); + + if (n) + for (var s in n) + if (!type || n[s]['.type'] == type) + sa.push(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; + var n = this.state.creates; + var c = this.state.changes; + var d = this.state.deletes; + + if (typeof(sid) == 'undefined') + return undefined; + + /* requested option in a just created section */ + if (n[conf] && n[conf][sid]) + { + if (!n[conf]) + return undefined; + + if (typeof(opt) == 'undefined') + return n[conf][sid]; + + return n[conf][sid][opt]; + } + + /* requested an option value */ + if (typeof(opt) != 'undefined') + { + /* 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] && typeof(c[conf][sid][opt]) != 'undefined') + 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; + var n = this.state.creates; + var c = this.state.changes; + var d = this.state.deletes; + + if (typeof(sid) == 'undefined' || + typeof(opt) == 'undefined' || + opt.charAt(0) == '.') + return; + + if (n[conf] && n[conf][sid]) + { + if (typeof(val) != 'undefined') + n[conf][sid][opt] = val; + else + delete n[conf][sid][opt]; + } + else if (typeof(val) != 'undefined') + { + /* 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] = L.filterArray(d[conf][sid], opt); + + c[conf][sid][opt] = val; + } + else + { + /* only delete in existing sections */ + if (!v[conf] || !v[conf][sid]) + 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, undefined); + }, + + get_first: function(conf, type, opt) + { + var sid = undefined; + + L.uci.sections(conf, type, function(s) { + if (typeof(sid) != 'string') + sid = s['.name']; + }); + + return this.get(conf, sid, opt); + }, + + set_first: function(conf, type, opt, val) + { + var sid = undefined; + + L.uci.sections(conf, type, function(s) { + if (typeof(sid) != 'string') + sid = s['.name']; + }); + + return this.set(conf, sid, opt, val); + }, + + unset_first: function(conf, type, opt) + { + return this.set_first(conf, type, opt, undefined); + }, + + swap: function(conf, sid1, sid2) + { + var s1 = this.get(conf, sid1); + var s2 = this.get(conf, sid2); + var n1 = s1 ? s1['.index'] : NaN; + var n2 = s2 ? s2['.index'] : NaN; + + if (isNaN(n1) || isNaN(n2)) + return false; + + s1['.index'] = n2; + s2['.index'] = n1; + + this.state.reorder[conf] = true; + + return true; + }, + + save: function() + { + L.rpc.batch(); + + var v = this.state.values; + var n = this.state.creates; + var c = this.state.changes; + var d = this.state.deletes; + + var self = this; + var snew = [ ]; + var pkgs = { }; + + 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]); + + 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]) + 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]; + self.callDelete(conf, sid, (o === true) ? undefined : o); + } + + pkgs[conf] = true; + } + + return L.rpc.flush().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 = L.toArray(pkgs); + + self.unload(pkgs); + + return self.load(pkgs); + }); + }, + + apply: function(timeout) + { + var self = this; + var date = new Date(); + var deferred = $.Deferred(); + + if (typeof(timeout) != 'number' || timeout < 1) + timeout = 10; + + self.callApply(timeout, true).then(function(rv) { + if (rv != 0) + { + deferred.rejectWith(self, [ rv ]); + return; + } + + 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 + deferred.rejectWith(self, [ rv ]); + + return; + } + + deferred.resolveWith(self, [ rv ]); + }); + }; + + window.setTimeout(try_confirm, 1000); + }); + + return deferred; + }, + + changes: L.rpc.declare({ + object: 'uci', + method: 'changes', + expect: { changes: { } } + }), + + readable: function(conf) + { + return L.session.hasACL('uci', conf, 'read'); + }, + + writable: function(conf) + { + return L.session.hasACL('uci', conf, 'write'); + } +}); diff --git a/luci2/htdocs/luci2/ui.js b/luci2/htdocs/luci2/ui.js new file mode 100644 index 0000000..1fb9a49 --- /dev/null +++ b/luci2/htdocs/luci2/ui.js @@ -0,0 +1,1324 @@ +(function() { + var ui_class = { + saveScrollTop: function() + { + this._scroll_top = $(document).scrollTop(); + }, + + restoreScrollTop: function() + { + if (typeof(this._scroll_top) == 'undefined') + return; + + $(document).scrollTop(this._scroll_top); + + delete this._scroll_top; + }, + + loading: function(enable) + { + var win = $(window); + var body = $('body'); + + var state = this._loading || (this._loading = { + modal: $('<div />') + .css('z-index', 2000) + .addClass('modal fade') + .append($('<div />') + .addClass('modal-dialog') + .append($('<div />') + .addClass('modal-content luci2-modal-loader') + .append($('<div />') + .addClass('modal-body') + .text(L.tr('Loading data…'))))) + .appendTo(body) + .modal({ + backdrop: 'static', + keyboard: false + }) + }); + + state.modal.modal(enable ? 'show' : 'hide'); + }, + + dialog: function(title, content, options) + { + var win = $(window); + var body = $('body'); + var self = this; + + var state = this._dialog || (this._dialog = { + dialog: $('<div />') + .addClass('modal fade') + .append($('<div />') + .addClass('modal-dialog') + .append($('<div />') + .addClass('modal-content') + .append($('<div />') + .addClass('modal-header') + .append('<h4 />') + .addClass('modal-title')) + .append($('<div />') + .addClass('modal-body')) + .append($('<div />') + .addClass('modal-footer') + .append(self.button(L.tr('Close'), 'primary') + .click(function() { + $(this).parents('div.modal').modal('hide'); + }))))) + .appendTo(body) + }); + + if (typeof(options) != 'object') + options = { }; + + if (title === false) + { + state.dialog.modal('hide'); + + return state.dialog; + } + + var cnt = state.dialog.children().children().children('div.modal-body'); + var ftr = state.dialog.children().children().children('div.modal-footer'); + + ftr.empty().show(); + + if (options.style == 'confirm') + { + ftr.append(L.ui.button(L.tr('Ok'), 'primary') + .click(options.confirm || function() { L.ui.dialog(false) })); + + ftr.append(L.ui.button(L.tr('Cancel'), 'default') + .click(options.cancel || function() { L.ui.dialog(false) })); + } + else if (options.style == 'close') + { + ftr.append(L.ui.button(L.tr('Close'), 'primary') + .click(options.close || function() { L.ui.dialog(false) })); + } + else if (options.style == 'wait') + { + ftr.append(L.ui.button(L.tr('Close'), 'primary') + .attr('disabled', true)); + } + + if (options.wide) + { + state.dialog.addClass('wide'); + } + else + { + state.dialog.removeClass('wide'); + } + + state.dialog.find('h4:first').text(title); + state.dialog.modal('show'); + + cnt.empty().append(content); + + return state.dialog; + }, + + upload: function(title, content, options) + { + var state = L.ui._upload || (L.ui._upload = { + form: $('<form />') + .attr('method', 'post') + .attr('action', '/cgi-bin/luci-upload') + .attr('enctype', 'multipart/form-data') + .attr('target', 'cbi-fileupload-frame') + .append($('<p />')) + .append($('<input />') + .attr('type', 'hidden') + .attr('name', 'sessionid')) + .append($('<input />') + .attr('type', 'hidden') + .attr('name', 'filename')) + .append($('<input />') + .attr('type', 'file') + .attr('name', 'filedata') + .addClass('cbi-input-file')) + .append($('<div />') + .css('width', '100%') + .addClass('progress progress-striped active') + .append($('<div />') + .addClass('progress-bar') + .css('width', '100%'))) + .append($('<iframe />') + .addClass('pull-right') + .attr('name', 'cbi-fileupload-frame') + .css('width', '1px') + .css('height', '1px') + .css('visibility', 'hidden')), + + finish_cb: function(ev) { + $(this).off('load'); + + var body = (this.contentDocument || this.contentWindow.document).body; + if (body.firstChild.tagName.toLowerCase() == 'pre') + body = body.firstChild; + + var json; + try { + json = $.parseJSON(body.innerHTML); + } catch(e) { + json = { + message: L.tr('Invalid server response received'), + error: [ -1, L.tr('Invalid data') ] + }; + }; + + if (json.error) + { + L.ui.dialog(L.tr('File upload'), [ + $('<p />').text(L.tr('The file upload failed with the server response below:')), + $('<pre />').addClass('alert-message').text(json.message || json.error[1]), + $('<p />').text(L.tr('In case of network problems try uploading the file again.')) + ], { style: 'close' }); + } + else if (typeof(state.success_cb) == 'function') + { + state.success_cb(json); + } + }, + + confirm_cb: function() { + var f = state.form.find('.cbi-input-file'); + var b = state.form.find('.progress'); + var p = state.form.find('p'); + + if (!f.val()) + return; + + state.form.find('iframe').on('load', state.finish_cb); + state.form.submit(); + + f.hide(); + b.show(); + p.text(L.tr('File upload in progress …')); + + state.form.parent().parent().find('button').prop('disabled', true); + } + }); + + state.form.find('.progress').hide(); + state.form.find('.cbi-input-file').val('').show(); + state.form.find('p').text(content || L.tr('Select the file to upload and press "%s" to proceed.').format(L.tr('Ok'))); + + state.form.find('[name=sessionid]').val(L.globals.sid); + state.form.find('[name=filename]').val(options.filename); + + state.success_cb = options.success; + + L.ui.dialog(title || L.tr('File upload'), state.form, { + style: 'confirm', + confirm: state.confirm_cb + }); + }, + + reconnect: function() + { + var protocols = (location.protocol == 'https:') ? [ 'http', 'https' ] : [ 'http' ]; + var ports = (location.protocol == 'https:') ? [ 80, location.port || 443 ] : [ location.port || 80 ]; + var address = location.hostname.match(/^[A-Fa-f0-9]*:[A-Fa-f0-9:]+$/) ? '[' + location.hostname + ']' : location.hostname; + var images = $(); + var interval, timeout; + + L.ui.dialog( + L.tr('Waiting for device'), [ + $('<p />').text(L.tr('Please stand by while the device is reconfiguring …')), + $('<div />') + .css('width', '100%') + .addClass('progressbar') + .addClass('intermediate') + .append($('<div />') + .css('width', '100%')) + ], { style: 'wait' } + ); + + for (var i = 0; i < protocols.length; i++) + images = images.add($('<img />').attr('url', protocols[i] + '://' + address + ':' + ports[i])); + + //L.network.getNetworkStatus(function(s) { + // for (var i = 0; i < protocols.length; i++) + // { + // for (var j = 0; j < s.length; j++) + // { + // for (var k = 0; k < s[j]['ipv4-address'].length; k++) + // images = images.add($('<img />').attr('url', protocols[i] + '://' + s[j]['ipv4-address'][k].address + ':' + ports[i])); + // + // for (var l = 0; l < s[j]['ipv6-address'].length; l++) + // images = images.add($('<img />').attr('url', protocols[i] + '://[' + s[j]['ipv6-address'][l].address + ']:' + ports[i])); + // } + // } + //}).then(function() { + images.on('load', function() { + var url = this.getAttribute('url'); + L.session.isAlive().then(function(access) { + if (access) + { + window.clearTimeout(timeout); + window.clearInterval(interval); + L.ui.dialog(false); + images = null; + } + else + { + location.href = url; + } + }); + }); + + interval = window.setInterval(function() { + images.each(function() { + this.setAttribute('src', this.getAttribute('url') + L.globals.resource + '/icons/loading.gif?r=' + Math.random()); + }); + }, 5000); + + timeout = window.setTimeout(function() { + window.clearInterval(interval); + images.off('load'); + + L.ui.dialog( + L.tr('Device not responding'), + L.tr('The device was not responding within 180 seconds, you might need to manually reconnect your computer or use SSH to regain access.'), + { style: 'close' } + ); + }, 180000); + //}); + }, + + login: function(invalid) + { + var state = L.ui._login || (L.ui._login = { + form: $('<form />') + .attr('target', '') + .attr('method', 'post') + .append($('<p />') + .addClass('alert alert-danger') + .text(L.tr('Wrong username or password given!'))) + .append($('<p />') + .append($('<label />') + .text(L.tr('Username')) + .append($('<br />')) + .append($('<input />') + .attr('type', 'text') + .attr('name', 'username') + .attr('value', 'root') + .addClass('form-control') + .keypress(function(ev) { + if (ev.which == 10 || ev.which == 13) + state.confirm_cb(); + })))) + .append($('<p />') + .append($('<label />') + .text(L.tr('Password')) + .append($('<br />')) + .append($('<input />') + .attr('type', 'password') + .attr('name', 'password') + .addClass('form-control') + .keypress(function(ev) { + if (ev.which == 10 || ev.which == 13) + state.confirm_cb(); + })))) + .append($('<p />') + .text(L.tr('Enter your username and password above, then click "%s" to proceed.').format(L.tr('Ok')))), + + response_cb: function(response) { + if (!response.ubus_rpc_session) + { + L.ui.login(true); + } + else + { + L.globals.sid = response.ubus_rpc_session; + L.setHash('id', L.globals.sid); + L.session.startHeartbeat(); + L.ui.dialog(false); + state.deferred.resolve(); + } + }, + + confirm_cb: function() { + var u = state.form.find('[name=username]').val(); + var p = state.form.find('[name=password]').val(); + + if (!u) + return; + + L.ui.dialog( + L.tr('Logging in'), [ + $('<p />').text(L.tr('Log in in progress …')), + $('<div />') + .css('width', '100%') + .addClass('progressbar') + .addClass('intermediate') + .append($('<div />') + .css('width', '100%')) + ], { style: 'wait' } + ); + + L.globals.sid = '00000000000000000000000000000000'; + L.session.login(u, p).then(state.response_cb); + } + }); + + if (!state.deferred || state.deferred.state() != 'pending') + state.deferred = $.Deferred(); + + /* try to find sid from hash */ + var sid = L.getHash('id'); + if (sid && sid.match(/^[a-f0-9]{32}$/)) + { + L.globals.sid = sid; + L.session.isAlive().then(function(access) { + if (access) + { + L.session.startHeartbeat(); + state.deferred.resolve(); + } + else + { + L.setHash('id', undefined); + L.ui.login(); + } + }); + + return state.deferred; + } + + if (invalid) + state.form.find('.alert-message').show(); + else + state.form.find('.alert-message').hide(); + + L.ui.dialog(L.tr('Authorization Required'), state.form, { + style: 'confirm', + confirm: state.confirm_cb + }); + + state.form.find('[name=password]').focus(); + + return state.deferred; + }, + + cryptPassword: L.rpc.declare({ + object: 'luci2.ui', + method: 'crypt', + params: [ 'data' ], + expect: { crypt: '' } + }), + + + mergeACLScope: function(acl_scope, scope) + { + if ($.isArray(scope)) + { + for (var i = 0; i < scope.length; i++) + acl_scope[scope[i]] = true; + } + else if ($.isPlainObject(scope)) + { + for (var object_name in scope) + { + if (!$.isArray(scope[object_name])) + continue; + + var acl_object = acl_scope[object_name] || (acl_scope[object_name] = { }); + + for (var i = 0; i < scope[object_name].length; i++) + acl_object[scope[object_name][i]] = true; + } + } + }, + + mergeACLPermission: function(acl_perm, perm) + { + if ($.isPlainObject(perm)) + { + for (var scope_name in perm) + { + var acl_scope = acl_perm[scope_name] || (acl_perm[scope_name] = { }); + L.ui.mergeACLScope(acl_scope, perm[scope_name]); + } + } + }, + + mergeACLGroup: function(acl_group, group) + { + if ($.isPlainObject(group)) + { + if (!acl_group.description) + acl_group.description = group.description; + + if (group.read) + { + var acl_perm = acl_group.read || (acl_group.read = { }); + L.ui.mergeACLPermission(acl_perm, group.read); + } + + if (group.write) + { + var acl_perm = acl_group.write || (acl_group.write = { }); + L.ui.mergeACLPermission(acl_perm, group.write); + } + } + }, + + callACLsCallback: function(trees) + { + var acl_tree = { }; + + for (var i = 0; i < trees.length; i++) + { + if (!$.isPlainObject(trees[i])) + continue; + + for (var group_name in trees[i]) + { + var acl_group = acl_tree[group_name] || (acl_tree[group_name] = { }); + L.ui.mergeACLGroup(acl_group, trees[i][group_name]); + } + } + + return acl_tree; + }, + + callACLs: L.rpc.declare({ + object: 'luci2.ui', + method: 'acls', + expect: { acls: [ ] } + }), + + getAvailableACLs: function() + { + return this.callACLs().then(this.callACLsCallback); + }, + + renderChangeIndicator: function() + { + return $('<ul />') + .addClass('nav navbar-nav navbar-right') + .append($('<li />') + .append($('<a />') + .attr('id', 'changes') + .attr('href', '#') + .append($('<span />') + .addClass('label label-info')))); + }, + + callMenuCallback: function(entries) + { + L.globals.mainMenu = new L.ui.menu(); + L.globals.mainMenu.entries(entries); + + $('#mainmenu') + .empty() + .append(L.globals.mainMenu.render(0, 1)) + .append(L.ui.renderChangeIndicator()); + }, + + callMenu: L.rpc.declare({ + object: 'luci2.ui', + method: 'menu', + expect: { menu: { } } + }), + + renderMainMenu: function() + { + return this.callMenu().then(this.callMenuCallback); + }, + + renderViewMenu: function() + { + $('#viewmenu') + .empty() + .append(L.globals.mainMenu.render(2, 900)); + }, + + renderView: function() + { + var node = arguments[0]; + var name = node.view.split(/\//).join('.'); + var cname = L.toClassName(name); + var views = L.views || (L.views = { }); + var args = [ ]; + + for (var i = 1; i < arguments.length; i++) + args.push(arguments[i]); + + if (L.globals.currentView) + L.globals.currentView.finish(); + + L.ui.renderViewMenu(); + L.setHash('view', node.view); + + if (views[cname] instanceof L.ui.view) + { + L.globals.currentView = views[cname]; + return views[cname].render.apply(views[cname], args); + } + + var url = L.globals.resource + '/view/' + name + '.js'; + + return $.ajax(url, { + method: 'GET', + cache: true, + dataType: 'text' + }).then(function(data) { + try { + var viewConstructorSource = ( + '(function(L, $) { ' + + 'return %s' + + '})(L, $);\n\n' + + '//@ sourceURL=%s' + ).format(data, url); + + var viewConstructor = eval(viewConstructorSource); + + views[cname] = new viewConstructor({ + name: name, + acls: node.write || { } + }); + + L.globals.currentView = views[cname]; + return views[cname].render.apply(views[cname], args); + } + catch(e) { + alert('Unable to instantiate view "%s": %s'.format(url, e)); + }; + + return $.Deferred().resolve(); + }); + }, + + changeView: function() + { + var name = L.getHash('view'); + var node = L.globals.defaultNode; + + if (name && L.globals.mainMenu) + node = L.globals.mainMenu.getNode(name); + + if (node) + { + L.ui.loading(true); + L.ui.renderView(node).then(function() { + L.ui.loading(false); + }); + } + }, + + updateHostname: function() + { + return L.system.getBoardInfo().then(function(info) { + if (info.hostname) + $('#hostname').text(info.hostname); + }); + }, + + updateChanges: function() + { + return L.uci.changes().then(function(changes) { + var n = 0; + var html = ''; + + for (var config in changes) + { + var log = [ ]; + + for (var i = 0; i < changes[config].length; i++) + { + var c = changes[config][i]; + + switch (c[0]) + { + case 'order': + log.push('uci reorder %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2])); + break; + + case 'remove': + if (c.length < 3) + log.push('uci delete %s.<del>%s</del>'.format(config, c[1])); + else + log.push('uci delete %s.%s.<del>%s</del>'.format(config, c[1], c[2])); + break; + + case 'rename': + if (c.length < 4) + log.push('uci rename %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3])); + else + log.push('uci rename %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4])); + break; + + case 'add': + log.push('uci add %s <ins>%s</ins> (= <ins><strong>%s</strong></ins>)'.format(config, c[2], c[1])); + break; + + case 'list-add': + log.push('uci add_list %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4])); + break; + + case 'list-del': + log.push('uci del_list %s.%s.<del>%s=<strong>%s</strong></del>'.format(config, c[1], c[2], c[3], c[4])); + break; + + case 'set': + if (c.length < 4) + log.push('uci set %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2])); + else + log.push('uci set %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4])); + break; + } + } + + html += '<code>/etc/config/%s</code><pre class="uci-changes">%s</pre>'.format(config, log.join('\n')); + n += changes[config].length; + } + + if (n > 0) + $('#changes') + .click(function(ev) { + L.ui.dialog(L.tr('Staged configuration changes'), html, { + style: 'confirm', + confirm: function() { + L.uci.apply().then( + function(code) { alert('Success with code ' + code); }, + function(code) { alert('Error with code ' + code); } + ); + } + }); + ev.preventDefault(); + }) + .children('span') + .show() + .text(L.trcp('Pending configuration changes', '1 change', '%d changes', n).format(n)); + else + $('#changes').children('span').hide(); + }); + }, + + load: function() + { + var self = this; + + self.loading(true); + + $.when( + L.session.updateACLs(), + self.updateHostname(), + self.updateChanges(), + self.renderMainMenu(), + L.network.load() + ).then(function() { + self.renderView(L.globals.defaultNode).then(function() { + self.loading(false); + }); + + $(window).on('hashchange', function() { + self.changeView(); + }); + }); + }, + + button: function(label, style, title) + { + style = style || 'default'; + + return $('<button />') + .attr('type', 'button') + .attr('title', title ? title : '') + .addClass('btn btn-' + style) + .text(label); + } + }; + + ui_class.AbstractWidget = Class.extend({ + i18n: function(text) { + return text; + }, + + label: function() { + var key = arguments[0]; + var args = [ ]; + + for (var i = 1; i < arguments.length; i++) + args.push(arguments[i]); + + switch (typeof(this.options[key])) + { + case 'undefined': + return ''; + + case 'function': + return this.options[key].apply(this, args); + + default: + return ''.format.apply('' + this.options[key], args); + } + }, + + toString: function() { + return $('<div />').append(this.render()).html(); + }, + + insertInto: function(id) { + return $(id).empty().append(this.render()); + }, + + appendTo: function(id) { + return $(id).append(this.render()); + }, + + on: function(evname, evfunc) + { + var evnames = L.toArray(evname); + + if (!this.events) + this.events = { }; + + for (var i = 0; i < evnames.length; i++) + this.events[evnames[i]] = evfunc; + + return this; + }, + + trigger: function(evname, evdata) + { + if (this.events) + { + var evnames = L.toArray(evname); + + for (var i = 0; i < evnames.length; i++) + if (this.events[evnames[i]]) + this.events[evnames[i]].call(this, evdata); + } + + return this; + } + }); + + ui_class.view = ui_class.AbstractWidget.extend({ + _fetch_template: function() + { + return $.ajax(L.globals.resource + '/template/' + this.options.name + '.htm', { + method: 'GET', + cache: true, + dataType: 'text', + success: function(data) { + data = data.replace(/<%([#:=])?(.+?)%>/g, function(match, p1, p2) { + p2 = p2.replace(/^\s+/, '').replace(/\s+$/, ''); + switch (p1) + { + case '#': + return ''; + + case ':': + return L.tr(p2); + + case '=': + return L.globals[p2] || ''; + + default: + return '(?' + match + ')'; + } + }); + + $('#maincontent').append(data); + } + }); + }, + + execute: function() + { + throw "Not implemented"; + }, + + render: function() + { + var container = $('#maincontent'); + + container.empty(); + + if (this.title) + container.append($('<h2 />').append(this.title)); + + if (this.description) + container.append($('<p />').append(this.description)); + + var self = this; + var args = [ ]; + + for (var i = 0; i < arguments.length; i++) + args.push(arguments[i]); + + return this._fetch_template().then(function() { + return L.deferrable(self.execute.apply(self, args)); + }); + }, + + repeat: function(func, interval) + { + var self = this; + + if (!self._timeouts) + self._timeouts = [ ]; + + var index = self._timeouts.length; + + if (typeof(interval) != 'number') + interval = 5000; + + var setTimer, runTimer; + + setTimer = function() { + if (self._timeouts) + self._timeouts[index] = window.setTimeout(runTimer, interval); + }; + + runTimer = function() { + L.deferrable(func.call(self)).then(setTimer, setTimer); + }; + + runTimer(); + }, + + finish: function() + { + if ($.isArray(this._timeouts)) + { + for (var i = 0; i < this._timeouts.length; i++) + window.clearTimeout(this._timeouts[i]); + + delete this._timeouts; + } + } + }); + + ui_class.menu = ui_class.AbstractWidget.extend({ + init: function() { + this._nodes = { }; + }, + + entries: function(entries) + { + for (var entry in entries) + { + var path = entry.split(/\//); + var node = this._nodes; + + for (i = 0; i < path.length; i++) + { + if (!node.childs) + node.childs = { }; + + if (!node.childs[path[i]]) + node.childs[path[i]] = { }; + + node = node.childs[path[i]]; + } + + $.extend(node, entries[entry]); + } + }, + + sortNodesCallback: function(a, b) + { + var x = a.index || 0; + var y = b.index || 0; + return (x - y); + }, + + firstChildView: function(node) + { + if (node.view) + return node; + + var nodes = [ ]; + for (var child in (node.childs || { })) + nodes.push(node.childs[child]); + + nodes.sort(this.sortNodesCallback); + + for (var i = 0; i < nodes.length; i++) + { + var child = this.firstChildView(nodes[i]); + if (child) + { + for (var key in child) + if (!node.hasOwnProperty(key) && child.hasOwnProperty(key)) + node[key] = child[key]; + + return node; + } + } + + return undefined; + }, + + handleClick: function(ev) + { + L.setHash('view', ev.data); + + ev.preventDefault(); + this.blur(); + }, + + renderNodes: function(childs, level, min, max) + { + var nodes = [ ]; + for (var node in childs) + { + var child = this.firstChildView(childs[node]); + if (child) + nodes.push(childs[node]); + } + + nodes.sort(this.sortNodesCallback); + + var list = $('<ul />'); + + if (level == 0) + list.addClass('nav').addClass('navbar-nav'); + else if (level == 1) + list.addClass('dropdown-menu').addClass('navbar-inverse'); + + for (var i = 0; i < nodes.length; i++) + { + if (!L.globals.defaultNode) + { + var v = L.getHash('view'); + if (!v || v == nodes[i].view) + L.globals.defaultNode = nodes[i]; + } + + var item = $('<li />') + .append($('<a />') + .attr('href', '#') + .text(L.tr(nodes[i].title))) + .appendTo(list); + + if (nodes[i].childs && level < max) + { + item.addClass('dropdown'); + + item.find('a') + .addClass('dropdown-toggle') + .attr('data-toggle', 'dropdown') + .append('<b class="caret"></b>'); + + item.append(this.renderNodes(nodes[i].childs, level + 1)); + } + else + { + item.find('a').click(nodes[i].view, this.handleClick); + } + } + + return list.get(0); + }, + + render: function(min, max) + { + var top = min ? this.getNode(L.globals.defaultNode.view, min) : this._nodes; + return this.renderNodes(top.childs, 0, min, max); + }, + + getNode: function(path, max) + { + var p = path.split(/\//); + var n = this._nodes; + + if (typeof(max) == 'undefined') + max = p.length; + + for (var i = 0; i < max; i++) + { + if (!n.childs[p[i]]) + return undefined; + + n = n.childs[p[i]]; + } + + return n; + } + }); + + ui_class.table = ui_class.AbstractWidget.extend({ + init: function() + { + this._rows = [ ]; + }, + + row: function(values) + { + if ($.isArray(values)) + { + this._rows.push(values); + } + else if ($.isPlainObject(values)) + { + var v = [ ]; + for (var i = 0; i < this.options.columns.length; i++) + { + var col = this.options.columns[i]; + + if (typeof col.key == 'string') + v.push(values[col.key]); + else + v.push(null); + } + this._rows.push(v); + } + }, + + rows: function(rows) + { + for (var i = 0; i < rows.length; i++) + this.row(rows[i]); + }, + + render: function(id) + { + var fieldset = document.createElement('fieldset'); + fieldset.className = 'cbi-section'; + + if (this.options.caption) + { + var legend = document.createElement('legend'); + $(legend).append(this.options.caption); + fieldset.appendChild(legend); + } + + var table = document.createElement('table'); + table.className = 'table table-condensed table-hover'; + + var has_caption = false; + var has_description = false; + + for (var i = 0; i < this.options.columns.length; i++) + if (this.options.columns[i].caption) + { + has_caption = true; + break; + } + else if (this.options.columns[i].description) + { + has_description = true; + break; + } + + if (has_caption) + { + var tr = table.insertRow(-1); + tr.className = 'cbi-section-table-titles'; + + for (var i = 0; i < this.options.columns.length; i++) + { + var col = this.options.columns[i]; + var th = document.createElement('th'); + th.className = 'cbi-section-table-cell'; + + tr.appendChild(th); + + if (col.width) + th.style.width = col.width; + + if (col.align) + th.style.textAlign = col.align; + + if (col.caption) + $(th).append(col.caption); + } + } + + if (has_description) + { + var tr = table.insertRow(-1); + tr.className = 'cbi-section-table-descr'; + + for (var i = 0; i < this.options.columns.length; i++) + { + var col = this.options.columns[i]; + var th = document.createElement('th'); + th.className = 'cbi-section-table-cell'; + + tr.appendChild(th); + + if (col.width) + th.style.width = col.width; + + if (col.align) + th.style.textAlign = col.align; + + if (col.description) + $(th).append(col.description); + } + } + + if (this._rows.length == 0) + { + if (this.options.placeholder) + { + var tr = table.insertRow(-1); + var td = tr.insertCell(-1); + td.className = 'cbi-section-table-cell'; + + td.colSpan = this.options.columns.length; + $(td).append(this.options.placeholder); + } + } + else + { + for (var i = 0; i < this._rows.length; i++) + { + var tr = table.insertRow(-1); + + for (var j = 0; j < this.options.columns.length; j++) + { + var col = this.options.columns[j]; + var td = tr.insertCell(-1); + + var val = this._rows[i][j]; + + if (typeof(val) == 'undefined') + val = col.placeholder; + + if (typeof(val) == 'undefined') + val = ''; + + if (col.width) + td.style.width = col.width; + + if (col.align) + td.style.textAlign = col.align; + + if (typeof col.format == 'string') + $(td).append(col.format.format(val)); + else if (typeof col.format == 'function') + $(td).append(col.format(val, i)); + else + $(td).append(val); + } + } + } + + this._rows = [ ]; + fieldset.appendChild(table); + + return fieldset; + } + }); + + ui_class.progress = ui_class.AbstractWidget.extend({ + render: function() + { + var vn = parseInt(this.options.value) || 0; + var mn = parseInt(this.options.max) || 100; + var pc = Math.floor((100 / mn) * vn); + + var text; + + if (typeof(this.options.format) == 'string') + text = this.options.format.format(this.options.value, this.options.max, pc); + else if (typeof(this.options.format) == 'function') + text = this.options.format(pc); + else + text = '%.2f%%'.format(pc); + + return $('<div />') + .addClass('progress') + .append($('<div />') + .addClass('progress-bar') + .addClass('progress-bar-info') + .css('width', pc + '%')) + .append($('<small />') + .text(text)); + } + }); + + ui_class.devicebadge = ui_class.AbstractWidget.extend({ + render: function() + { + var l2dev = this.options.l2_device || this.options.device; + var l3dev = this.options.l3_device; + var dev = l3dev || l2dev || '?'; + + var span = document.createElement('span'); + span.className = 'badge'; + + if (typeof(this.options.signal) == 'number' || + typeof(this.options.noise) == 'number') + { + var r = 'none'; + if (typeof(this.options.signal) != 'undefined' && + typeof(this.options.noise) != 'undefined') + { + var q = (-1 * (this.options.noise - this.options.signal)) / 5; + if (q < 1) + r = '0'; + else if (q < 2) + r = '0-25'; + else if (q < 3) + r = '25-50'; + else if (q < 4) + r = '50-75'; + else + r = '75-100'; + } + + span.appendChild(document.createElement('img')); + span.lastChild.src = L.globals.resource + '/icons/signal-' + r + '.png'; + + if (r == 'none') + span.title = L.tr('No signal'); + else + span.title = '%s: %d %s / %s: %d %s'.format( + L.tr('Signal'), this.options.signal, L.tr('dBm'), + L.tr('Noise'), this.options.noise, L.tr('dBm') + ); + } + else + { + var type = 'ethernet'; + var desc = L.tr('Ethernet device'); + + if (l3dev != l2dev) + { + type = 'tunnel'; + desc = L.tr('Tunnel interface'); + } + else if (dev.indexOf('br-') == 0) + { + type = 'bridge'; + desc = L.tr('Bridge'); + } + else if (dev.indexOf('.') > 0) + { + type = 'vlan'; + desc = L.tr('VLAN interface'); + } + else if (dev.indexOf('wlan') == 0 || + dev.indexOf('ath') == 0 || + dev.indexOf('wl') == 0) + { + type = 'wifi'; + desc = L.tr('Wireless Network'); + } + + span.appendChild(document.createElement('img')); + span.lastChild.src = L.globals.resource + '/icons/' + type + (this.options.up ? '' : '_disabled') + '.png'; + span.title = desc; + } + + $(span).append(' '); + $(span).append(dev); + + return span; + } + }); + + return Class.extend(ui_class); +})(); diff --git a/luci2/htdocs/luci2/wireless.js b/luci2/htdocs/luci2/wireless.js new file mode 100644 index 0000000..39121a6 --- /dev/null +++ b/luci2/htdocs/luci2/wireless.js @@ -0,0 +1,156 @@ +Class.extend({ + listDeviceNames: L.rpc.declare({ + object: 'iwinfo', + method: 'devices', + expect: { 'devices': [ ] }, + filter: function(data) { + data.sort(); + return data; + } + }), + + getDeviceStatus: L.rpc.declare({ + object: 'iwinfo', + method: 'info', + params: [ 'device' ], + expect: { '': { } }, + filter: function(data, params) { + if (!$.isEmptyObject(data)) + { + data['device'] = params['device']; + return data; + } + return undefined; + } + }), + + getAssocList: L.rpc.declare({ + object: 'iwinfo', + method: 'assoclist', + params: [ 'device' ], + expect: { results: [ ] }, + filter: function(data, params) { + for (var i = 0; i < data.length; i++) + data[i]['device'] = params['device']; + + data.sort(function(a, b) { + if (a.bssid < b.bssid) + return -1; + else if (a.bssid > b.bssid) + return 1; + else + return 0; + }); + + return data; + } + }), + + getWirelessStatus: function() { + return this.listDeviceNames().then(function(names) { + L.rpc.batch(); + + for (var i = 0; i < names.length; i++) + L.wireless.getDeviceStatus(names[i]); + + return L.rpc.flush(); + }).then(function(networks) { + var rv = { }; + + var phy_attrs = [ + 'country', 'channel', 'frequency', 'frequency_offset', + 'txpower', 'txpower_offset', 'hwmodes', 'hardware', 'phy' + ]; + + var net_attrs = [ + 'ssid', 'bssid', 'mode', 'quality', 'quality_max', + 'signal', 'noise', 'bitrate', 'encryption' + ]; + + for (var i = 0; i < networks.length; i++) + { + var phy = rv[networks[i].phy] || ( + rv[networks[i].phy] = { networks: [ ] } + ); + + var net = { + device: networks[i].device + }; + + for (var j = 0; j < phy_attrs.length; j++) + phy[phy_attrs[j]] = networks[i][phy_attrs[j]]; + + for (var j = 0; j < net_attrs.length; j++) + net[net_attrs[j]] = networks[i][net_attrs[j]]; + + phy.networks.push(net); + } + + return rv; + }); + }, + + getAssocLists: function() + { + return this.listDeviceNames().then(function(names) { + L.rpc.batch(); + + for (var i = 0; i < names.length; i++) + L.wireless.getAssocList(names[i]); + + return L.rpc.flush(); + }).then(function(assoclists) { + var rv = [ ]; + + for (var i = 0; i < assoclists.length; i++) + for (var j = 0; j < assoclists[i].length; j++) + rv.push(assoclists[i][j]); + + return rv; + }); + }, + + formatEncryption: function(enc) + { + var format_list = function(l, s) + { + var rv = [ ]; + for (var i = 0; i < l.length; i++) + rv.push(l[i].toUpperCase()); + return rv.join(s ? s : ', '); + } + + if (!enc || !enc.enabled) + return L.tr('None'); + + if (enc.wep) + { + if (enc.wep.length == 2) + return L.tr('WEP Open/Shared') + ' (%s)'.format(format_list(enc.ciphers, ', ')); + else if (enc.wep[0] == 'shared') + return L.tr('WEP Shared Auth') + ' (%s)'.format(format_list(enc.ciphers, ', ')); + else + return L.tr('WEP Open System') + ' (%s)'.format(format_list(enc.ciphers, ', ')); + } + else if (enc.wpa) + { + if (enc.wpa.length == 2) + return L.tr('mixed WPA/WPA2') + ' %s (%s)'.format( + format_list(enc.authentication, '/'), + format_list(enc.ciphers, ', ') + ); + else if (enc.wpa[0] == 2) + return 'WPA2 %s (%s)'.format( + format_list(enc.authentication, '/'), + format_list(enc.ciphers, ', ') + ); + else + return 'WPA %s (%s)'.format( + format_list(enc.authentication, '/'), + format_list(enc.ciphers, ', ') + ); + } + + return L.tr('Unknown'); + } +}); |