luci-base: add uci.js and rpc.js classes
[oweals/luci.git] / modules / luci-base / htdocs / luci-static / resources / rpc.js
1 'use strict';
2
3 var rpcRequestRegistry = {},
4     rpcRequestBatch = null,
5     rpcRequestID = 1,
6     rpcSessionID = L.env.sessionid || '00000000000000000000000000000000';
7
8 return L.Class.extend({
9         call: function(req, cbFn) {
10                 var cb = cbFn.bind(this, req),
11                     q = '';
12
13                 if (Array.isArray(req)) {
14                         if (req.length == 0)
15                                 return Promise.resolve([]);
16
17                         for (var i = 0; i < req.length; i++)
18                                 q += '%s%s.%s'.format(
19                                         q ? ';' : '/',
20                                         req[i].params[1],
21                                         req[i].params[2]
22                                 );
23                 }
24                 else {
25                         q += '/%s.%s'.format(req.params[1], req.params[2]);
26                 }
27
28                 return L.Request.post(L.url('admin/ubus') + q, req, {
29                         timeout: (L.env.rpctimeout || 5) * 1000,
30                         credentials: true
31                 }).then(cb);
32         },
33
34         handleListReply: function(req, msg) {
35                 var list = msg.result;
36
37                 /* verify message frame */
38                 if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id || !Array.isArray(list))
39                         list = [ ];
40
41                 req.resolve(list);
42         },
43
44         handleCallReply: function(reqs, res) {
45                 var type = Object.prototype.toString,
46                     data = [],
47                     msg = null;
48
49                 if (!res.ok)
50                         L.error('RPCError', 'RPC call failed with HTTP error %d: %s',
51                                 res.status, res.statusText || '?');
52
53                 msg = res.json();
54
55                 if (!Array.isArray(reqs)) {
56                         msg = [ msg ];
57                         reqs = [ reqs ];
58                 }
59
60                 for (var i = 0; i < msg.length; i++) {
61                         /* fetch related request info */
62                         var req = rpcRequestRegistry[reqs[i].id];
63                         if (typeof(req) != 'object')
64                                 throw 'No related request for JSON response';
65
66                         /* fetch response attribute and verify returned type */
67                         var ret = undefined;
68
69                         /* verify message frame */
70                         if (typeof(msg[i]) == 'object' && msg[i].jsonrpc == '2.0') {
71                                 if (typeof(msg[i].error) == 'object' && msg[i].error.code && msg[i].error.message)
72                                         req.reject(new Error('RPC call failed with error %d: %s'
73                                                 .format(msg[i].error.code, msg[i].error.message || '?')));
74                                 else if (Array.isArray(msg[i].result) && msg[i].result[0] == 0)
75                                         ret = (msg[i].result.length > 1) ? msg[i].result[1] : msg[i].result[0];
76                         }
77                         else {
78                                 req.reject(new Error('Invalid message frame received'));
79                         }
80
81                         if (req.expect) {
82                                 for (var key in req.expect) {
83                                         if (ret != null && key != '')
84                                                 ret = ret[key];
85
86                                         if (ret == null || type.call(ret) != type.call(req.expect[key]))
87                                                 ret = req.expect[key];
88
89                                         break;
90                                 }
91                         }
92
93                         /* apply filter */
94                         if (typeof(req.filter) == 'function') {
95                                 req.priv[0] = ret;
96                                 req.priv[1] = req.params;
97                                 ret = req.filter.apply(this, req.priv);
98                         }
99
100                         req.resolve(ret);
101
102                         /* store response data */
103                         if (typeof(req.index) == 'number')
104                                 data[req.index] = ret;
105                         else
106                                 data = ret;
107
108                         /* delete request object */
109                         delete rpcRequestRegistry[reqs[i].id];
110                 }
111
112                 return Promise.resolve(data);
113         },
114
115         list: function() {
116                 var msg = {
117                         jsonrpc: '2.0',
118                         id:      rpcRequestID++,
119                         method:  'list',
120                         params:  arguments.length ? this.varargs(arguments) : undefined
121                 };
122
123                 return this.call(msg, this.handleListReply);
124         },
125
126         batch: function() {
127                 if (!Array.isArray(rpcRequestBatch))
128                         rpcRequestBatch = [ ];
129         },
130
131         flush: function() {
132                 if (!Array.isArray(rpcRequestBatch))
133                         return Promise.resolve([]);
134
135                 var req = rpcRequestBatch;
136                 rpcRequestBatch = null;
137
138                 /* call rpc */
139                 return this.call(req, this.handleCallReply);
140         },
141
142         declare: function(options) {
143                 return Function.prototype.bind.call(function(rpc, options) {
144                         var args = this.varargs(arguments, 2);
145                         return new Promise(function(resolveFn, rejectFn) {
146                                 /* build parameter object */
147                                 var p_off = 0;
148                                 var params = { };
149                                 if (Array.isArray(options.params))
150                                         for (p_off = 0; p_off < options.params.length; p_off++)
151                                                 params[options.params[p_off]] = args[p_off];
152
153                                 /* all remaining arguments are private args */
154                                 var priv = [ undefined, undefined ];
155                                 for (; p_off < args.length; p_off++)
156                                         priv.push(args[p_off]);
157
158                                 /* store request info */
159                                 var req = rpcRequestRegistry[rpcRequestID] = {
160                                         expect:  options.expect,
161                                         filter:  options.filter,
162                                         resolve: resolveFn,
163                                         reject:  rejectFn,
164                                         params:  params,
165                                         priv:    priv
166                                 };
167
168                                 /* build message object */
169                                 var msg = {
170                                         jsonrpc: '2.0',
171                                         id:      rpcRequestID++,
172                                         method:  'call',
173                                         params:  [
174                                                 rpcSessionID,
175                                                 options.object,
176                                                 options.method,
177                                                 params
178                                         ]
179                                 };
180
181                                 /* when a batch is in progress then store index in request data
182                                  * and push message object onto the stack */
183                                 if (Array.isArray(rpcRequestBatch))
184                                         req.index = rpcRequestBatch.push(msg) - 1;
185
186                                 /* call rpc */
187                                 else
188                                         rpc.call(msg, rpc.handleCallReply);
189                         });
190                 }, this, this, options);
191         },
192
193         setSessionID: function(sid) {
194                 rpcSessionID = sid;
195         }
196 });