luci-base: tie cached system features to user session
[project/luci.git] / modules / luci-base / htdocs / luci-static / resources / luci.js
1 (function(window, document, undefined) {
2 'use strict';
3
4 /* Object.assign polyfill for IE */
5 if (typeof Object.assign !== 'function') {
6 Object.defineProperty(Object, 'assign', {
7 value: function assign(target, varArgs) {
8 if (target == null)
9 throw new TypeError('Cannot convert undefined or null to object');
10
11 var to = Object(target);
12
13 for (var index = 1; index < arguments.length; index++)
14 if (arguments[index] != null)
15 for (var nextKey in arguments[index])
16 if (Object.prototype.hasOwnProperty.call(arguments[index], nextKey))
17 to[nextKey] = arguments[index][nextKey];
18
19 return to;
20 },
21 writable: true,
22 configurable: true
23 });
24 }
25
26 /* Promise.finally polyfill */
27 if (typeof Promise.prototype.finally !== 'function') {
28 Promise.prototype.finally = function(fn) {
29 var onFinally = function(cb) {
30 return Promise.resolve(fn.call(this)).then(cb);
31 };
32
33 return this.then(
34 function(result) { return onFinally.call(this, function() { return result }) },
35 function(reason) { return onFinally.call(this, function() { return Promise.reject(reason) }) }
36 );
37 };
38 }
39
40 /*
41 * Class declaration and inheritance helper
42 */
43
44 var toCamelCase = function(s) {
45 return s.replace(/(?:^|[\. -])(.)/g, function(m0, m1) { return m1.toUpperCase() });
46 };
47
48 var superContext = null, Class = Object.assign(function() {}, {
49 extend: function(properties) {
50 var props = {
51 __base__: { value: this.prototype },
52 __name__: { value: properties.__name__ || 'anonymous' }
53 };
54
55 var ClassConstructor = function() {
56 if (!(this instanceof ClassConstructor))
57 throw new TypeError('Constructor must not be called without "new"');
58
59 if (Object.getPrototypeOf(this).hasOwnProperty('__init__')) {
60 if (typeof(this.__init__) != 'function')
61 throw new TypeError('Class __init__ member is not a function');
62
63 this.__init__.apply(this, arguments)
64 }
65 else {
66 this.super('__init__', arguments);
67 }
68 };
69
70 for (var key in properties)
71 if (!props[key] && properties.hasOwnProperty(key))
72 props[key] = { value: properties[key], writable: true };
73
74 ClassConstructor.prototype = Object.create(this.prototype, props);
75 ClassConstructor.prototype.constructor = ClassConstructor;
76 Object.assign(ClassConstructor, this);
77 ClassConstructor.displayName = toCamelCase(props.__name__.value + 'Class');
78
79 return ClassConstructor;
80 },
81
82 singleton: function(properties /*, ... */) {
83 return Class.extend(properties)
84 .instantiate(Class.prototype.varargs(arguments, 1));
85 },
86
87 instantiate: function(args) {
88 return new (Function.prototype.bind.apply(this,
89 Class.prototype.varargs(args, 0, null)))();
90 },
91
92 call: function(self, method) {
93 if (typeof(this.prototype[method]) != 'function')
94 throw new ReferenceError(method + ' is not defined in class');
95
96 return this.prototype[method].apply(self, self.varargs(arguments, 1));
97 },
98
99 isSubclass: function(_class) {
100 return (_class != null &&
101 typeof(_class) == 'function' &&
102 _class.prototype instanceof this);
103 },
104
105 prototype: {
106 varargs: function(args, offset /*, ... */) {
107 return Array.prototype.slice.call(arguments, 2)
108 .concat(Array.prototype.slice.call(args, offset));
109 },
110
111 super: function(key, callArgs) {
112 for (superContext = Object.getPrototypeOf(superContext ||
113 Object.getPrototypeOf(this));
114 superContext && !superContext.hasOwnProperty(key);
115 superContext = Object.getPrototypeOf(superContext)) { }
116
117 if (!superContext)
118 return null;
119
120 var res = superContext[key];
121
122 if (arguments.length > 1) {
123 if (typeof(res) != 'function')
124 throw new ReferenceError(key + ' is not a function in base class');
125
126 if (typeof(callArgs) != 'object')
127 callArgs = this.varargs(arguments, 1);
128
129 res = res.apply(this, callArgs);
130 }
131
132 superContext = null;
133
134 return res;
135 },
136
137 toString: function() {
138 var s = '[' + this.constructor.displayName + ']', f = true;
139 for (var k in this) {
140 if (this.hasOwnProperty(k)) {
141 s += (f ? ' {\n' : '') + ' ' + k + ': ' + typeof(this[k]) + '\n';
142 f = false;
143 }
144 }
145 return s + (f ? '' : '}');
146 }
147 }
148 });
149
150
151 /*
152 * HTTP Request helper
153 */
154
155 var Headers = Class.extend({
156 __name__: 'LuCI.XHR.Headers',
157 __init__: function(xhr) {
158 var hdrs = this.headers = {};
159 xhr.getAllResponseHeaders().split(/\r\n/).forEach(function(line) {
160 var m = /^([^:]+):(.*)$/.exec(line);
161 if (m != null)
162 hdrs[m[1].trim().toLowerCase()] = m[2].trim();
163 });
164 },
165
166 has: function(name) {
167 return this.headers.hasOwnProperty(String(name).toLowerCase());
168 },
169
170 get: function(name) {
171 var key = String(name).toLowerCase();
172 return this.headers.hasOwnProperty(key) ? this.headers[key] : null;
173 }
174 });
175
176 var Response = Class.extend({
177 __name__: 'LuCI.XHR.Response',
178 __init__: function(xhr, url, duration, headers, content) {
179 this.ok = (xhr.status >= 200 && xhr.status <= 299);
180 this.status = xhr.status;
181 this.statusText = xhr.statusText;
182 this.headers = (headers != null) ? headers : new Headers(xhr);
183 this.duration = duration;
184 this.url = url;
185 this.xhr = xhr;
186
187 if (content != null && typeof(content) == 'object') {
188 this.responseJSON = content;
189 this.responseText = null;
190 }
191 else if (content != null) {
192 this.responseJSON = null;
193 this.responseText = String(content);
194 }
195 else {
196 this.responseJSON = null;
197 this.responseText = xhr.responseText;
198 }
199 },
200
201 clone: function(content) {
202 var copy = new Response(this.xhr, this.url, this.duration, this.headers, content);
203
204 copy.ok = this.ok;
205 copy.status = this.status;
206 copy.statusText = this.statusText;
207
208 return copy;
209 },
210
211 json: function() {
212 if (this.responseJSON == null)
213 this.responseJSON = JSON.parse(this.responseText);
214
215 return this.responseJSON;
216 },
217
218 text: function() {
219 if (this.responseText == null && this.responseJSON != null)
220 this.responseText = JSON.stringify(this.responseJSON);
221
222 return this.responseText;
223 }
224 });
225
226
227 var requestQueue = [];
228
229 function isQueueableRequest(opt) {
230 if (!classes.rpc)
231 return false;
232
233 if (opt.method != 'POST' || typeof(opt.content) != 'object')
234 return false;
235
236 if (opt.nobatch === true)
237 return false;
238
239 var rpcBaseURL = Request.expandURL(classes.rpc.getBaseURL());
240
241 return (rpcBaseURL != null && opt.url.indexOf(rpcBaseURL) == 0);
242 }
243
244 function flushRequestQueue() {
245 if (!requestQueue.length)
246 return;
247
248 var reqopt = Object.assign({}, requestQueue[0][0], { content: [], nobatch: true }),
249 batch = [];
250
251 for (var i = 0; i < requestQueue.length; i++) {
252 batch[i] = requestQueue[i];
253 reqopt.content[i] = batch[i][0].content;
254 }
255
256 requestQueue.length = 0;
257
258 Request.request(rpcBaseURL, reqopt).then(function(reply) {
259 var json = null, req = null;
260
261 try { json = reply.json() }
262 catch(e) { }
263
264 while ((req = batch.shift()) != null)
265 if (Array.isArray(json) && json.length)
266 req[2].call(reqopt, reply.clone(json.shift()));
267 else
268 req[1].call(reqopt, new Error('No related RPC reply'));
269 }).catch(function(error) {
270 var req = null;
271
272 while ((req = batch.shift()) != null)
273 req[1].call(reqopt, error);
274 });
275 }
276
277 var Request = Class.singleton({
278 __name__: 'LuCI.Request',
279
280 interceptors: [],
281
282 expandURL: function(url) {
283 if (!/^(?:[^/]+:)?\/\//.test(url))
284 url = location.protocol + '//' + location.host + url;
285
286 return url;
287 },
288
289 request: function(target, options) {
290 var state = { xhr: new XMLHttpRequest(), url: this.expandURL(target), start: Date.now() },
291 opt = Object.assign({}, options, state),
292 content = null,
293 contenttype = null,
294 callback = this.handleReadyStateChange;
295
296 return new Promise(function(resolveFn, rejectFn) {
297 opt.xhr.onreadystatechange = callback.bind(opt, resolveFn, rejectFn);
298 opt.method = String(opt.method || 'GET').toUpperCase();
299
300 if ('query' in opt) {
301 var q = (opt.query != null) ? Object.keys(opt.query).map(function(k) {
302 if (opt.query[k] != null) {
303 var v = (typeof(opt.query[k]) == 'object')
304 ? JSON.stringify(opt.query[k])
305 : String(opt.query[k]);
306
307 return '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(v));
308 }
309 else {
310 return encodeURIComponent(k);
311 }
312 }).join('&') : '';
313
314 if (q !== '') {
315 switch (opt.method) {
316 case 'GET':
317 case 'HEAD':
318 case 'OPTIONS':
319 opt.url += ((/\?/).test(opt.url) ? '&' : '?') + q;
320 break;
321
322 default:
323 if (content == null) {
324 content = q;
325 contenttype = 'application/x-www-form-urlencoded';
326 }
327 }
328 }
329 }
330
331 if (!opt.cache)
332 opt.url += ((/\?/).test(opt.url) ? '&' : '?') + (new Date()).getTime();
333
334 if (isQueueableRequest(opt)) {
335 requestQueue.push([opt, rejectFn, resolveFn]);
336 requestAnimationFrame(flushRequestQueue);
337 return;
338 }
339
340 if ('username' in opt && 'password' in opt)
341 opt.xhr.open(opt.method, opt.url, true, opt.username, opt.password);
342 else
343 opt.xhr.open(opt.method, opt.url, true);
344
345 opt.xhr.responseType = 'text';
346
347 if ('overrideMimeType' in opt.xhr)
348 opt.xhr.overrideMimeType('application/octet-stream');
349
350 if ('timeout' in opt)
351 opt.xhr.timeout = +opt.timeout;
352
353 if ('credentials' in opt)
354 opt.xhr.withCredentials = !!opt.credentials;
355
356 if (opt.content != null) {
357 switch (typeof(opt.content)) {
358 case 'function':
359 content = opt.content(xhr);
360 break;
361
362 case 'object':
363 if (!(opt.content instanceof FormData)) {
364 content = JSON.stringify(opt.content);
365 contenttype = 'application/json';
366 }
367 else {
368 content = opt.content;
369 }
370 break;
371
372 default:
373 content = String(opt.content);
374 }
375 }
376
377 if ('headers' in opt)
378 for (var header in opt.headers)
379 if (opt.headers.hasOwnProperty(header)) {
380 if (header.toLowerCase() != 'content-type')
381 opt.xhr.setRequestHeader(header, opt.headers[header]);
382 else
383 contenttype = opt.headers[header];
384 }
385
386 if ('progress' in opt && 'upload' in opt.xhr)
387 opt.xhr.upload.addEventListener('progress', opt.progress);
388
389 if (contenttype != null)
390 opt.xhr.setRequestHeader('Content-Type', contenttype);
391
392 try {
393 opt.xhr.send(content);
394 }
395 catch (e) {
396 rejectFn.call(opt, e);
397 }
398 });
399 },
400
401 handleReadyStateChange: function(resolveFn, rejectFn, ev) {
402 var xhr = this.xhr,
403 duration = Date.now() - this.start;
404
405 if (xhr.readyState !== 4)
406 return;
407
408 if (xhr.status === 0 && xhr.statusText === '') {
409 if (duration >= this.timeout)
410 rejectFn.call(this, new Error('XHR request timed out'));
411 else
412 rejectFn.call(this, new Error('XHR request aborted by browser'));
413 }
414 else {
415 var response = new Response(
416 xhr, xhr.responseURL || this.url, duration);
417
418 Promise.all(Request.interceptors.map(function(fn) { return fn(response) }))
419 .then(resolveFn.bind(this, response))
420 .catch(rejectFn.bind(this));
421 }
422 },
423
424 get: function(url, options) {
425 return this.request(url, Object.assign({ method: 'GET' }, options));
426 },
427
428 post: function(url, data, options) {
429 return this.request(url, Object.assign({ method: 'POST', content: data }, options));
430 },
431
432 addInterceptor: function(interceptorFn) {
433 if (typeof(interceptorFn) == 'function')
434 this.interceptors.push(interceptorFn);
435 return interceptorFn;
436 },
437
438 removeInterceptor: function(interceptorFn) {
439 var oldlen = this.interceptors.length, i = oldlen;
440 while (i--)
441 if (this.interceptors[i] === interceptorFn)
442 this.interceptors.splice(i, 1);
443 return (this.interceptors.length < oldlen);
444 },
445
446 poll: {
447 add: function(interval, url, options, callback) {
448 if (isNaN(interval) || interval <= 0)
449 throw new TypeError('Invalid poll interval');
450
451 var ival = interval >>> 0,
452 opts = Object.assign({}, options, { timeout: ival * 1000 - 5 });
453
454 return Poll.add(function() {
455 return Request.request(url, options).then(function(res) {
456 if (!Poll.active())
457 return;
458
459 try {
460 callback(res, res.json(), res.duration);
461 }
462 catch (err) {
463 callback(res, null, res.duration);
464 }
465 });
466 }, ival);
467 },
468
469 remove: function(entry) { return Poll.remove(entry) },
470 start: function() { return Poll.start() },
471 stop: function() { return Poll.stop() },
472 active: function() { return Poll.active() }
473 }
474 });
475
476 var Poll = Class.singleton({
477 __name__: 'LuCI.Poll',
478
479 queue: [],
480
481 add: function(fn, interval) {
482 if (interval == null || interval <= 0)
483 interval = window.L ? window.L.env.pollinterval : null;
484
485 if (isNaN(interval) || typeof(fn) != 'function')
486 throw new TypeError('Invalid argument to LuCI.Poll.add()');
487
488 for (var i = 0; i < this.queue.length; i++)
489 if (this.queue[i].fn === fn)
490 return false;
491
492 var e = {
493 r: true,
494 i: interval >>> 0,
495 fn: fn
496 };
497
498 this.queue.push(e);
499
500 if (this.tick != null && !this.active())
501 this.start();
502
503 return true;
504 },
505
506 remove: function(fn) {
507 if (typeof(fn) != 'function')
508 throw new TypeError('Invalid argument to LuCI.Poll.remove()');
509
510 var len = this.queue.length;
511
512 for (var i = len; i > 0; i--)
513 if (this.queue[i-1].fn === fn)
514 this.queue.splice(i-1, 1);
515
516 if (!this.queue.length && this.stop())
517 this.tick = 0;
518
519 return (this.queue.length != len);
520 },
521
522 start: function() {
523 if (this.active())
524 return false;
525
526 this.tick = 0;
527
528 if (this.queue.length) {
529 this.timer = window.setInterval(this.step, 1000);
530 this.step();
531 document.dispatchEvent(new CustomEvent('poll-start'));
532 }
533
534 return true;
535 },
536
537 stop: function() {
538 if (!this.active())
539 return false;
540
541 document.dispatchEvent(new CustomEvent('poll-stop'));
542 window.clearInterval(this.timer);
543 delete this.timer;
544 delete this.tick;
545 return true;
546 },
547
548 step: function() {
549 for (var i = 0, e = null; (e = Poll.queue[i]) != null; i++) {
550 if ((Poll.tick % e.i) != 0)
551 continue;
552
553 if (!e.r)
554 continue;
555
556 e.r = false;
557
558 Promise.resolve(e.fn()).finally((function() { this.r = true }).bind(e));
559 }
560
561 Poll.tick = (Poll.tick + 1) % Math.pow(2, 32);
562 },
563
564 active: function() {
565 return (this.timer != null);
566 }
567 });
568
569
570 var dummyElem = null,
571 domParser = null,
572 originalCBIInit = null,
573 rpcBaseURL = null,
574 sysFeatures = null,
575 classes = {};
576
577 var LuCI = Class.extend({
578 __name__: 'LuCI',
579 __init__: function(env) {
580
581 document.querySelectorAll('script[src*="/luci.js"]').forEach(function(s) {
582 if (env.base_url == null || env.base_url == '') {
583 var m = (s.getAttribute('src') || '').match(/^(.*)\/luci\.js(?:\?v=([^?]+))?$/);
584 if (m) {
585 env.base_url = m[1];
586 env.resource_version = m[2];
587 }
588 }
589 });
590
591 if (env.base_url == null)
592 this.error('InternalError', 'Cannot find url of luci.js');
593
594 Object.assign(this.env, env);
595
596 document.addEventListener('poll-start', function(ev) {
597 document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
598 e.style.display = (e.id == 'xhr_poll_status_off') ? 'none' : '';
599 });
600 });
601
602 document.addEventListener('poll-stop', function(ev) {
603 document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
604 e.style.display = (e.id == 'xhr_poll_status_on') ? 'none' : '';
605 });
606 });
607
608 var domReady = new Promise(function(resolveFn, rejectFn) {
609 document.addEventListener('DOMContentLoaded', resolveFn);
610 });
611
612 Promise.all([
613 domReady,
614 this.require('ui'),
615 this.require('rpc'),
616 this.require('form'),
617 this.probeRPCBaseURL()
618 ]).then(this.setupDOM.bind(this)).catch(this.error);
619
620 originalCBIInit = window.cbi_init;
621 window.cbi_init = function() {};
622 },
623
624 raise: function(type, fmt /*, ...*/) {
625 var e = null,
626 msg = fmt ? String.prototype.format.apply(fmt, this.varargs(arguments, 2)) : null,
627 stack = null;
628
629 if (type instanceof Error) {
630 e = type;
631
632 if (msg)
633 e.message = msg + ': ' + e.message;
634 }
635 else {
636 try { throw new Error('stacktrace') }
637 catch (e2) { stack = (e2.stack || '').split(/\n/) }
638
639 e = new (window[type || 'Error'] || Error)(msg || 'Unspecified error');
640 e.name = type || 'Error';
641 }
642
643 stack = (stack || []).map(function(frame) {
644 frame = frame.replace(/(.*?)@(.+):(\d+):(\d+)/g, 'at $1 ($2:$3:$4)').trim();
645 return frame ? ' ' + frame : '';
646 });
647
648 if (!/^ at /.test(stack[0]))
649 stack.shift();
650
651 if (/\braise /.test(stack[0]))
652 stack.shift();
653
654 if (/\berror /.test(stack[0]))
655 stack.shift();
656
657 if (stack.length)
658 e.message += '\n' + stack.join('\n');
659
660 if (window.console && console.debug)
661 console.debug(e);
662
663 throw e;
664 },
665
666 error: function(type, fmt /*, ...*/) {
667 try {
668 L.raise.apply(L, Array.prototype.slice.call(arguments));
669 }
670 catch (e) {
671 if (!e.reported) {
672 if (L.ui)
673 L.ui.addNotification(e.name || _('Runtime error'),
674 E('pre', {}, e.message), 'danger');
675 else
676 L.dom.content(document.querySelector('#maincontent'),
677 E('pre', { 'class': 'alert-message error' }, e.message));
678
679 e.reported = true;
680 }
681
682 throw e;
683 }
684 },
685
686 bind: function(fn, self /*, ... */) {
687 return Function.prototype.bind.apply(fn, this.varargs(arguments, 2, self));
688 },
689
690 /* Class require */
691 require: function(name, from) {
692 var L = this, url = null, from = from || [];
693
694 /* Class already loaded */
695 if (classes[name] != null) {
696 /* Circular dependency */
697 if (from.indexOf(name) != -1)
698 L.raise('DependencyError',
699 'Circular dependency: class "%s" depends on "%s"',
700 name, from.join('" which depends on "'));
701
702 return classes[name];
703 }
704
705 url = '%s/%s.js%s'.format(L.env.base_url, name.replace(/\./g, '/'), (L.env.resource_version ? '?v=' + L.env.resource_version : ''));
706 from = [ name ].concat(from);
707
708 var compileClass = function(res) {
709 if (!res.ok)
710 L.raise('NetworkError',
711 'HTTP error %d while loading class file "%s"', res.status, url);
712
713 var source = res.text(),
714 requirematch = /^require[ \t]+(\S+)(?:[ \t]+as[ \t]+([a-zA-Z_]\S*))?$/,
715 strictmatch = /^use[ \t]+strict$/,
716 depends = [],
717 args = '';
718
719 /* find require statements in source */
720 for (var i = 0, off = -1, quote = -1, esc = false; i < source.length; i++) {
721 var chr = source.charCodeAt(i);
722
723 if (esc) {
724 esc = false;
725 }
726 else if (chr == 92) {
727 esc = true;
728 }
729 else if (chr == quote) {
730 var s = source.substring(off, i),
731 m = requirematch.exec(s);
732
733 if (m) {
734 var dep = m[1], as = m[2] || dep.replace(/[^a-zA-Z0-9_]/g, '_');
735 depends.push(L.require(dep, from));
736 args += ', ' + as;
737 }
738 else if (!strictmatch.exec(s)) {
739 break;
740 }
741
742 off = -1;
743 quote = -1;
744 }
745 else if (quote == -1 && (chr == 34 || chr == 39)) {
746 off = i + 1;
747 quote = chr;
748 }
749 }
750
751 /* load dependencies and instantiate class */
752 return Promise.all(depends).then(function(instances) {
753 var _factory, _class;
754
755 try {
756 _factory = eval(
757 '(function(window, document, L%s) { %s })\n\n//# sourceURL=%s\n'
758 .format(args, source, res.url));
759 }
760 catch (error) {
761 L.raise('SyntaxError', '%s\n in %s:%s',
762 error.message, res.url, error.lineNumber || '?');
763 }
764
765 _factory.displayName = toCamelCase(name + 'ClassFactory');
766 _class = _factory.apply(_factory, [window, document, L].concat(instances));
767
768 if (!Class.isSubclass(_class))
769 L.error('TypeError', '"%s" factory yields invalid constructor', name);
770
771 if (_class.displayName == 'AnonymousClass')
772 _class.displayName = toCamelCase(name + 'Class');
773
774 var ptr = Object.getPrototypeOf(L),
775 parts = name.split(/\./),
776 instance = new _class();
777
778 for (var i = 0; ptr && i < parts.length - 1; i++)
779 ptr = ptr[parts[i]];
780
781 if (ptr)
782 ptr[parts[i]] = instance;
783
784 classes[name] = instance;
785
786 return instance;
787 });
788 };
789
790 /* Request class file */
791 classes[name] = Request.get(url, { cache: true }).then(compileClass);
792
793 return classes[name];
794 },
795
796 /* DOM setup */
797 probeRPCBaseURL: function() {
798 if (rpcBaseURL == null) {
799 try {
800 rpcBaseURL = window.sessionStorage.getItem('rpcBaseURL');
801 }
802 catch (e) { }
803 }
804
805 if (rpcBaseURL == null) {
806 var rpcFallbackURL = this.url('admin/ubus');
807
808 rpcBaseURL = Request.get('/ubus/').then(function(res) {
809 return (rpcBaseURL = (res.status == 400) ? '/ubus/' : rpcFallbackURL);
810 }, function() {
811 return (rpcBaseURL = rpcFallbackURL);
812 }).then(function(url) {
813 try {
814 window.sessionStorage.setItem('rpcBaseURL', url);
815 }
816 catch (e) { }
817
818 return url;
819 });
820 }
821
822 return Promise.resolve(rpcBaseURL);
823 },
824
825 probeSystemFeatures: function() {
826 var sessionid = classes.rpc.getSessionID();
827
828 if (sysFeatures == null) {
829 try {
830 var data = JSON.parse(window.sessionStorage.getItem('sysFeatures'));
831
832 if (this.isObject(data) && this.isObject(data[sessionid]))
833 sysFeatures = data[sessionid];
834 }
835 catch (e) {}
836 }
837
838 if (!this.isObject(sysFeatures)) {
839 sysFeatures = classes.rpc.declare({
840 object: 'luci',
841 method: 'getFeatures',
842 expect: { '': {} }
843 })().then(function(features) {
844 try {
845 var data = {};
846 data[sessionid] = features;
847
848 window.sessionStorage.setItem('sysFeatures', JSON.stringify(data));
849 }
850 catch (e) {}
851
852 sysFeatures = features;
853
854 return features;
855 });
856 }
857
858 return Promise.resolve(sysFeatures);
859 },
860
861 hasSystemFeature: function() {
862 var ft = sysFeatures[arguments[0]];
863
864 if (arguments.length == 2)
865 return this.isObject(ft) ? ft[arguments[1]] : null;
866
867 return (ft != null && ft != false);
868 },
869
870 notifySessionExpiry: function() {
871 Poll.stop();
872
873 L.ui.showModal(_('Session expired'), [
874 E('div', { class: 'alert-message warning' },
875 _('A new login is required since the authentication session expired.')),
876 E('div', { class: 'right' },
877 E('div', {
878 class: 'btn primary',
879 click: function() {
880 var loc = window.location;
881 window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
882 }
883 }, _('To login…')))
884 ]);
885
886 L.raise('SessionError', 'Login session is expired');
887 },
888
889 setupDOM: function(res) {
890 var domEv = res[0],
891 uiClass = res[1],
892 rpcClass = res[2],
893 formClass = res[3],
894 rpcBaseURL = res[4];
895
896 rpcClass.setBaseURL(rpcBaseURL);
897
898 rpcClass.addInterceptor(function(msg, req) {
899 if (!L.isObject(msg) || !L.isObject(msg.error) || msg.error.code != -32002)
900 return;
901
902 if (!L.isObject(req) || (req.object == 'session' && req.method == 'access'))
903 return;
904
905 return rpcClass.declare({
906 'object': 'session',
907 'method': 'access',
908 'params': [ 'scope', 'object', 'function' ],
909 'expect': { access: true }
910 })('uci', 'luci', 'read').catch(L.notifySessionExpiry);
911 });
912
913 Request.addInterceptor(function(res) {
914 var isDenied = false;
915
916 if (res.status == 403 && res.headers.get('X-LuCI-Login-Required') == 'yes')
917 isDenied = true;
918
919 if (!isDenied)
920 return;
921
922 L.notifySessionExpiry();
923 });
924
925 return this.probeSystemFeatures().finally(this.initDOM);
926 },
927
928 initDOM: function() {
929 originalCBIInit();
930 Poll.start();
931 document.dispatchEvent(new CustomEvent('luci-loaded'));
932 },
933
934 env: {},
935
936 /* URL construction helpers */
937 path: function(prefix, parts) {
938 var url = [ prefix || '' ];
939
940 for (var i = 0; i < parts.length; i++)
941 if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts[i]))
942 url.push('/', parts[i]);
943
944 if (url.length === 1)
945 url.push('/');
946
947 return url.join('');
948 },
949
950 url: function() {
951 return this.path(this.env.scriptname, arguments);
952 },
953
954 resource: function() {
955 return this.path(this.env.resource, arguments);
956 },
957
958 location: function() {
959 return this.path(this.env.scriptname, this.env.requestpath);
960 },
961
962
963 /* Data helpers */
964 isObject: function(val) {
965 return (val != null && typeof(val) == 'object');
966 },
967
968 sortedKeys: function(obj, key, sortmode) {
969 if (obj == null || typeof(obj) != 'object')
970 return [];
971
972 return Object.keys(obj).map(function(e) {
973 var v = (key != null) ? obj[e][key] : e;
974
975 switch (sortmode) {
976 case 'addr':
977 v = (v != null) ? v.replace(/(?:^|[.:])([0-9a-fA-F]{1,4})/g,
978 function(m0, m1) { return ('000' + m1.toLowerCase()).substr(-4) }) : null;
979 break;
980
981 case 'num':
982 v = (v != null) ? +v : null;
983 break;
984 }
985
986 return [ e, v ];
987 }).filter(function(e) {
988 return (e[1] != null);
989 }).sort(function(a, b) {
990 return (a[1] > b[1]);
991 }).map(function(e) {
992 return e[0];
993 });
994 },
995
996 toArray: function(val) {
997 if (val == null)
998 return [];
999 else if (Array.isArray(val))
1000 return val;
1001 else if (typeof(val) == 'object')
1002 return [ val ];
1003
1004 var s = String(val).trim();
1005
1006 if (s == '')
1007 return [];
1008
1009 return s.split(/\s+/);
1010 },
1011
1012
1013 /* HTTP resource fetching */
1014 get: function(url, args, cb) {
1015 return this.poll(null, url, args, cb, false);
1016 },
1017
1018 post: function(url, args, cb) {
1019 return this.poll(null, url, args, cb, true);
1020 },
1021
1022 poll: function(interval, url, args, cb, post) {
1023 if (interval !== null && interval <= 0)
1024 interval = this.env.pollinterval;
1025
1026 var data = post ? { token: this.env.token } : null,
1027 method = post ? 'POST' : 'GET';
1028
1029 if (!/^(?:\/|\S+:\/\/)/.test(url))
1030 url = this.url(url);
1031
1032 if (args != null)
1033 data = Object.assign(data || {}, args);
1034
1035 if (interval !== null)
1036 return Request.poll.add(interval, url, { method: method, query: data }, cb);
1037 else
1038 return Request.request(url, { method: method, query: data })
1039 .then(function(res) {
1040 var json = null;
1041 if (/^application\/json\b/.test(res.headers.get('Content-Type')))
1042 try { json = res.json() } catch(e) {}
1043 cb(res.xhr, json, res.duration);
1044 });
1045 },
1046
1047 stop: function(entry) { return Poll.remove(entry) },
1048 halt: function() { return Poll.stop() },
1049 run: function() { return Poll.start() },
1050
1051 /* DOM manipulation */
1052 dom: Class.singleton({
1053 __name__: 'LuCI.DOM',
1054
1055 elem: function(e) {
1056 return (e != null && typeof(e) == 'object' && 'nodeType' in e);
1057 },
1058
1059 parse: function(s) {
1060 var elem;
1061
1062 try {
1063 domParser = domParser || new DOMParser();
1064 elem = domParser.parseFromString(s, 'text/html').body.firstChild;
1065 }
1066 catch(e) {}
1067
1068 if (!elem) {
1069 try {
1070 dummyElem = dummyElem || document.createElement('div');
1071 dummyElem.innerHTML = s;
1072 elem = dummyElem.firstChild;
1073 }
1074 catch (e) {}
1075 }
1076
1077 return elem || null;
1078 },
1079
1080 matches: function(node, selector) {
1081 var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
1082 return m ? m.call(node, selector) : false;
1083 },
1084
1085 parent: function(node, selector) {
1086 if (this.elem(node) && node.closest)
1087 return node.closest(selector);
1088
1089 while (this.elem(node))
1090 if (this.matches(node, selector))
1091 return node;
1092 else
1093 node = node.parentNode;
1094
1095 return null;
1096 },
1097
1098 append: function(node, children) {
1099 if (!this.elem(node))
1100 return null;
1101
1102 if (Array.isArray(children)) {
1103 for (var i = 0; i < children.length; i++)
1104 if (this.elem(children[i]))
1105 node.appendChild(children[i]);
1106 else if (children !== null && children !== undefined)
1107 node.appendChild(document.createTextNode('' + children[i]));
1108
1109 return node.lastChild;
1110 }
1111 else if (typeof(children) === 'function') {
1112 return this.append(node, children(node));
1113 }
1114 else if (this.elem(children)) {
1115 return node.appendChild(children);
1116 }
1117 else if (children !== null && children !== undefined) {
1118 node.innerHTML = '' + children;
1119 return node.lastChild;
1120 }
1121
1122 return null;
1123 },
1124
1125 content: function(node, children) {
1126 if (!this.elem(node))
1127 return null;
1128
1129 var dataNodes = node.querySelectorAll('[data-idref]');
1130
1131 for (var i = 0; i < dataNodes.length; i++)
1132 delete this.registry[dataNodes[i].getAttribute('data-idref')];
1133
1134 while (node.firstChild)
1135 node.removeChild(node.firstChild);
1136
1137 return this.append(node, children);
1138 },
1139
1140 attr: function(node, key, val) {
1141 if (!this.elem(node))
1142 return null;
1143
1144 var attr = null;
1145
1146 if (typeof(key) === 'object' && key !== null)
1147 attr = key;
1148 else if (typeof(key) === 'string')
1149 attr = {}, attr[key] = val;
1150
1151 for (key in attr) {
1152 if (!attr.hasOwnProperty(key) || attr[key] == null)
1153 continue;
1154
1155 switch (typeof(attr[key])) {
1156 case 'function':
1157 node.addEventListener(key, attr[key]);
1158 break;
1159
1160 case 'object':
1161 node.setAttribute(key, JSON.stringify(attr[key]));
1162 break;
1163
1164 default:
1165 node.setAttribute(key, attr[key]);
1166 }
1167 }
1168 },
1169
1170 create: function() {
1171 var html = arguments[0],
1172 attr = arguments[1],
1173 data = arguments[2],
1174 elem;
1175
1176 if (!(attr instanceof Object) || Array.isArray(attr))
1177 data = attr, attr = null;
1178
1179 if (Array.isArray(html)) {
1180 elem = document.createDocumentFragment();
1181 for (var i = 0; i < html.length; i++)
1182 elem.appendChild(this.create(html[i]));
1183 }
1184 else if (this.elem(html)) {
1185 elem = html;
1186 }
1187 else if (html.charCodeAt(0) === 60) {
1188 elem = this.parse(html);
1189 }
1190 else {
1191 elem = document.createElement(html);
1192 }
1193
1194 if (!elem)
1195 return null;
1196
1197 this.attr(elem, attr);
1198 this.append(elem, data);
1199
1200 return elem;
1201 },
1202
1203 registry: {},
1204
1205 data: function(node, key, val) {
1206 var id = node.getAttribute('data-idref');
1207
1208 /* clear all data */
1209 if (arguments.length > 1 && key == null) {
1210 if (id != null) {
1211 node.removeAttribute('data-idref');
1212 val = this.registry[id]
1213 delete this.registry[id];
1214 return val;
1215 }
1216
1217 return null;
1218 }
1219
1220 /* clear a key */
1221 else if (arguments.length > 2 && key != null && val == null) {
1222 if (id != null) {
1223 val = this.registry[id][key];
1224 delete this.registry[id][key];
1225 return val;
1226 }
1227
1228 return null;
1229 }
1230
1231 /* set a key */
1232 else if (arguments.length > 2 && key != null && val != null) {
1233 if (id == null) {
1234 do { id = Math.floor(Math.random() * 0xffffffff).toString(16) }
1235 while (this.registry.hasOwnProperty(id));
1236
1237 node.setAttribute('data-idref', id);
1238 this.registry[id] = {};
1239 }
1240
1241 return (this.registry[id][key] = val);
1242 }
1243
1244 /* get all data */
1245 else if (arguments.length == 1) {
1246 if (id != null)
1247 return this.registry[id];
1248
1249 return null;
1250 }
1251
1252 /* get a key */
1253 else if (arguments.length == 2) {
1254 if (id != null)
1255 return this.registry[id][key];
1256 }
1257
1258 return null;
1259 },
1260
1261 bindClassInstance: function(node, inst) {
1262 if (!(inst instanceof Class))
1263 L.error('TypeError', 'Argument must be a class instance');
1264
1265 return this.data(node, '_class', inst);
1266 },
1267
1268 findClassInstance: function(node) {
1269 var inst = null;
1270
1271 do {
1272 inst = this.data(node, '_class');
1273 node = node.parentNode;
1274 }
1275 while (!(inst instanceof Class) && node != null);
1276
1277 return inst;
1278 },
1279
1280 callClassMethod: function(node, method /*, ... */) {
1281 var inst = this.findClassInstance(node);
1282
1283 if (inst == null || typeof(inst[method]) != 'function')
1284 return null;
1285
1286 return inst[method].apply(inst, inst.varargs(arguments, 2));
1287 },
1288
1289 isEmpty: function(node, ignoreFn) {
1290 for (var child = node.firstElementChild; child != null; child = child.nextElementSibling)
1291 if (!child.classList.contains('hidden') && (!ignoreFn || !ignoreFn(child)))
1292 return false;
1293
1294 return true;
1295 }
1296 }),
1297
1298 Poll: Poll,
1299 Class: Class,
1300 Request: Request,
1301
1302 view: Class.extend({
1303 __name__: 'LuCI.View',
1304
1305 __init__: function() {
1306 var vp = document.getElementById('view');
1307
1308 L.dom.content(vp, E('div', { 'class': 'spinning' }, _('Loading view…')));
1309
1310 return Promise.resolve(this.load())
1311 .then(L.bind(this.render, this))
1312 .then(L.bind(function(nodes) {
1313 var vp = document.getElementById('view');
1314
1315 L.dom.content(vp, nodes);
1316 L.dom.append(vp, this.addFooter());
1317 }, this)).catch(L.error);
1318 },
1319
1320 load: function() {},
1321 render: function() {},
1322
1323 handleSave: function(ev) {
1324 var tasks = [];
1325
1326 document.getElementById('maincontent')
1327 .querySelectorAll('.cbi-map').forEach(function(map) {
1328 tasks.push(L.dom.callClassMethod(map, 'save'));
1329 });
1330
1331 return Promise.all(tasks);
1332 },
1333
1334 handleSaveApply: function(ev) {
1335 return this.handleSave(ev).then(function() {
1336 L.ui.changes.apply(true);
1337 });
1338 },
1339
1340 handleReset: function(ev) {
1341 var tasks = [];
1342
1343 document.getElementById('maincontent')
1344 .querySelectorAll('.cbi-map').forEach(function(map) {
1345 tasks.push(L.dom.callClassMethod(map, 'reset'));
1346 });
1347
1348 return Promise.all(tasks);
1349 },
1350
1351 addFooter: function() {
1352 var footer = E([]);
1353
1354 if (this.handleSaveApply || this.handleSave || this.handleReset) {
1355 footer.appendChild(E('div', { 'class': 'cbi-page-actions' }, [
1356 this.handleSaveApply ? E('button', {
1357 'class': 'cbi-button cbi-button-apply',
1358 'click': L.ui.createHandlerFn(this, 'handleSaveApply')
1359 }, [ _('Save & Apply') ]) : '', ' ',
1360 this.handleSave ? E('button', {
1361 'class': 'cbi-button cbi-button-save',
1362 'click': L.ui.createHandlerFn(this, 'handleSave')
1363 }, [ _('Save') ]) : '', ' ',
1364 this.handleReset ? E('button', {
1365 'class': 'cbi-button cbi-button-reset',
1366 'click': L.ui.createHandlerFn(this, 'handleReset')
1367 }, [ _('Reset') ]) : ''
1368 ]));
1369 }
1370
1371 return footer;
1372 }
1373 })
1374 });
1375
1376 var XHR = Class.extend({
1377 __name__: 'LuCI.XHR',
1378 __init__: function() {
1379 if (window.console && console.debug)
1380 console.debug('Direct use XHR() is deprecated, please use L.Request instead');
1381 },
1382
1383 _response: function(cb, res, json, duration) {
1384 if (this.active)
1385 cb(res, json, duration);
1386 delete this.active;
1387 },
1388
1389 get: function(url, data, callback, timeout) {
1390 this.active = true;
1391 L.get(url, data, this._response.bind(this, callback), timeout);
1392 },
1393
1394 post: function(url, data, callback, timeout) {
1395 this.active = true;
1396 L.post(url, data, this._response.bind(this, callback), timeout);
1397 },
1398
1399 cancel: function() { delete this.active },
1400 busy: function() { return (this.active === true) },
1401 abort: function() {},
1402 send_form: function() { L.error('InternalError', 'Not implemented') },
1403 });
1404
1405 XHR.get = function() { return window.L.get.apply(window.L, arguments) };
1406 XHR.post = function() { return window.L.post.apply(window.L, arguments) };
1407 XHR.poll = function() { return window.L.poll.apply(window.L, arguments) };
1408 XHR.stop = Request.poll.remove.bind(Request.poll);
1409 XHR.halt = Request.poll.stop.bind(Request.poll);
1410 XHR.run = Request.poll.start.bind(Request.poll);
1411 XHR.running = Request.poll.active.bind(Request.poll);
1412
1413 window.XHR = XHR;
1414 window.LuCI = LuCI;
1415 })(window, document);