Merge pull request #1735 from sumpfralle/olsr-jsoninfo-parser-handle-empty-result
[oweals/luci.git] / modules / luci-base / htdocs / luci-static / resources / rpc.js
index cc22d0aeb4f1f4bc7ad6c467611dbe2e33bc14da..9c74bfdaf084ede74f69f98347395fb51c49b291 100644 (file)
 'use strict';
 
-var rpcRequestRegistry = {},
-    rpcRequestBatch = null,
-    rpcRequestID = 1,
-    rpcSessionID = L.env.sessionid || '00000000000000000000000000000000';
+var rpcRequestID = 1,
+    rpcSessionID = L.env.sessionid || '00000000000000000000000000000000',
+    rpcBaseURL = L.url('admin/ubus'),
+    rpcInterceptorFns = [];
 
 return L.Class.extend({
-       call: function(req, cbFn) {
-               var cb = cbFn.bind(this, req),
-                   q = '';
+       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]
-                               );
+                               if (req[i].params)
+                                       q += '%s%s.%s'.format(
+                                               q ? ';' : '/',
+                                               req[i].params[1],
+                                               req[i].params[2]
+                                       );
                }
-               else {
+               else if (req.params) {
                        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,
+               return L.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;
-
-               if (!res.ok)
-                       L.error('RPCError', 'RPC call failed with HTTP error %d: %s',
-                               res.status, res.statusText || '?');
-
-               msg = res.json();
+               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 (!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 (!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 (req.expect) {
-                               for (var key in req.expect) {
-                                       if (ret != null && key != '')
-                                               ret = ret[key];
+               if (!req.object && !req.method) {
+                       ret = msg.result;
+               }
+               else if (Array.isArray(msg.result)) {
+                       ret = (msg.result.length > 1) ? msg.result[1] : msg.result[0];
+               }
 
-                                       if (ret == null || type.call(ret) != type.call(req.expect[key]))
-                                               ret = req.expect[key];
+               if (req.expect) {
+                       for (var key in req.expect) {
+                               if (ret != null && key != '')
+                                       ret = ret[key];
 
-                                       break;
-                               }
-                       }
+                               if (ret == null || type.call(ret) != type.call(req.expect[key]))
+                                       ret = req.expect[key];
 
-                       /* apply filter */
-                       if (typeof(req.filter) == 'function') {
-                               req.priv[0] = ret;
-                               req.priv[1] = req.params;
-                               ret = req.filter.apply(this, req.priv);
+                               break;
                        }
+               }
 
-                       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];
+               /* 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);
        },
 
        list: function() {
@@ -120,23 +114,16 @@ return L.Class.extend({
                        params:  arguments.length ? this.varargs(arguments) : undefined
                };
 
-               return this.call(msg, this.handleListReply);
-       },
+               return new Promise(L.bind(function(resolveFn, rejectFn) {
+                       /* store request info */
+                       var req = {
+                               resolve: resolveFn,
+                               reject:  rejectFn
+                       };
 
-       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);
+                       /* call rpc */
+                       this.call(msg, this.parseCallReply.bind(this, req));
+               }, this));
        },
 
        declare: function(options) {
@@ -156,13 +143,15 @@ 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
                                };
 
                                /* build message object */
@@ -178,19 +167,56 @@ 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);
        },
 
+       getSessionID: function() {
+               return rpcSessionID;
+       },
+
        setSessionID: function(sid) {
                rpcSessionID = sid;
+       },
+
+       getBaseURL: function() {
+               return rpcBaseURL;
+       },
+
+       setBaseURL: function(url) {
+               rpcBaseURL = url;
+       },
+
+       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');
+               }
+       },
+
+       addInterceptor: function(interceptorFn) {
+               if (typeof(interceptorFn) == 'function')
+                       rpcInterceptorFns.push(interceptorFn);
+               return interceptorFn;
+       },
+
+       removeInterceptor: function(interceptorFn) {
+               var oldlen = rpcInterceptorFns.length, i = oldlen;
+               while (i--)
+                       if (rpcInterceptorFns[i] === interceptorFn)
+                               rpcInterceptorFns.splice(i, 1);
+               return (rpcInterceptorFns.length < oldlen);
        }
 });