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