1 (function(window, document, undefined) {
2 /* Object.assign polyfill for IE */
3 if (typeof Object.assign !== 'function') {
4 Object.defineProperty(Object, 'assign', {
5 value: function assign(target, varArgs) {
7 throw new TypeError('Cannot convert undefined or null to object');
9 var to = Object(target);
11 for (var index = 1; index < arguments.length; index++)
12 if (arguments[index] != null)
13 for (var nextKey in arguments[index])
14 if (Object.prototype.hasOwnProperty.call(arguments[index], nextKey))
15 to[nextKey] = arguments[index][nextKey];
25 * Class declaration and inheritance helper
28 var toCamelCase = function(s) {
29 return s.replace(/(?:^|[\. -])(.)/g, function(m0, m1) { return m1.toUpperCase() });
32 var superContext = null, Class = Object.assign(function() {}, {
33 extend: function(properties) {
35 __base__: { value: this.prototype },
36 __name__: { value: properties.__name__ || 'anonymous' }
39 var ClassConstructor = function() {
40 if (!(this instanceof ClassConstructor))
41 throw new TypeError('Constructor must not be called without "new"');
43 if (Object.getPrototypeOf(this).hasOwnProperty('__init__')) {
44 if (typeof(this.__init__) != 'function')
45 throw new TypeError('Class __init__ member is not a function');
47 this.__init__.apply(this, arguments)
50 this.super('__init__', arguments);
54 for (var key in properties)
55 if (!props[key] && properties.hasOwnProperty(key))
56 props[key] = { value: properties[key], writable: true };
58 ClassConstructor.prototype = Object.create(this.prototype, props);
59 ClassConstructor.prototype.constructor = ClassConstructor;
60 Object.assign(ClassConstructor, this);
61 ClassConstructor.displayName = toCamelCase(props.__name__.value + 'Class');
63 return ClassConstructor;
66 singleton: function(properties /*, ... */) {
67 return Class.extend(properties)
68 .instantiate(Class.prototype.varargs(arguments, 1));
71 instantiate: function(args) {
72 return new (Function.prototype.bind.apply(this,
73 Class.prototype.varargs(args, 0, null)))();
76 call: function(self, method) {
77 if (typeof(this.prototype[method]) != 'function')
78 throw new ReferenceError(method + ' is not defined in class');
80 return this.prototype[method].apply(self, self.varargs(arguments, 1));
83 isSubclass: function(_class) {
84 return (_class != null &&
85 typeof(_class) == 'function' &&
86 _class.prototype instanceof this);
90 varargs: function(args, offset /*, ... */) {
91 return Array.prototype.slice.call(arguments, 2)
92 .concat(Array.prototype.slice.call(args, offset));
95 super: function(key, callArgs) {
96 for (superContext = Object.getPrototypeOf(superContext ||
97 Object.getPrototypeOf(this));
98 superContext && !superContext.hasOwnProperty(key);
99 superContext = Object.getPrototypeOf(superContext)) { }
104 var res = superContext[key];
106 if (arguments.length > 1) {
107 if (typeof(res) != 'function')
108 throw new ReferenceError(key + ' is not a function in base class');
110 if (typeof(callArgs) != 'object')
111 callArgs = this.varargs(arguments, 1);
113 res = res.apply(this, callArgs);
121 toString: function() {
122 var s = '[' + this.constructor.displayName + ']', f = true;
123 for (var k in this) {
124 if (this.hasOwnProperty(k)) {
125 s += (f ? ' {\n' : '') + ' ' + k + ': ' + typeof(this[k]) + '\n';
129 return s + (f ? '' : '}');
136 * HTTP Request helper
139 Headers = Class.extend({
140 __name__: 'LuCI.XHR.Headers',
141 __init__: function(xhr) {
142 var hdrs = this.headers = {};
143 xhr.getAllResponseHeaders().split(/\r\n/).forEach(function(line) {
144 var m = /^([^:]+):(.*)$/.exec(line);
146 hdrs[m[1].trim().toLowerCase()] = m[2].trim();
150 has: function(name) {
151 return this.headers.hasOwnProperty(String(name).toLowerCase());
154 get: function(name) {
155 var key = String(name).toLowerCase();
156 return this.headers.hasOwnProperty(key) ? this.headers[key] : null;
160 Response = Class.extend({
161 __name__: 'LuCI.XHR.Response',
162 __init__: function(xhr, url, duration) {
163 this.ok = (xhr.status >= 200 && xhr.status <= 299);
164 this.status = xhr.status;
165 this.statusText = xhr.statusText;
166 this.responseText = xhr.responseText;
167 this.headers = new Headers(xhr);
168 this.duration = duration;
174 return JSON.parse(this.responseText);
178 return this.responseText;
182 Request = Class.singleton({
183 __name__: 'LuCI.Request',
187 request: function(target, options) {
188 var state = { xhr: new XMLHttpRequest(), url: target, start: Date.now() },
189 opt = Object.assign({}, options, state),
192 callback = this.handleReadyStateChange;
194 return new Promise(function(resolveFn, rejectFn) {
195 opt.xhr.onreadystatechange = callback.bind(opt, resolveFn, rejectFn);
196 opt.method = String(opt.method || 'GET').toUpperCase();
198 if ('query' in opt) {
199 var q = (opt.query != null) ? Object.keys(opt.query).map(function(k) {
200 if (opt.query[k] != null) {
201 var v = (typeof(opt.query[k]) == 'object')
202 ? JSON.stringify(opt.query[k])
203 : String(opt.query[k]);
205 return '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(v));
208 return encodeURIComponent(k);
213 switch (opt.method) {
217 opt.url += ((/\?/).test(opt.url) ? '&' : '?') + q;
221 if (content == null) {
223 contenttype = 'application/x-www-form-urlencoded';
230 opt.url += ((/\?/).test(opt.url) ? '&' : '?') + (new Date()).getTime();
232 if (!/^(?:[^/]+:)?\/\//.test(opt.url))
233 opt.url = location.protocol + '//' + location.host + opt.url;
235 if ('username' in opt && 'password' in opt)
236 opt.xhr.open(opt.method, opt.url, true, opt.username, opt.password);
238 opt.xhr.open(opt.method, opt.url, true);
240 opt.xhr.responseType = 'text';
241 opt.xhr.overrideMimeType('application/octet-stream');
243 if ('timeout' in opt)
244 opt.xhr.timeout = +opt.timeout;
246 if ('credentials' in opt)
247 opt.xhr.withCredentials = !!opt.credentials;
249 if (opt.content != null) {
250 switch (typeof(opt.content)) {
252 content = opt.content(xhr);
256 content = JSON.stringify(opt.content);
257 contenttype = 'application/json';
261 content = String(opt.content);
265 if ('headers' in opt)
266 for (var header in opt.headers)
267 if (opt.headers.hasOwnProperty(header)) {
268 if (header.toLowerCase() != 'content-type')
269 opt.xhr.setRequestHeader(header, opt.headers[header]);
271 contenttype = opt.headers[header];
274 if (contenttype != null)
275 opt.xhr.setRequestHeader('Content-Type', contenttype);
278 opt.xhr.send(content);
281 rejectFn.call(opt, e);
286 handleReadyStateChange: function(resolveFn, rejectFn, ev) {
289 if (xhr.readyState !== 4)
292 if (xhr.status === 0 && xhr.statusText === '') {
293 rejectFn.call(this, new Error('XHR request aborted by browser'));
296 var response = new Response(
297 xhr, xhr.responseURL || this.url, Date.now() - this.start);
299 Promise.all(Request.interceptors.map(function(fn) { return fn(response) }))
300 .then(resolveFn.bind(this, response))
301 .catch(rejectFn.bind(this));
310 get: function(url, options) {
311 return this.request(url, Object.assign({ method: 'GET' }, options));
314 post: function(url, data, options) {
315 return this.request(url, Object.assign({ method: 'POST', content: data }, options));
318 addInterceptor: function(interceptorFn) {
319 if (typeof(interceptorFn) == 'function')
320 this.interceptors.push(interceptorFn);
321 return interceptorFn;
324 removeInterceptor: function(interceptorFn) {
325 var oldlen = this.interceptors.length, i = oldlen;
327 if (this.interceptors[i] === interceptorFn)
328 this.interceptors.splice(i, 1);
329 return (this.interceptors.length < oldlen);
332 poll: Class.singleton({
333 __name__: 'LuCI.Request.Poll',
337 add: function(interval, url, options, callback) {
338 if (isNaN(interval) || interval <= 0)
339 throw new TypeError('Invalid poll interval');
352 remove: function(entry) {
353 var oldlen = this.queue.length, i = oldlen;
356 if (this.queue[i] === entry) {
357 delete this.queue[i].running;
358 this.queue.splice(i, 1);
361 if (!this.queue.length)
364 return (this.queue.length < oldlen);
368 if (!this.queue.length || this.active())
372 this.timer = window.setInterval(this.step, 1000);
374 document.dispatchEvent(new CustomEvent('poll-start'));
382 document.dispatchEvent(new CustomEvent('poll-stop'));
383 window.clearInterval(this.timer);
390 Request.poll.queue.forEach(function(e) {
391 if ((Request.poll.tick % e.interval) != 0)
397 var opts = Object.assign({}, e.options,
398 { timeout: e.interval * 1000 - 5 });
401 Request.request(e.url, opts)
402 .then(function(res) {
403 if (!e.running || !Request.poll.active())
407 e.callback(res, res.json(), res.duration);
410 e.callback(res, null, res.duration);
413 .finally(function() { delete e.running });
416 Request.poll.tick = (Request.poll.tick + 1) % Math.pow(2, 32);
420 return (this.timer != null);
426 var dummyElem = null,
428 originalCBIInit = null,
431 LuCI = Class.extend({
433 __init__: function(env) {
434 Object.assign(this.env, env);
436 document.addEventListener('DOMContentLoaded', this.setupDOM.bind(this));
438 document.addEventListener('poll-start', function(ev) {
439 document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
440 e.style.display = (e.id == 'xhr_poll_status_off') ? 'none' : '';
444 document.addEventListener('poll-stop', function(ev) {
445 document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
446 e.style.display = (e.id == 'xhr_poll_status_on') ? 'none' : '';
450 originalCBIInit = window.cbi_init;
451 window.cbi_init = function() {};
454 error: function(type, fmt /*, ...*/) {
456 msg = fmt ? String.prototype.format.apply(fmt, this.varargs(arguments, 2)) : null,
459 if (type instanceof Error) {
461 stack = (e.stack || '').split(/\n/);
464 e.message = msg + ': ' + e.message;
467 e = new (window[type || 'Error'] || Error)(msg || 'Unspecified error');
468 e.name = type || 'Error';
470 try { throw new Error('stacktrace') }
471 catch (e2) { stack = (e2.stack || '').split(/\n/) }
473 /* IE puts the exception message into the first line */
474 if (stack[0] == 'Error: stacktrace')
477 /* Pop L.error() invocation from stack */
481 /* Append shortened & beautified stacktrace to message */
482 e.message += '\n' + stack.join('\n')
483 .replace(/(.*?)@(.+):(\d+):(\d+)/g, ' at $1 ($2:$3:$4)');
485 if (window.console && console.debug)
492 require: function(name, from) {
493 var L = this, url = null, from = from || [];
495 /* Class already loaded */
496 if (classes[name] != null) {
497 /* Circular dependency */
498 if (from.indexOf(name) != -1)
499 L.error('DependencyError',
500 'Circular dependency: class "%s" depends on "%s"',
501 name, from.join('" which depends on "'));
503 return classes[name];
506 document.querySelectorAll('script[src$="/luci.js"]').forEach(function(s) {
507 url = '%s/%s.js'.format(
508 s.getAttribute('src').replace(/\/luci\.js$/, ''),
509 name.replace(/\./g, '/'));
513 L.error('InternalError', 'Cannot find url of luci.js');
515 from = [ name ].concat(from);
517 var compileClass = function(res) {
519 L.error('NetworkError',
520 'HTTP error %d while loading class file "%s"', res.status, url);
522 var source = res.text(),
523 reqmatch = /(?:^|\n)[ \t]*(?:["']require[ \t]+(\S+)(?:[ \t]+as[ \t]+([a-zA-Z_]\S*))?["']);/g,
527 /* find require statements in source */
528 for (var m = reqmatch.exec(source); m; m = reqmatch.exec(source)) {
529 var dep = m[1], as = m[2] || dep.replace(/[^a-zA-Z0-9_]/g, '_');
530 depends.push(L.require(dep, from));
534 /* load dependencies and instantiate class */
535 return Promise.all(depends).then(function(instances) {
538 '(function(window, document, L%s) { %s })\n\n//# sourceURL=%s\n'
539 .format(args, source, res.url));
542 L.error('SyntaxError', '%s\n in %s:%s',
543 error.message, res.url, error.lineNumber || '?');
546 _factory.displayName = toCamelCase(name + 'ClassFactory');
547 _class = _factory.apply(_factory, [window, document, L].concat(instances));
549 if (!Class.isSubclass(_class))
550 L.error('TypeError', '"%s" factory yields invalid constructor', name);
552 if (_class.displayName == 'AnonymousClass')
553 _class.displayName = toCamelCase(name + 'Class');
555 var ptr = Object.getPrototypeOf(L),
556 parts = name.split(/\./),
557 instance = new _class();
559 for (var i = 0; ptr && i < parts.length - 1; i++)
563 L.error('DependencyError',
564 'Parent "%s" for class "%s" is missing',
565 parts.slice(0, i).join('.'), name);
567 classes[name] = ptr[parts[i]] = instance;
573 /* Request class file */
574 classes[name] = Request.get(url, { cache: true }).then(compileClass);
576 return classes[name];
580 setupDOM: function(ev) {
584 Request.addInterceptor(function(res) {
585 if (res.status != 403 || res.headers.get('X-LuCI-Login-Required') != 'yes')
590 L.ui.showModal(_('Session expired'), [
591 E('div', { class: 'alert-message warning' },
592 _('A new login is required since the authentication session expired.')),
593 E('div', { class: 'right' },
595 class: 'btn primary',
597 var loc = window.location;
598 window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
600 }, _('To login…')))
603 L.error('AuthenticationError', 'Session expired');
607 Request.poll.start();
609 document.dispatchEvent(new CustomEvent('luci-loaded'));
610 }).catch(function(error) {
611 alert('LuCI class loading error:\n' + error);
617 /* URL construction helpers */
618 path: function(prefix, parts) {
619 var url = [ prefix || '' ];
621 for (var i = 0; i < parts.length; i++)
622 if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts[i]))
623 url.push('/', parts[i]);
625 if (url.length === 1)
632 return this.path(this.env.scriptname, arguments);
635 resource: function() {
636 return this.path(this.env.resource, arguments);
639 location: function() {
640 return this.path(this.env.scriptname, this.env.requestpath);
644 /* HTTP resource fetching */
645 get: function(url, args, cb) {
646 return this.poll(null, url, args, cb, false);
649 post: function(url, args, cb) {
650 return this.poll(null, url, args, cb, true);
653 poll: function(interval, url, args, cb, post) {
654 if (interval !== null && interval <= 0)
655 interval = this.env.pollinterval;
657 var data = post ? { token: this.env.token } : null,
658 method = post ? 'POST' : 'GET';
660 if (!/^(?:\/|\S+:\/\/)/.test(url))
664 data = Object.assign(data || {}, args);
666 if (interval !== null)
667 return Request.poll.add(interval, url, { method: method, query: data }, cb);
669 return Request.request(url, { method: method, query: data })
670 .then(function(res) {
672 if (/^application\/json\b/.test(res.headers.get('Content-Type')))
673 try { json = res.json() } catch(e) {}
674 cb(res.xhr, json, res.duration);
678 stop: function(entry) { return Request.poll.remove(entry) },
679 halt: function() { return Request.poll.stop() },
680 run: function() { return Request.poll.start() },
682 /* DOM manipulation */
683 dom: Class.singleton({
684 __name__: 'LuCI.DOM',
687 return (e != null && typeof(e) == 'object' && 'nodeType' in e);
694 domParser = domParser || new DOMParser();
695 elem = domParser.parseFromString(s, 'text/html').body.firstChild;
701 dummyElem = dummyElem || document.createElement('div');
702 dummyElem.innerHTML = s;
703 elem = dummyElem.firstChild;
711 matches: function(node, selector) {
712 var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
713 return m ? m.call(node, selector) : false;
716 parent: function(node, selector) {
717 if (this.elem(node) && node.closest)
718 return node.closest(selector);
720 while (this.elem(node))
721 if (this.matches(node, selector))
724 node = node.parentNode;
729 append: function(node, children) {
730 if (!this.elem(node))
733 if (Array.isArray(children)) {
734 for (var i = 0; i < children.length; i++)
735 if (this.elem(children[i]))
736 node.appendChild(children[i]);
737 else if (children !== null && children !== undefined)
738 node.appendChild(document.createTextNode('' + children[i]));
740 return node.lastChild;
742 else if (typeof(children) === 'function') {
743 return this.append(node, children(node));
745 else if (this.elem(children)) {
746 return node.appendChild(children);
748 else if (children !== null && children !== undefined) {
749 node.innerHTML = '' + children;
750 return node.lastChild;
756 content: function(node, children) {
757 if (!this.elem(node))
760 while (node.firstChild)
761 node.removeChild(node.firstChild);
763 return this.append(node, children);
766 attr: function(node, key, val) {
767 if (!this.elem(node))
772 if (typeof(key) === 'object' && key !== null)
774 else if (typeof(key) === 'string')
775 attr = {}, attr[key] = val;
778 if (!attr.hasOwnProperty(key) || attr[key] == null)
781 switch (typeof(attr[key])) {
783 node.addEventListener(key, attr[key]);
787 node.setAttribute(key, JSON.stringify(attr[key]));
791 node.setAttribute(key, attr[key]);
797 var html = arguments[0],
802 if (!(attr instanceof Object) || Array.isArray(attr))
803 data = attr, attr = null;
805 if (Array.isArray(html)) {
806 elem = document.createDocumentFragment();
807 for (var i = 0; i < html.length; i++)
808 elem.appendChild(this.create(html[i]));
810 else if (this.elem(html)) {
813 else if (html.charCodeAt(0) === 60) {
814 elem = this.parse(html);
817 elem = document.createElement(html);
823 this.attr(elem, attr);
824 this.append(elem, data);
835 __name__: 'LuCI.XHR',
836 __init__: function() {
837 if (window.console && console.debug)
838 console.debug('Direct use XHR() is deprecated, please use L.Request instead');
841 _response: function(cb, res, json, duration) {
843 cb(res, json, duration);
847 get: function(url, data, callback, timeout) {
849 L.get(url, data, this._response.bind(this, callback), timeout);
852 post: function(url, data, callback, timeout) {
854 L.post(url, data, this._response.bind(this, callback), timeout);
857 cancel: function() { delete this.active },
858 busy: function() { return (this.active === true) },
859 abort: function() {},
860 send_form: function() { L.error('InternalError', 'Not implemented') },
863 XHR.get = function() { return window.L.get.apply(window.L, arguments) };
864 XHR.post = function() { return window.L.post.apply(window.L, arguments) };
865 XHR.poll = function() { return window.L.poll.apply(window.L, arguments) };
866 XHR.stop = Request.poll.remove.bind(Request.poll);
867 XHR.halt = Request.poll.stop.bind(Request.poll);
868 XHR.run = Request.poll.start.bind(Request.poll);
869 XHR.running = Request.poll.active.bind(Request.poll);
873 })(window, document);