diff options
author | Jo-Philipp Wich <jo@mein.io> | 2019-10-02 08:59:00 +0200 |
---|---|---|
committer | Jo-Philipp Wich <jo@mein.io> | 2019-10-02 08:59:41 +0200 |
commit | b2809cebd8009e7a68e2ad7da95b6335e1e9527b (patch) | |
tree | 10746094ea9f5d7110abb0bca1699153b0b60233 /modules/luci-base/htdocs/luci-static | |
parent | 93934c08ca0d20c132138f23f4ded68c1b8080b8 (diff) |
luci-base: luci.js, rpc.js, uci.js, network.js: add JSDoc annotations
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
Diffstat (limited to 'modules/luci-base/htdocs/luci-static')
-rw-r--r-- | modules/luci-base/htdocs/luci-static/resources/luci.js | 1682 | ||||
-rw-r--r-- | modules/luci-base/htdocs/luci-static/resources/network.js | 1731 | ||||
-rw-r--r-- | modules/luci-base/htdocs/luci-static/resources/rpc.js | 257 | ||||
-rw-r--r-- | modules/luci-base/htdocs/luci-static/resources/uci.js | 405 |
4 files changed, 4045 insertions, 30 deletions
diff --git a/modules/luci-base/htdocs/luci-static/resources/luci.js b/modules/luci-base/htdocs/luci-static/resources/luci.js index 4e3c8445a..cad720853 100644 --- a/modules/luci-base/htdocs/luci-static/resources/luci.js +++ b/modules/luci-base/htdocs/luci-static/resources/luci.js @@ -1,3 +1,14 @@ +/** + * @class LuCI + * @classdesc + * + * This is the LuCI base class. It is automatically instantiated and + * accessible using the global `L` variable. + * + * @param {Object} env + * The environment settings to use for the LuCI runtime. + */ + (function(window, document, undefined) { 'use strict'; @@ -45,7 +56,34 @@ return s.replace(/(?:^|[\. -])(.)/g, function(m0, m1) { return m1.toUpperCase() }); }; + /** + * @class Class + * @hideconstructor + * @memberof LuCI + * @classdesc + * + * `LuCI.Class` is the abstract base class all LuCI classes inherit from. + * + * It provides simple means to create subclasses of given classes and + * implements prototypal inheritance. + */ var superContext = null, Class = Object.assign(function() {}, { + /** + * Extends this base class with the properties described in + * `properties` and returns a new subclassed Class instance + * + * @memberof LuCI.Class + * + * @param {Object<string, *>} properties + * An object describing the properties to add to the new + * subclass. + * + * @returns {LuCI.Class} + * Returns a new LuCI.Class sublassed from this class, extended + * by the given properties and with its prototype set to this base + * class to enable inheritance. The resulting value represents a + * class constructor and can be instantiated with `new`. + */ extend: function(properties) { var props = { __base__: { value: this.prototype }, @@ -79,16 +117,61 @@ return ClassConstructor; }, + /** + * Extends this base class with the properties described in + * `properties`, instantiates the resulting subclass using + * the additional optional arguments passed to this function + * and returns the resulting subclassed Class instance. + * + * This function serves as a convenience shortcut for + * {@link LuCI.Class.extend Class.extend()} and subsequent + * `new`. + * + * @memberof LuCI.Class + * + * @param {Object<string, *>} properties + * An object describing the properties to add to the new + * subclass. + * + * @param {...*} [new_args] + * Specifies arguments to be passed to the subclass constructor + * as-is in order to instantiate the new subclass. + * + * @returns {LuCI.Class} + * Returns a new LuCI.Class instance extended by the given + * properties with its prototype set to this base class to + * enable inheritance. + */ singleton: function(properties /*, ... */) { return Class.extend(properties) .instantiate(Class.prototype.varargs(arguments, 1)); }, + /** + * Calls the class constructor using `new` with the given argument + * array being passed as variadic parameters to the constructor. + * + * @memberof LuCI.Class + * + * @param {Array<*>} params + * An array of arbitrary values which will be passed as arguments + * to the constructor function. + * + * @param {...*} [new_args] + * Specifies arguments to be passed to the subclass constructor + * as-is in order to instantiate the new subclass. + * + * @returns {LuCI.Class} + * Returns a new LuCI.Class instance extended by the given + * properties with its prototype set to this base class to + * enable inheritance. + */ instantiate: function(args) { return new (Function.prototype.bind.apply(this, Class.prototype.varargs(args, 0, null)))(); }, + /* unused */ call: function(self, method) { if (typeof(this.prototype[method]) != 'function') throw new ReferenceError(method + ' is not defined in class'); @@ -96,18 +179,91 @@ return this.prototype[method].apply(self, self.varargs(arguments, 1)); }, - isSubclass: function(_class) { - return (_class != null && - typeof(_class) == 'function' && - _class.prototype instanceof this); + /** + * Checks whether the given class value is a subclass of this class. + * + * @memberof LuCI.Class + * + * @param {LuCI.Class} classValue + * The class object to test. + * + * @returns {boolean} + * Returns `true` when the given `classValue` is a subclass of this + * class or `false` if the given value is not a valid class or not + * a subclass of this class'. + */ + isSubclass: function(classValue) { + return (classValue != null && + typeof(classValue) == 'function' && + classValue.prototype instanceof this); }, prototype: { + /** + * Extract all values from the given argument array beginning from + * `offset` and prepend any further given optional parameters to + * the beginning of the resulting array copy. + * + * @memberof LuCI.Class + * @instance + * + * @param {Array<*>} args + * The array to extract the values from. + * + * @param {number} offset + * The offset from which to extract the values. An offset of `0` + * would copy all values till the end. + * + * @param {...*} [extra_args] + * Extra arguments to add to prepend to the resultung array. + * + * @returns {Array<*>} + * Returns a new array consisting of the optional extra arguments + * and the values extracted from the `args` array beginning with + * `offset`. + */ varargs: function(args, offset /*, ... */) { return Array.prototype.slice.call(arguments, 2) .concat(Array.prototype.slice.call(args, offset)); }, + /** + * Walks up the parent class chain and looks for a class member + * called `key` in any of the parent classes this class inherits + * from. Returns the member value of the superclass or calls the + * member as function and returns its return value when the + * optional `callArgs` array is given. + * + * This function has two signatures and is sensitive to the + * amount of arguments passed to it: + * - `super('key')` - + * Returns the value of `key` when found within one of the + * parent classes. + * - `super('key', ['arg1', 'arg2'])` - + * Calls the `key()` method with parameters `arg1` and `arg2` + * when found within one of the parent classes. + * + * @memberof LuCI.Class + * @instance + * + * @param {string} key + * The name of the superclass member to retrieve. + * + * @param {Array<*>} [callArgs] + * An optional array of function call parameters to use. When + * this parameter is specified, the found member value is called + * as function using the values of this array as arguments. + * + * @throws {ReferenceError} + * Throws a `ReferenceError` when `callArgs` are specified and + * the found member named by `key` is not a function value. + * + * @returns {*|null} + * Returns the value of the found member or the return value of + * the call to the found method. Returns `null` when no member + * was found in the parent class chain or when the call to the + * superclass method returned `null`. + */ super: function(key, callArgs) { for (superContext = Object.getPrototypeOf(superContext || Object.getPrototypeOf(this)); @@ -134,6 +290,14 @@ return res; }, + /** + * Returns a string representation of this class. + * + * @returns {string} + * Returns a string representation of this class containing the + * constructor functions `displayName` and describing the class + * members and their respective types. + */ toString: function() { var s = '[' + this.constructor.displayName + ']', f = true; for (var k in this) { @@ -148,11 +312,16 @@ }); - /* - * HTTP Request helper + /** + * @class + * @memberof LuCI + * @hideconstructor + * @classdesc + * + * The `Headers` class is an internal utility class exposed in HTTP + * response objects using the `response.headers` property. */ - - var Headers = Class.extend({ + var Headers = Class.extend(/** @lends LuCI.Headers.prototype */ { __name__: 'LuCI.XHR.Headers', __init__: function(xhr) { var hdrs = this.headers = {}; @@ -163,25 +332,106 @@ }); }, + /** + * Checks whether the given header name is present. + * Note: Header-Names are case-insensitive. + * + * @instance + * @memberof LuCI.Headers + * @param {string} name + * The header name to check + * + * @returns {boolean} + * Returns `true` if the header name is present, `false` otherwise + */ has: function(name) { return this.headers.hasOwnProperty(String(name).toLowerCase()); }, + /** + * Returns the value of the given header name. + * Note: Header-Names are case-insensitive. + * + * @instance + * @memberof LuCI.Headers + * @param {string} name + * The header name to read + * + * @returns {string|null} + * The value of the given header name or `null` if the header isn't present. + */ get: function(name) { var key = String(name).toLowerCase(); return this.headers.hasOwnProperty(key) ? this.headers[key] : null; } }); + /** + * @class + * @memberof LuCI + * @hideconstructor + * @classdesc + * + * The `Response` class is an internal utility class representing HTTP responses. + */ var Response = Class.extend({ __name__: 'LuCI.XHR.Response', __init__: function(xhr, url, duration, headers, content) { + /** + * Describes whether the response is successful (status codes `200..299`) or not + * @instance + * @memberof LuCI.Response + * @name ok + * @type {boolean} + */ this.ok = (xhr.status >= 200 && xhr.status <= 299); + + /** + * The numeric HTTP status code of the response + * @instance + * @memberof LuCI.Response + * @name status + * @type {number} + */ this.status = xhr.status; + + /** + * The HTTP status description message of the response + * @instance + * @memberof LuCI.Response + * @name statusText + * @type {string} + */ this.statusText = xhr.statusText; + + /** + * The HTTP headers of the response + * @instance + * @memberof LuCI.Response + * @name headers + * @type {LuCI.Headers} + */ this.headers = (headers != null) ? headers : new Headers(xhr); + + /** + * The total duration of the HTTP request in milliseconds + * @instance + * @memberof LuCI.Response + * @name duration + * @type {number} + */ this.duration = duration; + + /** + * The final URL of the request, i.e. after following redirects. + * @instance + * @memberof LuCI.Response + * @name url + * @type {string} + */ this.url = url; + + /* privates */ this.xhr = xhr; if (content != null && typeof(content) == 'object') { @@ -198,6 +448,20 @@ } }, + /** + * Clones the given response object, optionally overriding the content + * of the cloned instance. + * + * @instance + * @memberof LuCI.Response + * @param {*} [content] + * Override the content of the cloned response. Object values will be + * treated as JSON response data, all other types will be converted + * using `String()` and treated as response text. + * + * @returns {LuCI.Response} + * The cloned `Response` instance. + */ clone: function(content) { var copy = new Response(this.xhr, this.url, this.duration, this.headers, content); @@ -208,6 +472,17 @@ return copy; }, + /** + * Access the response content as JSON data. + * + * @instance + * @memberof LuCI.Response + * @throws {SyntaxError} + * Throws `SyntaxError` if the content isn't valid JSON. + * + * @returns {*} + * The parsed JSON data. + */ json: function() { if (this.responseJSON == null) this.responseJSON = JSON.parse(this.responseText); @@ -215,6 +490,14 @@ return this.responseJSON; }, + /** + * Access the response content as string. + * + * @instance + * @memberof LuCI.Response + * @returns {string} + * The response content. + */ text: function() { if (this.responseText == null && this.responseJSON != null) this.responseText = JSON.stringify(this.responseJSON); @@ -274,11 +557,32 @@ }); } - var Request = Class.singleton({ + /** + * @class + * @memberof LuCI + * @hideconstructor + * @classdesc + * + * The `Request` class allows initiating HTTP requests and provides utilities + * for dealing with responses. + */ + var Request = Class.singleton(/** @lends LuCI.Request.prototype */ { __name__: 'LuCI.Request', interceptors: [], + /** + * Turn the given relative URL into an absolute URL if necessary. + * + * @instance + * @memberof LuCI.Request + * @param {string} url + * The URL to convert. + * + * @returns {string} + * The absolute URL derived from the given one, or the original URL + * if it already was absolute. + */ expandURL: function(url) { if (!/^(?:[^/]+:)?\/\//.test(url)) url = location.protocol + '//' + location.host + url; @@ -286,6 +590,61 @@ return url; }, + /** + * @typedef {Object} RequestOptions + * @memberof LuCI.Request + * + * @property {string} [method=GET] + * The HTTP method to use, e.g. `GET` or `POST`. + * + * @property {Object<string, Object|string>} [query] + * Query string data to append to the URL. Non-string values of the + * given object will be converted to JSON. + * + * @property {boolean} [cache=false] + * Specifies whether the HTTP response may be retrieved from cache. + * + * @property {string} [username] + * Provides a username for HTTP basic authentication. + * + * @property {string} [password] + * Provides a password for HTTP basic authentication. + * + * @property {number} [timeout] + * Specifies the request timeout in seconds. + * + * @property {boolean} [credentials=false] + * Whether to include credentials such as cookies in the request. + * + * @property {*} [content] + * Specifies the HTTP message body to send along with the request. + * If the value is a function, it is invoked and the return value + * used as content, if it is a FormData instance, it is used as-is, + * if it is an object, it will be converted to JSON, in all other + * cases it is converted to a string. + * + * @property {Object<string, string>} [header] + * Specifies HTTP headers to set for the request. + * + * @property {function} [progress] + * An optional request callback function which receives ProgressEvent + * instances as sole argument during the HTTP request transfer. + */ + + /** + * Initiate an HTTP request to the given target. + * + * @instance + * @memberof LuCI.Request + * @param {string} target + * The URL to request. + * + * @param {LuCI.Request.RequestOptions} [options] + * Additional options to configure the request. + * + * @returns {Promise<LuCI.Response>} + * The resulting HTTP response. + */ request: function(target, options) { var state = { xhr: new XMLHttpRequest(), url: this.expandURL(target), start: Date.now() }, opt = Object.assign({}, options, state), @@ -421,20 +780,86 @@ } }, + /** + * Initiate an HTTP GET request to the given target. + * + * @instance + * @memberof LuCI.Request + * @param {string} target + * The URL to request. + * + * @param {LuCI.Request.RequestOptions} [options] + * Additional options to configure the request. + * + * @returns {Promise<LuCI.Response>} + * The resulting HTTP response. + */ get: function(url, options) { return this.request(url, Object.assign({ method: 'GET' }, options)); }, + /** + * Initiate an HTTP POST request to the given target. + * + * @instance + * @memberof LuCI.Request + * @param {string} target + * The URL to request. + * + * @param {*} [data] + * The request data to send, see {@link LuCI.Request.RequestOptions} for details. + * + * @param {LuCI.Request.RequestOptions} [options] + * Additional options to configure the request. + * + * @returns {Promise<LuCI.Response>} + * The resulting HTTP response. + */ post: function(url, data, options) { return this.request(url, Object.assign({ method: 'POST', content: data }, options)); }, + /** + * Interceptor functions are invoked whenever an HTTP reply is received, in the order + * these functions have been registered. + * @callback LuCI.Request.interceptorFn + * @param {LuCI.Response} res + * The HTTP response object + */ + + /** + * Register an HTTP response interceptor function. Interceptor + * functions are useful to perform default actions on incoming HTTP + * responses, such as checking for expired authentication or for + * implementing request retries before returning a failure. + * + * @instance + * @memberof LuCI.Request + * @param {LuCI.Request.interceptorFn} interceptorFn + * The interceptor function to register. + * + * @returns {LuCI.Request.interceptorFn} + * The registered function. + */ addInterceptor: function(interceptorFn) { if (typeof(interceptorFn) == 'function') this.interceptors.push(interceptorFn); return interceptorFn; }, + /** + * Remove an HTTP response interceptor function. The passed function + * value must be the very same value that was used to register the + * function. + * + * @instance + * @memberof LuCI.Request + * @param {LuCI.Request.interceptorFn} interceptorFn + * The interceptor function to remove. + * + * @returns {boolean} + * Returns `true` if any function has been removed, else `false`. + */ removeInterceptor: function(interceptorFn) { var oldlen = this.interceptors.length, i = oldlen; while (i--) @@ -443,7 +868,59 @@ return (this.interceptors.length < oldlen); }, + /** + * @class + * @memberof LuCI.Request + * @hideconstructor + * @classdesc + * + * The `Request.poll` class provides some convience wrappers around + * {@link LuCI.Poll} mainly to simplify registering repeating HTTP + * request calls as polling functions. + */ poll: { + /** + * The callback function is invoked whenever an HTTP reply to a + * polled request is received or when the polled request timed + * out. + * + * @callback LuCI.Request.poll~callbackFn + * @param {LuCI.Response} res + * The HTTP response object. + * + * @param {*} data + * The response JSON if the response could be parsed as such, + * else `null`. + * + * @param {number} duration + * The total duration of the request in milliseconds. + */ + + /** + * Register a repeating HTTP request with an optional callback + * to invoke whenever a response for the request is received. + * + * @instance + * @memberof LuCI.Request.poll + * @param {number} interval + * The poll interval in seconds. + * + * @param {string} url + * The URL to request on each poll. + * + * @param {LuCI.Request.RequestOptions} [options] + * Additional options to configure the request. + * + * @param {LuCI.Request.poll~callbackFn} [callback] + * {@link LuCI.Request.poll~callbackFn Callback} function to + * invoke for each HTTP reply. + * + * @throws {TypeError} + * Throws `TypeError` when an invalid interval was passed. + * + * @returns {function} + * Returns the internally created poll function. + */ add: function(interval, url, options, callback) { if (isNaN(interval) || interval <= 0) throw new TypeError('Invalid poll interval'); @@ -451,7 +928,7 @@ var ival = interval >>> 0, opts = Object.assign({}, options, { timeout: ival * 1000 - 5 }); - return Poll.add(function() { + var fn = function() { return Request.request(url, options).then(function(res) { if (!Poll.active()) return; @@ -463,21 +940,86 @@ callback(res, null, res.duration); } }); - }, ival); + }; + + return (Poll.add(fn, ival) ? fn : null); }, + /** + * Remove a polling request that has been previously added using `add()`. + * This function is essentially a wrapper around + * {@link LuCI.Poll.remove LuCI.Poll.remove()}. + * + * @instance + * @memberof LuCI.Request.poll + * @param {function} entry + * The poll function returned by {@link LuCI.Request.poll#add add()}. + * + * @returns {boolean} + * Returns `true` if any function has been removed, else `false`. + */ remove: function(entry) { return Poll.remove(entry) }, + + /** + * Alias for {@link LuCI.Poll.start LuCI.Poll.start()}. + * + * @instance + * @memberof LuCI.Request.poll + */ start: function() { return Poll.start() }, + + /** + * Alias for {@link LuCI.Poll.stop LuCI.Poll.stop()}. + * + * @instance + * @memberof LuCI.Request.poll + */ stop: function() { return Poll.stop() }, + + /** + * Alias for {@link LuCI.Poll.active LuCI.Poll.active()}. + * + * @instance + * @memberof LuCI.Request.poll + */ active: function() { return Poll.active() } } }); - var Poll = Class.singleton({ + /** + * @class + * @memberof LuCI + * @hideconstructor + * @classdesc + * + * The `Poll` class allows registering and unregistering poll actions, + * as well as starting, stopping and querying the state of the polling + * loop. + */ + var Poll = Class.singleton(/** @lends LuCI.Poll.prototype */ { __name__: 'LuCI.Poll', queue: [], + /** + * Add a new operation to the polling loop. If the polling loop is not + * already started at this point, it will be implicitely started. + * + * @instance + * @memberof LuCI.Poll + * @param {function} fn + * The function to invoke on each poll interval. + * + * @param {number} interval + * The poll interval in seconds. + * + * @throws {TypeError} + * Throws `TypeError` when an invalid interval was passed. + * + * @returns {boolean} + * Returns `true` if the function has been added or `false` if it + * already is registered. + */ add: function(fn, interval) { if (interval == null || interval <= 0) interval = window.L ? window.L.env.pollinterval : null; @@ -503,6 +1045,22 @@ return true; }, + /** + * Remove an operation from the polling loop. If no further operatons + * are registered, the polling loop is implicitely stopped. + * + * @instance + * @memberof LuCI.Poll + * @param {function} fn + * The function to remove. + * + * @throws {TypeError} + * Throws `TypeError` when the given argument isn't a function. + * + * @returns {boolean} + * Returns `true` if the function has been removed or `false` if it + * wasn't found. + */ remove: function(fn) { if (typeof(fn) != 'function') throw new TypeError('Invalid argument to LuCI.Poll.remove()'); @@ -519,6 +1077,16 @@ return (this.queue.length != len); }, + /** + * (Re)start the polling loop. Dispatches a custom `poll-start` event + * to the `document` object upon successful start. + * + * @instance + * @memberof LuCI.Poll + * @returns {boolean} + * Returns `true` if polling has been started (or if no functions + * where registered) or `false` when the polling loop already runs. + */ start: function() { if (this.active()) return false; @@ -534,6 +1102,16 @@ return true; }, + /** + * Stop the polling loop. Dispatches a custom `poll-stop` event + * to the `document` object upon successful stop. + * + * @instance + * @memberof LuCI.Poll + * @returns {boolean} + * Returns `true` if polling has been stopped or `false` if it din't + * run to begin with. + */ stop: function() { if (!this.active()) return false; @@ -545,6 +1123,7 @@ return true; }, + /* private */ step: function() { for (var i = 0, e = null; (e = Poll.queue[i]) != null; i++) { if ((Poll.tick % e.i) != 0) @@ -561,6 +1140,13 @@ Poll.tick = (Poll.tick + 1) % Math.pow(2, 32); }, + /** + * Test whether the polling loop is running. + * + * @instance + * @memberof LuCI.Poll + * @returns {boolean} - Returns `true` if polling is active, else `false`. + */ active: function() { return (this.timer != null); } @@ -574,7 +1160,7 @@ sysFeatures = null, classes = {}; - var LuCI = Class.extend({ + var LuCI = Class.extend(/** @lends LuCI.prototype */ { __name__: 'LuCI', __init__: function(env) { @@ -621,6 +1207,30 @@ window.cbi_init = function() {}; }, + /** + * Captures the current stack trace and throws an error of the + * specified type as a new exception. Also logs the exception as + * error to the debug console if it is available. + * + * @instance + * @memberof LuCI + * + * @param {Error|string} [type=Error] + * Either a string specifying the type of the error to throw or an + * existing `Error` instance to copy. + * + * @param {string} [fmt=Unspecified error] + * A format string which is used to form the error message, together + * with all subsequent optional arguments. + * + * @param {...*} [args] + * Zero or more variable arguments to the supplied format string. + * + * @throws {Error} + * Throws the created error object with the captured stack trace + * appended to the message and the type set to the given type + * argument or copied from the given error instance. + */ raise: function(type, fmt /*, ...*/) { var e = null, msg = fmt ? String.prototype.format.apply(fmt, this.varargs(arguments, 2)) : null, @@ -663,6 +1273,30 @@ throw e; }, + /** + * A wrapper around {@link LuCI#raise raise()} which also renders + * the error either as modal overlay when `ui.js` is already loaed + * or directly into the view body. + * + * @instance + * @memberof LuCI + * + * @param {Error|string} [type=Error] + * Either a string specifying the type of the error to throw or an + * existing `Error` instance to copy. + * + * @param {string} [fmt=Unspecified error] + * A format string which is used to form the error message, together + * with all subsequent optional arguments. + * + * @param {...*} [args] + * Zero or more variable arguments to the supplied format string. + * + * @throws {Error} + * Throws the created error object with the captured stack trace + * appended to the message and the type set to the given type + * argument or copied from the given error instance. + */ error: function(type, fmt /*, ...*/) { try { L.raise.apply(L, Array.prototype.slice.call(arguments)); @@ -683,11 +1317,65 @@ } }, + /** + * Return a bound function using the given `self` as `this` context + * and any further arguments as parameters to the bound function. + * + * @instance + * @memberof LuCI + * + * @param {function} fn + * The function to bind. + * + * @param {*} self + * The value to bind as `this` context to the specified function. + * + * @param {...*} [args] + * Zero or more variable arguments which are bound to the function + * as parameters. + * + * @returns {function} + * Returns the bound function. + */ bind: function(fn, self /*, ... */) { return Function.prototype.bind.apply(fn, this.varargs(arguments, 2, self)); }, - /* Class require */ + /** + * Load an additional LuCI JavaScript class and its dependencies, + * instantiate it and return the resulting class instance. Each + * class is only loaded once. Subsequent attempts to load the same + * class will return the already instantiated class. + * + * @instance + * @memberof LuCI + * + * @param {string} name + * The name of the class to load in dotted notation. Dots will + * be replaced by spaces and joined with the runtime-determined + * base URL of LuCI.js to form an absolute URL to load the class + * file from. + * + * @throws {DependencyError} + * Throws a `DependencyError` when the class to load includes + * circular dependencies. + * + * @throws {NetworkError} + * Throws `NetworkError` when the underlying {@link LuCI.Request} + * call failed. + * + * @throws {SyntaxError} + * Throws `SyntaxError` when the loaded class file code cannot + * be interpreted by `eval`. + * + * @throws {TypeError} + * Throws `TypeError` when the class file could be loaded and + * interpreted, but when invoking its code did not yield a valid + * class instance. + * + * @returns {Promise<LuCI#Class>} + * Returns the instantiated class. + */ require: function(name, from) { var L = this, url = null, from = from || []; @@ -699,7 +1387,7 @@ 'Circular dependency: class "%s" depends on "%s"', name, from.join('" which depends on "')); - return classes[name]; + return Promise.resolve(classes[name]); } url = '%s/%s.js%s'.format(L.env.base_url, name.replace(/\./g, '/'), (L.env.resource_version ? '?v=' + L.env.resource_version : '')); @@ -858,6 +1546,31 @@ return Promise.resolve(sysFeatures); }, + /** + * Test whether a particular system feature is available, such as + * hostapd SAE support or an installed firewall. The features are + * queried once at the beginning of the LuCI session and cached in + * `SessionStorage` throughout the lifetime of the associated tab or + * browser window. + * + * @instance + * @memberof LuCI + * + * @param {string} feature + * The feature to test. For detailed list of known feature flags, + * see `/modules/luci-base/root/usr/libexec/rpcd/luci`. + * + * @param {string} [subfeature] + * Some feature classes like `hostapd` provide sub-feature flags, + * such as `sae` or `11w` support. The `subfeature` argument can + * be used to query these. + * + * @return {boolean|null} + * Return `true` if the queried feature (and sub-feature) is available + * or `false` if the requested feature isn't present or known. + * Return `null` when a sub-feature was queried for a feature which + * has no sub-features. + */ hasSystemFeature: function() { var ft = sysFeatures[arguments[0]]; @@ -867,6 +1580,7 @@ return (ft != null && ft != false); }, + /* private */ notifySessionExpiry: function() { Poll.stop(); @@ -886,6 +1600,7 @@ L.raise('SessionError', 'Login session is expired'); }, + /* private */ setupDOM: function(res) { var domEv = res[0], uiClass = res[1], @@ -925,15 +1640,42 @@ return this.probeSystemFeatures().finally(this.initDOM); }, + /* private */ initDOM: function() { originalCBIInit(); Poll.start(); document.dispatchEvent(new CustomEvent('luci-loaded')); }, + /** + * The `env` object holds environment settings used by LuCI, such + * as request timeouts, base URLs etc. + * + * @instance + * @memberof LuCI + */ env: {}, - /* URL construction helpers */ + /** + * Construct a relative URL path from the given prefix and parts. + * The resulting URL is guaranteed to only contain the characters + * `a-z`, `A-Z`, `0-9`, `_`, `.`, `%`, `,`, `;`, and `-` as well + * as `/` for the path separator. + * + * @instance + * @memberof LuCI + * + * @param {string} [prefix] + * The prefix to join the given parts with. If the `prefix` is + * omitted, it defaults to an empty string. + * + * @param {string[]} [parts] + * An array of parts to join into an URL path. Parts may contain + * slashes and any of the other characters mentioned above. + * + * @return {string} + * Return the joined URL path. + */ path: function(prefix, parts) { var url = [ prefix || '' ]; @@ -947,24 +1689,108 @@ return url.join(''); }, + /** + * Construct an URL pathrelative to the script path of the server + * side LuCI application (usually `/cgi-bin/luci`). + * + * The resulting URL is guaranteed to only contain the characters + * `a-z`, `A-Z`, `0-9`, `_`, `.`, `%`, `,`, `;`, and `-` as well + * as `/` for the path separator. + * + * @instance + * @memberof LuCI + * + * @param {string[]} [parts] + * An array of parts to join into an URL path. Parts may contain + * slashes and any of the other characters mentioned above. + * + * @return {string} + * Returns the resulting URL path. + */ url: function() { return this.path(this.env.scriptname, arguments); }, + /** + * Construct an URL path relative to the global static resource path + * of the LuCI ui (usually `/luci-static/resources`). + * + * The resulting URL is guaranteed to only contain the characters + * `a-z`, `A-Z`, `0-9`, `_`, `.`, `%`, `,`, `;`, and `-` as well + * as `/` for the path separator. + * + * @instance + * @memberof LuCI + * + * @param {string[]} [parts] + * An array of parts to join into an URL path. Parts may contain + * slashes and any of the other characters mentioned above. + * + * @return {string} + * Returns the resulting URL path. + */ resource: function() { return this.path(this.env.resource, arguments); }, + /** + * Return the complete URL path to the current view. + * + * @instance + * @memberof LuCI + * + * @return {string} + * Returns the URL path to the current view. + */ location: function() { return this.path(this.env.scriptname, this.env.requestpath); }, - /* Data helpers */ + /** + * Tests whether the passed argument is a JavaScript object. + * This function is meant to be an object counterpart to the + * standard `Array.isArray()` function. + * + * @instance + * @memberof LuCI + * + * @param {*} [val] + * The value to test + * + * @return {boolean} + * Returns `true` if the given value is of type object and + * not `null`, else returns `false`. + */ isObject: function(val) { return (val != null && typeof(val) == 'object'); }, + /** + * Return an array of sorted object keys, optionally sorted by + * a different key or a different sorting mode. + * + * @instance + * @memberof LuCI + * + * @param {object} obj + * The object to extract the keys from. If the given value is + * not an object, the function will return an empty array. + * + * @param {string} [key] + * Specifies the key to order by. This is mainly useful for + * nested objects of objects or objects of arrays when sorting + * shall not be performed by the primary object keys but by + * some other key pointing to a value within the nested values. + * + * @param {string} [sortmode] + * May be either `addr` or `num` to override the natural + * lexicographic sorting with a sorting suitable for IP/MAC style + * addresses or numeric values respectively. + * + * @return {string[]} + * Returns an array containing the sorted keys of the given object. + */ sortedKeys: function(obj, key, sortmode) { if (obj == null || typeof(obj) != 'object') return []; @@ -993,6 +1819,23 @@ }); }, + /** + * Converts the given value to an array. If the given value is of + * type array, it is returned as-is, values of type object are + * returned as one-element array containing the object, empty + * strings and `null` values are returned as empty array, all other + * values are converted using `String()`, trimmed, split on white + * space and returned as array. + * + * @instance + * @memberof LuCI + * + * @param {*} val + * The value to convert into an array. + * + * @return {Array<*>} + * Returns the resulting array. + */ toArray: function(val) { if (val == null) return []; @@ -1010,15 +1853,117 @@ }, - /* HTTP resource fetching */ + /** + * The request callback function is invoked whenever an HTTP + * reply to a request made using the `L.get()`, `L.post()` or + * `L.poll()` function is timed out or received successfully. + * + * @instance + * @memberof LuCI + * + * @callback LuCI.requestCallbackFn + * @param {XMLHTTPRequest} xhr + * The XMLHTTPRequest instance used to make the request. + * + * @param {*} data + * The response JSON if the response could be parsed as such, + * else `null`. + * + * @param {number} duration + * The total duration of the request in milliseconds. + */ + + /** + * Issues a GET request to the given url and invokes the specified + * callback function. The function is a wrapper around + * {@link LuCI.Request#request Request.request()}. + * + * @deprecated + * @instance + * @memberof LuCI + * + * @param {string} url + * The URL to request. + * + * @param {Object<string, string>} [args] + * Additional query string arguments to append to the URL. + * + * @param {LuCI.requestCallbackFn} cb + * The callback function to invoke when the request finishes. + * + * @return {Promise<null>} + * Returns a promise resolving to `null` when concluded. + */ get: function(url, args, cb) { return this.poll(null, url, args, cb, false); }, + /** + * Issues a POST request to the given url and invokes the specified + * callback function. The function is a wrapper around + * {@link LuCI.Request#request Request.request()}. The request is + * sent using `application/x-www-form-urlencoded` encoding and will + * contain a field `token` with the current value of `LuCI.env.token` + * by default. + * + * @deprecated + * @instance + * @memberof LuCI + * + * @param {string} url + * The URL to request. + * + * @param {Object<string, string>} [args] + * Additional post arguments to append to the request body. + * + * @param {LuCI.requestCallbackFn} cb + * The callback function to invoke when the request finishes. + * + * @return {Promise<null>} + * Returns a promise resolving to `null` when concluded. + */ post: function(url, args, cb) { return this.poll(null, url, args, cb, true); }, + /** + * Register a polling HTTP request that invokes the specified + * callback function. The function is a wrapper around + * {@link LuCI.Request.poll#add Request.poll.add()}. + * + * @deprecated + * @instance + * @memberof LuCI + * + * @param {number} interval + * The poll interval to use. If set to a value less than or equal + * to `0`, it will default to the global poll interval configured + * in `LuCI.env.pollinterval`. + * + * @param {string} url + * The URL to request. + * + * @param {Object<string, string>} [args] + * Specifies additional arguments for the request. For GET requests, + * the arguments are appended to the URL as query string, for POST + * requests, they'll be added to the request body. + * + * @param {LuCI.requestCallbackFn} cb + * The callback function to invoke whenever a request finishes. + * + * @param {boolean} [post=false] + * When set to `false` or not specified, poll requests will be made + * using the GET method. When set to `true`, POST requests will be + * issued. In case of POST requests, the request body will contain + * an argument `token` with the current value of `LuCI.env.token` by + * default, regardless of the parameters specified with `args`. + * + * @return {function} + * Returns the internally created function that has been passed to + * {@link LuCI.Request.poll#add Request.poll.add()}. This value can + * be passed to {@link LuCI.Poll.remove Poll.remove()} to remove the + * polling request. + */ poll: function(interval, url, args, cb, post) { if (interval !== null && interval <= 0) interval = this.env.pollinterval; @@ -1044,18 +1989,91 @@ }); }, + /** + * Deprecated wrapper around {@link LuCI.Poll.remove Poll.remove()}. + * + * @deprecated + * @instance + * @memberof LuCI + * + * @param {function} entry + * The polling function to remove. + * + * @return {boolean} + * Returns `true` when the function has been removed or `false` if + * it could not be found. + */ stop: function(entry) { return Poll.remove(entry) }, + + /** + * Deprecated wrapper around {@link LuCI.Poll.stop Poll.stop()}. + * + * @deprecated + * @instance + * @memberof LuCI + * + * @return {boolean} + * Returns `true` when the polling loop has been stopped or `false` + * when it didn't run to begin with. + */ halt: function() { return Poll.stop() }, + + /** + * Deprecated wrapper around {@link LuCI.Poll.start Poll.start()}. + * + * @deprecated + * @instance + * @memberof LuCI + * + * @return {boolean} + * Returns `true` when the polling loop has been started or `false` + * when it was already running. + */ run: function() { return Poll.start() }, - /* DOM manipulation */ - dom: Class.singleton({ + + /** + * @class + * @memberof LuCI + * @hideconstructor + * @classdesc + * + * The `dom` class provides convenience method for creating and + * manipulating DOM elements. + */ + dom: Class.singleton(/* @lends LuCI.dom.prototype */ { __name__: 'LuCI.DOM', + /** + * Tests whether the given argument is a valid DOM `Node`. + * + * @instance + * @memberof LuCI.dom + * @param {*} e + * The value to test. + * + * @returns {boolean} + * Returns `true` if the value is a DOM `Node`, else `false`. + */ elem: function(e) { return (e != null && typeof(e) == 'object' && 'nodeType' in e); }, + /** + * Parses a given string as HTML and returns the first child node. + * + * @instance + * @memberof LuCI.dom + * @param {string} s + * A string containing an HTML fragment to parse. Note that only + * the first result of the resulting structure is returned, so an + * input value of `<div>foo</div> <div>bar</div>` will only return + * the first `div` element node. + * + * @returns {Node} + * Returns the first DOM `Node` extracted from the HTML fragment or + * `null` on parsing failures or if no element could be found. + */ parse: function(s) { var elem; @@ -1077,11 +2095,54 @@ return elem || null; }, + /** + * Tests whether a given `Node` matches the given query selector. + * + * This function is a convenience wrapper around the standard + * `Node.matches("selector")` function with the added benefit that + * the `node` argument may be a non-`Node` value, in which case + * this function simply returns `false`. + * + * @instance + * @memberof LuCI.dom + * @param {*} node + * The `Node` argument to test the selector against. + * + * @param {string} [selector] + * The query selector expression to test against the given node. + * + * @returns {boolean} + * Returns `true` if the given node matches the specified selector + * or `false` when the node argument is no valid DOM `Node` or the + * selector didn't match. + */ matches: function(node, selector) { var m = this.elem(node) ? node.matches || node.msMatchesSelector : null; return m ? m.call(node, selector) : false; }, + /** + * Returns the closest parent node that matches the given query + * selector expression. + * + * This function is a convenience wrapper around the standard + * `Node.closest("selector")` function with the added benefit that + * the `node` argument may be a non-`Node` value, in which case + * this function simply returns `null`. + * + * @instance + * @memberof LuCI.dom + * @param {*} node + * The `Node` argument to find the closest parent for. + * + * @param {string} [selector] + * The query selector expression to test against each parent. + * + * @returns {Node|null} + * Returns the closest parent node matching the selector or + * `null` when the node argument is no valid DOM `Node` or the + * selector didn't match any parent. + */ parent: function(node, selector) { if (this.elem(node) && node.closest) return node.closest(selector); @@ -1095,6 +2156,42 @@ return null; }, + /** + * Appends the given children data to the given node. + * + * @instance + * @memberof LuCI.dom + * @param {*} node + * The `Node` argument to append the children to. + * + * @param {*} [children] + * The childrens to append to the given node. + * + * When `children` is an array, then each item of the array + * will be either appended as child element or text node, + * depending on whether the item is a DOM `Node` instance or + * some other non-`null` value. Non-`Node`, non-`null` values + * will be converted to strings first before being passed as + * argument to `createTextNode()`. + * + * When `children` is a function, it will be invoked with + * the passed `node` argument as sole parameter and the `append` + * function will be invoked again, with the given `node` argument + * as first and the return value of the `children` function as + * second parameter. + * + * When `children` is is a DOM `Node` instance, it will be + * appended to the given `node`. + * + * When `children` is any other non-`null` value, it will be + * converted to a string and appened to the `innerHTML` property + * of the given `node`. + * + * @returns {Node|null} + * Returns the last children `Node` appended to the node or `null` + * if either the `node` argument was no valid DOM `node` or if the + * `children` was `null` or didn't result in further DOM nodes. + */ append: function(node, children) { if (!this.elem(node)) return null; @@ -1122,6 +2219,46 @@ return null; }, + /** + * Replaces the content of the given node with the given children. + * + * This function first removes any children of the given DOM + * `Node` and then adds the given given children following the + * rules outlined below. + * + * @instance + * @memberof LuCI.dom + * @param {*} node + * The `Node` argument to replace the children of. + * + * @param {*} [children] + * The childrens to replace into the given node. + * + * When `children` is an array, then each item of the array + * will be either appended as child element or text node, + * depending on whether the item is a DOM `Node` instance or + * some other non-`null` value. Non-`Node`, non-`null` values + * will be converted to strings first before being passed as + * argument to `createTextNode()`. + * + * When `children` is a function, it will be invoked with + * the passed `node` argument as sole parameter and the `append` + * function will be invoked again, with the given `node` argument + * as first and the return value of the `children` function as + * second parameter. + * + * When `children` is is a DOM `Node` instance, it will be + * appended to the given `node`. + * + * When `children` is any other non-`null` value, it will be + * converted to a string and appened to the `innerHTML` property + * of the given `node`. + * + * @returns {Node|null} + * Returns the last children `Node` appended to the node or `null` + * if either the `node` argument was no valid DOM `node` or if the + * `children` was `null` or didn't result in further DOM nodes. + */ content: function(node, children) { if (!this.elem(node)) return null; @@ -1137,6 +2274,39 @@ return this.append(node, children); }, + /** + * Sets attributes or registers event listeners on element nodes. + * + * @instance + * @memberof LuCI.dom + * @param {*} node + * The `Node` argument to set the attributes or add the event + * listeners for. When the given `node` value is not a valid + * DOM `Node`, the function returns and does nothing. + * + * @param {string|Object<string, *>} key + * Specifies either the attribute or event handler name to use, + * or an object containing multiple key, value pairs which are + * each added to the node as either attribute or event handler, + * depending on the respective value. + * + * @param {*} [val] + * Specifies the attribute value or event handler function to add. + * If the `key` parameter is an `Object`, this parameter will be + * ignored. + * + * When `val` is of type function, it will be registered as event + * handler on the given `node` with the `key` parameter being the + * event name. + * + * When `val` is of type object, it will be serialized as JSON and + * added as attribute to the given `node`, using the given `key` + * as attribute name. + * + * When `val` is of any other type, it will be added as attribute + * to the given `node` as-is, with the underlying `setAttribute()` + * call implicitely turning it into a string. + */ attr: function(node, key, val) { if (!this.elem(node)) return null; @@ -1167,6 +2337,54 @@ } }, + /** + * Creates a new DOM `Node` from the given `html`, `attr` and + * `data` parameters. + * + * This function has multiple signatures, it can be either invoked + * in the form `create(html[, attr[, data]])` or in the form + * `create(html[, data])`. The used variant is determined from the + * type of the second argument. + * + * @instance + * @memberof LuCI.dom + * @param {*} html + * Describes the node to create. + * + * When the value of `html` is of type array, a `DocumentFragment` + * node is created and each item of the array is first converted + * to a DOM `Node` by passing it through `create()` and then added + * as child to the fragment. + * + * When the value of `html` is a DOM `Node` instance, no new + * element will be created but the node will be used as-is. + * + * When the value of `html` is a string starting with `<`, it will + * be passed to `dom.parse()` and the resulting value is used. + * + * When the value of `html` is any other string, it will be passed + * to `document.createElement()` for creating a new DOM `Node` of + * the given name. + * + * @param {Object<string, *>} [attr] + * Specifies an Object of key, value pairs to set as attributes + * or event handlers on the created node. Refer to + * {@link LuCI.dom#attr dom.attr()} for details. + * + * @param {*} [data] + * Specifies children to append to the newly created element. + * Refer to {@link LuCI.dom#append dom.append()} for details. + * + * @throws {InvalidCharacterError} + * Throws an `InvalidCharacterError` when the given `html` + * argument contained malformed markup (such as not escaped + * `&` characters in XHTML mode) or when the given node name + * in `html` contains characters which are not legal in DOM + * element names, such as spaces. + * + * @returns {Node} + * Returns the newly created `Node`. + */ create: function() { var html = arguments[0], attr = arguments[1], @@ -1202,6 +2420,47 @@ registry: {}, + /** + * Attaches or detaches arbitrary data to and from a DOM `Node`. + * + * This function is useful to attach non-string values or runtime + * data that is not serializable to DOM nodes. To decouple data + * from the DOM, values are not added directly to nodes, but + * inserted into a registry instead which is then referenced by a + * string key stored as `data-idref` attribute in the node. + * + * This function has multiple signatures and is sensitive to the + * number of arguments passed to it. + * + * - `dom.data(node)` - + * Fetches all data associated with the given node. + * - `dom.data(node, key)` - + * Fetches a specific key associated with the given node. + * - `dom.data(node, key, val)` - + * Sets a specific key to the given value associated with the + * given node. + * - `dom.data(node, null)` - + * Clears any data associated with the node. + * - `dom.data(node, key, null)` - + * Clears the given key associated with the node. + * + * @instance + * @memberof LuCI.dom + * @param {Node} node + * The DOM `Node` instance to set or retrieve the data for. + * + * @param {string|null} [key] + * This is either a string specifying the key to retrieve, or + * `null` to unset the entire node data. + * + * @param {*|null} [val] + * This is either a non-`null` value to set for a given key or + * `null` to remove the given `key` from the specified node. + * + * @returns {*} + * Returns the get or set value, or `null` when no value could + * be found. + */ data: function(node, key, val) { var id = node.getAttribute('data-idref'); @@ -1258,6 +2517,30 @@ return null; }, + /** + * Binds the given class instance ot the specified DOM `Node`. + * + * This function uses the `dom.data()` facility to attach the + * passed instance of a Class to a node. This is needed for + * complex widget elements or similar where the corresponding + * class instance responsible for the element must be retrieved + * from DOM nodes obtained by `querySelector()` or similar means. + * + * @instance + * @memberof LuCI.dom + * @param {Node} node + * The DOM `Node` instance to bind the class to. + * + * @param {Class} inst + * The Class instance to bind to the node. + * + * @throws {TypeError} + * Throws a `TypeError` when the given instance argument isn't + * a valid Class instance. + * + * @returns {Class} + * Returns the bound class instance. + */ bindClassInstance: function(node, inst) { if (!(inst instanceof Class)) L.error('TypeError', 'Argument must be a class instance'); @@ -1265,6 +2548,19 @@ return this.data(node, '_class', inst); }, + /** + * Finds a bound class instance on the given node itself or the + * first bound instance on its closest parent node. + * + * @instance + * @memberof LuCI.dom + * @param {Node} node + * The DOM `Node` instance to start from. + * + * @returns {Class|null} + * Returns the founds class instance if any or `null` if no bound + * class could be found on the node itself or any of its parents. + */ findClassInstance: function(node) { var inst = null; @@ -1277,6 +2573,28 @@ return inst; }, + /** + * Finds a bound class instance on the given node itself or the + * first bound instance on its closest parent node and invokes + * the specified method name on the found class instance. + * + * @instance + * @memberof LuCI.dom + * @param {Node} node + * The DOM `Node` instance to start from. + * + * @param {string} method + * The name of the method to invoke on the found class instance. + * + * @param {...*} params + * Additional arguments to pass to the invoked method as-is. + * + * @returns {*|null} + * Returns the return value of the invoked method if a class + * instance and method has been found. Returns `null` if either + * no bound class instance could be found, or if the found + * instance didn't have the requested `method`. + */ callClassMethod: function(node, method /*, ... */) { var inst = this.findClassInstance(node); @@ -1286,6 +2604,43 @@ return inst[method].apply(inst, inst.varargs(arguments, 2)); }, + /** + * The ignore callback function is invoked by `isEmpty()` for each + * child node to decide whether to ignore a child node or not. + * + * When this function returns `false`, the node passed to it is + * ignored, else not. + * + * @callback LuCI.dom~ignoreCallbackFn + * @param {Node} node + * The child node to test. + * + * @returns {boolean} + * Boolean indicating whether to ignore the node or not. + */ + + /** + * Tests whether a given DOM `Node` instance is empty or appears + * empty. + * + * Any element child nodes which have the CSS class `hidden` set + * or for which the optionally passed `ignoreFn` callback function + * returns `false` are ignored. + * + * @instance + * @memberof LuCI.dom + * @param {Node} node + * The DOM `Node` instance to test. + * + * @param {LuCI.dom~ignoreCallbackFn} [ignoreFn] + * Specifies an optional function which is invoked for each child + * node to decide whether the child node should be ignored or not. + * + * @returns {boolean} + * Returns `true` if the node does not have any children or if + * any children node either has a `hidden` CSS class or a `false` + * result when testing it using the given `ignoreFn`. + */ isEmpty: function(node, ignoreFn) { for (var child = node.firstElementChild; child != null; child = child.nextElementSibling) if (!child.classList.contains('hidden') && (!ignoreFn || !ignoreFn(child))) @@ -1299,7 +2654,16 @@ Class: Class, Request: Request, - view: Class.extend({ + /** + * @class + * @memberof LuCI + * @hideconstructor + * @classdesc + * + * The `view` class forms the basis of views and provides a standard + * set of methods to inherit from. + */ + view: Class.extend(/* @lends LuCI.view.prototype */ { __name__: 'LuCI.View', __init__: function() { @@ -1317,9 +2681,91 @@ }, this)).catch(L.error); }, + /** + * The load function is invoked before the view is rendered. + * + * The invocation of this function is wrapped by + * `Promise.resolve()` so it may return Promises if needed. + * + * The return value of the function (or the resolved values + * of the promise returned by it) will be passed as first + * argument to `render()`. + * + * This function is supposed to be overwritten by subclasses, + * the default implementation does nothing. + * + * @instance + * @abstract + * @memberof LuCI.view + * + * @returns {*|Promise<*>} + * May return any value or a Promise resolving to any value. + */ load: function() {}, + + /** + * The render function is invoked after the + * {@link LuCI.view#load load()} function and responsible + * for setting up the view contents. It must return a DOM + * `Node` or `DocumentFragment` holding the contents to + * insert into the view area. + * + * The invocation of this function is wrapped by + * `Promise.resolve()` so it may return Promises if needed. + * + * The return value of the function (or the resolved values + * of the promise returned by it) will be inserted into the + * main content area using + * {@link LuCI.dom#append dom.append()}. + * + * This function is supposed to be overwritten by subclasses, + * the default implementation does nothing. + * + * @instance + * @abstract + * @memberof LuCI.view + * @param {*|null} load_results + * This function will receive the return value of the + * {@link LuCI.view#load view.load()} function as first + * argument. + * + * @returns {Node|Promise<Node>} + * Should return a DOM `Node` value or a `Promise` resolving + * to a `Node` value. + */ render: function() {}, + /** + * The handleSave function is invoked when the user clicks + * the `Save` button in the page action footer. + * + * The default implementation should be sufficient for most + * views using {@link form#Map form.Map()} based forms - it + * will iterate all forms present in the view and invoke + * the {@link form#Map#save Map.save()} method on each form. + * + * Views not using `Map` instances or requiring other special + * logic should overwrite `handleSave()` with a custom + * implementation. + * + * To disable the `Save` page footer button, views extending + * this base class should overwrite the `handleSave` function + * with `null`. + * + * The invocation of this function is wrapped by + * `Promise.resolve()` so it may return Promises if needed. + * + * @instance + * @memberof LuCI.view + * @param {Event} ev + * The DOM event that triggered the function. + * + * @returns {*|Promise<*>} + * Any return values of this function are discarded, but + * passed through `Promise.resolve()` to ensure that any + * returned promise runs to completion before the button + * is reenabled. + */ handleSave: function(ev) { var tasks = []; @@ -1331,12 +2777,76 @@ return Promise.all(tasks); }, + /** + * The handleSaveApply function is invoked when the user clicks + * the `Save & Apply` button in the page action footer. + * + * The default implementation should be sufficient for most + * views using {@link form#Map form.Map()} based forms - it + * will first invoke + * {@link LuCI.view.handleSave view.handleSave()} and then + * call {@link ui#changes#apply ui.changes.apply()} to start the + * modal config apply and page reload flow. + * + * Views not using `Map` instances or requiring other special + * logic should overwrite `handleSaveApply()` with a custom + * implementation. + * + * To disable the `Save & Apply` page footer button, views + * extending this base class should overwrite the + * `handleSaveApply` function with `null`. + * + * The invocation of this function is wrapped by + * `Promise.resolve()` so it may return Promises if needed. + * + * @instance + * @memberof LuCI.view + * @param {Event} ev + * The DOM event that triggered the function. + * + * @returns {*|Promise<*>} + * Any return values of this function are discarded, but + * passed through `Promise.resolve()` to ensure that any + * returned promise runs to completion before the button + * is reenabled. + */ handleSaveApply: function(ev) { return this.handleSave(ev).then(function() { L.ui.changes.apply(true); }); }, + /** + * The handleReset function is invoked when the user clicks + * the `Reset` button in the page action footer. + * + * The default implementation should be sufficient for most + * views using {@link form#Map form.Map()} based forms - it + * will iterate all forms present in the view and invoke + * the {@link form#Map#save Map.reset()} method on each form. + * + * Views not using `Map` instances or requiring other special + * logic should overwrite `handleReset()` with a custom + * implementation. + * + * To disable the `Reset` page footer button, views extending + * this base class should overwrite the `handleReset` function + * with `null`. + * + * The invocation of this function is wrapped by + * `Promise.resolve()` so it may return Promises if needed. + * + * @instance + * @memberof LuCI.view + * @param {Event} ev + * The DOM event that triggered the function. + * + * @returns {*|Promise<*>} + * Any return values of this function are discarded, but + * passed through `Promise.resolve()` to ensure that any + * returned promise runs to completion before the button + * is reenabled. + */ handleReset: function(ev) { var tasks = []; @@ -1348,6 +2858,29 @@ return Promise.all(tasks); }, + /** + * Renders a standard page action footer if any of the + * `handleSave()`, `handleSaveApply()` or `handleReset()` + * functions are defined. + * + * The default implementation should be sufficient for most + * views - it will render a standard page footer with action + * buttons labeled `Save`, `Save & Apply` and `Reset` + * triggering the `handleSave()`, `handleSaveApply()` and + * `handleReset()` functions respectively. + * + * When any of these `handle*()` functions is overwritten + * with `null` by a view extending this class, the + * corresponding button will not be rendered. + * + * @instance + * @memberof LuCI.view + * @returns {DocumentFragment} + * Returns a `DocumentFragment` containing the footer bar + * with buttons for each corresponding `handle*()` action + * or an empty `DocumentFragment` if all three `handle*()` + * methods are overwritten with `null`. + */ addFooter: function() { var footer = E([]); @@ -1373,7 +2906,20 @@ }) }); - var XHR = Class.extend({ + /** + * @class + * @memberof LuCI + * @deprecated + * @classdesc + * + * The `LuCI.XHR` class is a legacy compatibility shim for the + * functionality formerly provided by `xhr.js`. It is registered as global + * `window.XHR` symbol for compatibility with legacy code. + * + * New code should use {@link LuCI.Request} instead to implement HTTP + * request handling. + */ + var XHR = Class.extend(/** @lends LuCI.XHR.prototype */ { __name__: 'LuCI.XHR', __init__: function() { if (window.console && console.debug) @@ -1386,19 +2932,111 @@ delete this.active; }, + /** + * This function is a legacy wrapper around + * {@link LuCI#get LuCI.get()}. + * + * @instance + * @deprecated + * @memberof LuCI.XHR + * + * @param {string} url + * The URL to request + * + * @param {Object} [data] + * Additional query string data + * + * @param {LuCI.requestCallbackFn} [callback] + * Callback function to invoke on completion + * + * @param {number} [timeout] + * Request timeout to use + * + * @return {Promise<null>} + */ get: function(url, data, callback, timeout) { this.active = true; L.get(url, data, this._response.bind(this, callback), timeout); }, + /** + * This function is a legacy wrapper around + * {@link LuCI#post LuCI.post()}. + * + * @instance + * @deprecated + * @memberof LuCI.XHR + * + * @param {string} url + * The URL to request + * + * @param {Object} [data] + * Additional data to append to the request body. + * + * @param {LuCI.requestCallbackFn} [callback] + * Callback function to invoke on completion + * + * @param {number} [timeout] + * Request timeout to use + * + * @return {Promise<null>} + */ post: function(url, data, callback, timeout) { this.active = true; L.post(url, data, this._response.bind(this, callback), timeout); }, + /** + * Cancels a running request. + * + * This function does not actually cancel the underlying + * `XMLHTTPRequest` request but it sets a flag which prevents the + * invocation of the callback function when the request eventually + * finishes or timed out. + * + * @instance + * @deprecated + * @memberof LuCI.XHR + */ cancel: function() { delete this.active }, + + /** + * Checks the running state of the request. + * + * @instance + * @deprecated + * @memberof LuCI.XHR + * + * @returns {boolean} + * Returns `true` if the request is still running or `false` if it + * already completed. + */ busy: function() { return (this.active === true) }, + + /** + * Ignored for backwards compatibility. + * + * This function does nothing. + * + * @instance + * @deprecated + * @memberof LuCI.XHR + */ abort: function() {}, + + /** + * Existing for backwards compatibility. + * + * This function simply throws an `InternalError` when invoked. + * + * @instance + * @deprecated + * @memberof LuCI.XHR + * + * @throws {InternalError} + * Throws an `InternalError` with the message `Not implemented` + * when invoked. + */ send_form: function() { L.error('InternalError', 'Not implemented') }, }); diff --git a/modules/luci-base/htdocs/luci-static/resources/network.js b/modules/luci-base/htdocs/luci-static/resources/network.js index a9e65dac5..a71c97c30 100644 --- a/modules/luci-base/htdocs/luci-static/resources/network.js +++ b/modules/luci-base/htdocs/luci-static/resources/network.js @@ -700,16 +700,140 @@ function enumerateNetworks() { var Hosts, Network, Protocol, Device, WifiDevice, WifiNetwork; -Network = L.Class.extend({ +/** + * @class + * @memberof LuCI + * @hideconstructor + * @classdesc + * + * The `LuCI.Network` class combines data from multiple `ubus` apis to + * provide an abstraction of the current network configuration state. + * + * It provides methods to enumerate interfaces and devices, to query + * current configuration details and to manipulate settings. + */ +Network = L.Class.extend(/** @lends LuCI.Network.prototype */ { + /** + * Converts the given prefix size in bits to a netmask. + * + * @method + * + * @param {number} bits + * The prefix size in bits. + * + * @param {boolean} [v6=false] + * Whether to convert the bits value into an IPv4 netmask (`false`) or + * an IPv6 netmask (`true`). + * + * @returns {null|string} + * Returns a string containing the netmask corresponding to the bit count + * or `null` when the given amount of bits exceeds the maximum possible + * value of `32` for IPv4 or `128` for IPv6. + */ prefixToMask: prefixToMask, + + /** + * Converts the given netmask to a prefix size in bits. + * + * @method + * + * @param {string} netmask + * The netmask to convert into a bit count. + * + * @param {boolean} [v6=false] + * Whether to parse the given netmask as IPv4 (`false`) or IPv6 (`true`) + * address. + * + * @returns {null|number} + * Returns the number of prefix bits contained in the netmask or `null` + * if the given netmask value was invalid. + */ maskToPrefix: maskToPrefix, + + /** + * An encryption entry describes active wireless encryption settings + * such as the used key management protocols, active ciphers and + * protocol versions. + * + * @typedef {Object<string, boolean|Array<number|string>>} LuCI.Network.WifiEncryption + * @memberof LuCI.Network + * + * @property {boolean} enabled + * Specifies whether any kind of encryption, such as `WEP` or `WPA` is + * enabled. If set to `false`, then no encryption is active and the + * corresponding network is open. + * + * @property {string[]} [wep] + * When the `wep` property exists, the network uses WEP encryption. + * In this case, the property is set to an array of active WEP modes + * which might be either `open`, `shared` or both. + * + * @property {number[]} [wpa] + * When the `wpa` property exists, the network uses WPA security. + * In this case, the property is set to an array containing the WPA + * protocol versions used, e.g. `[ 1, 2 ]` for WPA/WPA2 mixed mode or + * `[ 3 ]` for WPA3-SAE. + * + * @property {string[]} [authentication] + * The `authentication` property only applies to WPA encryption and + * is defined when the `wpa` property is set as well. It points to + * an array of active authentication suites used by the network, e.g. + * `[ "psk" ]` for a WPA(2)-PSK network or `[ "psk", "sae" ]` for + * mixed WPA2-PSK/WPA3-SAE encryption. + * + * @property {string[]} [ciphers] + * If either WEP or WPA encryption is active, then the `ciphers` + * property will be set to an array describing the active encryption + * ciphers used by the network, e.g. `[ "tkip", "ccmp" ]` for a + * WPA/WPA2-PSK mixed network or `[ "wep-40", "wep-104" ]` for an + * WEP network. + */ + + /** + * Converts a given {@link LuCI.Network.WifiEncryption encryption entry} + * into a human readable string such as `mixed WPA/WPA2 PSK (TKIP, CCMP)` + * or `WPA3 SAE (CCMP)`. + * + * @method + * + * @param {LuCI.Network.WifiEncryption} encryption + * The wireless encryption entry to convert. + * + * @returns {null|string} + * Returns the description string for the given encryption entry or + * `null` if the given entry was invalid. + */ formatWifiEncryption: formatWifiEncryption, + /** + * Flushes the local network state cache and fetches updated information + * from the remote `ubus` apis. + * + * @returns {Promise<Object>} + * Returns a promise resolving to the internal network state object. + */ flushCache: function() { initNetworkState(true); return _init; }, + /** + * Instantiates the given {@link LuCI.Network.Protocol Protocol} backend, + * optionally using the given network name. + * + * @param {string} protoname + * The protocol backend to use, e.g. `static` or `dhcp`. + * + * @param {string} [netname=__dummy__] + * The network name to use for the instantiated protocol. This should be + * usually set to one of the interfaces described in /etc/config/network + * but it is allowed to omit it, e.g. to query protocol capabilities + * without the need for an existing interface. + * + * @returns {null|LuCI.Network.Protocol} + * Returns the instantiated protocol backend class or `null` if the given + * protocol isn't known. + */ getProtocol: function(protoname, netname) { var v = _protocols[protoname]; if (v != null) @@ -718,6 +842,13 @@ Network = L.Class.extend({ return null; }, + /** + * Obtains instances of all known {@link LuCI.Network.Protocol Protocol} + * backend classes. + * + * @returns {Array<LuCI.Network.Protocol>} + * Returns an array of protocol class instances. + */ getProtocols: function() { var rv = []; @@ -727,6 +858,24 @@ Network = L.Class.extend({ return rv; }, + /** + * Registers a new {@link LuCI.Network.Protocol Protocol} subclass + * with the given methods and returns the resulting subclass value. + * + * This functions internally calls + * {@link LuCI.Class.extend Class.extend()} on the `Network.Protocol` + * base class. + * + * @param {string} protoname + * The name of the new protocol to register. + * + * @param {Object<string, *>} methods + * The member methods and values of the new `Protocol` subclass to + * be passed to {@link LuCI.Class.extend Class.extend()}. + * + * @returns {LuCI.Network.Protocol} + * Returns the new `Protocol` subclass. + */ registerProtocol: function(protoname, methods) { var spec = L.isObject(_protospecs) ? _protospecs[protoname] : null; var proto = Protocol.extend(Object.assign({ @@ -760,10 +909,34 @@ Network = L.Class.extend({ return proto; }, + /** + * Registers a new regular expression pattern to recognize + * virtual interfaces. + * + * @param {RegExp} pat + * A `RegExp` instance to match a virtual interface name + * such as `6in4-wan` or `tun0`. + */ registerPatternVirtual: function(pat) { iface_patterns_virtual.push(pat); }, + /** + * Registers a new human readable translation string for a `Protocol` + * error code. + * + * @param {string} code + * The `ubus` protocol error code to register a translation for, e.g. + * `NO_DEVICE`. + * + * @param {string} message + * The message to use as translation for the given protocol error code. + * + * @returns {boolean} + * Returns `true` if the error code description has been added or `false` + * if either the arguments were invalid or if there already was a + * description for the given code. + */ registerErrorCode: function(code, message) { if (typeof(code) == 'string' && typeof(message) == 'string' && @@ -775,6 +948,26 @@ Network = L.Class.extend({ return false; }, + /** + * Adds a new network of the given name and update it with the given + * uci option values. + * + * If a network with the given name already exist but is empty, then + * this function will update its option, otherwise it will do nothing. + * + * @param {string} name + * The name of the network to add. Must be in the format `[a-zA-Z0-9_]+`. + * + * @param {Object<string, string|string[]>} [options] + * An object of uci option values to set on the new network or to + * update in an existing, empty network. + * + * @returns {Promise<null|LuCI.Network.Protocol>} + * Returns a promise resolving to the `Protocol` subclass instance + * describing the added network or resolving to `null` if the name + * was invalid or if a non-empty network of the given name already + * existed. + */ addNetwork: function(name, options) { return this.getNetwork(name).then(L.bind(function(existingNetwork) { if (name != null && /^[a-zA-Z0-9_]+$/.test(name) && existingNetwork == null) { @@ -800,6 +993,18 @@ Network = L.Class.extend({ }, this)); }, + /** + * Get a {@link LuCI.Network.Protocol Protocol} instance describing + * the network with the given name. + * + * @param {string} name + * The logical interface name of the network get, e.g. `lan` or `wan`. + * + * @returns {Promise<null|LuCI.Network.Protocol>} + * Returns a promise resolving to a + * {@link LuCI.Network.Protocol Protocol} subclass instance describing + * the network or `null` if the network did not exist. + */ getNetwork: function(name) { return initNetworkState().then(L.bind(function() { var section = (name != null) ? uci.get('network', name) : null; @@ -817,10 +1022,30 @@ Network = L.Class.extend({ }, this)); }, + /** + * Gets an array containing all known networks. + * + * @returns {Promise<Array<LuCI.Network.Protocol>>} + * Returns a promise resolving to a name-sorted array of + * {@link LuCI.Network.Protocol Protocol} subclass instances + * describing all known networks. + */ getNetworks: function() { return initNetworkState().then(L.bind(enumerateNetworks, this)); }, + /** + * Deletes the given network and its references from the network and + * firewall configuration. + * + * @param {string} name + * The name of the network to delete. + * + * @returns {Promise<boolean>} + * Returns a promise resolving to either `true` if the network and + * references to it were successfully deleted from the configuration or + * `false` if the given network could not be found. + */ deleteNetwork: function(name) { var requireFirewall = Promise.resolve(L.require('firewall')).catch(function() {}); @@ -869,6 +1094,22 @@ Network = L.Class.extend({ }); }, + /** + * Rename the given network and its references to a new name. + * + * @param {string} oldName + * The current name of the network. + * + * @param {string} newName + * The name to rename the network to, must be in the format + * `[a-z-A-Z0-9_]+`. + * + * @returns {Promise<boolean>} + * Returns a promise resolving to either `true` if the network was + * successfully renamed or `false` if the new name was invalid, if + * a network with the new name already exists or if the network to + * rename could not be found. + */ renameNetwork: function(oldName, newName) { return initNetworkState().then(function() { if (newName == null || !/^[a-zA-Z0-9_]+$/.test(newName) || uci.get('network', newName) != null) @@ -918,6 +1159,18 @@ Network = L.Class.extend({ }); }, + /** + * Get a {@link LuCI.Network.Device Device} instance describing the + * given network device. + * + * @param {string} name + * The name of the network device to get, e.g. `eth0` or `br-lan`. + * + * @returns {Promise<null|LuCI.Network.Device>} + * Returns a promise resolving to the `Device` instance describing + * the network device or `null` if the given device name could not + * be found. + */ getDevice: function(name) { return initNetworkState().then(L.bind(function() { if (name == null) @@ -934,6 +1187,13 @@ Network = L.Class.extend({ }, this)); }, + /** + * Get a sorted list of all found network devices. + * + * @returns {Promise<Array<LuCI.Network.Device>>} + * Returns a promise resolving to a sorted array of `Device` class + * instances describing the network devices found on the system. + */ getDevices: function() { return initNetworkState().then(L.bind(function() { var devices = {}; @@ -1032,10 +1292,38 @@ Network = L.Class.extend({ }, this)); }, + /** + * Test if a given network device name is in the list of patterns for + * device names to ignore. + * + * Ignored device names are usually Linux network devices which are + * spawned implicitly by kernel modules such as `tunl0` or `hwsim0` + * and which are unsuitable for use in network configuration. + * + * @param {string} name + * The device name to test. + * + * @returns {boolean} + * Returns `true` if the given name is in the ignore pattern list, + * else returns `false`. + */ isIgnoredDevice: function(name) { return isIgnoredIfname(name); }, + /** + * Get a {@link LuCI.Network.WifiDevice WifiDevice} instance describing + * the given wireless radio. + * + * @param {string} devname + * The configuration name of the wireless radio to lookup, e.g. `radio0` + * for the first mac80211 phy on the system. + * + * @returns {Promise<null|LuCI.Network.WifiDevice>} + * Returns a promise resolving to the `WifiDevice` instance describing + * the underlying radio device or `null` if the wireless radio could not + * be found. + */ getWifiDevice: function(devname) { return initNetworkState().then(L.bind(function() { var existingDevice = uci.get('wireless', devname); @@ -1047,6 +1335,15 @@ Network = L.Class.extend({ }, this)); }, + /** + * Obtain a list of all configured radio devices. + * + * @returns {Promise<Array<LuCI.Network.WifiDevice>>} + * Returns a promise resolving to an array of `WifiDevice` instances + * describing the wireless radios configured in the system. + * The order of the array corresponds to the order of the radios in + * the configuration. + */ getWifiDevices: function() { return initNetworkState().then(L.bind(function() { var uciWifiDevices = uci.sections('wireless', 'wifi-device'), @@ -1061,6 +1358,21 @@ Network = L.Class.extend({ }, this)); }, + /** + * Get a {@link LuCI.Network.WifiNetwork WifiNetwork} instance describing + * the given wireless network. + * + * @param {string} netname + * The name of the wireless network to lookup. This may be either an uci + * configuration section ID, a network ID in the form `radio#.network#` + * or a Linux network device name like `wlan0` which is resolved to the + * corresponding configuration section through `ubus` runtime information. + * + * @returns {Promise<null|LuCI.Network.WifiNetwork>} + * Returns a promise resolving to the `WifiNetwork` instance describing + * the wireless network or `null` if the corresponding network could not + * be found. + */ getWifiNetwork: function(netname) { var sid, res, netid, radioname, radiostate, netstate; @@ -1110,6 +1422,20 @@ Network = L.Class.extend({ }, this)); }, + /** + * Adds a new wireless network to the configuration and sets its options + * to the provided values. + * + * @param {Object<string, string|string[]>} options + * The options to set for the newly added wireless network. This object + * must at least contain a `device` property which is set to the radio + * name the new network belongs to. + * + * @returns {Promise<null|LuCI.Network.WifiNetwork>} + * Returns a promise resolving to a `WifiNetwork` instance describing + * the newly added wireless network or `null` if the given options + * were invalid or if the associated radio device could not be found. + */ addWifiNetwork: function(options) { return initNetworkState().then(L.bind(function() { if (options == null || @@ -1121,6 +1447,7 @@ Network = L.Class.extend({ if (existingDevice == null || existingDevice['.type'] != 'wifi-device') return null; + /* XXX: need to add a named section (wifinet#) here */ var sid = uci.add('wireless', 'wifi-iface'); for (var key in options) if (options.hasOwnProperty(key)) @@ -1133,6 +1460,20 @@ Network = L.Class.extend({ }, this)); }, + /** + * Deletes the given wireless network from the configuration. + * + * @param {string} netname + * The name of the network to remove. This may be either a + * network ID in the form `radio#.network#` or a Linux network device + * name like `wlan0` which is resolved to the corresponding configuration + * section through `ubus` runtime information. + * + * @returns {Promise<boolean>} + * Returns a promise resolving to `true` if the wireless network has been + * successfully deleted from the configuration or `false` if it could not + * be found. + */ deleteWifiNetwork: function(netname) { return initNetworkState().then(L.bind(function() { var sid = getWifiSidByIfname(netname); @@ -1145,6 +1486,7 @@ Network = L.Class.extend({ }, this)); }, + /* private */ getStatusByRoute: function(addr, mask) { return initNetworkState().then(L.bind(function() { var rv = []; @@ -1174,6 +1516,7 @@ Network = L.Class.extend({ }, this)); }, + /* private */ getStatusByAddress: function(addr) { return initNetworkState().then(L.bind(function() { var rv = []; @@ -1203,6 +1546,16 @@ Network = L.Class.extend({ }, this)); }, + /** + * Get IPv4 wan networks. + * + * This function looks up all networks having a default `0.0.0.0/0` route + * and returns them as array. + * + * @returns {Promise<Array<LuCI.Network.Protocol>>} + * Returns a promise resolving to an array of `Protocol` subclass + * instances describing the found default route interfaces. + */ getWANNetworks: function() { return this.getStatusByRoute('0.0.0.0', 0).then(L.bind(function(statuses) { var rv = []; @@ -1214,6 +1567,16 @@ Network = L.Class.extend({ }, this)); }, + /** + * Get IPv6 wan networks. + * + * This function looks up all networks having a default `::/0` route + * and returns them as array. + * + * @returns {Promise<Array<LuCI.Network.Protocol>>} + * Returns a promise resolving to an array of `Protocol` subclass + * instances describing the found IPv6 default route interfaces. + */ getWAN6Networks: function() { return this.getStatusByRoute('::', 0).then(L.bind(function(statuses) { var rv = []; @@ -1225,12 +1588,47 @@ Network = L.Class.extend({ }, this)); }, + /** + * Describes an swconfig switch topology by specifying the CPU + * connections and external port labels of a switch. + * + * @typedef {Object<string, Object|Array>} SwitchTopology + * @memberof LuCI.Network + * + * @property {Object<number, string>} netdevs + * The `netdevs` property points to an object describing the CPU port + * connections of the switch. The numeric key of the enclosed object is + * the port number, the value contains the Linux network device name the + * port is hardwired to. + * + * @property {Array<Object<string, boolean|number|string>>} ports + * The `ports` property points to an array describing the populated + * ports of the switch in the external label order. Each array item is + * an object containg the following keys: + * - `num` - the internal switch port number + * - `label` - the label of the port, e.g. `LAN 1` or `CPU (eth0)` + * - `device` - the connected Linux network device name (CPU ports only) + * - `tagged` - a boolean indicating whether the port must be tagged to + * function (CPU ports only) + */ + + /** + * Returns the topologies of all swconfig switches found on the system. + * + * @returns {Promise<Object<string, LuCI.Network.SwitchTopology>>} + * Returns a promise resolving to an object containing the topologies + * of each switch. The object keys correspond to the name of the switches + * such as `switch0`, the values are + * {@link LuCI.Network.SwitchTopology SwitchTopology} objects describing + * the layout. + */ getSwitchTopologies: function() { return initNetworkState().then(function() { return _state.switches; }); }, + /* private */ instantiateNetwork: function(name, proto) { if (name == null) return null; @@ -1241,6 +1639,7 @@ Network = L.Class.extend({ return new protoClass(name); }, + /* private */ instantiateDevice: function(name, network, extend) { if (extend != null) return new (Device.extend(extend))(name, network); @@ -1248,24 +1647,54 @@ Network = L.Class.extend({ return new Device(name, network); }, + /* private */ instantiateWifiDevice: function(radioname, radiostate) { return new WifiDevice(radioname, radiostate); }, + /* private */ instantiateWifiNetwork: function(sid, radioname, radiostate, netid, netstate) { return new WifiNetwork(sid, radioname, radiostate, netid, netstate); }, + /** + * Obtains the the network device name of the given object. + * + * @param {LuCI.Network.Protocol|LuCI.Network.Device|LuCI.Network.WifiDevice|LuCI.Network.WifiNetwork|string} obj + * The object to get the device name from. + * + * @returns {null|string} + * Returns a string containing the device name or `null` if the given + * object could not be converted to a name. + */ getIfnameOf: function(obj) { return ifnameOf(obj); }, + /** + * Queries the internal DSL modem type from board information. + * + * @returns {Promise<null|string>} + * Returns a promise resolving to the type of the internal modem + * (e.g. `vdsl`) or to `null` if no internal modem is present. + */ getDSLModemType: function() { return initNetworkState().then(function() { return _state.hasDSLModem ? _state.hasDSLModem.type : null; }); }, + /** + * Queries aggregated information about known hosts. + * + * This function aggregates information from various sources such as + * DHCP lease databases, ARP and IPv6 neighbour entries, wireless + * association list etc. and returns a {@link LuCI.Network.Hosts Hosts} + * class instance describing the found hosts. + * + * @returns {Promise<LuCI.Network.Hosts>} + * Returns a `Hosts` instance describing host known on the system. + */ getHostHints: function() { return initNetworkState().then(function() { return new Hosts(_state.hosts); @@ -1273,23 +1702,77 @@ Network = L.Class.extend({ } }); -Hosts = L.Class.extend({ +/** + * @class + * @memberof LuCI.Network + * @hideconstructor + * @classdesc + * + * The `LuCI.Network.Hosts` class encapsulates host information aggregated + * from multiple sources and provides convenience functions to access the + * host information by different criteria. + */ +Hosts = L.Class.extend(/** @lends LuCI.Network.Hosts.prototype */ { __init__: function(hosts) { this.hosts = hosts; }, + /** + * Lookup the hostname associated with the given MAC address. + * + * @param {string} mac + * The MAC address to lookup. + * + * @returns {null|string} + * Returns the hostname associated with the given MAC or `null` if + * no matching host could be found or if no hostname is known for + * the corresponding host. + */ getHostnameByMACAddr: function(mac) { return this.hosts[mac] ? this.hosts[mac].name : null; }, + /** + * Lookup the IPv4 address associated with the given MAC address. + * + * @param {string} mac + * The MAC address to lookup. + * + * @returns {null|string} + * Returns the IPv4 address associated with the given MAC or `null` if + * no matching host could be found or if no IPv4 address is known for + * the corresponding host. + */ getIPAddrByMACAddr: function(mac) { return this.hosts[mac] ? this.hosts[mac].ipv4 : null; }, + /** + * Lookup the IPv6 address associated with the given MAC address. + * + * @param {string} mac + * The MAC address to lookup. + * + * @returns {null|string} + * Returns the IPv6 address associated with the given MAC or `null` if + * no matching host could be found or if no IPv6 address is known for + * the corresponding host. + */ getIP6AddrByMACAddr: function(mac) { return this.hosts[mac] ? this.hosts[mac].ipv6 : null; }, + /** + * Lookup the hostname associated with the given IPv4 address. + * + * @param {string} ipaddr + * The IPv4 address to lookup. + * + * @returns {null|string} + * Returns the hostname associated with the given IPv4 or `null` if + * no matching host could be found or if no hostname is known for + * the corresponding host. + */ getHostnameByIPAddr: function(ipaddr) { for (var mac in this.hosts) if (this.hosts[mac].ipv4 == ipaddr && this.hosts[mac].name != null) @@ -1297,6 +1780,17 @@ Hosts = L.Class.extend({ return null; }, + /** + * Lookup the MAC address associated with the given IPv4 address. + * + * @param {string} ipaddr + * The IPv4 address to lookup. + * + * @returns {null|string} + * Returns the MAC address associated with the given IPv4 or `null` if + * no matching host could be found or if no MAC address is known for + * the corresponding host. + */ getMACAddrByIPAddr: function(ipaddr) { for (var mac in this.hosts) if (this.hosts[mac].ipv4 == ipaddr) @@ -1304,6 +1798,17 @@ Hosts = L.Class.extend({ return null; }, + /** + * Lookup the hostname associated with the given IPv6 address. + * + * @param {string} ipaddr + * The IPv6 address to lookup. + * + * @returns {null|string} + * Returns the hostname associated with the given IPv6 or `null` if + * no matching host could be found or if no hostname is known for + * the corresponding host. + */ getHostnameByIP6Addr: function(ip6addr) { for (var mac in this.hosts) if (this.hosts[mac].ipv6 == ip6addr && this.hosts[mac].name != null) @@ -1311,6 +1816,17 @@ Hosts = L.Class.extend({ return null; }, + /** + * Lookup the MAC address associated with the given IPv6 address. + * + * @param {string} ipaddr + * The IPv6 address to lookup. + * + * @returns {null|string} + * Returns the MAC address associated with the given IPv6 or `null` if + * no matching host could be found or if no MAC address is known for + * the corresponding host. + */ getMACAddrByIP6Addr: function(ip6addr) { for (var mac in this.hosts) if (this.hosts[mac].ipv6 == ip6addr) @@ -1318,6 +1834,27 @@ Hosts = L.Class.extend({ return null; }, + /** + * Return an array of (MAC address, name hint) tuples sorted by + * MAC address. + * + * @param {boolean} [preferIp6=false] + * Whether to prefer IPv6 addresses (`true`) or IPv4 addresses (`false`) + * as name hint when no hostname is known for a specific MAC address. + * + * @returns {Array<Array<string>>} + * Returns an array of arrays containing a name hint for each found + * MAC address on the system. The array is sorted ascending by MAC. + * + * Each item of the resulting array is a two element array with the + * MAC being the first element and the name hint being the second + * element. The name hint is either the hostname, an IPv4 or an IPv6 + * address related to the MAC address. + * + * If no hostname but both IPv4 and IPv6 addresses are known, the + * `preferIP6` flag specifies whether the IPv6 or the IPv4 address + * is used as hint. + */ getMACHints: function(preferIp6) { var rv = []; for (var mac in this.hosts) { @@ -1331,7 +1868,17 @@ Hosts = L.Class.extend({ } }); -Protocol = L.Class.extend({ +/** + * @class + * @memberof LuCI.Network + * @hideconstructor + * @classdesc + * + * The `Network.Protocol` class serves as base for protocol specific + * subclasses which describe logical UCI networks defined by `config + * interface` sections in `/etc/config/network`. + */ +Protocol = L.Class.extend(/** @lends LuCI.Network.Protocol.prototype */ { __init__: function(name) { this.sid = name; }, @@ -1354,14 +1901,41 @@ Protocol = L.Class.extend({ } }, + /** + * Read the given UCI option value of this network. + * + * @param {string} opt + * The UCI option name to read. + * + * @returns {null|string|string[]} + * Returns the UCI option value or `null` if the requested option is + * not found. + */ get: function(opt) { return uci.get('network', this.sid, opt); }, + /** + * Set the given UCI option of this network to the given value. + * + * @param {string} opt + * The name of the UCI option to set. + * + * @param {null|string|string[]} val + * The value to set or `null` to remove the given option from the + * configuration. + */ set: function(opt, val) { return uci.set('network', this.sid, opt, val); }, + /** + * Get the associared Linux network device of this network. + * + * @returns {null|string} + * Returns the name of the associated network device or `null` if + * it could not be determined. + */ getIfname: function() { var ifname; @@ -1377,10 +1951,31 @@ Protocol = L.Class.extend({ return (res != null ? res[0] : null); }, + /** + * Get the name of this network protocol class. + * + * This function will be overwritten by subclasses created by + * {@link LuCI.Network#registerProtocol Network.registerProtocol()}. + * + * @abstract + * @returns {string} + * Returns the name of the network protocol implementation, e.g. + * `static` or `dhcp`. + */ getProtocol: function() { return null; }, + /** + * Return a human readable description for the protcol, such as + * `Static address` or `DHCP client`. + * + * This function should be overwritten by subclasses. + * + * @abstract + * @returns {string} + * Returns the description string. + */ getI18n: function() { switch (this.getProtocol()) { case 'none': return _('Unmanaged'); @@ -1390,18 +1985,52 @@ Protocol = L.Class.extend({ } }, + /** + * Get the type of the underlying interface. + * + * This function actually is a convenience wrapper around + * `proto.get("type")` and is mainly used by other `LuCI.Network` code + * to check whether the interface is declared as bridge in UCI. + * + * @returns {null|string} + * Returns the value of the `type` option of the associated logical + * interface or `null` if no `type` option is set. + */ getType: function() { return this._get('type'); }, + /** + * Get the name of the associated logical interface. + * + * @returns {string} + * Returns the logical interface name, such as `lan` or `wan`. + */ getName: function() { return this.sid; }, + /** + * Get the uptime of the logical interface. + * + * @returns {number} + * Returns the uptime of the associated interface in seconds. + */ getUptime: function() { return this._ubus('uptime') || 0; }, + /** + * Get the logical interface expiry time in seconds. + * + * For protocols that have a concept of a lease, such as DHCP or + * DHCPv6, this function returns the remaining time in seconds + * until the lease expires. + * + * @returns {number} + * Returns the amount of seconds until the lease expires or `-1` + * if it isn't applicable to the associated protocol. + */ getExpiry: function() { var u = this._ubus('uptime'), d = this._ubus('data'); @@ -1415,10 +2044,29 @@ Protocol = L.Class.extend({ return -1; }, + /** + * Get the metric value of the logical interface. + * + * @returns {number} + * Returns the current metric value used for device and network + * routes spawned by the associated logical interface. + */ getMetric: function() { return this._ubus('metric') || 0; }, + /** + * Get the requested firewall zone name of the logical interface. + * + * Some protocol implementations request a specific firewall zone + * to trigger inclusion of their resulting network devices into the + * firewall rule set. + * + * @returns {null|string} + * Returns the requested firewall zone name as published in the + * `ubus` runtime information or `null` if the remote protocol + * handler didn't request a zone. + */ getZoneName: function() { var d = this._ubus('data'); @@ -1428,11 +2076,26 @@ Protocol = L.Class.extend({ return null; }, + /** + * Query the first (primary) IPv4 address of the logical interface. + * + * @returns {null|string} + * Returns the primary IPv4 address registered by the protocol handler + * or `null` if no IPv4 addresses were set. + */ getIPAddr: function() { var addrs = this._ubus('ipv4-address'); return ((Array.isArray(addrs) && addrs.length) ? addrs[0].address : null); }, + /** + * Query all IPv4 addresses of the logical interface. + * + * @returns {string[]} + * Returns an array of IPv4 addresses in CIDR notation which have been + * registered by the protocol handler. The order of the resulting array + * follows the order of the addresses in `ubus` runtime information. + */ getIPAddrs: function() { var addrs = this._ubus('ipv4-address'), rv = []; @@ -1444,12 +2107,27 @@ Protocol = L.Class.extend({ return rv; }, + /** + * Query the first (primary) IPv4 netmask of the logical interface. + * + * @returns {null|string} + * Returns the netmask of the primary IPv4 address registered by the + * protocol handler or `null` if no IPv4 addresses were set. + */ getNetmask: function() { var addrs = this._ubus('ipv4-address'); if (Array.isArray(addrs) && addrs.length) return prefixToMask(addrs[0].mask, false); }, + /** + * Query the gateway (nexthop) of the default route associated with + * this logical interface. + * + * @returns {string} + * Returns a string containing the IPv4 nexthop address of the associated + * default route or `null` if no default route was found. + */ getGatewayAddr: function() { var routes = this._ubus('route'); @@ -1463,6 +2141,13 @@ Protocol = L.Class.extend({ return null; }, + /** + * Query the IPv4 DNS servers associated with the logical interface. + * + * @returns {string[]} + * Returns an array of IPv4 DNS servers registered by the remote + * protocol backend. + */ getDNSAddrs: function() { var addrs = this._ubus('dns-server'), rv = []; @@ -1475,6 +2160,13 @@ Protocol = L.Class.extend({ return rv; }, + /** + * Query the first (primary) IPv6 address of the logical interface. + * + * @returns {null|string} + * Returns the primary IPv6 address registered by the protocol handler + * in CIDR notation or `null` if no IPv6 addresses were set. + */ getIP6Addr: function() { var addrs = this._ubus('ipv6-address'); @@ -1489,6 +2181,14 @@ Protocol = L.Class.extend({ return null; }, + /** + * Query all IPv6 addresses of the logical interface. + * + * @returns {string[]} + * Returns an array of IPv6 addresses in CIDR notation which have been + * registered by the protocol handler. The order of the resulting array + * follows the order of the addresses in `ubus` runtime information. + */ getIP6Addrs: function() { var addrs = this._ubus('ipv6-address'), rv = []; @@ -1508,6 +2208,13 @@ Protocol = L.Class.extend({ return rv; }, + /** + * Query the IPv6 DNS servers associated with the logical interface. + * + * @returns {string[]} + * Returns an array of IPv6 DNS servers registered by the remote + * protocol backend. + */ getDNS6Addrs: function() { var addrs = this._ubus('dns-server'), rv = []; @@ -1520,6 +2227,13 @@ Protocol = L.Class.extend({ return rv; }, + /** + * Query the routed IPv6 prefix associated with the logical interface. + * + * @returns {null|string} + * Returns the routed IPv6 prefix registered by the remote protocol + * handler or `null` if no prefix is present. + */ getIP6Prefix: function() { var prefixes = this._ubus('ipv6-prefix'); @@ -1529,6 +2243,22 @@ Protocol = L.Class.extend({ return null; }, + /** + * Query interface error messages published in `ubus` runtime state. + * + * Interface errors are emitted by remote protocol handlers if the setup + * of the underlying logical interface failed, e.g. due to bad + * configuration or network connectivity issues. + * + * This function will translate the found error codes to human readable + * messages using the descriptions registered by + * {@link LuCI.Network#registerErrorCode Network.registerErrorCode()} + * and fall back to `"Unknown error (%s)"` where `%s` is replaced by the + * error code in case no translation can be found. + * + * @returns {string[]} + * Returns an array of translated interface error messages. + */ getErrors: function() { var errors = this._ubus('errors'), rv = null; @@ -1546,30 +2276,117 @@ Protocol = L.Class.extend({ return rv; }, + /** + * Checks whether the underlying logical interface is declared as bridge. + * + * @returns {boolean} + * Returns `true` when the interface is declared with `option type bridge` + * and when the associated protocol implementation is not marked virtual + * or `false` when the logical interface is no bridge. + */ isBridge: function() { return (!this.isVirtual() && this.getType() == 'bridge'); }, + /** + * Get the name of the opkg package providing the protocol functionality. + * + * This function should be overwritten by protocol specific subclasses. + * + * @abstract + * + * @returns {string} + * Returns the name of the opkg package required for the protocol to + * function, e.g. `odhcp6c` for the `dhcpv6` prototocol. + */ getOpkgPackage: function() { return null; }, + /** + * Checks whether the protocol functionality is installed. + * + * This function exists for compatibility with old code, it always + * returns `true`. + * + * @deprecated + * @abstract + * + * @returns {boolean} + * Returns `true` if the protocol support is installed, else `false`. + */ isInstalled: function() { return true; }, + /** + * Checks whether this protocol is "virtual". + * + * A "virtual" protocol is a protocol which spawns its own interfaces + * on demand instead of using existing physical interfaces. + * + * Examples for virtual protocols are `6in4` which `gre` spawn tunnel + * network device on startup, examples for non-virtual protcols are + * `dhcp` or `static` which apply IP configuration to existing interfaces. + * + * This function should be overwritten by subclasses. + * + * @returns {boolean} + * Returns a boolean indicating whether the underlying protocol spawns + * dynamic interfaces (`true`) or not (`false`). + */ isVirtual: function() { return false; }, + /** + * Checks whether this protocol is "floating". + * + * A "floating" protocol is a protocol which spawns its own interfaces + * on demand, like a virtual one but which relies on an existinf lower + * level interface to initiate the connection. + * + * An example for such a protocol is "pppoe". + * + * This function exists for backwards compatibility with older code + * but should not be used anymore. + * + * @deprecated + * @returns {boolean} + * Returns a boolean indicating whether this protocol is floating (`true`) + * or not (`false`). + */ isFloating: function() { return false; }, + /** + * Checks whether this logical interface is dynamic. + * + * A dynamic interface is an interface which has been created at runtime, + * e.g. as sub-interface of another interface, but which is not backed by + * any user configuration. Such dynamic interfaces cannot be edited but + * only brought down or restarted. + * + * @returns {boolean} + * Returns a boolean indicating whether this interface is dynamic (`true`) + * or not (`false`). + */ isDynamic: function() { return (this._ubus('dynamic') == true); }, + /** + * Checks whether this interface is an alias interface. + * + * Alias interfaces are interfaces layering on top of another interface + * and are denoted by a special `@interfacename` notation in the + * underlying `ifname` option. + * + * @returns {null|string} + * Returns the name of the parent interface if this logical interface + * is an alias or `null` if it is not an alias interface. + */ isAlias: function() { var ifnames = L.toArray(uci.get('network', this.sid, 'ifname')), parent = null; @@ -1583,6 +2400,13 @@ Protocol = L.Class.extend({ return parent; }, + /** + * Checks whether this logical interface is "empty", meaning that ut + * has no network devices attached. + * + * @returns {boolean} + * Returns `true` if this logical interface is empty, else `false`. + */ isEmpty: function() { if (this.isFloating()) return false; @@ -1599,10 +2423,29 @@ Protocol = L.Class.extend({ return empty; }, + /** + * Checks whether this logical interface is configured and running. + * + * @returns {boolean} + * Returns `true` when the interface is active or `false` when it is not. + */ isUp: function() { return (this._ubus('up') == true); }, + /** + * Add the given network device to the logical interface. + * + * @param {LuCI.Network.Protocol|LuCI.Network.Device|LuCI.Network.WifiDevice|LuCI.Network.WifiNetwork|string} device + * The object or device name to add to the logical interface. In case the + * given argument is not a string, it is resolved though the + * {@link LuCI.Network#getIfnameOf Network.getIfnameOf()} function. + * + * @returns {boolean} + * Returns `true` if the device name has been added or `false` if any + * argument was invalid, if the device was already part of the logical + * interface or if the logical interface is virtual. + */ addDevice: function(ifname) { ifname = ifnameOf(ifname); @@ -1617,6 +2460,19 @@ Protocol = L.Class.extend({ return appendValue('network', this.sid, 'ifname', ifname); }, + /** + * Remove the given network device from the logical interface. + * + * @param {LuCI.Network.Protocol|LuCI.Network.Device|LuCI.Network.WifiDevice|LuCI.Network.WifiNetwork|string} device + * The object or device name to remove from the logical interface. In case + * the given argument is not a string, it is resolved though the + * {@link LuCI.Network#getIfnameOf Network.getIfnameOf()} function. + * + * @returns {boolean} + * Returns `true` if the device name has been added or `false` if any + * argument was invalid, if the device was already part of the logical + * interface or if the logical interface is virtual. + */ deleteDevice: function(ifname) { var rv = false; @@ -1636,6 +2492,14 @@ Protocol = L.Class.extend({ return rv; }, + /** + * Returns the Linux network device associated with this logical + * interface. + * + * @returns {LuCI.Network.Device} + * Returns a `Network.Device` class instance representing the + * expected Linux network device according to the configuration. + */ getDevice: function() { if (this.isVirtual()) { var ifname = '%s-%s'.format(this.getProtocol(), this.sid); @@ -1661,16 +2525,42 @@ Protocol = L.Class.extend({ } }, + /** + * Returns the layer 2 linux network device currently associated + * with this logical interface. + * + * @returns {LuCI.Network.Device} + * Returns a `Network.Device` class instance representing the Linux + * network device currently associated with the logical interface. + */ getL2Device: function() { var ifname = this._ubus('device'); return (ifname != null ? L.network.instantiateDevice(ifname, this) : null); }, + /** + * Returns the layer 3 linux network device currently associated + * with this logical interface. + * + * @returns {LuCI.Network.Device} + * Returns a `Network.Device` class instance representing the Linux + * network device currently associated with the logical interface. + */ getL3Device: function() { var ifname = this._ubus('l3_device'); return (ifname != null ? L.network.instantiateDevice(ifname, this) : null); }, + /** + * Returns a list of network sub-devices associated with this logical + * interface. + * + * @returns {null|Array<LuCI.Network.Device>} + * Returns an array of of `Network.Device` class instances representing + * the sub-devices attached to this logical interface or `null` if the + * logical interface does not support sub-devices, e.g. because it is + * virtual and not a bridge. + */ getDevices: function() { var rv = []; @@ -1712,6 +2602,19 @@ Protocol = L.Class.extend({ return rv; }, + /** + * Checks whether this logical interface contains the given device + * object. + * + * @param {LuCI.Network.Protocol|LuCI.Network.Device|LuCI.Network.WifiDevice|LuCI.Network.WifiNetwork|string} device + * The object or device name to check. In case the given argument is not + * a string, it is resolved though the + * {@link LuCI.Network#getIfnameOf Network.getIfnameOf()} function. + * + * @returns {boolean} + * Returns `true` when this logical interface contains the given network + * device or `false` if not. + */ containsDevice: function(ifname) { ifname = ifnameOf(ifname); @@ -1744,7 +2647,16 @@ Protocol = L.Class.extend({ } }); -Device = L.Class.extend({ +/** + * @class + * @memberof LuCI.Network + * @hideconstructor + * @classdesc + * + * A `Network.Device` class instance represents an underlying Linux network + * device and allows querying device details such as packet statistics or MTU. + */ +Device = L.Class.extend(/** @lends LuCI.Network.Device.prototype */ { __init__: function(ifname, network) { var wif = getWifiSidByIfname(ifname); @@ -1773,29 +2685,73 @@ Device = L.Class.extend({ return rv; }, + /** + * Get the name of the network device. + * + * @returns {string} + * Returns the name of the device, e.g. `eth0` or `wlan0`. + */ getName: function() { return (this.wif != null ? this.wif.getIfname() : this.ifname); }, + /** + * Get the MAC address of the device. + * + * @returns {null|string} + * Returns the MAC address of the device or `null` if not applicable, + * e.g. for non-ethernet tunnel devices. + */ getMAC: function() { var mac = this._devstate('macaddr'); return mac ? mac.toUpperCase() : null; }, + /** + * Get the MTU of the device. + * + * @returns {number} + * Returns the MTU of the device. + */ getMTU: function() { return this._devstate('mtu'); }, + /** + * Get the IPv4 addresses configured on the device. + * + * @returns {string[]} + * Returns an array of IPv4 address strings. + */ getIPAddrs: function() { var addrs = this._devstate('ipaddrs'); return (Array.isArray(addrs) ? addrs : []); }, + /** + * Get the IPv6 addresses configured on the device. + * + * @returns {string[]} + * Returns an array of IPv6 address strings. + */ getIP6Addrs: function() { var addrs = this._devstate('ip6addrs'); return (Array.isArray(addrs) ? addrs : []); }, + /** + * Get the type of the device.. + * + * @returns {string} + * Returns a string describing the type of the network device: + * - `alias` if it is an abstract alias device (`@` notation) + * - `wifi` if it is a wireless interface (e.g. `wlan0`) + * - `bridge` if it is a bridge device (e.g. `br-lan`) + * - `tunnel` if it is a tun or tap device (e.g. `tun0`) + * - `vlan` if it is a vlan device (e.g. `eth0.1`) + * - `switch` if it is a switch device (e.g.`eth1` connected to switch0) + * - `ethernet` for all other device types + */ getType: function() { if (this.ifname != null && this.ifname.charAt(0) == '@') return 'alias'; @@ -1813,6 +2769,13 @@ Device = L.Class.extend({ return 'ethernet'; }, + /** + * Get a short description string for the device. + * + * @returns {string} + * Returns the device name for non-wifi devices or a string containing + * the operation mode and SSID for wifi devices. + */ getShortName: function() { if (this.wif != null) return this.wif.getShortName(); @@ -1820,6 +2783,13 @@ Device = L.Class.extend({ return this.ifname; }, + /** + * Get a long description string for the device. + * + * @returns {string} + * Returns a string containing the type description and device name + * for non-wifi devices or operation mode and ssid for wifi ones. + */ getI18n: function() { if (this.wif != null) { return '%s: %s "%s"'.format( @@ -1831,6 +2801,13 @@ Device = L.Class.extend({ return '%s: "%s"'.format(this.getTypeI18n(), this.getName()); }, + /** + * Get a string describing the device type. + * + * @returns {string} + * Returns a string describing the type, e.g. "Wireless Adapter" or + * "Bridge". + */ getTypeI18n: function() { switch (this.getType()) { case 'alias': @@ -1856,6 +2833,14 @@ Device = L.Class.extend({ } }, + /** + * Get the associated bridge ports of the device. + * + * @returns {null|Array<LuCI.Network.Device>} + * Returns an array of `Network.Device` instances representing the ports + * (slave interfaces) of the bridge or `null` when this device isn't + * a Linux bridge. + */ getPorts: function() { var br = _state.bridges[this.ifname], rv = []; @@ -1871,16 +2856,37 @@ Device = L.Class.extend({ return rv; }, + /** + * Get the bridge ID + * + * @returns {null|string} + * Returns the ID of this network bridge or `null` if this network + * device is not a Linux bridge. + */ getBridgeID: function() { var br = _state.bridges[this.ifname]; return (br != null ? br.id : null); }, + /** + * Get the bridge STP setting + * + * @returns {boolean} + * Returns `true` when this device is a Linux bridge and has `stp` + * enabled, else `false`. + */ getBridgeSTP: function() { var br = _state.bridges[this.ifname]; return (br != null ? !!br.stp : false); }, + /** + * Checks whether this device is up. + * + * @returns {boolean} + * Returns `true` when the associated device is running pr `false` + * when it is down or absent. + */ isUp: function() { var up = this._devstate('flags', 'up'); @@ -1890,38 +2896,91 @@ Device = L.Class.extend({ return up; }, + /** + * Checks whether this device is a Linux bridge. + * + * @returns {boolean} + * Returns `true` when the network device is present and a Linux bridge, + * else `false`. + */ isBridge: function() { return (this.getType() == 'bridge'); }, + /** + * Checks whether this device is part of a Linux bridge. + * + * @returns {boolean} + * Returns `true` when this network device is part of a bridge, + * else `false`. + */ isBridgePort: function() { return (this._devstate('bridge') != null); }, + /** + * Get the amount of transmitted bytes. + * + * @returns {number} + * Returns the amount of bytes transmitted by the network device. + */ getTXBytes: function() { var stat = this._devstate('stats'); return (stat != null ? stat.tx_bytes || 0 : 0); }, + /** + * Get the amount of received bytes. + * + * @returns {number} + * Returns the amount of bytes received by the network device. + */ getRXBytes: function() { var stat = this._devstate('stats'); return (stat != null ? stat.rx_bytes || 0 : 0); }, + /** + * Get the amount of transmitted packets. + * + * @returns {number} + * Returns the amount of packets transmitted by the network device. + */ getTXPackets: function() { var stat = this._devstate('stats'); return (stat != null ? stat.tx_packets || 0 : 0); }, + /** + * Get the amount of received packets. + * + * @returns {number} + * Returns the amount of packets received by the network device. + */ getRXPackets: function() { var stat = this._devstate('stats'); return (stat != null ? stat.rx_packets || 0 : 0); }, + /** + * Get the primary logical interface this device is assigned to. + * + * @returns {null|LuCI.Network.Protocol} + * Returns a `Network.Protocol` instance representing the logical + * interface this device is attached to or `null` if it is not + * assigned to any logical interface. + */ getNetwork: function() { return this.getNetworks()[0]; }, + /** + * Get the logical interfaces this device is assigned to. + * + * @returns {Array<LuCI.Network.Protocol>} + * Returns an array of `Network.Protocol` instances representing the + * logical interfaces this device is assigned to. + */ getNetworks: function() { if (this.networks == null) { this.networks = []; @@ -1938,12 +2997,30 @@ Device = L.Class.extend({ return this.networks; }, + /** + * Get the related wireless network this device is related to. + * + * @returns {null|LuCI.Network.WifiNetwork} + * Returns a `Network.WifiNetwork` instance representing the wireless + * network corresponding to this network device or `null` if this device + * is not a wireless device. + */ getWifiNetwork: function() { return (this.wif != null ? this.wif : null); } }); -WifiDevice = L.Class.extend({ +/** + * @class + * @memberof LuCI.Network + * @hideconstructor + * @classdesc + * + * A `Network.WifiDevice` class instance represents a wireless radio device + * present on the system and provides wireless capability information as + * well as methods for enumerating related wireless networks. + */ +WifiDevice = L.Class.extend(/** @lends LuCI.Network.WifiDevice.prototype */ { __init__: function(name, radiostate) { var uciWifiDevice = uci.get('wireless', name); @@ -1960,6 +3037,7 @@ WifiDevice = L.Class.extend({ }; }, + /* private */ ubus: function(/* ... */) { var v = this._ubusdata; @@ -1972,32 +3050,105 @@ WifiDevice = L.Class.extend({ return v; }, + /** + * Read the given UCI option value of this wireless device. + * + * @param {string} opt + * The UCI option name to read. + * + * @returns {null|string|string[]} + * Returns the UCI option value or `null` if the requested option is + * not found. + */ get: function(opt) { return uci.get('wireless', this.sid, opt); }, + /** + * Set the given UCI option of this network to the given value. + * + * @param {string} opt + * The name of the UCI option to set. + * + * @param {null|string|string[]} val + * The value to set or `null` to remove the given option from the + * configuration. + */ set: function(opt, value) { return uci.set('wireless', this.sid, opt, value); }, + /** + * Checks whether this wireless radio is disabled. + * + * @returns {boolean} + * Returns `true` when the wireless radio is marked as disabled in `ubus` + * runtime state or when the `disabled` option is set in the corresponding + * UCI configuration. + */ isDisabled: function() { return this.ubus('dev', 'disabled') || this.get('disabled') == '1'; }, + /** + * Get the configuration name of this wireless radio. + * + * @returns {string} + * Returns the UCI section name (e.g. `radio0`) of the corresponding + * radio configuration which also serves as unique logical identifier + * for the wireless phy. + */ getName: function() { return this.sid; }, + /** + * Gets a list of supported hwmodes. + * + * The hwmode values describe the frequency band and wireless standard + * versions supported by the wireless phy. + * + * @returns {string[]} + * Returns an array of valid hwmode values for this radio. Currently + * known mode values are: + * - `a` - Legacy 802.11a mode, 5 GHz, up to 54 Mbit/s + * - `b` - Legacy 802.11b mode, 2.4 GHz, up to 11 Mbit/s + * - `g` - Legacy 802.11g mode, 2.4 GHz, up to 54 Mbit/s + * - `n` - IEEE 802.11n mode, 2.4 or 5 GHz, up to 600 Mbit/s + * - `ac` - IEEE 802.11ac mode, 5 GHz, up to 6770 Mbit/s + */ getHWModes: function() { var hwmodes = this.ubus('dev', 'iwinfo', 'hwmodes'); return Array.isArray(hwmodes) ? hwmodes : [ 'b', 'g' ]; }, + /** + * Gets a list of supported htmodes. + * + * The htmode values describe the wide-frequency options supported by + * the wireless phy. + * + * @returns {string[]} + * Returns an array of valid htmode values for this radio. Currently + * known mode values are: + * - `HT20` - applicable to IEEE 802.11n, 20 MHz wide channels + * - `HT40` - applicable to IEEE 802.11n, 40 MHz wide channels + * - `VHT20` - applicable to IEEE 802.11ac, 20 MHz wide channels + * - `VHT40` - applicable to IEEE 802.11ac, 40 MHz wide channels + * - `VHT80` - applicable to IEEE 802.11ac, 80 MHz wide channels + * - `VHT160` - applicable to IEEE 802.11ac, 160 MHz wide channels + */ getHTModes: function() { var htmodes = this.ubus('dev', 'iwinfo', 'htmodes'); return (Array.isArray(htmodes) && htmodes.length) ? htmodes : null; }, + /** + * Get a string describing the wireless radio hardware. + * + * @returns {string} + * Returns the description string. + */ getI18n: function() { var hw = this.ubus('dev', 'iwinfo', 'hardware'), type = L.isObject(hw) ? hw.name : null; @@ -2017,10 +3168,59 @@ WifiDevice = L.Class.extend({ return '%s 802.11%s Wireless Controller (%s)'.format(type || 'Generic', modestr, this.getName()); }, + /** + * A wireless scan result object describes a neighbouring wireless + * network found in the vincinity. + * + * @typedef {Object<string, number|string|LuCI.Network.WifiEncryption>} WifiScanResult + * @memberof LuCI.Network + * + * @property {string} ssid + * The SSID / Mesh ID of the network. + * + * @property {string} bssid + * The BSSID if the network. + * + * @property {string} mode + * The operation mode of the network (`Master`, `Ad-Hoc`, `Mesh Point`). + * + * @property {number} channel + * The wireless channel of the network. + * + * @property {number} signal + * The received signal strength of the network in dBm. + * + * @property {number} quality + * The numeric quality level of the signal, can be used in conjunction + * with `quality_max` to calculate a quality percentage. + * + * @property {number} quality_max + * The maximum possible quality level of the signal, can be used in + * conjunction with `quality` to calculate a quality percentage. + * + * @property {LuCI.Network.WifiEncryption} encryption + * The encryption used by the wireless network. + */ + + /** + * Trigger a wireless scan on this radio device and obtain a list of + * nearby networks. + * + * @returns {Promise<Array<LuCI.Network.WifiScanResult>>} + * Returns a promise resolving to an array of scan result objects + * describing the networks found in the vincinity. + */ getScanList: function() { return callIwinfoScan(this.sid); }, + /** + * Check whether the wireless radio is marked as up in the `ubus` + * runtime state. + * + * @returns {boolean} + * Returns `true` when the radio device is up, else `false`. + */ isUp: function() { if (L.isObject(_state.radios[this.sid])) return (_state.radios[this.sid].up == true); @@ -2028,6 +3228,21 @@ WifiDevice = L.Class.extend({ return false; }, + /** + * Get the wifi network of the given name belonging to this radio device + * + * @param {string} network + * The name of the wireless network to lookup. This may be either an uci + * configuration section ID, a network ID in the form `radio#.network#` + * or a Linux network device name like `wlan0` which is resolved to the + * corresponding configuration section through `ubus` runtime information. + * + * @returns {Promise<LuCI.Network.WifiNetwork>} + * Returns a promise resolving to a `Network.WifiNetwork` instance + * representing the wireless network and rejecting with `null` if + * the given network could not be found or is not associated with + * this radio device. + */ getWifiNetwork: function(network) { return L.network.getWifiNetwork(network).then(L.bind(function(networkInstance) { var uciWifiIface = (networkInstance.sid ? uci.get('wireless', networkInstance.sid) : null); @@ -2039,6 +3254,14 @@ WifiDevice = L.Class.extend({ }, this)); }, + /** + * Get all wireless networks associated with this wireless radio device. + * + * @returns {Promise<Array<LuCI.Network.WifiNetwork>>} + * Returns a promise resolving to an array of `Network.WifiNetwork` + * instances respresenting the wireless networks associated with this + * radio device. + */ getWifiNetworks: function() { var uciWifiIfaces = uci.sections('wireless', 'wifi-iface'), tasks = []; @@ -2050,6 +3273,18 @@ WifiDevice = L.Class.extend({ return Promise.all(tasks); }, + /** + * Adds a new wireless network associated with this radio device to the + * configuration and sets its options to the provided values. + * + * @param {Object<string, string|string[]>} [options] + * The options to set for the newly added wireless network. + * + * @returns {Promise<null|LuCI.Network.WifiNetwork>} + * Returns a promise resolving to a `WifiNetwork` instance describing + * the newly added wireless network or `null` if the given options + * were invalid. + */ addWifiNetwork: function(options) { if (!L.isObject(options)) options = {}; @@ -2059,6 +3294,22 @@ WifiDevice = L.Class.extend({ return L.network.addWifiNetwork(options); }, + /** + * Deletes the wireless network with the given name associated with this + * radio device. + * + * @param {string} network + * The name of the wireless network to lookup. This may be either an uci + * configuration section ID, a network ID in the form `radio#.network#` + * or a Linux network device name like `wlan0` which is resolved to the + * corresponding configuration section through `ubus` runtime information. + * + * @returns {Promise<boolean>} + * Returns a promise resolving to `true` when the wireless network was + * successfully deleted from the configuration or `false` when the given + * network could not be found or if the found network was not associated + * with this wireless radio device. + */ deleteWifiNetwork: function(network) { var sid = null; @@ -2081,7 +3332,18 @@ WifiDevice = L.Class.extend({ } }); -WifiNetwork = L.Class.extend({ +/** + * @class + * @memberof LuCI.Network + * @hideconstructor + * @classdesc + * + * A `Network.WifiNetwork` instance represents a wireless network (vif) + * configured on top of a radio device and provides functions for querying + * the runtime state of the network. Most radio devices support multiple + * such networks in parallel. + */ +WifiNetwork = L.Class.extend(/** @lends LuCI.Network.WifiNetwork.prototype */ { __init__: function(sid, radioname, radiostate, netid, netstate) { this.sid = sid; this.netid = netid; @@ -2104,22 +3366,68 @@ WifiNetwork = L.Class.extend({ return v; }, + /** + * Read the given UCI option value of this wireless network. + * + * @param {string} opt + * The UCI option name to read. + * + * @returns {null|string|string[]} + * Returns the UCI option value or `null` if the requested option is + * not found. + */ get: function(opt) { return uci.get('wireless', this.sid, opt); }, + /** + * Set the given UCI option of this network to the given value. + * + * @param {string} opt + * The name of the UCI option to set. + * + * @param {null|string|string[]} val + * The value to set or `null` to remove the given option from the + * configuration. + */ set: function(opt, value) { return uci.set('wireless', this.sid, opt, value); }, + /** + * Checks whether this wireless network is disabled. + * + * @returns {boolean} + * Returns `true` when the wireless radio is marked as disabled in `ubus` + * runtime state or when the `disabled` option is set in the corresponding + * UCI configuration. + */ isDisabled: function() { return this.ubus('dev', 'disabled') || this.get('disabled') == '1'; }, + /** + * Get the configured operation mode of the wireless network. + * + * @returns {string} + * Returns the configured operation mode. Possible values are: + * - `ap` - Master (Access Point) mode + * - `sta` - Station (client) mode + * - `adhoc` - Ad-Hoc (IBSS) mode + * - `mesh` - Mesh (IEEE 802.11s) mode + * - `monitor` - Monitor mode + */ getMode: function() { return this.ubus('net', 'config', 'mode') || this.get('mode') || 'ap'; }, + /** + * Get the configured SSID of the wireless network. + * + * @returns {null|string} + * Returns the configured SSID value or `null` when this network is + * in mesh mode. + */ getSSID: function() { if (this.getMode() == 'mesh') return null; @@ -2127,6 +3435,13 @@ WifiNetwork = L.Class.extend({ return this.ubus('net', 'config', 'ssid') || this.get('ssid'); }, + /** + * Get the configured Mesh ID of the wireless network. + * + * @returns {null|string} + * Returns the configured mesh ID value or `null` when this network + * is not in mesh mode. + */ getMeshID: function() { if (this.getMode() != 'mesh') return null; @@ -2134,22 +3449,59 @@ WifiNetwork = L.Class.extend({ return this.ubus('net', 'config', 'mesh_id') || this.get('mesh_id'); }, + /** + * Get the configured BSSID of the wireless network. + * + * @returns {null|string} + * Returns the BSSID value or `null` if none has been specified. + */ getBSSID: function() { return this.ubus('net', 'config', 'bssid') || this.get('bssid'); }, + /** + * Get the names of the logical interfaces this wireless network is + * attached to. + * + * @returns {string[]} + * Returns an array of logical interface names. + */ getNetworkNames: function() { return L.toArray(this.ubus('net', 'config', 'network') || this.get('network')); }, + /** + * Get the internal network ID of this wireless network. + * + * The network ID is a LuCI specific identifer in the form + * `radio#.network#` to identify wireless networks by their corresponding + * radio and network index numbers. + * + * @returns {string} + * Returns the LuCI specific network ID. + */ getID: function() { return this.netid; }, + /** + * Get the configuration ID of this wireless network. + * + * @returns {string} + * Returns the corresponding UCI section ID of the network. + */ getName: function() { return this.sid; }, + /** + * Get the Linux network device name. + * + * @returns {null|string} + * Returns the current Linux network device name as resolved from + * `ubus` runtime information or `null` if this network has no + * associated network device, e.g. when not configured or up. + */ getIfname: function() { var ifname = this.ubus('net', 'ifname') || this.ubus('net', 'iwinfo', 'ifname'); @@ -2159,10 +3511,25 @@ WifiNetwork = L.Class.extend({ return ifname; }, + /** + * Get the name of the corresponding wifi radio device. + * + * @returns {null|string} + * Returns the name of the radio device this network is configured on + * or `null` if it cannot be determined. + */ getWifiDeviceName: function() { return this.ubus('radio') || this.get('device'); }, + /** + * Get the corresponding wifi radio device. + * + * @returns {null|LuCI.Network.WifiDevice} + * Returns a `Network.WifiDevice` instance representing the corresponding + * wifi radio device or `null` if the related radio device could not be + * found. + */ getWifiDevice: function() { var radioname = this.getWifiDeviceName(); @@ -2172,6 +3539,18 @@ WifiNetwork = L.Class.extend({ return L.network.getWifiDevice(radioname); }, + /** + * Check whether the radio network is up. + * + * This function actually queries the up state of the related radio + * device and assumes this network to be up as well when the parent + * radio is up. This is due to the fact that OpenWrt does not control + * virtual interfaces individually but within one common hostapd + * instance. + * + * @returns {boolean} + * Returns `true` when the network is up, else `false`. + */ isUp: function() { var device = this.getDevice(); @@ -2181,6 +3560,23 @@ WifiNetwork = L.Class.extend({ return device.isUp(); }, + /** + * Query the current operation mode from runtime information. + * + * @returns {string} + * Returns the human readable mode name as reported by `ubus` runtime + * state. Possible returned values are: + * - `Master` + * - `Ad-Hoc` + * - `Client` + * - `Monitor` + * - `Master (VLAN)` + * - `WDS` + * - `Mesh Point` + * - `P2P Client` + * - `P2P Go` + * - `Unknown` + */ getActiveMode: function() { var mode = this.ubus('net', 'iwinfo', 'mode') || this.ubus('net', 'config', 'mode') || this.get('mode') || 'ap'; @@ -2194,6 +3590,14 @@ WifiNetwork = L.Class.extend({ } }, + /** + * Query the current operation mode from runtime information as + * translated string. + * + * @returns {string} + * Returns the translated, human readable mode name as reported by + *`ubus` runtime state. + */ getActiveModeI18n: function() { var mode = this.getActiveMode(); @@ -2207,22 +3611,227 @@ WifiNetwork = L.Class.extend({ } }, + /** + * Query the current SSID from runtime information. + * + * @returns {string} + * Returns the current SSID or Mesh ID as reported by `ubus` runtime + * information. + */ getActiveSSID: function() { return this.ubus('net', 'iwinfo', 'ssid') || this.ubus('net', 'config', 'ssid') || this.get('ssid'); }, + /** + * Query the current BSSID from runtime information. + * + * @returns {string} + * Returns the current BSSID or Mesh ID as reported by `ubus` runtime + * information. + */ getActiveBSSID: function() { return this.ubus('net', 'iwinfo', 'bssid') || this.ubus('net', 'config', 'bssid') || this.get('bssid'); }, + /** + * Query the current encryption settings from runtime information. + * + * @returns {string} + * Returns a string describing the current encryption or `-` if the the + * encryption state could not be found in `ubus` runtime information. + */ getActiveEncryption: function() { return formatWifiEncryption(this.ubus('net', 'iwinfo', 'encryption')) || '-'; }, + /** + * A wireless peer entry describes the properties of a remote wireless + * peer associated with a local network. + * + * @typedef {Object<string, boolean|number|string|LuCI.Network.WifiRateEntry>} WifiPeerEntry + * @memberof LuCI.Network + * + * @property {string} mac + * The MAC address (BSSID). + * + * @property {number} signal + * The received signal strength. + * + * @property {number} [signal_avg] + * The average signal strength if supported by the driver. + * + * @property {number} [noise] + * The current noise floor of the radio. May be `0` or absent if not + * supported by the driver. + * + * @property {number} inactive + * The amount of milliseconds the peer has been inactive, e.g. due + * to powersave. + * + * @property {number} connected_time + * The amount of milliseconds the peer is associated to this network. + * + * @property {number} [thr] + * The estimated throughput of the peer, May be `0` or absent if not + * supported by the driver. + * + * @property {boolean} authorized + * Specifies whether the peer is authorized to associate to this network. + * + * @property {boolean} authenticated + * Specifies whether the peer completed authentication to this network. + * + * @property {string} preamble + * The preamble mode used by the peer. May be `long` or `short`. + * + * @property {boolean} wme + * Specifies whether the peer supports WME/WMM capabilities. + * + * @property {boolean} mfp + * Specifies whether management frame protection is active. + * + * @property {boolean} tdls + * Specifies whether TDLS is active. + * + * @property {number} [mesh llid] + * The mesh LLID, may be `0` or absent if not applicable or supported + * by the driver. + * + * @property {number} [mesh plid] + * The mesh PLID, may be `0` or absent if not applicable or supported + * by the driver. + * + * @property {string} [mesh plink] + * The mesh peer link state description, may be an empty string (`''`) + * or absent if not applicable or supported by the driver. + * + * The following states are known: + * - `LISTEN` + * - `OPN_SNT` + * - `OPN_RCVD` + * - `CNF_RCVD` + * - `ESTAB` + * - `HOLDING` + * - `BLOCKED` + * - `UNKNOWN` + * + * @property {number} [mesh local PS] + * The local powersafe mode for the peer link, may be an empty + * string (`''`) or absent if not applicable or supported by + * the driver. + * + * The following modes are known: + * - `ACTIVE` (no power save) + * - `LIGHT SLEEP` + * - `DEEP SLEEP` + * - `UNKNOWN` + * + * @property {number} [mesh peer PS] + * The remote powersafe mode for the peer link, may be an empty + * string (`''`) or absent if not applicable or supported by + * the driver. + * + * The following modes are known: + * - `ACTIVE` (no power save) + * - `LIGHT SLEEP` + * - `DEEP SLEEP` + * - `UNKNOWN` + * + * @property {number} [mesh non-peer PS] + * The powersafe mode for all non-peer neigbours, may be an empty + * string (`''`) or absent if not applicable or supported by the driver. + * + * The following modes are known: + * - `ACTIVE` (no power save) + * - `LIGHT SLEEP` + * - `DEEP SLEEP` + * - `UNKNOWN` + * + * @property {LuCI.Network.WifiRateEntry} rx + * Describes the receiving wireless rate from the peer. + * + * @property {LuCI.Network.WifiRateEntry} tx + * Describes the transmitting wireless rate to the peer. + */ + + /** + * A wireless rate entry describes the properties of a wireless + * transmission rate to or from a peer. + * + * @typedef {Object<string, boolean|number>} WifiRateEntry + * @memberof LuCI.Network + * + * @property {number} [drop_misc] + * The amount of received misc. packages that have been dropped, e.g. + * due to corruption or missing authentication. Only applicable to + * receiving rates. + * + * @property {number} packets + * The amount of packets that have been received or sent. + * + * @property {number} bytes + * The amount of bytes that have been received or sent. + * + * @property {number} [failed] + * The amount of failed tranmission attempts. Only applicable to + * transmit rates. + * + * @property {number} [retries] + * The amount of retried transmissions. Only applicable to transmit + * rates. + * + * @property {boolean} is_ht + * Specifies whether this rate is an HT (IEEE 802.11n) rate. + * + * @property {boolean} is_vht + * Specifies whether this rate is an VHT (IEEE 802.11ac) rate. + * + * @property {number} mhz + * The channel width in MHz used for the transmission. + * + * @property {number} rate + * The bitrate in bit/s of the transmission. + * + * @property {number} [mcs] + * The MCS index of the used transmission rate. Only applicable to + * HT or VHT rates. + * + * @property {number} [40mhz] + * Specifies whether the tranmission rate used 40MHz wide channel. + * Only applicable to HT or VHT rates. + * + * Note: this option exists for backwards compatibility only and its + * use is discouraged. The `mhz` field should be used instead to + * determine the channel width. + * + * @property {boolean} [short_gi] + * Specifies whether a short guard interval is used for the transmission. + * Only applicable to HT or VHT rates. + * + * @property {number} [nss] + * Specifies the number of spatial streams used by the transmission. + * Only applicable to VHT rates. + */ + + /** + * Fetch the list of associated peers. + * + * @returns {Promise<Array<LuCI.Network.WifiPeerEntry>>} + * Returns a promise resolving to an array of wireless peers associated + * with this network. + */ getAssocList: function() { return callIwinfoAssoclist(this.getIfname()); }, + /** + * Query the current operating frequency of the wireless network. + * + * @returns {null|string} + * Returns the current operating frequency of the network from `ubus` + * runtime information in GHz or `null` if the information is not + * available. + */ getFrequency: function() { var freq = this.ubus('net', 'iwinfo', 'frequency'); @@ -2232,6 +3841,15 @@ WifiNetwork = L.Class.extend({ return null; }, + /** + * Query the current average bitrate of all peers associated to this + * wireless network. + * + * @returns {null|number} + * Returns the average bit rate among all peers associated to the network + * as reported by `ubus` runtime information or `null` if the information + * is not available. + */ getBitRate: function() { var rate = this.ubus('net', 'iwinfo', 'bitrate'); @@ -2241,30 +3859,84 @@ WifiNetwork = L.Class.extend({ return null; }, + /** + * Query the current wireless channel. + * + * @returns {null|number} + * Returns the wireless channel as reported by `ubus` runtime information + * or `null` if it cannot be determined. + */ getChannel: function() { return this.ubus('net', 'iwinfo', 'channel') || this.ubus('dev', 'config', 'channel') || this.get('channel'); }, + /** + * Query the current wireless signal. + * + * @returns {null|number} + * Returns the wireless signal in dBm as reported by `ubus` runtime + * information or `null` if it cannot be determined. + */ getSignal: function() { return this.ubus('net', 'iwinfo', 'signal') || 0; }, + /** + * Query the current radio noise floor. + * + * @returns {number} + * Returns the radio noise floor in dBm as reported by `ubus` runtime + * information or `0` if it cannot be determined. + */ getNoise: function() { return this.ubus('net', 'iwinfo', 'noise') || 0; }, + /** + * Query the current country code. + * + * @returns {string} + * Returns the wireless country code as reported by `ubus` runtime + * information or `00` if it cannot be determined. + */ getCountryCode: function() { return this.ubus('net', 'iwinfo', 'country') || this.ubus('dev', 'config', 'country') || '00'; }, + /** + * Query the current radio TX power. + * + * @returns {null|number} + * Returns the wireless network transmit power in dBm as reported by + * `ubus` runtime information or `null` if it cannot be determined. + */ getTXPower: function() { return this.ubus('net', 'iwinfo', 'txpower'); }, + /** + * Query the radio TX power offset. + * + * Some wireless radios have a fixed power offset, e.g. due to the + * use of external amplifiers. + * + * @returns {number} + * Returns the wireless network transmit power offset in dBm as reported + * by `ubus` runtime information or `0` if there is no offset, or if it + * cannot be determined. + */ getTXPowerOffset: function() { return this.ubus('net', 'iwinfo', 'txpower_offset') || 0; }, + /** + * Calculate the current signal. + * + * @deprecated + * @returns {number} + * Returns the calculated signal level, which is the difference between + * noise and signal (SNR), divided by 5. + */ getSignalLevel: function(signal, noise) { if (this.getActiveBSSID() == '00:00:00:00:00:00') return -1; @@ -2280,6 +3952,14 @@ WifiNetwork = L.Class.extend({ return 0; }, + /** + * Calculate the current signal quality percentage. + * + * @returns {number} + * Returns the calculated signal quality in percent. The value is + * calculated from the `quality` and `quality_max` indicators reported + * by `ubus` runtime state. + */ getSignalPercent: function() { var qc = this.ubus('net', 'iwinfo', 'quality') || 0, qm = this.ubus('net', 'iwinfo', 'quality_max') || 0; @@ -2290,12 +3970,29 @@ WifiNetwork = L.Class.extend({ return 0; }, + /** + * Get a short description string for this wireless network. + * + * @returns {string} + * Returns a string describing this network, consisting of the + * active operation mode, followed by either the SSID, BSSID or + * internal network ID, depending on which information is available. + */ getShortName: function() { return '%s "%s"'.format( this.getActiveModeI18n(), this.getActiveSSID() || this.getActiveBSSID() || this.getID()); }, + /** + * Get a description string for this wireless network. + * + * @returns {string} + * Returns a string describing this network, consisting of the + * term `Wireless Network`, followed by the active operation mode, + * the SSID, BSSID or internal network ID and the Linux network device + * name, depending on which information is available. + */ getI18n: function() { return '%s: %s "%s" (%s)'.format( _('Wireless Network'), @@ -2304,10 +4001,25 @@ WifiNetwork = L.Class.extend({ this.getIfname()); }, + /** + * Get the primary logical interface this wireless network is attached to. + * + * @returns {null|LuCI.Network.Protocol} + * Returns a `Network.Protocol` instance representing the logical + * interface or `null` if this network is not attached to any logical + * interface. + */ getNetwork: function() { return this.getNetworks()[0]; }, + /** + * Get the logical interfaces this wireless network is attached to. + * + * @returns {Array<LuCI.Network.Protocol>} + * Returns an array of `Network.Protocol` instances representing the + * logical interfaces this wireless network is attached to. + */ getNetworks: function() { var networkNames = this.getNetworkNames(), networks = []; @@ -2326,6 +4038,13 @@ WifiNetwork = L.Class.extend({ return networks; }, + /** + * Get the associated Linux network device. + * + * @returns {LuCI.Network.Device} + * Returns a `Network.Device` instance representing the Linux network + * device associted with this wireless network. + */ getDevice: function() { return L.network.instantiateDevice(this.getIfname()); } diff --git a/modules/luci-base/htdocs/luci-static/resources/rpc.js b/modules/luci-base/htdocs/luci-static/resources/rpc.js index 9c74bfdaf..9b642444f 100644 --- a/modules/luci-base/htdocs/luci-static/resources/rpc.js +++ b/modules/luci-base/htdocs/luci-static/resources/rpc.js @@ -5,7 +5,17 @@ var rpcRequestID = 1, rpcBaseURL = L.url('admin/ubus'), rpcInterceptorFns = []; -return L.Class.extend({ +/** + * @class rpc + * @memberof LuCI + * @hideconstructor + * @classdesc + * + * The `LuCI.rpc` class provides high level ubus JSON-RPC abstractions + * and means for listing and invoking remove RPC methods. + */ +return L.Class.extend(/** @lends LuCI.rpc.prototype */ { + /* privates */ call: function(req, cb, nobatch) { var q = ''; @@ -106,6 +116,27 @@ return L.Class.extend({ req.resolve(ret); }, + /** + * Lists available remote ubus objects or the method signatures of + * specific objects. + * + * This function has two signatures and is sensitive to the number of + * arguments passed to it: + * - `list()` - + * Returns an array containing the names of all remote `ubus` objects + * - `list("objname", ...)` + * Returns method signatures for each given `ubus` object name. + * + * @param {...string} [objectNames] + * If any object names are given, this function will return the method + * signatures of each given object. + * + * @returns {Promise<Array<string>|Object<string, Object<string, Object<string, string>>>>} + * When invoked without arguments, this function will return a promise + * resolving to an array of `ubus` object names. When invoked with one or + * more arguments, a promise resolving to an object describing the method + * signatures of each requested `ubus` object name will be returned. + */ list: function() { var msg = { jsonrpc: '2.0', @@ -126,6 +157,138 @@ return L.Class.extend({ }, this)); }, + /** + * @typedef {Object} DeclareOptions + * @memberof LuCI.rpc + * + * @property {string} object + * The name of the remote `ubus` object to invoke. + * + * @property {string} method + * The name of the remote `ubus` method to invoke. + * + * @property {string[]} [params] + * Lists the named parameters expected by the remote `ubus` RPC method. + * The arguments passed to the resulting generated method call function + * will be mapped to named parameters in the order they appear in this + * array. + * + * Extraneous parameters passed to the generated function will not be + * sent to the remote procedure but are passed to the + * {@link LuCI.rpc~filterFn filter function} if one is specified. + * + * Examples: + * - `params: [ "foo", "bar" ]` - + * When the resulting call function is invoked with `fn(true, false)`, + * the corresponding args object sent to the remote procedure will be + * `{ foo: true, bar: false }`. + * - `params: [ "test" ], filter: function(reply, args, extra) { ... }` - + * When the resultung generated function is invoked with + * `fn("foo", "bar", "baz")` then `{ "test": "foo" }` will be sent as + * argument to the remote procedure and the filter function will be + * invoked with `filterFn(reply, [ "foo" ], "bar", "baz")` + * + * @property {Object<string,*>} [expect] + * Describes the expected return data structure. The given object is + * supposed to contain a single key selecting the value to use from + * the returned `ubus` reply object. The value of the sole key within + * the `expect` object is used to infer the expected type of the received + * `ubus` reply data. + * + * If the received data does not contain `expect`'s key, or if the + * type of the data differs from the type of the value in the expect + * object, the expect object's value is returned as default instead. + * + * The key in the `expect` object may be an empty string (`''`) in which + * case the entire reply object is selected instead of one of its subkeys. + * + * If the `expect` option is omitted, the received reply will be returned + * as-is, regardless of its format or type. + * + * Examples: + * - `expect: { '': { error: 'Invalid response' } }` - + * This requires the entire `ubus` reply to be a plain JavaScript + * object. If the reply isn't an object but e.g. an array or a numeric + * error code instead, it will get replaced with + * `{ error: 'Invalid response' }` instead. + * - `expect: { results: [] }` - + * This requires the received `ubus` reply to be an object containing + * a key `results` with an array as value. If the received reply does + * not contain such a key, or if `reply.results` points to a non-array + * value, the empty array (`[]`) will be used instead. + * - `expect: { success: false }` - + * This requires the received `ubus` reply to be an object containing + * a key `success` with a boolean value. If the reply does not contain + * `success` or if `reply.success` is not a boolean value, `false` will + * be returned as default instead. + * + * @property {LuCI.rpc~filterFn} [filter] + * Specfies an optional filter function which is invoked to transform the + * received reply data before it is returned to the caller. + * + */ + + /** + * The filter function is invoked to transform a received `ubus` RPC call + * reply before returning it to the caller. + * + * @callback LuCI.rpc~filterFn + * + * @param {*} data + * The received `ubus` reply data or a subset of it as described in the + * `expect` option of the RPC call declaration. In case of remote call + * errors, `data` is numeric `ubus` error code instead. + * + * @param {Array<*>} args + * The arguments the RPC method has been invoked with. + * + * @param {...*} extraArgs + * All extraneous arguments passed to the RPC method exceeding the number + * of arguments describes in the RPC call declaration. + * + * @return {*} + * The return value of the filter function will be returned to the caller + * of the RPC method as-is. + */ + + /** + * The generated invocation function is returned by + * {@link LuCI.rpc#declare rpc.declare()} and encapsulates a single + * RPC method call. + * + * Calling this function will execute a remote `ubus` HTTP call request + * using the arguments passed to it as arguments and return a promise + * resolving to the received reply values. + * + * @callback LuCI.rpc~invokeFn + * + * @param {...*} params + * The parameters to pass to the remote procedure call. The given + * positional arguments will be named to named RPC parameters according + * to the names specified in the `params` array of the method declaration. + * + * Any additional parameters exceeding the amount of arguments in the + * `params` declaration are passed as private extra arguments to the + * declared filter function. + * + * @return {Promise<*>} + * Returns a promise resolving to the result data of the remote `ubus` + * RPC method invocation, optionally substituted and filtered according + * to the `expect` and `filter` declarations. + */ + + /** + * Describes a remote RPC call procedure and returns a function + * implementing it. + * + * @param {LuCI.rpc.DeclareOptions} options + * If any object names are given, this function will return the method + * signatures of each given object. + * + * @returns {LuCI.rpc~invokeFn} + * Returns a new function implementing the method call described in + * `options`. + */ declare: function(options) { return Function.prototype.bind.call(function(rpc, options) { var args = this.varargs(arguments, 2); @@ -173,22 +336,58 @@ return L.Class.extend({ }, this, this, options); }, + /** + * Returns the current RPC session id. + * + * @returns {string} + * Returns the 32 byte session ID string used for authenticating remote + * requests. + */ getSessionID: function() { return rpcSessionID; }, + /** + * Set the RPC session id to use. + * + * @param {string} sid + * Sets the 32 byte session ID string used for authenticating remote + * requests. + */ setSessionID: function(sid) { rpcSessionID = sid; }, + /** + * Returns the current RPC base URL. + * + * @returns {string} + * Returns the RPC URL endpoint to issue requests against. + */ getBaseURL: function() { return rpcBaseURL; }, + /** + * Set the RPC base URL to use. + * + * @param {string} sid + * Sets the RPC URL endpoint to issue requests against. + */ setBaseURL: function(url) { rpcBaseURL = url; }, + /** + * Translates a numeric `ubus` error code into a human readable + * description. + * + * @param {number} statusCode + * The numeric status code. + * + * @returns {string} + * Returns the textual description of the code. + */ getStatusText: function(statusCode) { switch (statusCode) { case 0: return _('Command OK'); @@ -206,12 +405,68 @@ return L.Class.extend({ } }, + /** + * Registered interceptor functions are invoked before the standard reply + * parsing and handling logic. + * + * By returning rejected promises, interceptor functions can cause the + * invocation function to fail, regardless of the received reply. + * + * Interceptors may also modify their message argument in-place to + * rewrite received replies before they're processed by the standard + * response handling code. + * + * A common use case for such functions is to detect failing RPC replies + * due to expired authentication in order to trigger a new login. + * + * @callback LuCI.rpc~interceptorFn + * + * @param {*} msg + * The unprocessed, JSON decoded remote RPC method call reply. + * + * Since interceptors run before the standard parsing logic, the reply + * data is not verified for correctness or filtered according to + * `expect` and `filter` specifications in the declarations. + * + * @param {Object} req + * The related request object which is an extended variant of the + * declaration object, allowing access to internals of the invocation + * function such as `filter`, `expect` or `params` values. + * + * @return {Promise<*>|*} + * Interceptor functions may return a promise to defer response + * processing until some delayed work completed. Any values the returned + * promise resolves to are ignored. + * + * When the returned promise rejects with an error, the invocation + * function will fail too, forwarding the error to the caller. + */ + + /** + * Registers a new interceptor function. + * + * @param {LuCI.rpc~interceptorFn} interceptorFn + * The inteceptor function to register. + * + * @returns {LuCI.rpc~interceptorFn} + * Returns the given function value. + */ addInterceptor: function(interceptorFn) { if (typeof(interceptorFn) == 'function') rpcInterceptorFns.push(interceptorFn); return interceptorFn; }, + /** + * Removes a registered interceptor function. + * + * @param {LuCI.rpc~interceptorFn} interceptorFn + * The inteceptor function to remove. + * + * @returns {boolean} + * Returns `true` if the given function has been removed or `false` + * if it has not been found. + */ removeInterceptor: function(interceptorFn) { var oldlen = rpcInterceptorFns.length, i = oldlen; while (i--) diff --git a/modules/luci-base/htdocs/luci-static/resources/uci.js b/modules/luci-base/htdocs/luci-static/resources/uci.js index 17f11eecb..677edf6ad 100644 --- a/modules/luci-base/htdocs/luci-static/resources/uci.js +++ b/modules/luci-base/htdocs/luci-static/resources/uci.js @@ -1,7 +1,18 @@ 'use strict'; 'require rpc'; -return L.Class.extend({ +/** + * @class uci + * @memberof LuCI + * @hideconstructor + * @classdesc + * + * The `LuCI.uci` class utilizes {@link LuCI.rpc} to declare low level + * remote UCI `ubus` procedures and implements a local caching and data + * manipulation layer on top to allow for synchroneous operations on + * UCI configuration data. + */ +return L.Class.extend(/** @lends LuCI.uci.prototype */ { __init__: function() { this.state = { newidx: 0, @@ -22,6 +33,7 @@ return L.Class.extend({ expect: { values: { } } }), + callOrder: rpc.declare({ object: 'uci', method: 'order', @@ -58,6 +70,21 @@ return L.Class.extend({ method: 'confirm' }), + + /** + * Generates a new, unique section ID for the given configuration. + * + * Note that the generated ID is temporary, it will get replaced by an + * identifier in the form `cfgXXXXXX` once the configuration is saved + * by the remote `ubus` UCI api. + * + * @param {string} config + * The configuration to generate the new section ID for. + * + * @returns {string} + * A newly generated, unique section ID in the form `newXXXXXX` + * where `X` denotes a hexadecimal digit. + */ createSID: function(conf) { var v = this.state.values, n = this.state.creates, @@ -70,6 +97,25 @@ return L.Class.extend({ return sid; }, + /** + * Resolves a given section ID in extended notation to the internal + * section ID value. + * + * @param {string} config + * The configuration to resolve the section ID for. + * + * @param {string} sid + * The section ID to resolve. If the ID is in the form `@typename[#]`, + * it will get resolved to an internal anonymous ID in the forms + * `cfgXXXXXX`/`newXXXXXX` or to the name of a section in case it points + * to a named section. When the given ID is not in extended notation, + * it will be returned as-is. + * + * @returns {string|null} + * Returns the resolved section ID or the original given ID if it was + * not in extended notation. Returns `null` when an extended ID could + * not be resolved to existing section ID. + */ resolveSID: function(conf, sid) { if (typeof(sid) != 'string') return sid; @@ -88,6 +134,7 @@ return L.Class.extend({ return sid; }, + /* private */ reorderSections: function() { var v = this.state.values, n = this.state.creates, @@ -129,6 +176,7 @@ return L.Class.extend({ return Promise.all(tasks); }, + /* private */ loadPackage: function(packageName) { if (this.loaded[packageName] == null) return (this.loaded[packageName] = this.callLoad(packageName)); @@ -136,6 +184,24 @@ return L.Class.extend({ return Promise.resolve(this.loaded[packageName]); }, + /** + * Loads the given UCI configurations from the remote `ubus` api. + * + * Loaded configurations are cached and only loaded once. Subsequent + * load operations of the same configurations will return the cached + * data. + * + * To force reloading a configuration, it has to be unloaded with + * {@link LuCI.uci#unload uci.unload()} first. + * + * @param {string|string[]} config + * The name of the configuration or an array of configuration + * names to load. + * + * @returns {Promise<string[]>} + * Returns a promise resolving to the names of the configurations + * that have been successfully loaded. + */ load: function(packages) { var self = this, pkgs = [ ], @@ -161,6 +227,13 @@ return L.Class.extend({ }); }, + /** + * Unloads the given UCI configurations from the local cache. + * + * @param {string|string[]} config + * The name of the configuration or an array of configuration + * names to unload. + */ unload: function(packages) { if (!Array.isArray(packages)) packages = [ packages ]; @@ -175,6 +248,24 @@ return L.Class.extend({ } }, + /** + * Adds a new section of the given type to the given configuration, + * optionally named according to the given name. + * + * @param {string} config + * The name of the configuration to add the section to. + * + * @param {string} type + * The type of the section to add. + * + * @param {string} [name] + * The name of the section to add. If the name is omitted, an anonymous + * section will be added instead. + * + * @returns {string} + * Returns the section ID of the newly added section which is equivalent + * to the given name for non-anonymous sections. + */ add: function(conf, type, name) { var n = this.state.creates, sid = name || this.createSID(conf); @@ -193,6 +284,15 @@ return L.Class.extend({ return sid; }, + /** + * Removes the section with the given ID from the given configuration. + * + * @param {string} config + * The name of the configuration to remove the section from. + * + * @param {string} sid + * The ID of the section to remove. + */ remove: function(conf, sid) { var n = this.state.creates, c = this.state.changes, @@ -213,6 +313,74 @@ return L.Class.extend({ } }, + /** + * A section object represents the options and their corresponding values + * enclosed within a configuration section, as well as some additional + * meta data such as sort indexes and internal ID. + * + * Any internal metadata fields are prefixed with a dot which is isn't + * an allowed character for normal option names. + * + * @typedef {Object<string, boolean|number|string|string[]>} SectionObject + * @memberof LuCI.uci + * + * @property {boolean} .anonymous + * The `.anonymous` property specifies whether the configuration is + * anonymous (`true`) or named (`false`). + * + * @property {number} .index + * The `.index` property specifes the sort order of the section. + * + * @property {string} .name + * The `.name` property holds the name of the section object. It may be + * either an anonymous ID in the form `cfgXXXXXX` or `newXXXXXX` with `X` + * being a hexadecimal digit or a string holding the name of the section. + * + * @property {string} .type + * The `.type` property contains the type of the corresponding uci + * section. + * + * @property {string|string[]} * + * A section object may contain an arbitrary number of further properties + * representing the uci option enclosed in the section. + * + * All option property names will be in the form `[A-Za-z0-9_]+` and + * either contain a string value or an array of strings, in case the + * underlying option is an UCI list. + */ + + /** + * The sections callback is invoked for each section found within + * the given configuration and receives the section object and its + * associated name as arguments. + * + * @callback LuCI.uci~sectionsFn + * + * @param {LuCI.uci.SectionObject} section + * The section object. + * + * @param {string} sid + * The name or ID of the section. + */ + + /** + * Enumerates the sections of the given configuration, optionally + * filtered by type. + * + * @param {string} config + * The name of the configuration to enumerate the sections for. + * + * @param {string} [type] + * Enumerate only sections of the given type. If omitted, enumerate + * all sections. + * + * @param {LuCI.uci~sectionsFn} [cb] + * An optional callback to invoke for each enumerated section. + * + * @returns {Array<LuCI.uci.SectionObject>} + * Returns a sorted array of the section objects within the given + * configuration, filtered by type of a type has been specified. + */ sections: function(conf, type, cb) { var sa = [ ], v = this.state.values[conf], @@ -247,6 +415,31 @@ return L.Class.extend({ return sa; }, + /** + * Gets the value of the given option within the specified section + * of the given configuration or the entire section object if the + * option name is omitted. + * + * @param {string} config + * The name of the configuration to read the value from. + * + * @param {string} sid + * The name or ID of the section to read. + * + * @param {string} [option] + * The option name to read the value from. If the option name is + * omitted or `null`, the entire section is returned instead. + * + * @returns {null|string|string[]|LuCI.uci.SectionObject} + * - Returns a string containing the option value in case of a + * plain UCI option. + * - Returns an array of strings containing the option values in + * case of `option` pointing to an UCI list. + * - Returns a {@link LuCI.uci.SectionObject section object} if + * the `option` argument has been omitted or is `null`. + * - Returns `null` if the config, section or option has not been + * found or if the corresponding configuration is not loaded. + */ get: function(conf, sid, opt) { var v = this.state.values, n = this.state.creates, @@ -299,6 +492,27 @@ return L.Class.extend({ return undefined; }, + /** + * Sets the value of the given option within the specified section + * of the given configuration. + * + * If either config, section or option is null, or if `option` begins + * with a dot, the function will do nothing. + * + * @param {string} config + * The name of the configuration to set the option value in. + * + * @param {string} sid + * The name or ID of the section to set the option value in. + * + * @param {string} option + * The option name to set the value for. + * + * @param {null|string|string[]} value + * The option value to set. If the value is `null` or an empty string, + * the option will be removed, otherwise it will be set or overwritten + * with the given value. + */ set: function(conf, sid, opt, val) { var v = this.state.values, n = this.state.creates, @@ -354,10 +568,53 @@ return L.Class.extend({ } }, + /** + * Remove the given option within the specified section of the given + * configuration. + * + * This function is a convenience wrapper around + * `uci.set(config, section, option, null)`. + * + * @param {string} config + * The name of the configuration to remove the option from. + * + * @param {string} sid + * The name or ID of the section to remove the option from. + * + * @param {string} option + * The name of the option to remove. + */ unset: function(conf, sid, opt) { return this.set(conf, sid, opt, null); }, + /** + * Gets the value of the given option or the entire section object of + * the first found section of the specified type or the first found + * section of the entire configuration if no type is specfied. + * + * @param {string} config + * The name of the configuration to read the value from. + * + * @param {string} [type] + * The type of the first section to find. If it is `null`, the first + * section of the entire config is read, otherwise the first section + * matching the given type. + * + * @param {string} [option] + * The option name to read the value from. If the option name is + * omitted or `null`, the entire section is returned instead. + * + * @returns {null|string|string[]|LuCI.uci.SectionObject} + * - Returns a string containing the option value in case of a + * plain UCI option. + * - Returns an array of strings containing the option values in + * case of `option` pointing to an UCI list. + * - Returns a {@link LuCI.uci.SectionObject section object} if + * the `option` argument has been omitted or is `null`. + * - Returns `null` if the config, section or option has not been + * found or if the corresponding configuration is not loaded. + */ get_first: function(conf, type, opt) { var sid = null; @@ -369,6 +626,30 @@ return L.Class.extend({ return this.get(conf, sid, opt); }, + /** + * Sets the value of the given option within the first found section + * of the given configuration matching the specified type or within + * the first section of the entire config when no type has is specified. + * + * If either config, type or option is null, or if `option` begins + * with a dot, the function will do nothing. + * + * @param {string} config + * The name of the configuration to set the option value in. + * + * @param {string} [type] + * The type of the first section to find. If it is `null`, the first + * section of the entire config is written to, otherwise the first + * section matching the given type is used. + * + * @param {string} option + * The option name to set the value for. + * + * @param {null|string|string[]} value + * The option value to set. If the value is `null` or an empty string, + * the option will be removed, otherwise it will be set or overwritten + * with the given value. + */ set_first: function(conf, type, opt, val) { var sid = null; @@ -380,10 +661,60 @@ return L.Class.extend({ return this.set(conf, sid, opt, val); }, + /** + * Removes the given option within the first found section of the given + * configuration matching the specified type or within the first section + * of the entire config when no type has is specified. + * + * This function is a convenience wrapper around + * `uci.set_first(config, type, option, null)`. + * + * @param {string} config + * The name of the configuration to set the option value in. + * + * @param {string} [type] + * The type of the first section to find. If it is `null`, the first + * section of the entire config is written to, otherwise the first + * section matching the given type is used. + * + * @param {string} option + * The option name to set the value for. + */ unset_first: function(conf, type, opt) { return this.set_first(conf, type, opt, null); }, + /** + * Move the first specified section within the given configuration + * before or after the second specified section. + * + * @param {string} config + * The configuration to move the section within. + * + * @param {string} sid1 + * The ID of the section to move within the configuration. + * + * @param {string} [sid2] + * The ID of the target section for the move operation. If the + * `after` argument is `false` or not specified, the section named by + * `sid1` will be moved before this target section, if the `after` + * argument is `true`, the `sid1` section will be moved after this + * section. + * + * When the `sid2` argument is `null`, the section specified by `sid1` + * is moved to the end of the configuration. + * + * @param {boolean} [after=false] + * When `true`, the section `sid1` is moved after the section `sid2`, + * when `false`, the section `sid1` is moved before `sid2`. + * + * If `sid2` is null, then this parameter has no effect and the section + * `sid1` is moved to the end of the configuration instead. + * + * @returns {boolean} + * Returns `true` when the section was successfully moved, or `false` + * when either the section specified by `sid1` or by `sid2` is not found. + */ move: function(conf, sid1, sid2, after) { var sa = this.sections(conf), s1 = null, s2 = null; @@ -428,6 +759,16 @@ return L.Class.extend({ return true; }, + /** + * Submits all local configuration changes to the remove `ubus` api, + * adds, removes and reorders remote sections as needed and reloads + * all loaded configurations to resynchronize the local state with + * the remote configuration values. + * + * @returns {string[]} + * Returns a promise resolving to an array of configuration names which + * have been reloaded by the save operation. + */ save: function() { var v = this.state.values, n = this.state.creates, @@ -503,6 +844,17 @@ return L.Class.extend({ }); }, + /** + * Instructs the remote `ubus` UCI api to commit all saved changes with + * rollback protection and attempts to confirm the pending commit + * operation to cancel the rollback timer. + * + * @param {number} [timeout=10] + * Override the confirmation timeout after which a rollback is triggered. + * + * @returns {Promise<number>} + * Returns a promise resolving/rejecting with the `ubus` RPC status code. + */ apply: function(timeout) { var self = this, date = new Date(); @@ -532,6 +884,57 @@ return L.Class.extend({ }); }, + /** + * An UCI change record is a plain array containing the change operation + * name as first element, the affected section ID as second argument + * and an optional third and fourth argument whose meanings depend on + * the operation. + * + * @typedef {string[]} ChangeRecord + * @memberof LuCI.uci + * + * @property {string} 0 + * The operation name - may be one of `add`, `set`, `remove`, `order`, + * `list-add`, `list-del` or `rename`. + * + * @property {string} 1 + * The section ID targeted by the operation. + * + * @property {string} 2 + * The meaning of the third element depends on the operation. + * - For `add` it is type of the section that has been added + * - For `set` it either is the option name if a fourth element exists, + * or the type of a named section which has been added when the change + * entry only contains three elements. + * - For `remove` it contains the name of the option that has been + * removed. + * - For `order` it specifies the new sort index of the section. + * - For `list-add` it contains the name of the list option a new value + * has been added to. + * - For `list-del` it contains the name of the list option a value has + * been removed from. + * - For `rename` it contains the name of the option that has been + * renamed if a fourth element exists, else it contains the new name + * a section has been renamed to if the change entry only contains + * three elements. + * + * @property {string} 4 + * The meaning of the fourth element depends on the operation. + * - For `set` it is the value an option has been set to. + * - For `list-add` it is the new value that has been added to a + * list option. + * - For `rename` it is the new name of an option that has been + * renamed. + */ + + /** + * Fetches uncommitted UCI changes from the remote `ubus` RPC api. + * + * @method + * @returns {Promise<Object<string, Array<LuCI.uci.ChangeRecord>>>} + * Returns a promise resolving to an object containing the configuration + * names as keys and arrays of related change records as values. + */ changes: rpc.declare({ object: 'uci', method: 'changes', |