~ubuntu-branches/ubuntu/saucy/trac-xmlrpc/saucy

« back to all changes in this revision

Viewing changes to trunk/tracrpc/json_rpc.py

  • Committer: Package Import Robot
  • Author(s): W. Martin Borgert, Jakub Wilk
  • Date: 2011-12-31 18:27:57 UTC
  • mfrom: (1.1.1)
  • Revision ID: package-import@ubuntu.com-20111231182757-oe70jynzu0lk5hny
Tags: 1.1.2+r10706-1
* New upstream version (Closes: #653726). Works with Trac 0.12.
* Fixed lintians.
  [Jakub Wilk <jwilk@debian.org>]
* Replace deprecated > operator with >=.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
"""
 
3
License: BSD
 
4
 
 
5
(c) 2009      ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no)
 
6
"""
 
7
 
 
8
import datetime
 
9
from itertools import izip
 
10
import re
 
11
from types import GeneratorType
 
12
 
 
13
from trac.core import *
 
14
from trac.perm import PermissionError
 
15
from trac.resource import ResourceNotFound
 
16
from trac.util.datefmt import utc
 
17
from trac.util.text import to_unicode
 
18
from trac.web.api import RequestDone
 
19
 
 
20
from tracrpc.api import IRPCProtocol, XMLRPCSystem, Binary, \
 
21
        RPCError, MethodNotFound, ProtocolException
 
22
from tracrpc.util import exception_to_unicode, empty, prepare_docs
 
23
 
 
24
__all__ = ['JsonRpcProtocol']
 
25
 
 
26
try:
 
27
    import json
 
28
    if not (hasattr(json, 'JSONEncoder') \
 
29
            and hasattr(json, 'JSONDecoder')):
 
30
        raise AttributeError("Incorrect JSON library found.")
 
31
except (ImportError, AttributeError):
 
32
    try:
 
33
        import simplejson as json
 
34
    except ImportError:
 
35
        json = None
 
36
        __all__ = []
 
37
 
 
38
if json:
 
39
    class TracRpcJSONEncoder(json.JSONEncoder):
 
40
        """ Extending the JSON encoder to support some additional types:
 
41
        1. datetime.datetime => {'__jsonclass__': ["datetime", "<rfc3339str>"]}
 
42
        2. tracrpc.api.Binary => {'__jsonclass__': ["binary", "<base64str>"]}
 
43
        3. empty => '' """
 
44
 
 
45
        def default(self, obj):
 
46
            if isinstance(obj, datetime.datetime):
 
47
                # http://www.ietf.org/rfc/rfc3339.txt
 
48
                return {'__jsonclass__': ["datetime",
 
49
                                obj.strftime('%Y-%m-%dT%H:%M:%S')]}
 
50
            elif isinstance(obj, Binary):
 
51
                return {'__jsonclass__': ["binary",
 
52
                                obj.data.encode("base64")]}
 
53
            elif obj is empty:
 
54
                return ''
 
55
            else:
 
56
                return json.JSONEncoder(self, obj)
 
57
 
 
58
    class TracRpcJSONDecoder(json.JSONDecoder):
 
59
        """ Extending the JSON decoder to support some additional types:
 
60
        1. {'__jsonclass__': ["datetime", "<rfc3339str>"]} => datetime.datetime
 
61
        2. {'__jsonclass__': ["binary", "<base64str>"]} => tracrpc.api.Binary """
 
62
 
 
63
        dt = re.compile(
 
64
            '^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,}))?')
 
65
 
 
66
        def _normalize(self, obj):
 
67
            """ Helper to traverse JSON decoded object for custom types. """
 
68
            if isinstance(obj, tuple):
 
69
                return tuple(self._normalize(item) for item in obj)
 
70
            elif isinstance(obj, list):
 
71
                return [self._normalize(item) for item in obj]
 
72
            elif isinstance(obj, dict):
 
73
                if obj.keys() == ['__jsonclass__']:
 
74
                    kind, val = obj['__jsonclass__']
 
75
                    if kind == 'datetime':
 
76
                        dt = self.dt.match(val)
 
77
                        if not dt:
 
78
                            raise Exception(
 
79
                                    "Invalid datetime string (%s)" % val)
 
80
                        dt = tuple([int(i) for i in dt.groups() if i])
 
81
                        kw_args = {'tzinfo': utc}
 
82
                        return datetime.datetime(*dt, **kw_args)
 
83
                    elif kind == 'binary':
 
84
                        try:
 
85
                            bin = val.decode("base64")
 
86
                            return Binary(bin)
 
87
                        except:
 
88
                            raise Exception("Invalid base64 string")
 
89
                    else:
 
90
                        raise Exception("Unknown __jsonclass__: %s" % kind)
 
91
                else:
 
92
                    return dict(self._normalize(obj.items()))
 
93
            elif isinstance(obj, basestring):
 
94
                return to_unicode(obj)
 
95
            else:
 
96
                return obj
 
97
 
 
98
        def decode(self, obj, *args, **kwargs):
 
99
            obj = json.JSONDecoder.decode(self, obj, *args, **kwargs)
 
100
            return self._normalize(obj)
 
101
 
 
102
    class JsonProtocolException(ProtocolException):
 
103
        """Impossible to handle JSON-RPC request."""
 
104
        def __init__(self, details, code=-32603, title=None, show_traceback=False):
 
105
            ProtocolException.__init__(self, details, title, show_traceback)
 
106
            self.code = code
 
107
 
 
108
    class JsonRpcProtocol(Component):
 
109
        r"""
 
110
        Example `POST` request using `curl` with `Content-Type` header
 
111
        and body:
 
112
 
 
113
        {{{
 
114
        user: ~ > cat body.json
 
115
        {"params": ["WikiStart"], "method": "wiki.getPage", "id": 123}
 
116
        user: ~ > curl -H "Content-Type: application/json" --data @body.json ${req.abs_href.rpc()}
 
117
        {"id": 123, "error": null, "result": "= Welcome to....
 
118
        }}}
 
119
    
 
120
        Implementation details:
 
121
    
 
122
          * JSON-RPC has no formalized type system, so a class-hint system is used
 
123
            for input and output of non-standard types:
 
124
            * `{"__jsonclass__": ["datetime", "YYYY-MM-DDTHH:MM:SS"]} => DateTime (UTC)`
 
125
            * `{"__jsonclass__": ["binary", "<base64-encoded>"]} => Binary`
 
126
          * `"id"` is optional, and any marker value received with a
 
127
            request is returned with the response.
 
128
        """
 
129
 
 
130
        implements(IRPCProtocol)
 
131
 
 
132
        # IRPCProtocol methods
 
133
 
 
134
        def rpc_info(self):
 
135
            return ('JSON-RPC', prepare_docs(self.__doc__))
 
136
 
 
137
        def rpc_match(self):
 
138
            yield('rpc', 'application/json')
 
139
            # Legacy path - provided for backwards compatibility:
 
140
            yield ('jsonrpc', 'application/json')
 
141
 
 
142
        def parse_rpc_request(self, req, content_type):
 
143
            """ Parse JSON-RPC requests"""
 
144
            if not json:
 
145
                self.log.debug("RPC(json) call ignored (not available).")
 
146
                raise JsonProtocolException("Error: JSON-RPC not available.\n")
 
147
            try:
 
148
                data = json.load(req, cls=TracRpcJSONDecoder)
 
149
                self.log.info("RPC(json) JSON-RPC request ID : %s.", data.get('id'))
 
150
                if data.get('method') == 'system.multicall':
 
151
                    # Prepare for multicall
 
152
                    self.log.debug("RPC(json) Multicall request %s", data)
 
153
                    params = data.get('params', [])
 
154
                    for signature in params :
 
155
                        signature['methodName'] = signature.get('method', '')
 
156
                    data['params'] = [params]
 
157
                return data
 
158
            except Exception, e:
 
159
                # Abort with exception - no data can be read
 
160
                self.log.error("RPC(json) decode error %s", 
 
161
                                  exception_to_unicode(e, traceback=True))
 
162
                raise JsonProtocolException(e, -32700)
 
163
 
 
164
        def send_rpc_result(self, req, result):
 
165
            """Send JSON-RPC response back to the caller."""
 
166
            rpcreq = req.rpc
 
167
            r_id = rpcreq.get('id')
 
168
            try:
 
169
                if rpcreq.get('method') == 'system.multicall': 
 
170
                    # Custom multicall
 
171
                    args = (rpcreq.get('params') or [[]])[0]
 
172
                    mcresults = [self._json_result(
 
173
                                            isinstance(value, Exception) and \
 
174
                                                        value or value[0], \
 
175
                                            sig.get('id') or r_id) \
 
176
                                  for sig, value in izip(args, result)]
 
177
                
 
178
                    response = self._json_result(mcresults, r_id)
 
179
                else:
 
180
                    response = self._json_result(result, r_id)
 
181
                try: # JSON encoding
 
182
                    self.log.debug("RPC(json) result: %s" % repr(response))
 
183
                    response = json.dumps(response, cls=TracRpcJSONEncoder)
 
184
                except Exception, e:
 
185
                    response = json.dumps(self._json_error(e, r_id=r_id),
 
186
                                            cls=TracRpcJSONEncoder)
 
187
            except Exception, e:
 
188
                self.log.error("RPC(json) error %s" % exception_to_unicode(e,
 
189
                                                        traceback=True))
 
190
                response = json.dumps(self._json_error(e, r_id=r_id),
 
191
                                cls=TracRpcJSONEncoder)
 
192
            self._send_response(req, response + '\n', rpcreq['mimetype'])
 
193
 
 
194
        def send_rpc_error(self, req, e):
 
195
            """Send a JSON-RPC fault message back to the caller. """
 
196
            rpcreq = req.rpc
 
197
            r_id = rpcreq.get('id')
 
198
            response = json.dumps(self._json_error(e, r_id=r_id), \
 
199
                                      cls=TracRpcJSONEncoder)
 
200
            self._send_response(req, response + '\n', rpcreq['mimetype'])
 
201
 
 
202
        # Internal methods
 
203
 
 
204
        def _send_response(self, req, response, content_type='application/json'):
 
205
            self.log.debug("RPC(json) encoded response: %s" % response)
 
206
            response = to_unicode(response).encode("utf-8")
 
207
            req.send_response(200)
 
208
            req.send_header('Content-Type', content_type)
 
209
            req.send_header('Content-Length', len(response))
 
210
            req.end_headers()
 
211
            req.write(response)
 
212
            raise RequestDone()
 
213
 
 
214
        def _json_result(self, result, r_id=None):
 
215
            """ Create JSON-RPC response dictionary. """
 
216
            if not isinstance(result, Exception):
 
217
                return {'result': result, 'error': None, 'id': r_id}
 
218
            else :
 
219
                return self._json_error(result, r_id=r_id)
 
220
 
 
221
        def _json_error(self, e, c=None, r_id=None):
 
222
            """ Makes a response dictionary that is an error. """
 
223
            if isinstance(e, MethodNotFound):
 
224
                c = -32601
 
225
            elif isinstance(e, PermissionError):
 
226
                c = 403
 
227
            elif isinstance(e, ResourceNotFound):
 
228
                c = 404
 
229
            else:
 
230
                c = c or hasattr(e, 'code') and e.code or -32603
 
231
            return {'result': None, 'id': r_id, 'error': {
 
232
                    'name': hasattr(e, 'name') and e.name or 'JSONRPCError',
 
233
                    'code': c,
 
234
                    'message': to_unicode(e)}}
 
235