});
}
+ /* Promise.finally polyfill */
+ if (typeof Promise.prototype.finally !== 'function') {
+ Promise.prototype.finally = function(fn) {
+ var onFinally = function(cb) {
+ return Promise.resolve(fn.call(this)).then(cb);
+ };
+
+ return this.then(
+ function(result) { return onFinally.call(this, function() { return result }) },
+ function(reason) { return onFinally.call(this, function() { return Promise.reject(reason) }) }
+ );
+ };
+ }
+
/*
* Class declaration and inheritance helper
*/
});
- var requestQueue = [],
- rpcBaseURL = null;
+ var requestQueue = [];
function isQueueableRequest(opt) {
if (!classes.rpc)
if (opt.nobatch === true)
return false;
- if (rpcBaseURL == null)
- rpcBaseURL = Request.expandURL(classes.rpc.getBaseURL());
+ var rpcBaseURL = Request.expandURL(classes.rpc.getBaseURL());
return (rpcBaseURL != null && opt.url.indexOf(rpcBaseURL) == 0);
}
opt.xhr.open(opt.method, opt.url, true);
opt.xhr.responseType = 'text';
- opt.xhr.overrideMimeType('application/octet-stream');
+
+ if ('overrideMimeType' in opt.xhr)
+ opt.xhr.overrideMimeType('application/octet-stream');
if ('timeout' in opt)
opt.xhr.timeout = +opt.timeout;
break;
case 'object':
- content = JSON.stringify(opt.content);
- contenttype = 'application/json';
+ if (!(opt.content instanceof FormData)) {
+ content = JSON.stringify(opt.content);
+ contenttype = 'application/json';
+ }
+ else {
+ content = opt.content;
+ }
break;
default:
contenttype = opt.headers[header];
}
+ if ('progress' in opt && 'upload' in opt.xhr)
+ opt.xhr.upload.addEventListener('progress', opt.progress);
+
if (contenttype != null)
opt.xhr.setRequestHeader('Content-Type', contenttype);
},
handleReadyStateChange: function(resolveFn, rejectFn, ev) {
- var xhr = this.xhr;
+ var xhr = this.xhr,
+ duration = Date.now() - this.start;
if (xhr.readyState !== 4)
return;
if (xhr.status === 0 && xhr.statusText === '') {
- rejectFn.call(this, new Error('XHR request aborted by browser'));
+ if (duration >= this.timeout)
+ rejectFn.call(this, new Error('XHR request timed out'));
+ else
+ rejectFn.call(this, new Error('XHR request aborted by browser'));
}
else {
var response = new Response(
- xhr, xhr.responseURL || this.url, Date.now() - this.start);
+ xhr, xhr.responseURL || this.url, duration);
Promise.all(Request.interceptors.map(function(fn) { return fn(response) }))
.then(resolveFn.bind(this, response))
.catch(rejectFn.bind(this));
}
-
- try {
- xhr.abort();
- }
- catch(e) {}
},
get: function(url, options) {
return true;
},
- remove: function(entry) {
+ remove: function(fn) {
if (typeof(fn) != 'function')
throw new TypeError('Invalid argument to LuCI.Poll.remove()');
var dummyElem = null,
domParser = null,
originalCBIInit = null,
+ rpcBaseURL = null,
+ sysFeatures = null,
classes = {};
var LuCI = Class.extend({
__init__: function(env) {
document.querySelectorAll('script[src*="/luci.js"]').forEach(function(s) {
- if (env.base_url == null || env.base_url == '')
- env.base_url = s.getAttribute('src').replace(/\/luci\.js(?:\?v=[^?]+)?$/, '');
+ if (env.base_url == null || env.base_url == '') {
+ var m = (s.getAttribute('src') || '').match(/^(.*)\/luci\.js(?:\?v=([^?]+))?$/);
+ if (m) {
+ env.base_url = m[1];
+ env.resource_version = m[2];
+ }
+ }
});
if (env.base_url == null)
Promise.all([
domReady,
this.require('ui'),
- this.require('form')
+ this.require('rpc'),
+ this.require('form'),
+ this.probeRPCBaseURL()
]).then(this.setupDOM.bind(this)).catch(this.error);
originalCBIInit = window.cbi_init;
if (type instanceof Error) {
e = type;
- stack = (e.stack || '').split(/\n/);
if (msg)
e.message = msg + ': ' + e.message;
}
else {
+ try { throw new Error('stacktrace') }
+ catch (e2) { stack = (e2.stack || '').split(/\n/) }
+
e = new (window[type || 'Error'] || Error)(msg || 'Unspecified error');
e.name = type || 'Error';
}
+ stack = (stack || []).map(function(frame) {
+ frame = frame.replace(/(.*?)@(.+):(\d+):(\d+)/g, 'at $1 ($2:$3:$4)').trim();
+ return frame ? ' ' + frame : '';
+ });
+
+ if (!/^ at /.test(stack[0]))
+ stack.shift();
+
+ if (/\braise /.test(stack[0]))
+ stack.shift();
+
+ if (/\berror /.test(stack[0]))
+ stack.shift();
+
+ if (stack.length)
+ e.message += '\n' + stack.join('\n');
+
if (window.console && console.debug)
console.debug(e);
L.raise.apply(L, Array.prototype.slice.call(arguments));
}
catch (e) {
- var stack = (e.stack || '').split(/\n/).map(function(frame) {
- frame = frame.replace(/(.*?)@(.+):(\d+):(\d+)/g, 'at $1 ($2:$3:$4)').trim();
- return frame ? ' ' + frame : '';
- });
-
- if (!/^ at /.test(stack[0]))
- stack.shift();
-
- if (/\braise /.test(stack[0]))
- stack.shift();
-
- if (/\berror /.test(stack[0]))
- stack.shift();
-
- stack = stack.length ? '\n' + stack.join('\n') : '';
+ if (!e.reported) {
+ if (L.ui)
+ L.ui.addNotification(e.name || _('Runtime error'),
+ E('pre', {}, e.message), 'danger');
+ else
+ L.dom.content(document.querySelector('#maincontent'),
+ E('pre', { 'class': 'alert-message error' }, e.message));
- if (L.ui)
- L.ui.showModal(e.name || _('Runtime error'),
- E('pre', { 'class': 'alert-message error' }, e.message + stack));
- else
- L.dom.content(document.querySelector('#maincontent'),
- E('pre', { 'class': 'alert-message error' }, e + stack));
+ e.reported = true;
+ }
throw e;
}
return classes[name];
}
- url = '%s/%s.js'.format(L.env.base_url, name.replace(/\./g, '/'));
+ url = '%s/%s.js%s'.format(L.env.base_url, name.replace(/\./g, '/'), (L.env.resource_version ? '?v=' + L.env.resource_version : ''));
from = [ name ].concat(from);
var compileClass = function(res) {
},
/* DOM setup */
- setupDOM: function(ev) {
- Request.addInterceptor(function(res) {
- if (res.status != 403 || res.headers.get('X-LuCI-Login-Required') != 'yes')
+ probeRPCBaseURL: function() {
+ if (rpcBaseURL == null) {
+ try {
+ rpcBaseURL = window.sessionStorage.getItem('rpcBaseURL');
+ }
+ catch (e) { }
+ }
+
+ if (rpcBaseURL == null) {
+ var rpcFallbackURL = this.url('admin/ubus');
+
+ rpcBaseURL = Request.get('/ubus/').then(function(res) {
+ return (rpcBaseURL = (res.status == 400) ? '/ubus/' : rpcFallbackURL);
+ }, function() {
+ return (rpcBaseURL = rpcFallbackURL);
+ }).then(function(url) {
+ try {
+ window.sessionStorage.setItem('rpcBaseURL', url);
+ }
+ catch (e) { }
+
+ return url;
+ });
+ }
+
+ return Promise.resolve(rpcBaseURL);
+ },
+
+ probeSystemFeatures: function() {
+ var sessionid = classes.rpc.getSessionID();
+
+ if (sysFeatures == null) {
+ try {
+ var data = JSON.parse(window.sessionStorage.getItem('sysFeatures'));
+
+ if (this.isObject(data) && this.isObject(data[sessionid]))
+ sysFeatures = data[sessionid];
+ }
+ catch (e) {}
+ }
+
+ if (!this.isObject(sysFeatures)) {
+ sysFeatures = classes.rpc.declare({
+ object: 'luci',
+ method: 'getFeatures',
+ expect: { '': {} }
+ })().then(function(features) {
+ try {
+ var data = {};
+ data[sessionid] = features;
+
+ window.sessionStorage.setItem('sysFeatures', JSON.stringify(data));
+ }
+ catch (e) {}
+
+ sysFeatures = features;
+
+ return features;
+ });
+ }
+
+ return Promise.resolve(sysFeatures);
+ },
+
+ hasSystemFeature: function() {
+ var ft = sysFeatures[arguments[0]];
+
+ if (arguments.length == 2)
+ return this.isObject(ft) ? ft[arguments[1]] : null;
+
+ return (ft != null && ft != false);
+ },
+
+ notifySessionExpiry: function() {
+ Poll.stop();
+
+ L.ui.showModal(_('Session expired'), [
+ E('div', { class: 'alert-message warning' },
+ _('A new login is required since the authentication session expired.')),
+ E('div', { class: 'right' },
+ E('div', {
+ class: 'btn primary',
+ click: function() {
+ var loc = window.location;
+ window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
+ }
+ }, _('To login…')))
+ ]);
+
+ L.raise('SessionError', 'Login session is expired');
+ },
+
+ setupDOM: function(res) {
+ var domEv = res[0],
+ uiClass = res[1],
+ rpcClass = res[2],
+ formClass = res[3],
+ rpcBaseURL = res[4];
+
+ rpcClass.setBaseURL(rpcBaseURL);
+
+ rpcClass.addInterceptor(function(msg, req) {
+ if (!L.isObject(msg) || !L.isObject(msg.error) || msg.error.code != -32002)
return;
- Poll.stop();
-
- L.ui.showModal(_('Session expired'), [
- E('div', { class: 'alert-message warning' },
- _('A new login is required since the authentication session expired.')),
- E('div', { class: 'right' },
- E('div', {
- class: 'btn primary',
- click: function() {
- var loc = window.location;
- window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
- }
- }, _('To login…')))
- ]);
+ if (!L.isObject(req) || (req.object == 'session' && req.method == 'access'))
+ return;
- throw 'Session expired';
+ return rpcClass.declare({
+ 'object': 'session',
+ 'method': 'access',
+ 'params': [ 'scope', 'object', 'function' ],
+ 'expect': { access: true }
+ })('uci', 'luci', 'read').catch(L.notifySessionExpiry);
});
- originalCBIInit();
+ Request.addInterceptor(function(res) {
+ var isDenied = false;
- Poll.start();
+ if (res.status == 403 && res.headers.get('X-LuCI-Login-Required') == 'yes')
+ isDenied = true;
+
+ if (!isDenied)
+ return;
+ L.notifySessionExpiry();
+ });
+
+ return this.probeSystemFeatures().finally(this.initDOM);
+ },
+
+ initDOM: function() {
+ originalCBIInit();
+ Poll.start();
document.dispatchEvent(new CustomEvent('luci-loaded'));
},
},
+ /* Data helpers */
+ isObject: function(val) {
+ return (val != null && typeof(val) == 'object');
+ },
+
+ sortedKeys: function(obj, key, sortmode) {
+ if (obj == null || typeof(obj) != 'object')
+ return [];
+
+ return Object.keys(obj).map(function(e) {
+ var v = (key != null) ? obj[e][key] : e;
+
+ switch (sortmode) {
+ case 'addr':
+ v = (v != null) ? v.replace(/(?:^|[.:])([0-9a-fA-F]{1,4})/g,
+ function(m0, m1) { return ('000' + m1.toLowerCase()).substr(-4) }) : null;
+ break;
+
+ case 'num':
+ v = (v != null) ? +v : null;
+ break;
+ }
+
+ return [ e, v ];
+ }).filter(function(e) {
+ return (e[1] != null);
+ }).sort(function(a, b) {
+ return (a[1] > b[1]);
+ }).map(function(e) {
+ return e[0];
+ });
+ },
+
+ toArray: function(val) {
+ if (val == null)
+ return [];
+ else if (Array.isArray(val))
+ return val;
+ else if (typeof(val) == 'object')
+ return [ val ];
+
+ var s = String(val).trim();
+
+ if (s == '')
+ return [];
+
+ return s.split(/\s+/);
+ },
+
+
/* HTTP resource fetching */
get: function(url, args, cb) {
return this.poll(null, url, args, cb, false);
return null;
return inst[method].apply(inst, inst.varargs(arguments, 2));
+ },
+
+ isEmpty: function(node, ignoreFn) {
+ for (var child = node.firstElementChild; child != null; child = child.nextElementSibling)
+ if (!child.classList.contains('hidden') && (!ignoreFn || !ignoreFn(child)))
+ return false;
+
+ return true;
}
}),
},
addFooter: function() {
- var footer = E([]),
- mc = document.getElementById('maincontent');
+ var footer = E([]);
- if (mc.querySelector('.cbi-map')) {
+ if (this.handleSaveApply || this.handleSave || this.handleReset) {
footer.appendChild(E('div', { 'class': 'cbi-page-actions' }, [
- E('input', {
+ this.handleSaveApply ? E('button', {
'class': 'cbi-button cbi-button-apply',
- 'type': 'button',
- 'value': _('Save & Apply'),
- 'click': L.bind(this.handleSaveApply, this)
- }), ' ',
- E('input', {
+ 'click': L.ui.createHandlerFn(this, 'handleSaveApply')
+ }, [ _('Save & Apply') ]) : '', ' ',
+ this.handleSave ? E('button', {
'class': 'cbi-button cbi-button-save',
- 'type': 'submit',
- 'value': _('Save'),
- 'click': L.bind(this.handleSave, this)
- }), ' ',
- E('input', {
+ 'click': L.ui.createHandlerFn(this, 'handleSave')
+ }, [ _('Save') ]) : '', ' ',
+ this.handleReset ? E('button', {
'class': 'cbi-button cbi-button-reset',
- 'type': 'button',
- 'value': _('Reset'),
- 'click': L.bind(this.handleReset, this)
- })
+ 'click': L.ui.createHandlerFn(this, 'handleReset')
+ }, [ _('Reset') ]) : ''
]));
}