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