X-Git-Url: http://git.archive.openwrt.org/?p=project%2Fluci2%2Fui.git;a=blobdiff_plain;f=luci2%2Fhtdocs%2Fluci2%2Fluci2.js;h=e47f5bdc9199ac62a7c834a412f9255bd413cb40;hp=a012e5f662d3479579c090dd281b0058ed089271;hb=e93a839d17e933f970baf9b2481817d0598e1746;hpb=2f0e65dd27549ef4fde5b18588083968a59b66bd diff --git a/luci2/htdocs/luci2/luci2.js b/luci2/htdocs/luci2/luci2.js index a012e5f..e47f5bd 100644 --- a/luci2/htdocs/luci2/luci2.js +++ b/luci2/htdocs/luci2/luci2.js @@ -175,53 +175,7 @@ String.prototype.format = function() function LuCI2() { - var _luci2 = this; - - var alphacmp = function(a, b) - { - if (a < b) - return -1; - else if (a > b) - return 1; - else - return 0; - }; - - var retcb = function(cb, rv) - { - if (typeof(cb) == 'function') - cb(rv); - - return rv; - }; - - var isa = function(x, t) - { - if (typeof(x) != 'string' && typeof(t) == 'string') - return (Object.prototype.toString.call(x) == '[object ' + t + ']'); - - return (Object.prototype.toString.call(x) == Object.prototype.toString.call(t)); - }; - - var rcall = function(obj, func, params, res_attr, res_default, cb, filter) - { - if (typeof(params) == 'undefined') - params = { }; - - return _luci2.rpc.call(obj, func, params).then(function(res) { - if (res[0] != 0 || typeof(res[1]) == 'undefined') - return retcb(cb, res_default); - - var rv = (typeof(res_attr) != 'undefined') ? res[1][res_attr] : res[1]; - if (typeof(rv) == 'undefined' || (typeof(res_default) != 'undefined' && !isa(rv, res_default))) - return retcb(cb, res_default); - - if (typeof(filter) == 'function') - rv = filter(rv); - - return retcb(cb, rv); - }); - }; + var L = this; var Class = function() { }; @@ -260,7 +214,7 @@ function LuCI2() _class.prototype = prototype; _class.prototype.constructor = _class; - _class.extend = arguments.callee; + _class.extend = Class.extend; return _class; }; @@ -274,7 +228,7 @@ function LuCI2() return obj; }; - this.deferred = function(x) + this.isDeferred = function(x) { return (typeof(x) == 'object' && typeof(x.then) == 'function' && @@ -283,7 +237,7 @@ function LuCI2() this.deferrable = function() { - if (this.deferred(arguments[0])) + if (this.isDeferred(arguments[0])) return arguments[0]; var d = $.Deferred(); @@ -299,42 +253,42 @@ function LuCI2() plural: function(n) { return 0 + (n != 1) }, init: function() { - if (_luci2.i18n.loaded) + if (L.i18n.loaded) return; var lang = (navigator.userLanguage || navigator.language || 'en').toLowerCase(); var langs = (lang.indexOf('-') > -1) ? [ lang, lang.split(/-/)[0] ] : [ lang ]; for (var i = 0; i < langs.length; i++) - $.ajax('%s/i18n/base.%s.json'.format(_luci2.globals.resource, langs[i]), { + $.ajax('%s/i18n/base.%s.json'.format(L.globals.resource, langs[i]), { async: false, cache: true, dataType: 'json', success: function(data) { - $.extend(_luci2.i18n.catalog, data); + $.extend(L.i18n.catalog, data); - var pe = _luci2.i18n.catalog['']; + var pe = L.i18n.catalog['']; if (pe) { - delete _luci2.i18n.catalog['']; + delete L.i18n.catalog['']; try { var pf = new Function('n', 'return 0 + (' + pe + ')'); - _luci2.i18n.plural = pf; + L.i18n.plural = pf; } catch (e) { }; } } }); - _luci2.i18n.loaded = true; + L.i18n.loaded = true; } }; this.tr = function(msgid) { - _luci2.i18n.init(); + L.i18n.init(); - var msgstr = _luci2.i18n.catalog[msgid]; + var msgstr = L.i18n.catalog[msgid]; if (typeof(msgstr) == 'undefined') return msgid; @@ -346,23 +300,23 @@ function LuCI2() this.trp = function(msgid, msgid_plural, count) { - _luci2.i18n.init(); + L.i18n.init(); - var msgstr = _luci2.i18n.catalog[msgid]; + var msgstr = L.i18n.catalog[msgid]; if (typeof(msgstr) == 'undefined') return (count == 1) ? msgid : msgid_plural; else if (typeof(msgstr) == 'string') return msgstr; else - return msgstr[_luci2.i18n.plural(count)]; + return msgstr[L.i18n.plural(count)]; }; this.trc = function(msgctx, msgid) { - _luci2.i18n.init(); + L.i18n.init(); - var msgstr = _luci2.i18n.catalog[msgid + '\u0004' + msgctx]; + var msgstr = L.i18n.catalog[msgid + '\u0004' + msgctx]; if (typeof(msgstr) == 'undefined') return msgid; @@ -374,16 +328,16 @@ function LuCI2() this.trcp = function(msgctx, msgid, msgid_plural, count) { - _luci2.i18n.init(); + L.i18n.init(); - var msgstr = _luci2.i18n.catalog[msgid + '\u0004' + msgctx]; + var msgstr = L.i18n.catalog[msgid + '\u0004' + msgctx]; if (typeof(msgstr) == 'undefined') return (count == 1) ? msgid : msgid_plural; else if (typeof(msgstr) == 'string') return msgstr; else - return msgstr[_luci2.i18n.plural(count)]; + return msgstr[L.i18n.plural(count)]; }; this.setHash = function(key, value) @@ -410,8 +364,10 @@ function LuCI2() h += keys[i] + ':' + data[keys[i]]; } - if (h) + if (h.length) location.hash = '#' + h; + else + location.hash = ''; }; this.getHash = function(key) @@ -432,600 +388,1090 @@ function LuCI2() return data; }; - this.globals = { - resource: '/luci2' + this.toArray = function(x) + { + switch (typeof(x)) + { + case 'number': + case 'boolean': + return [ x ]; + + case 'string': + var r = [ ]; + var l = x.split(/\s+/); + for (var i = 0; i < l.length; i++) + if (l[i].length > 0) + r.push(l[i]); + return r; + + case 'object': + if ($.isArray(x)) + { + var r = [ ]; + for (var i = 0; i < x.length; i++) + r.push(x[i]); + return r; + } + else if ($.isPlainObject(x)) + { + var r = [ ]; + for (var k in x) + if (x.hasOwnProperty(k)) + r.push(k); + return r.sort(); + } + } + + return [ ]; }; - this.rpc = { + this.toObject = function(x) + { + switch (typeof(x)) + { + case 'number': + case 'boolean': + return { x: true }; + + case 'string': + var r = { }; + var l = x.split(/\x+/); + for (var i = 0; i < l.length; i++) + if (l[i].length > 0) + r[l[i]] = true; + return r; + + case 'object': + if ($.isArray(x)) + { + var r = { }; + for (var i = 0; i < x.length; i++) + r[x[i]] = true; + return r; + } + else if ($.isPlainObject(x)) + { + return x; + } + } - _msg_id: 1, + return { }; + }; - _wrap_msg: function(method, object, func, args) - { - if (typeof(args) != 'object') - args = { }; + this.filterArray = function(array, item) + { + if (!$.isArray(array)) + return [ ]; - return { - id: _luci2.rpc._msg_id++, - jsonrpc: "2.0", - method: method, - params: (method == 'call') ? [ _luci2.globals.sid, object, func, args ] : object - }; - }, + for (var i = 0; i < array.length; i++) + if (array[i] === item) + array.splice(i--, 1); - _parse_response: function(keys, priv) - { - return function(data) { - var obj; - try { - obj = $.parseJSON(data); - } catch(e) { } + return array; + }; - if (typeof(obj) != 'object') - return undefined; + this.toClassName = function(str, suffix) + { + var n = ''; + var l = str.split(/[\/.]/); - /* is a batched response */ - if (keys) - { - var rv = { }; - for (var i = 0; i < obj.length; i++) - { - var p = (typeof(priv) != 'undefined') ? priv[i] : undefined; + for (var i = 0; i < l.length; i++) + if (l[i].length > 0) + n += l[i].charAt(0).toUpperCase() + l[i].substr(1).toLowerCase(); - if ($.isArray(obj[i].result) && typeof(priv) != 'undefined') - obj[i].result[2] = p; + if (typeof(suffix) == 'string') + n += suffix; - if (obj[i].jsonrpc != '2.0' || obj[i].error || !obj[i].result) - rv[keys[i]] = [ 4 /* UBUS_STATUS_NO_DATA */, undefined, p ]; - else - rv[keys[i]] = obj[i].result; - } - return rv; - } + return n; + }; - if (obj.jsonrpc != '2.0' || obj.error || !obj.result) - return [ 4 /* UBUS_STATUS_NO_DATA */, undefined, priv ]; + this.toColor = function(str) + { + if (typeof(str) != 'string' || str.length == 0) + return '#CCCCCC'; - if ($.isArray(obj.result) && typeof(priv) != 'undefined') - obj.result[2] = priv; + if (str == 'wan') + return '#F09090'; + else if (str == 'lan') + return '#90F090'; - return obj.result; - }; - }, + var i = 0, hash = 0; - _post_msg: function(message, cb, keys, priv) - { - return $.ajax('/ubus', { - cache: false, - contentType: 'application/json', - data: JSON.stringify(message), - dataFilter: _luci2.rpc._parse_response(keys, priv), - dataType: 'text', - success: cb, - type: 'POST' - }); - }, + while (i < str.length) + hash = str.charCodeAt(i++) + ((hash << 5) - hash); - _post_single: function(object, method, args, cb, priv) - { - var msg = _luci2.rpc._wrap_msg('call', object, method, args, priv); - return _luci2.rpc._post_msg(msg, cb, undefined, priv); - }, + var r = (hash & 0xFF) % 128; + var g = ((hash >> 8) & 0xFF) % 128; + + var min = 0; + var max = 128; + + if ((r + g) < 128) + min = 128 - r - g; + else + max = 255 - r - g; + + var b = min + (((hash >> 16) & 0xFF) % (max - min)); + + return '#%02X%02X%02X'.format(0xFF - r, 0xFF - g, 0xFF - b); + }; + + this.parseIPv4 = function(str) + { + if ((typeof(str) != 'string' && !(str instanceof String)) || + !str.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) + return undefined; - _post_batch: function(methods, cb) + var num = [ ]; + var parts = str.split(/\./); + + for (var i = 0; i < parts.length; i++) { - if (typeof(methods) != 'object') + var n = parseInt(parts[i], 10); + if (isNaN(n) || n > 255) return undefined; - var msgs = [ ]; - var keys = [ ]; - var priv = [ ]; + num.push(n); + } + + return num; + }; + + this.parseIPv6 = function(str) + { + if ((typeof(str) != 'string' && !(str instanceof String)) || + !str.match(/^[a-fA-F0-9:]+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/)) + return undefined; + + var parts = str.split(/::/); + if (parts.length == 0 || parts.length > 2) + return undefined; - for (var k in methods) + var lnum = [ ]; + if (parts[0].length > 0) + { + var left = parts[0].split(/:/); + for (var i = 0; i < left.length; i++) { - if (typeof(methods[k]) != 'object' || methods[k].length < 2) - continue; + var n = parseInt(left[i], 16); + if (isNaN(n)) + return undefined; - keys.push(k); - priv.push(methods[k][3]); - msgs.push(_luci2.rpc._wrap_msg('call', methods[k][0], methods[k][1], methods[k][2])); + lnum.push((n / 256) >> 0); + lnum.push(n % 256); } + } - if (msgs.length > 0) - return _luci2.rpc._post_msg(msgs, cb, keys, priv); + var rnum = [ ]; + if (parts.length > 1 && parts[1].length > 0) + { + var right = parts[1].split(/:/); - return _luci2.deferrable([ ]); - }, + for (var i = 0; i < right.length; i++) + { + if (right[i].indexOf('.') > 0) + { + var addr = L.parseIPv4(right[i]); + if (!addr) + return undefined; - call: function() - { - var a = arguments; - if (typeof a[0] == 'string') - return _luci2.rpc._post_single(a[0], a[1], a[2], a[3], a[4]); - else - return _luci2.rpc._post_batch(a[0], a[1]); - }, + rnum.push.apply(rnum, addr); + continue; + } - list: function(objects) - { - var msg = _luci2.rpc._wrap_msg('list', objects); - return _luci2.rpc._post_msg(msg); - }, + var n = parseInt(right[i], 16); + if (isNaN(n)) + return undefined; - access: function(scope, object, method, cb) - { - return _luci2.rpc._post_single('session', 'access', { - 'sid': _luci2.globals.sid, - 'scope': scope, - 'object': object, - 'function': method - }, function(rv) { - return retcb(cb, (rv[0] == 0 && rv[1] && rv[1].access == true)); - }); + rnum.push((n / 256) >> 0); + rnum.push(n % 256); + } } + + if (rnum.length > 0 && (lnum.length + rnum.length) > 15) + return undefined; + + var num = [ ]; + + num.push.apply(num, lnum); + + for (var i = 0; i < (16 - lnum.length - rnum.length); i++) + num.push(0); + + num.push.apply(num, rnum); + + if (num.length > 16) + return undefined; + + return num; }; - this.uci = { + this.isNetmask = function(addr) + { + if (!$.isArray(addr)) + return false; - writable: function(cb) - { - return _luci2.rpc.access('ubus', 'uci', 'commit', cb); - }, + var c; - add: function(config, type, cb) - { - return rcall('uci', 'add', { config: config, type: type }, 'section', '', cb); - }, + for (c = 0; (c < addr.length) && (addr[c] == 255); c++); + + if (c == addr.length) + return true; - apply: function() + if ((addr[c] == 254) || (addr[c] == 252) || (addr[c] == 248) || + (addr[c] == 240) || (addr[c] == 224) || (addr[c] == 192) || + (addr[c] == 128) || (addr[c] == 0)) { + for (c++; (c < addr.length) && (addr[c] == 0); c++); - }, + if (c == addr.length) + return true; + } - changes: function(config) - { - return rcall('uci', 'changes', { config: config }, 'changes', [ ], cb); - }, + return false; + }; + + this.globals = { + timeout: 15000, + resource: '/luci2', + sid: '00000000000000000000000000000000' + }; + + this.rpc = { - commit: function(config) + _id: 1, + _batch: undefined, + _requests: { }, + + _call: function(req, cb) { - return rcall('uci', 'commit', { config: config }, undefined, undefined, 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); }, - 'delete': function(config, section, option) + _list_cb: function(msg) { - var req = { config: config, section: section }; + var list = msg.result; - if (isa(option, 'Array')) - req.options = option; - else - req.option = option; + /* verify message frame */ + if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id || !$.isArray(list)) + list = [ ]; - return rcall('uci', 'delete', req, undefined, undefined, cb); + return $.Deferred().resolveWith(this, [ list ]); }, - delete_all: function(config, type, matches) + _call_cb: function(msg) { - return rcall('uci', 'delete', { config: config, type: type, match: matches }, undefined, undefined, cb); - }, + var data = [ ]; + var type = Object.prototype.toString; + var reqs = this._rpc_req; - foreach: function(config, type, cb) - { - return rcall('uci', 'get', { config: config, type: type }, 'values', { }, function(sections) { - for (var s in sections) - cb(sections[s]); - }); - }, + if (!$.isArray(reqs)) + { + msg = [ msg ]; + reqs = [ reqs ]; + } - get: function(config, section, option, cb) - { - return rcall('uci', 'get', { config: config, section: section, option: option }, undefined, { }, function(res) { - if (typeof(option) == 'undefined') - return retcb(cb, (res.values && res.values['.type']) ? res.values['.type'] : undefined); + 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'; - return retcb(cb, res.value); - }); - }, + /* fetch response attribute and verify returned type */ + var ret = undefined; - get_all: function(config, section, cb) - { - return rcall('uci', 'get', { config: config, section: section }, 'values', { }, cb); - }, + /* 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]; - get_first: function(config, type, option, cb) - { - return rcall('uci', 'get', { config: config, type: type }, 'values', { }, function(sections) { - for (var s in sections) + if (req.expect) { - var val = (typeof(option) == 'string') ? sections[s][option] : sections[s]['.name']; + 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]; - if (typeof(val) != 'undefined') - return retcb(cb, val); + break; + } } - return retcb(cb, undefined); - }); - }, + /* apply filter */ + if (typeof(req.filter) == 'function') + { + req.priv[0] = ret; + req.priv[1] = req.params; + ret = req.filter.apply(L.rpc, req.priv); + } - section: function(config, type, name, values, cb) - { - return rcall('uci', 'add', { config: config, type: type, name: name, values: values }, 'section', undefined, cb); + /* 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 ]); }, - set: function(config, section, option, value, cb) + list: function() { - if (typeof(value) == 'undefined' && typeof(option) == 'string') - return rcall('uci', 'add', { config: config, section: section, type: option }, undefined, undefined, cb); - else if (isa(option, 'Object')) - return rcall('uci', 'set', { config: config, section: section, values: option }, undefined, undefined, cb); - else + var params = [ ]; + for (var i = 0; i < arguments.length; i++) + params[i] = arguments[i]; - var values = { }; - values[option] = value; + var msg = { + jsonrpc: '2.0', + id: this._id++, + method: 'list', + params: (params.length > 0) ? params : undefined + }; - return rcall('uci', 'set', { config: config, section: section, values: values }, undefined, undefined, cb); + return this._call(msg, this._list_cb); }, - order: function(config, sections, cb) + batch: function() { - return rcall('uci', 'order', { config: config, sections: sections }, undefined, undefined, cb); - } - }; + if (!$.isArray(this._batch)) + this._batch = [ ]; + }, - this.network = { - getNetworkStatus: function(cb) + flush: function() { - var ifaces = [ ]; - var assign = function(target, key) - { - return function(value) { - if (typeof(value) != 'undefined' && !$.isEmptyObject(value)) - target[key] = value; - }; - }; + if (!$.isArray(this._batch)) + return L.deferrable([ ]); - return _luci2.rpc.list().then(function(data) { - var requests = [ ]; + var req = this._batch; + delete this._batch; - for (var i = 0; i < data.length; i++) - { - if (data[i].indexOf('network.interface.') != 0) - continue; + /* call rpc */ + return this._call(req, this._call_cb); + }, - var ifname = data[i].substring(18); - if (ifname == 'loopback') - continue; + declare: function(options) + { + var _rpc = this; - var iface = { 'name': ifname }; + 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]; - ifaces.push(iface); - requests.push(['network.interface', 'status', { 'interface': ifname }, iface]); - } + /* all remaining arguments are private args */ + var priv = [ undefined, undefined ]; + for (; p_off < arguments.length; p_off++) + priv.push(arguments[p_off]); - return _luci2.rpc.call(requests, function(responses) { - for (var key in responses) - if (responses[key][0] == 0 && responses[key][1] && responses[key][2]) - $.extend(responses[key][2], responses[key][1]); - }); - }).then(function() { - var requests = [ ]; + /* 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 + ] + }; - for (var i = 0; i < ifaces.length; i++) + /* when a batch is in progress then store index in request data + * and push message object onto the stack */ + if ($.isArray(_rpc._batch)) { - var iface = ifaces[i]; + req.index = _rpc._batch.push(msg) - 1; + return L.deferrable(msg); + } - var dev = iface.l3_device || iface.l2_device; - if (!dev) - continue; + /* call rpc */ + return _rpc._call(msg, _rpc._call_cb); + }; + } + }; - iface.device = { 'name': dev }; - requests[dev] = ['network.device', 'status', { 'name': dev }, iface.device]; - } + this.UCIContext = Class.extend({ - return _luci2.rpc.call(requests, function(responses) { - for (var key in responses) - if (responses[key][0] == 0 && responses[key][1] && responses[key][2]) - $.extend(responses[key][2], responses[key][1]); - }); - }).then(function() { - var requests = [ ]; + init: function() + { + this.state = { + newidx: 0, + values: { }, + creates: { }, + changes: { }, + deletes: { }, + reorder: { } + }; + }, - for (var i = 0; i < ifaces.length; i++) - { - var iface = ifaces[i]; - if (!iface.device) - continue; + _load: L.rpc.declare({ + object: 'uci', + method: 'get', + params: [ 'config' ], + expect: { values: { } } + }), - var subdevs = iface.device['bridge-members']; - if (!subdevs) - continue; + _order: L.rpc.declare({ + object: 'uci', + method: 'order', + params: [ 'config', 'sections' ] + }), - iface.subdevices = [ ]; - for (var j = 0; j < subdevs.length; j++) - { - iface.subdevices[j] = { 'name': subdevs[j] }; - requests.push(['network.device', 'status', { 'name': subdevs[j] }, iface.subdevices[j]]); - } - } + _add: L.rpc.declare({ + object: 'uci', + method: 'add', + params: [ 'config', 'type', 'name', 'values' ], + expect: { section: '' } + }), - return _luci2.rpc.call(requests, function(responses) { - for (var key in responses) - if (responses[key][0] == 0 && responses[key][1] && responses[key][2]) - $.extend(responses[key][2], responses[key][1]); - }); - }).then(function() { - var requests = [ ]; + _set: L.rpc.declare({ + object: 'uci', + method: 'set', + params: [ 'config', 'section', 'values' ] + }), - for (var i = 0; i < ifaces.length; i++) - { - var iface = ifaces[i]; + _delete: L.rpc.declare({ + object: 'uci', + method: 'delete', + params: [ 'config', 'section', 'options' ] + }), - if (iface.device) - requests.push(['iwinfo', 'info', { 'device': iface.device.name }, iface.device]); + _newid: function(conf) + { + var v = this.state.values; + var n = this.state.creates; + var sid; - if (iface.subdevices) - for (var j = 0; j < iface.subdevices.length; j++) - requests.push(['iwinfo', 'info', { 'device': iface.subdevices[j].name }, iface.subdevices[j]]); - } + do { + sid = "new%06x".format(Math.random() * 0xFFFFFF); + } while ((n[conf] && n[conf][sid]) || (v[conf] && v[conf][sid])); - return _luci2.rpc.call(requests, function(responses) { - for (var key in responses) - if (responses[key][0] == 0 && responses[key][1] && responses[key][2]) - if (!$.isEmptyObject(responses[key][1])) - responses[key][2].wireless = responses[key][1]; - }); - }).then(function() { - ifaces.sort(function(a, b) { - if (a['interface'] < b['interface']) - return -1; - else if (a['interface'] > b['interface']) - return 1; - else - return 0; - }); - return retcb(cb, ifaces); - }); + return sid; }, - findWanInterfaces: function(cb) + load: function(packages) { - return _luci2.rpc.list().then(function(data) { - var requests = { }; - for (var i = 0; i < data.length; i++) - { - if (data[i].indexOf('network.interface.') == 0) - { - var ifname = data[i].substring(18); - requests[ifname] = ['network.interface', 'status', { 'interface': ifname }]; - } - } - return _luci2.rpc.call(requests); - }).then(function(responses) { - var rv = [ ]; - for (var ifname in responses) - { - var response = responses[ifname]; + var self = this; + var seen = { }; + var pkgs = [ ]; - if (response[0] != 0 || !response[1] || !response[1].route) - continue; + if (!$.isArray(packages)) + packages = [ packages ]; - for (var rn = 0, rt = response[1].route[rn]; - rn < response[1].route.length; - rn++, rt = response[1].route[rn]) - { - if (typeof(rt.table) != 'undefined') - continue; + L.rpc.batch(); - if (rt.target == '0.0.0.0' && rt.mask == 0) - rv[0] = response[1]; - else if (rt.target == '::' && rt.mask == 0) - rv[1] = response[1]; - } + 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._load(packages[i]); } - return retcb(cb, rv); - }); - }, + return L.rpc.flush().then(function(responses) { + for (var i = 0; i < responses.length; i++) + self.state.values[pkgs[i]] = responses[i]; - getDHCPLeases: function(cb) - { - return rcall('luci2.network', 'dhcp_leases', undefined, 'leases', [ ], cb); + return pkgs; + }); }, - getDHCPv6Leases: function(cb) + unload: function(packages) { - return rcall('luci2.network', 'dhcp6_leases', undefined, 'leases', [ ], cb); - }, + if (!$.isArray(packages)) + packages = [ packages ]; - getRoutes: function(cb) - { - return rcall('luci2.network', 'routes', undefined, 'routes', [ ], cb); + 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]]; + } }, - getIPv6Routes: function(cb) + add: function(conf, type, name) { - return rcall('luci2.network', 'routes6', undefined, 'routes', [ ], cb); - }, + var n = this.state.creates; + var sid = this._newid(conf); - getARPTable: function(cb) - { - return rcall('luci2.network', 'arp_table', undefined, 'entries', [ ], cb); - }, + if (!n[conf]) + n[conf] = { }; - getInterfaceStatus: function(iface, cb) - { - return rcall('network.interface', 'status', { 'interface': iface }, undefined, { }, cb, function(rv) { - rv['interface'] = iface; - rv['l2_device'] = rv['device']; - return rv; - }); - }, + n[conf][sid] = { + '.type': type, + '.name': sid, + '.create': name, + '.anonymous': !name, + '.index': 1000 + this.state.newidx++ + }; - getDeviceStatus: function(dev, cb) - { - return rcall('network.device', 'status', { name: dev }, undefined, { }, cb, function(rv) { - if (typeof(dev) == 'string') - rv.device = dev; - return rv; - }); + return sid; }, - getConntrackCount: function(cb) + remove: function(conf, sid) { - return rcall('luci2.network', 'conntrack_count', undefined, undefined, { - count: 0, - limit: 0 - }, cb); - } - }; + var n = this.state.creates; + var c = this.state.changes; + var d = this.state.deletes; - this.wireless = { - getDevices: function(cb) { - return rcall('iwinfo', 'devices', undefined, 'devices', [ ], cb, function(rv) { - rv.sort(); - return rv; + /* 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; }, - getInfo: function(dev, cb) { - var parse_info = function(device, info, rv) + 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 (!rv[info.phy]) - rv[info.phy] = { - networks: [ ] - }; + if (!n[conf]) + return undefined; - var phy = rv[info.phy]; + if (typeof(opt) == 'undefined') + return n[conf][sid]; - var phy_attrs = [ - 'country', 'channel', 'frequency', 'frequency_offset', - 'txpower', 'txpower_offset', 'hwmodes', 'hardware', 'phy' - ]; + return n[conf][sid][opt]; + } - var net_attrs = [ - 'ssid', 'bssid', 'mode', 'quality', 'quality_max', - 'signal', 'noise', 'bitrate', 'encryption' - ]; + /* 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 < phy_attrs.length; i++) - phy[phy_attrs[i]] = info[phy_attrs[i]]; + for (var i = 0; i < d[conf][sid].length; i++) + if (d[conf][sid][i] == opt) + return undefined; + } - var net = { - device: device - }; + /* check whether option was changed */ + if (c[conf] && c[conf][sid] && typeof(c[conf][sid][opt]) != 'undefined') + return c[conf][sid][opt]; - for (var i = 0; i < net_attrs.length; i++) - net[net_attrs[i]] = info[net_attrs[i]]; + /* return base value */ + if (v[conf] && v[conf][sid]) + return v[conf][sid][opt]; - phy.networks.push(net); + return undefined; + } - return phy; - }; + /* 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 (!dev) + 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') { - return _luci2.wireless.getDevices().then(function(devices) { - var requests = [ ]; + /* do not set within deleted section */ + if (d[conf] && d[conf][sid] === true) + return; - for (var i = 0; i < devices.length; i++) - { - if (devices[i].indexOf('.sta') >= 0) - continue; + /* only set in existing sections */ + if (!v[conf] || !v[conf][sid]) + return; - requests[devices[i]] = [ 'iwinfo', 'info', { device: devices[i] } ]; - } + if (!c[conf]) + c[conf] = { }; - return _luci2.rpc.call(requests); - }).then(function(responses) { - var rv = { }; + if (!c[conf][sid]) + c[conf][sid] = { }; - for (var device in responses) - { - var response = responses[device]; + /* undelete option */ + if (d[conf] && d[conf][sid]) + d[conf][sid] = L.filterArray(d[conf][sid], opt); - if (response[0] != 0 || !response[1]) - continue; + c[conf][sid][opt] = val; + } + else + { + /* only delete in existing sections */ + if (!v[conf] || !v[conf][sid]) + return; - parse_info(device, response[1], rv); - } + if (!d[conf]) + d[conf] = { }; - return retcb(cb, rv); - }); + 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); + }, - return _luci2.rpc.call('iwinfo', 'info', { device: dev }).then(function(response) { - if (response[0] != 0 || !response[1]) - return retcb(cb, { }); + set_first: function(conf, type, opt, val) + { + var sid = undefined; - return retcb(cb, parse_info(dev, response[1], { })); + 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); + }, + + _reload: function() + { + var pkgs = [ ]; + + for (var pkg in this.state.values) + pkgs.push(pkg); + + this.init(); + + return this.load(pkgs); }, - getAssocList: function(dev, cb) + _reorder: function() { - if (!dev) + 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) { - return _luci2.wireless.getDevices().then(function(devices) { - var requests = { }; + 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._order(c, sids); + } + } + + this.state.reorder = { }; + return L.rpc.flush(); + }, + + 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; + }, - for (var i = 0; i < devices.length; i++) + 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]) { - if (devices[i].indexOf('.sta') >= 0) - continue; + 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]; + } - requests[devices[i]] = [ 'iwinfo', 'assoclist', { device: devices[i] } ]; + snew.push(n[conf][sid]); + + self._add(r.config, r.type, r.name, r.values); } - return _luci2.rpc.call(requests); - }).then(function(responses) { - var rv = [ ]; + pkgs[conf] = true; + } + + if (c) + for (var conf in c) + { + for (var sid in c[conf]) + self._set(conf, sid, c[conf][sid]); + + pkgs[conf] = true; + } - for (var device in responses) + if (d) + for (var conf in d) + { + for (var sid in d[conf]) { - var response = responses[device]; + var o = d[conf][sid]; + self._delete(conf, sid, (o === true) ? undefined : o); + } - if (response[0] != 0 || !response[1] || !response[1].results) - continue; + 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._reorder(); + }).then(function() { + pkgs = L.toArray(pkgs); + + self.unload(pkgs); + + return self.load(pkgs); + }); + }, + + _apply: L.rpc.declare({ + object: 'uci', + method: 'apply', + params: [ 'timeout', 'rollback' ] + }), + + _confirm: L.rpc.declare({ + object: 'uci', + method: 'confirm' + }), + + apply: function(timeout) + { + var self = this; + var date = new Date(); + var deferred = $.Deferred(); + + if (typeof(timeout) != 'number' || timeout < 1) + timeout = 10; + + self._apply(timeout, true).then(function(rv) { + if (rv != 0) + { + deferred.rejectWith(self, [ rv ]); + return; + } - for (var i = 0; i < response[1].results.length; i++) + var try_deadline = date.getTime() + 1000 * timeout; + var try_confirm = function() + { + return self._confirm().then(function(rv) { + if (rv != 0) { - var station = response[1].results[i]; + if (date.getTime() < try_deadline) + window.setTimeout(try_confirm, 250); + else + deferred.rejectWith(self, [ rv ]); - station.device = device; - rv.push(station); + return; } - } - rv.sort(function(a, b) { - return (a.device == b.device) - ? (a.bssid < b.bssid) - : (a.device > b.device) - ; + 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']; - return retcb(cb, rv); + 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; } + }), - return _luci2.rpc.call('iwinfo', 'assoclist', { device: dev }).then(function(response) { - var rv = [ ]; + 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' + ]; - if (response[0] != 0 || !response[1] || !response[1].results) - return retcb(cb, rv); + var net_attrs = [ + 'ssid', 'bssid', 'mode', 'quality', 'quality_max', + 'signal', 'noise', 'bitrate', 'encryption' + ]; - for (var i = 0; i < response[1].results.length; i++) + for (var i = 0; i < networks.length; i++) { - var station = response[1].results[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]]; - station.device = dev; - rv.push(station); + for (var j = 0; j < net_attrs.length; j++) + net[net_attrs[j]] = networks[i][net_attrs[j]]; + + phy.networks.push(net); } - rv.sort(function(a, b) { - return (a.bssid < b.bssid); - }); + 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 retcb(cb, rv); + 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; }); }, @@ -1040,21 +1486,21 @@ function LuCI2() } if (!enc || !enc.enabled) - return _luci2.tr('None'); + return L.tr('None'); if (enc.wep) { if (enc.wep.length == 2) - return _luci2.tr('WEP Open/Shared') + ' (%s)'.format(format_list(enc.ciphers, ', ')); + return L.tr('WEP Open/Shared') + ' (%s)'.format(format_list(enc.ciphers, ', ')); else if (enc.wep[0] == 'shared') - return _luci2.tr('WEP Shared Auth') + ' (%s)'.format(format_list(enc.ciphers, ', ')); + return L.tr('WEP Shared Auth') + ' (%s)'.format(format_list(enc.ciphers, ', ')); else - return _luci2.tr('WEP Open System') + ' (%s)'.format(format_list(enc.ciphers, ', ')); + return L.tr('WEP Open System') + ' (%s)'.format(format_list(enc.ciphers, ', ')); } else if (enc.wpa) { if (enc.wpa.length == 2) - return _luci2.tr('mixed WPA/WPA2') + ' %s (%s)'.format( + return L.tr('mixed WPA/WPA2') + ' %s (%s)'.format( format_list(enc.authentication, '/'), format_list(enc.ciphers, ', ') ); @@ -1070,479 +1516,1788 @@ function LuCI2() ); } - return _luci2.tr('Unknown'); + return L.tr('Unknown'); } }; - this.system = { - getInfo: function(cb) + this.firewall = { + getZoneColor: function(zone) { - return _luci2.rpc.call({ - info: [ 'system', 'info', { } ], - board: [ 'system', 'board', { } ], - disk: [ 'luci2.system', 'diskfree', { } ] - }).then(function(responses) { - var rv = { }; + if ($.isPlainObject(zone)) + zone = zone.name; - if (responses.info[0] == 0) - $.extend(rv, responses.info[1]); + if (zone == 'lan') + return '#90f090'; + else if (zone == 'wan') + return '#f09090'; - if (responses.board[0] == 0) - $.extend(rv, responses.board[1]); + for (var i = 0, hash = 0; + i < zone.length; + hash = zone.charCodeAt(i++) + ((hash << 5) - hash)); - if (responses.disk[0] == 0) - $.extend(rv, responses.disk[1]); + for (var i = 0, color = '#'; + i < 3; + color += ('00' + ((hash >> i++ * 8) & 0xFF).tostring(16)).slice(-2)); - return retcb(cb, rv); - }); + 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 = { + _device_blacklist: [ + /^gre[0-9]+$/, + /^gretap[0-9]+$/, + /^ifb[0-9]+$/, + /^ip6tnl[0-9]+$/, + /^sit[0-9]+$/, + /^wlan[0-9]+\.sta[0-9]+$/ + ], + + _cache_functions: [ + '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': [ ] } + }) + ], + + _fetch_protocol: 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._protos[proto] = new protoClass(); + } + catch(e) { + alert('Unable to instantiate proto "%s": %s'.format(url, e)); + }; + + def.resolve(); + }).fail(function() { + def.resolve(); + }); + + return def; + }, + + _fetch_protocols: function() + { + var self = L.NetworkModel; + var deferreds = [ + self._fetch_protocol('none') + ]; + + for (var proto in self._cache.protolist) + deferreds.push(self._fetch_protocol(proto)); + + return $.when.apply($, deferreds); + }, + + _fetch_swstate: L.rpc.declare({ + object: 'luci2.network', + method: 'switch_info', + params: [ 'switch' ], + expect: { 'info': { } } + }), + + _fetch_swstate_cb: function(responses) { + var self = L.NetworkModel; + var swlist = self._cache.swlist; + var swstate = self._cache.swstate = { }; + + for (var i = 0; i < responses.length; i++) + swstate[swlist[i]] = responses[i]; + }, + + _fetch_cache_cb: 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._cache_functions.length; i += 3) + if (!level || self._cache_functions[i + 1] == level) + self._cache[self._cache_functions[i]] = responses.shift(); + + if (!level) + { + L.rpc.batch(); + + for (var i = 0; i < self._cache.swlist.length; i++) + self._fetch_swstate(self._cache.swlist[i]); + + return L.rpc.flush().then(self._fetch_swstate_cb); + } + + return L.deferrable(); + } + ); + }, + + _fetch_cache: function(level) + { + var self = L.NetworkModel; + + return L.uci.load(['network', 'wireless']).then(function() { + L.rpc.batch(); + + for (var i = 0; i < self._cache_functions.length; i += 3) + if (!level || self._cache_functions[i + 1] == level) + self._cache_functions[i + 2](); + + return L.rpc.flush().then(self._fetch_cache_cb(level || 0)); + }); + }, + + _get: function(pkg, sid, key) + { + return L.uci.get(pkg, sid, key); + }, + + _set: function(pkg, sid, key, val) + { + return L.uci.set(pkg, sid, key, val); + }, + + _is_blacklisted: function(dev) + { + for (var i = 0; i < this._device_blacklist.length; i++) + if (dev.match(this._device_blacklist[i])) + return true; + + return false; + }, + + _sort_devices: 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; + }, + + _get_dev: function(ifname) + { + var alias = (ifname.charAt(0) == '@'); + return this._devs[ifname] || ( + this._devs[ifname] = { + ifname: ifname, + kind: alias ? 'alias' : 'ethernet', + type: alias ? 0 : 1, + up: false, + changed: { } + } + ); + }, + + _get_iface: function(name) + { + return this._ifaces[name] || ( + this._ifaces[name] = { + name: name, + proto: this._protos.none, + changed: { } + } + ); + }, + + _parse_devices: function() + { + var self = L.NetworkModel; + var wificount = { }; + + for (var ifname in self._cache.devstate) + { + if (self._is_blacklisted(ifname)) + continue; + + var dev = self._cache.devstate[ifname]; + var entry = self._get_dev(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._cache.devlist.length; i++) + { + var dev = self._cache.devlist[i]; + + if (self._is_blacklisted(dev.device)) + continue; + + var entry = self._get_dev(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._get_dev(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._get_dev(ifnames[j]); + + if (s['.name'] != 'loopback') + { + var entry = self._get_dev('@%s'.format(s['.name'])); + + entry.type = 0; + entry.kind = 'alias'; + entry.sid = sid; + } + } + else if (s['.type'] == 'switch_vlan' && s.device) + { + var sw = self._cache.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._get_dev(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._cache.wifistate[s.device]) + { + var ifcs = self._cache.wifistate[s.device].interfaces; + for (var ifc in ifcs) + { + if (ifcs[ifc].section == sid) + { + ifname = ifcs[ifc].ifname; + break; + } + } + } + + var entry = self._get_dev(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._devs) + { + var dev = self._devs[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._get_dev('br-%s'.format(s['.name'])); + entry.type = 1; + entry.kind = 'bridge'; + entry.sid = sid; + entry.ports = ifnames.sort(); + } + } + }, + + _parse_interfaces: 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._get_iface(s['.name']); + var proto = self._protos[s.proto] || self._protos.none; + + var l3dev = undefined; + var l2dev = undefined; + + var ifnames = L.toArray(s.ifname); + + for (var ifname in self._devs) + { + var dev = self._devs[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._cache.ifstate.length; i++) + { + var iface = self._cache.ifstate[i]; + var entry = self._get_iface(iface['interface']); + var proto = self._protos[iface.proto] || self._protos.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._cache) + return L.deferrable(); + + self._cache = { }; + self._devs = { }; + self._ifaces = { }; + self._protos = { }; + + return self._fetch_cache() + .then(self._fetch_protocols) + .then(self._parse_devices) + .then(self._parse_interfaces); + }, + + update: function() + { + delete this._cache; + return this.init(); + }, + + refreshInterfaceStatus: function() + { + return this._fetch_cache(1).then(this._parse_interfaces); + }, + + refreshDeviceStatus: function() + { + return this._fetch_cache(2).then(this._parse_devices); + }, + + refreshStatus: function() + { + return this._fetch_cache(1) + .then(this._fetch_cache(2)) + .then(this._parse_devices) + .then(this._parse_interfaces); + }, + + getDevices: function() + { + var devs = [ ]; + + for (var ifname in this._devs) + if (ifname != 'lo') + devs.push(new L.NetworkModel.Device(this._devs[ifname])); + + return devs.sort(this._sort_devices); + }, + + getDeviceByInterface: function(iface) + { + if (iface instanceof L.NetworkModel.Interface) + iface = iface.name(); + + if (this._ifaces[iface]) + return this.getDevice(this._ifaces[iface].l3dev) || + this.getDevice(this._ifaces[iface].l2dev); + + return undefined; + }, + + getDevice: function(ifname) + { + if (this._devs[ifname]) + return new L.NetworkModel.Device(this._devs[ifname]); + + return undefined; + }, + + createDevice: function(name) + { + return new L.NetworkModel.Device(this._get_dev(name)); + }, + + getInterfaces: function() + { + var ifaces = [ ]; + + for (var name in this._ifaces) + 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._ifaces) + { + var iface = this._ifaces[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._ifaces[iface]) + return new L.NetworkModel.Interface(this._ifaces[iface]); + + return undefined; + }, + + getProtocols: function() + { + var rv = [ ]; + + for (var proto in this._protos) + { + var pr = this._protos[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; + }); + }, + + _find_wan: function(ipaddr) + { + for (var i = 0; i < this._cache.ifstate.length; i++) + { + var ifstate = this._cache.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._find_wan('0.0.0.0'); + }, + + findWAN6: function() + { + return this._find_wan('::'); + }, + + resolveAlias: function(ifname) + { + if (ifname instanceof L.NetworkModel.Device) + ifname = ifname.name(); + + var dev = this._devs[ifname]; + var seen = { }; + + while (dev && dev.kind == 'alias') + { + // loop + if (seen[dev.ifname]) + return undefined; + + var ifc = this._ifaces[dev.sid]; + + seen[dev.ifname] = true; + dev = ifc ? this._devs[ifc.l3dev] : undefined; + } + + return dev ? this.getDevice(dev.ifname) : undefined; + } + }; + + this.NetworkModel.Device = Class.extend({ + _wifi_modes: { + ap: L.tr('Master'), + sta: L.tr('Client'), + adhoc: L.tr('Ad-Hoc'), + monitor: L.tr('Monitor'), + wds: L.tr('Static WDS') + }, + + _status: function(key) + { + var s = L.NetworkModel._cache.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.NetworkModel._get(pkg, sid, key); + }, + + set: function(key, val) + { + var sid = this.options.sid; + var pkg = (this.options.kind == 'wifi') ? 'wireless' : 'network'; + return L.NetworkModel._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._wifi_modes[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._cache.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._devs[net.options.l2dev]; + if (dev && dev.kind == 'bridge' && dev.ports) + return ($.inArray(this.options.ifname, dev.ports) > -1); + } + + return false; }, - getProcessList: function(cb) + getMTU: function() { - return rcall('luci2.system', 'process_list', undefined, 'processes', [ ], cb, function(rv) { - rv.sort(function(a, b) { return a.pid - b.pid }); - return rv; - }); + var dev = L.NetworkModel._cache.devstate[this.options.ifname]; + if (dev && !isNaN(dev.mtu)) + return dev.mtu; + + return undefined; }, - getSystemLog: function(cb) + getMACAddress: function() { - return rcall('luci2.system', 'syslog', undefined, 'log', '', cb); + if (this.options.type != 1) + return undefined; + + var dev = L.NetworkModel._cache.devstate[this.options.ifname]; + if (dev && dev.macaddr) + return dev.macaddr.toUpperCase(); + + return undefined; }, - getKernelLog: function(cb) + getInterfaces: function() { - return rcall('luci2.system', 'dmesg', undefined, 'log', '', cb); + return L.NetworkModel.getInterfacesByDevice(this.options.name); }, - getZoneInfo: function(cb) + getStatistics: function() { - return $.getJSON(_luci2.globals.resource + '/zoneinfo.json', cb); + var s = this._status('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) + }; }, - canSendSignal: function(cb) + getTrafficHistory: function() { - return _luci2.rpc.access('ubus', 'luci2.system', 'process_signal', cb); + var def = new Array(120); + + for (var i = 0; i < 120; i++) + def[i] = 0; + + var h = L.NetworkModel._cache.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) + }; }, - sendSignal: function(pid, sig, cb) + removeFromInterface: function(iface) { - return _luci2.rpc.call('luci2.system', 'process_signal', { pid: pid, signal: sig }).then(function(response) { - return retcb(cb, response[0] == 0); - }); + 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())); }, - initList: function(cb) + attachToInterface: function(iface) { - return rcall('luci2.system', 'init_list', undefined, 'initscripts', [ ], cb, function(rv) { - rv.sort(function(a, b) { return (a.start || 0) - (b.start || 0) }); - return rv; - }); - }, + if (!(iface instanceof L.NetworkModel.Interface)) + iface = L.NetworkModel.getInterface(iface); - initEnabled: function(init, cb) + 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({ + _status: function(key) { - return this.initList(function(list) { - for (var i = 0; i < list.length; i++) - if (list[i].name == init) - return retcb(cb, !!list[i].enabled); + var s = L.NetworkModel._cache.ifstate; - return retcb(cb, false); - }); + for (var i = 0; i < s.length; i++) + if (s[i]['interface'] == this.options.name) + return key ? s[i][key] : s[i]; + + return undefined; }, - initRun: function(init, action, cb) + get: function(key) { - return _luci2.rpc.call('luci2.system', 'init_action', { name: init, action: action }).then(function(response) { - return retcb(cb, response[0] == 0); - }); + return L.NetworkModel._get('network', this.options.name, key); }, - canInitRun: function(cb) + set: function(key, val) { - return _luci2.rpc.access('ubus', 'luci2.system', 'init_action', cb); + return L.NetworkModel._set('network', this.options.name, key, val); }, - initStart: function(init, cb) { return _luci2.system.initRun(init, 'start', cb) }, - initStop: function(init, cb) { return _luci2.system.initRun(init, 'stop', cb) }, - initRestart: function(init, cb) { return _luci2.system.initRun(init, 'restart', cb) }, - initReload: function(init, cb) { return _luci2.system.initRun(init, 'reload', cb) }, - initEnable: function(init, cb) { return _luci2.system.initRun(init, 'enable', cb) }, - initDisable: function(init, cb) { return _luci2.system.initRun(init, 'disable', cb) }, - - - getRcLocal: function(cb) + name: function() { - return rcall('luci2.system', 'rclocal_get', undefined, 'data', '', cb); + return this.options.name; }, - setRcLocal: function(data, cb) + protocol: function() { - return rcall('luci2.system', 'rclocal_set', { data: data }, undefined, undefined, cb); + return (this.get('proto') || 'none'); }, - canSetRcLocal: function(cb) + isUp: function() { - return _luci2.rpc.access('ubus', 'luci2.system', 'rclocal_set', cb); + return (this._status('up') === true); }, - - getCrontab: function(cb) + isVirtual: function() { - return rcall('luci2.system', 'crontab_get', undefined, 'data', '', cb); + return (typeof(this.options.sid) != 'string'); }, - setCrontab: function(data, cb) + getProtocol: function() { - return rcall('luci2.system', 'crontab_set', { data: data }, undefined, undefined, cb); + var prname = this.get('proto') || 'none'; + return L.NetworkModel._protos[prname] || L.NetworkModel._protos.none; }, - canSetCrontab: function(cb) + getUptime: function() { - return _luci2.rpc.access('ubus', 'luci2.system', 'crontab_set', cb); + var uptime = this._status('uptime'); + return isNaN(uptime) ? 0 : uptime; }, - - getSSHKeys: function(cb) + getDevice: function(resolveAlias) { - return rcall('luci2.system', 'sshkeys_get', undefined, 'keys', [ ], cb); + if (this.options.l3dev) + return L.NetworkModel.getDevice(this.options.l3dev); + + return undefined; }, - setSSHKeys: function(keys, cb) + getPhysdev: function() { - return rcall('luci2.system', 'sshkeys_set', { keys: keys }, undefined, undefined, cb); + if (this.options.l2dev) + return L.NetworkModel.getDevice(this.options.l2dev); + + return undefined; }, - canSetSSHKeys: function(cb) + getSubdevices: function() { - return _luci2.rpc.access('ubus', 'luci2.system', 'sshkeys_set', cb); - }, + var rv = [ ]; + var dev = this.options.l2dev ? + L.NetworkModel._devs[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])); - setPassword: function(user, pass, cb) - { - return rcall('luci2.system', 'password_set', { user: user, password: pass }, undefined, undefined, cb); + return rv; }, - canSetPassword: function(cb) + getIPv4Addrs: function(mask) { - return _luci2.rpc.access('ubus', 'luci2.system', 'password_set', cb); - }, + var rv = [ ]; + var addrs = this._status('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)); - listLEDs: function(cb) - { - return rcall('luci2.system', 'led_list', undefined, 'leds', [ ], cb); + return rv; }, - listUSBDevices: function(cb) + getIPv6Addrs: function(mask) { - return rcall('luci2.system', 'usb_list', undefined, 'devices', [ ], cb); - }, + var rv = [ ]; + var addrs; + addrs = this._status('ipv6-address'); - testUpgrade: function(cb) - { - return rcall('luci2.system', 'upgrade_test', undefined, undefined, { }, cb); + 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._status('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; }, - startUpgrade: function(keep, cb) + getDNSAddrs: function() { - return rcall('luci2.system', 'upgrade_start', { keep: !!keep }, undefined, undefined, cb); + var rv = [ ]; + var addrs = this._status('dns-server'); + + if (addrs) + for (var i = 0; i < addrs.length; i++) + rv.push(addrs[i]); + + return rv; }, - cleanUpgrade: function(cb) + getIPv4DNS: function() { - return rcall('luci2.system', 'upgrade_clean', undefined, undefined, undefined, cb); + var rv = [ ]; + var dns = this._status('dns-server'); + + if (dns) + for (var i = 0; i < dns.length; i++) + if (dns[i].indexOf(':') == -1) + rv.push(dns[i]); + + return rv; }, - canUpgrade: function(cb) + getIPv6DNS: function() { - return _luci2.rpc.access('ubus', 'luci2.system', 'upgrade_start', cb); - }, + var rv = [ ]; + var dns = this._status('dns-server'); + if (dns) + for (var i = 0; i < dns.length; i++) + if (dns[i].indexOf(':') > -1) + rv.push(dns[i]); - restoreBackup: function(cb) - { - return rcall('luci2.system', 'backup_restore', undefined, undefined, undefined, cb); + return rv; }, - cleanBackup: function(cb) + getIPv4Gateway: function() { - return rcall('luci2.system', 'backup_clean', undefined, undefined, undefined, cb); + var rt = this._status('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; }, - canRestoreBackup: function(cb) + getIPv6Gateway: function() { - return _luci2.rpc.access('ubus', 'luci2.system', 'backup_restore', cb); - }, + var rt = this._status('route'); + if (rt) + for (var i = 0; i < rt.length; i++) + if (rt[i].target == '::' && rt[i].mask == 0) + return rt[i].nexthop; - getBackupConfig: function(cb) - { - return rcall('luci2.system', 'backup_config_get', undefined, 'config', '', cb); + return undefined; }, - setBackupConfig: function(data, cb) + getStatistics: function() { - return rcall('luci2.system', 'backup_config_set', { data: data }, undefined, undefined, cb); + var dev = this.getDevice() || new L.NetworkModel.Device({}); + return dev.getStatistics(); }, - canSetBackupConfig: function(cb) + getTrafficHistory: function() { - return _luci2.rpc.access('ubus', 'luci2.system', 'backup_config_set', cb); + var dev = this.getDevice() || new L.NetworkModel.Device({}); + return dev.getTrafficHistory(); }, - - listBackup: function(cb) + renderBadge: function() { - return rcall('luci2.system', 'backup_list', undefined, 'files', [ ], cb); - }, + var badge = $('') + .addClass('badge') + .text('%s: '.format(this.name())); + var dev = this.getDevice(); + var subdevs = this.getSubdevices(); - performReboot: function(cb) - { - return rcall('luci2.system', 'reboot', undefined, undefined, undefined, cb); + if (subdevs.length) + for (var j = 0; j < subdevs.length; j++) + badge.append($('') + .attr('src', subdevs[j].icon()) + .attr('title', '%s (%s)'.format(subdevs[j].description(), subdevs[j].name() || '?'))); + else if (dev) + badge.append($('') + .attr('src', dev.icon()) + .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?'))); + else + badge.append($('').text(L.tr('(No devices attached)'))); + + return badge; }, - canPerformReboot: function(cb) + setDevices: function(devs) { - return _luci2.rpc.access('ubus', 'luci2.system', 'reboot', cb); - } - }; + 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]; - this.opkg = { - updateLists: function(cb) + if (!(dev instanceof L.NetworkModel.Device)) + dev = L.NetworkModel.getDevice(dev); + + if (dev) + dev.attachToInterface(this); + } + } + }, + + changeProtocol: function(proto) { - return rcall('luci2.opkg', 'update', undefined, undefined, { }, cb); + var pr = L.NetworkModel._protos[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; + } + } }, - _fetchPackages: function(action, offset, limit, pattern, cb) + createForm: function(mapwidget) { - var packages = [ ]; - var reqlimit = Math.min(limit, 100); + 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'; + }; - if (reqlimit <= 0) - reqlimit = 100; + 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(); + }); - return _luci2.rpc.call('luci2.opkg', action, { offset: offset, limit: reqlimit, pattern: pattern }).then(function(response) { - if (response[0] != 0 || !response[1] || !response[1].total) - return retcb(cb, { length: 0, total: 0 }); + var protos = L.NetworkModel.getProtocols(); - packages.push.apply(packages, response[1].packages); - packages.total = response[1].total; + for (var i = 0; i < protos.length; i++) + pr.value(protos[i].name, protos[i].description); - if (limit <= 0) - limit = response[1].total; + proto.populateForm(section, self); - if (packages.length >= limit) - return retcb(cb, packages); + 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: '' + }); - var requests = [ ]; - for (var i = offset + packages.length; i < limit; i += 100) - requests.push(['luci2.opkg', action, { offset: i, limit: (Math.min(i + 100, limit) % 100) || 100, pattern: pattern }]); + 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' + }) - return _luci2.rpc.call(requests); - }).then(function(responses) { - for (var key in responses) + mac.ucivalue = function(sid) { - var response = responses[key]; + if (device) + return device.get('macaddr'); - if (response[0] != 0 || !response[1] || !response[1].packages) - continue; + return this.callSuper('ucivalue', sid); + }; - packages.push.apply(packages, response[1].packages); - packages.total = response[1].total; - } + 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; + }; + } - return retcb(cb, packages); + section.taboption('physical', L.cbi.InputValue, 'mtu', { + caption: L.tr('Override MTU'), + optional: true, + placeholder: device ? device.getMTU() : undefined, + datatype: 'range(1, 9000)' }); - }, - listPackages: function(offset, limit, pattern, cb) - { - return _luci2.opkg._fetchPackages('list', offset, limit, pattern, cb); - }, + 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; + } + }); - installedPackages: function(offset, limit, pattern, cb) + this.NetworkModel.Protocol = this.NetworkModel.Interface.extend({ + description: '__unknown__', + tunnel: false, + virtual: false, + + populateForm: function(section, iface) { - return _luci2.opkg._fetchPackages('list_installed', offset, limit, pattern, cb); - }, - findPackages: function(offset, limit, pattern, cb) + } + }); + + 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) { - return _luci2.opkg._fetchPackages('find', offset, limit, pattern, 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; + }); }, - installPackage: function(name, cb) + + 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 rcall('luci2.opkg', 'install', { 'package': name }, undefined, { }, 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; + }); }, - removePackage: function(name, cb) + 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 rcall('luci2.opkg', 'remove', { 'package': name }, undefined, { }, cb); + return L.session.access('ubus', 'session', 'access'); }, - getConfig: function(cb) + startHeartbeat: function() { - return rcall('luci2.opkg', 'config_get', undefined, 'config', '', cb); + this._hearbeatInterval = window.setInterval(function() { + L.session.isAlive().then(function(alive) { + if (!alive) + { + L.session.stopHeartbeat(); + L.ui.login(true); + } + + }); + }, L.globals.timeout * 2); }, - setConfig: function(data, cb) + stopHeartbeat: function() { - return rcall('luci2.opkg', 'config_set', { data: data }, undefined, undefined, cb); + if (typeof(this._hearbeatInterval) != 'undefined') + { + window.clearInterval(this._hearbeatInterval); + delete this._hearbeatInterval; + } }, - canInstallPackage: function(cb) + + _acls: { }, + + _fetch_acls: L.rpc.declare({ + object: 'session', + method: 'access', + expect: { '': { } } + }), + + _fetch_acls_cb: function(acls) { - return _luci2.rpc.access('ubus', 'luci2.opkg', 'install', cb); + L.session._acls = acls; }, - canRemovePackage: function(cb) + updateACLs: function() { - return _luci2.rpc.access('ubus', 'luci2.opkg', 'remove', cb); + return L.session._fetch_acls() + .then(L.session._fetch_acls_cb); }, - canSetConfig: function(cb) + hasACL: function(scope, object, func) { - return _luci2.rpc.access('ubus', 'luci2.opkg', 'config_set', cb); + var acls = L.session._acls; + + 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 div = _luci2._modal || ( - _luci2._modal = $('
') - .addClass('cbi-modal-loader') - .append($('
').text(_luci2.tr('Loading data...'))) + + var state = L.ui._loading || (L.ui._loading = { + modal: $('
') + .css('z-index', 2000) + .addClass('modal fade') + .append($('
') + .addClass('modal-dialog') + .append($('
') + .addClass('modal-content luci2-modal-loader') + .append($('
') + .addClass('modal-body') + .text(L.tr('Loading data…'))))) .appendTo(body) - ); + .modal({ + backdrop: 'static', + keyboard: false + }) + }); - if (enable) - { - body.css('overflow', 'hidden'); - body.css('padding', 0); - body.css('width', win.width()); - body.css('height', win.height()); - div.css('width', win.width()); - div.css('height', win.height()); - div.show(); - } - else - { - div.hide(); - body.css('overflow', ''); - body.css('padding', ''); - body.css('width', ''); - body.css('height', ''); - } + state.modal.modal(enable ? 'show' : 'hide'); }, dialog: function(title, content, options) { var win = $(window); var body = $('body'); - var div = _luci2._dialog || ( - _luci2._dialog = $('
') - .addClass('cbi-modal-dialog') + + var state = L.ui._dialog || (L.ui._dialog = { + dialog: $('
') + .addClass('modal fade') .append($('
') + .addClass('modal-dialog') .append($('
') - .addClass('cbi-modal-dialog-header')) - .append($('
') - .addClass('cbi-modal-dialog-body')) - .append($('
') - .addClass('cbi-modal-dialog-footer') - .append($('