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