Merge pull request #3291 from rs/feature_nextdns
[project/luci.git] / modules / luci-base / htdocs / luci-static / resources / luci.js
1 /**
2 * @class LuCI
3 * @classdesc
4 *
5 * This is the LuCI base class. It is automatically instantiated and
6 * accessible using the global `L` variable.
7 *
8 * @param {Object} env
9 * The environment settings to use for the LuCI runtime.
10 */
11
12 (function(window, document, undefined) {
13 'use strict';
14
15 /* Object.assign polyfill for IE */
16 if (typeof Object.assign !== 'function') {
17 Object.defineProperty(Object, 'assign', {
18 value: function assign(target, varArgs) {
19 if (target == null)
20 throw new TypeError('Cannot convert undefined or null to object');
21
22 var to = Object(target);
23
24 for (var index = 1; index < arguments.length; index++)
25 if (arguments[index] != null)
26 for (var nextKey in arguments[index])
27 if (Object.prototype.hasOwnProperty.call(arguments[index], nextKey))
28 to[nextKey] = arguments[index][nextKey];
29
30 return to;
31 },
32 writable: true,
33 configurable: true
34 });
35 }
36
37 /* Promise.finally polyfill */
38 if (typeof Promise.prototype.finally !== 'function') {
39 Promise.prototype.finally = function(fn) {
40 var onFinally = function(cb) {
41 return Promise.resolve(fn.call(this)).then(cb);
42 };
43
44 return this.then(
45 function(result) { return onFinally.call(this, function() { return result }) },
46 function(reason) { return onFinally.call(this, function() { return Promise.reject(reason) }) }
47 );
48 };
49 }
50
51 /*
52 * Class declaration and inheritance helper
53 */
54
55 var toCamelCase = function(s) {
56 return s.replace(/(?:^|[\. -])(.)/g, function(m0, m1) { return m1.toUpperCase() });
57 };
58
59 /**
60 * @class Class
61 * @hideconstructor
62 * @memberof LuCI
63 * @classdesc
64 *
65 * `LuCI.Class` is the abstract base class all LuCI classes inherit from.
66 *
67 * It provides simple means to create subclasses of given classes and
68 * implements prototypal inheritance.
69 */
70 var superContext = null, Class = Object.assign(function() {}, {
71 /**
72 * Extends this base class with the properties described in
73 * `properties` and returns a new subclassed Class instance
74 *
75 * @memberof LuCI.Class
76 *
77 * @param {Object<string, *>} properties
78 * An object describing the properties to add to the new
79 * subclass.
80 *
81 * @returns {LuCI.Class}
82 * Returns a new LuCI.Class sublassed from this class, extended
83 * by the given properties and with its prototype set to this base
84 * class to enable inheritance. The resulting value represents a
85 * class constructor and can be instantiated with `new`.
86 */
87 extend: function(properties) {
88 var props = {
89 __base__: { value: this.prototype },
90 __name__: { value: properties.__name__ || 'anonymous' }
91 };
92
93 var ClassConstructor = function() {
94 if (!(this instanceof ClassConstructor))
95 throw new TypeError('Constructor must not be called without "new"');
96
97 if (Object.getPrototypeOf(this).hasOwnProperty('__init__')) {
98 if (typeof(this.__init__) != 'function')
99 throw new TypeError('Class __init__ member is not a function');
100
101 this.__init__.apply(this, arguments)
102 }
103 else {
104 this.super('__init__', arguments);
105 }
106 };
107
108 for (var key in properties)
109 if (!props[key] && properties.hasOwnProperty(key))
110 props[key] = { value: properties[key], writable: true };
111
112 ClassConstructor.prototype = Object.create(this.prototype, props);
113 ClassConstructor.prototype.constructor = ClassConstructor;
114 Object.assign(ClassConstructor, this);
115 ClassConstructor.displayName = toCamelCase(props.__name__.value + 'Class');
116
117 return ClassConstructor;
118 },
119
120 /**
121 * Extends this base class with the properties described in
122 * `properties`, instantiates the resulting subclass using
123 * the additional optional arguments passed to this function
124 * and returns the resulting subclassed Class instance.
125 *
126 * This function serves as a convenience shortcut for
127 * {@link LuCI.Class.extend Class.extend()} and subsequent
128 * `new`.
129 *
130 * @memberof LuCI.Class
131 *
132 * @param {Object<string, *>} properties
133 * An object describing the properties to add to the new
134 * subclass.
135 *
136 * @param {...*} [new_args]
137 * Specifies arguments to be passed to the subclass constructor
138 * as-is in order to instantiate the new subclass.
139 *
140 * @returns {LuCI.Class}
141 * Returns a new LuCI.Class instance extended by the given
142 * properties with its prototype set to this base class to
143 * enable inheritance.
144 */
145 singleton: function(properties /*, ... */) {
146 return Class.extend(properties)
147 .instantiate(Class.prototype.varargs(arguments, 1));
148 },
149
150 /**
151 * Calls the class constructor using `new` with the given argument
152 * array being passed as variadic parameters to the constructor.
153 *
154 * @memberof LuCI.Class
155 *
156 * @param {Array<*>} params
157 * An array of arbitrary values which will be passed as arguments
158 * to the constructor function.
159 *
160 * @param {...*} [new_args]
161 * Specifies arguments to be passed to the subclass constructor
162 * as-is in order to instantiate the new subclass.
163 *
164 * @returns {LuCI.Class}
165 * Returns a new LuCI.Class instance extended by the given
166 * properties with its prototype set to this base class to
167 * enable inheritance.
168 */
169 instantiate: function(args) {
170 return new (Function.prototype.bind.apply(this,
171 Class.prototype.varargs(args, 0, null)))();
172 },
173
174 /* unused */
175 call: function(self, method) {
176 if (typeof(this.prototype[method]) != 'function')
177 throw new ReferenceError(method + ' is not defined in class');
178
179 return this.prototype[method].apply(self, self.varargs(arguments, 1));
180 },
181
182 /**
183 * Checks whether the given class value is a subclass of this class.
184 *
185 * @memberof LuCI.Class
186 *
187 * @param {LuCI.Class} classValue
188 * The class object to test.
189 *
190 * @returns {boolean}
191 * Returns `true` when the given `classValue` is a subclass of this
192 * class or `false` if the given value is not a valid class or not
193 * a subclass of this class'.
194 */
195 isSubclass: function(classValue) {
196 return (classValue != null &&
197 typeof(classValue) == 'function' &&
198 classValue.prototype instanceof this);
199 },
200
201 prototype: {
202 /**
203 * Extract all values from the given argument array beginning from
204 * `offset` and prepend any further given optional parameters to
205 * the beginning of the resulting array copy.
206 *
207 * @memberof LuCI.Class
208 * @instance
209 *
210 * @param {Array<*>} args
211 * The array to extract the values from.
212 *
213 * @param {number} offset
214 * The offset from which to extract the values. An offset of `0`
215 * would copy all values till the end.
216 *
217 * @param {...*} [extra_args]
218 * Extra arguments to add to prepend to the resultung array.
219 *
220 * @returns {Array<*>}
221 * Returns a new array consisting of the optional extra arguments
222 * and the values extracted from the `args` array beginning with
223 * `offset`.
224 */
225 varargs: function(args, offset /*, ... */) {
226 return Array.prototype.slice.call(arguments, 2)
227 .concat(Array.prototype.slice.call(args, offset));
228 },
229
230 /**
231 * Walks up the parent class chain and looks for a class member
232 * called `key` in any of the parent classes this class inherits
233 * from. Returns the member value of the superclass or calls the
234 * member as function and returns its return value when the
235 * optional `callArgs` array is given.
236 *
237 * This function has two signatures and is sensitive to the
238 * amount of arguments passed to it:
239 * - `super('key')` -
240 * Returns the value of `key` when found within one of the
241 * parent classes.
242 * - `super('key', ['arg1', 'arg2'])` -
243 * Calls the `key()` method with parameters `arg1` and `arg2`
244 * when found within one of the parent classes.
245 *
246 * @memberof LuCI.Class
247 * @instance
248 *
249 * @param {string} key
250 * The name of the superclass member to retrieve.
251 *
252 * @param {Array<*>} [callArgs]
253 * An optional array of function call parameters to use. When
254 * this parameter is specified, the found member value is called
255 * as function using the values of this array as arguments.
256 *
257 * @throws {ReferenceError}
258 * Throws a `ReferenceError` when `callArgs` are specified and
259 * the found member named by `key` is not a function value.
260 *
261 * @returns {*|null}
262 * Returns the value of the found member or the return value of
263 * the call to the found method. Returns `null` when no member
264 * was found in the parent class chain or when the call to the
265 * superclass method returned `null`.
266 */
267 super: function(key, callArgs) {
268 for (superContext = Object.getPrototypeOf(superContext ||
269 Object.getPrototypeOf(this));
270 superContext && !superContext.hasOwnProperty(key);
271 superContext = Object.getPrototypeOf(superContext)) { }
272
273 if (!superContext)
274 return null;
275
276 var res = superContext[key];
277
278 if (arguments.length > 1) {
279 if (typeof(res) != 'function')
280 throw new ReferenceError(key + ' is not a function in base class');
281
282 if (typeof(callArgs) != 'object')
283 callArgs = this.varargs(arguments, 1);
284
285 res = res.apply(this, callArgs);
286 }
287
288 superContext = null;
289
290 return res;
291 },
292
293 /**
294 * Returns a string representation of this class.
295 *
296 * @returns {string}
297 * Returns a string representation of this class containing the
298 * constructor functions `displayName` and describing the class
299 * members and their respective types.
300 */
301 toString: function() {
302 var s = '[' + this.constructor.displayName + ']', f = true;
303 for (var k in this) {
304 if (this.hasOwnProperty(k)) {
305 s += (f ? ' {\n' : '') + ' ' + k + ': ' + typeof(this[k]) + '\n';
306 f = false;
307 }
308 }
309 return s + (f ? '' : '}');
310 }
311 }
312 });
313
314
315 /**
316 * @class
317 * @memberof LuCI
318 * @hideconstructor
319 * @classdesc
320 *
321 * The `Headers` class is an internal utility class exposed in HTTP
322 * response objects using the `response.headers` property.
323 */
324 var Headers = Class.extend(/** @lends LuCI.Headers.prototype */ {
325 __name__: 'LuCI.XHR.Headers',
326 __init__: function(xhr) {
327 var hdrs = this.headers = {};
328 xhr.getAllResponseHeaders().split(/\r\n/).forEach(function(line) {
329 var m = /^([^:]+):(.*)$/.exec(line);
330 if (m != null)
331 hdrs[m[1].trim().toLowerCase()] = m[2].trim();
332 });
333 },
334
335 /**
336 * Checks whether the given header name is present.
337 * Note: Header-Names are case-insensitive.
338 *
339 * @instance
340 * @memberof LuCI.Headers
341 * @param {string} name
342 * The header name to check
343 *
344 * @returns {boolean}
345 * Returns `true` if the header name is present, `false` otherwise
346 */
347 has: function(name) {
348 return this.headers.hasOwnProperty(String(name).toLowerCase());
349 },
350
351 /**
352 * Returns the value of the given header name.
353 * Note: Header-Names are case-insensitive.
354 *
355 * @instance
356 * @memberof LuCI.Headers
357 * @param {string} name
358 * The header name to read
359 *
360 * @returns {string|null}
361 * The value of the given header name or `null` if the header isn't present.
362 */
363 get: function(name) {
364 var key = String(name).toLowerCase();
365 return this.headers.hasOwnProperty(key) ? this.headers[key] : null;
366 }
367 });
368
369 /**
370 * @class
371 * @memberof LuCI
372 * @hideconstructor
373 * @classdesc
374 *
375 * The `Response` class is an internal utility class representing HTTP responses.
376 */
377 var Response = Class.extend({
378 __name__: 'LuCI.XHR.Response',
379 __init__: function(xhr, url, duration, headers, content) {
380 /**
381 * Describes whether the response is successful (status codes `200..299`) or not
382 * @instance
383 * @memberof LuCI.Response
384 * @name ok
385 * @type {boolean}
386 */
387 this.ok = (xhr.status >= 200 && xhr.status <= 299);
388
389 /**
390 * The numeric HTTP status code of the response
391 * @instance
392 * @memberof LuCI.Response
393 * @name status
394 * @type {number}
395 */
396 this.status = xhr.status;
397
398 /**
399 * The HTTP status description message of the response
400 * @instance
401 * @memberof LuCI.Response
402 * @name statusText
403 * @type {string}
404 */
405 this.statusText = xhr.statusText;
406
407 /**
408 * The HTTP headers of the response
409 * @instance
410 * @memberof LuCI.Response
411 * @name headers
412 * @type {LuCI.Headers}
413 */
414 this.headers = (headers != null) ? headers : new Headers(xhr);
415
416 /**
417 * The total duration of the HTTP request in milliseconds
418 * @instance
419 * @memberof LuCI.Response
420 * @name duration
421 * @type {number}
422 */
423 this.duration = duration;
424
425 /**
426 * The final URL of the request, i.e. after following redirects.
427 * @instance
428 * @memberof LuCI.Response
429 * @name url
430 * @type {string}
431 */
432 this.url = url;
433
434 /* privates */
435 this.xhr = xhr;
436
437 if (content != null && typeof(content) == 'object') {
438 this.responseJSON = content;
439 this.responseText = null;
440 }
441 else if (content != null) {
442 this.responseJSON = null;
443 this.responseText = String(content);
444 }
445 else {
446 this.responseJSON = null;
447 this.responseText = xhr.responseText;
448 }
449 },
450
451 /**
452 * Clones the given response object, optionally overriding the content
453 * of the cloned instance.
454 *
455 * @instance
456 * @memberof LuCI.Response
457 * @param {*} [content]
458 * Override the content of the cloned response. Object values will be
459 * treated as JSON response data, all other types will be converted
460 * using `String()` and treated as response text.
461 *
462 * @returns {LuCI.Response}
463 * The cloned `Response` instance.
464 */
465 clone: function(content) {
466 var copy = new Response(this.xhr, this.url, this.duration, this.headers, content);
467
468 copy.ok = this.ok;
469 copy.status = this.status;
470 copy.statusText = this.statusText;
471
472 return copy;
473 },
474
475 /**
476 * Access the response content as JSON data.
477 *
478 * @instance
479 * @memberof LuCI.Response
480 * @throws {SyntaxError}
481 * Throws `SyntaxError` if the content isn't valid JSON.
482 *
483 * @returns {*}
484 * The parsed JSON data.
485 */
486 json: function() {
487 if (this.responseJSON == null)
488 this.responseJSON = JSON.parse(this.responseText);
489
490 return this.responseJSON;
491 },
492
493 /**
494 * Access the response content as string.
495 *
496 * @instance
497 * @memberof LuCI.Response
498 * @returns {string}
499 * The response content.
500 */
501 text: function() {
502 if (this.responseText == null && this.responseJSON != null)
503 this.responseText = JSON.stringify(this.responseJSON);
504
505 return this.responseText;
506 }
507 });
508
509
510 var requestQueue = [];
511
512 function isQueueableRequest(opt) {
513 if (!classes.rpc)
514 return false;
515
516 if (opt.method != 'POST' || typeof(opt.content) != 'object')
517 return false;
518
519 if (opt.nobatch === true)
520 return false;
521
522 var rpcBaseURL = Request.expandURL(classes.rpc.getBaseURL());
523
524 return (rpcBaseURL != null && opt.url.indexOf(rpcBaseURL) == 0);
525 }
526
527 function flushRequestQueue() {
528 if (!requestQueue.length)
529 return;
530
531 var reqopt = Object.assign({}, requestQueue[0][0], { content: [], nobatch: true }),
532 batch = [];
533
534 for (var i = 0; i < requestQueue.length; i++) {
535 batch[i] = requestQueue[i];
536 reqopt.content[i] = batch[i][0].content;
537 }
538
539 requestQueue.length = 0;
540
541 Request.request(rpcBaseURL, reqopt).then(function(reply) {
542 var json = null, req = null;
543
544 try { json = reply.json() }
545 catch(e) { }
546
547 while ((req = batch.shift()) != null)
548 if (Array.isArray(json) && json.length)
549 req[2].call(reqopt, reply.clone(json.shift()));
550 else
551 req[1].call(reqopt, new Error('No related RPC reply'));
552 }).catch(function(error) {
553 var req = null;
554
555 while ((req = batch.shift()) != null)
556 req[1].call(reqopt, error);
557 });
558 }
559
560 /**
561 * @class
562 * @memberof LuCI
563 * @hideconstructor
564 * @classdesc
565 *
566 * The `Request` class allows initiating HTTP requests and provides utilities
567 * for dealing with responses.
568 */
569 var Request = Class.singleton(/** @lends LuCI.Request.prototype */ {
570 __name__: 'LuCI.Request',
571
572 interceptors: [],
573
574 /**
575 * Turn the given relative URL into an absolute URL if necessary.
576 *
577 * @instance
578 * @memberof LuCI.Request
579 * @param {string} url
580 * The URL to convert.
581 *
582 * @returns {string}
583 * The absolute URL derived from the given one, or the original URL
584 * if it already was absolute.
585 */
586 expandURL: function(url) {
587 if (!/^(?:[^/]+:)?\/\//.test(url))
588 url = location.protocol + '//' + location.host + url;
589
590 return url;
591 },
592
593 /**
594 * @typedef {Object} RequestOptions
595 * @memberof LuCI.Request
596 *
597 * @property {string} [method=GET]
598 * The HTTP method to use, e.g. `GET` or `POST`.
599 *
600 * @property {Object<string, Object|string>} [query]
601 * Query string data to append to the URL. Non-string values of the
602 * given object will be converted to JSON.
603 *
604 * @property {boolean} [cache=false]
605 * Specifies whether the HTTP response may be retrieved from cache.
606 *
607 * @property {string} [username]
608 * Provides a username for HTTP basic authentication.
609 *
610 * @property {string} [password]
611 * Provides a password for HTTP basic authentication.
612 *
613 * @property {number} [timeout]
614 * Specifies the request timeout in seconds.
615 *
616 * @property {boolean} [credentials=false]
617 * Whether to include credentials such as cookies in the request.
618 *
619 * @property {*} [content]
620 * Specifies the HTTP message body to send along with the request.
621 * If the value is a function, it is invoked and the return value
622 * used as content, if it is a FormData instance, it is used as-is,
623 * if it is an object, it will be converted to JSON, in all other
624 * cases it is converted to a string.
625 *
626 * @property {Object<string, string>} [header]
627 * Specifies HTTP headers to set for the request.
628 *
629 * @property {function} [progress]
630 * An optional request callback function which receives ProgressEvent
631 * instances as sole argument during the HTTP request transfer.
632 */
633
634 /**
635 * Initiate an HTTP request to the given target.
636 *
637 * @instance
638 * @memberof LuCI.Request
639 * @param {string} target
640 * The URL to request.
641 *
642 * @param {LuCI.Request.RequestOptions} [options]
643 * Additional options to configure the request.
644 *
645 * @returns {Promise<LuCI.Response>}
646 * The resulting HTTP response.
647 */
648 request: function(target, options) {
649 var state = { xhr: new XMLHttpRequest(), url: this.expandURL(target), start: Date.now() },
650 opt = Object.assign({}, options, state),
651 content = null,
652 contenttype = null,
653 callback = this.handleReadyStateChange;
654
655 return new Promise(function(resolveFn, rejectFn) {
656 opt.xhr.onreadystatechange = callback.bind(opt, resolveFn, rejectFn);
657 opt.method = String(opt.method || 'GET').toUpperCase();
658
659 if ('query' in opt) {
660 var q = (opt.query != null) ? Object.keys(opt.query).map(function(k) {
661 if (opt.query[k] != null) {
662 var v = (typeof(opt.query[k]) == 'object')
663 ? JSON.stringify(opt.query[k])
664 : String(opt.query[k]);
665
666 return '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(v));
667 }
668 else {
669 return encodeURIComponent(k);
670 }
671 }).join('&') : '';
672
673 if (q !== '') {
674 switch (opt.method) {
675 case 'GET':
676 case 'HEAD':
677 case 'OPTIONS':
678 opt.url += ((/\?/).test(opt.url) ? '&' : '?') + q;
679 break;
680
681 default:
682 if (content == null) {
683 content = q;
684 contenttype = 'application/x-www-form-urlencoded';
685 }
686 }
687 }
688 }
689
690 if (!opt.cache)
691 opt.url += ((/\?/).test(opt.url) ? '&' : '?') + (new Date()).getTime();
692
693 if (isQueueableRequest(opt)) {
694 requestQueue.push([opt, rejectFn, resolveFn]);
695 requestAnimationFrame(flushRequestQueue);
696 return;
697 }
698
699 if ('username' in opt && 'password' in opt)
700 opt.xhr.open(opt.method, opt.url, true, opt.username, opt.password);
701 else
702 opt.xhr.open(opt.method, opt.url, true);
703
704 opt.xhr.responseType = 'text';
705
706 if ('overrideMimeType' in opt.xhr)
707 opt.xhr.overrideMimeType('application/octet-stream');
708
709 if ('timeout' in opt)
710 opt.xhr.timeout = +opt.timeout;
711
712 if ('credentials' in opt)
713 opt.xhr.withCredentials = !!opt.credentials;
714
715 if (opt.content != null) {
716 switch (typeof(opt.content)) {
717 case 'function':
718 content = opt.content(xhr);
719 break;
720
721 case 'object':
722 if (!(opt.content instanceof FormData)) {
723 content = JSON.stringify(opt.content);
724 contenttype = 'application/json';
725 }
726 else {
727 content = opt.content;
728 }
729 break;
730
731 default:
732 content = String(opt.content);
733 }
734 }
735
736 if ('headers' in opt)
737 for (var header in opt.headers)
738 if (opt.headers.hasOwnProperty(header)) {
739 if (header.toLowerCase() != 'content-type')
740 opt.xhr.setRequestHeader(header, opt.headers[header]);
741 else
742 contenttype = opt.headers[header];
743 }
744
745 if ('progress' in opt && 'upload' in opt.xhr)
746 opt.xhr.upload.addEventListener('progress', opt.progress);
747
748 if (contenttype != null)
749 opt.xhr.setRequestHeader('Content-Type', contenttype);
750
751 try {
752 opt.xhr.send(content);
753 }
754 catch (e) {
755 rejectFn.call(opt, e);
756 }
757 });
758 },
759
760 handleReadyStateChange: function(resolveFn, rejectFn, ev) {
761 var xhr = this.xhr,
762 duration = Date.now() - this.start;
763
764 if (xhr.readyState !== 4)
765 return;
766
767 if (xhr.status === 0 && xhr.statusText === '') {
768 if (duration >= this.timeout)
769 rejectFn.call(this, new Error('XHR request timed out'));
770 else
771 rejectFn.call(this, new Error('XHR request aborted by browser'));
772 }
773 else {
774 var response = new Response(
775 xhr, xhr.responseURL || this.url, duration);
776
777 Promise.all(Request.interceptors.map(function(fn) { return fn(response) }))
778 .then(resolveFn.bind(this, response))
779 .catch(rejectFn.bind(this));
780 }
781 },
782
783 /**
784 * Initiate an HTTP GET request to the given target.
785 *
786 * @instance
787 * @memberof LuCI.Request
788 * @param {string} target
789 * The URL to request.
790 *
791 * @param {LuCI.Request.RequestOptions} [options]
792 * Additional options to configure the request.
793 *
794 * @returns {Promise<LuCI.Response>}
795 * The resulting HTTP response.
796 */
797 get: function(url, options) {
798 return this.request(url, Object.assign({ method: 'GET' }, options));
799 },
800
801 /**
802 * Initiate an HTTP POST request to the given target.
803 *
804 * @instance
805 * @memberof LuCI.Request
806 * @param {string} target
807 * The URL to request.
808 *
809 * @param {*} [data]
810 * The request data to send, see {@link LuCI.Request.RequestOptions} for details.
811 *
812 * @param {LuCI.Request.RequestOptions} [options]
813 * Additional options to configure the request.
814 *
815 * @returns {Promise<LuCI.Response>}
816 * The resulting HTTP response.
817 */
818 post: function(url, data, options) {
819 return this.request(url, Object.assign({ method: 'POST', content: data }, options));
820 },
821
822 /**
823 * Interceptor functions are invoked whenever an HTTP reply is received, in the order
824 * these functions have been registered.
825 * @callback LuCI.Request.interceptorFn
826 * @param {LuCI.Response} res
827 * The HTTP response object
828 */
829
830 /**
831 * Register an HTTP response interceptor function. Interceptor
832 * functions are useful to perform default actions on incoming HTTP
833 * responses, such as checking for expired authentication or for
834 * implementing request retries before returning a failure.
835 *
836 * @instance
837 * @memberof LuCI.Request
838 * @param {LuCI.Request.interceptorFn} interceptorFn
839 * The interceptor function to register.
840 *
841 * @returns {LuCI.Request.interceptorFn}
842 * The registered function.
843 */
844 addInterceptor: function(interceptorFn) {
845 if (typeof(interceptorFn) == 'function')
846 this.interceptors.push(interceptorFn);
847 return interceptorFn;
848 },
849
850 /**
851 * Remove an HTTP response interceptor function. The passed function
852 * value must be the very same value that was used to register the
853 * function.
854 *
855 * @instance
856 * @memberof LuCI.Request
857 * @param {LuCI.Request.interceptorFn} interceptorFn
858 * The interceptor function to remove.
859 *
860 * @returns {boolean}
861 * Returns `true` if any function has been removed, else `false`.
862 */
863 removeInterceptor: function(interceptorFn) {
864 var oldlen = this.interceptors.length, i = oldlen;
865 while (i--)
866 if (this.interceptors[i] === interceptorFn)
867 this.interceptors.splice(i, 1);
868 return (this.interceptors.length < oldlen);
869 },
870
871 /**
872 * @class
873 * @memberof LuCI.Request
874 * @hideconstructor
875 * @classdesc
876 *
877 * The `Request.poll` class provides some convience wrappers around
878 * {@link LuCI.Poll} mainly to simplify registering repeating HTTP
879 * request calls as polling functions.
880 */
881 poll: {
882 /**
883 * The callback function is invoked whenever an HTTP reply to a
884 * polled request is received or when the polled request timed
885 * out.
886 *
887 * @callback LuCI.Request.poll~callbackFn
888 * @param {LuCI.Response} res
889 * The HTTP response object.
890 *
891 * @param {*} data
892 * The response JSON if the response could be parsed as such,
893 * else `null`.
894 *
895 * @param {number} duration
896 * The total duration of the request in milliseconds.
897 */
898
899 /**
900 * Register a repeating HTTP request with an optional callback
901 * to invoke whenever a response for the request is received.
902 *
903 * @instance
904 * @memberof LuCI.Request.poll
905 * @param {number} interval
906 * The poll interval in seconds.
907 *
908 * @param {string} url
909 * The URL to request on each poll.
910 *
911 * @param {LuCI.Request.RequestOptions} [options]
912 * Additional options to configure the request.
913 *
914 * @param {LuCI.Request.poll~callbackFn} [callback]
915 * {@link LuCI.Request.poll~callbackFn Callback} function to
916 * invoke for each HTTP reply.
917 *
918 * @throws {TypeError}
919 * Throws `TypeError` when an invalid interval was passed.
920 *
921 * @returns {function}
922 * Returns the internally created poll function.
923 */
924 add: function(interval, url, options, callback) {
925 if (isNaN(interval) || interval <= 0)
926 throw new TypeError('Invalid poll interval');
927
928 var ival = interval >>> 0,
929 opts = Object.assign({}, options, { timeout: ival * 1000 - 5 });
930
931 var fn = function() {
932 return Request.request(url, options).then(function(res) {
933 if (!Poll.active())
934 return;
935
936 try {
937 callback(res, res.json(), res.duration);
938 }
939 catch (err) {
940 callback(res, null, res.duration);
941 }
942 });
943 };
944
945 return (Poll.add(fn, ival) ? fn : null);
946 },
947
948 /**
949 * Remove a polling request that has been previously added using `add()`.
950 * This function is essentially a wrapper around
951 * {@link LuCI.Poll.remove LuCI.Poll.remove()}.
952 *
953 * @instance
954 * @memberof LuCI.Request.poll
955 * @param {function} entry
956 * The poll function returned by {@link LuCI.Request.poll#add add()}.
957 *
958 * @returns {boolean}
959 * Returns `true` if any function has been removed, else `false`.
960 */
961 remove: function(entry) { return Poll.remove(entry) },
962
963 /**
964 * Alias for {@link LuCI.Poll.start LuCI.Poll.start()}.
965 *
966 * @instance
967 * @memberof LuCI.Request.poll
968 */
969 start: function() { return Poll.start() },
970
971 /**
972 * Alias for {@link LuCI.Poll.stop LuCI.Poll.stop()}.
973 *
974 * @instance
975 * @memberof LuCI.Request.poll
976 */
977 stop: function() { return Poll.stop() },
978
979 /**
980 * Alias for {@link LuCI.Poll.active LuCI.Poll.active()}.
981 *
982 * @instance
983 * @memberof LuCI.Request.poll
984 */
985 active: function() { return Poll.active() }
986 }
987 });
988
989 /**
990 * @class
991 * @memberof LuCI
992 * @hideconstructor
993 * @classdesc
994 *
995 * The `Poll` class allows registering and unregistering poll actions,
996 * as well as starting, stopping and querying the state of the polling
997 * loop.
998 */
999 var Poll = Class.singleton(/** @lends LuCI.Poll.prototype */ {
1000 __name__: 'LuCI.Poll',
1001
1002 queue: [],
1003
1004 /**
1005 * Add a new operation to the polling loop. If the polling loop is not
1006 * already started at this point, it will be implicitely started.
1007 *
1008 * @instance
1009 * @memberof LuCI.Poll
1010 * @param {function} fn
1011 * The function to invoke on each poll interval.
1012 *
1013 * @param {number} interval
1014 * The poll interval in seconds.
1015 *
1016 * @throws {TypeError}
1017 * Throws `TypeError` when an invalid interval was passed.
1018 *
1019 * @returns {boolean}
1020 * Returns `true` if the function has been added or `false` if it
1021 * already is registered.
1022 */
1023 add: function(fn, interval) {
1024 if (interval == null || interval <= 0)
1025 interval = window.L ? window.L.env.pollinterval : null;
1026
1027 if (isNaN(interval) || typeof(fn) != 'function')
1028 throw new TypeError('Invalid argument to LuCI.Poll.add()');
1029
1030 for (var i = 0; i < this.queue.length; i++)
1031 if (this.queue[i].fn === fn)
1032 return false;
1033
1034 var e = {
1035 r: true,
1036 i: interval >>> 0,
1037 fn: fn
1038 };
1039
1040 this.queue.push(e);
1041
1042 if (this.tick != null && !this.active())
1043 this.start();
1044
1045 return true;
1046 },
1047
1048 /**
1049 * Remove an operation from the polling loop. If no further operatons
1050 * are registered, the polling loop is implicitely stopped.
1051 *
1052 * @instance
1053 * @memberof LuCI.Poll
1054 * @param {function} fn
1055 * The function to remove.
1056 *
1057 * @throws {TypeError}
1058 * Throws `TypeError` when the given argument isn't a function.
1059 *
1060 * @returns {boolean}
1061 * Returns `true` if the function has been removed or `false` if it
1062 * wasn't found.
1063 */
1064 remove: function(fn) {
1065 if (typeof(fn) != 'function')
1066 throw new TypeError('Invalid argument to LuCI.Poll.remove()');
1067
1068 var len = this.queue.length;
1069
1070 for (var i = len; i > 0; i--)
1071 if (this.queue[i-1].fn === fn)
1072 this.queue.splice(i-1, 1);
1073
1074 if (!this.queue.length && this.stop())
1075 this.tick = 0;
1076
1077 return (this.queue.length != len);
1078 },
1079
1080 /**
1081 * (Re)start the polling loop. Dispatches a custom `poll-start` event
1082 * to the `document` object upon successful start.
1083 *
1084 * @instance
1085 * @memberof LuCI.Poll
1086 * @returns {boolean}
1087 * Returns `true` if polling has been started (or if no functions
1088 * where registered) or `false` when the polling loop already runs.
1089 */
1090 start: function() {
1091 if (this.active())
1092 return false;
1093
1094 this.tick = 0;
1095
1096 if (this.queue.length) {
1097 this.timer = window.setInterval(this.step, 1000);
1098 this.step();
1099 document.dispatchEvent(new CustomEvent('poll-start'));
1100 }
1101
1102 return true;
1103 },
1104
1105 /**
1106 * Stop the polling loop. Dispatches a custom `poll-stop` event
1107 * to the `document` object upon successful stop.
1108 *
1109 * @instance
1110 * @memberof LuCI.Poll
1111 * @returns {boolean}
1112 * Returns `true` if polling has been stopped or `false` if it din't
1113 * run to begin with.
1114 */
1115 stop: function() {
1116 if (!this.active())
1117 return false;
1118
1119 document.dispatchEvent(new CustomEvent('poll-stop'));
1120 window.clearInterval(this.timer);
1121 delete this.timer;
1122 delete this.tick;
1123 return true;
1124 },
1125
1126 /* private */
1127 step: function() {
1128 for (var i = 0, e = null; (e = Poll.queue[i]) != null; i++) {
1129 if ((Poll.tick % e.i) != 0)
1130 continue;
1131
1132 if (!e.r)
1133 continue;
1134
1135 e.r = false;
1136
1137 Promise.resolve(e.fn()).finally((function() { this.r = true }).bind(e));
1138 }
1139
1140 Poll.tick = (Poll.tick + 1) % Math.pow(2, 32);
1141 },
1142
1143 /**
1144 * Test whether the polling loop is running.
1145 *
1146 * @instance
1147 * @memberof LuCI.Poll
1148 * @returns {boolean} - Returns `true` if polling is active, else `false`.
1149 */
1150 active: function() {
1151 return (this.timer != null);
1152 }
1153 });
1154
1155
1156 var dummyElem = null,
1157 domParser = null,
1158 originalCBIInit = null,
1159 rpcBaseURL = null,
1160 sysFeatures = null,
1161 classes = {};
1162
1163 var LuCI = Class.extend(/** @lends LuCI.prototype */ {
1164 __name__: 'LuCI',
1165 __init__: function(env) {
1166
1167 document.querySelectorAll('script[src*="/luci.js"]').forEach(function(s) {
1168 if (env.base_url == null || env.base_url == '') {
1169 var m = (s.getAttribute('src') || '').match(/^(.*)\/luci\.js(?:\?v=([^?]+))?$/);
1170 if (m) {
1171 env.base_url = m[1];
1172 env.resource_version = m[2];
1173 }
1174 }
1175 });
1176
1177 if (env.base_url == null)
1178 this.error('InternalError', 'Cannot find url of luci.js');
1179
1180 Object.assign(this.env, env);
1181
1182 document.addEventListener('poll-start', function(ev) {
1183 document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
1184 e.style.display = (e.id == 'xhr_poll_status_off') ? 'none' : '';
1185 });
1186 });
1187
1188 document.addEventListener('poll-stop', function(ev) {
1189 document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
1190 e.style.display = (e.id == 'xhr_poll_status_on') ? 'none' : '';
1191 });
1192 });
1193
1194 var domReady = new Promise(function(resolveFn, rejectFn) {
1195 document.addEventListener('DOMContentLoaded', resolveFn);
1196 });
1197
1198 Promise.all([
1199 domReady,
1200 this.require('ui'),
1201 this.require('rpc'),
1202 this.require('form'),
1203 this.probeRPCBaseURL()
1204 ]).then(this.setupDOM.bind(this)).catch(this.error);
1205
1206 originalCBIInit = window.cbi_init;
1207 window.cbi_init = function() {};
1208 },
1209
1210 /**
1211 * Captures the current stack trace and throws an error of the
1212 * specified type as a new exception. Also logs the exception as
1213 * error to the debug console if it is available.
1214 *
1215 * @instance
1216 * @memberof LuCI
1217 *
1218 * @param {Error|string} [type=Error]
1219 * Either a string specifying the type of the error to throw or an
1220 * existing `Error` instance to copy.
1221 *
1222 * @param {string} [fmt=Unspecified error]
1223 * A format string which is used to form the error message, together
1224 * with all subsequent optional arguments.
1225 *
1226 * @param {...*} [args]
1227 * Zero or more variable arguments to the supplied format string.
1228 *
1229 * @throws {Error}
1230 * Throws the created error object with the captured stack trace
1231 * appended to the message and the type set to the given type
1232 * argument or copied from the given error instance.
1233 */
1234 raise: function(type, fmt /*, ...*/) {
1235 var e = null,
1236 msg = fmt ? String.prototype.format.apply(fmt, this.varargs(arguments, 2)) : null,
1237 stack = null;
1238
1239 if (type instanceof Error) {
1240 e = type;
1241
1242 if (msg)
1243 e.message = msg + ': ' + e.message;
1244 }
1245 else {
1246 try { throw new Error('stacktrace') }
1247 catch (e2) { stack = (e2.stack || '').split(/\n/) }
1248
1249 e = new (window[type || 'Error'] || Error)(msg || 'Unspecified error');
1250 e.name = type || 'Error';
1251 }
1252
1253 stack = (stack || []).map(function(frame) {
1254 frame = frame.replace(/(.*?)@(.+):(\d+):(\d+)/g, 'at $1 ($2:$3:$4)').trim();
1255 return frame ? ' ' + frame : '';
1256 });
1257
1258 if (!/^ at /.test(stack[0]))
1259 stack.shift();
1260
1261 if (/\braise /.test(stack[0]))
1262 stack.shift();
1263
1264 if (/\berror /.test(stack[0]))
1265 stack.shift();
1266
1267 if (stack.length)
1268 e.message += '\n' + stack.join('\n');
1269
1270 if (window.console && console.debug)
1271 console.debug(e);
1272
1273 throw e;
1274 },
1275
1276 /**
1277 * A wrapper around {@link LuCI#raise raise()} which also renders
1278 * the error either as modal overlay when `ui.js` is already loaed
1279 * or directly into the view body.
1280 *
1281 * @instance
1282 * @memberof LuCI
1283 *
1284 * @param {Error|string} [type=Error]
1285 * Either a string specifying the type of the error to throw or an
1286 * existing `Error` instance to copy.
1287 *
1288 * @param {string} [fmt=Unspecified error]
1289 * A format string which is used to form the error message, together
1290 * with all subsequent optional arguments.
1291 *
1292 * @param {...*} [args]
1293 * Zero or more variable arguments to the supplied format string.
1294 *
1295 * @throws {Error}
1296 * Throws the created error object with the captured stack trace
1297 * appended to the message and the type set to the given type
1298 * argument or copied from the given error instance.
1299 */
1300 error: function(type, fmt /*, ...*/) {
1301 try {
1302 L.raise.apply(L, Array.prototype.slice.call(arguments));
1303 }
1304 catch (e) {
1305 if (!e.reported) {
1306 if (L.ui)
1307 L.ui.addNotification(e.name || _('Runtime error'),
1308 E('pre', {}, e.message), 'danger');
1309 else
1310 L.dom.content(document.querySelector('#maincontent'),
1311 E('pre', { 'class': 'alert-message error' }, e.message));
1312
1313 e.reported = true;
1314 }
1315
1316 throw e;
1317 }
1318 },
1319
1320 /**
1321 * Return a bound function using the given `self` as `this` context
1322 * and any further arguments as parameters to the bound function.
1323 *
1324 * @instance
1325 * @memberof LuCI
1326 *
1327 * @param {function} fn
1328 * The function to bind.
1329 *
1330 * @param {*} self
1331 * The value to bind as `this` context to the specified function.
1332 *
1333 * @param {...*} [args]
1334 * Zero or more variable arguments which are bound to the function
1335 * as parameters.
1336 *
1337 * @returns {function}
1338 * Returns the bound function.
1339 */
1340 bind: function(fn, self /*, ... */) {
1341 return Function.prototype.bind.apply(fn, this.varargs(arguments, 2, self));
1342 },
1343
1344 /**
1345 * Load an additional LuCI JavaScript class and its dependencies,
1346 * instantiate it and return the resulting class instance. Each
1347 * class is only loaded once. Subsequent attempts to load the same
1348 * class will return the already instantiated class.
1349 *
1350 * @instance
1351 * @memberof LuCI
1352 *
1353 * @param {string} name
1354 * The name of the class to load in dotted notation. Dots will
1355 * be replaced by spaces and joined with the runtime-determined
1356 * base URL of LuCI.js to form an absolute URL to load the class
1357 * file from.
1358 *
1359 * @throws {DependencyError}
1360 * Throws a `DependencyError` when the class to load includes
1361 * circular dependencies.
1362 *
1363 * @throws {NetworkError}
1364 * Throws `NetworkError` when the underlying {@link LuCI.Request}
1365 * call failed.
1366 *
1367 * @throws {SyntaxError}
1368 * Throws `SyntaxError` when the loaded class file code cannot
1369 * be interpreted by `eval`.
1370 *
1371 * @throws {TypeError}
1372 * Throws `TypeError` when the class file could be loaded and
1373 * interpreted, but when invoking its code did not yield a valid
1374 * class instance.
1375 *
1376 * @returns {Promise<LuCI#Class>}
1377 * Returns the instantiated class.
1378 */
1379 require: function(name, from) {
1380 var L = this, url = null, from = from || [];
1381
1382 /* Class already loaded */
1383 if (classes[name] != null) {
1384 /* Circular dependency */
1385 if (from.indexOf(name) != -1)
1386 L.raise('DependencyError',
1387 'Circular dependency: class "%s" depends on "%s"',
1388 name, from.join('" which depends on "'));
1389
1390 return Promise.resolve(classes[name]);
1391 }
1392
1393 url = '%s/%s.js%s'.format(L.env.base_url, name.replace(/\./g, '/'), (L.env.resource_version ? '?v=' + L.env.resource_version : ''));
1394 from = [ name ].concat(from);
1395
1396 var compileClass = function(res) {
1397 if (!res.ok)
1398 L.raise('NetworkError',
1399 'HTTP error %d while loading class file "%s"', res.status, url);
1400
1401 var source = res.text(),
1402 requirematch = /^require[ \t]+(\S+)(?:[ \t]+as[ \t]+([a-zA-Z_]\S*))?$/,
1403 strictmatch = /^use[ \t]+strict$/,
1404 depends = [],
1405 args = '';
1406
1407 /* find require statements in source */
1408 for (var i = 0, off = -1, quote = -1, esc = false; i < source.length; i++) {
1409 var chr = source.charCodeAt(i);
1410
1411 if (esc) {
1412 esc = false;
1413 }
1414 else if (chr == 92) {
1415 esc = true;
1416 }
1417 else if (chr == quote) {
1418 var s = source.substring(off, i),
1419 m = requirematch.exec(s);
1420
1421 if (m) {
1422 var dep = m[1], as = m[2] || dep.replace(/[^a-zA-Z0-9_]/g, '_');
1423 depends.push(L.require(dep, from));
1424 args += ', ' + as;
1425 }
1426 else if (!strictmatch.exec(s)) {
1427 break;
1428 }
1429
1430 off = -1;
1431 quote = -1;
1432 }
1433 else if (quote == -1 && (chr == 34 || chr == 39)) {
1434 off = i + 1;
1435 quote = chr;
1436 }
1437 }
1438
1439 /* load dependencies and instantiate class */
1440 return Promise.all(depends).then(function(instances) {
1441 var _factory, _class;
1442
1443 try {
1444 _factory = eval(
1445 '(function(window, document, L%s) { %s })\n\n//# sourceURL=%s\n'
1446 .format(args, source, res.url));
1447 }
1448 catch (error) {
1449 L.raise('SyntaxError', '%s\n in %s:%s',
1450 error.message, res.url, error.lineNumber || '?');
1451 }
1452
1453 _factory.displayName = toCamelCase(name + 'ClassFactory');
1454 _class = _factory.apply(_factory, [window, document, L].concat(instances));
1455
1456 if (!Class.isSubclass(_class))
1457 L.error('TypeError', '"%s" factory yields invalid constructor', name);
1458
1459 if (_class.displayName == 'AnonymousClass')
1460 _class.displayName = toCamelCase(name + 'Class');
1461
1462 var ptr = Object.getPrototypeOf(L),
1463 parts = name.split(/\./),
1464 instance = new _class();
1465
1466 for (var i = 0; ptr && i < parts.length - 1; i++)
1467 ptr = ptr[parts[i]];
1468
1469 if (ptr)
1470 ptr[parts[i]] = instance;
1471
1472 classes[name] = instance;
1473
1474 return instance;
1475 });
1476 };
1477
1478 /* Request class file */
1479 classes[name] = Request.get(url, { cache: true }).then(compileClass);
1480
1481 return classes[name];
1482 },
1483
1484 /* DOM setup */
1485 probeRPCBaseURL: function() {
1486 if (rpcBaseURL == null) {
1487 try {
1488 rpcBaseURL = window.sessionStorage.getItem('rpcBaseURL');
1489 }
1490 catch (e) { }
1491 }
1492
1493 if (rpcBaseURL == null) {
1494 var rpcFallbackURL = this.url('admin/ubus');
1495
1496 rpcBaseURL = Request.get('/ubus/').then(function(res) {
1497 return (rpcBaseURL = (res.status == 400) ? '/ubus/' : rpcFallbackURL);
1498 }, function() {
1499 return (rpcBaseURL = rpcFallbackURL);
1500 }).then(function(url) {
1501 try {
1502 window.sessionStorage.setItem('rpcBaseURL', url);
1503 }
1504 catch (e) { }
1505
1506 return url;
1507 });
1508 }
1509
1510 return Promise.resolve(rpcBaseURL);
1511 },
1512
1513 probeSystemFeatures: function() {
1514 var sessionid = classes.rpc.getSessionID();
1515
1516 if (sysFeatures == null) {
1517 try {
1518 var data = JSON.parse(window.sessionStorage.getItem('sysFeatures'));
1519
1520 if (this.isObject(data) && this.isObject(data[sessionid]))
1521 sysFeatures = data[sessionid];
1522 }
1523 catch (e) {}
1524 }
1525
1526 if (!this.isObject(sysFeatures)) {
1527 sysFeatures = classes.rpc.declare({
1528 object: 'luci',
1529 method: 'getFeatures',
1530 expect: { '': {} }
1531 })().then(function(features) {
1532 try {
1533 var data = {};
1534 data[sessionid] = features;
1535
1536 window.sessionStorage.setItem('sysFeatures', JSON.stringify(data));
1537 }
1538 catch (e) {}
1539
1540 sysFeatures = features;
1541
1542 return features;
1543 });
1544 }
1545
1546 return Promise.resolve(sysFeatures);
1547 },
1548
1549 /**
1550 * Test whether a particular system feature is available, such as
1551 * hostapd SAE support or an installed firewall. The features are
1552 * queried once at the beginning of the LuCI session and cached in
1553 * `SessionStorage` throughout the lifetime of the associated tab or
1554 * browser window.
1555 *
1556 * @instance
1557 * @memberof LuCI
1558 *
1559 * @param {string} feature
1560 * The feature to test. For detailed list of known feature flags,
1561 * see `/modules/luci-base/root/usr/libexec/rpcd/luci`.
1562 *
1563 * @param {string} [subfeature]
1564 * Some feature classes like `hostapd` provide sub-feature flags,
1565 * such as `sae` or `11w` support. The `subfeature` argument can
1566 * be used to query these.
1567 *
1568 * @return {boolean|null}
1569 * Return `true` if the queried feature (and sub-feature) is available
1570 * or `false` if the requested feature isn't present or known.
1571 * Return `null` when a sub-feature was queried for a feature which
1572 * has no sub-features.
1573 */
1574 hasSystemFeature: function() {
1575 var ft = sysFeatures[arguments[0]];
1576
1577 if (arguments.length == 2)
1578 return this.isObject(ft) ? ft[arguments[1]] : null;
1579
1580 return (ft != null && ft != false);
1581 },
1582
1583 /* private */
1584 notifySessionExpiry: function() {
1585 Poll.stop();
1586
1587 L.ui.showModal(_('Session expired'), [
1588 E('div', { class: 'alert-message warning' },
1589 _('A new login is required since the authentication session expired.')),
1590 E('div', { class: 'right' },
1591 E('div', {
1592 class: 'btn primary',
1593 click: function() {
1594 var loc = window.location;
1595 window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
1596 }
1597 }, _('To login…')))
1598 ]);
1599
1600 L.raise('SessionError', 'Login session is expired');
1601 },
1602
1603 /* private */
1604 setupDOM: function(res) {
1605 var domEv = res[0],
1606 uiClass = res[1],
1607 rpcClass = res[2],
1608 formClass = res[3],
1609 rpcBaseURL = res[4];
1610
1611 rpcClass.setBaseURL(rpcBaseURL);
1612
1613 rpcClass.addInterceptor(function(msg, req) {
1614 if (!L.isObject(msg) || !L.isObject(msg.error) || msg.error.code != -32002)
1615 return;
1616
1617 if (!L.isObject(req) || (req.object == 'session' && req.method == 'access'))
1618 return;
1619
1620 return rpcClass.declare({
1621 'object': 'session',
1622 'method': 'access',
1623 'params': [ 'scope', 'object', 'function' ],
1624 'expect': { access: true }
1625 })('uci', 'luci', 'read').catch(L.notifySessionExpiry);
1626 });
1627
1628 Request.addInterceptor(function(res) {
1629 var isDenied = false;
1630
1631 if (res.status == 403 && res.headers.get('X-LuCI-Login-Required') == 'yes')
1632 isDenied = true;
1633
1634 if (!isDenied)
1635 return;
1636
1637 L.notifySessionExpiry();
1638 });
1639
1640 return this.probeSystemFeatures().finally(this.initDOM);
1641 },
1642
1643 /* private */
1644 initDOM: function() {
1645 originalCBIInit();
1646 Poll.start();
1647 document.dispatchEvent(new CustomEvent('luci-loaded'));
1648 },
1649
1650 /**
1651 * The `env` object holds environment settings used by LuCI, such
1652 * as request timeouts, base URLs etc.
1653 *
1654 * @instance
1655 * @memberof LuCI
1656 */
1657 env: {},
1658
1659 /**
1660 * Construct a relative URL path from the given prefix and parts.
1661 * The resulting URL is guaranteed to only contain the characters
1662 * `a-z`, `A-Z`, `0-9`, `_`, `.`, `%`, `,`, `;`, and `-` as well
1663 * as `/` for the path separator.
1664 *
1665 * @instance
1666 * @memberof LuCI
1667 *
1668 * @param {string} [prefix]
1669 * The prefix to join the given parts with. If the `prefix` is
1670 * omitted, it defaults to an empty string.
1671 *
1672 * @param {string[]} [parts]
1673 * An array of parts to join into an URL path. Parts may contain
1674 * slashes and any of the other characters mentioned above.
1675 *
1676 * @return {string}
1677 * Return the joined URL path.
1678 */
1679 path: function(prefix, parts) {
1680 var url = [ prefix || '' ];
1681
1682 for (var i = 0; i < parts.length; i++)
1683 if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts[i]))
1684 url.push('/', parts[i]);
1685
1686 if (url.length === 1)
1687 url.push('/');
1688
1689 return url.join('');
1690 },
1691
1692 /**
1693 * Construct an URL pathrelative to the script path of the server
1694 * side LuCI application (usually `/cgi-bin/luci`).
1695 *
1696 * The resulting URL is guaranteed to only contain the characters
1697 * `a-z`, `A-Z`, `0-9`, `_`, `.`, `%`, `,`, `;`, and `-` as well
1698 * as `/` for the path separator.
1699 *
1700 * @instance
1701 * @memberof LuCI
1702 *
1703 * @param {string[]} [parts]
1704 * An array of parts to join into an URL path. Parts may contain
1705 * slashes and any of the other characters mentioned above.
1706 *
1707 * @return {string}
1708 * Returns the resulting URL path.
1709 */
1710 url: function() {
1711 return this.path(this.env.scriptname, arguments);
1712 },
1713
1714 /**
1715 * Construct an URL path relative to the global static resource path
1716 * of the LuCI ui (usually `/luci-static/resources`).
1717 *
1718 * The resulting URL is guaranteed to only contain the characters
1719 * `a-z`, `A-Z`, `0-9`, `_`, `.`, `%`, `,`, `;`, and `-` as well
1720 * as `/` for the path separator.
1721 *
1722 * @instance
1723 * @memberof LuCI
1724 *
1725 * @param {string[]} [parts]
1726 * An array of parts to join into an URL path. Parts may contain
1727 * slashes and any of the other characters mentioned above.
1728 *
1729 * @return {string}
1730 * Returns the resulting URL path.
1731 */
1732 resource: function() {
1733 return this.path(this.env.resource, arguments);
1734 },
1735
1736 /**
1737 * Return the complete URL path to the current view.
1738 *
1739 * @instance
1740 * @memberof LuCI
1741 *
1742 * @return {string}
1743 * Returns the URL path to the current view.
1744 */
1745 location: function() {
1746 return this.path(this.env.scriptname, this.env.requestpath);
1747 },
1748
1749
1750 /**
1751 * Tests whether the passed argument is a JavaScript object.
1752 * This function is meant to be an object counterpart to the
1753 * standard `Array.isArray()` function.
1754 *
1755 * @instance
1756 * @memberof LuCI
1757 *
1758 * @param {*} [val]
1759 * The value to test
1760 *
1761 * @return {boolean}
1762 * Returns `true` if the given value is of type object and
1763 * not `null`, else returns `false`.
1764 */
1765 isObject: function(val) {
1766 return (val != null && typeof(val) == 'object');
1767 },
1768
1769 /**
1770 * Return an array of sorted object keys, optionally sorted by
1771 * a different key or a different sorting mode.
1772 *
1773 * @instance
1774 * @memberof LuCI
1775 *
1776 * @param {object} obj
1777 * The object to extract the keys from. If the given value is
1778 * not an object, the function will return an empty array.
1779 *
1780 * @param {string} [key]
1781 * Specifies the key to order by. This is mainly useful for
1782 * nested objects of objects or objects of arrays when sorting
1783 * shall not be performed by the primary object keys but by
1784 * some other key pointing to a value within the nested values.
1785 *
1786 * @param {string} [sortmode]
1787 * May be either `addr` or `num` to override the natural
1788 * lexicographic sorting with a sorting suitable for IP/MAC style
1789 * addresses or numeric values respectively.
1790 *
1791 * @return {string[]}
1792 * Returns an array containing the sorted keys of the given object.
1793 */
1794 sortedKeys: function(obj, key, sortmode) {
1795 if (obj == null || typeof(obj) != 'object')
1796 return [];
1797
1798 return Object.keys(obj).map(function(e) {
1799 var v = (key != null) ? obj[e][key] : e;
1800
1801 switch (sortmode) {
1802 case 'addr':
1803 v = (v != null) ? v.replace(/(?:^|[.:])([0-9a-fA-F]{1,4})/g,
1804 function(m0, m1) { return ('000' + m1.toLowerCase()).substr(-4) }) : null;
1805 break;
1806
1807 case 'num':
1808 v = (v != null) ? +v : null;
1809 break;
1810 }
1811
1812 return [ e, v ];
1813 }).filter(function(e) {
1814 return (e[1] != null);
1815 }).sort(function(a, b) {
1816 return (a[1] > b[1]);
1817 }).map(function(e) {
1818 return e[0];
1819 });
1820 },
1821
1822 /**
1823 * Converts the given value to an array. If the given value is of
1824 * type array, it is returned as-is, values of type object are
1825 * returned as one-element array containing the object, empty
1826 * strings and `null` values are returned as empty array, all other
1827 * values are converted using `String()`, trimmed, split on white
1828 * space and returned as array.
1829 *
1830 * @instance
1831 * @memberof LuCI
1832 *
1833 * @param {*} val
1834 * The value to convert into an array.
1835 *
1836 * @return {Array<*>}
1837 * Returns the resulting array.
1838 */
1839 toArray: function(val) {
1840 if (val == null)
1841 return [];
1842 else if (Array.isArray(val))
1843 return val;
1844 else if (typeof(val) == 'object')
1845 return [ val ];
1846
1847 var s = String(val).trim();
1848
1849 if (s == '')
1850 return [];
1851
1852 return s.split(/\s+/);
1853 },
1854
1855 /**
1856 * Returns a promise resolving with either the given value or or with
1857 * the given default in case the input value is a rejecting promise.
1858 *
1859 * @instance
1860 * @memberof LuCI
1861 *
1862 * @param {*} value
1863 * The value to resolve the promise with.
1864 *
1865 * @param {*} defvalue
1866 * The default value to resolve the promise with in case the given
1867 * input value is a rejecting promise.
1868 *
1869 * @returns {Promise<*>}
1870 * Returns a new promise resolving either to the given input value or
1871 * to the given default value on error.
1872 */
1873 resolveDefault: function(value, defvalue) {
1874 return Promise.resolve(value).catch(function() { return defvalue });
1875 },
1876
1877 /**
1878 * The request callback function is invoked whenever an HTTP
1879 * reply to a request made using the `L.get()`, `L.post()` or
1880 * `L.poll()` function is timed out or received successfully.
1881 *
1882 * @instance
1883 * @memberof LuCI
1884 *
1885 * @callback LuCI.requestCallbackFn
1886 * @param {XMLHTTPRequest} xhr
1887 * The XMLHTTPRequest instance used to make the request.
1888 *
1889 * @param {*} data
1890 * The response JSON if the response could be parsed as such,
1891 * else `null`.
1892 *
1893 * @param {number} duration
1894 * The total duration of the request in milliseconds.
1895 */
1896
1897 /**
1898 * Issues a GET request to the given url and invokes the specified
1899 * callback function. The function is a wrapper around
1900 * {@link LuCI.Request#request Request.request()}.
1901 *
1902 * @deprecated
1903 * @instance
1904 * @memberof LuCI
1905 *
1906 * @param {string} url
1907 * The URL to request.
1908 *
1909 * @param {Object<string, string>} [args]
1910 * Additional query string arguments to append to the URL.
1911 *
1912 * @param {LuCI.requestCallbackFn} cb
1913 * The callback function to invoke when the request finishes.
1914 *
1915 * @return {Promise<null>}
1916 * Returns a promise resolving to `null` when concluded.
1917 */
1918 get: function(url, args, cb) {
1919 return this.poll(null, url, args, cb, false);
1920 },
1921
1922 /**
1923 * Issues a POST request to the given url and invokes the specified
1924 * callback function. The function is a wrapper around
1925 * {@link LuCI.Request#request Request.request()}. The request is
1926 * sent using `application/x-www-form-urlencoded` encoding and will
1927 * contain a field `token` with the current value of `LuCI.env.token`
1928 * by default.
1929 *
1930 * @deprecated
1931 * @instance
1932 * @memberof LuCI
1933 *
1934 * @param {string} url
1935 * The URL to request.
1936 *
1937 * @param {Object<string, string>} [args]
1938 * Additional post arguments to append to the request body.
1939 *
1940 * @param {LuCI.requestCallbackFn} cb
1941 * The callback function to invoke when the request finishes.
1942 *
1943 * @return {Promise<null>}
1944 * Returns a promise resolving to `null` when concluded.
1945 */
1946 post: function(url, args, cb) {
1947 return this.poll(null, url, args, cb, true);
1948 },
1949
1950 /**
1951 * Register a polling HTTP request that invokes the specified
1952 * callback function. The function is a wrapper around
1953 * {@link LuCI.Request.poll#add Request.poll.add()}.
1954 *
1955 * @deprecated
1956 * @instance
1957 * @memberof LuCI
1958 *
1959 * @param {number} interval
1960 * The poll interval to use. If set to a value less than or equal
1961 * to `0`, it will default to the global poll interval configured
1962 * in `LuCI.env.pollinterval`.
1963 *
1964 * @param {string} url
1965 * The URL to request.
1966 *
1967 * @param {Object<string, string>} [args]
1968 * Specifies additional arguments for the request. For GET requests,
1969 * the arguments are appended to the URL as query string, for POST
1970 * requests, they'll be added to the request body.
1971 *
1972 * @param {LuCI.requestCallbackFn} cb
1973 * The callback function to invoke whenever a request finishes.
1974 *
1975 * @param {boolean} [post=false]
1976 * When set to `false` or not specified, poll requests will be made
1977 * using the GET method. When set to `true`, POST requests will be
1978 * issued. In case of POST requests, the request body will contain
1979 * an argument `token` with the current value of `LuCI.env.token` by
1980 * default, regardless of the parameters specified with `args`.
1981 *
1982 * @return {function}
1983 * Returns the internally created function that has been passed to
1984 * {@link LuCI.Request.poll#add Request.poll.add()}. This value can
1985 * be passed to {@link LuCI.Poll.remove Poll.remove()} to remove the
1986 * polling request.
1987 */
1988 poll: function(interval, url, args, cb, post) {
1989 if (interval !== null && interval <= 0)
1990 interval = this.env.pollinterval;
1991
1992 var data = post ? { token: this.env.token } : null,
1993 method = post ? 'POST' : 'GET';
1994
1995 if (!/^(?:\/|\S+:\/\/)/.test(url))
1996 url = this.url(url);
1997
1998 if (args != null)
1999 data = Object.assign(data || {}, args);
2000
2001 if (interval !== null)
2002 return Request.poll.add(interval, url, { method: method, query: data }, cb);
2003 else
2004 return Request.request(url, { method: method, query: data })
2005 .then(function(res) {
2006 var json = null;
2007 if (/^application\/json\b/.test(res.headers.get('Content-Type')))
2008 try { json = res.json() } catch(e) {}
2009 cb(res.xhr, json, res.duration);
2010 });
2011 },
2012
2013 /**
2014 * Deprecated wrapper around {@link LuCI.Poll.remove Poll.remove()}.
2015 *
2016 * @deprecated
2017 * @instance
2018 * @memberof LuCI
2019 *
2020 * @param {function} entry
2021 * The polling function to remove.
2022 *
2023 * @return {boolean}
2024 * Returns `true` when the function has been removed or `false` if
2025 * it could not be found.
2026 */
2027 stop: function(entry) { return Poll.remove(entry) },
2028
2029 /**
2030 * Deprecated wrapper around {@link LuCI.Poll.stop Poll.stop()}.
2031 *
2032 * @deprecated
2033 * @instance
2034 * @memberof LuCI
2035 *
2036 * @return {boolean}
2037 * Returns `true` when the polling loop has been stopped or `false`
2038 * when it didn't run to begin with.
2039 */
2040 halt: function() { return Poll.stop() },
2041
2042 /**
2043 * Deprecated wrapper around {@link LuCI.Poll.start Poll.start()}.
2044 *
2045 * @deprecated
2046 * @instance
2047 * @memberof LuCI
2048 *
2049 * @return {boolean}
2050 * Returns `true` when the polling loop has been started or `false`
2051 * when it was already running.
2052 */
2053 run: function() { return Poll.start() },
2054
2055
2056 /**
2057 * @class
2058 * @memberof LuCI
2059 * @hideconstructor
2060 * @classdesc
2061 *
2062 * The `dom` class provides convenience method for creating and
2063 * manipulating DOM elements.
2064 */
2065 dom: Class.singleton(/* @lends LuCI.dom.prototype */ {
2066 __name__: 'LuCI.DOM',
2067
2068 /**
2069 * Tests whether the given argument is a valid DOM `Node`.
2070 *
2071 * @instance
2072 * @memberof LuCI.dom
2073 * @param {*} e
2074 * The value to test.
2075 *
2076 * @returns {boolean}
2077 * Returns `true` if the value is a DOM `Node`, else `false`.
2078 */
2079 elem: function(e) {
2080 return (e != null && typeof(e) == 'object' && 'nodeType' in e);
2081 },
2082
2083 /**
2084 * Parses a given string as HTML and returns the first child node.
2085 *
2086 * @instance
2087 * @memberof LuCI.dom
2088 * @param {string} s
2089 * A string containing an HTML fragment to parse. Note that only
2090 * the first result of the resulting structure is returned, so an
2091 * input value of `<div>foo</div> <div>bar</div>` will only return
2092 * the first `div` element node.
2093 *
2094 * @returns {Node}
2095 * Returns the first DOM `Node` extracted from the HTML fragment or
2096 * `null` on parsing failures or if no element could be found.
2097 */
2098 parse: function(s) {
2099 var elem;
2100
2101 try {
2102 domParser = domParser || new DOMParser();
2103 elem = domParser.parseFromString(s, 'text/html').body.firstChild;
2104 }
2105 catch(e) {}
2106
2107 if (!elem) {
2108 try {
2109 dummyElem = dummyElem || document.createElement('div');
2110 dummyElem.innerHTML = s;
2111 elem = dummyElem.firstChild;
2112 }
2113 catch (e) {}
2114 }
2115
2116 return elem || null;
2117 },
2118
2119 /**
2120 * Tests whether a given `Node` matches the given query selector.
2121 *
2122 * This function is a convenience wrapper around the standard
2123 * `Node.matches("selector")` function with the added benefit that
2124 * the `node` argument may be a non-`Node` value, in which case
2125 * this function simply returns `false`.
2126 *
2127 * @instance
2128 * @memberof LuCI.dom
2129 * @param {*} node
2130 * The `Node` argument to test the selector against.
2131 *
2132 * @param {string} [selector]
2133 * The query selector expression to test against the given node.
2134 *
2135 * @returns {boolean}
2136 * Returns `true` if the given node matches the specified selector
2137 * or `false` when the node argument is no valid DOM `Node` or the
2138 * selector didn't match.
2139 */
2140 matches: function(node, selector) {
2141 var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
2142 return m ? m.call(node, selector) : false;
2143 },
2144
2145 /**
2146 * Returns the closest parent node that matches the given query
2147 * selector expression.
2148 *
2149 * This function is a convenience wrapper around the standard
2150 * `Node.closest("selector")` function with the added benefit that
2151 * the `node` argument may be a non-`Node` value, in which case
2152 * this function simply returns `null`.
2153 *
2154 * @instance
2155 * @memberof LuCI.dom
2156 * @param {*} node
2157 * The `Node` argument to find the closest parent for.
2158 *
2159 * @param {string} [selector]
2160 * The query selector expression to test against each parent.
2161 *
2162 * @returns {Node|null}
2163 * Returns the closest parent node matching the selector or
2164 * `null` when the node argument is no valid DOM `Node` or the
2165 * selector didn't match any parent.
2166 */
2167 parent: function(node, selector) {
2168 if (this.elem(node) && node.closest)
2169 return node.closest(selector);
2170
2171 while (this.elem(node))
2172 if (this.matches(node, selector))
2173 return node;
2174 else
2175 node = node.parentNode;
2176
2177 return null;
2178 },
2179
2180 /**
2181 * Appends the given children data to the given node.
2182 *
2183 * @instance
2184 * @memberof LuCI.dom
2185 * @param {*} node
2186 * The `Node` argument to append the children to.
2187 *
2188 * @param {*} [children]
2189 * The childrens to append to the given node.
2190 *
2191 * When `children` is an array, then each item of the array
2192 * will be either appended as child element or text node,
2193 * depending on whether the item is a DOM `Node` instance or
2194 * some other non-`null` value. Non-`Node`, non-`null` values
2195 * will be converted to strings first before being passed as
2196 * argument to `createTextNode()`.
2197 *
2198 * When `children` is a function, it will be invoked with
2199 * the passed `node` argument as sole parameter and the `append`
2200 * function will be invoked again, with the given `node` argument
2201 * as first and the return value of the `children` function as
2202 * second parameter.
2203 *
2204 * When `children` is is a DOM `Node` instance, it will be
2205 * appended to the given `node`.
2206 *
2207 * When `children` is any other non-`null` value, it will be
2208 * converted to a string and appened to the `innerHTML` property
2209 * of the given `node`.
2210 *
2211 * @returns {Node|null}
2212 * Returns the last children `Node` appended to the node or `null`
2213 * if either the `node` argument was no valid DOM `node` or if the
2214 * `children` was `null` or didn't result in further DOM nodes.
2215 */
2216 append: function(node, children) {
2217 if (!this.elem(node))
2218 return null;
2219
2220 if (Array.isArray(children)) {
2221 for (var i = 0; i < children.length; i++)
2222 if (this.elem(children[i]))
2223 node.appendChild(children[i]);
2224 else if (children !== null && children !== undefined)
2225 node.appendChild(document.createTextNode('' + children[i]));
2226
2227 return node.lastChild;
2228 }
2229 else if (typeof(children) === 'function') {
2230 return this.append(node, children(node));
2231 }
2232 else if (this.elem(children)) {
2233 return node.appendChild(children);
2234 }
2235 else if (children !== null && children !== undefined) {
2236 node.innerHTML = '' + children;
2237 return node.lastChild;
2238 }
2239
2240 return null;
2241 },
2242
2243 /**
2244 * Replaces the content of the given node with the given children.
2245 *
2246 * This function first removes any children of the given DOM
2247 * `Node` and then adds the given given children following the
2248 * rules outlined below.
2249 *
2250 * @instance
2251 * @memberof LuCI.dom
2252 * @param {*} node
2253 * The `Node` argument to replace the children of.
2254 *
2255 * @param {*} [children]
2256 * The childrens to replace into the given node.
2257 *
2258 * When `children` is an array, then each item of the array
2259 * will be either appended as child element or text node,
2260 * depending on whether the item is a DOM `Node` instance or
2261 * some other non-`null` value. Non-`Node`, non-`null` values
2262 * will be converted to strings first before being passed as
2263 * argument to `createTextNode()`.
2264 *
2265 * When `children` is a function, it will be invoked with
2266 * the passed `node` argument as sole parameter and the `append`
2267 * function will be invoked again, with the given `node` argument
2268 * as first and the return value of the `children` function as
2269 * second parameter.
2270 *
2271 * When `children` is is a DOM `Node` instance, it will be
2272 * appended to the given `node`.
2273 *
2274 * When `children` is any other non-`null` value, it will be
2275 * converted to a string and appened to the `innerHTML` property
2276 * of the given `node`.
2277 *
2278 * @returns {Node|null}
2279 * Returns the last children `Node` appended to the node or `null`
2280 * if either the `node` argument was no valid DOM `node` or if the
2281 * `children` was `null` or didn't result in further DOM nodes.
2282 */
2283 content: function(node, children) {
2284 if (!this.elem(node))
2285 return null;
2286
2287 var dataNodes = node.querySelectorAll('[data-idref]');
2288
2289 for (var i = 0; i < dataNodes.length; i++)
2290 delete this.registry[dataNodes[i].getAttribute('data-idref')];
2291
2292 while (node.firstChild)
2293 node.removeChild(node.firstChild);
2294
2295 return this.append(node, children);
2296 },
2297
2298 /**
2299 * Sets attributes or registers event listeners on element nodes.
2300 *
2301 * @instance
2302 * @memberof LuCI.dom
2303 * @param {*} node
2304 * The `Node` argument to set the attributes or add the event
2305 * listeners for. When the given `node` value is not a valid
2306 * DOM `Node`, the function returns and does nothing.
2307 *
2308 * @param {string|Object<string, *>} key
2309 * Specifies either the attribute or event handler name to use,
2310 * or an object containing multiple key, value pairs which are
2311 * each added to the node as either attribute or event handler,
2312 * depending on the respective value.
2313 *
2314 * @param {*} [val]
2315 * Specifies the attribute value or event handler function to add.
2316 * If the `key` parameter is an `Object`, this parameter will be
2317 * ignored.
2318 *
2319 * When `val` is of type function, it will be registered as event
2320 * handler on the given `node` with the `key` parameter being the
2321 * event name.
2322 *
2323 * When `val` is of type object, it will be serialized as JSON and
2324 * added as attribute to the given `node`, using the given `key`
2325 * as attribute name.
2326 *
2327 * When `val` is of any other type, it will be added as attribute
2328 * to the given `node` as-is, with the underlying `setAttribute()`
2329 * call implicitely turning it into a string.
2330 */
2331 attr: function(node, key, val) {
2332 if (!this.elem(node))
2333 return null;
2334
2335 var attr = null;
2336
2337 if (typeof(key) === 'object' && key !== null)
2338 attr = key;
2339 else if (typeof(key) === 'string')
2340 attr = {}, attr[key] = val;
2341
2342 for (key in attr) {
2343 if (!attr.hasOwnProperty(key) || attr[key] == null)
2344 continue;
2345
2346 switch (typeof(attr[key])) {
2347 case 'function':
2348 node.addEventListener(key, attr[key]);
2349 break;
2350
2351 case 'object':
2352 node.setAttribute(key, JSON.stringify(attr[key]));
2353 break;
2354
2355 default:
2356 node.setAttribute(key, attr[key]);
2357 }
2358 }
2359 },
2360
2361 /**
2362 * Creates a new DOM `Node` from the given `html`, `attr` and
2363 * `data` parameters.
2364 *
2365 * This function has multiple signatures, it can be either invoked
2366 * in the form `create(html[, attr[, data]])` or in the form
2367 * `create(html[, data])`. The used variant is determined from the
2368 * type of the second argument.
2369 *
2370 * @instance
2371 * @memberof LuCI.dom
2372 * @param {*} html
2373 * Describes the node to create.
2374 *
2375 * When the value of `html` is of type array, a `DocumentFragment`
2376 * node is created and each item of the array is first converted
2377 * to a DOM `Node` by passing it through `create()` and then added
2378 * as child to the fragment.
2379 *
2380 * When the value of `html` is a DOM `Node` instance, no new
2381 * element will be created but the node will be used as-is.
2382 *
2383 * When the value of `html` is a string starting with `<`, it will
2384 * be passed to `dom.parse()` and the resulting value is used.
2385 *
2386 * When the value of `html` is any other string, it will be passed
2387 * to `document.createElement()` for creating a new DOM `Node` of
2388 * the given name.
2389 *
2390 * @param {Object<string, *>} [attr]
2391 * Specifies an Object of key, value pairs to set as attributes
2392 * or event handlers on the created node. Refer to
2393 * {@link LuCI.dom#attr dom.attr()} for details.
2394 *
2395 * @param {*} [data]
2396 * Specifies children to append to the newly created element.
2397 * Refer to {@link LuCI.dom#append dom.append()} for details.
2398 *
2399 * @throws {InvalidCharacterError}
2400 * Throws an `InvalidCharacterError` when the given `html`
2401 * argument contained malformed markup (such as not escaped
2402 * `&` characters in XHTML mode) or when the given node name
2403 * in `html` contains characters which are not legal in DOM
2404 * element names, such as spaces.
2405 *
2406 * @returns {Node}
2407 * Returns the newly created `Node`.
2408 */
2409 create: function() {
2410 var html = arguments[0],
2411 attr = arguments[1],
2412 data = arguments[2],
2413 elem;
2414
2415 if (!(attr instanceof Object) || Array.isArray(attr))
2416 data = attr, attr = null;
2417
2418 if (Array.isArray(html)) {
2419 elem = document.createDocumentFragment();
2420 for (var i = 0; i < html.length; i++)
2421 elem.appendChild(this.create(html[i]));
2422 }
2423 else if (this.elem(html)) {
2424 elem = html;
2425 }
2426 else if (html.charCodeAt(0) === 60) {
2427 elem = this.parse(html);
2428 }
2429 else {
2430 elem = document.createElement(html);
2431 }
2432
2433 if (!elem)
2434 return null;
2435
2436 this.attr(elem, attr);
2437 this.append(elem, data);
2438
2439 return elem;
2440 },
2441
2442 registry: {},
2443
2444 /**
2445 * Attaches or detaches arbitrary data to and from a DOM `Node`.
2446 *
2447 * This function is useful to attach non-string values or runtime
2448 * data that is not serializable to DOM nodes. To decouple data
2449 * from the DOM, values are not added directly to nodes, but
2450 * inserted into a registry instead which is then referenced by a
2451 * string key stored as `data-idref` attribute in the node.
2452 *
2453 * This function has multiple signatures and is sensitive to the
2454 * number of arguments passed to it.
2455 *
2456 * - `dom.data(node)` -
2457 * Fetches all data associated with the given node.
2458 * - `dom.data(node, key)` -
2459 * Fetches a specific key associated with the given node.
2460 * - `dom.data(node, key, val)` -
2461 * Sets a specific key to the given value associated with the
2462 * given node.
2463 * - `dom.data(node, null)` -
2464 * Clears any data associated with the node.
2465 * - `dom.data(node, key, null)` -
2466 * Clears the given key associated with the node.
2467 *
2468 * @instance
2469 * @memberof LuCI.dom
2470 * @param {Node} node
2471 * The DOM `Node` instance to set or retrieve the data for.
2472 *
2473 * @param {string|null} [key]
2474 * This is either a string specifying the key to retrieve, or
2475 * `null` to unset the entire node data.
2476 *
2477 * @param {*|null} [val]
2478 * This is either a non-`null` value to set for a given key or
2479 * `null` to remove the given `key` from the specified node.
2480 *
2481 * @returns {*}
2482 * Returns the get or set value, or `null` when no value could
2483 * be found.
2484 */
2485 data: function(node, key, val) {
2486 var id = node.getAttribute('data-idref');
2487
2488 /* clear all data */
2489 if (arguments.length > 1 && key == null) {
2490 if (id != null) {
2491 node.removeAttribute('data-idref');
2492 val = this.registry[id]
2493 delete this.registry[id];
2494 return val;
2495 }
2496
2497 return null;
2498 }
2499
2500 /* clear a key */
2501 else if (arguments.length > 2 && key != null && val == null) {
2502 if (id != null) {
2503 val = this.registry[id][key];
2504 delete this.registry[id][key];
2505 return val;
2506 }
2507
2508 return null;
2509 }
2510
2511 /* set a key */
2512 else if (arguments.length > 2 && key != null && val != null) {
2513 if (id == null) {
2514 do { id = Math.floor(Math.random() * 0xffffffff).toString(16) }
2515 while (this.registry.hasOwnProperty(id));
2516
2517 node.setAttribute('data-idref', id);
2518 this.registry[id] = {};
2519 }
2520
2521 return (this.registry[id][key] = val);
2522 }
2523
2524 /* get all data */
2525 else if (arguments.length == 1) {
2526 if (id != null)
2527 return this.registry[id];
2528
2529 return null;
2530 }
2531
2532 /* get a key */
2533 else if (arguments.length == 2) {
2534 if (id != null)
2535 return this.registry[id][key];
2536 }
2537
2538 return null;
2539 },
2540
2541 /**
2542 * Binds the given class instance ot the specified DOM `Node`.
2543 *
2544 * This function uses the `dom.data()` facility to attach the
2545 * passed instance of a Class to a node. This is needed for
2546 * complex widget elements or similar where the corresponding
2547 * class instance responsible for the element must be retrieved
2548 * from DOM nodes obtained by `querySelector()` or similar means.
2549 *
2550 * @instance
2551 * @memberof LuCI.dom
2552 * @param {Node} node
2553 * The DOM `Node` instance to bind the class to.
2554 *
2555 * @param {Class} inst
2556 * The Class instance to bind to the node.
2557 *
2558 * @throws {TypeError}
2559 * Throws a `TypeError` when the given instance argument isn't
2560 * a valid Class instance.
2561 *
2562 * @returns {Class}
2563 * Returns the bound class instance.
2564 */
2565 bindClassInstance: function(node, inst) {
2566 if (!(inst instanceof Class))
2567 L.error('TypeError', 'Argument must be a class instance');
2568
2569 return this.data(node, '_class', inst);
2570 },
2571
2572 /**
2573 * Finds a bound class instance on the given node itself or the
2574 * first bound instance on its closest parent node.
2575 *
2576 * @instance
2577 * @memberof LuCI.dom
2578 * @param {Node} node
2579 * The DOM `Node` instance to start from.
2580 *
2581 * @returns {Class|null}
2582 * Returns the founds class instance if any or `null` if no bound
2583 * class could be found on the node itself or any of its parents.
2584 */
2585 findClassInstance: function(node) {
2586 var inst = null;
2587
2588 do {
2589 inst = this.data(node, '_class');
2590 node = node.parentNode;
2591 }
2592 while (!(inst instanceof Class) && node != null);
2593
2594 return inst;
2595 },
2596
2597 /**
2598 * Finds a bound class instance on the given node itself or the
2599 * first bound instance on its closest parent node and invokes
2600 * the specified method name on the found class instance.
2601 *
2602 * @instance
2603 * @memberof LuCI.dom
2604 * @param {Node} node
2605 * The DOM `Node` instance to start from.
2606 *
2607 * @param {string} method
2608 * The name of the method to invoke on the found class instance.
2609 *
2610 * @param {...*} params
2611 * Additional arguments to pass to the invoked method as-is.
2612 *
2613 * @returns {*|null}
2614 * Returns the return value of the invoked method if a class
2615 * instance and method has been found. Returns `null` if either
2616 * no bound class instance could be found, or if the found
2617 * instance didn't have the requested `method`.
2618 */
2619 callClassMethod: function(node, method /*, ... */) {
2620 var inst = this.findClassInstance(node);
2621
2622 if (inst == null || typeof(inst[method]) != 'function')
2623 return null;
2624
2625 return inst[method].apply(inst, inst.varargs(arguments, 2));
2626 },
2627
2628 /**
2629 * The ignore callback function is invoked by `isEmpty()` for each
2630 * child node to decide whether to ignore a child node or not.
2631 *
2632 * When this function returns `false`, the node passed to it is
2633 * ignored, else not.
2634 *
2635 * @callback LuCI.dom~ignoreCallbackFn
2636 * @param {Node} node
2637 * The child node to test.
2638 *
2639 * @returns {boolean}
2640 * Boolean indicating whether to ignore the node or not.
2641 */
2642
2643 /**
2644 * Tests whether a given DOM `Node` instance is empty or appears
2645 * empty.
2646 *
2647 * Any element child nodes which have the CSS class `hidden` set
2648 * or for which the optionally passed `ignoreFn` callback function
2649 * returns `false` are ignored.
2650 *
2651 * @instance
2652 * @memberof LuCI.dom
2653 * @param {Node} node
2654 * The DOM `Node` instance to test.
2655 *
2656 * @param {LuCI.dom~ignoreCallbackFn} [ignoreFn]
2657 * Specifies an optional function which is invoked for each child
2658 * node to decide whether the child node should be ignored or not.
2659 *
2660 * @returns {boolean}
2661 * Returns `true` if the node does not have any children or if
2662 * any children node either has a `hidden` CSS class or a `false`
2663 * result when testing it using the given `ignoreFn`.
2664 */
2665 isEmpty: function(node, ignoreFn) {
2666 for (var child = node.firstElementChild; child != null; child = child.nextElementSibling)
2667 if (!child.classList.contains('hidden') && (!ignoreFn || !ignoreFn(child)))
2668 return false;
2669
2670 return true;
2671 }
2672 }),
2673
2674 Poll: Poll,
2675 Class: Class,
2676 Request: Request,
2677
2678 /**
2679 * @class
2680 * @memberof LuCI
2681 * @hideconstructor
2682 * @classdesc
2683 *
2684 * The `view` class forms the basis of views and provides a standard
2685 * set of methods to inherit from.
2686 */
2687 view: Class.extend(/* @lends LuCI.view.prototype */ {
2688 __name__: 'LuCI.View',
2689
2690 __init__: function() {
2691 var vp = document.getElementById('view');
2692
2693 L.dom.content(vp, E('div', { 'class': 'spinning' }, _('Loading view…')));
2694
2695 return Promise.resolve(this.load())
2696 .then(L.bind(this.render, this))
2697 .then(L.bind(function(nodes) {
2698 var vp = document.getElementById('view');
2699
2700 L.dom.content(vp, nodes);
2701 L.dom.append(vp, this.addFooter());
2702 }, this)).catch(L.error);
2703 },
2704
2705 /**
2706 * The load function is invoked before the view is rendered.
2707 *
2708 * The invocation of this function is wrapped by
2709 * `Promise.resolve()` so it may return Promises if needed.
2710 *
2711 * The return value of the function (or the resolved values
2712 * of the promise returned by it) will be passed as first
2713 * argument to `render()`.
2714 *
2715 * This function is supposed to be overwritten by subclasses,
2716 * the default implementation does nothing.
2717 *
2718 * @instance
2719 * @abstract
2720 * @memberof LuCI.view
2721 *
2722 * @returns {*|Promise<*>}
2723 * May return any value or a Promise resolving to any value.
2724 */
2725 load: function() {},
2726
2727 /**
2728 * The render function is invoked after the
2729 * {@link LuCI.view#load load()} function and responsible
2730 * for setting up the view contents. It must return a DOM
2731 * `Node` or `DocumentFragment` holding the contents to
2732 * insert into the view area.
2733 *
2734 * The invocation of this function is wrapped by
2735 * `Promise.resolve()` so it may return Promises if needed.
2736 *
2737 * The return value of the function (or the resolved values
2738 * of the promise returned by it) will be inserted into the
2739 * main content area using
2740 * {@link LuCI.dom#append dom.append()}.
2741 *
2742 * This function is supposed to be overwritten by subclasses,
2743 * the default implementation does nothing.
2744 *
2745 * @instance
2746 * @abstract
2747 * @memberof LuCI.view
2748 * @param {*|null} load_results
2749 * This function will receive the return value of the
2750 * {@link LuCI.view#load view.load()} function as first
2751 * argument.
2752 *
2753 * @returns {Node|Promise<Node>}
2754 * Should return a DOM `Node` value or a `Promise` resolving
2755 * to a `Node` value.
2756 */
2757 render: function() {},
2758
2759 /**
2760 * The handleSave function is invoked when the user clicks
2761 * the `Save` button in the page action footer.
2762 *
2763 * The default implementation should be sufficient for most
2764 * views using {@link form#Map form.Map()} based forms - it
2765 * will iterate all forms present in the view and invoke
2766 * the {@link form#Map#save Map.save()} method on each form.
2767 *
2768 * Views not using `Map` instances or requiring other special
2769 * logic should overwrite `handleSave()` with a custom
2770 * implementation.
2771 *
2772 * To disable the `Save` page footer button, views extending
2773 * this base class should overwrite the `handleSave` function
2774 * with `null`.
2775 *
2776 * The invocation of this function is wrapped by
2777 * `Promise.resolve()` so it may return Promises if needed.
2778 *
2779 * @instance
2780 * @memberof LuCI.view
2781 * @param {Event} ev
2782 * The DOM event that triggered the function.
2783 *
2784 * @returns {*|Promise<*>}
2785 * Any return values of this function are discarded, but
2786 * passed through `Promise.resolve()` to ensure that any
2787 * returned promise runs to completion before the button
2788 * is reenabled.
2789 */
2790 handleSave: function(ev) {
2791 var tasks = [];
2792
2793 document.getElementById('maincontent')
2794 .querySelectorAll('.cbi-map').forEach(function(map) {
2795 tasks.push(L.dom.callClassMethod(map, 'save'));
2796 });
2797
2798 return Promise.all(tasks);
2799 },
2800
2801 /**
2802 * The handleSaveApply function is invoked when the user clicks
2803 * the `Save & Apply` button in the page action footer.
2804 *
2805 * The default implementation should be sufficient for most
2806 * views using {@link form#Map form.Map()} based forms - it
2807 * will first invoke
2808 * {@link LuCI.view.handleSave view.handleSave()} and then
2809 * call {@link ui#changes#apply ui.changes.apply()} to start the
2810 * modal config apply and page reload flow.
2811 *
2812 * Views not using `Map` instances or requiring other special
2813 * logic should overwrite `handleSaveApply()` with a custom
2814 * implementation.
2815 *
2816 * To disable the `Save & Apply` page footer button, views
2817 * extending this base class should overwrite the
2818 * `handleSaveApply` function with `null`.
2819 *
2820 * The invocation of this function is wrapped by
2821 * `Promise.resolve()` so it may return Promises if needed.
2822 *
2823 * @instance
2824 * @memberof LuCI.view
2825 * @param {Event} ev
2826 * The DOM event that triggered the function.
2827 *
2828 * @returns {*|Promise<*>}
2829 * Any return values of this function are discarded, but
2830 * passed through `Promise.resolve()` to ensure that any
2831 * returned promise runs to completion before the button
2832 * is reenabled.
2833 */
2834 handleSaveApply: function(ev, mode) {
2835 return this.handleSave(ev).then(function() {
2836 L.ui.changes.apply(mode == '0');
2837 });
2838 },
2839
2840 /**
2841 * The handleReset function is invoked when the user clicks
2842 * the `Reset` button in the page action footer.
2843 *
2844 * The default implementation should be sufficient for most
2845 * views using {@link form#Map form.Map()} based forms - it
2846 * will iterate all forms present in the view and invoke
2847 * the {@link form#Map#save Map.reset()} method on each form.
2848 *
2849 * Views not using `Map` instances or requiring other special
2850 * logic should overwrite `handleReset()` with a custom
2851 * implementation.
2852 *
2853 * To disable the `Reset` page footer button, views extending
2854 * this base class should overwrite the `handleReset` function
2855 * with `null`.
2856 *
2857 * The invocation of this function is wrapped by
2858 * `Promise.resolve()` so it may return Promises if needed.
2859 *
2860 * @instance
2861 * @memberof LuCI.view
2862 * @param {Event} ev
2863 * The DOM event that triggered the function.
2864 *
2865 * @returns {*|Promise<*>}
2866 * Any return values of this function are discarded, but
2867 * passed through `Promise.resolve()` to ensure that any
2868 * returned promise runs to completion before the button
2869 * is reenabled.
2870 */
2871 handleReset: function(ev) {
2872 var tasks = [];
2873
2874 document.getElementById('maincontent')
2875 .querySelectorAll('.cbi-map').forEach(function(map) {
2876 tasks.push(L.dom.callClassMethod(map, 'reset'));
2877 });
2878
2879 return Promise.all(tasks);
2880 },
2881
2882 /**
2883 * Renders a standard page action footer if any of the
2884 * `handleSave()`, `handleSaveApply()` or `handleReset()`
2885 * functions are defined.
2886 *
2887 * The default implementation should be sufficient for most
2888 * views - it will render a standard page footer with action
2889 * buttons labeled `Save`, `Save & Apply` and `Reset`
2890 * triggering the `handleSave()`, `handleSaveApply()` and
2891 * `handleReset()` functions respectively.
2892 *
2893 * When any of these `handle*()` functions is overwritten
2894 * with `null` by a view extending this class, the
2895 * corresponding button will not be rendered.
2896 *
2897 * @instance
2898 * @memberof LuCI.view
2899 * @returns {DocumentFragment}
2900 * Returns a `DocumentFragment` containing the footer bar
2901 * with buttons for each corresponding `handle*()` action
2902 * or an empty `DocumentFragment` if all three `handle*()`
2903 * methods are overwritten with `null`.
2904 */
2905 addFooter: function() {
2906 var footer = E([]);
2907
2908 var saveApplyBtn = this.handleSaveApply ? new L.ui.ComboButton('0', {
2909 0: [ _('Save & Apply') ],
2910 1: [ _('Apply unchecked') ]
2911 }, {
2912 classes: {
2913 0: 'cbi-button cbi-button-apply important',
2914 1: 'cbi-button cbi-button-negative important'
2915 },
2916 click: L.ui.createHandlerFn(this, 'handleSaveApply')
2917 }).render() : E([]);
2918
2919 if (this.handleSaveApply || this.handleSave || this.handleReset) {
2920 footer.appendChild(E('div', { 'class': 'cbi-page-actions' }, [
2921 saveApplyBtn, ' ',
2922 this.handleSave ? E('button', {
2923 'class': 'cbi-button cbi-button-save',
2924 'click': L.ui.createHandlerFn(this, 'handleSave')
2925 }, [ _('Save') ]) : '', ' ',
2926 this.handleReset ? E('button', {
2927 'class': 'cbi-button cbi-button-reset',
2928 'click': L.ui.createHandlerFn(this, 'handleReset')
2929 }, [ _('Reset') ]) : ''
2930 ]));
2931 }
2932
2933 return footer;
2934 }
2935 })
2936 });
2937
2938 /**
2939 * @class
2940 * @memberof LuCI
2941 * @deprecated
2942 * @classdesc
2943 *
2944 * The `LuCI.XHR` class is a legacy compatibility shim for the
2945 * functionality formerly provided by `xhr.js`. It is registered as global
2946 * `window.XHR` symbol for compatibility with legacy code.
2947 *
2948 * New code should use {@link LuCI.Request} instead to implement HTTP
2949 * request handling.
2950 */
2951 var XHR = Class.extend(/** @lends LuCI.XHR.prototype */ {
2952 __name__: 'LuCI.XHR',
2953 __init__: function() {
2954 if (window.console && console.debug)
2955 console.debug('Direct use XHR() is deprecated, please use L.Request instead');
2956 },
2957
2958 _response: function(cb, res, json, duration) {
2959 if (this.active)
2960 cb(res, json, duration);
2961 delete this.active;
2962 },
2963
2964 /**
2965 * This function is a legacy wrapper around
2966 * {@link LuCI#get LuCI.get()}.
2967 *
2968 * @instance
2969 * @deprecated
2970 * @memberof LuCI.XHR
2971 *
2972 * @param {string} url
2973 * The URL to request
2974 *
2975 * @param {Object} [data]
2976 * Additional query string data
2977 *
2978 * @param {LuCI.requestCallbackFn} [callback]
2979 * Callback function to invoke on completion
2980 *
2981 * @param {number} [timeout]
2982 * Request timeout to use
2983 *
2984 * @return {Promise<null>}
2985 */
2986 get: function(url, data, callback, timeout) {
2987 this.active = true;
2988 L.get(url, data, this._response.bind(this, callback), timeout);
2989 },
2990
2991 /**
2992 * This function is a legacy wrapper around
2993 * {@link LuCI#post LuCI.post()}.
2994 *
2995 * @instance
2996 * @deprecated
2997 * @memberof LuCI.XHR
2998 *
2999 * @param {string} url
3000 * The URL to request
3001 *
3002 * @param {Object} [data]
3003 * Additional data to append to the request body.
3004 *
3005 * @param {LuCI.requestCallbackFn} [callback]
3006 * Callback function to invoke on completion
3007 *
3008 * @param {number} [timeout]
3009 * Request timeout to use
3010 *
3011 * @return {Promise<null>}
3012 */
3013 post: function(url, data, callback, timeout) {
3014 this.active = true;
3015 L.post(url, data, this._response.bind(this, callback), timeout);
3016 },
3017
3018 /**
3019 * Cancels a running request.
3020 *
3021 * This function does not actually cancel the underlying
3022 * `XMLHTTPRequest` request but it sets a flag which prevents the
3023 * invocation of the callback function when the request eventually
3024 * finishes or timed out.
3025 *
3026 * @instance
3027 * @deprecated
3028 * @memberof LuCI.XHR
3029 */
3030 cancel: function() { delete this.active },
3031
3032 /**
3033 * Checks the running state of the request.
3034 *
3035 * @instance
3036 * @deprecated
3037 * @memberof LuCI.XHR
3038 *
3039 * @returns {boolean}
3040 * Returns `true` if the request is still running or `false` if it
3041 * already completed.
3042 */
3043 busy: function() { return (this.active === true) },
3044
3045 /**
3046 * Ignored for backwards compatibility.
3047 *
3048 * This function does nothing.
3049 *
3050 * @instance
3051 * @deprecated
3052 * @memberof LuCI.XHR
3053 */
3054 abort: function() {},
3055
3056 /**
3057 * Existing for backwards compatibility.
3058 *
3059 * This function simply throws an `InternalError` when invoked.
3060 *
3061 * @instance
3062 * @deprecated
3063 * @memberof LuCI.XHR
3064 *
3065 * @throws {InternalError}
3066 * Throws an `InternalError` with the message `Not implemented`
3067 * when invoked.
3068 */
3069 send_form: function() { L.error('InternalError', 'Not implemented') },
3070 });
3071
3072 XHR.get = function() { return window.L.get.apply(window.L, arguments) };
3073 XHR.post = function() { return window.L.post.apply(window.L, arguments) };
3074 XHR.poll = function() { return window.L.poll.apply(window.L, arguments) };
3075 XHR.stop = Request.poll.remove.bind(Request.poll);
3076 XHR.halt = Request.poll.stop.bind(Request.poll);
3077 XHR.run = Request.poll.start.bind(Request.poll);
3078 XHR.running = Request.poll.active.bind(Request.poll);
3079
3080 window.XHR = XHR;
3081 window.LuCI = LuCI;
3082 })(window, document);