Commits

Anonymous committed b302540

Initial version of protocol-provider patch. Patch does not currently apply cleanly - it hasn't been updated since summer.

  • Participants
  • Parent commits e122336

Comments (0)

Files changed (2)

-# Placed by Bitbucket
+
+# Make protocols pluggable (including default supported ones)
+# TODO: Types cleanup (Binary + datetime + all method docs)
+# TODO: Clean up method docs to not presume specific protocols
+# TODO: Make protocol documentation from plugin => HTML docs
+# TODO: Nicer display of enabled/disabled for known protocols
+# TODO: Abstract _send_error() using regular http status codes, and let each make the appropriate type of request
+t5437/t5437-protocol_api-r6141.diff
+

File t5437/t5437-protocol_api-r6141.diff

+diff --git a/trunk/README.wiki b/trunk/README.wiki
+--- a/trunk/README.wiki
++++ b/trunk/README.wiki
+@@ -31,12 +31,18 @@
+ `<project_url/jsonrpc` for projects that have the plugin installed and enabled.
+ It can be accessed by all users that have been granted `XML_RPC` permission.
+ 
+-== Developing ==
++== Development ==
+ 
+-The Trac RPC plugin has interfaces that make it possible to extend the set of
+-methods available for remote procedure calls. See source for documentation.
++The Trac RPC plugin uses pluggable interfaces to do all its work. That means it
++is easy to extend, and currently supports:
++ * protocols; add a new protocol in addition to the builtin ones and read input
++   and answer request in whatever form and format needed.
++ * methods; adding new methods available for remote procedure calls that will
++   work for any enabled protocol.
+ 
+-The source code can be obtained from: http://trac-hacks.org/svn/xmlrpcplugin/
++See source for documentation. The source code can be obtained from:
++
++http://trac-hacks.org/svn/xmlrpcplugin/
+ 
+ For work on the plugin itself (for submitting patches and more), please verify
+ patches by running unittests (requires Trac source code on path):
+diff --git a/trunk/setup.py b/trunk/setup.py
+--- a/trunk/setup.py
++++ b/trunk/setup.py
+@@ -10,7 +10,7 @@
+ 
+ setup(
+     name='TracXMLRPC',
+-    version='1.0.6',
++    version='1.0.7',
+     license='BSD',
+     author='Alec Thomas',
+     author_email='alec@swapoff.org',
+diff --git a/trunk/tracrpc/__init__.py b/trunk/tracrpc/__init__.py
+--- a/trunk/tracrpc/__init__.py
++++ b/trunk/tracrpc/__init__.py
+@@ -7,6 +7,8 @@
+ """
+ 
+ from tracrpc.api import *
++from tracrpc.json_rpc import *
++from tracrpc.xml_rpc import *
+ from tracrpc.web_ui import *
+ from tracrpc.ticket import *
+ from tracrpc.wiki import *
+diff --git a/trunk/tracrpc/api.py b/trunk/tracrpc/api.py
+--- a/trunk/tracrpc/api.py
++++ b/trunk/tracrpc/api.py
+@@ -35,6 +35,26 @@
+     return decorator
+ 
+ 
++class IRPCProtocol(Interface):
++    
++    def rpc_info():
++        """ Returns a tuple of (name, status, docs). Method provides
++        general information about the protocol used for the RPC HTML view.
++        name: Shortname like 'XML-RPC'
++        status: True if protocol is able to serve requests
++        docs: HTML rendered documentation for the protocol
++        """
++
++    def rpc_match():
++        """ Return an iterable of (path_item, content_type) combinations that will
++        be handled by the implementation.
++        
++        path_item: Single word to use for matching against (/login)?/<path_item>
++        content_type: Match against 'Content-Type' request header. """
++
++    def rpc_process(req, content_type):
++        """ Process the RPC request and finalize response. """
++
+ class IXMLRPCHandler(Interface):
+     def xmlrpc_namespace():
+         """ Provide the namespace in which a set of methods lives.
+@@ -56,7 +76,6 @@
+         followed by argument types.
+         """
+ 
+-
+ class AbstractRPCHandler(Component):
+     implements(IXMLRPCHandler)
+     abstract = True
+diff --git a/trunk/tracrpc/json_rpc.py b/trunk/tracrpc/json_rpc.py
+new file mode 100644
+--- /dev/null
++++ b/trunk/tracrpc/json_rpc.py
+@@ -0,0 +1,176 @@
++# -*- coding: utf-8 -*-
++"""
++License: BSD
++
++(c) 2009      ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no)
++"""
++
++import re
++import datetime
++import xmlrpclib
++from types import GeneratorType
++
++from trac.core import *
++from trac.perm import PermissionError
++from trac.util.datefmt import utc
++from trac.util.text import to_unicode
++
++from tracrpc.api import IRPCProtocol, XMLRPCSystem
++from tracrpc.util import exception_to_unicode
++
++# try:
++try:
++    import json
++except:
++    import simplejson as json
++
++class TracRpcJSONEncoder(json.JSONEncoder):
++    """ Extending the JSON encoder to support some additional types:
++    1. datetime.datetime => {'__jsonclass__': ["datetime", "<rfc3339str>"]}
++    2. xmlrpclib.Fault => unicode
++    3. xmlrpclib.Binary => {'__jsonclass__': ["binary", "<base64str>"]} """
++
++    def default(self, obj):
++        if isinstance(obj, datetime.datetime):
++            # http://www.ietf.org/rfc/rfc3339.txt
++            return {'__jsonclass__': ["datetime",
++                            obj.strftime('%Y-%m-%dT%H:%M:%S')]}
++        elif isinstance(obj, xmlrpclib.Binary):
++            return {'__jsonclass__': ["binary",
++                            obj.data.encode("base64")]}
++        elif isinstance(obj, xmlrpclib.Fault):
++            return to_unicode(obj)
++        else:
++            return json.JSONEncoder(self, obj)
++
++class TracRpcJSONDecoder(json.JSONDecoder):
++    """ Extending the JSON decoder to support some additional types:
++    1. {'__jsonclass__': ["datetime", "<rfc3339str>"]} => datetime.datetime
++    2. {'__jsonclass__': ["binary", "<base64str>"]} => xmlrpclib.Binary """
++
++    dt = re.compile(
++        '^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,}))?')
++
++    def _normalize(self, obj):
++        """ Helper to traverse JSON decoded object for custom types. """
++        if isinstance(obj, tuple):
++            return tuple(self._normalize(item) for item in obj)
++        elif isinstance(obj, list):
++            return [self._normalize(item) for item in obj]
++        elif isinstance(obj, dict):
++            if obj.keys() == ['__jsonclass__']:
++                kind, val = obj['__jsonclass__']
++                if kind == 'datetime':
++                    dt = self.dt.match(val)
++                    if not dt:
++                        raise Exception(
++                                "Invalid datetime string (%s)" % val)
++                    dt = tuple([int(i) for i in dt.groups() if i])
++                    kw_args = {'tzinfo': utc}
++                    return datetime.datetime(*dt, **kw_args)
++                elif kind == 'binary':
++                    try:
++                        bin = val.decode("base64")
++                        return xmlrpclib.Binary(bin)
++                    except:
++                        raise Exception("Invalid base64 string")
++                else:
++                    raise Exception("Unknown __jsonclass__: %s" % kind)
++            else:
++                return dict(self._normalize(obj.items()))
++        elif isinstance(obj, basestring):
++            return to_unicode(obj)
++        else:
++            return obj
++
++    def decode(self, obj, *args, **kwargs):
++        obj = json.JSONDecoder.decode(self, obj, *args, **kwargs)
++        return self._normalize(obj)
++
++# except ImportError:
++#     json = None
++
++
++class JsonRpcProtocol(Component):
++
++    implements(IRPCProtocol)
++
++    # IRPCProtocol methods
++
++    def rpc_info(self):
++        return ('JSON-RPC', True, "Docs for JSON-RPC rendered here...")
++
++    def rpc_match(self):
++        yield ('jsonrpc', 'application/json')
++
++    def rpc_process(self, req, content_type):
++        """ Handles JSON-RPC requests """
++        if not json:
++            self.log.debug("RPC(json) call ignored (not available).")
++            self._send_response(req, "Error: JSON-RPC not available.\n",
++                                content_type)
++        try:
++            data = json.load(req, cls=TracRpcJSONDecoder)
++        except Exception, e:
++            # Abort with exception - no data can be read
++            self.log.error("RPC(json) decode error %s" % \
++                    exception_to_unicode(e, traceback=True))
++            response = json.dumps(self._json_error(e, -32700),
++                                    cls=TracRpcJSONEncoder)
++            self._send_response(req, response + '\n', content_type)
++            return
++        self.log.debug("RPC(json) call by '%s': %s" % (req.authname, data))
++        args = data.get('params') or []
++        r_id = data.get('id', None)
++        method = data.get('method', '')
++        try:
++            req.perm.require('XML_RPC') # Need at least XML_RPC
++            if method == 'system.multicall': # Custom multicall
++                results = []
++                for mc in args:
++                    results.append(self._json_call(req, mc.get('method', ''),
++                        mc.get('params') or [], mc.get('id') or r_id))
++                response = {'result': results, 'error': None, 'id': r_id}
++            else:
++                response = self._json_call(req, method, args, r_id)
++            try: # JSON encoding
++                self.log.debug("RPC(json) result: %s" % repr(response))
++                response = json.dumps(response, cls=TracRpcJSONEncoder)
++            except Exception, e:
++                response = json.dumps(self._json_error(e, r_id=r_id),
++                                        cls=TracRpcJSONEncoder)
++        except PermissionError, e:
++            response = json.dumps(self._json_error(e, -32600, r_id=r_id),
++                cls=TracRpcJSONEncoder)
++        except Exception, e:
++            self.log.error("RPC(json) error %s" % exception_to_unicode(e,
++                                                    traceback=True))
++            response = json.dumps(self._json_error(e), cls=TracRpcJSONEncoder)
++        self.log.debug("RPC(json) encoded result: %s" % response)
++        self._send_response(req, response + '\n', content_type)
++
++    # Internal methods
++
++    def _send_response(self, req, response, content_type='application/json'):
++        response = to_unicode(response).encode("utf-8")
++        req.send_response(200)
++        req.send_header('Content-Type', content_type)
++        req.send_header('Content-Length', len(response))
++        req.end_headers()
++        req.write(response)
++
++    def _json_call(self, req, method, args, r_id=None):
++        """ Call method and create response dictionary. """
++        try:
++            result = (XMLRPCSystem(self.env).get_method(method)(req, args))[0]
++            if isinstance(result, GeneratorType):
++                result = list(result)
++            return {'result': result, 'error': None, 'id': r_id}
++        except Exception, e:
++            return self._json_error(e, r_id=r_id)
++
++    def _json_error(self, e, c=-32603, r_id=None):
++        """ Makes a response dictionary that is an error. """
++        return {'result': None, 'id': r_id, 'error': {
++                'name': 'JSONRPCError', 'code': c, 'message': to_unicode(e)}}
++
+diff --git a/trunk/tracrpc/templates/xmlrpclist.html b/trunk/tracrpc/templates/xmlrpclist.html
+--- a/trunk/tracrpc/templates/xmlrpclist.html
++++ b/trunk/tracrpc/templates/xmlrpclist.html
+@@ -15,9 +15,9 @@
+       <h1>Remote Procedure Call (RPC) Interface</h1>
+       <dl>
+         <dt>API Status:</dt>
+-          <dd>XML-RPC is <strong>available</strong></dd>
+-          <dd>JSON-RPC is <strong>
+-            ${xmlrpc.json and 'available' or 'not available'}</strong></dd>
++          <dd py:for="protocol in xmlrpc.protocols">
++            ${protocol[0]} is <strong>
++              ${protocol[1] and 'available' or 'not available'}</strong></dd>
+       </dl>
+ 
+       <h2>RPC exported functions</h2>
+diff --git a/trunk/tracrpc/tests/__init__.py b/trunk/tracrpc/tests/__init__.py
+--- a/trunk/tracrpc/tests/__init__.py
++++ b/trunk/tracrpc/tests/__init__.py
+@@ -68,10 +68,10 @@
+ 
+     def suite():
+         suite = unittest.TestSuite()
+-        import tracrpc.tests.xml
+-        suite.addTest(tracrpc.tests.xml.suite())
+-        import tracrpc.tests.json
+-        suite.addTest(tracrpc.tests.json.suite())
++        import tracrpc.tests.xml_rpc
++        suite.addTest(tracrpc.tests.xml_rpc.suite())
++        import tracrpc.tests.json_rpc
++        suite.addTest(tracrpc.tests.json_rpc.suite())
+         import tracrpc.tests.ticket
+         suite.addTest(tracrpc.tests.ticket.suite())
+         import tracrpc.tests.wiki
+diff --git a/trunk/tracrpc/tests/json.py b/trunk/tracrpc/tests/json_rpc.py
+rename from trunk/tracrpc/tests/json.py
+rename to trunk/tracrpc/tests/json_rpc.py
+--- a/trunk/tracrpc/tests/json.py
++++ b/trunk/tracrpc/tests/json_rpc.py
+@@ -11,7 +11,7 @@
+ import urllib2
+ from StringIO import StringIO
+ 
+-from tracrpc.web_ui import json
++from tracrpc.json_rpc import json
+ 
+ if json:
+     from tracrpc.tests import rpc_testenv
+diff --git a/trunk/tracrpc/tests/xml.py b/trunk/tracrpc/tests/xml_rpc.py
+rename from trunk/tracrpc/tests/xml.py
+rename to trunk/tracrpc/tests/xml_rpc.py
+diff --git a/trunk/tracrpc/util.py b/trunk/tracrpc/util.py
+--- a/trunk/tracrpc/util.py
++++ b/trunk/tracrpc/util.py
+@@ -6,24 +6,6 @@
+ (c) 2009      ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no)
+ """
+ 
+-import datetime
+-import time
+-import xmlrpclib
+-
+-from trac.util.datefmt import utc
+-
+-### PUBLIC
+-
+-def to_xmlrpc_datetime(dt):
+-    """ Convert a datetime.datetime object to a xmlrpclib DateTime object """
+-    return xmlrpclib.DateTime(dt.utctimetuple())
+-
+-def from_xmlrpc_datetime(data):
+-    """Return datetime (in utc) from XMLRPC datetime string (is always utc)"""
+-    t = list(time.strptime(data.value, "%Y%m%dT%H:%M:%S")[0:6])
+-    return apply(datetime.datetime, t, {'tzinfo': utc})
+-
+-### INTERNAL / COMPAT
+ 
+ try:
+     # Method only available in Trac 0.11.3 or higher
+diff --git a/trunk/tracrpc/web_ui.py b/trunk/tracrpc/web_ui.py
+--- a/trunk/tracrpc/web_ui.py
++++ b/trunk/tracrpc/web_ui.py
+@@ -6,292 +6,70 @@
+ (c) 2009      ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no)
+ """
+ 
+-import re
+-import xmlrpclib
+-import datetime
+-import base64
+-from StringIO import StringIO
+ from pkg_resources import resource_filename
+-from types import GeneratorType
+-
+-import genshi
+ 
+ from trac.core import *
+-from trac.perm import PermissionError
+-from trac.util.datefmt import utc
+-from trac.util.text import to_unicode
++from trac.web.api import RequestDone
+ from trac.web.main import IRequestHandler
+ from trac.web.chrome import ITemplateProvider, add_stylesheet
+ from trac.wiki.formatter import wiki_to_oneliner
+ 
+-from tracrpc.api import IXMLRPCHandler, XMLRPCSystem
+-from tracrpc.util import from_xmlrpc_datetime, to_xmlrpc_datetime
+-from tracrpc.util import exception_to_unicode
++from tracrpc.api import XMLRPCSystem, IRPCProtocol
+ 
+-try:
+-    try:
+-        import json
+-    except:
+-        import simplejson as json
+-
+-    class TracRpcJSONEncoder(json.JSONEncoder):
+-        """ Extending the JSON encoder to support some additional types:
+-        1. datetime.datetime => {'__jsonclass__': ["datetime", "<rfc3339str>"]}
+-        2. xmlrpclib.Fault => unicode
+-        3. xmlrpclib.Binary => {'__jsonclass__': ["binary", "<base64str>"]} """
+-
+-        def default(self, obj):
+-            if isinstance(obj, datetime.datetime):
+-                # http://www.ietf.org/rfc/rfc3339.txt
+-                return {'__jsonclass__': ["datetime",
+-                                obj.strftime('%Y-%m-%dT%H:%M:%S')]}
+-            elif isinstance(obj, xmlrpclib.Binary):
+-                return {'__jsonclass__': ["binary",
+-                                obj.data.encode("base64")]}
+-            elif isinstance(obj, xmlrpclib.Fault):
+-                return to_unicode(obj)
+-            else:
+-                return json.JSONEncoder(self, obj)
+-
+-    class TracRpcJSONDecoder(json.JSONDecoder):
+-        """ Extending the JSON decoder to support some additional types:
+-        1. {'__jsonclass__': ["datetime", "<rfc3339str>"]} => datetime.datetime
+-        2. {'__jsonclass__': ["binary", "<base64str>"]} => xmlrpclib.Binary """
+-
+-        dt = re.compile(
+-            '^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,}))?')
+-
+-        def _normalize(self, obj):
+-            """ Helper to traverse JSON decoded object for custom types. """
+-            if isinstance(obj, tuple):
+-                return tuple(self._normalize(item) for item in obj)
+-            elif isinstance(obj, list):
+-                return [self._normalize(item) for item in obj]
+-            elif isinstance(obj, dict):
+-                if obj.keys() == ['__jsonclass__']:
+-                    kind, val = obj['__jsonclass__']
+-                    if kind == 'datetime':
+-                        dt = self.dt.match(val)
+-                        if not dt:
+-                            raise Exception(
+-                                    "Invalid datetime string (%s)" % val)
+-                        dt = tuple([int(i) for i in dt.groups() if i])
+-                        kw_args = {'tzinfo': utc}
+-                        return datetime.datetime(*dt, **kw_args)
+-                    elif kind == 'binary':
+-                        try:
+-                            bin = val.decode("base64")
+-                            return xmlrpclib.Binary(bin)
+-                        except:
+-                            raise Exception("Invalid base64 string")
+-                    else:
+-                        raise Exception("Unknown __jsonclass__: %s" % kind)
+-                else:
+-                    return dict(self._normalize(obj.items()))
+-            elif isinstance(obj, basestring):
+-                return to_unicode(obj)
+-            else:
+-                return obj
+-
+-        def decode(self, obj, *args, **kwargs):
+-            obj = json.JSONDecoder.decode(self, obj, *args, **kwargs)
+-            return self._normalize(obj)
+-    
+-except ImportError:
+-    json = None
+ 
+ class XMLRPCWeb(Component):
+     """ Handle XML-RPC calls from HTTP clients, as well as presenting a list of
+         methods available to the currently logged in user. Browsing to
+         <trac>/xmlrpc or <trac>/login/xmlrpc will display this list. """
+ 
+-    content_type_re = re.compile(r'(text|application)/(xml|json)')
+-
+     implements(IRequestHandler, ITemplateProvider)
+ 
++    protocols = ExtensionPoint(IRPCProtocol)
++
+     # IRequestHandler methods
++
+     def match_request(self, req):
+-        if req.path_info in ('/login/xmlrpc', '/xmlrpc'):
+-            return True
+-        if req.path_info in ('/login/jsonrpc', '/jsonrpc'):
+-            return True
+-
+-    def _send_response(self, req, response, content_type='application/xml'):
+-        response = to_unicode(response).encode("utf-8")
+-        req.send_response(200)
+-        req.send_header('Content-Type', content_type)
+-        req.send_header('Content-Length', len(response))
+-        req.end_headers()
+-        req.write(response)
++        for protocol in self.protocols:
++            for match in protocol.rpc_match():
++                if req.path_info in ['/%s' % match[0], '/login/%s' % match[0]]:
++                    return True
+ 
+     def process_request(self, req):
++        content_type = req.get_header('Content-Type') or 'text/html'
+ 
+-        content_type = req.get_header('Content-Type') or 'text/html'
+-        if not self.content_type_re.match(content_type):
+-            # Dump RPC functions
+-            req.perm.require('XML_RPC') # Need at least XML_RPC
+-            namespaces = {}
+-            for method in XMLRPCSystem(self.env).all_methods(req):
+-                namespace = method.namespace.replace('.', '_')
+-                if namespace not in namespaces:
+-                    namespaces[namespace] = {
+-                        'description' : wiki_to_oneliner(method.namespace_description, self.env),
+-                        'methods' : [],
+-                        'namespace' : method.namespace,
+-                        }
+-                try:
+-                    namespaces[namespace]['methods'].append((method.signature, wiki_to_oneliner(method.description, self.env), method.permission))
+-                except Exception, e:
+-                    from StringIO import StringIO
+-                    import traceback
+-                    out = StringIO()
+-                    traceback.print_exc(file=out)
+-                    raise Exception('%s: %s\n%s' % (method.name, str(e), out.getvalue()))
+-            add_stylesheet(req, 'common/css/wiki.css')
+-            return ('xmlrpclist.html', {'xmlrpc': {'functions': namespaces,
+-                                    'json': json and True or False}}, None)
++        for protocol in self.protocols:
++            for match in protocol.rpc_match():
++                if content_type.startswith(match[1]):
++                    protocol.rpc_process(req, content_type)
++                    raise RequestDone
+ 
+-        # Handle RPC call
+-        if content_type.startswith('application/json'):
+-            if json:
+-                self.process_json_request(req, content_type)
+-            else:
+-                self.log.debug("RPC(json) call ignored (not available).")
+-                self._send_response(req, "Error: JSON-RPC not available.\n",
+-                                    content_type)
+-        else:
+-            self.process_xml_request(req, content_type)
+-
+-    def process_xml_request(self, req, content_type):
+-        """ Handles XML-RPC requests """
+-        args, method = xmlrpclib.loads(req.read(int(req.get_header('Content-Length'))))
+-        self.log.debug("RPC(xml) call by '%s', method '%s' with args: %s" \
+-                                    % (req.authname, method, repr(args)))
+-        args = self._normalize_xml_input(args)
+-        try:
+-            req.perm.require('XML_RPC') # Need at least XML_RPC
+-            result = XMLRPCSystem(self.env).get_method(method)(req, args)
+-            self.env.log.debug("RPC(xml) '%s' result: %s" % (method, repr(result)))
+-            result = tuple(self._normalize_xml_output(result))
+-            self._send_response(req, xmlrpclib.dumps(result, methodresponse=True), content_type)
+-        except PermissionError, e:
+-            self._send_response(req, xmlrpclib.dumps(xmlrpclib.Fault(1, to_unicode(e))),
+-                                    content_type)
+-        except xmlrpclib.Fault, e:
+-            self.log.error(e)
+-            self._send_response(req, xmlrpclib.dumps(e), content_type)
+-        except Exception, e:
+-            self.log.error(e)
+-            import traceback
+-            from StringIO import StringIO
+-            out = StringIO()
+-            traceback.print_exc(file = out)
+-            self.log.error(out.getvalue())
+-            self._send_response(req, xmlrpclib.dumps(xmlrpclib.Fault(2, "'%s' while executing '%s()'" % (str(e), method))))
+-
+-    def process_json_request(self, req, content_type):
+-        """ Handles JSON-RPC requests """
+-        try:
+-            data = json.load(req, cls=TracRpcJSONDecoder)
+-        except Exception, e:
+-            # Abort with exception - no data can be read
+-            self.log.error("RPC(json) decode error %s" % \
+-                    exception_to_unicode(e, traceback=True))
+-            response = json.dumps(self._json_error(e, -32700),
+-                                    cls=TracRpcJSONEncoder)
+-            self._send_response(req, response + '\n', content_type)
+-            return
+-        self.log.debug("RPC(json) call by '%s': %s" % (req.authname, data))
+-        args = data.get('params') or []
+-        r_id = data.get('id', None)
+-        method = data.get('method', '')
+-        try:
+-            req.perm.require('XML_RPC') # Need at least XML_RPC
+-            if method == 'system.multicall': # Custom multicall
+-                results = []
+-                for mc in args:
+-                    results.append(self._json_call(req, mc.get('method', ''),
+-                        mc.get('params') or [], mc.get('id') or r_id))
+-                response = {'result': results, 'error': None, 'id': r_id}
+-            else:
+-                response = self._json_call(req, method, args, r_id)
+-            try: # JSON encoding
+-                self.log.debug("RPC(json) result: %s" % repr(response))
+-                response = json.dumps(response, cls=TracRpcJSONEncoder)
++        # No match - dump RPC documentation
++        req.perm.require('XML_RPC') # Need at least XML_RPC
++        namespaces = {}
++        for method in XMLRPCSystem(self.env).all_methods(req):
++            namespace = method.namespace.replace('.', '_')
++            if namespace not in namespaces:
++                namespaces[namespace] = {
++                    'description' : wiki_to_oneliner(
++                                    method.namespace_description, self.env),
++                    'methods' : [],
++                    'namespace' : method.namespace,
++                    }
++            try:
++                namespaces[namespace]['methods'].append(
++                        (method.signature,
++                        wiki_to_oneliner(method.description, self.env),
++                        method.permission))
+             except Exception, e:
+-                response = json.dumps(self._json_error(e, r_id=r_id),
+-                                        cls=TracRpcJSONEncoder)
+-        except PermissionError, e:
+-            response = json.dumps(self._json_error(e, -32600, r_id=r_id),
+-                cls=TracRpcJSONEncoder)
+-        except Exception, e:
+-            self.log.error("RPC(json) error %s" % exception_to_unicode(e,
+-                                                    traceback=True))
+-            response = json.dumps(self._json_error(e), cls=TracRpcJSONEncoder)
+-        self.log.debug("RPC(json) encoded result: %s" % response)
+-        self._send_response(req, response + '\n', content_type)
+-
+-    def _json_call(self, req, method, args, r_id=None):
+-        """ Call method and create response dictionary. """
+-        try:
+-            result = (XMLRPCSystem(self.env).get_method(method)(req, args))[0]
+-            if isinstance(result, GeneratorType):
+-                result = list(result)
+-            return {'result': result, 'error': None, 'id': r_id}
+-        except Exception, e:
+-            return self._json_error(e, r_id=r_id)
+-
+-    def _json_error(self, e, c=-32603, r_id=None):
+-        """ Makes a response dictionary that is an error. """
+-        return {'result': None, 'id': r_id, 'error': {
+-                'name': 'JSONRPCError', 'code': c, 'message': to_unicode(e)}}
+-
+-    def _normalize_xml_input(self, args):
+-        """ Normalizes arguments (at any level - traversing dicts and lists):
+-        1. xmlrpc.DateTime is converted to Python datetime
+-        2. String line-endings same as from web (`\n` => `\r\n`)
+-        """
+-        new_args = []
+-        for arg in args:
+-            # self.env.log.debug("arg %s, type %s" % (arg, type(arg)))
+-            if isinstance(arg, xmlrpclib.DateTime):
+-                new_args.append(from_xmlrpc_datetime(arg))
+-            elif isinstance(arg, basestring):
+-                new_args.append(arg.replace("\n", "\r\n"))
+-            elif isinstance(arg, dict):
+-                for key in arg.keys():
+-                    arg[key] = self._normalize_xml_input([arg[key]])[0]
+-                new_args.append(arg)
+-            elif isinstance(arg, list) or isinstance(arg, tuple):
+-                new_args.append(self._normalize_xml_input(arg))
+-            else:
+-                new_args.append(arg)
+-        return new_args
+-
+-    def _normalize_xml_output(self, result):
+-        """ Normalizes and converts output (traversing it):
+-        1. None => ''
+-        2. datetime => xmlrpclib.DateTime
+-        3. genshi.builder.Fragment|genshi.core.Markup => unicode
+-        """
+-        new_result = []
+-        for res in result:
+-            if isinstance(res, datetime.datetime):
+-                new_result.append(to_xmlrpc_datetime(res))
+-            elif res == None:
+-                new_result.append('')
+-            elif isinstance(res, genshi.builder.Fragment) \
+-                        or isinstance(res, genshi.core.Markup):
+-                new_result.append(to_unicode(res))
+-            elif isinstance(res, dict):
+-                for key in res.keys():
+-                    res[key] = self._normalize_xml_output([res[key]])[0]
+-                new_result.append(res)
+-            elif isinstance(res, list) or isinstance(res, tuple):
+-                new_result.append(self._normalize_xml_output(res))
+-            else:
+-                new_result.append(res)
+-        return new_result
++                from StringIO import StringIO
++                import traceback
++                out = StringIO()
++                traceback.print_exc(file=out)
++                raise Exception('%s: %s\n%s' % (method.name,
++                                                str(e), out.getvalue()))
++        add_stylesheet(req, 'common/css/wiki.css')
++        return ('xmlrpclist.html', {'xmlrpc': {'functions': namespaces,
++                    'protocols': [p.rpc_info() for p in self.protocols]}}, None)
+ 
+     # ITemplateProvider
+     def get_htdocs_dirs(self):
+diff --git a/trunk/tracrpc/xml_rpc.py b/trunk/tracrpc/xml_rpc.py
+new file mode 100644
+--- /dev/null
++++ b/trunk/tracrpc/xml_rpc.py
+@@ -0,0 +1,126 @@
++# -*- coding: utf-8 -*-
++"""
++License: BSD
++
++(c) 2005-2008 ::: Alec Thomas (alec@swapoff.org)
++(c) 2009      ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no)
++"""
++
++import datetime
++import time
++import xmlrpclib
++
++import genshi
++
++from trac.core import *
++from trac.perm import PermissionError
++from trac.util.text import to_unicode
++
++from tracrpc.api import XMLRPCSystem, IRPCProtocol
++
++
++def to_xmlrpc_datetime(dt):
++    """ Convert a datetime.datetime object to a xmlrpclib DateTime object """
++    return xmlrpclib.DateTime(dt.utctimetuple())
++
++def from_xmlrpc_datetime(data):
++    """Return datetime (in utc) from XMLRPC datetime string (is always utc)"""
++    t = list(time.strptime(data.value, "%Y%m%dT%H:%M:%S")[0:6])
++    return apply(datetime.datetime, t, {'tzinfo': utc})
++
++class XmlRpcProtocol(Component):
++
++    implements(IRPCProtocol)
++
++    # IRPCProtocol methods
++
++    def rpc_info(self):
++        return ('XML-RPC', True, "Docs for XML-RPC rendered here...")
++
++    def rpc_match(self):
++        yield ('xmlrpc', 'application/xml')
++        yield ('xmlrpc', 'text/xml')
++
++    def rpc_process(self, req, content_type):
++        """ Handles XML-RPC requests """
++        args, method = xmlrpclib.loads(req.read(int(req.get_header('Content-Length'))))
++        self.log.debug("RPC(xml) call by '%s', method '%s' with args: %s" \
++                                    % (req.authname, method, repr(args)))
++        args = self._normalize_xml_input(args)
++        try:
++            req.perm.require('XML_RPC') # Need at least XML_RPC
++            result = XMLRPCSystem(self.env).get_method(method)(req, args)
++            self.env.log.debug("RPC(xml) '%s' result: %s" % (method, repr(result)))
++            result = tuple(self._normalize_xml_output(result))
++            self._send_response(req, xmlrpclib.dumps(result, methodresponse=True), content_type)
++        except PermissionError, e:
++            self._send_response(req, xmlrpclib.dumps(xmlrpclib.Fault(1, to_unicode(e))),
++                                    content_type)
++        except xmlrpclib.Fault, e:
++            self.log.error(e)
++            self._send_response(req, xmlrpclib.dumps(e), content_type)
++        except Exception, e:
++            self.log.error(e)
++            import traceback
++            from StringIO import StringIO
++            out = StringIO()
++            traceback.print_exc(file = out)
++            self.log.error(out.getvalue())
++            self._send_response(req, xmlrpclib.dumps(xmlrpclib.Fault(2, "'%s' while executing '%s()'" % (str(e), method))))
++
++    # Internal methods
++
++    def _send_response(self, req, response, content_type='application/xml'):
++        response = to_unicode(response).encode("utf-8")
++        req.send_response(200)
++        req.send_header('Content-Type', content_type)
++        req.send_header('Content-Length', len(response))
++        req.end_headers()
++        req.write(response)
++
++    def _normalize_xml_input(self, args):
++        """ Normalizes arguments (at any level - traversing dicts and lists):
++        1. xmlrpc.DateTime is converted to Python datetime
++        2. String line-endings same as from web (`\n` => `\r\n`)
++        """
++        new_args = []
++        for arg in args:
++            # self.env.log.debug("arg %s, type %s" % (arg, type(arg)))
++            if isinstance(arg, xmlrpclib.DateTime):
++                new_args.append(from_xmlrpc_datetime(arg))
++            elif isinstance(arg, basestring):
++                new_args.append(arg.replace("\n", "\r\n"))
++            elif isinstance(arg, dict):
++                for key in arg.keys():
++                    arg[key] = self._normalize_xml_input([arg[key]])[0]
++                new_args.append(arg)
++            elif isinstance(arg, list) or isinstance(arg, tuple):
++                new_args.append(self._normalize_xml_input(arg))
++            else:
++                new_args.append(arg)
++        return new_args
++
++    def _normalize_xml_output(self, result):
++        """ Normalizes and converts output (traversing it):
++        1. None => ''
++        2. datetime => xmlrpclib.DateTime
++        3. genshi.builder.Fragment|genshi.core.Markup => unicode
++        """
++        new_result = []
++        for res in result:
++            if isinstance(res, datetime.datetime):
++                new_result.append(to_xmlrpc_datetime(res))
++            elif res == None:
++                new_result.append('')
++            elif isinstance(res, genshi.builder.Fragment) \
++                        or isinstance(res, genshi.core.Markup):
++                new_result.append(to_unicode(res))
++            elif isinstance(res, dict):
++                for key in res.keys():
++                    res[key] = self._normalize_xml_output([res[key]])[0]
++                new_result.append(res)
++            elif isinstance(res, list) or isinstance(res, tuple):
++                new_result.append(self._normalize_xml_output(res))
++            else:
++                new_result.append(res)
++        return new_result