luci-base: luci.js: fix undefined "this" when /ubus/ is unavailable
[oweals/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);