summaryrefslogtreecommitdiffhomepage
path: root/modules/luci-base/htdocs/luci-static/resources/luci.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/luci-base/htdocs/luci-static/resources/luci.js')
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/luci.js418
1 files changed, 389 insertions, 29 deletions
diff --git a/modules/luci-base/htdocs/luci-static/resources/luci.js b/modules/luci-base/htdocs/luci-static/resources/luci.js
index 8d7f88d77b..5fb0bcfe79 100644
--- a/modules/luci-base/htdocs/luci-static/resources/luci.js
+++ b/modules/luci-base/htdocs/luci-static/resources/luci.js
@@ -131,6 +131,298 @@
}
});
+
+ /*
+ * HTTP Request helper
+ */
+
+ Headers = Class.extend({
+ __name__: 'LuCI.XHR.Headers',
+ __init__: function(xhr) {
+ var hdrs = this.headers = {};
+ xhr.getAllResponseHeaders().split(/\r\n/).forEach(function(line) {
+ var m = /^([^:]+):(.*)$/.exec(line);
+ if (m != null)
+ hdrs[m[1].trim().toLowerCase()] = m[2].trim();
+ });
+ },
+
+ has: function(name) {
+ return this.headers.hasOwnProperty(String(name).toLowerCase());
+ },
+
+ get: function(name) {
+ var key = String(name).toLowerCase();
+ return this.headers.hasOwnProperty(key) ? this.headers[key] : null;
+ }
+ });
+
+ Response = Class.extend({
+ __name__: 'LuCI.XHR.Response',
+ __init__: function(xhr, url, duration) {
+ this.ok = (xhr.status >= 200 && xhr.status <= 299);
+ this.status = xhr.status;
+ this.statusText = xhr.statusText;
+ this.responseText = xhr.responseText;
+ this.headers = new Headers(xhr);
+ this.duration = duration;
+ this.url = url;
+ this.xhr = xhr;
+ },
+
+ json: function() {
+ return JSON.parse(this.responseText);
+ },
+
+ text: function() {
+ return this.responseText;
+ }
+ });
+
+ Request = Class.singleton({
+ __name__: 'LuCI.Request',
+
+ interceptors: [],
+
+ request: function(target, options) {
+ var state = { xhr: new XMLHttpRequest(), url: target, start: Date.now() },
+ opt = Object.assign({}, options, state),
+ content = null,
+ contenttype = null,
+ callback = this.handleReadyStateChange;
+
+ return new Promise(function(resolveFn, rejectFn) {
+ opt.xhr.onreadystatechange = callback.bind(opt, resolveFn, rejectFn);
+ opt.method = String(opt.method || 'GET').toUpperCase();
+
+ if ('query' in opt) {
+ var q = (opt.query != null) ? Object.keys(opt.query).map(function(k) {
+ if (opt.query[k] != null) {
+ var v = (typeof(opt.query[k]) == 'object')
+ ? JSON.stringify(opt.query[k])
+ : String(opt.query[k]);
+
+ return '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(v));
+ }
+ else {
+ return encodeURIComponent(k);
+ }
+ }).join('&') : '';
+
+ if (q !== '') {
+ switch (opt.method) {
+ case 'GET':
+ case 'HEAD':
+ case 'OPTIONS':
+ opt.url += ((/\?/).test(opt.url) ? '&' : '?') + q;
+ break;
+
+ default:
+ if (content == null) {
+ content = q;
+ contenttype = 'application/x-www-form-urlencoded';
+ }
+ }
+ }
+ }
+
+ if (!opt.cache)
+ opt.url += ((/\?/).test(opt.url) ? '&' : '?') + (new Date()).getTime();
+
+ if (!/^(?:[^/]+:)?\/\//.test(opt.url))
+ opt.url = location.protocol + '//' + location.host + opt.url;
+
+ if ('username' in opt && 'password' in opt)
+ opt.xhr.open(opt.method, opt.url, true, opt.username, opt.password);
+ else
+ opt.xhr.open(opt.method, opt.url, true);
+
+ opt.xhr.responseType = 'text';
+ opt.xhr.overrideMimeType('application/octet-stream');
+
+ if ('timeout' in opt)
+ opt.xhr.timeout = +opt.timeout;
+
+ if ('credentials' in opt)
+ opt.xhr.withCredentials = !!opt.credentials;
+
+ if (opt.content != null) {
+ switch (typeof(opt.content)) {
+ case 'function':
+ content = opt.content(xhr);
+ break;
+
+ case 'object':
+ content = JSON.stringify(opt.content);
+ contenttype = 'application/json';
+ break;
+
+ default:
+ content = String(opt.content);
+ }
+ }
+
+ if ('headers' in opt)
+ for (var header in opt.headers)
+ if (opt.headers.hasOwnProperty(header)) {
+ if (header.toLowerCase() != 'content-type')
+ opt.xhr.setRequestHeader(header, opt.headers[header]);
+ else
+ contenttype = opt.headers[header];
+ }
+
+ if (contenttype != null)
+ opt.xhr.setRequestHeader('Content-Type', contenttype);
+
+ try {
+ opt.xhr.send(content);
+ }
+ catch (e) {
+ rejectFn.call(opt, e);
+ }
+ });
+ },
+
+ handleReadyStateChange: function(resolveFn, rejectFn, ev) {
+ var xhr = this.xhr;
+
+ if (xhr.readyState !== 4)
+ return;
+
+ if (xhr.status === 0 && xhr.statusText === '') {
+ rejectFn.call(this, new Error('XHR request aborted by browser'));
+ }
+ else {
+ var response = new Response(
+ xhr, xhr.responseURL || this.url, Date.now() - this.start);
+
+ 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 this.request(url, Object.assign({ method: 'GET' }, options));
+ },
+
+ post: function(url, data, options) {
+ return this.request(url, Object.assign({ method: 'POST', content: data }, options));
+ },
+
+ addInterceptor: function(interceptorFn) {
+ if (typeof(interceptorFn) == 'function')
+ this.interceptors.push(interceptorFn);
+ return interceptorFn;
+ },
+
+ removeInterceptor: function(interceptorFn) {
+ var oldlen = this.interceptors.length, i = oldlen;
+ while (i--)
+ if (this.interceptors[i] === interceptorFn)
+ this.interceptors.splice(i, 1);
+ return (this.interceptors.length < oldlen);
+ },
+
+ poll: Class.singleton({
+ __name__: 'LuCI.Request.Poll',
+
+ queue: [],
+
+ add: function(interval, url, options, callback) {
+ if (isNaN(interval) || interval <= 0)
+ throw new TypeError('Invalid poll interval');
+
+ var e = {
+ interval: interval,
+ url: url,
+ options: options,
+ callback: callback
+ };
+
+ this.queue.push(e);
+ return e;
+ },
+
+ remove: function(entry) {
+ var oldlen = this.queue.length, i = oldlen;
+
+ while (i--)
+ if (this.queue[i] === entry) {
+ delete this.queue[i].running;
+ this.queue.splice(i, 1);
+ }
+
+ if (!this.queue.length)
+ this.stop();
+
+ return (this.queue.length < oldlen);
+ },
+
+ start: function() {
+ if (!this.queue.length || this.active())
+ return false;
+
+ this.tick = 0;
+ this.timer = window.setInterval(this.step, 1000);
+ this.step();
+ document.dispatchEvent(new CustomEvent('poll-start'));
+ return true;
+ },
+
+ stop: function() {
+ if (!this.active())
+ return false;
+
+ document.dispatchEvent(new CustomEvent('poll-stop'));
+ window.clearInterval(this.timer);
+ delete this.timer;
+ delete this.tick;
+ return true;
+ },
+
+ step: function() {
+ Request.poll.queue.forEach(function(e) {
+ if ((Request.poll.tick % e.interval) != 0)
+ return;
+
+ if (e.running)
+ return;
+
+ var opts = Object.assign({}, e.options,
+ { timeout: e.interval * 1000 - 5 });
+
+ e.running = true;
+ Request.request(e.url, opts)
+ .then(function(res) {
+ if (!e.running || !Request.poll.active())
+ return;
+
+ try {
+ e.callback(res, res.json(), res.duration);
+ }
+ catch (err) {
+ e.callback(res, null, res.duration);
+ }
+ })
+ .finally(function() { delete e.running });
+ });
+
+ Request.poll.tick = (Request.poll.tick + 1) % Math.pow(2, 32);
+ },
+
+ active: function() {
+ return (this.timer != null);
+ }
+ })
+ });
+
+
var modalDiv = null,
tooltipDiv = null,
tooltipTimeout = null,
@@ -167,48 +459,41 @@
/* HTTP resource fetching */
get: function(url, args, cb) {
- return this.poll(0, url, args, cb, false);
+ return this.poll(null, url, args, cb, false);
},
post: function(url, args, cb) {
- return this.poll(0, url, args, cb, true);
+ return this.poll(null, url, args, cb, true);
},
poll: function(interval, url, args, cb, post) {
- var data = post ? { token: this.env.token } : null;
+ if (interval !== null && interval <= 0)
+ interval = this.env.pollinterval;
+
+ var data = post ? { token: this.env.token } : null,
+ method = post ? 'POST' : 'GET';
if (!/^(?:\/|\S+:\/\/)/.test(url))
url = this.url(url);
- if (typeof(args) === 'object' && args !== null) {
- data = data || {};
-
- for (var key in args)
- if (args.hasOwnProperty(key))
- switch (typeof(args[key])) {
- case 'string':
- case 'number':
- case 'boolean':
- data[key] = args[key];
- break;
-
- case 'object':
- data[key] = JSON.stringify(args[key]);
- break;
- }
- }
+ if (args != null)
+ data = Object.assign(data || {}, args);
- if (interval > 0)
- return XHR.poll(interval, url, data, cb, post);
- else if (post)
- return XHR.post(url, data, cb);
+ if (interval !== null)
+ return Request.poll.add(interval, url, { method: method, query: data }, cb);
else
- return XHR.get(url, data, cb);
+ return Request.request(url, { method: method, query: data })
+ .then(function(res) {
+ var json = null;
+ if (/^application\/json\b/.test(res.headers.get('Content-Type')))
+ try { json = res.json() } catch(e) {}
+ cb(res.xhr, json, res.duration);
+ });
},
- stop: function(entry) { XHR.stop(entry) },
- halt: function() { XHR.halt() },
- run: function() { XHR.run() },
+ stop: function(entry) { return Request.poll.remove(entry) },
+ halt: function() { return Request.poll.stop() },
+ run: function() { return Request.poll.start() },
/* Modal dialog */
@@ -312,7 +597,8 @@
return node;
},
- Class: Class
+ Class: Class,
+ Request: Request
};
/* Tabs */
@@ -622,6 +908,30 @@
/* Setup */
LuCI.prototype.setupDOM = function(ev) {
this.tabs.init();
+
+ Request.addInterceptor(function(res) {
+ if (res.status != 403 || res.headers.get('X-LuCI-Login-Required') != 'yes')
+ return;
+
+ Request.poll.stop();
+
+ L.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…')))
+ ]);
+
+ return Promise.reject(new Error('Session expired'));
+ });
+
+ Request.poll.start();
};
function LuCI(env) {
@@ -638,8 +948,58 @@
document.addEventListener('focus', this.showTooltip.bind(this), true);
document.addEventListener('blur', this.hideTooltip.bind(this), true);
+ document.addEventListener('poll-start', function(ev) {
+ document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
+ e.style.display = (e.id == 'xhr_poll_status_off') ? 'none' : '';
+ });
+ });
+
+ document.addEventListener('poll-stop', function(ev) {
+ document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
+ e.style.display = (e.id == 'xhr_poll_status_on') ? 'none' : '';
+ });
+ });
+
document.addEventListener('DOMContentLoaded', this.setupDOM.bind(this));
}
+ XHR = Class.extend({
+ __name__: 'LuCI.XHR',
+ __init__: function() {
+ if (window.console && console.debug)
+ console.debug('Direct use XHR() is deprecated, please use L.Request instead');
+ },
+
+ _response: function(cb, res, json, duration) {
+ if (this.active)
+ cb(res, json, duration);
+ delete this.active;
+ },
+
+ get: function(url, data, callback, timeout) {
+ this.active = true;
+ L.get(url, data, this._response.bind(this, callback), timeout);
+ },
+
+ post: function(url, data, callback, timeout) {
+ this.active = true;
+ L.post(url, data, this._response.bind(this, callback), timeout);
+ },
+
+ cancel: function() { delete this.active },
+ busy: function() { return (this.active === true) },
+ abort: function() {},
+ send_form: function() { throw 'Not implemented' },
+ });
+
+ XHR.get = function() { return window.L.get.apply(window.L, arguments) };
+ XHR.post = function() { return window.L.post.apply(window.L, arguments) };
+ XHR.poll = function() { return window.L.poll.apply(window.L, arguments) };
+ XHR.stop = Request.poll.remove.bind(Request.poll);
+ XHR.halt = Request.poll.stop.bind(Request.poll);
+ XHR.run = Request.poll.start.bind(Request.poll);
+ XHR.running = Request.poll.active.bind(Request.poll);
+
+ window.XHR = XHR;
window.LuCI = LuCI;
})(window, document);