diff options
9 files changed, 4242 insertions, 188 deletions
diff --git a/applications/luci-app-vpnbypass/Makefile b/applications/luci-app-vpnbypass/Makefile index 2e26a5158e..51c3dafa70 100644 --- a/applications/luci-app-vpnbypass/Makefile +++ b/applications/luci-app-vpnbypass/Makefile @@ -10,7 +10,7 @@ LUCI_TITLE:=VPN Bypass Web UI LUCI_DESCRIPTION:=Provides Web UI for VPNBypass service. LUCI_DEPENDS:=+luci-mod-admin-full +vpnbypass LUCI_PKGARCH:=all -PKG_RELEASE:=9 +PKG_RELEASE:=10 include ../../luci.mk diff --git a/applications/luci-app-vpnbypass/luasrc/model/cbi/vpnbypass.lua b/applications/luci-app-vpnbypass/luasrc/model/cbi/vpnbypass.lua index 95971fab1b..75c681ec44 100644 --- a/applications/luci-app-vpnbypass/luasrc/model/cbi/vpnbypass.lua +++ b/applications/luci-app-vpnbypass/luasrc/model/cbi/vpnbypass.lua @@ -30,7 +30,11 @@ function en.write() sys.init.enable(packageName) sys.init.start(packageName) end - http.redirect(dispatcher.build_url("admin/services/" .. packageName)) + if dispatcher.lookup("admin/vpn") then + http.redirect(dispatcher.build_url("admin/vpn/" .. packageName)) + else + http.redirect(dispatcher.build_url("admin/services/" .. packageName)) + end end s = m:section(NamedSection, "config", "vpnbypass", translate("VPN Bypass Rules")) diff --git a/modules/luci-base/htdocs/luci-static/resources/luci.js b/modules/luci-base/htdocs/luci-static/resources/luci.js index 4e3c8445a9..cad7208532 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 a9e65dac51..a71c97c307 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 9c74bfdaf0..9b642444fa 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 17f11eecb8..677edf6add 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', diff --git a/modules/luci-base/po/zh-cn/base.po b/modules/luci-base/po/zh-cn/base.po index 50317fbd23..1f89f8af1c 100644 --- a/modules/luci-base/po/zh-cn/base.po +++ b/modules/luci-base/po/zh-cn/base.po @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"PO-Revision-Date: 2019-05-17 09:18+0800\n" +"PO-Revision-Date: 2019-09-29 15:29+0800\n" "Last-Translator: Yangfl <mmyangfl@gmail.com>\n" "Language-Team: <debian-l10n-chinese@lists.debian.org>\n" "Language: \n" @@ -247,7 +247,7 @@ msgstr "" #: modules/luci-base/htdocs/luci-static/resources/ui.js:1605 msgid "A directory with the same name already exists." -msgstr "" +msgstr "已存在同名的目录。" #: modules/luci-base/htdocs/luci-static/resources/luci.js:875 msgid "A new login is required since the authentication session expired." @@ -319,7 +319,7 @@ msgstr "ATU-C 系统供应商 ID" #: modules/luci-base/htdocs/luci-static/resources/tools/widgets.js:528 #: modules/luci-base/htdocs/luci-static/resources/tools/widgets.js:532 msgid "Absent Interface" -msgstr "" +msgstr "接口缺失" #: protocols/luci-proto-ppp/htdocs/luci-static/resources/protocol/pppoe.js:47 msgid "Access Concentrator" @@ -379,7 +379,7 @@ msgstr "添加" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:880 msgid "Add ATM Bridge" -msgstr "" +msgstr "添加 ATM 网桥" #: modules/luci-base/htdocs/luci-static/resources/protocol/static.js:92 msgid "Add IPv4 address…" @@ -391,15 +391,15 @@ msgstr "添加 IPv6 地址…" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/leds.js:47 msgid "Add LED action" -msgstr "" +msgstr "添加 LED 动作" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js:215 msgid "Add VLAN" -msgstr "" +msgstr "添加 VLAN" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/dropbear.js:14 msgid "Add instance" -msgstr "" +msgstr "添加实例" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js:153 #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js:159 @@ -418,7 +418,7 @@ msgstr "添加新接口…" #: protocols/luci-proto-wireguard/htdocs/luci-static/resources/protocol/wireguard.js:99 msgid "Add peer" -msgstr "" +msgstr "添加对等点" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js:105 msgid "Additional Hosts files" @@ -655,7 +655,7 @@ msgstr "任意区域" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:268 msgid "Apply backup?" -msgstr "" +msgstr "应用备份?" #: modules/luci-base/htdocs/luci-static/resources/ui.js:2522 msgid "Apply request failed with status <code>%h</code>" @@ -663,7 +663,7 @@ msgstr "应用请求失败,状态 <code>%h</code>" #: modules/luci-base/htdocs/luci-static/resources/ui.js:2409 msgid "Apply unchecked" -msgstr "" +msgstr "强制应用" #: modules/luci-mod-status/luasrc/view/admin_status/index/10-system.htm:19 msgid "Architecture" @@ -696,7 +696,7 @@ msgstr "关联数" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/mounts.js:178 msgid "Attempt to enable configured mount points for attached devices" -msgstr "" +msgstr "尝试为连接的设备启用已配置的挂载点" #: protocols/luci-proto-openconnect/htdocs/luci-static/resources/protocol/openconnect.js:101 #: protocols/luci-proto-vpnc/htdocs/luci-static/resources/protocol/vpnc.js:64 @@ -893,7 +893,7 @@ msgstr "开机自动运行" #: modules/luci-base/htdocs/luci-static/resources/ui.js:1697 #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:96 msgid "Browse…" -msgstr "" +msgstr "浏览…" #: modules/luci-mod-status/luasrc/view/admin_status/index/20-memory.htm:18 msgid "Buffered" @@ -905,7 +905,7 @@ msgstr "CA 证书,如果留空,则证书将在第一次连接后被保存。 #: protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/464xlat.js:7 msgid "CLAT configuration failed" -msgstr "" +msgstr "CLAT 配置失败" #: modules/luci-mod-status/luasrc/model/cbi/admin_status/processes.lua:13 msgid "CPU usage (%)" @@ -978,12 +978,12 @@ msgstr "选中此选项以从无线中删除现有网络。" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:259 msgid "Checking archive…" -msgstr "" +msgstr "正在检查归档…" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:340 #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:342 msgid "Checking image…" -msgstr "" +msgstr "正在检查镜像…" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:512 msgid "Choose mtdblock" @@ -1080,11 +1080,11 @@ msgstr "命令" #: modules/luci-base/htdocs/luci-static/resources/rpc.js:194 msgid "Command OK" -msgstr "" +msgstr "命令成功" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/startup.js:41 msgid "Command failed" -msgstr "" +msgstr "命令失败" #: modules/luci-mod-status/htdocs/luci-static/resources/view/status/iptables.js:64 msgid "Comment" @@ -1126,7 +1126,7 @@ msgstr "配置已回滚!" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:952 msgid "Confirm disconnect" -msgstr "" +msgstr "确认断开连接" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/password.js:50 msgid "Confirmation" @@ -1146,7 +1146,7 @@ msgstr "尝试连接失败" #: modules/luci-base/htdocs/luci-static/resources/rpc.js:204 msgid "Connection lost" -msgstr "" +msgstr "失去连接" #: modules/luci-mod-status/luasrc/controller/admin/status.lua:32 msgid "Connections" @@ -1156,13 +1156,13 @@ msgstr "连接" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:463 #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/startup.js:66 msgid "Contents have been saved." -msgstr "" +msgstr "内容已保存。" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:618 #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:281 #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:391 msgid "Continue" -msgstr "" +msgstr "继续" #: modules/luci-base/htdocs/luci-static/resources/ui.js:2431 msgid "" @@ -1188,7 +1188,7 @@ msgstr "创建/分配防火墙区域" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:739 msgid "Create interface" -msgstr "" +msgstr "创建接口" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/system.js:165 msgid "Critical" @@ -1200,7 +1200,7 @@ msgstr "Cron 日志级别" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:447 msgid "Current power" -msgstr "" +msgstr "当前功率" #: modules/luci-base/htdocs/luci-static/resources/tools/widgets.js:552 #: modules/luci-base/htdocs/luci-static/resources/tools/widgets.js:554 @@ -1399,11 +1399,11 @@ msgstr "删除密钥" #: modules/luci-base/htdocs/luci-static/resources/ui.js:1654 msgid "Delete permission denied" -msgstr "" +msgstr "删除没有权限" #: modules/luci-base/htdocs/luci-static/resources/ui.js:1656 msgid "Delete request failed: %d %s" -msgstr "" +msgstr "删除请求失败:%d %s" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:723 msgid "Delete this network" @@ -1419,7 +1419,7 @@ msgstr "描述" #: modules/luci-base/htdocs/luci-static/resources/ui.js:1754 msgid "Deselect" -msgstr "" +msgstr "取消" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/system.js:215 msgid "Design" @@ -1433,7 +1433,7 @@ msgstr "目标地址" #: modules/luci-base/htdocs/luci-static/resources/tools/widgets.js:46 #: modules/luci-base/htdocs/luci-static/resources/tools/widgets.js:151 msgid "Destination zone" -msgstr "" +msgstr "目标区域" #: modules/luci-base/htdocs/luci-static/resources/tools/widgets.js:54 #: modules/luci-base/htdocs/luci-static/resources/tools/widgets.js:177 @@ -1458,7 +1458,7 @@ msgstr "设备配置" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:81 msgid "Device is not active" -msgstr "" +msgstr "设备未激活" #: modules/luci-mod-system/luasrc/view/admin_system/reboot.htm:23 msgid "Device is rebooting..." @@ -1467,7 +1467,7 @@ msgstr "设备正在重启…" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:167 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:513 msgid "Device is restarting…" -msgstr "" +msgstr "设备正在重启…" #: modules/luci-base/htdocs/luci-static/resources/ui.js:2430 msgid "Device unreachable!" @@ -1589,7 +1589,7 @@ msgstr "不转发本地网络的反向查询" #: modules/luci-base/htdocs/luci-static/resources/ui.js:1639 msgid "Do you really want to delete \"%s\" ?" -msgstr "" +msgstr "您真的要删除“%s”吗?" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js:188 msgid "Do you really want to delete the following SSH key?" @@ -1597,11 +1597,11 @@ msgstr "您真的要删除以下 SSH 密钥吗?" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:241 msgid "Do you really want to erase all settings?" -msgstr "" +msgstr "您真的要清除所有设置吗?" #: modules/luci-base/htdocs/luci-static/resources/ui.js:1637 msgid "Do you really want to recursively delete the directory \"%s\" ?" -msgstr "" +msgstr "您真的要删除目录“%s”吗?" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js:74 msgid "Domain required" @@ -1640,7 +1640,7 @@ msgstr "下游 SNR 偏移" #: modules/luci-base/htdocs/luci-static/resources/form.js:1171 msgid "Drag to reorder" -msgstr "" +msgstr "拖动以重排" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/dropbear.js:11 msgid "Dropbear Instance" @@ -1704,7 +1704,7 @@ msgstr "编辑此网络" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:669 msgid "Edit wireless network" -msgstr "" +msgstr "编辑无线网络" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/system.js:167 msgid "Emergency" @@ -1817,7 +1817,7 @@ msgstr "在此桥接上启用生成树协议" #: protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/dslite.js:59 msgid "Encapsulation limit" -msgstr "" +msgstr "封装限制" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:853 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:911 @@ -1890,11 +1890,11 @@ msgstr "扩展 HOSTS 文件中的主机后缀" #: modules/luci-base/htdocs/luci-static/resources/protocol/static.js:195 msgid "Expecting an hexadecimal assignment hint" -msgstr "" +msgstr "这里需要一个十六进制值" #: modules/luci-base/htdocs/luci-static/resources/validation.js:59 msgid "Expecting: %s" -msgstr "" +msgstr "需要:%s" #: modules/luci-mod-status/htdocs/luci-static/resources/view/status/index.js:71 msgid "Expires" @@ -1947,7 +1947,7 @@ msgstr "FT 协议" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/password.js:82 msgid "Failed to change the system password." -msgstr "" +msgstr "更改系统密码失败。" #: modules/luci-base/htdocs/luci-static/resources/ui.js:2389 msgid "Failed to confirm apply within %ds, waiting for rollback…" @@ -1955,7 +1955,7 @@ msgstr "在 %d 秒内确认应用失败,等待回滚…" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/startup.js:45 msgid "Failed to execute \"/etc/init.d/%s %s\" action: %s" -msgstr "" +msgstr "执行“/etc/init.d/%s %s”失败:%s" #: modules/luci-base/htdocs/luci-static/resources/ui.js:1561 msgid "File" @@ -1963,11 +1963,11 @@ msgstr "文件" #: modules/luci-base/htdocs/luci-static/resources/ui.js:1514 msgid "File not accessible" -msgstr "" +msgstr "文件无法访问" #: modules/luci-base/htdocs/luci-static/resources/ui.js:1698 msgid "Filename" -msgstr "" +msgstr "文件名" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js:280 msgid "Filename of the boot image advertised to clients" @@ -2040,7 +2040,7 @@ msgstr "刷写固件…" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:406 msgid "Flash image?" -msgstr "" +msgstr "刷写固件?" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:525 msgid "Flash new firmware image" @@ -2053,7 +2053,7 @@ msgstr "刷新操作" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:414 #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:416 msgid "Flashing…" -msgstr "" +msgstr "正在刷写..." #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:550 msgid "Force" @@ -2129,6 +2129,8 @@ msgid "" "Further information about WireGuard interfaces and peers at <a href='http://" "wireguard.com'>wireguard.com</a>." msgstr "" +"关于 WireGuard 接口和对等点的更多信息请访问 <a href='http://wireguard." +"com'>wireguard.com</a>。" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:77 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:130 @@ -2681,11 +2683,11 @@ msgstr "接口配置" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:103 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:99 msgid "Interface has %d pending changes" -msgstr "" +msgstr "接口有 %d 个未应用的更改" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:57 msgid "Interface is marked for deletion" -msgstr "" +msgstr "接口被标记为删除" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:162 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/network.js:3 @@ -2698,11 +2700,11 @@ msgstr "正在关闭接口..." #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:205 msgid "Interface is starting..." -msgstr "" +msgstr "正在启动接口…" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:208 msgid "Interface is stopping..." -msgstr "" +msgstr "正在停止接口…" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:954 msgid "Interface name" @@ -2736,7 +2738,7 @@ msgstr "无效" #: protocols/luci-proto-wireguard/htdocs/luci-static/resources/protocol/wireguard.js:10 msgid "Invalid Base64 key string" -msgstr "" +msgstr "无效的 Base64 密钥" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js:281 msgid "Invalid VLAN ID given! Only IDs between %d and %d are allowed." @@ -2748,15 +2750,15 @@ msgstr "无效的 VLAN ID!只允许唯一的 ID" #: modules/luci-base/htdocs/luci-static/resources/rpc.js:196 msgid "Invalid argument" -msgstr "" +msgstr "无效参数" #: modules/luci-base/htdocs/luci-static/resources/rpc.js:195 msgid "Invalid command" -msgstr "" +msgstr "无效命令" #: protocols/luci-proto-wireguard/htdocs/luci-static/resources/protocol/wireguard.js:80 msgid "Invalid hexadecimal value" -msgstr "" +msgstr "无效 16 进制值" #: modules/luci-base/luasrc/view/sysauth.htm:12 msgid "Invalid username and/or password! Please try again." @@ -3013,12 +3015,12 @@ msgstr "加载中" #: modules/luci-base/htdocs/luci-static/resources/ui.js:1819 msgid "Loading directory contents…" -msgstr "" +msgstr "正在载入目录内容…" #: modules/luci-base/htdocs/luci-static/resources/luci.js:1308 #: modules/luci-base/luasrc/view/view.htm:4 msgid "Loading view…" -msgstr "" +msgstr "正在载入视图…" #: modules/luci-base/htdocs/luci-static/resources/network.js:10 #: modules/luci-base/luasrc/model/network.lua:30 @@ -3188,7 +3190,7 @@ msgstr "手动" #: modules/luci-base/htdocs/luci-static/resources/network.js:2191 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:594 msgid "Master" -msgstr "" +msgstr "主" #: modules/luci-mod-status/htdocs/luci-static/resources/view/status/index.js:122 msgid "Max. Attainable Data Rate (ATTNDR)" @@ -3222,7 +3224,7 @@ msgstr "最大地址分配数量。" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:761 msgid "Maximum transmit power" -msgstr "" +msgstr "最大传输功率" #: modules/luci-base/luasrc/view/wifi_assoclist.htm:21 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:78 @@ -3247,7 +3249,7 @@ msgstr "内存使用率(%)" #: modules/luci-base/htdocs/luci-static/resources/network.js:2194 msgid "Mesh" -msgstr "" +msgstr "Mesh" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:806 msgid "Mesh Id" @@ -3255,7 +3257,7 @@ msgstr "Mesh ID" #: modules/luci-base/htdocs/luci-static/resources/rpc.js:197 msgid "Method not found" -msgstr "" +msgstr "方法未找到" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/routes.js:45 #: modules/luci-mod-status/luasrc/view/admin_status/routes.htm:76 @@ -3323,7 +3325,7 @@ msgstr "需要更多字符" #: modules/luci-base/htdocs/luci-static/resources/form.js:1057 msgid "More…" -msgstr "" +msgstr "更多…" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/mounts.js:216 msgid "Mount Point" @@ -3351,7 +3353,7 @@ msgstr "配置存储设备挂载到文件系统中的位置和参数" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/mounts.js:178 msgid "Mount attached devices" -msgstr "" +msgstr "挂载已连接的设备" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/mounts.js:186 msgid "Mount filesystems not specifically configured" @@ -3456,7 +3458,7 @@ msgstr "无接口的网络。" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:665 msgid "New interface name…" -msgstr "" +msgstr "新接口名称…" #: modules/luci-base/luasrc/view/cbi/delegator.htm:11 msgid "Next »" @@ -3473,7 +3475,7 @@ msgstr "本接口未配置 DHCP 服务器" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1162 msgid "No Encryption" -msgstr "" +msgstr "无加密" #: protocols/luci-proto-vpnc/htdocs/luci-static/resources/protocol/vpnc.js:89 msgid "No NAT-T" @@ -3481,11 +3483,11 @@ msgstr "无 NAT-T" #: modules/luci-base/htdocs/luci-static/resources/rpc.js:199 msgid "No data received" -msgstr "" +msgstr "没有接收到数据" #: modules/luci-base/htdocs/luci-static/resources/ui.js:1764 msgid "No entries in this directory" -msgstr "" +msgstr "此目录中没有内容" #: modules/luci-mod-system/luasrc/model/cbi/admin_system/backupfiles.lua:82 msgid "No files found" @@ -3516,7 +3518,7 @@ msgstr "未设置密码!" #: protocols/luci-proto-wireguard/htdocs/luci-static/resources/protocol/wireguard.js:104 msgid "No peers defined yet" -msgstr "" +msgstr "尚未定义对等点" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js:129 #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js:271 @@ -3529,7 +3531,7 @@ msgstr "本链没有规则" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:53 msgid "No signal" -msgstr "" +msgstr "无信号" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:145 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:766 @@ -3583,7 +3585,7 @@ msgstr "未连接" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:139 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:232 msgid "Not present" -msgstr "" +msgstr "不存在" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:94 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/network.js:105 @@ -3592,7 +3594,7 @@ msgstr "开机时不启动" #: modules/luci-base/htdocs/luci-static/resources/rpc.js:202 msgid "Not supported" -msgstr "" +msgstr "不支持" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/system.js:162 msgid "Notice" @@ -3635,7 +3637,7 @@ msgstr "关闭时间" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/routes.js:95 msgid "On-Link route" -msgstr "" +msgstr "On-Link 路由" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/leds.js:64 msgid "On-State Delay" @@ -3647,7 +3649,7 @@ msgstr "请指定主机名或MAC地址!" #: modules/luci-base/htdocs/luci-static/resources/validation.js:462 msgid "One of the following: %s" -msgstr "" +msgstr "可选值:%s" #: modules/luci-base/luasrc/view/cbi/nullsection.htm:17 #: modules/luci-base/luasrc/view/cbi/ucisection.htm:22 @@ -3771,7 +3773,7 @@ msgstr "网络出口" #: modules/luci-base/htdocs/luci-static/resources/tools/widgets.js:46 #: modules/luci-base/htdocs/luci-static/resources/tools/widgets.js:151 msgid "Output zone" -msgstr "" +msgstr "出口区域" #: modules/luci-base/htdocs/luci-static/resources/protocol/dhcp.js:54 #: modules/luci-base/htdocs/luci-static/resources/protocol/static.js:219 @@ -3827,7 +3829,7 @@ msgstr "总览" #: modules/luci-base/htdocs/luci-static/resources/ui.js:1606 msgid "Overwrite existing file \"%s\" ?" -msgstr "" +msgstr "覆盖已存在的文件“%s”吗?" #: modules/luci-mod-status/luasrc/model/cbi/admin_status/processes.lua:11 msgid "Owner" @@ -4024,7 +4026,7 @@ msgstr "执行重置" #: modules/luci-base/htdocs/luci-static/resources/rpc.js:200 msgid "Permission denied" -msgstr "" +msgstr "没有权限" #: protocols/luci-proto-wireguard/htdocs/luci-static/resources/protocol/wireguard.js:136 msgid "Persistent Keep Alive" @@ -4064,7 +4066,7 @@ msgstr "请输入用户名和密码。" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:81 msgid "Please select the file to upload." -msgstr "" +msgstr "请选择要上传的文件。" #: modules/luci-mod-status/htdocs/luci-static/resources/view/status/iptables.js:45 msgid "Policy" @@ -4076,7 +4078,7 @@ msgstr "端口" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/leds.js:133 msgid "Port %s" -msgstr "" +msgstr "端口 %s" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js:274 msgid "Port status:" @@ -4236,7 +4238,7 @@ msgstr "接收速率" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1966 msgid "RX Rate / TX Rate" -msgstr "" +msgstr "接收速率/发送速率" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1207 msgid "Radius-Accounting-Port" @@ -4326,7 +4328,7 @@ msgstr "正在重启…" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:301 #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:310 msgid "Rebooting…" -msgstr "" +msgstr "正在重启…" #: modules/luci-mod-system/luasrc/view/admin_system/reboot.htm:11 msgid "Reboots the operating system of your device" @@ -4394,7 +4396,7 @@ msgstr "请求指定长度的 IPv6 前缀" #: modules/luci-base/htdocs/luci-static/resources/rpc.js:201 msgid "Request timeout" -msgstr "" +msgstr "请求超时" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1536 msgid "Required" @@ -4425,21 +4427,21 @@ msgstr "" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1096 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1097 msgid "Requires hostapd" -msgstr "" +msgstr "需要 hostapd" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1100 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1101 msgid "Requires hostapd with EAP support" -msgstr "" +msgstr "需要带 EAP 支持的 hostapd" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1102 msgid "Requires hostapd with OWE support" -msgstr "" +msgstr "需要带 OWE 支持的 hostapd" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1098 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1099 msgid "Requires hostapd with SAE support" -msgstr "" +msgstr "需要带 SAE 支持的 hostapd" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1533 msgid "" @@ -4462,22 +4464,22 @@ msgstr "需要上级支持 DNSSEC,验证未签名的响应确实是来自未 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1120 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1121 msgid "Requires wpa-supplicant" -msgstr "" +msgstr "需要 wpa-supplicant" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1112 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1113 msgid "Requires wpa-supplicant with EAP support" -msgstr "" +msgstr "需要带 EAP 支持的 wpa-supplicant" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1114 msgid "Requires wpa-supplicant with OWE support" -msgstr "" +msgstr "需要带 OWE 支持的 wpa-supplicant" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1110 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1111 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1124 msgid "Requires wpa-supplicant with SAE support" -msgstr "" +msgstr "需要带 SAE 支持的 wpa-supplicant" #: modules/luci-base/htdocs/luci-static/resources/luci.js:1367 #: modules/luci-base/luasrc/view/cbi/delegator.htm:17 @@ -4505,7 +4507,7 @@ msgstr "解析文件" #: modules/luci-base/htdocs/luci-static/resources/rpc.js:198 msgid "Resource not found" -msgstr "" +msgstr "未找到资源" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:302 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:693 @@ -4565,7 +4567,7 @@ msgstr "路由允许的 IP" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/routes.js:72 msgid "Route table" -msgstr "" +msgstr "路由表" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/routes.js:59 msgid "Route type" @@ -4606,7 +4608,7 @@ msgstr "文件系统检查" #: modules/luci-base/htdocs/luci-static/resources/luci.js:673 msgid "Runtime error" -msgstr "" +msgstr "运行时错误" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:360 msgid "SHA256" @@ -4707,7 +4709,7 @@ msgstr "" #: modules/luci-base/htdocs/luci-static/resources/ui.js:1646 #: modules/luci-base/htdocs/luci-static/resources/ui.js:1809 msgid "Select file…" -msgstr "" +msgstr "选择文件…" #: protocols/luci-proto-3g/htdocs/luci-static/resources/protocol/3g.js:144 #: protocols/luci-proto-ppp/htdocs/luci-static/resources/protocol/ppp.js:128 @@ -4755,7 +4757,7 @@ msgstr "" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:594 msgid "Set this interface as master for the dhcpv6 relay." -msgstr "" +msgstr "将此接口设置为 dhcpv6 中继的主接口。" #: protocols/luci-proto-qmi/htdocs/luci-static/resources/protocol/qmi.js:23 #: protocols/luci-proto-qmi/luasrc/model/network/proto_qmi.lua:55 @@ -4808,7 +4810,7 @@ msgstr "信号" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1965 msgid "Signal / Noise" -msgstr "" +msgstr "信号/噪声" #: modules/luci-mod-status/htdocs/luci-static/resources/view/status/index.js:125 msgid "Signal Attenuation (SATN)" @@ -4877,7 +4879,7 @@ msgstr "源地址" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/routes.js:83 msgid "Source Address" -msgstr "" +msgstr "源地址" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/mounts.js:315 msgid "Specifies the directory the device is attached to" @@ -4901,6 +4903,8 @@ msgid "" "on regulatory requirements and wireless usage, the actual transmit power may " "be reduced by the driver." msgstr "" +"指定最大发射功率。依据监管要求和使用情况,驱动程序可能将实际发射功率限定在此" +"值以下" #: protocols/luci-proto-ipip/htdocs/luci-static/resources/protocol/ipip.js:63 msgid "Specify a TOS (Type of Service)." @@ -5053,7 +5057,7 @@ msgstr "切换到 CIDR 列表记法" #: modules/luci-base/htdocs/luci-static/resources/ui.js:1547 msgid "Symbolic link" -msgstr "" +msgstr "符号链接" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/system.js:71 msgid "Sync with NTP-Server" @@ -5131,7 +5135,7 @@ msgstr "关闭" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/mounts.js:120 msgid "The <em>block mount</em> command failed with code %d" -msgstr "" +msgstr "<em>block mount</em> 命令失败,代码 %d" #: protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/6in4.js:77 msgid "" @@ -5172,6 +5176,9 @@ msgid "" "or revert all pending changes to keep the currently working configuration " "state." msgstr "" +"应用更改后 %d 秒内无法访问该设备,稳妥起见,该配置被回滚。如果您仍然认为更改" +"的配置是正确的,请强制应用。或者您可以消除此警告并在更改配置后尝试再次应用," +"或者还原所有未应用的更改以保持当前工作的配置状态。" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/mounts.js:303 #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/mounts.js:415 @@ -5184,11 +5191,11 @@ msgstr "存储器或分区的设备文件(例如:<code>/dev/sda1</code>)" msgid "" "The existing wireless configuration needs to be changed for LuCI to function " "properly." -msgstr "" +msgstr "为了使 LuCI 正常运行,需要更改现有的无线配置。" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:246 msgid "The firstboot command failed with code %d" -msgstr "" +msgstr "firstboot 命令失败,代码 %d" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:356 msgid "" @@ -5196,6 +5203,8 @@ msgid "" "compare them with the original file to ensure data integrity. <br /> Click " "\"Proceed\" below to start the flash procedure." msgstr "" +"刷写镜像已上传。下面是列出的校验和及文件大小,将它们与原始文件进行比较以确保" +"数据完整性。<br />单击下面的“继续”开始刷写。" #: modules/luci-mod-status/luasrc/view/admin_status/routes.htm:38 msgid "The following rules are currently active on this system." @@ -5203,7 +5212,7 @@ msgstr "以下规则当前在系统中处于活动状态。" #: modules/luci-base/htdocs/luci-static/resources/protocol/static.js:154 msgid "The gateway address must not be a local IP address" -msgstr "" +msgstr "网关地址不能是本地 IP 地址" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js:154 msgid "The given SSH public key has already been added." @@ -5217,11 +5226,11 @@ msgstr "给定的 SSH 公钥无效。请提供适当的公共 RSA 或 ECDSA 密 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:668 msgid "The interface name is already used" -msgstr "" +msgstr "接口名称已被使用" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:674 msgid "The interface name is too long" -msgstr "" +msgstr "接口名称过长" #: protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/6rd.js:61 #: protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/map.js:55 @@ -5241,7 +5250,7 @@ msgstr "所创建隧道的本地 IPv4 地址(可选)。" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1819 msgid "The network name is already used" -msgstr "" +msgstr "网络名称已被使用" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js:135 msgid "" @@ -5259,15 +5268,15 @@ msgstr "" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:306 msgid "The reboot command failed with code %d" -msgstr "" +msgstr "reboot 命令失败,代码 %d" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:295 msgid "The restore command failed with code %d" -msgstr "" +msgstr "restore 命令失败,代码 %d" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1149 msgid "The selected %s mode is incompatible with %s encryption" -msgstr "" +msgstr "模式 %s 与 %s 加密方法不兼容" #: modules/luci-base/luasrc/view/csrftoken.htm:11 msgid "The submitted security token is invalid or already expired!" @@ -5294,6 +5303,8 @@ msgid "" "The system is rebooting now. If the restored configuration changed the " "current LAN IP address, you might need to reconnect manually." msgstr "" +"系统正在重启。如果还原的配置更改了当前 LAN 的 IP 地址,则可能需要手动重新连" +"接。" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/password.js:80 msgid "The system password has been successfully changed." @@ -5301,7 +5312,7 @@ msgstr "系统密码已更改成功。" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:440 msgid "The sysupgrade command failed with code %d" -msgstr "" +msgstr "sysupgrade 命令失败,代码 %d" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:269 msgid "" @@ -5309,10 +5320,12 @@ msgid "" "listed below. Press \"Continue\" to restore the backup and reboot, or " "\"Cancel\" to abort the operation." msgstr "" +"上传的备份归档有效,并且包含以下列出的文件。点击“继续”恢复备份并重新启动,或" +"点击“取消”中止操作。" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:264 msgid "The uploaded backup archive is not readable" -msgstr "" +msgstr "无法读取上传的备份归档" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:377 msgid "" @@ -5322,7 +5335,7 @@ msgstr "不支持所上传的映像文件格式,请选择适合当前平台的 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js:427 msgid "There are no active leases" -msgstr "" +msgstr "没有已分配的租约" #: modules/luci-base/luasrc/view/lease_status.htm:29 #: modules/luci-base/luasrc/view/lease_status.htm:61 @@ -5331,7 +5344,7 @@ msgstr "没有已分配的租约。" #: modules/luci-base/htdocs/luci-static/resources/ui.js:2514 msgid "There are no changes to apply" -msgstr "" +msgstr "没有待应用的更改" #: themes/luci-theme-bootstrap/luasrc/view/themes/bootstrap/header.htm:174 #: themes/luci-theme-material/luasrc/view/themes/material/header.htm:212 @@ -5348,11 +5361,11 @@ msgstr "中继的 IPv4 地址" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1448 msgid "This authentication type is not applicable to the selected EAP method." -msgstr "" +msgstr "此身份验证类型不适用于所选的 EAP 方法。" #: protocols/luci-proto-openconnect/htdocs/luci-static/resources/protocol/openconnect.js:54 msgid "This does not look like a valid PEM file" -msgstr "" +msgstr "这不是有效的 PEM 文件" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js:163 msgid "" @@ -5360,6 +5373,8 @@ msgid "" "'server=1.2.3.4' for domain-specific or full upstream <abbr title=\"Domain " "Name System\">DNS</abbr> servers." msgstr "" +"此文件可能包含格式如“server=/domain/1.2.3.4”或“server=1.2.3.4”之类的行。前者" +"为特定的域指定 DNS 服务器,后者则不限定服务器的解析范围。" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:543 #: modules/luci-mod-system/luasrc/model/cbi/admin_system/backupfiles.lua:16 @@ -5574,7 +5589,7 @@ msgstr "无法获取客户端 ID" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/mounts.js:244 msgid "Unable to obtain mount information" -msgstr "" +msgstr "无法取得挂载信息" #: protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/dslite.js:7 #: protocols/luci-proto-ipv6/luasrc/model/network/proto_4x6.lua:61 @@ -5590,7 +5605,7 @@ msgstr "无法解析 Pear 主机名" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:465 #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/startup.js:68 msgid "Unable to save contents: %s" -msgstr "" +msgstr "无法保存内容:%s" #: modules/luci-mod-status/htdocs/luci-static/resources/view/status/index.js:132 msgid "Unavailable Seconds (UAS)" @@ -5608,7 +5623,7 @@ msgstr "未知错误(%s)" #: modules/luci-base/htdocs/luci-static/resources/rpc.js:205 msgid "Unknown error code" -msgstr "" +msgstr "未知错误代码" #: modules/luci-base/htdocs/luci-static/resources/network.js:1386 #: modules/luci-base/htdocs/luci-static/resources/protocol/none.js:6 @@ -5632,7 +5647,7 @@ msgstr "未保存的配置" #: modules/luci-base/htdocs/luci-static/resources/rpc.js:203 msgid "Unspecified error" -msgstr "" +msgstr "未指定的错误" #: protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/map.js:9 #: protocols/luci-proto-ipv6/luasrc/model/network/proto_4x6.lua:64 @@ -5655,7 +5670,7 @@ msgstr "上移" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:153 msgid "Upload" -msgstr "" +msgstr "上传" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:527 msgid "" @@ -5674,21 +5689,21 @@ msgstr "上传备份…" #: modules/luci-base/htdocs/luci-static/resources/ui.js:1703 msgid "Upload file" -msgstr "" +msgstr "上传文件" #: modules/luci-base/htdocs/luci-static/resources/ui.js:1678 msgid "Upload file…" -msgstr "" +msgstr "上传文件…" #: modules/luci-base/htdocs/luci-static/resources/ui.js:1623 #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:142 msgid "Upload request failed: %s" -msgstr "" +msgstr "上传请求失败:%s" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:80 #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:118 msgid "Uploading file…" -msgstr "" +msgstr "正在上传文件…" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:613 msgid "" @@ -5696,6 +5711,8 @@ msgid "" "assigned with a name in the form <em>wifinet#</em> and the network will be " "restarted to apply the updated configuration." msgstr "" +"点击“继续”后,将为匿名的“wifi-iface”段分配一个名称,格式为 <em>wifinet#</" +"em>,并且网络将重新启动以应用更新的配置。" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/iface_status.js:14 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:74 @@ -5836,8 +5853,8 @@ msgid "" "Used for two different purposes: RADIUS NAS ID and 802.11r R0KH-ID. Not " "needed with normal WPA(2)-PSK/WPA3-SAE." msgstr "" -"用于两种不同的用途:RADIUS NAS ID 和 802.11r R0KH-ID,普通 WPA(2)-PSK/WPA3-SAE 不需" -"要。" +"用于两种不同的用途:RADIUS NAS ID 和 802.11r R0KH-ID,普通 WPA(2)-PSK/WPA3-" +"SAE 不需要。" #: protocols/luci-proto-openconnect/htdocs/luci-static/resources/protocol/openconnect.js:110 msgid "User certificate (PEM encoded)" @@ -5867,7 +5884,7 @@ msgstr "%q 上的 VLAN" #: modules/luci-base/luasrc/controller/admin/index.lua:55 msgid "VPN" -msgstr "" +msgstr "VPN" #: protocols/luci-proto-vpnc/htdocs/luci-static/resources/protocol/vpnc.js:42 msgid "VPN Local address" @@ -5906,7 +5923,7 @@ msgstr "请求 DHCP 时发送的 Vendor Class 选项" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js:343 msgid "Verifying the uploaded image file." -msgstr "" +msgstr "正在验证上传的镜像文件。" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:52 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/network.js:76 @@ -5956,7 +5973,7 @@ msgstr "等待命令执行完成…" #: modules/luci-base/htdocs/luci-static/resources/ui.js:2481 msgid "Waiting for configuration to get applied… %ds" -msgstr "" +msgstr "正在等待配置被应用… %ds" #: modules/luci-mod-system/luasrc/view/admin_system/reboot.htm:56 msgid "Waiting for device..." @@ -6022,7 +6039,7 @@ msgstr "无线安全" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:611 msgid "Wireless configuration migration" -msgstr "" +msgstr "无线配置迁移" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:101 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:139 @@ -6061,7 +6078,7 @@ msgstr "是" msgid "" "You appear to be currently connected to the device via the \"%h\" interface. " "Do you really want to shut down the interface?" -msgstr "" +msgstr "您似乎正通过“%h”连接到此设备,确认要关闭它吗?" #: modules/luci-mod-system/htdocs/luci-static/resources/view/system/startup.js:123 msgid "" @@ -6113,7 +6130,7 @@ msgstr "自动" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/routes.js:84 msgid "automatic" -msgstr "" +msgstr "自动" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js:78 msgid "baseT" @@ -6176,7 +6193,7 @@ msgstr "已禁用" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:433 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:467 msgid "driver default" -msgstr "" +msgstr "驱动默认" #: modules/luci-base/luasrc/view/lease_status.htm:17 #: modules/luci-base/luasrc/view/lease_status.htm:46 @@ -6222,7 +6239,7 @@ msgstr "如果对象是一个网络" #: protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/dslite.js:63 msgid "ignore" -msgstr "" +msgstr "忽略" #: modules/luci-base/htdocs/luci-static/resources/tools/widgets.js:56 #: modules/luci-base/htdocs/luci-static/resources/tools/widgets.js:177 @@ -6264,7 +6281,7 @@ msgstr "本地 <abbr title=\"Domain Name System\">DNS</abbr> 解析文件" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1168 msgid "medium security" -msgstr "" +msgstr "中等安全性" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1360 msgid "minutes" @@ -6312,7 +6329,7 @@ msgstr "开" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1169 msgid "open network" -msgstr "" +msgstr "开放网络" #: modules/luci-base/htdocs/luci-static/resources/tools/widgets.js:56 #: modules/luci-base/luasrc/view/cbi/firewall_zonelist.htm:46 @@ -6365,7 +6382,7 @@ msgstr "无状态 + 有状态" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1167 msgid "strong security" -msgstr "" +msgstr "强安全性" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js:346 msgid "tagged" @@ -6561,7 +6578,7 @@ msgstr "值小于或等于 %f" #: modules/luci-base/htdocs/luci-static/resources/validation.js:425 msgid "value with %d characters" -msgstr "" +msgstr "值有 %d 个字符" #: modules/luci-base/htdocs/luci-static/resources/validation.js:436 msgid "value with at least %d characters" @@ -6573,7 +6590,7 @@ msgstr "值至多为 %d 个字符" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:1169 msgid "weak security" -msgstr "" +msgstr "弱安全性" #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js:39 #: modules/luci-mod-network/htdocs/luci-static/resources/view/network/network.js:34 diff --git a/modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json b/modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json index 31c154cbcb..54caa74363 100644 --- a/modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json +++ b/modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json @@ -34,7 +34,6 @@ "/proc/mtd": [ "read" ], "/proc/partitions": [ "read" ], "/proc/sys/kernel/hostname": [ "read" ], - "/sys/devices/virtual/mtd/*/name": [ "read" ], "/sys/devices/virtual/ubi/*/name": [ "read" ] }, "ubus": { @@ -44,6 +43,7 @@ "network.device": [ "status" ], "network.interface": [ "dump" ], "network": [ "get_proto_handlers" ], + "system": [ "validate_firmware_image" ], "uci": [ "changes", "get" ] }, "uci": [ "*" ] diff --git a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js index 08c97650ea..2309c82682 100644 --- a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js +++ b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/flash.js @@ -2,7 +2,7 @@ 'require form'; 'require rpc'; -var callFileStat, callFileRead, callFileWrite, callFileExec, callFileRemove; +var callFileStat, callFileRead, callFileWrite, callFileExec, callFileRemove, callSystemValidateFirmwareImage; callFileStat = rpc.declare({ object: 'file', @@ -38,6 +38,13 @@ callFileRemove = rpc.declare({ params: [ 'path' ] }); +callSystemValidateFirmwareImage = rpc.declare({ + object: 'system', + method: 'validate_firmware_image', + params: [ 'path' ], + expect: { '': { valid: false, forcable: true } } +}); + function pingDevice(proto, ipaddr) { var target = '%s://%s%s?%s'.format(proto || 'http', ipaddr || window.location.host, L.resource('icons/loading.gif'), Math.random()); @@ -208,7 +215,7 @@ var mapdata = { actions: {}, config: {} }; return L.view.extend({ load: function() { - var max_mtd = 10, max_ubi = 2, max_ubi_vol = 4; + var max_ubi = 2, max_ubi_vol = 4; var tasks = [ callFileStat('/lib/upgrade/platform.sh'), callFileRead('/proc/sys/kernel/hostname'), @@ -216,9 +223,6 @@ return L.view.extend({ callFileRead('/proc/partitions') ]; - for (var i = 0; i < max_mtd; i++) - tasks.push(callFileRead('/sys/devices/virtual/mtd/mtd%d/name'.format(i))); - for (var i = 0; i < max_ubi; i++) for (var j = 0; j < max_ubi_vol; j++) tasks.push(callFileRead('/sys/devices/virtual/ubi/ubi%d/ubi%d_%d/name'.format(i, i, j))); @@ -345,13 +349,19 @@ return L.view.extend({ E('span', { 'class': 'spinning' }, _('Verifying the uploaded image file.')) ]); + return callSystemValidateFirmwareImage('/tmp/firmware.bin') + .then(function(res) { return [ reply, res ]; }); + }, this, ev.target)) + .then(L.bind(function(btn, reply) { return callFileExec('/sbin/sysupgrade', [ '--test', '/tmp/firmware.bin' ]) - .then(function(res) { return [ reply, res ] }); + .then(function(res) { reply.push(res); return reply; }); }, this, ev.target)) .then(L.bind(function(btn, res) { - var keep = document.querySelector('[data-name="keep"] input[type="checkbox"]'), + var keep = E('input', { type: 'checkbox' }), force = E('input', { type: 'checkbox' }), - is_invalid = (res[1].code != 0), + is_valid = res[1].valid, + is_forceable = res[1].forceable, + allow_backup = res[1].allow_backup, is_too_big = (storage_size > 0 && res[0].size > storage_size), body = []; @@ -359,11 +369,10 @@ return L.view.extend({ body.push(E('ul', {}, [ res[0].size ? E('li', {}, '%s: %1024.2mB'.format(_('Size'), res[0].size)) : '', res[0].checksum ? E('li', {}, '%s: %s'.format(_('MD5'), res[0].checksum)) : '', - res[0].sha256sum ? E('li', {}, '%s: %s'.format(_('SHA256'), res[0].sha256sum)) : '', - E('li', {}, keep.checked ? _('Configuration files will be kept') : _('Caution: Configuration files will be erased')) + res[0].sha256sum ? E('li', {}, '%s: %s'.format(_('SHA256'), res[0].sha256sum)) : '' ])); - if (is_invalid || is_too_big) + if (!is_valid || is_too_big) body.push(E('hr')); if (is_too_big) @@ -371,15 +380,27 @@ return L.view.extend({ _('It appears that you are trying to flash an image that does not fit into the flash memory, please verify the image file!') ])); - if (is_invalid) + if (!is_valid) body.push(E('p', { 'class': 'alert-message' }, [ - res[1].stderr ? res[1].stderr : '', - res[1].stderr ? E('br') : '', - res[1].stderr ? E('br') : '', + res[2].stderr ? res[2].stderr : '', + res[2].stderr ? E('br') : '', + res[2].stderr ? E('br') : '', _('The uploaded image file does not contain a supported format. Make sure that you choose the generic image format for your platform.') ])); - if (is_invalid || is_too_big) + if (!allow_backup) + body.push(E('p', { 'class': 'alert-message' }, [ + _('The uploaded firmware does not allow keeping current configuration.') + ])); + if (allow_backup) + keep.checked = true; + else + keep.disabled = true; + body.push(E('p', {}, E('label', { 'class': 'btn' }, [ + keep, ' ', _('Keep settings and retain the current configuration') + ]))); + + if ((!is_valid || is_too_big) && is_forceable) body.push(E('p', {}, E('label', { 'class': 'btn alert-message danger' }, [ force, ' ', _('Force upgrade'), E('br'), E('br'), @@ -389,7 +410,7 @@ return L.view.extend({ var cntbtn = E('button', { 'class': 'btn cbi-button-action important', 'click': L.ui.createHandlerFn(this, 'handleSysupgradeConfirm', btn, keep.checked, force.checked), - 'disabled': (is_invalid || is_too_big) ? true : null + 'disabled': (!is_valid || is_too_big) ? true : null }, [ _('Continue') ]); body.push(E('div', { 'class': 'right' }, [ @@ -473,7 +494,7 @@ return L.view.extend({ hostname = rpc_replies[1], procmtd = rpc_replies[2], procpart = rpc_replies[3], - has_rootfs_data = rpc_replies.slice(4).filter(function(n) { return n == 'rootfs_data' })[0], + has_rootfs_data = (procmtd.match(/"rootfs_data"/) != null) || rpc_replies.slice(4).filter(function(n) { return n == 'rootfs_data' })[0], storage_size = findStorageSize(procmtd, procpart), m, s, o, ss; @@ -528,15 +549,12 @@ return L.view.extend({ o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Flash new firmware image'), has_sysupgrade - ? _('Upload a sysupgrade-compatible image here to replace the running firmware. Check "Keep settings" to retain the current configuration (requires a compatible firmware image).') + ? _('Upload a sysupgrade-compatible image here to replace the running firmware.') : _('Sorry, there is no sysupgrade support present; a new firmware image must be flashed manually. Please refer to the wiki for device specific install instructions.')); ss = o.subsection; if (has_sysupgrade) { - o = ss.option(form.Flag, 'keep', _('Keep settings')); - o.default = o.enabled; - o = ss.option(form.Button, 'sysupgrade', _('Image')); o.inputstyle = 'action important'; o.inputtitle = _('Flash image...'); |