X-Git-Url: http://git.openwrt.org/?a=blobdiff_plain;f=modules%2Fluci-base%2Fhtdocs%2Fluci-static%2Fresources%2Frpc.js;h=f37f7bb6a4e8f432d1ddaccaa7d86cb3f2485329;hb=ddf6357cdae574b5e88213fa6af0a96f695e4283;hp=cc22d0aeb4f1f4bc7ad6c467611dbe2e33bc14da;hpb=1dd910148eaf7b9ed7130d1a067465dd43940da3;p=project%2Fluci.git diff --git a/modules/luci-base/htdocs/luci-static/resources/rpc.js b/modules/luci-base/htdocs/luci-static/resources/rpc.js index cc22d0aeb4..f37f7bb6a4 100644 --- a/modules/luci-base/htdocs/luci-static/resources/rpc.js +++ b/modules/luci-base/htdocs/luci-static/resources/rpc.js @@ -1,117 +1,145 @@ 'use strict'; - -var rpcRequestRegistry = {}, - rpcRequestBatch = null, - rpcRequestID = 1, - rpcSessionID = L.env.sessionid || '00000000000000000000000000000000'; - -return L.Class.extend({ - call: function(req, cbFn) { - var cb = cbFn.bind(this, req), - q = ''; +'require baseclass'; +'require request'; + +var rpcRequestID = 1, + rpcSessionID = L.env.sessionid || '00000000000000000000000000000000', + rpcBaseURL = L.url('admin/ubus'), + rpcInterceptorFns = []; + +/** + * @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 baseclass.extend(/** @lends LuCI.rpc.prototype */ { + /* privates */ + call: function(req, cb, nobatch) { + var q = ''; if (Array.isArray(req)) { if (req.length == 0) return Promise.resolve([]); for (var i = 0; i < req.length; i++) - q += '%s%s.%s'.format( - q ? ';' : '/', - req[i].params[1], - req[i].params[2] - ); - } - else { - q += '/%s.%s'.format(req.params[1], req.params[2]); + if (req[i].params) + q += '%s%s.%s'.format( + q ? ';' : '/', + req[i].params[1], + req[i].params[2] + ); } - return L.Request.post(L.url('admin/ubus') + q, req, { - timeout: (L.env.rpctimeout || 5) * 1000, + return request.post(rpcBaseURL + q, req, { + timeout: (L.env.rpctimeout || 20) * 1000, + nobatch: nobatch, credentials: true - }).then(cb); + }).then(cb, cb); }, - handleListReply: function(req, msg) { - var list = msg.result; + parseCallReply: function(req, res) { + var msg = null; - /* verify message frame */ - if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id || !Array.isArray(list)) - list = [ ]; + if (res instanceof Error) + return req.reject(res); - req.resolve(list); - }, - - handleCallReply: function(reqs, res) { - var type = Object.prototype.toString, - data = [], - msg = null; + try { + if (!res.ok) + L.raise('RPCError', 'RPC call to %s/%s failed with HTTP error %d: %s', + req.object, req.method, res.status, res.statusText || '?'); - if (!res.ok) - L.error('RPCError', 'RPC call failed with HTTP error %d: %s', - res.status, res.statusText || '?'); - - msg = res.json(); - - if (!Array.isArray(reqs)) { - msg = [ msg ]; - reqs = [ reqs ]; + msg = res.json(); + } + catch (e) { + return req.reject(e); } - for (var i = 0; i < msg.length; i++) { - /* fetch related request info */ - var req = rpcRequestRegistry[reqs[i].id]; - if (typeof(req) != 'object') - throw 'No related request for JSON response'; + /* + * The interceptor args are intentionally swapped. + * Response is passed as first arg to align with Request class interceptors + */ + Promise.all(rpcInterceptorFns.map(function(fn) { return fn(msg, req) })) + .then(this.handleCallReply.bind(this, req, msg)) + .catch(req.reject); + }, - /* fetch response attribute and verify returned type */ - var ret = undefined; + handleCallReply: function(req, msg) { + var type = Object.prototype.toString, + ret = null; + try { /* verify message frame */ - if (typeof(msg[i]) == 'object' && msg[i].jsonrpc == '2.0') { - if (typeof(msg[i].error) == 'object' && msg[i].error.code && msg[i].error.message) - req.reject(new Error('RPC call failed with error %d: %s' - .format(msg[i].error.code, msg[i].error.message || '?'))); - else if (Array.isArray(msg[i].result) && msg[i].result[0] == 0) - ret = (msg[i].result.length > 1) ? msg[i].result[1] : msg[i].result[0]; - } - else { - req.reject(new Error('Invalid message frame received')); - } - - if (req.expect) { - for (var key in req.expect) { - if (ret != null && key != '') - ret = ret[key]; + if (!L.isObject(msg) || msg.jsonrpc != '2.0') + L.raise('RPCError', 'RPC call to %s/%s returned invalid message frame', + req.object, req.method); + + /* check error condition */ + if (L.isObject(msg.error) && msg.error.code && msg.error.message) + L.raise('RPCError', 'RPC call to %s/%s failed with error %d: %s', + req.object, req.method, msg.error.code, msg.error.message || '?'); + } + catch (e) { + return req.reject(e); + } - if (ret == null || type.call(ret) != type.call(req.expect[key])) - ret = req.expect[key]; + if (!req.object && !req.method) { + ret = msg.result; + } + else if (Array.isArray(msg.result)) { + if (req.raise && msg.result[0] !== 0) + L.raise('RPCError', 'RPC call to %s/%s failed with ubus code %d: %s', + req.object, req.method, msg.result[0], this.getStatusText(msg.result[0])); - break; - } - } + ret = (msg.result.length > 1) ? msg.result[1] : msg.result[0]; + } - /* apply filter */ - if (typeof(req.filter) == 'function') { - req.priv[0] = ret; - req.priv[1] = req.params; - ret = req.filter.apply(this, req.priv); - } + if (req.expect) { + for (var key in req.expect) { + if (ret != null && key != '') + ret = ret[key]; - req.resolve(ret); + if (ret == null || type.call(ret) != type.call(req.expect[key])) + ret = req.expect[key]; - /* store response data */ - if (typeof(req.index) == 'number') - data[req.index] = ret; - else - data = ret; + break; + } + } - /* delete request object */ - delete rpcRequestRegistry[reqs[i].id]; + /* apply filter */ + if (typeof(req.filter) == 'function') { + req.priv[0] = ret; + req.priv[1] = req.params; + ret = req.filter.apply(this, req.priv); } - return Promise.resolve(data); + 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|Object>>>} + * 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', @@ -120,25 +148,154 @@ return L.Class.extend({ params: arguments.length ? this.varargs(arguments) : undefined }; - return this.call(msg, this.handleListReply); - }, - - batch: function() { - if (!Array.isArray(rpcRequestBatch)) - rpcRequestBatch = [ ]; - }, - - flush: function() { - if (!Array.isArray(rpcRequestBatch)) - return Promise.resolve([]); + return new Promise(L.bind(function(resolveFn, rejectFn) { + /* store request info */ + var req = { + resolve: resolveFn, + reject: rejectFn + }; - var req = rpcRequestBatch; - rpcRequestBatch = null; - - /* call rpc */ - return this.call(req, this.handleCallReply); + /* call rpc */ + this.call(msg, this.parseCallReply.bind(this, req)); + }, 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} [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. + * + * @property {boolean} [reject=false] + * If set to `true`, non-zero ubus call status codes are treated as fatal + * error and lead to the rejection of the call promise. The default + * behaviour is to resolve with the call return code value instead. + */ + + /** + * 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); @@ -156,13 +313,16 @@ return L.Class.extend({ priv.push(args[p_off]); /* store request info */ - var req = rpcRequestRegistry[rpcRequestID] = { + var req = { expect: options.expect, filter: options.filter, resolve: resolveFn, reject: rejectFn, params: params, - priv: priv + priv: priv, + object: options.object, + method: options.method, + raise: options.reject }; /* build message object */ @@ -178,19 +338,148 @@ return L.Class.extend({ ] }; - /* when a batch is in progress then store index in request data - * and push message object onto the stack */ - if (Array.isArray(rpcRequestBatch)) - req.index = rpcRequestBatch.push(msg) - 1; - /* call rpc */ - else - rpc.call(msg, rpc.handleCallReply); + rpc.call(msg, rpc.parseCallReply.bind(rpc, req), options.nobatch); }); }, 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'); + case 1: return _('Invalid command'); + case 2: return _('Invalid argument'); + case 3: return _('Method not found'); + case 4: return _('Resource not found'); + case 5: return _('No data received'); + case 6: return _('Permission denied'); + case 7: return _('Request timeout'); + case 8: return _('Not supported'); + case 9: return _('Unspecified error'); + case 10: return _('Connection lost'); + default: return _('Unknown error code'); + } + }, + + /** + * 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--) + if (rpcInterceptorFns[i] === interceptorFn) + rpcInterceptorFns.splice(i, 1); + return (rpcInterceptorFns.length < oldlen); } });