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