luci/modules/luci-base/htdocs/luci-static/resources/luci.js
Jo-Philipp Wich 5b7924c808 luci-base: luci.js: tweak error handling
If the ui class is loaded, use its modalDialog facility to display runtime
errors, alternatively render them into the maincontent area.

Also prevent duplication of stack trace information and throw a low level
error on session expiration to allow higher layers to properly handle it.

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
2019-07-07 15:25:49 +02:00

1061 lines
26 KiB
JavaScript

(function(window, document, undefined) {
/* Object.assign polyfill for IE */
if (typeof Object.assign !== 'function') {
Object.defineProperty(Object, 'assign', {
value: function assign(target, varArgs) {
if (target == null)
throw new TypeError('Cannot convert undefined or null to object');
var to = Object(target);
for (var index = 1; index < arguments.length; index++)
if (arguments[index] != null)
for (var nextKey in arguments[index])
if (Object.prototype.hasOwnProperty.call(arguments[index], nextKey))
to[nextKey] = arguments[index][nextKey];
return to;
},
writable: true,
configurable: true
});
}
/*
* Class declaration and inheritance helper
*/
var toCamelCase = function(s) {
return s.replace(/(?:^|[\. -])(.)/g, function(m0, m1) { return m1.toUpperCase() });
};
var superContext = null, Class = Object.assign(function() {}, {
extend: function(properties) {
var props = {
__base__: { value: this.prototype },
__name__: { value: properties.__name__ || 'anonymous' }
};
var ClassConstructor = function() {
if (!(this instanceof ClassConstructor))
throw new TypeError('Constructor must not be called without "new"');
if (Object.getPrototypeOf(this).hasOwnProperty('__init__')) {
if (typeof(this.__init__) != 'function')
throw new TypeError('Class __init__ member is not a function');
this.__init__.apply(this, arguments)
}
else {
this.super('__init__', arguments);
}
};
for (var key in properties)
if (!props[key] && properties.hasOwnProperty(key))
props[key] = { value: properties[key], writable: true };
ClassConstructor.prototype = Object.create(this.prototype, props);
ClassConstructor.prototype.constructor = ClassConstructor;
Object.assign(ClassConstructor, this);
ClassConstructor.displayName = toCamelCase(props.__name__.value + 'Class');
return ClassConstructor;
},
singleton: function(properties /*, ... */) {
return Class.extend(properties)
.instantiate(Class.prototype.varargs(arguments, 1));
},
instantiate: function(args) {
return new (Function.prototype.bind.apply(this,
Class.prototype.varargs(args, 0, null)))();
},
call: function(self, method) {
if (typeof(this.prototype[method]) != 'function')
throw new ReferenceError(method + ' is not defined in class');
return this.prototype[method].apply(self, self.varargs(arguments, 1));
},
isSubclass: function(_class) {
return (_class != null &&
typeof(_class) == 'function' &&
_class.prototype instanceof this);
},
prototype: {
varargs: function(args, offset /*, ... */) {
return Array.prototype.slice.call(arguments, 2)
.concat(Array.prototype.slice.call(args, offset));
},
super: function(key, callArgs) {
for (superContext = Object.getPrototypeOf(superContext ||
Object.getPrototypeOf(this));
superContext && !superContext.hasOwnProperty(key);
superContext = Object.getPrototypeOf(superContext)) { }
if (!superContext)
return null;
var res = superContext[key];
if (arguments.length > 1) {
if (typeof(res) != 'function')
throw new ReferenceError(key + ' is not a function in base class');
if (typeof(callArgs) != 'object')
callArgs = this.varargs(arguments, 1);
res = res.apply(this, callArgs);
}
superContext = null;
return res;
},
toString: function() {
var s = '[' + this.constructor.displayName + ']', f = true;
for (var k in this) {
if (this.hasOwnProperty(k)) {
s += (f ? ' {\n' : '') + ' ' + k + ': ' + typeof(this[k]) + '\n';
f = false;
}
}
return s + (f ? '' : '}');
}
}
});
/*
* 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 dummyElem = null,
domParser = null,
originalCBIInit = null,
classes = {};
LuCI = Class.extend({
__name__: 'LuCI',
__init__: function(env) {
Object.assign(this.env, env);
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' : '';
});
});
var domReady = new Promise(function(resolveFn, rejectFn) {
document.addEventListener('DOMContentLoaded', resolveFn);
});
Promise.all([
domReady,
this.require('ui')
]).then(this.setupDOM.bind(this)).catch(function(error) {
alert('LuCI class loading error:\n' + error);
});
originalCBIInit = window.cbi_init;
window.cbi_init = function() {};
},
error: function(type, fmt /*, ...*/) {
var e = null,
msg = fmt ? String.prototype.format.apply(fmt, this.varargs(arguments, 2)) : null,
stack = null;
if (type instanceof Error) {
e = type;
stack = (e.stack || '').split(/\n/);
if (msg)
e.message = msg + ': ' + e.message;
}
else {
e = new (window[type || 'Error'] || Error)(msg || 'Unspecified error');
e.name = type || 'Error';
try { throw new Error('stacktrace') }
catch (e2) { stack = (e2.stack || '').split(/\n/) }
/* IE puts the exception message into the first line */
if (stack[0] == 'Error: stacktrace')
stack.shift();
/* Pop L.error() invocation from stack */
stack.shift();
}
/* Append shortened & beautified stacktrace to message */
var trace = stack.join('\n')
.replace(/(.*?)@(.+):(\d+):(\d+)/g, ' at $1 ($2:$3:$4)');
if (e.message.indexOf(trace) == -1)
e.message += '\n' + trace;
if (window.console && console.debug)
console.debug(e);
if (this.ui)
this.ui.showModal(_('Runtime error'),
E('pre', { 'class': 'alert-message error' }, e));
else
L.dom.content(document.querySelector('#maincontent'),
E('pre', { 'class': 'alert-message error' }, e));
throw e;
},
bind: function(fn, self /*, ... */) {
return Function.prototype.bind.apply(fn, this.varargs(arguments, 2, self));
},
/* Class require */
require: function(name, from) {
var L = this, url = null, from = from || [];
/* Class already loaded */
if (classes[name] != null) {
/* Circular dependency */
if (from.indexOf(name) != -1)
L.error('DependencyError',
'Circular dependency: class "%s" depends on "%s"',
name, from.join('" which depends on "'));
return classes[name];
}
document.querySelectorAll('script[src$="/luci.js"]').forEach(function(s) {
url = '%s/%s.js'.format(
s.getAttribute('src').replace(/\/luci\.js$/, ''),
name.replace(/\./g, '/'));
});
if (url == null)
L.error('InternalError', 'Cannot find url of luci.js');
from = [ name ].concat(from);
var compileClass = function(res) {
if (!res.ok)
L.error('NetworkError',
'HTTP error %d while loading class file "%s"', res.status, url);
var source = res.text(),
reqmatch = /(?:^|\n)[ \t]*(?:["']require[ \t]+(\S+)(?:[ \t]+as[ \t]+([a-zA-Z_]\S*))?["']);/g,
depends = [],
args = '';
/* find require statements in source */
for (var m = reqmatch.exec(source); m; m = reqmatch.exec(source)) {
var dep = m[1], as = m[2] || dep.replace(/[^a-zA-Z0-9_]/g, '_');
depends.push(L.require(dep, from));
args += ', ' + as;
}
/* load dependencies and instantiate class */
return Promise.all(depends).then(function(instances) {
try {
_factory = eval(
'(function(window, document, L%s) { %s })\n\n//# sourceURL=%s\n'
.format(args, source, res.url));
}
catch (error) {
L.error('SyntaxError', '%s\n in %s:%s',
error.message, res.url, error.lineNumber || '?');
}
_factory.displayName = toCamelCase(name + 'ClassFactory');
_class = _factory.apply(_factory, [window, document, L].concat(instances));
if (!Class.isSubclass(_class))
L.error('TypeError', '"%s" factory yields invalid constructor', name);
if (_class.displayName == 'AnonymousClass')
_class.displayName = toCamelCase(name + 'Class');
var ptr = Object.getPrototypeOf(L),
parts = name.split(/\./),
instance = new _class();
for (var i = 0; ptr && i < parts.length - 1; i++)
ptr = ptr[parts[i]];
if (!ptr)
L.error('DependencyError',
'Parent "%s" for class "%s" is missing',
parts.slice(0, i).join('.'), name);
classes[name] = ptr[parts[i]] = instance;
return instance;
});
};
/* Request class file */
classes[name] = Request.get(url, { cache: true }).then(compileClass);
return classes[name];
},
/* DOM setup */
setupDOM: function(ev) {
Request.addInterceptor(function(res) {
if (res.status != 403 || res.headers.get('X-LuCI-Login-Required') != 'yes')
return;
Request.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…')))
]);
throw 'Session expired';
});
originalCBIInit();
Request.poll.start();
document.dispatchEvent(new CustomEvent('luci-loaded'));
},
env: {},
/* URL construction helpers */
path: function(prefix, parts) {
var url = [ prefix || '' ];
for (var i = 0; i < parts.length; i++)
if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts[i]))
url.push('/', parts[i]);
if (url.length === 1)
url.push('/');
return url.join('');
},
url: function() {
return this.path(this.env.scriptname, arguments);
},
resource: function() {
return this.path(this.env.resource, arguments);
},
location: function() {
return this.path(this.env.scriptname, this.env.requestpath);
},
/* HTTP resource fetching */
get: function(url, args, cb) {
return this.poll(null, url, args, cb, false);
},
post: function(url, args, cb) {
return this.poll(null, url, args, cb, true);
},
poll: function(interval, url, args, cb, post) {
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 (args != null)
data = Object.assign(data || {}, args);
if (interval !== null)
return Request.poll.add(interval, url, { method: method, query: data }, cb);
else
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) { return Request.poll.remove(entry) },
halt: function() { return Request.poll.stop() },
run: function() { return Request.poll.start() },
/* DOM manipulation */
dom: Class.singleton({
__name__: 'LuCI.DOM',
elem: function(e) {
return (e != null && typeof(e) == 'object' && 'nodeType' in e);
},
parse: function(s) {
var elem;
try {
domParser = domParser || new DOMParser();
elem = domParser.parseFromString(s, 'text/html').body.firstChild;
}
catch(e) {}
if (!elem) {
try {
dummyElem = dummyElem || document.createElement('div');
dummyElem.innerHTML = s;
elem = dummyElem.firstChild;
}
catch (e) {}
}
return elem || null;
},
matches: function(node, selector) {
var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
return m ? m.call(node, selector) : false;
},
parent: function(node, selector) {
if (this.elem(node) && node.closest)
return node.closest(selector);
while (this.elem(node))
if (this.matches(node, selector))
return node;
else
node = node.parentNode;
return null;
},
append: function(node, children) {
if (!this.elem(node))
return null;
if (Array.isArray(children)) {
for (var i = 0; i < children.length; i++)
if (this.elem(children[i]))
node.appendChild(children[i]);
else if (children !== null && children !== undefined)
node.appendChild(document.createTextNode('' + children[i]));
return node.lastChild;
}
else if (typeof(children) === 'function') {
return this.append(node, children(node));
}
else if (this.elem(children)) {
return node.appendChild(children);
}
else if (children !== null && children !== undefined) {
node.innerHTML = '' + children;
return node.lastChild;
}
return null;
},
content: function(node, children) {
if (!this.elem(node))
return null;
var dataNodes = node.querySelectorAll('[data-idref]');
for (var i = 0; i < dataNodes.length; i++)
delete this.registry[dataNodes[i].getAttribute('data-idref')];
while (node.firstChild)
node.removeChild(node.firstChild);
return this.append(node, children);
},
attr: function(node, key, val) {
if (!this.elem(node))
return null;
var attr = null;
if (typeof(key) === 'object' && key !== null)
attr = key;
else if (typeof(key) === 'string')
attr = {}, attr[key] = val;
for (key in attr) {
if (!attr.hasOwnProperty(key) || attr[key] == null)
continue;
switch (typeof(attr[key])) {
case 'function':
node.addEventListener(key, attr[key]);
break;
case 'object':
node.setAttribute(key, JSON.stringify(attr[key]));
break;
default:
node.setAttribute(key, attr[key]);
}
}
},
create: function() {
var html = arguments[0],
attr = arguments[1],
data = arguments[2],
elem;
if (!(attr instanceof Object) || Array.isArray(attr))
data = attr, attr = null;
if (Array.isArray(html)) {
elem = document.createDocumentFragment();
for (var i = 0; i < html.length; i++)
elem.appendChild(this.create(html[i]));
}
else if (this.elem(html)) {
elem = html;
}
else if (html.charCodeAt(0) === 60) {
elem = this.parse(html);
}
else {
elem = document.createElement(html);
}
if (!elem)
return null;
this.attr(elem, attr);
this.append(elem, data);
return elem;
},
registry: {},
data: function(node, key, val) {
var id = node.getAttribute('data-idref');
/* clear all data */
if (arguments.length > 1 && key == null) {
if (id != null) {
node.removeAttribute('data-idref');
val = this.registry[id]
delete this.registry[id];
return val;
}
return null;
}
/* clear a key */
else if (arguments.length > 2 && key != null && val == null) {
if (id != null) {
val = this.registry[id][key];
delete this.registry[id][key];
return val;
}
return null;
}
/* set a key */
else if (arguments.length > 2 && key != null && val != null) {
if (id == null) {
do { id = Math.floor(Math.random() * 0xffffffff).toString(16) }
while (this.registry.hasOwnProperty(id));
node.setAttribute('data-idref', id);
this.registry[id] = {};
}
return (this.registry[id][key] = val);
}
/* get all data */
else if (arguments.length == 1) {
if (id != null)
return this.registry[id];
return null;
}
/* get a key */
else if (arguments.length == 2) {
if (id != null)
return this.registry[id][key];
}
return null;
},
bindClassInstance: function(node, inst) {
if (!(inst instanceof Class))
L.error('TypeError', 'Argument must be a class instance');
return this.data(node, '_class', inst);
},
findClassInstance: function(node) {
var inst = null;
do {
inst = this.data(node, '_class');
node = node.parentNode;
}
while (!(inst instanceof Class) && node != null);
return inst;
},
callClassMethod: function(node, method /*, ... */) {
var inst = this.findClassInstance(node);
if (inst == null || typeof(inst[method]) != 'function')
return null;
return inst[method].apply(inst, inst.varargs(arguments, 2));
}
}),
Class: Class,
Request: Request,
view: Class.extend({
__name__: 'LuCI.View',
__init__: function() {
var mc = document.getElementById('maincontent');
L.dom.content(mc, E('div', { 'class': 'spinning' }, _('Loading view…')));
return Promise.resolve(this.load())
.then(L.bind(this.render, this))
.then(L.bind(function(nodes) {
var mc = document.getElementById('maincontent');
L.dom.content(mc, nodes);
L.dom.append(mc, this.addFooter());
}, this));
},
load: function() {},
render: function() {},
handleSave: function(ev) {
var tasks = [];
document.getElementById('maincontent')
.querySelectorAll('.cbi-map').forEach(function(map) {
tasks.push(L.dom.callClassMethod(map, 'save'));
});
return Promise.all(tasks);
},
handleSaveApply: function(ev) {
return this.handleSave(ev).then(function() {
L.ui.changes.apply(true);
});
},
handleReset: function(ev) {
var tasks = [];
document.getElementById('maincontent')
.querySelectorAll('.cbi-map').forEach(function(map) {
tasks.push(L.dom.callClassMethod(map, 'reset'));
});
return Promise.all(tasks);
},
addFooter: function() {
var footer = E([]),
mc = document.getElementById('maincontent');
if (mc.querySelector('.cbi-map')) {
footer.appendChild(E('div', { 'class': 'cbi-page-actions' }, [
E('input', {
'class': 'cbi-button cbi-button-apply',
'type': 'button',
'value': _('Save & Apply'),
'click': L.bind(this.handleSaveApply, this)
}), ' ',
E('input', {
'class': 'cbi-button cbi-button-save',
'type': 'submit',
'value': _('Save'),
'click': L.bind(this.handleSave, this)
}), ' ',
E('input', {
'class': 'cbi-button cbi-button-reset',
'type': 'button',
'value': _('Reset'),
'click': L.bind(this.handleReset, this)
})
]));
}
return footer;
}
})
});
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() { L.error('InternalError', '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);