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