summaryrefslogtreecommitdiffhomepage
path: root/modules/luci-base
diff options
context:
space:
mode:
authorJo-Philipp Wich <jo@mein.io>2019-01-06 17:08:37 +0100
committerJo-Philipp Wich <jo@mein.io>2019-07-07 15:25:49 +0200
commit843031f8197aa2c86585360c0f6d8a3b1ee02dee (patch)
tree935aeb6098b3cd7f94b61426a8b98a0869de1366 /modules/luci-base
parentfee8c1fc9c9b1334ada08e94d4b75c9d418c098b (diff)
luci-base: luci.js: add HTTP request functions
Add a fetch() inspired HTTP request utility class to luci.js and replace the old xhr.js class with a stub using the new request api. Signed-off-by: Jo-Philipp Wich <jo@mein.io>
Diffstat (limited to 'modules/luci-base')
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/luci.js418
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/xhr.js251
2 files changed, 390 insertions, 279 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);
diff --git a/modules/luci-base/htdocs/luci-static/resources/xhr.js b/modules/luci-base/htdocs/luci-static/resources/xhr.js
index 3133898b5e..10bc88e1f4 100644
--- a/modules/luci-base/htdocs/luci-static/resources/xhr.js
+++ b/modules/luci-base/htdocs/luci-static/resources/xhr.js
@@ -1,250 +1 @@
-/*
- * xhr.js - XMLHttpRequest helper class
- * (c) 2008-2018 Jo-Philipp Wich <jo@mein.io>
- */
-
-XHR.prototype = {
- _encode: function(obj) {
- obj = obj ? obj : { };
- obj['_'] = Math.random();
-
- if (typeof obj == 'object') {
- var code = '';
- var self = this;
-
- for (var k in obj)
- code += (code ? '&' : '') +
- k + '=' + encodeURIComponent(obj[k]);
-
- return code;
- }
-
- return obj;
- },
-
- _response: function(callback, ts) {
- if (this._xmlHttp.readyState !== 4)
- return;
-
- var status = this._xmlHttp.status,
- login = this._xmlHttp.getResponseHeader("X-LuCI-Login-Required"),
- type = this._xmlHttp.getResponseHeader("Content-Type"),
- json = null;
-
- if (status === 403 && login === 'yes') {
- XHR.halt();
-
- 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…')))
- ]);
- }
- else if (type && type.toLowerCase().match(/^application\/json\b/)) {
- try {
- json = JSON.parse(this._xmlHttp.responseText);
- }
- catch(e) {
- json = null;
- }
- }
-
- callback(this._xmlHttp, json, Date.now() - ts);
- },
-
- busy: function() {
- if (!this._xmlHttp)
- return false;
-
- switch (this._xmlHttp.readyState)
- {
- case 1:
- case 2:
- case 3:
- return true;
-
- default:
- return false;
- }
- },
-
- abort: function() {
- if (this.busy())
- this._xmlHttp.abort();
- },
-
- get: function(url, data, callback, timeout) {
- this._xmlHttp = new XMLHttpRequest();
-
- var xhr = this._xmlHttp,
- code = this._encode(data);
-
- url = location.protocol + '//' + location.host + url;
-
- if (code)
- if (url.substr(url.length-1,1) == '&')
- url += code;
- else
- url += '?' + code;
-
- xhr.open('GET', url, true);
-
- if (!isNaN(timeout))
- xhr.timeout = timeout;
-
- xhr.onreadystatechange = this._response.bind(this, callback, Date.now());
- xhr.send(null);
- },
-
- post: function(url, data, callback, timeout) {
- this._xmlHttp = new XMLHttpRequest();
-
- var xhr = this._xmlHttp,
- code = this._encode(data);
-
- xhr.open('POST', url, true);
-
- if (!isNaN(timeout))
- xhr.timeout = timeout;
-
- xhr.onreadystatechange = this._response.bind(this, callback, Date.now());
- xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
- xhr.send(code);
- },
-
- cancel: function() {
- this._xmlHttp.onreadystatechange = function() {};
- this._xmlHttp.abort();
- },
-
- send_form: function(form, callback, extra_values) {
- var code = '';
-
- for (var i = 0; i < form.elements.length; i++) {
- var e = form.elements[i];
-
- if (e.options) {
- code += (code ? '&' : '') +
- form.elements[i].name + '=' + encodeURIComponent(
- e.options[e.selectedIndex].value
- );
- }
- else if (e.length) {
- for (var j = 0; j < e.length; j++)
- if (e[j].name) {
- code += (code ? '&' : '') +
- e[j].name + '=' + encodeURIComponent(e[j].value);
- }
- }
- else {
- code += (code ? '&' : '') +
- e.name + '=' + encodeURIComponent(e.value);
- }
- }
-
- if (typeof extra_values == 'object')
- for (var key in extra_values)
- code += (code ? '&' : '') +
- key + '=' + encodeURIComponent(extra_values[key]);
-
- return (form.method == 'get'
- ? this.get(form.getAttribute('action'), code, callback)
- : this.post(form.getAttribute('action'), code, callback));
- }
-}
-
-XHR.get = function(url, data, callback) {
- (new XHR()).get(url, data, callback);
-}
-
-XHR.post = function(url, data, callback) {
- (new XHR()).post(url, data, callback);
-}
-
-XHR.poll = function(interval, url, data, callback, post) {
- if (isNaN(interval) || interval <= 0)
- interval = L.env.pollinterval;
-
- if (!XHR._q) {
- XHR._t = 0;
- XHR._q = [ ];
- XHR._r = function() {
- for (var i = 0, e = XHR._q[0]; i < XHR._q.length; e = XHR._q[++i])
- {
- if (!(XHR._t % e.interval) && !e.xhr.busy())
- e.xhr[post ? 'post' : 'get'](e.url, e.data, e.callback, e.interval * 1000 * 5 - 5);
- }
-
- XHR._t++;
- };
- }
-
- var e = {
- interval: interval,
- callback: callback,
- url: url,
- data: data,
- xhr: new XHR()
- };
-
- XHR._q.push(e);
-
- return e;
-}
-
-XHR.stop = function(e) {
- for (var i = 0; XHR._q && XHR._q[i]; i++) {
- if (XHR._q[i] === e) {
- e.xhr.cancel();
- XHR._q.splice(i, 1);
- return true;
- }
- }
-
- return false;
-}
-
-XHR.halt = function() {
- if (XHR._i) {
- /* show & set poll indicator */
- try {
- document.getElementById('xhr_poll_status').style.display = '';
- document.getElementById('xhr_poll_status_on').style.display = 'none';
- document.getElementById('xhr_poll_status_off').style.display = '';
- } catch(e) { }
-
- window.clearInterval(XHR._i);
- XHR._i = null;
- }
-}
-
-XHR.run = function() {
- if (XHR._r && !XHR._i) {
- /* show & set poll indicator */
- try {
- document.getElementById('xhr_poll_status').style.display = '';
- document.getElementById('xhr_poll_status_on').style.display = '';
- document.getElementById('xhr_poll_status_off').style.display = 'none';
- } catch(e) { }
-
- /* kick first round manually to prevent one second lag when setting up
- * the poll interval */
- XHR._r();
- XHR._i = window.setInterval(XHR._r, 1000);
- }
-}
-
-XHR.running = function() {
- return !!(XHR._r && XHR._i);
-}
-
-function XHR() {}
-
-document.addEventListener('DOMContentLoaded', XHR.run);
+/* replaced by luci.js */