X-Git-Url: http://git.archive.openwrt.org/?p=project%2Fluci2%2Fui.git;a=blobdiff_plain;f=luci2%2Fhtdocs%2Fluci2%2Fluci2.js;h=13519e68328b849bef7f68259799d9564d04814e;hp=cdc7c4fc539ea33f54f4213e530fe46c1f21f8a6;hb=db7efa7afe905478b473851b7e8407a538df8969;hpb=e2b9cb5d91976ee7aa6d9dbe0372c74ad6bbe17e diff --git a/luci2/htdocs/luci2/luci2.js b/luci2/htdocs/luci2/luci2.js index cdc7c4f..13519e6 100644 --- a/luci2/htdocs/luci2/luci2.js +++ b/luci2/htdocs/luci2/luci2.js @@ -214,7 +214,7 @@ function LuCI2() _class.prototype = prototype; _class.prototype.constructor = _class; - _class.extend = arguments.callee; + _class.extend = Class.extend; return _class; }; @@ -364,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) @@ -386,8 +388,105 @@ function LuCI2() return data; }; + 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.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; + } + } + + return { }; + }; + + this.filterArray = function(array, item) + { + if (!$.isArray(array)) + return [ ]; + + for (var i = 0; i < array.length; i++) + if (array[i] === item) + array.splice(i--, 1); + + return array; + }; + + this.toClassName = function(str, suffix) + { + var n = ''; + var l = str.split(/[\/.]/); + + 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 (typeof(suffix) == 'string') + n += suffix; + + return n; + }; + this.globals = { - timeout: 3000, + timeout: 15000, resource: '/luci2', sid: '00000000000000000000000000000000' }; @@ -406,43 +505,48 @@ function LuCI2() data: JSON.stringify(req), dataType: 'json', type: 'POST', - timeout: _luci2.globals.timeout - }).then(cb); + timeout: _luci2.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) - throw 'Invalid JSON response'; + if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id || !$.isArray(list)) + list = [ ]; - return msg.result; + return $.Deferred().resolveWith(this, [ list ]); }, _call_cb: function(msg) { var data = [ ]; var type = Object.prototype.toString; + var reqs = this._rpc_req; - if (!$.isArray(msg)) + if (!$.isArray(reqs)) + { msg = [ msg ]; + reqs = [ reqs ]; + } for (var i = 0; i < msg.length; i++) { - /* verify message frame */ - if (typeof(msg[i]) != 'object' || msg[i].jsonrpc != '2.0' || !msg[i].id) - throw 'Invalid JSON response'; - /* fetch related request info */ - var req = _luci2.rpc._requests[msg[i].id]; + var req = _luci2.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; - if ($.isArray(msg[i].result) && msg[i].result[0] == 0) - ret = (msg[i].result.length > 1) ? msg[i].result[1] : msg[i].result[0]; + /* 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) { @@ -451,7 +555,7 @@ function LuCI2() if (typeof(ret) != 'undefined' && key != '') ret = ret[key]; - if (type.call(ret) != type.call(req.expect[key])) + if (typeof(ret) == 'undefined' || type.call(ret) != type.call(req.expect[key])) ret = req.expect[key]; break; @@ -473,10 +577,10 @@ function LuCI2() data = ret; /* delete request object */ - delete _luci2.rpc._requests[msg[i].id]; + delete _luci2.rpc._requests[reqs[i].id]; } - return data; + return $.Deferred().resolveWith(this, [ data ]); }, list: function() @@ -565,355 +669,482 @@ function LuCI2() } }; - this.uci = { + this.UCIContext = Class.extend({ - writable: function() + init: function() { - return _luci2.session.access('ubus', 'uci', 'commit'); + this.state = { + newid: 0, + values: { }, + creates: { }, + changes: { }, + deletes: { }, + reorder: { } + }; }, - add: _luci2.rpc.declare({ + _load: _luci2.rpc.declare({ object: 'uci', - method: 'add', - params: [ 'config', 'type', 'name', 'values' ], - expect: { section: '' } + method: 'get', + params: [ 'config' ], + expect: { values: { } } }), - apply: function() - { - - }, - - changes: _luci2.rpc.declare({ + _order: _luci2.rpc.declare({ object: 'uci', - method: 'changes', - params: [ 'config' ], - expect: { changes: [ ] } + method: 'order', + params: [ 'config', 'sections' ] }), - commit: _luci2.rpc.declare({ + _add: _luci2.rpc.declare({ object: 'uci', - method: 'commit', - params: [ 'config' ] + method: 'add', + params: [ 'config', 'type', 'name', 'values' ], + expect: { section: '' } }), - _delete_one: _luci2.rpc.declare({ + _set: _luci2.rpc.declare({ object: 'uci', - method: 'delete', - params: [ 'config', 'section', 'option' ] + method: 'set', + params: [ 'config', 'section', 'values' ] }), - _delete_multiple: _luci2.rpc.declare({ + _delete: _luci2.rpc.declare({ object: 'uci', method: 'delete', params: [ 'config', 'section', 'options' ] }), - 'delete': function(config, section, option) + load: function(packages) { - if ($.isArray(option)) - return this._delete_multiple(config, section, option); - else - return this._delete_one(config, section, option); - }, + var self = this; + var seen = { }; + var pkgs = [ ]; - delete_all: _luci2.rpc.declare({ - object: 'uci', - method: 'delete', - params: [ 'config', 'type', 'match' ] - }), + if (!$.isArray(packages)) + packages = [ packages ]; - _foreach: _luci2.rpc.declare({ - object: 'uci', - method: 'get', - params: [ 'config', 'type' ], - expect: { values: { } } - }), + _luci2.rpc.batch(); - foreach: function(config, type, cb) - { - return this._foreach(config, type).then(function(sections) { - for (var s in sections) - cb(sections[s]); + for (var i = 0; i < packages.length; i++) + if (!seen[packages[i]]) + { + pkgs.push(packages[i]); + seen[packages[i]] = true; + self._load(packages[i]); + } + + return _luci2.rpc.flush().then(function(responses) { + for (var i = 0; i < responses.length; i++) + self.state.values[pkgs[i]] = responses[i]; + + return pkgs; }); }, - get: _luci2.rpc.declare({ - object: 'uci', - method: 'get', - params: [ 'config', 'section', 'option' ], - expect: { '': { } }, - filter: function(data, params) { - if (typeof(params.option) == 'undefined') - return data.values ? data.values['.type'] : undefined; - else - return data.value; - } - }), + unload: function(packages) + { + if (!$.isArray(packages)) + packages = [ packages ]; - get_all: _luci2.rpc.declare({ - object: 'uci', - method: 'get', - params: [ 'config', 'section' ], - expect: { values: { } }, - filter: function(data, params) { - if (typeof(params.section) == 'string') - data['.section'] = params.section; - else if (typeof(params.config) == 'string') - data['.package'] = params.config; - return data; + 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]]; } - }), + }, - get_first: function(config, type, option) + add: function(conf, type, name) { - return this._foreach(config, type).then(function(sections) { - for (var s in sections) - { - var val = (typeof(option) == 'string') ? sections[s][option] : sections[s]['.name']; + var c = this.state.creates; + var s = '.new.%d'.format(this.state.newid++); - if (typeof(val) != 'undefined') - return val; - } + if (!c[conf]) + c[conf] = { }; - return undefined; - }); + c[conf][s] = { + '.type': type, + '.name': s, + '.create': name, + '.anonymous': !name, + '.index': 1000 + this.state.newid + }; + + return s; }, - section: _luci2.rpc.declare({ - object: 'uci', - method: 'add', - params: [ 'config', 'type', 'name', 'values' ], - expect: { section: '' } - }), + remove: function(conf, sid) + { + var n = this.state.creates; + var c = this.state.changes; + var d = this.state.deletes; - _set: _luci2.rpc.declare({ - object: 'uci', - method: 'set', - params: [ 'config', 'section', 'values' ] - }), + /* requested deletion of a just created section */ + if (sid.indexOf('.new.') == 0) + { + if (n[conf]) + delete n[conf][sid]; + } + else + { + if (c[conf]) + delete c[conf][sid]; + + if (!d[conf]) + d[conf] = { }; + + d[conf][sid] = true; + } + }, - set: function(config, section, option, value) + sections: function(conf, type, cb) { - if (typeof(value) == 'undefined' && typeof(option) == 'string') - return this.section(config, section, option); /* option -> type */ - else if ($.isPlainObject(option)) - return this._set(config, section, option); /* option -> values */ + 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]; - var values = { }; - values[option] = value; + if (!v) + return sa; - return this._set(config, section, values); - }, + 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)); - order: _luci2.rpc.declare({ - object: 'uci', - method: 'order', - params: [ 'config', 'sections' ] - }) - }; + if (n) + for (var s in n) + if (!type || n[s]['.type'] == type) + sa.push(n[s]); - this.network = { - listNetworkNames: function() { - return _luci2.rpc.list('network.interface.*').then(function(list) { - var names = [ ]; - for (var name in list) - if (name != 'network.interface.loopback') - names.push(name.substring(18)); - names.sort(); - return names; + sa.sort(function(a, b) { + return a['.index'] - b['.index']; }); - }, - listDeviceNames: _luci2.rpc.declare({ - object: 'network.device', - method: 'status', - expect: { '': { } }, - filter: function(data) { - var names = [ ]; - for (var name in data) - if (name != 'lo') - names.push(name); - names.sort(); - return names; - } - }), + 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']); - getNetworkStatus: function() + return sa; + }, + + get: function(conf, sid, opt) { - var nets = [ ]; - var devs = { }; + var v = this.state.values; + var n = this.state.creates; + var c = this.state.changes; + var d = this.state.deletes; - return this.listNetworkNames().then(function(names) { - _luci2.rpc.batch(); + if (typeof(sid) == 'undefined') + return undefined; - for (var i = 0; i < names.length; i++) - _luci2.network.getInterfaceStatus(names[i]); + /* requested option in a just created section */ + if (sid.indexOf('.new.') == 0) + { + if (!n[conf]) + return undefined; - return _luci2.rpc.flush(); - }).then(function(networks) { - for (var i = 0; i < networks.length; i++) + 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]) { - var net = nets[i] = networks[i]; - var dev = net.l3_device || net.l2_device; - if (dev) - net.device = devs[dev] = { }; - } + if (d[conf][sid] === true) + return undefined; - _luci2.rpc.batch(); + for (var i = 0; i < d[conf][sid].length; i++) + if (d[conf][sid][i] == opt) + return undefined; + } - for (var dev in devs) - _luci2.network.listDeviceNamestatus(dev); + /* check whether option was changed */ + if (c[conf] && c[conf][sid] && typeof(c[conf][sid][opt]) != 'undefined') + return c[conf][sid][opt]; - return _luci2.rpc.flush(); - }).then(function(devices) { - _luci2.rpc.batch(); + /* return base value */ + if (v[conf] && v[conf][sid]) + return v[conf][sid][opt]; - for (var i = 0; i < devices.length; i++) - { - var brm = devices[i]['bridge-members']; - delete devices[i]['bridge-members']; + return undefined; + } - $.extend(devs[devices[i]['device']], devices[i]); + /* requested an entire section */ + if (v[conf]) + return v[conf][sid]; - if (!brm) - continue; + return undefined; + }, - devs[devices[i]['device']].subdevices = [ ]; + set: function(conf, sid, opt, val) + { + var n = this.state.creates; + var c = this.state.changes; + var d = this.state.deletes; - for (var j = 0; j < brm.length; j++) - { - if (!devs[brm[j]]) - { - devs[brm[j]] = { }; - _luci2.network.listDeviceNamestatus(brm[j]); - } + if (typeof(sid) == 'undefined' || + typeof(opt) == 'undefined' || + opt.charAt(0) == '.') + return; - devs[devices[i]['device']].subdevices[j] = devs[brm[j]]; - } + if (sid.indexOf('.new.') == 0) + { + 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; - return _luci2.rpc.flush(); - }).then(function(subdevices) { - for (var i = 0; i < subdevices.length; i++) - $.extend(devs[subdevices[i]['device']], subdevices[i]); + if (!c[conf]) + c[conf] = { }; - _luci2.rpc.batch(); + if (!c[conf][sid]) + c[conf][sid] = { }; - for (var dev in devs) - _luci2.wireless.getDeviceStatus(dev); + /* undelete option */ + if (d[conf] && d[conf][sid]) + d[conf][sid] = _luci2.filterArray(d[conf][sid], opt); - return _luci2.rpc.flush(); - }).then(function(wifidevices) { - for (var i = 0; i < wifidevices.length; i++) - if (wifidevices[i]) - devs[wifidevices[i]['device']].wireless = wifidevices[i]; + c[conf][sid][opt] = val; + } + else + { + if (!d[conf]) + d[conf] = { }; - nets.sort(function(a, b) { - if (a['interface'] < b['interface']) - return -1; - else if (a['interface'] > b['interface']) - return 1; - else - return 0; - }); + if (!d[conf][sid]) + d[conf][sid] = [ ]; - return nets; - }); + if (d[conf][sid] !== true) + d[conf][sid].push(opt); + } }, - findWanInterfaces: function(cb) + unset: function(conf, sid, opt) { - return this.listNetworkNames().then(function(names) { - _luci2.rpc.batch(); + return this.set(conf, sid, opt, undefined); + }, - for (var i = 0; i < names.length; i++) - _luci2.network.getInterfaceStatus(names[i]); + _reload: function() + { + var pkgs = [ ]; - return _luci2.rpc.flush(); - }).then(function(interfaces) { - var rv = [ undefined, undefined ]; + for (var pkg in this.state.values) + pkgs.push(pkg); - for (var i = 0; i < interfaces.length; i++) - { - for (var j = 0; j < interfaces[i].route.length; j++) - { - var rt = interfaces[i].route[j]; + this.init(); - if (typeof(rt.table) != 'undefined') - continue; + return this.load(pkgs); + }, - if (rt.target == '0.0.0.0' && rt.mask == 0) - rv[0] = interfaces[i]; - else if (rt.target == '::' && rt.mask == 0) - rv[1] = interfaces[i]; - } + _reorder: function() + { + var v = this.state.values; + var n = this.state.creates; + var r = this.state.reorder; + + if ($.isEmptyObject(r)) + return _luci2.deferrable(); + + _luci2.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 && 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); } + } - return rv; + this.state.reorder = { }; + return _luci2.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; + }, + + save: function() + { + _luci2.rpc.batch(); + + var self = this; + var snew = [ ]; + + if (self.state.creates) + for (var c in self.state.creates) + for (var s in self.state.creates[c]) + { + var r = { + config: c, + values: { } + }; + + for (var k in self.state.creates[c][s]) + { + if (k == '.type') + r.type = self.state.creates[c][s][k]; + else if (k == '.create') + r.name = self.state.creates[c][s][k]; + else if (k.charAt(0) != '.') + r.values[k] = self.state.creates[c][s][k]; + } + + snew.push(self.state.creates[c][s]); + + self._add(r.config, r.type, r.name, r.values); + } + + if (self.state.changes) + for (var c in self.state.changes) + for (var s in self.state.changes[c]) + self._set(c, s, self.state.changes[c][s]); + + if (self.state.deletes) + for (var c in self.state.deletes) + for (var s in self.state.deletes[c]) + { + var o = self.state.deletes[c][s]; + self._delete(c, s, (o === true) ? undefined : o); + } + + return _luci2.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(); }); }, - getDHCPLeases: _luci2.rpc.declare({ - object: 'luci2.network', - method: 'dhcp_leases', - expect: { leases: [ ] } + _apply: _luci2.rpc.declare({ + object: 'uci', + method: 'apply', + params: [ 'timeout', 'rollback' ] }), - getDHCPv6Leases: _luci2.rpc.declare({ - object: 'luci2.network', - method: 'dhcp6_leases', - expect: { leases: [ ] } + _confirm: _luci2.rpc.declare({ + object: 'uci', + method: 'confirm' }), - getRoutes: _luci2.rpc.declare({ - object: 'luci2.network', - method: 'routes', - expect: { routes: [ ] } - }), + apply: function(timeout) + { + var self = this; + var date = new Date(); + var deferred = $.Deferred(); - getIPv6Routes: _luci2.rpc.declare({ - object: 'luci2.network', - method: 'routes', - expect: { routes: [ ] } - }), + if (typeof(timeout) != 'number' || timeout < 1) + timeout = 10; - getARPTable: _luci2.rpc.declare({ - object: 'luci2.network', - method: 'arp_table', - expect: { entries: [ ] } - }), + self._apply(timeout, true).then(function(rv) { + if (rv != 0) + { + deferred.rejectWith(self, [ rv ]); + return; + } - getInterfaceStatus: _luci2.rpc.declare({ - object: 'network.interface', - method: 'status', - params: [ 'interface' ], - expect: { '': { } }, - filter: function(data, params) { - data['interface'] = params['interface']; - data['l2_device'] = data['device']; - delete data['device']; - return data; - } - }), + var try_deadline = date.getTime() + 1000 * timeout; + var try_confirm = function() + { + return self._confirm().then(function(rv) { + if (rv != 0) + { + if (date.getTime() < try_deadline) + window.setTimeout(try_confirm, 250); + else + deferred.rejectWith(self, [ rv ]); - listDeviceNamestatus: _luci2.rpc.declare({ - object: 'network.device', - method: 'status', - params: [ 'name' ], - expect: { '': { } }, - filter: function(data, params) { - data['device'] = params['name']; - return data; - } + return; + } + + deferred.resolveWith(self, [ rv ]); + }); + }; + + window.setTimeout(try_confirm, 1000); + }); + + return deferred; + }, + + changes: _luci2.rpc.declare({ + object: 'uci', + method: 'changes', + expect: { changes: { } } }), - getConntrackCount: _luci2.rpc.declare({ - object: 'luci2.network', - method: 'conntrack_count', - expect: { '': { count: 0, limit: 0 } } - }) - }; + readable: function(conf) + { + return _luci2.session.hasACL('uci', conf, 'read'); + }, + + writable: function(conf) + { + return _luci2.session.hasACL('uci', conf, 'write'); + } + }); + + this.uci = new this.UCIContext(); this.wireless = { listDeviceNames: _luci2.rpc.declare({ @@ -1072,162 +1303,1626 @@ function LuCI2() } }; - this.system = { - getSystemInfo: _luci2.rpc.declare({ - object: 'system', - method: 'info', - expect: { '': { } } - }), - - getBoardInfo: _luci2.rpc.declare({ - object: 'system', - method: 'board', - expect: { '': { } } - }), - - getDiskInfo: _luci2.rpc.declare({ - object: 'luci2.system', - method: 'diskfree', - expect: { '': { } } - }), - - getInfo: function(cb) + this.firewall = { + getZoneColor: function(zone) { - _luci2.rpc.batch(); + if ($.isPlainObject(zone)) + zone = zone.name; - this.getSystemInfo(); - this.getBoardInfo(); - this.getDiskInfo(); + if (zone == 'lan') + return '#90f090'; + else if (zone == 'wan') + return '#f09090'; - return _luci2.rpc.flush().then(function(info) { - var rv = { }; + for (var i = 0, hash = 0; + i < zone.length; + hash = zone.charCodeAt(i++) + ((hash << 5) - hash)); - $.extend(rv, info[0]); - $.extend(rv, info[1]); - $.extend(rv, info[2]); + for (var i = 0, color = '#'; + i < 3; + color += ('00' + ((hash >> i++ * 8) & 0xFF).tostring(16)).slice(-2)); - return rv; - }); + return color; }, - getProcessList: _luci2.rpc.declare({ - object: 'luci2.system', - method: 'process_list', - expect: { processes: [ ] }, - filter: function(data) { - data.sort(function(a, b) { return a.pid - b.pid }); - return data; - } - }), + findZoneByNetwork: function(network) + { + var self = this; + var zone = undefined; - getSystemLog: _luci2.rpc.declare({ - object: 'luci2.system', - method: 'syslog', - expect: { log: '' } - }), + return _luci2.uci.sections('firewall', 'zone', function(z) { + if (!z.name || !z.network) + return; - getKernelLog: _luci2.rpc.declare({ - object: 'luci2.system', - method: 'dmesg', - expect: { log: '' } - }), + if (!$.isArray(z.network)) + z.network = z.network.split(/\s+/); - getZoneInfo: function(cb) - { - return $.getJSON(_luci2.globals.resource + '/zoneinfo.json', cb); - }, + 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); - sendSignal: _luci2.rpc.declare({ - object: 'luci2.system', - method: 'process_signal', - params: [ 'pid', 'signal' ], - filter: function(data) { - return (data == 0); - } - }), + return zone; + }); + } + }; - initList: _luci2.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; - } - }), + 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, _luci2.rpc.declare({ + object: 'network', + method: 'get_proto_handlers', + expect: { '': { } } + }), + 'ifstate', 1, _luci2.rpc.declare({ + object: 'network.interface', + method: 'dump', + expect: { 'interface': [ ] } + }), + 'devstate', 2, _luci2.rpc.declare({ + object: 'network.device', + method: 'status', + expect: { '': { } } + }), + 'wifistate', 0, _luci2.rpc.declare({ + object: 'network.wireless', + method: 'status', + expect: { '': { } } + }), + 'bwstate', 2, _luci2.rpc.declare({ + object: 'luci2.network.bwmon', + method: 'statistics', + expect: { 'statistics': { } } + }), + 'devlist', 2, _luci2.rpc.declare({ + object: 'luci2.network', + method: 'device_list', + expect: { 'devices': [ ] } + }), + 'swlist', 0, _luci2.rpc.declare({ + object: 'luci2.network', + method: 'switch_list', + expect: { 'switches': [ ] } + }) + ], + + _fetch_protocol: function(proto) + { + var url = _luci2.globals.resource + '/proto/' + proto + '.js'; + var self = _luci2.NetworkModel; + + var def = $.Deferred(); + + $.ajax(url, { + method: 'GET', + cache: true, + dataType: 'text' + }).then(function(data) { + try { + var protoConstructorSource = ( + '(function(L, $) { ' + + 'return %s' + + '})(_luci2, $);\n\n' + + '//@ sourceURL=%s' + ).format(data, url); - 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; + var protoClass = eval(protoConstructorSource); - return false; + 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; }, - initRun: _luci2.rpc.declare({ - object: 'luci2.system', - method: 'init_action', - params: [ 'name', 'action' ], - filter: function(data) { - return (data == 0); - } - }), + _fetch_protocols: function() + { + var self = _luci2.NetworkModel; + var deferreds = [ ]; - 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) }, + for (var proto in self._cache.protolist) + deferreds.push(self._fetch_protocol(proto)); + return $.when.apply($, deferreds); + }, - getRcLocal: _luci2.rpc.declare({ - object: 'luci2.system', - method: 'rclocal_get', - expect: { data: '' } + _fetch_swstate: _luci2.rpc.declare({ + object: 'luci2.network', + method: 'switch_info', + params: [ 'switch' ], + expect: { 'info': { } } }), - setRcLocal: _luci2.rpc.declare({ - object: 'luci2.system', - method: 'rclocal_set', - params: [ 'data' ] - }), + _fetch_swstate_cb: function(responses) { + var self = _luci2.NetworkModel; + var swlist = self._cache.swlist; + var swstate = self._cache.swstate = { }; + for (var i = 0; i < responses.length; i++) + swstate[swlist[i]] = responses[i]; + }, - getCrontab: _luci2.rpc.declare({ - object: 'luci2.system', - method: 'crontab_get', - expect: { data: '' } - }), + _fetch_cache_cb: function(level) + { + var self = _luci2.NetworkModel; + var name = '_fetch_cache_cb_' + level; - setCrontab: _luci2.rpc.declare({ - object: 'luci2.system', - method: 'crontab_set', - params: [ 'data' ] - }), + 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) + { + _luci2.rpc.batch(); - getSSHKeys: _luci2.rpc.declare({ - object: 'luci2.system', - method: 'sshkeys_get', - expect: { keys: [ ] } - }), + for (var i = 0; i < self._cache.swlist.length; i++) + self._fetch_swstate(self._cache.swlist[i]); - setSSHKeys: _luci2.rpc.declare({ - object: 'luci2.system', - method: 'sshkeys_set', - params: [ 'keys' ] - }), + return _luci2.rpc.flush().then(self._fetch_swstate_cb); + } + return _luci2.deferrable(); + } + ); + }, - setPassword: _luci2.rpc.declare({ - object: 'luci2.system', - method: 'password_set', - params: [ 'user', 'password' ] - }), + _fetch_cache: function(level) + { + var self = _luci2.NetworkModel; + + return _luci2.uci.load(['network', 'wireless']).then(function() { + _luci2.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 _luci2.rpc.flush().then(self._fetch_cache_cb(level || 0)); + }); + }, + + _get: function(pkg, sid, key) + { + return _luci2.uci.get(pkg, sid, key); + }, + + _set: function(pkg, sid, key, val) + { + return _luci2.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 = _luci2.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 = _luci2.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 = _luci2.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 = _luci2.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 = _luci2.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 = _luci2.toArray(s.ifname); + + for (var ifname in self._devs) + { + var dev = self._devs[ifname]; + + if (dev.kind != 'wifi') + continue; + + var wnets = _luci2.toArray(_luci2.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 = _luci2.NetworkModel; + var net = _luci2.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 = _luci2.toArray(s.ifname); + + for (var ifname in self._devs) + { + var dev = self._devs[ifname]; + + if (dev.kind != 'wifi') + continue; + + var wnets = _luci2.toArray(_luci2.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 _luci2.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 _luci2.NetworkModel.Device(this._devs[ifname])); + + return devs.sort(this._sort_devices); + }, + + getDeviceByInterface: function(iface) + { + if (iface instanceof _luci2.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 _luci2.NetworkModel.Device(this._devs[ifname]); + + return undefined; + }, + + createDevice: function(name) + { + return new _luci2.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 _luci2.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 _luci2.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 _luci2.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: _luci2.tr('Master'), + sta: _luci2.tr('Client'), + adhoc: _luci2.tr('Ad-Hoc'), + monitor: _luci2.tr('Monitor'), + wds: _luci2.tr('Static WDS') + }, + + _status: function(key) + { + var s = _luci2.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 _luci2.NetworkModel._get(pkg, sid, key); + }, + + set: function(key, val) + { + var sid = this.options.sid; + var pkg = (this.options.kind == 'wifi') ? 'wireless' : 'network'; + return _luci2.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 _luci2.tr('Alias for network "%s"').format(this.options.ifname.substring(1)); + + case 'bridge': + return _luci2.tr('Network bridge'); + + case 'ethernet': + return _luci2.tr('Network device'); + + case 'tunnel': + switch (this.options.type) + { + case 1: /* tuntap */ + return _luci2.tr('TAP device'); + + case 512: /* PPP */ + return _luci2.tr('PPP tunnel'); + + case 768: /* IP-IP Tunnel */ + return _luci2.tr('IP-in-IP tunnel'); + + case 769: /* IP6-IP6 Tunnel */ + return _luci2.tr('IPv6-in-IPv6 tunnel'); + + case 776: /* IPv6-in-IPv4 */ + return _luci2.tr('IPv6-over-IPv4 tunnel'); + break; + + case 778: /* GRE over IP */ + return _luci2.tr('GRE-over-IP tunnel'); + + default: + return _luci2.tr('Tunnel device'); + } + + case 'vlan': + return _luci2.tr('VLAN %d on %s').format(this.options.vid, this.options.vsw.model); + + case 'wifi': + var o = this.options; + return _luci2.trc('(Wifi-Mode) "(SSID)" on (radioX)', '%s "%h" on %s').format( + o.wmode ? this._wifi_modes[o.wmode] : _luci2.tr('Unknown mode'), + o.wssid || '?', o.wdev + ); + } + + return _luci2.tr('Unknown device'); + }, + + icon: function(up) + { + var kind = this.options.kind; + + if (kind == 'alias') + kind = 'ethernet'; + + if (typeof(up) == 'undefined') + up = this.isUp(); + + return _luci2.globals.resource + '/icons/%s%s.png'.format(kind, up ? '' : '_disabled'); + }, + + isUp: function() + { + var l = _luci2.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 _luci2.NetworkModel.Interface)) + net = _luci2.NetworkModel.getInterface(net); + + if (net) + { + if (net.options.l3dev == this.options.ifname || + net.options.l2dev == this.options.ifname) + return true; + + var dev = _luci2.NetworkModel._devs[net.options.l2dev]; + if (dev && dev.kind == 'bridge' && dev.ports) + return ($.inArray(this.options.ifname, dev.ports) > -1); + } + + return false; + }, + + getMTU: function() + { + var dev = _luci2.NetworkModel._cache.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 = _luci2.NetworkModel._cache.devstate[this.options.ifname]; + if (dev && dev.macaddr) + return dev.macaddr.toUpperCase(); + + return undefined; + }, + + getInterfaces: function() + { + return _luci2.NetworkModel.getInterfacesByDevice(this.options.name); + }, + + getStatistics: function() + { + 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) + }; + }, + + getTrafficHistory: function() + { + var def = new Array(120); + + for (var i = 0; i < 120; i++) + def[i] = 0; + + var h = _luci2.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) + }; + }, + + removeFromInterface: function(iface) + { + if (!(iface instanceof _luci2.NetworkModel.Interface)) + iface = _luci2.NetworkModel.getInterface(iface); + + if (!iface) + return; + + var ifnames = _luci2.toArray(iface.get('ifname')); + if ($.inArray(this.options.ifname, ifnames) > -1) + iface.set('ifname', _luci2.filterArray(ifnames, this.options.ifname)); + + if (this.options.kind != 'wifi') + return; + + var networks = _luci2.toArray(this.get('network')); + if ($.inArray(iface.name(), networks) > -1) + this.set('network', _luci2.filterArray(networks, iface.name())); + }, + + attachToInterface: function(iface) + { + if (!(iface instanceof _luci2.NetworkModel.Interface)) + iface = _luci2.NetworkModel.getInterface(iface); + + if (!iface) + return; + + if (this.options.kind != 'wifi') + { + var ifnames = _luci2.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 = _luci2.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) + { + var s = _luci2.NetworkModel._cache.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 _luci2.NetworkModel._get('network', this.options.name, key); + }, + + set: function(key, val) + { + return _luci2.NetworkModel._set('network', this.options.name, key, val); + }, + + name: function() + { + return this.options.name; + }, + + protocol: function() + { + return (this.get('proto') || 'none'); + }, + + isUp: function() + { + return (this._status('up') === true); + }, + + isVirtual: function() + { + return (typeof(this.options.sid) != 'string'); + }, + + getProtocol: function() + { + var prname = this.get('proto') || 'none'; + return _luci2.NetworkModel._protos[prname] || _luci2.NetworkModel._protos.none; + }, + + getUptime: function() + { + var uptime = this._status('uptime'); + return isNaN(uptime) ? 0 : uptime; + }, + + getDevice: function(resolveAlias) + { + if (this.options.l3dev) + return _luci2.NetworkModel.getDevice(this.options.l3dev); + + return undefined; + }, + + getPhysdev: function() + { + if (this.options.l2dev) + return _luci2.NetworkModel.getDevice(this.options.l2dev); + + return undefined; + }, + + getSubdevices: function() + { + var rv = [ ]; + var dev = this.options.l2dev ? + _luci2.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(_luci2.NetworkModel.getDevice(dev.ports[i])); + + return rv; + }, + + getIPv4Addrs: function(mask) + { + 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)); + + return rv; + }, + + getIPv6Addrs: function(mask) + { + var rv = [ ]; + var addrs; + + addrs = this._status('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._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; + }, + + getDNSAddrs: function() + { + var rv = [ ]; + var addrs = this._status('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._status('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._status('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._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; + }, + + getIPv6Gateway: function() + { + 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; + + return undefined; + }, + + getStatistics: function() + { + var dev = this.getDevice() || new _luci2.NetworkModel.Device({}); + return dev.getStatistics(); + }, + + getTrafficHistory: function() + { + var dev = this.getDevice() || new _luci2.NetworkModel.Device({}); + return dev.getTrafficHistory(); + }, + + 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 _luci2.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 _luci2.NetworkModel.Device)) + dev = _luci2.NetworkModel.getDevice(dev); + + if (dev) + dev.attachToInterface(this); + } + } + }, + + changeProtocol: function(proto) + { + var pr = _luci2.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; + } + } + }, + + createForm: function(mapwidget) + { + var self = this; + var proto = self.getProtocol(); + var device = self.getDevice(); + + if (!mapwidget) + mapwidget = _luci2.cbi.Map; + + var map = new mapwidget('network', { + caption: _luci2.tr('Configure "%s"').format(self.name()) + }); + + var section = map.section(_luci2.cbi.SingleSection, self.name(), { + anonymous: true + }); + + section.tab({ + id: 'general', + caption: _luci2.tr('General Settings') + }); + + section.tab({ + id: 'advanced', + caption: _luci2.tr('Advanced Settings') + }); + + section.tab({ + id: 'ipv6', + caption: _luci2.tr('IPv6') + }); + + section.tab({ + id: 'physical', + caption: _luci2.tr('Physical Settings') + }); + + + section.taboption('general', _luci2.cbi.CheckboxValue, 'auto', { + caption: _luci2.tr('Start on boot'), + optional: true, + initial: true + }); + + var pr = section.taboption('general', _luci2.cbi.ListValue, 'proto', { + caption: _luci2.tr('Protocol') + }); + + pr.ucivalue = function(sid) { + return self.get('proto') || 'none'; + }; + + var ok = section.taboption('general', _luci2.cbi.ButtonValue, '_confirm', { + caption: _luci2.tr('Really switch?'), + description: _luci2.tr('Changing the protocol will clear all configuration for this interface!'), + text: _luci2.tr('Change protocol') + }); + + ok.on('click', function(ev) { + self.changeProtocol(pr.formvalue(ev.data.sid)); + self.createForm(mapwidget).show(); + }); + + var protos = _luci2.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', _luci2.cbi.CheckboxValue, 'type', { + caption: _luci2.tr('Network bridge'), + description: _luci2.tr('Merges multiple devices into one logical bridge'), + optional: true, + enabled: 'bridge', + disabled: '', + initial: '' + }); + + section.taboption('physical', _luci2.cbi.DeviceList, '__iface_multi', { + caption: _luci2.tr('Devices'), + multiple: true, + bridges: false + }).depends('type', true); + + section.taboption('physical', _luci2.cbi.DeviceList, '__iface_single', { + caption: _luci2.tr('Device'), + multiple: false, + bridges: true + }).depends('type', false); + + var mac = section.taboption('physical', _luci2.cbi.InputValue, 'macaddr', { + caption: _luci2.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', _luci2.cbi.InputValue, 'mtu', { + caption: _luci2.tr('Override MTU'), + optional: true, + placeholder: device ? device.getMTU() : undefined, + datatype: 'range(1, 9000)' + }); + + section.taboption('physical', _luci2.cbi.InputValue, 'metric', { + caption: _luci2.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: _luci2.rpc.declare({ + object: 'system', + method: 'info', + expect: { '': { } } + }), + + getBoardInfo: _luci2.rpc.declare({ + object: 'system', + method: 'board', + expect: { '': { } } + }), + + getDiskInfo: _luci2.rpc.declare({ + object: 'luci2.system', + method: 'diskfree', + expect: { '': { } } + }), + + getInfo: function(cb) + { + _luci2.rpc.batch(); + + this.getSystemInfo(); + this.getBoardInfo(); + this.getDiskInfo(); + + return _luci2.rpc.flush().then(function(info) { + var rv = { }; + + $.extend(rv, info[0]); + $.extend(rv, info[1]); + $.extend(rv, info[2]); + + return rv; + }); + }, + + getProcessList: _luci2.rpc.declare({ + object: 'luci2.system', + method: 'process_list', + expect: { processes: [ ] }, + filter: function(data) { + data.sort(function(a, b) { return a.pid - b.pid }); + return data; + } + }), + + getSystemLog: _luci2.rpc.declare({ + object: 'luci2.system', + method: 'syslog', + expect: { log: '' } + }), + + getKernelLog: _luci2.rpc.declare({ + object: 'luci2.system', + method: 'dmesg', + expect: { log: '' } + }), + + getZoneInfo: function(cb) + { + return $.getJSON(_luci2.globals.resource + '/zoneinfo.json', cb); + }, + + sendSignal: _luci2.rpc.declare({ + object: 'luci2.system', + method: 'process_signal', + params: [ 'pid', 'signal' ], + filter: function(data) { + return (data == 0); + } + }), + + initList: _luci2.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: _luci2.rpc.declare({ + object: 'luci2.system', + method: 'init_action', + params: [ 'name', 'action' ], + filter: function(data) { + return (data == 0); + } + }), + + 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: _luci2.rpc.declare({ + object: 'luci2.system', + method: 'rclocal_get', + expect: { data: '' } + }), + + setRcLocal: _luci2.rpc.declare({ + object: 'luci2.system', + method: 'rclocal_set', + params: [ 'data' ] + }), + + + getCrontab: _luci2.rpc.declare({ + object: 'luci2.system', + method: 'crontab_get', + expect: { data: '' } + }), + + setCrontab: _luci2.rpc.declare({ + object: 'luci2.system', + method: 'crontab_set', + params: [ 'data' ] + }), + + + getSSHKeys: _luci2.rpc.declare({ + object: 'luci2.system', + method: 'sshkeys_get', + expect: { keys: [ ] } + }), + + setSSHKeys: _luci2.rpc.declare({ + object: 'luci2.system', + method: 'sshkeys_set', + params: [ 'keys' ] + }), + + + setPassword: _luci2.rpc.declare({ + object: 'luci2.system', + method: 'password_set', + params: [ 'user', 'password' ] + }), listLEDs: _luci2.rpc.declare({ @@ -1292,6 +2987,18 @@ function LuCI2() }), + testReset: _luci2.rpc.declare({ + object: 'luci2.system', + method: 'reset_test', + expect: { supported: false } + }), + + startReset: _luci2.rpc.declare({ + object: 'luci2.system', + method: 'reset_start' + }), + + performReboot: _luci2.rpc.declare({ object: 'luci2.system', method: 'reboot' @@ -1447,142 +3154,169 @@ function LuCI2() window.clearInterval(this._hearbeatInterval); delete this._hearbeatInterval; } + }, + + + _acls: { }, + + _fetch_acls: _luci2.rpc.declare({ + object: 'session', + method: 'access', + expect: { '': { } } + }), + + _fetch_acls_cb: function(acls) + { + _luci2.session._acls = acls; + }, + + updateACLs: function() + { + return _luci2.session._fetch_acls() + .then(_luci2.session._fetch_acls_cb); + }, + + hasACL: function(scope, object, func) + { + var acls = _luci2.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 = { + 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 = _luci2.ui._loading || (_luci2.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(_luci2.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 = _luci2.ui._dialog || (_luci2.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($('') - .addClass('cbi-button') - .text(_luci2.tr('Close')) - .click(function() { - $('body') - .css('overflow', '') - .css('padding', '') - .css('width', '') - .css('height', ''); - - $(this).parent().parent().parent().hide(); - })))) + .addClass('modal-content') + .append($('') + .addClass('modal-header') + .append('') + .addClass('modal-title')) + .append($('') + .addClass('modal-body')) + .append($('') + .addClass('modal-footer') + .append(_luci2.ui.button(_luci2.tr('Close'), 'primary') + .click(function() { + $(this).parents('div.modal').modal('hide'); + }))))) .appendTo(body) - ); + }); if (typeof(options) != 'object') options = { }; if (title === false) { - body - .css('overflow', '') - .css('padding', '') - .css('width', '') - .css('height', ''); - - _luci2._dialog.hide(); + state.dialog.modal('hide'); - return; + return state.dialog; } - var cnt = div.children().children('div.cbi-modal-dialog-body'); - var ftr = div.children().children('div.cbi-modal-dialog-footer'); + var cnt = state.dialog.children().children().children('div.modal-body'); + var ftr = state.dialog.children().children().children('div.modal-footer'); - ftr.empty(); + ftr.empty().show(); if (options.style == 'confirm') { - ftr.append($('') - .addClass('cbi-button') - .text(_luci2.tr('Ok')) + ftr.append(_luci2.ui.button(_luci2.tr('Ok'), 'primary') .click(options.confirm || function() { _luci2.ui.dialog(false) })); - ftr.append($('') - .addClass('cbi-button') - .text(_luci2.tr('Cancel')) + ftr.append(_luci2.ui.button(_luci2.tr('Cancel'), 'default') .click(options.cancel || function() { _luci2.ui.dialog(false) })); } else if (options.style == 'close') { - ftr.append($('') - .addClass('cbi-button') - .text(_luci2.tr('Close')) + ftr.append(_luci2.ui.button(_luci2.tr('Close'), 'primary') .click(options.close || function() { _luci2.ui.dialog(false) })); } else if (options.style == 'wait') { - ftr.append($('') - .addClass('cbi-button') - .text(_luci2.tr('Close')) + ftr.append(_luci2.ui.button(_luci2.tr('Close'), 'primary') .attr('disabled', true)); } - div.find('div.cbi-modal-dialog-header').text(title); - div.show(); + if (options.wide) + { + state.dialog.addClass('wide'); + } + else + { + state.dialog.removeClass('wide'); + } - cnt - .css('max-height', Math.floor(win.height() * 0.70) + 'px') - .empty() - .append(content); + state.dialog.find('h4:first').text(title); + state.dialog.modal('show'); - div.children() - .css('margin-top', -Math.floor(div.children().height() / 2) + 'px'); + cnt.empty().append(content); - 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()); + return state.dialog; }, upload: function(title, content, options) { - var form = _luci2._upload || ( - _luci2._upload = $('') + var state = _luci2.ui._upload || (_luci2.ui._upload = { + form: $('') .attr('method', 'post') .attr('action', '/cgi-bin/luci-upload') .attr('enctype', 'multipart/form-data') @@ -1590,31 +3324,28 @@ function LuCI2() .append($('')) .append($('') .attr('type', 'hidden') - .attr('name', 'sessionid') - .attr('value', _luci2.globals.sid)) + .attr('name', 'sessionid')) .append($('') .attr('type', 'hidden') - .attr('name', 'filename') - .attr('value', options.filename)) + .attr('name', 'filename')) .append($('') .attr('type', 'file') .attr('name', 'filedata') .addClass('cbi-input-file')) .append($('') .css('width', '100%') - .addClass('progressbar') - .addClass('intermediate') + .addClass('progress progress-striped active') .append($('') + .addClass('progress-bar') .css('width', '100%'))) .append($('') + .addClass('pull-right') .attr('name', 'cbi-fileupload-frame') .css('width', '1px') .css('height', '1px') - .css('visibility', 'hidden')) - ); + .css('visibility', 'hidden')), - var finish = _luci2._upload_finish_cb || ( - _luci2._upload_finish_cb = function(ev) { + finish_cb: function(ev) { $(this).off('load'); var body = (this.contentDocument || this.contentWindow.document).body; @@ -1639,41 +3370,43 @@ function LuCI2() $('').text(_luci2.tr('In case of network problems try uploading the file again.')) ], { style: 'close' }); } - else if (typeof(ev.data.cb) == 'function') + else if (typeof(state.success_cb) == 'function') { - ev.data.cb(json); + state.success_cb(json); } - } - ); + }, - var confirm = _luci2._upload_confirm_cb || ( - _luci2._upload_confirm_cb = function() { - var d = _luci2._upload; - var f = d.find('.cbi-input-file'); - var b = d.find('.progressbar'); - var p = d.find('p'); + 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; - d.find('iframe').on('load', { cb: options.success }, finish); - d.submit(); + state.form.find('iframe').on('load', state.finish_cb); + state.form.submit(); f.hide(); b.show(); p.text(_luci2.tr('File upload in progress â¦')); - _luci2._dialog.find('button').prop('disabled', true); + 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 || _luci2.tr('Select the file to upload and press "%s" to proceed.').format(_luci2.tr('Ok'))); + + state.form.find('[name=sessionid]').val(_luci2.globals.sid); + state.form.find('[name=filename]').val(options.filename); - _luci2._upload.find('.progressbar').hide(); - _luci2._upload.find('.cbi-input-file').val('').show(); - _luci2._upload.find('p').text(content || _luci2.tr('Select the file to upload and press "%s" to proceed.').format(_luci2.tr('Ok'))); + state.success_cb = options.success; - _luci2.ui.dialog(title || _luci2.tr('File upload'), _luci2._upload, { + _luci2.ui.dialog(title || _luci2.tr('File upload'), state.form, { style: 'confirm', - confirm: confirm + confirm: state.confirm_cb }); }, @@ -1751,32 +3484,10 @@ function LuCI2() login: function(invalid) { - if (!_luci2._login_deferred || _luci2._login_deferred.state() != 'pending') - _luci2._login_deferred = $.Deferred(); - - /* try to find sid from hash */ - var sid = _luci2.getHash('id'); - if (sid && sid.match(/^[a-f0-9]{32}$/)) - { - _luci2.globals.sid = sid; - _luci2.session.isAlive().then(function(access) { - if (access) - { - _luci2.session.startHeartbeat(); - _luci2._login_deferred.resolve(); - } - else - { - _luci2.setHash('id', undefined); - _luci2.ui.login(); - } - }); - - return _luci2._login_deferred; - } - - var form = _luci2._login || ( - _luci2._login = $('') + var state = _luci2.ui._login || (_luci2.ui._login = { + form: $('') + .attr('target', '') + .attr('method', 'post') .append($('') .addClass('alert-message') .text(_luci2.tr('Wrong username or password given!'))) @@ -1788,7 +3499,11 @@ function LuCI2() .attr('type', 'text') .attr('name', 'username') .attr('value', 'root') - .addClass('cbi-input-text')))) + .addClass('form-control') + .keypress(function(ev) { + if (ev.which == 10 || ev.which == 13) + state.confirm_cb(); + })))) .append($('') .append($('') .text(_luci2.tr('Password')) @@ -1796,13 +3511,15 @@ function LuCI2() .append($('') .attr('type', 'password') .attr('name', 'password') - .addClass('cbi-input-password')))) + .addClass('form-control') + .keypress(function(ev) { + if (ev.which == 10 || ev.which == 13) + state.confirm_cb(); + })))) .append($('') - .text(_luci2.tr('Enter your username and password above, then click "%s" to proceed.').format(_luci2.tr('Ok')))) - ); + .text(_luci2.tr('Enter your username and password above, then click "%s" to proceed.').format(_luci2.tr('Ok')))), - var response_cb = _luci2._login_response_cb || ( - _luci2._login_response_cb = function(response) { + response_cb: function(response) { if (!response.ubus_rpc_session) { _luci2.ui.login(true); @@ -1813,16 +3530,13 @@ function LuCI2() _luci2.setHash('id', _luci2.globals.sid); _luci2.session.startHeartbeat(); _luci2.ui.dialog(false); - _luci2._login_deferred.resolve(); + state.deferred.resolve(); } - } - ); + }, - var confirm_cb = _luci2._login_confirm_cb || ( - _luci2._login_confirm_cb = function() { - var d = _luci2._login; - var u = d.find('[name=username]').val(); - var p = d.find('[name=password]').val(); + confirm_cb: function() { + var u = state.form.find('[name=username]').val(); + var p = state.form.find('[name=password]').val(); if (!u) return; @@ -1840,23 +3554,56 @@ function LuCI2() ); _luci2.globals.sid = '00000000000000000000000000000000'; - _luci2.session.login(u, p).then(response_cb); + _luci2.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 = _luci2.getHash('id'); + if (sid && sid.match(/^[a-f0-9]{32}$/)) + { + _luci2.globals.sid = sid; + _luci2.session.isAlive().then(function(access) { + if (access) + { + _luci2.session.startHeartbeat(); + state.deferred.resolve(); + } + else + { + _luci2.setHash('id', undefined); + _luci2.ui.login(); + } + }); + + return state.deferred; + } if (invalid) - form.find('.alert-message').show(); + state.form.find('.alert-message').show(); else - form.find('.alert-message').hide(); + state.form.find('.alert-message').hide(); - _luci2.ui.dialog(_luci2.tr('Authorization Required'), form, { + _luci2.ui.dialog(_luci2.tr('Authorization Required'), state.form, { style: 'confirm', - confirm: confirm_cb + confirm: state.confirm_cb }); - return _luci2._login_deferred; + state.form.find('[name=password]').focus(); + + return state.deferred; }, + cryptPassword: _luci2.rpc.declare({ + object: 'luci2.ui', + method: 'crypt', + params: [ 'data' ], + expect: { crypt: '' } + }), + _acl_merge_scope: function(acl_scope, scope) { @@ -1958,9 +3705,17 @@ function LuCI2() .append(_luci2.globals.mainMenu.render(2, 900)); }, - renderView: function(node) + renderView: function() { + var node = arguments[0]; var name = node.view.split(/\//).join('.'); + var args = [ ]; + + for (var i = 1; i < arguments.length; i++) + args.push(arguments[i]); + + if (_luci2.globals.currentView) + _luci2.globals.currentView.finish(); _luci2.ui.renderViewMenu(); @@ -1970,61 +3725,196 @@ function LuCI2() _luci2.setHash('view', node.view); if (_luci2._views[name] instanceof _luci2.ui.view) - return _luci2._views[name].render(); + { + _luci2.globals.currentView = _luci2._views[name]; + return _luci2._views[name].render.apply(_luci2._views[name], args); + } + + var url = _luci2.globals.resource + '/view/' + name + '.js'; - return $.ajax(_luci2.globals.resource + '/view/' + name + '.js', { + return $.ajax(url, { method: 'GET', cache: true, dataType: 'text' }).then(function(data) { try { - var viewConstructor = (new Function(['L', '$'], 'return ' + data))(_luci2, $); + var viewConstructorSource = ( + '(function(L, $) { ' + + 'return %s' + + '})(_luci2, $);\n\n' + + '//@ sourceURL=%s' + ).format(data, url); + + var viewConstructor = eval(viewConstructorSource); _luci2._views[name] = new viewConstructor({ name: name, acls: node.write || { } }); - return _luci2._views[name].render(); + _luci2.globals.currentView = _luci2._views[name]; + return _luci2._views[name].render.apply(_luci2._views[name], args); } - catch(e) { }; + catch(e) { + alert('Unable to instantiate view "%s": %s'.format(url, e)); + }; return $.Deferred().resolve(); }); }, + updateHostname: function() + { + return _luci2.system.getBoardInfo().then(function(info) { + if (info.hostname) + $('#hostname').text(info.hostname); + }); + }, + + updateChanges: function() + { + return _luci2.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.%s=%s'.format(config, c[1], c[2])); + break; + + case 'remove': + if (c.length < 3) + log.push('uci delete %s./etc/config/%s
%s'.format(config, log.join('\n')); + n += changes[config].length; + } + + if (n > 0) + $('#changes') + .empty() + .show() + .append($('') + .attr('href', '#') + .addClass('label') + .addClass('notice') + .text(_luci2.trcp('Pending configuration changes', '1 change', '%d changes', n).format(n)) + .click(function(ev) { + _luci2.ui.dialog(_luci2.tr('Staged configuration changes'), html, { style: 'close' }); + ev.preventDefault(); + })); + else + $('#changes') + .hide(); + }); + }, + init: function() { _luci2.ui.loading(true); $.when( + _luci2.ui.updateHostname(), + _luci2.ui.updateChanges(), _luci2.ui.renderMainMenu() ).then(function() { _luci2.ui.renderView(_luci2.globals.defaultNode).then(function() { _luci2.ui.loading(false); }) }); + }, + + button: function(label, style, title) + { + style = style || 'default'; + + return $('') + .attr('type', 'button') + .attr('title', title ? title : '') + .addClass('btn btn-' + style) + .text(label); } }; - var AbstractWidget = Class.extend({ + this.ui.AbstractWidget = Class.extend({ i18n: function(text) { return text; }, - toString: function() { - var x = document.createElement('div'); - x.appendChild(this.render()); + 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); - return x.innerHTML; + default: + return ''.format.apply('' + this.options[key], args); + } + }, + + toString: function() { + return $('').append(this.render()).html(); }, insertInto: function(id) { return $(id).empty().append(this.render()); + }, + + appendTo: function(id) { + return $(id).append(this.render()); } }); - this.ui.view = AbstractWidget.extend({ + this.ui.view = this.ui.AbstractWidget.extend({ _fetch_template: function() { return $.ajax(_luci2.globals.resource + '/template/' + this.options.name + '.htm', { @@ -2070,16 +3960,58 @@ function LuCI2() container.append($('').append(this.title)); if (this.description) - container.append($('').addClass('cbi-map-descr').append(this.description)); + container.append($('').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 _luci2.deferrable(self.execute()); + return _luci2.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() { + _luci2.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 = AbstractWidget.extend({ + this.ui.menu = this.ui.AbstractWidget.extend({ init: function() { this._nodes = { }; }, @@ -2129,7 +4061,10 @@ function LuCI2() var child = this.firstChildView(nodes[i]); if (child) { - $.extend(node, child); + for (var key in child) + if (!node.hasOwnProperty(key) && child.hasOwnProperty(key)) + node[key] = child[key]; + return node; } } @@ -2163,9 +4098,9 @@ function LuCI2() var list = $('