From 1dd910148eaf7b9ed7130d1a067465dd43940da3 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Thu, 7 Feb 2019 18:53:25 +0100 Subject: [PATCH] luci-base: add uci.js and rpc.js classes Add a new rpc.js class which provides low level facilities to exchanges messages with the ubus rpc endpoint. Also introduce a new uci.js class which provides client side uci manipulation routines. Signed-off-by: Jo-Philipp Wich --- .../htdocs/luci-static/resources/rpc.js | 196 +++++++ .../htdocs/luci-static/resources/uci.js | 500 ++++++++++++++++++ 2 files changed, 696 insertions(+) create mode 100644 modules/luci-base/htdocs/luci-static/resources/rpc.js create mode 100644 modules/luci-base/htdocs/luci-static/resources/uci.js diff --git a/modules/luci-base/htdocs/luci-static/resources/rpc.js b/modules/luci-base/htdocs/luci-static/resources/rpc.js new file mode 100644 index 0000000000..cc22d0aeb4 --- /dev/null +++ b/modules/luci-base/htdocs/luci-static/resources/rpc.js @@ -0,0 +1,196 @@ +'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 = ''; + + 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]); + } + + return L.Request.post(L.url('admin/ubus') + q, req, { + timeout: (L.env.rpctimeout || 5) * 1000, + credentials: true + }).then(cb); + }, + + handleListReply: function(req, msg) { + var list = msg.result; + + /* verify message frame */ + if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id || !Array.isArray(list)) + list = [ ]; + + req.resolve(list); + }, + + handleCallReply: function(reqs, res) { + var type = Object.prototype.toString, + data = [], + msg = null; + + 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 ]; + } + + 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'; + + /* fetch response attribute and verify returned type */ + var ret = undefined; + + /* 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 (ret == null || type.call(ret) != type.call(req.expect[key])) + ret = req.expect[key]; + + break; + } + } + + /* apply filter */ + if (typeof(req.filter) == 'function') { + req.priv[0] = ret; + req.priv[1] = req.params; + ret = req.filter.apply(this, req.priv); + } + + req.resolve(ret); + + /* store response data */ + if (typeof(req.index) == 'number') + data[req.index] = ret; + else + data = ret; + + /* delete request object */ + delete rpcRequestRegistry[reqs[i].id]; + } + + return Promise.resolve(data); + }, + + list: function() { + var msg = { + jsonrpc: '2.0', + id: rpcRequestID++, + method: 'list', + 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([]); + + var req = rpcRequestBatch; + rpcRequestBatch = null; + + /* call rpc */ + return this.call(req, this.handleCallReply); + }, + + declare: function(options) { + return Function.prototype.bind.call(function(rpc, options) { + var args = this.varargs(arguments, 2); + return new Promise(function(resolveFn, rejectFn) { + /* build parameter object */ + var p_off = 0; + var params = { }; + if (Array.isArray(options.params)) + for (p_off = 0; p_off < options.params.length; p_off++) + params[options.params[p_off]] = args[p_off]; + + /* all remaining arguments are private args */ + var priv = [ undefined, undefined ]; + for (; p_off < args.length; p_off++) + priv.push(args[p_off]); + + /* store request info */ + var req = rpcRequestRegistry[rpcRequestID] = { + expect: options.expect, + filter: options.filter, + resolve: resolveFn, + reject: rejectFn, + params: params, + priv: priv + }; + + /* build message object */ + var msg = { + jsonrpc: '2.0', + id: rpcRequestID++, + method: 'call', + params: [ + rpcSessionID, + options.object, + options.method, + params + ] + }; + + /* 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); + }); + }, this, this, options); + }, + + setSessionID: function(sid) { + rpcSessionID = sid; + } +}); diff --git a/modules/luci-base/htdocs/luci-static/resources/uci.js b/modules/luci-base/htdocs/luci-static/resources/uci.js new file mode 100644 index 0000000000..fdb8c6ab48 --- /dev/null +++ b/modules/luci-base/htdocs/luci-static/resources/uci.js @@ -0,0 +1,500 @@ +'use strict'; +'require rpc'; + +return L.Class.extend({ + __init__: function() { + this.state = { + newidx: 0, + values: { }, + creates: { }, + changes: { }, + deletes: { }, + reorder: { } + }; + }, + + callLoad: rpc.declare({ + object: 'uci', + method: 'get', + params: [ 'config' ], + expect: { values: { } } + }), + + callOrder: rpc.declare({ + object: 'uci', + method: 'order', + params: [ 'config', 'sections' ] + }), + + callAdd: rpc.declare({ + object: 'uci', + method: 'add', + params: [ 'config', 'type', 'name', 'values' ], + expect: { section: '' } + }), + + callSet: rpc.declare({ + object: 'uci', + method: 'set', + params: [ 'config', 'section', 'values' ] + }), + + callDelete: rpc.declare({ + object: 'uci', + method: 'delete', + params: [ 'config', 'section', 'options' ] + }), + + callApply: rpc.declare({ + object: 'uci', + method: 'apply', + params: [ 'timeout', 'rollback' ] + }), + + callConfirm: rpc.declare({ + object: 'uci', + method: 'confirm' + }), + + createSID: function(conf) { + var v = this.state.values, + n = this.state.creates, + sid; + + do { + sid = "new%06x".format(Math.random() * 0xFFFFFF); + } while ((n[conf] && n[conf][sid]) || (v[conf] && v[conf][sid])); + + return sid; + }, + + reorderSections: function() { + var v = this.state.values, + n = this.state.creates, + r = this.state.reorder, + tasks = []; + + if (Object.keys(r).length === 0) + return Promise.resolve(); + + /* + gather all created and existing sections, sort them according + to their index value and issue an uci order call + */ + for (var c in r) { + var o = [ ]; + + if (n[c]) + for (var s in n[c]) + o.push(n[c][s]); + + for (var s in v[c]) + o.push(v[c][s]); + + if (o.length > 0) { + o.sort(function(a, b) { + return (a['.index'] - b['.index']); + }); + + var sids = [ ]; + + for (var i = 0; i < o.length; i++) + sids.push(o[i]['.name']); + + tasks.push(this.callOrder(c, sids)); + } + } + + this.state.reorder = { }; + return Promise.all(tasks); + }, + + load: function(packages) { + var self = this, + seen = { }, + pkgs = [ ], + tasks = []; + + if (!Array.isArray(packages)) + packages = [ packages ]; + + for (var i = 0; i < packages.length; i++) + if (!seen[packages[i]] && !self.state.values[packages[i]]) { + pkgs.push(packages[i]); + seen[packages[i]] = true; + tasks.push(self.callLoad(packages[i])); + } + + return Promise.all(tasks).then(function(responses) { + for (var i = 0; i < responses.length; i++) + self.state.values[pkgs[i]] = responses[i]; + + document.dispatchEvent(new CustomEvent('uci-loaded')); + + return pkgs; + }); + }, + + unload: function(packages) { + if (!Array.isArray(packages)) + packages = [ packages ]; + + for (var i = 0; i < packages.length; i++) { + delete this.state.values[packages[i]]; + delete this.state.creates[packages[i]]; + delete this.state.changes[packages[i]]; + delete this.state.deletes[packages[i]]; + } + }, + + add: function(conf, type, name) { + var n = this.state.creates, + sid = name || this.createSID(conf); + + if (!n[conf]) + n[conf] = { }; + + n[conf][sid] = { + '.type': type, + '.name': sid, + '.create': name, + '.anonymous': !name, + '.index': 1000 + this.state.newidx++ + }; + + return sid; + }, + + remove: function(conf, sid) { + var n = this.state.creates, + c = this.state.changes, + d = this.state.deletes; + + /* requested deletion of a just created section */ + if (n[conf] && n[conf][sid]) { + delete n[conf][sid]; + } + else { + if (c[conf]) + delete c[conf][sid]; + + if (!d[conf]) + d[conf] = { }; + + d[conf][sid] = true; + } + }, + + sections: function(conf, type, cb) { + var sa = [ ], + v = this.state.values[conf], + n = this.state.creates[conf], + c = this.state.changes[conf], + d = this.state.deletes[conf]; + + if (!v) + return sa; + + for (var s in v) + if (!d || d[s] !== true) + if (!type || v[s]['.type'] == type) + sa.push(Object.assign({ }, v[s], c ? c[s] : undefined)); + + if (n) + for (var s in n) + if (!type || n[s]['.type'] == type) + sa.push(Object.assign({ }, n[s])); + + sa.sort(function(a, b) { + return a['.index'] - b['.index']; + }); + + for (var i = 0; i < sa.length; i++) + sa[i]['.index'] = i; + + if (typeof(cb) == 'function') + for (var i = 0; i < sa.length; i++) + cb.call(this, sa[i], sa[i]['.name']); + + return sa; + }, + + get: function(conf, sid, opt) { + var v = this.state.values, + n = this.state.creates, + c = this.state.changes, + d = this.state.deletes; + + if (typeof(sid) == 'undefined') + return undefined; + + /* requested option in a just created section */ + if (n[conf] && n[conf][sid]) { + if (!n[conf]) + return undefined; + + if (typeof(opt) == 'undefined') + return n[conf][sid]; + + return n[conf][sid][opt]; + } + + /* requested an option value */ + if (typeof(opt) != 'undefined') { + /* check whether option was deleted */ + if (d[conf] && d[conf][sid]) { + if (d[conf][sid] === true) + return undefined; + + for (var i = 0; i < d[conf][sid].length; i++) + if (d[conf][sid][i] == opt) + return undefined; + } + + /* check whether option was changed */ + if (c[conf] && c[conf][sid] && typeof(c[conf][sid][opt]) != 'undefined') + return c[conf][sid][opt]; + + /* return base value */ + if (v[conf] && v[conf][sid]) + return v[conf][sid][opt]; + + return undefined; + } + + /* requested an entire section */ + if (v[conf]) + return v[conf][sid]; + + return undefined; + }, + + set: function(conf, sid, opt, val) { + var v = this.state.values, + n = this.state.creates, + c = this.state.changes, + d = this.state.deletes; + + if (sid == null || opt == null || opt.charAt(0) == '.') + return; + + if (n[conf] && n[conf][sid]) { + if (val != null) + n[conf][sid][opt] = val; + else + delete n[conf][sid][opt]; + } + else if (val != null && val !== '') { + /* do not set within deleted section */ + if (d[conf] && d[conf][sid] === true) + return; + + /* only set in existing sections */ + if (!v[conf] || !v[conf][sid]) + return; + + if (!c[conf]) + c[conf] = {}; + + if (!c[conf][sid]) + c[conf][sid] = {}; + + /* undelete option */ + if (d[conf] && d[conf][sid]) + d[conf][sid] = d[conf][sid].filter(function(o) { return o !== opt }); + + c[conf][sid][opt] = val; + } + else { + /* only delete in existing sections */ + if (!(v[conf] && v[conf][sid] && v[conf][sid].hasOwnProperty(opt)) && + !(c[conf] && c[conf][sid] && c[conf][sid].hasOwnProperty(opt))) + return; + + if (!d[conf]) + d[conf] = { }; + + if (!d[conf][sid]) + d[conf][sid] = [ ]; + + if (d[conf][sid] !== true) + d[conf][sid].push(opt); + } + }, + + unset: function(conf, sid, opt) { + return this.set(conf, sid, opt, null); + }, + + get_first: function(conf, type, opt) { + var sid = null; + + this.sections(conf, type, function(s) { + if (sid == null) + sid = s['.name']; + }); + + return this.get(conf, sid, opt); + }, + + set_first: function(conf, type, opt, val) { + var sid = null; + + this.sections(conf, type, function(s) { + if (sid == null) + sid = s['.name']; + }); + + return this.set(conf, sid, opt, val); + }, + + unset_first: function(conf, type, opt) { + return this.set_first(conf, type, opt, null); + }, + + move: function(conf, sid1, sid2, after) { + var sa = this.sections(conf), + s1 = null, s2 = null; + + for (var i = 0; i < sa.length; i++) { + if (sa[i]['.name'] != sid1) + continue; + + s1 = sa[i]; + sa.splice(i, 1); + break; + } + + if (s1 == null) + return false; + + if (sid2 == null) { + sa.push(s1); + } + else { + for (var i = 0; i < sa.length; i++) { + if (sa[i]['.name'] != sid2) + continue; + + s2 = sa[i]; + sa.splice(i + !!after, 0, s1); + break; + } + + if (s2 == null) + return false; + } + + for (var i = 0; i < sa.length; i++) + this.get(conf, sa[i]['.name'])['.index'] = i; + + this.state.reorder[conf] = true; + + return true; + }, + + save: function() { + var v = this.state.values, + n = this.state.creates, + c = this.state.changes, + d = this.state.deletes, + self = this, + snew = [ ], + pkgs = { }, + tasks = []; + + if (n) + for (var conf in n) { + for (var sid in n[conf]) { + var r = { + config: conf, + values: { } + }; + + for (var k in n[conf][sid]) { + if (k == '.type') + r.type = n[conf][sid][k]; + else if (k == '.create') + r.name = n[conf][sid][k]; + else if (k.charAt(0) != '.') + r.values[k] = n[conf][sid][k]; + } + + snew.push(n[conf][sid]); + tasks.push(self.callAdd(r.config, r.type, r.name, r.values)); + } + + pkgs[conf] = true; + } + + if (c) + for (var conf in c) { + for (var sid in c[conf]) + tasks.push(self.callSet(conf, sid, c[conf][sid])); + + pkgs[conf] = true; + } + + if (d) + for (var conf in d) { + for (var sid in d[conf]) { + var o = d[conf][sid]; + tasks.push(self.callDelete(conf, sid, (o === true) ? null : o)); + } + + pkgs[conf] = true; + } + + return Promise.all(tasks).then(function(responses) { + /* + array "snew" holds references to the created uci sections, + use it to assign the returned names of the new sections + */ + for (var i = 0; i < snew.length; i++) + snew[i]['.name'] = responses[i]; + + return self.reorderSections(); + }).then(function() { + pkgs = Object.keys(pkgs); + + self.unload(pkgs); + + return self.load(pkgs); + }); + }, + + apply: function(timeout) { + var self = this, + date = new Date(); + + if (typeof(timeout) != 'number' || timeout < 1) + timeout = 10; + + return self.callApply(timeout, true).then(function(rv) { + if (rv != 0) + return Promise.reject(rv); + + var try_deadline = date.getTime() + 1000 * timeout; + var try_confirm = function() { + return self.callConfirm().then(function(rv) { + if (rv != 0) { + if (date.getTime() < try_deadline) + window.setTimeout(try_confirm, 250); + else + return Promise.reject(rv); + } + + return rv; + }); + }; + + window.setTimeout(try_confirm, 1000); + }); + }, + + changes: rpc.declare({ + object: 'uci', + method: 'changes', + expect: { changes: { } } + }) +}); -- 2.30.2