/* LuCI2 - OpenWrt Web Interface Copyright 2013 Jo-Philipp Wich Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 */ String.prototype.format = function() { var html_esc = [/&/g, '&', /"/g, '"', /'/g, ''', //g, '>']; var quot_esc = [/"/g, '"', /'/g, ''']; function esc(s, r) { for( var i = 0; i < r.length; i += 2 ) s = s.replace(r[i], r[i+1]); return s; } var str = this; var out = ''; var re = /^(([^%]*)%('.|0|\x20)?(-)?(\d+)?(\.\d+)?(%|b|c|d|u|f|o|s|x|X|q|h|j|t|m))/; var a = b = [], numSubstitutions = 0, numMatches = 0; while ((a = re.exec(str)) != null) { var m = a[1]; var leftpart = a[2], pPad = a[3], pJustify = a[4], pMinLength = a[5]; var pPrecision = a[6], pType = a[7]; numMatches++; if (pType == '%') { subst = '%'; } else { if (numSubstitutions < arguments.length) { var param = arguments[numSubstitutions++]; var pad = ''; if (pPad && pPad.substr(0,1) == "'") pad = leftpart.substr(1,1); else if (pPad) pad = pPad; var justifyRight = true; if (pJustify && pJustify === "-") justifyRight = false; var minLength = -1; if (pMinLength) minLength = parseInt(pMinLength); var precision = -1; if (pPrecision && pType == 'f') precision = parseInt(pPrecision.substring(1)); var subst = param; switch(pType) { case 'b': subst = (parseInt(param) || 0).toString(2); break; case 'c': subst = String.fromCharCode(parseInt(param) || 0); break; case 'd': subst = (parseInt(param) || 0); break; case 'u': subst = Math.abs(parseInt(param) || 0); break; case 'f': subst = (precision > -1) ? ((parseFloat(param) || 0.0)).toFixed(precision) : (parseFloat(param) || 0.0); break; case 'o': subst = (parseInt(param) || 0).toString(8); break; case 's': subst = param; break; case 'x': subst = ('' + (parseInt(param) || 0).toString(16)).toLowerCase(); break; case 'X': subst = ('' + (parseInt(param) || 0).toString(16)).toUpperCase(); break; case 'h': subst = esc(param, html_esc); break; case 'q': subst = esc(param, quot_esc); break; case 'j': subst = String.serialize(param); break; case 't': var td = 0; var th = 0; var tm = 0; var ts = (param || 0); if (ts > 60) { tm = Math.floor(ts / 60); ts = (ts % 60); } if (tm > 60) { th = Math.floor(tm / 60); tm = (tm % 60); } if (th > 24) { td = Math.floor(th / 24); th = (th % 24); } subst = (td > 0) ? '%dd %dh %dm %ds'.format(td, th, tm, ts) : '%dh %dm %ds'.format(th, tm, ts); break; case 'm': var mf = pMinLength ? parseInt(pMinLength) : 1000; var pr = pPrecision ? Math.floor(10*parseFloat('0'+pPrecision)) : 2; var i = 0; var val = parseFloat(param || 0); var units = [ '', 'K', 'M', 'G', 'T', 'P', 'E' ]; for (i = 0; (i < units.length) && (val > mf); i++) val /= mf; subst = val.toFixed(pr) + ' ' + units[i]; break; } subst = (typeof(subst) == 'undefined') ? '' : subst.toString(); if (minLength > 0 && pad.length > 0) for (var i = 0; i < (minLength - subst.length); i++) subst = justifyRight ? (pad + subst) : (subst + pad); } } out += leftpart + subst; str = str.substr(m.length); } return out + str; } function LuCI2() { var _luci2 = this; var Class = function() { }; Class.extend = function(properties) { Class.initializing = true; var prototype = new this(); var superprot = this.prototype; Class.initializing = false; $.extend(prototype, properties, { callSuper: function() { var args = [ ]; var meth = arguments[0]; if (typeof(superprot[meth]) != 'function') return undefined; for (var i = 1; i < arguments.length; i++) args.push(arguments[i]); return superprot[meth].apply(this, args); } }); function _class() { this.options = arguments[0] || { }; if (!Class.initializing && typeof(this.init) == 'function') this.init.apply(this, arguments); } _class.prototype = prototype; _class.prototype.constructor = _class; _class.extend = Class.extend; return _class; }; this.defaults = function(obj, def) { for (var key in def) if (typeof(obj[key]) == 'undefined') obj[key] = def[key]; return obj; }; this.isDeferred = function(x) { return (typeof(x) == 'object' && typeof(x.then) == 'function' && typeof(x.promise) == 'function'); }; this.deferrable = function() { if (this.isDeferred(arguments[0])) return arguments[0]; var d = $.Deferred(); d.resolve.apply(d, arguments); return d.promise(); }; this.i18n = { loaded: false, catalog: { }, plural: function(n) { return 0 + (n != 1) }, init: function() { if (_luci2.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]), { async: false, cache: true, dataType: 'json', success: function(data) { $.extend(_luci2.i18n.catalog, data); var pe = _luci2.i18n.catalog['']; if (pe) { delete _luci2.i18n.catalog['']; try { var pf = new Function('n', 'return 0 + (' + pe + ')'); _luci2.i18n.plural = pf; } catch (e) { }; } } }); _luci2.i18n.loaded = true; } }; this.tr = function(msgid) { _luci2.i18n.init(); var msgstr = _luci2.i18n.catalog[msgid]; if (typeof(msgstr) == 'undefined') return msgid; else if (typeof(msgstr) == 'string') return msgstr; else return msgstr[0]; }; this.trp = function(msgid, msgid_plural, count) { _luci2.i18n.init(); var msgstr = _luci2.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)]; }; this.trc = function(msgctx, msgid) { _luci2.i18n.init(); var msgstr = _luci2.i18n.catalog[msgid + '\u0004' + msgctx]; if (typeof(msgstr) == 'undefined') return msgid; else if (typeof(msgstr) == 'string') return msgstr; else return msgstr[0]; }; this.trcp = function(msgctx, msgid, msgid_plural, count) { _luci2.i18n.init(); var msgstr = _luci2.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)]; }; this.setHash = function(key, value) { var h = ''; var data = this.getHash(undefined); if (typeof(value) == 'undefined') delete data[key]; else data[key] = value; var keys = [ ]; for (var k in data) keys.push(k); keys.sort(); for (var i = 0; i < keys.length; i++) { if (i > 0) h += ','; h += keys[i] + ':' + data[keys[i]]; } if (h.length) location.hash = '#' + h; else location.hash = ''; }; this.getHash = function(key) { var data = { }; var tuples = (location.hash || '#').substring(1).split(/,/); for (var i = 0; i < tuples.length; i++) { var tuple = tuples[i].split(/:/); if (tuple.length == 2) data[tuple[0]] = tuple[1]; } if (typeof(key) != 'undefined') return data[key]; 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: 15000, resource: '/luci2', sid: '00000000000000000000000000000000' }; this.rpc = { _id: 1, _batch: undefined, _requests: { }, _call: function(req, cb) { return $.ajax('/ubus', { cache: false, contentType: 'application/json', data: JSON.stringify(req), dataType: 'json', type: 'POST', timeout: _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 || !$.isArray(list)) list = [ ]; return $.Deferred().resolveWith(this, [ list ]); }, _call_cb: function(msg) { var data = [ ]; var type = Object.prototype.toString; var reqs = this._rpc_req; if (!$.isArray(reqs)) { msg = [ msg ]; reqs = [ reqs ]; } for (var i = 0; i < msg.length; i++) { /* fetch related request info */ var req = _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; /* verify message frame */ if (typeof(msg[i]) == 'object' && msg[i].jsonrpc == '2.0') if ($.isArray(msg[i].result) && msg[i].result[0] == 0) ret = (msg[i].result.length > 1) ? msg[i].result[1] : msg[i].result[0]; if (req.expect) { for (var key in req.expect) { if (typeof(ret) != 'undefined' && key != '') ret = ret[key]; if (typeof(ret) == 'undefined' || type.call(ret) != type.call(req.expect[key])) ret = req.expect[key]; break; } } /* apply filter */ if (typeof(req.filter) == 'function') { req.priv[0] = ret; req.priv[1] = req.params; ret = req.filter.apply(_luci2.rpc, req.priv); } /* store response data */ if (typeof(req.index) == 'number') data[req.index] = ret; else data = ret; /* delete request object */ delete _luci2.rpc._requests[reqs[i].id]; } return $.Deferred().resolveWith(this, [ data ]); }, list: function() { var params = [ ]; for (var i = 0; i < arguments.length; i++) params[i] = arguments[i]; var msg = { jsonrpc: '2.0', id: this._id++, method: 'list', params: (params.length > 0) ? params : undefined }; return this._call(msg, this._list_cb); }, batch: function() { if (!$.isArray(this._batch)) this._batch = [ ]; }, flush: function() { if (!$.isArray(this._batch)) return _luci2.deferrable([ ]); var req = this._batch; delete this._batch; /* call rpc */ return this._call(req, this._call_cb); }, declare: function(options) { var _rpc = this; return function() { /* build parameter object */ var p_off = 0; var params = { }; if ($.isArray(options.params)) for (p_off = 0; p_off < options.params.length; p_off++) params[options.params[p_off]] = arguments[p_off]; /* all remaining arguments are private args */ var priv = [ undefined, undefined ]; for (; p_off < arguments.length; p_off++) priv.push(arguments[p_off]); /* store request info */ var req = _rpc._requests[_rpc._id] = { expect: options.expect, filter: options.filter, params: params, priv: priv }; /* build message object */ var msg = { jsonrpc: '2.0', id: _rpc._id++, method: 'call', params: [ _luci2.globals.sid, options.object, options.method, params ] }; /* when a batch is in progress then store index in request data * and push message object onto the stack */ if ($.isArray(_rpc._batch)) { req.index = _rpc._batch.push(msg) - 1; return _luci2.deferrable(msg); } /* call rpc */ return _rpc._call(msg, _rpc._call_cb); }; } }; this.UCIContext = Class.extend({ init: function() { this.state = { newid: 0, values: { }, creates: { }, changes: { }, deletes: { }, reorder: { } }; }, _load: _luci2.rpc.declare({ object: 'uci', method: 'get', params: [ 'config' ], expect: { values: { } } }), _order: _luci2.rpc.declare({ object: 'uci', method: 'order', params: [ 'config', 'sections' ] }), _add: _luci2.rpc.declare({ object: 'uci', method: 'add', params: [ 'config', 'type', 'name', 'values' ], expect: { section: '' } }), _set: _luci2.rpc.declare({ object: 'uci', method: 'set', params: [ 'config', 'section', 'values' ] }), _delete: _luci2.rpc.declare({ object: 'uci', method: 'delete', params: [ 'config', 'section', 'options' ] }), load: function(packages) { var self = this; var seen = { }; var pkgs = [ ]; if (!$.isArray(packages)) packages = [ packages ]; _luci2.rpc.batch(); 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; }); }, unload: function(packages) { if (!$.isArray(packages)) packages = [ packages ]; for (var i = 0; i < packages.length; i++) { delete this.state.values[packages[i]]; delete this.state.creates[packages[i]]; delete this.state.changes[packages[i]]; delete this.state.deletes[packages[i]]; } }, add: function(conf, type, name) { var c = this.state.creates; var s = '.new.%d'.format(this.state.newid++); if (!c[conf]) c[conf] = { }; c[conf][s] = { '.type': type, '.name': s, '.create': name, '.anonymous': !name, '.index': 1000 + this.state.newid }; return s; }, remove: function(conf, sid) { var n = this.state.creates; var c = this.state.changes; var d = this.state.deletes; /* requested deletion of a just created section */ if (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; } }, sections: function(conf, type, cb) { var sa = [ ]; var v = this.state.values[conf]; var n = this.state.creates[conf]; var c = this.state.changes[conf]; var d = this.state.deletes[conf]; if (!v) return sa; for (var s in v) if (!d || d[s] !== true) if (!type || v[s]['.type'] == type) sa.push($.extend({ }, v[s], c ? c[s] : undefined)); if (n) for (var s in n) if (!type || n[s]['.type'] == type) sa.push(n[s]); sa.sort(function(a, b) { return a['.index'] - b['.index']; }); for (var i = 0; i < sa.length; i++) sa[i]['.index'] = i; if (typeof(cb) == 'function') for (var i = 0; i < sa.length; i++) cb.call(this, sa[i], sa[i]['.name']); return sa; }, get: function(conf, sid, opt) { var v = this.state.values; var n = this.state.creates; var c = this.state.changes; var d = this.state.deletes; if (typeof(sid) == 'undefined') return undefined; /* requested option in a just created section */ if (sid.indexOf('.new.') == 0) { if (!n[conf]) return undefined; if (typeof(opt) == 'undefined') return n[conf][sid]; return n[conf][sid][opt]; } /* requested an option value */ if (typeof(opt) != 'undefined') { /* check whether option was deleted */ if (d[conf] && d[conf][sid]) { if (d[conf][sid] === true) return undefined; for (var i = 0; i < d[conf][sid].length; i++) if (d[conf][sid][i] == opt) return undefined; } /* check whether option was changed */ if (c[conf] && c[conf][sid] && typeof(c[conf][sid][opt]) != 'undefined') return c[conf][sid][opt]; /* return base value */ if (v[conf] && v[conf][sid]) return v[conf][sid][opt]; return undefined; } /* requested an entire section */ if (v[conf]) return v[conf][sid]; return undefined; }, set: function(conf, sid, opt, val) { var n = this.state.creates; var c = this.state.changes; var d = this.state.deletes; if (typeof(sid) == 'undefined' || typeof(opt) == 'undefined' || opt.charAt(0) == '.') return; if (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; if (!c[conf]) c[conf] = { }; if (!c[conf][sid]) c[conf][sid] = { }; /* undelete option */ if (d[conf] && d[conf][sid]) d[conf][sid] = _luci2.filterArray(d[conf][sid], opt); c[conf][sid][opt] = val; } else { if (!d[conf]) d[conf] = { }; if (!d[conf][sid]) d[conf][sid] = [ ]; if (d[conf][sid] !== true) d[conf][sid].push(opt); } }, unset: function(conf, sid, opt) { return this.set(conf, sid, opt, undefined); }, _reload: function() { var pkgs = [ ]; for (var pkg in this.state.values) pkgs.push(pkg); this.init(); return this.load(pkgs); }, _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); } } 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(); }); }, _apply: _luci2.rpc.declare({ object: 'uci', method: 'apply', params: [ 'timeout', 'rollback' ] }), _confirm: _luci2.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; } 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 ]); return; } deferred.resolveWith(self, [ rv ]); }); }; window.setTimeout(try_confirm, 1000); }); return deferred; }, changes: _luci2.rpc.declare({ object: 'uci', method: 'changes', expect: { changes: { } } }), 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({ object: 'iwinfo', method: 'devices', expect: { 'devices': [ ] }, filter: function(data) { data.sort(); return data; } }), getDeviceStatus: _luci2.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: _luci2.rpc.declare({ object: 'iwinfo', method: 'assoclist', params: [ 'device' ], expect: { results: [ ] }, filter: function(data, params) { for (var i = 0; i < data.length; i++) data[i]['device'] = params['device']; data.sort(function(a, b) { if (a.bssid < b.bssid) return -1; else if (a.bssid > b.bssid) return 1; else return 0; }); return data; } }), getWirelessStatus: function() { return this.listDeviceNames().then(function(names) { _luci2.rpc.batch(); for (var i = 0; i < names.length; i++) _luci2.wireless.getDeviceStatus(names[i]); return _luci2.rpc.flush(); }).then(function(networks) { var rv = { }; var phy_attrs = [ 'country', 'channel', 'frequency', 'frequency_offset', 'txpower', 'txpower_offset', 'hwmodes', 'hardware', 'phy' ]; var net_attrs = [ 'ssid', 'bssid', 'mode', 'quality', 'quality_max', 'signal', 'noise', 'bitrate', 'encryption' ]; for (var i = 0; i < networks.length; i++) { var phy = rv[networks[i].phy] || ( rv[networks[i].phy] = { networks: [ ] } ); var net = { device: networks[i].device }; for (var j = 0; j < phy_attrs.length; j++) phy[phy_attrs[j]] = networks[i][phy_attrs[j]]; for (var j = 0; j < net_attrs.length; j++) net[net_attrs[j]] = networks[i][net_attrs[j]]; phy.networks.push(net); } return rv; }); }, getAssocLists: function() { return this.listDeviceNames().then(function(names) { _luci2.rpc.batch(); for (var i = 0; i < names.length; i++) _luci2.wireless.getAssocList(names[i]); return _luci2.rpc.flush(); }).then(function(assoclists) { var rv = [ ]; for (var i = 0; i < assoclists.length; i++) for (var j = 0; j < assoclists[i].length; j++) rv.push(assoclists[i][j]); return rv; }); }, formatEncryption: function(enc) { var format_list = function(l, s) { var rv = [ ]; for (var i = 0; i < l.length; i++) rv.push(l[i].toUpperCase()); return rv.join(s ? s : ', '); } if (!enc || !enc.enabled) return _luci2.tr('None'); if (enc.wep) { if (enc.wep.length == 2) return _luci2.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, ', ')); else return _luci2.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( format_list(enc.authentication, '/'), format_list(enc.ciphers, ', ') ); else if (enc.wpa[0] == 2) return 'WPA2 %s (%s)'.format( format_list(enc.authentication, '/'), format_list(enc.ciphers, ', ') ); else return 'WPA %s (%s)'.format( format_list(enc.authentication, '/'), format_list(enc.ciphers, ', ') ); } return _luci2.tr('Unknown'); } }; this.firewall = { getZoneColor: function(zone) { if ($.isPlainObject(zone)) zone = zone.name; if (zone == 'lan') return '#90f090'; else if (zone == 'wan') return '#f09090'; for (var i = 0, hash = 0; i < zone.length; hash = zone.charCodeAt(i++) + ((hash << 5) - hash)); for (var i = 0, color = '#'; i < 3; color += ('00' + ((hash >> i++ * 8) & 0xFF).tostring(16)).slice(-2)); return color; }, findZoneByNetwork: function(network) { var self = this; var zone = undefined; return _luci2.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, _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); 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 = _luci2.NetworkModel; var deferreds = [ ]; for (var proto in self._cache.protolist) deferreds.push(self._fetch_protocol(proto)); return $.when.apply($, deferreds); }, _fetch_swstate: _luci2.rpc.declare({ object: 'luci2.network', method: 'switch_info', params: [ 'switch' ], expect: { 'info': { } } }), _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]; }, _fetch_cache_cb: function(level) { var self = _luci2.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) { _luci2.rpc.batch(); for (var i = 0; i < self._cache.swlist.length; i++) self._fetch_swstate(self._cache.swlist[i]); return _luci2.rpc.flush().then(self._fetch_swstate_cb); } return _luci2.deferrable(); } ); }, _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({ object: 'luci2.system', method: 'led_list', expect: { leds: [ ] } }), listUSBDevices: _luci2.rpc.declare({ object: 'luci2.system', method: 'usb_list', expect: { devices: [ ] } }), testUpgrade: _luci2.rpc.declare({ object: 'luci2.system', method: 'upgrade_test', expect: { '': { } } }), startUpgrade: _luci2.rpc.declare({ object: 'luci2.system', method: 'upgrade_start', params: [ 'keep' ] }), cleanUpgrade: _luci2.rpc.declare({ object: 'luci2.system', method: 'upgrade_clean' }), restoreBackup: _luci2.rpc.declare({ object: 'luci2.system', method: 'backup_restore' }), cleanBackup: _luci2.rpc.declare({ object: 'luci2.system', method: 'backup_clean' }), getBackupConfig: _luci2.rpc.declare({ object: 'luci2.system', method: 'backup_config_get', expect: { config: '' } }), setBackupConfig: _luci2.rpc.declare({ object: 'luci2.system', method: 'backup_config_set', params: [ 'data' ] }), listBackup: _luci2.rpc.declare({ object: 'luci2.system', method: 'backup_list', expect: { files: [ ] } }), 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' }) }; this.opkg = { updateLists: _luci2.rpc.declare({ object: 'luci2.opkg', method: 'update', expect: { '': { } } }), _allPackages: _luci2.rpc.declare({ object: 'luci2.opkg', method: 'list', params: [ 'offset', 'limit', 'pattern' ], expect: { '': { } } }), _installedPackages: _luci2.rpc.declare({ object: 'luci2.opkg', method: 'list_installed', params: [ 'offset', 'limit', 'pattern' ], expect: { '': { } } }), _findPackages: _luci2.rpc.declare({ object: 'luci2.opkg', method: 'find', params: [ 'offset', 'limit', 'pattern' ], expect: { '': { } } }), _fetchPackages: function(action, offset, limit, pattern) { var packages = [ ]; return action(offset, limit, pattern).then(function(list) { if (!list.total || !list.packages) return { length: 0, total: 0 }; packages.push.apply(packages, list.packages); packages.total = list.total; if (limit <= 0) limit = list.total; if (packages.length >= limit) return packages; _luci2.rpc.batch(); for (var i = offset + packages.length; i < limit; i += 100) action(i, (Math.min(i + 100, limit) % 100) || 100, pattern); return _luci2.rpc.flush(); }).then(function(lists) { for (var i = 0; i < lists.length; i++) { if (!lists[i].total || !lists[i].packages) continue; packages.push.apply(packages, lists[i].packages); packages.total = lists[i].total; } return packages; }); }, listPackages: function(offset, limit, pattern) { return _luci2.opkg._fetchPackages(_luci2.opkg._allPackages, offset, limit, pattern); }, installedPackages: function(offset, limit, pattern) { return _luci2.opkg._fetchPackages(_luci2.opkg._installedPackages, offset, limit, pattern); }, findPackages: function(offset, limit, pattern) { return _luci2.opkg._fetchPackages(_luci2.opkg._findPackages, offset, limit, pattern); }, installPackage: _luci2.rpc.declare({ object: 'luci2.opkg', method: 'install', params: [ 'package' ], expect: { '': { } } }), removePackage: _luci2.rpc.declare({ object: 'luci2.opkg', method: 'remove', params: [ 'package' ], expect: { '': { } } }), getConfig: _luci2.rpc.declare({ object: 'luci2.opkg', method: 'config_get', expect: { config: '' } }), setConfig: _luci2.rpc.declare({ object: 'luci2.opkg', method: 'config_set', params: [ 'data' ] }) }; this.session = { login: _luci2.rpc.declare({ object: 'session', method: 'login', params: [ 'username', 'password' ], expect: { '': { } } }), access: _luci2.rpc.declare({ object: 'session', method: 'access', params: [ 'scope', 'object', 'function' ], expect: { access: false } }), isAlive: function() { return _luci2.session.access('ubus', 'session', 'access'); }, startHeartbeat: function() { this._hearbeatInterval = window.setInterval(function() { _luci2.session.isAlive().then(function(alive) { if (!alive) { _luci2.session.stopHeartbeat(); _luci2.ui.login(true); } }); }, _luci2.globals.timeout * 2); }, stopHeartbeat: function() { if (typeof(this._hearbeatInterval) != 'undefined') { 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 = { saveScrollTop: function() { this._scroll_top = $(document).scrollTop(); }, restoreScrollTop: function() { if (typeof(this._scroll_top) == 'undefined') return; $(document).scrollTop(this._scroll_top); delete this._scroll_top; }, loading: function(enable) { var win = $(window); var body = $('body'); var state = _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 }) }); state.modal.modal(enable ? 'show' : 'hide'); }, dialog: function(title, content, options) { var win = $(window); var body = $('body'); var state = _luci2.ui._dialog || (_luci2.ui._dialog = { dialog: $('
') .addClass('modal fade') .append($('
') .addClass('modal-dialog') .append($('
') .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) { state.dialog.modal('hide'); return state.dialog; } var cnt = state.dialog.children().children().children('div.modal-body'); var ftr = state.dialog.children().children().children('div.modal-footer'); ftr.empty().show(); if (options.style == 'confirm') { ftr.append(_luci2.ui.button(_luci2.tr('Ok'), 'primary') .click(options.confirm || function() { _luci2.ui.dialog(false) })); ftr.append(_luci2.ui.button(_luci2.tr('Cancel'), 'default') .click(options.cancel || function() { _luci2.ui.dialog(false) })); } else if (options.style == '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(_luci2.ui.button(_luci2.tr('Close'), 'primary') .attr('disabled', true)); } if (options.wide) { state.dialog.addClass('wide'); } else { state.dialog.removeClass('wide'); } state.dialog.find('h4:first').text(title); state.dialog.modal('show'); cnt.empty().append(content); return state.dialog; }, upload: function(title, content, options) { var state = _luci2.ui._upload || (_luci2.ui._upload = { form: $('
') .attr('method', 'post') .attr('action', '/cgi-bin/luci-upload') .attr('enctype', 'multipart/form-data') .attr('target', 'cbi-fileupload-frame') .append($('

')) .append($('') .attr('type', 'hidden') .attr('name', 'sessionid')) .append($('') .attr('type', 'hidden') .attr('name', 'filename')) .append($('') .attr('type', 'file') .attr('name', 'filedata') .addClass('cbi-input-file')) .append($('

') .css('width', '100%') .addClass('progress progress-striped active') .append($('
') .addClass('progress-bar') .css('width', '100%'))) .append($('