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