Code

* Refactor XMLRPC interface.
authorstefan <stefan@57a73879-2fb5-44c3-a270-3262357dd7e2>
Wed, 25 Feb 2009 18:17:39 +0000 (18:17 +0000)
committerstefan <stefan@57a73879-2fb5-44c3-a270-3262357dd7e2>
Wed, 25 Feb 2009 18:17:39 +0000 (18:17 +0000)
* Make it accessible through web-server.

git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/roundup/trunk@4171 57a73879-2fb5-44c3-a270-3262357dd7e2

roundup/cgi/client.py
roundup/scripts/roundup_xmlrpc_server.py
roundup/xmlrpc.py

index 7d534ca7563752a2126a8948f28d0ca6679386f6..0ea02f13388ea04c5bbde32a1ba79f160730f570 100644 (file)
@@ -16,6 +16,7 @@ from roundup.cgi.exceptions import *
 from roundup.cgi.form_parser import FormParser
 from roundup.mailer import Mailer, MessageSendError
 from roundup.cgi import accept_language
+from roundup import xmlrpc
 
 def initialiseSecurity(security):
     '''Create some Permissions and Roles on the security object
@@ -354,10 +355,41 @@ class Client:
         """ Wrap the real main in a try/finally so we always close off the db.
         """
         try:
-            self.inner_main()
+            if self.env.get('CONTENT_TYPE') == 'text/xml':
+                self.handle_xmlrpc()
+            else:
+                self.inner_main()
         finally:
             if hasattr(self, 'db'):
                 self.db.close()
+
+
+    def handle_xmlrpc(self):
+
+        # Pull the raw XML out of the form.  The "value" attribute
+        # will be the raw content of the POST request.
+        assert self.form.file
+        input = self.form.value
+        # So that the rest of Roundup can query the form in the
+        # usual way, we create an empty list of fields.
+        self.form.list = []
+
+        # Set the charset and language, since other parts of
+        # Roundup may depend upon that.
+        self.determine_charset()
+        self.determine_language()
+        # Open the database as the correct user.
+        self.determine_user()
+
+        # Call the appropriate XML-RPC method.
+        handler = xmlrpc.RoundupDispatcher(self.db, self.userid, self.translator,
+                                           allow_none=True)
+        output = handler.dispatch(input)
+        self.db.commit()
+
+        self.setHeader("Content-Type", "text/xml")
+        self.setHeader("Content-Length", str(len(output)))
+        self.write(output)
         
     def inner_main(self):
         """Process a request.
index 020cc3805c8e9c0b7f7949316d47c4b228ef623a..44c626f80c55c19db0367daee285dba8418b7666 100644 (file)
 # For license terms see the file COPYING.txt.
 #
 
-import getopt, os, sys, socket
-from roundup.xmlrpc import RoundupServer, RoundupRequestHandler
+import base64, getopt, os, sys, socket, urllib
+from roundup.xmlrpc import translate
+from roundup.xmlrpc import RoundupInstance
+import roundup.instance
 from roundup.instance import TrackerError
+from roundup.cgi.exceptions import Unauthorised
 from SimpleXMLRPCServer import SimpleXMLRPCServer
+from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler
+
+
+class RequestHandler(SimpleXMLRPCRequestHandler):
+    """A SimpleXMLRPCRequestHandler with support for basic
+    HTTP Authentication."""
+
+    TRACKER_HOMES = {}
+    TRACKERS = {}
+
+    def is_rpc_path_valid(self):
+        path = self.path.split('/')
+        name = urllib.unquote(path[1]).lower()
+        return name in self.TRACKER_HOMES
+
+    def get_tracker(self, name):
+        """Return a tracker instance for given tracker name."""
+
+        if name in self.TRACKERS:
+            return self.TRACKERS[name]
+
+        if name not in self.TRACKER_HOMES:
+            raise Exception('No such tracker "%s"'%name)
+        tracker_home = self.TRACKER_HOMES[name]
+        tracker = roundup.instance.open(tracker_home)
+        self.TRACKERS[name] = tracker
+        return tracker
+
+
+    def authenticate(self, tracker):
+
+        
+        # Try to extract username and password from HTTP Authentication.
+        username, password = None, None
+        authorization = self.headers.get('authorization', ' ')
+        scheme, challenge = authorization.split(' ', 1)
+
+        if scheme.lower() == 'basic':
+            decoded = base64.decodestring(challenge)
+            username, password = decoded.split(':')
+        if not username:
+            username = 'anonymous'
+        db = tracker.open('admin')
+        try:
+            userid = db.user.lookup(username)
+        except KeyError: # No such user
+            db.close()
+            raise Unauthorised, 'Invalid user'
+        stored = db.user.get(userid, 'password')
+        if stored != password:
+            # Wrong password
+            db.close()
+            raise Unauthorised, 'Invalid user'
+        db.setCurrentUser(username)
+        return db
+
+
+    def do_POST(self):
+        """Extract username and password from authorization header."""
+
+        db = None
+        try:
+            path = self.path.split('/')
+            tracker_name = urllib.unquote(path[1]).lower()
+            tracker = self.get_tracker(tracker_name)
+            db = self.authenticate(tracker)
+
+            instance = RoundupInstance(db, None)
+            self.server.register_instance(instance)
+            SimpleXMLRPCRequestHandler.do_POST(self)
+        except Unauthorised, message:
+            self.send_error(403, '%s (%s)'%(self.path, message))
+        except:
+            if db:
+                db.close()
+            exc, val, tb = sys.exc_info()
+            print exc, val, tb
+            raise
+        if db:
+            db.close()
+
+
+class Server(SimpleXMLRPCServer):
+
+    def _dispatch(self, method, params):
+
+        retn = SimpleXMLRPCServer._dispatch(self, method, params)
+        retn = translate(retn)
+        return retn
+
 
 def usage():
-    print """
+    print """Usage: %s: [options] [name=tracker home]+
 
 Options:
  -i instance home  -- specify the issue tracker "home directory" to administer
  -V                -- be verbose when importing
  -p, --port <port> -- port to listen on
 
-"""
+"""%sys.argv[0]
 
 def run():
 
@@ -30,38 +123,42 @@ def run():
         return 1
 
     verbose = False
-    tracker = ''
     port = 8000
     encoding = None
 
     for opt, arg in opts:
         if opt == '-V':
             verbose = True
-        elif opt == '-i':
-            tracker = arg
         elif opt in ['-p', '--port']:
             port = int(arg)
         elif opt in ['-e', '--encoding']:
             encoding = encoding
 
-        if sys.version_info[0:2] < (2,5):
-            if encoding:
-                print 'encodings not supported with python < 2.5'
-                sys.exit(-1)
-            server = SimpleXMLRPCServer(('', port), RoundupRequestHandler)
-        else:
-            server = SimpleXMLRPCServer(('', port), RoundupRequestHandler,
-                                        allow_none=True, encoding=encoding)
-    if not os.path.exists(tracker):
-        print 'Instance home does not exist.'
-        sys.exit(-1)
-    try:
-        object = RoundupServer(tracker, verbose)
-    except TrackerError:
-        print 'Instance home does not exist.'
-        sys.exit(-1)
+    tracker_homes = {}
+    for arg in args:
+        try:
+            name, home = arg.split('=', 1)
+            # Validate the argument
+            tracker = roundup.instance.open(home)
+        except ValueError:
+            print 'Instances must be name=home'
+            sys.exit(-1)
+        except TrackerError:
+            print 'Tracker home does not exist.'
+            sys.exit(-1)
+
+        tracker_homes[name] = home
+
+    RequestHandler.TRACKER_HOMES=tracker_homes
 
-    server.register_instance(object)
+    if sys.version_info[0:2] < (2,5):
+        if encoding:
+            print 'encodings not supported with python < 2.5'
+            sys.exit(-1)
+        server = Server(('', port), RequestHandler)
+    else:
+        server = Server(('', port), RequestHandler,
+                        allow_none=True, encoding=encoding)
 
     # Go into the main listener loop
     print 'Roundup XMLRPC server started on %s:%d' \
index c3eaccdcab8097c5afd9b5dd7edaf947eaad0c5d..45552872576c75a94f200567e1ca2f5d961d6d1b 100644 (file)
 # For license terms see the file COPYING.txt.
 #
 
-import base64
-import roundup.instance
 from roundup import hyperdb
 from roundup.cgi.exceptions import *
-from roundup.admin import UsageError
-from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler
-
-class RoundupRequestHandler(SimpleXMLRPCRequestHandler):
-    """A SimpleXMLRPCRequestHandler with support for basic
-    HTTP Authentication."""
-
-    def do_POST(self):
-        """Extract username and password from authorization header."""
-
-        # Try to extract username and password from HTTP Authentication.
-        self.username = None
-        self.password = None
-        authorization = self.headers.get('authorization', ' ')
-        scheme, challenge = authorization.split(' ', 1)
-
-        if scheme.lower() == 'basic':
-            decoded = base64.decodestring(challenge)
-            self.username, self.password = decoded.split(':')
-
-        SimpleXMLRPCRequestHandler.do_POST(self)
+from roundup.exceptions import UsageError
+from roundup.date import Date, Range, Interval
+from SimpleXMLRPCServer import *
+
+def translate(value):
+    """Translate value to becomes valid for XMLRPC transmission."""
+
+    if isinstance(value, (Date, Range, Interval)):
+        return repr(value)
+    elif type(value) is list:
+        return [translate(v) for v in value]
+    elif type(value) is tuple:
+        return tuple([translate(v) for v in value])
+    elif type(value) is dict:
+        return dict([[translate(k), translate(value[k])] for k in value])
+    else:
+        return value
+
+
+def props_from_args(db, cl, args, itemid=None):
+    """Construct a list of properties from the given arguments,
+    and return them after validation."""
+
+    props = {}
+    for arg in args:
+        if arg.find('=') == -1:
+            raise UsageError, 'argument "%s" not propname=value'%arg
+        l = arg.split('=')
+        if len(l) < 2:
+            raise UsageError, 'argument "%s" not propname=value'%arg
+        key, value = l[0], '='.join(l[1:])
+        if value:
+            try:
+                props[key] = hyperdb.rawToHyperdb(db, cl, itemid,
+                                                  key, value)
+            except hyperdb.HyperdbValueError, message:
+                raise UsageError, message
+        else:
+            props[key] = None
+
+    return props
+
+class RoundupInstance:
+    """The RoundupInstance provides the interface accessible through
+    the Python XMLRPC mapping."""
+
+    def __init__(self, db, translator):
+
+        self.db = db
+        self.userid = db.getuid()
+        self.translator = translator
+
+    def list(self, classname, propname=None):
+        cl = self.db.getclass(classname)
+        if not propname:
+            propname = cl.labelprop()
+        result = [cl.get(itemid, propname)
+                  for itemid in cl.list()
+                  if self.db.security.hasPermission('View', self.db.getuid(),
+                                                    classname, propname, itemid)
+                  ]
+        return result
 
-    def _dispatch(self, method, params):
-        """Inject username and password into function arguments."""
+    def filter(self, classname, search_matches, filterspec,
+               sort=[], group=[]):
+        cl = self.db.getclass(classname)
+        result = cl.filter(search_matches, filterspec, sort=sort, group=group)
+        return result
 
-        # Add username and password to function arguments
-        params = [self.username, self.password] + list(params)
-        return self.server._dispatch(method, params)
+    def display(self, designator, *properties):
+        classname, itemid = hyperdb.splitDesignator(designator)
+        cl = self.db.getclass(classname)
+        props = properties and list(properties) or cl.properties.keys()
+        props.sort()
+        for p in props:
+            if not self.db.security.hasPermission('View', self.db.getuid(),
+                                                  classname, p, itemid):
+                raise Unauthorised('Permission to view %s of %s denied'%
+                                   (p, designator))
+            result = [(prop, cl.get(itemid, prop)) for prop in props]
+        return dict(result)
 
+    def create(self, classname, *args):
+        if not self.db.security.hasPermission('Create', self.db.getuid(), classname):
+            raise Unauthorised('Permission to create %s denied'%classname)
 
-class RoundupRequest:
-    """Little helper class to handle common per-request tasks such
-    as authentication and login."""
+        cl = self.db.getclass(classname)
 
-    def __init__(self, tracker, username, password):
-        """Open the database for the given tracker, using the given
-        username and password."""
+        # convert types
+        props = props_from_args(self.db, cl, args)
 
-        self.tracker = tracker
-        self.db = self.tracker.open('admin')
-        try:
-            self.userid = self.db.user.lookup(username)
-        except KeyError: # No such user
-            self.db.close()
-            raise Unauthorised, 'Invalid user'
-        stored = self.db.user.get(self.userid, 'password')
-        if stored != password:
-            # Wrong password
-            self.db.close()
-            raise Unauthorised, 'Invalid user'
-        self.db.setCurrentUser(username)
-
-    def close(self):
-        """Close the database, after committing any changes, if needed."""
+        # check for the key property
+        key = cl.getkey()
+        if key and not props.has_key(key):
+            raise UsageError, 'you must provide the "%s" property.'%key
 
+        # do the actual create
         try:
-            self.db.commit()
-        finally:
-            self.db.close()
-
-    def get_class(self, classname):
-        """Return the class for the given classname."""
-
-        try:
-            return self.db.getclass(classname)
-        except KeyError:
-            raise UsageError, 'no such class "%s"'%classname
-
-    def props_from_args(self, cl, args, itemid=None):
-        """Construct a list of properties from the given arguments,
-        and return them after validation."""
-
-        props = {}
-        for arg in args:
-            if arg.find('=') == -1:
-                raise UsageError, 'argument "%s" not propname=value'%arg
-            l = arg.split('=')
-            if len(l) < 2:
-                raise UsageError, 'argument "%s" not propname=value'%arg
-            key, value = l[0], '='.join(l[1:])
-            if value:
-                try:
-                    props[key] = hyperdb.rawToHyperdb(self.db, cl, itemid,
-                        key, value)
-                except hyperdb.HyperdbValueError, message:
-                    raise UsageError, message
-            else:
-                props[key] = None
-
-        return props
-
-
-#The server object
-class RoundupServer:
-    """The RoundupServer provides the interface accessible through
-    the Python XMLRPC mapping. All methods take an additional username
-    and password argument so each request can be authenticated."""
-
-    def __init__(self, tracker, verbose = False):
-        self.tracker = roundup.instance.open(tracker)
-        self.verbose = verbose
-
-    def list(self, username, password, classname, propname=None):
-        r = RoundupRequest(self.tracker, username, password)
-        try:
-            cl = r.get_class(classname)
-            if not propname:
-                propname = cl.labelprop()
-            result = [cl.get(itemid, propname)
-                for itemid in cl.list()
-                     if r.db.security.hasPermission('View', r.userid,
-                         classname, propname, itemid)
-            ]
-        finally:
-            r.close()
+            result = cl.create(**props)
+        except (TypeError, IndexError, ValueError), message:
+            raise UsageError, message
         return result
 
-    def filter(self, username, password, classname, search_matches, filterspec,
-            sort=[], group=[]):
-        r = RoundupRequest(self.tracker, username, password)
-        try:
-            cl = r.get_class(classname)
-            result = cl.filter(search_matches, filterspec, sort=sort, group=group)
-        finally:
-            r.close()
-        return result
+    def set(self, designator, *args):
 
-    def display(self, username, password, designator, *properties):
-        r = RoundupRequest(self.tracker, username, password)
+        classname, itemid = hyperdb.splitDesignator(designator)
+        cl = self.db.getclass(classname)
+        props = props_from_args(self.db, cl, args, itemid) # convert types
+        for p in props.iterkeys():
+            if not self.db.security.hasPermission('Edit', self.db.getuid(),
+                                                  classname, p, itemid):
+                raise Unauthorised('Permission to edit %s of %s denied'%
+                                   (p, designator))
         try:
-            classname, itemid = hyperdb.splitDesignator(designator)
-            cl = r.get_class(classname)
-            props = properties and list(properties) or cl.properties.keys()
-            props.sort()
-            for p in props:
-                if not r.db.security.hasPermission('View', r.userid,
-                        classname, p, itemid):
-                    raise Unauthorised('Permission to view %s of %s denied'%
-                            (p, designator))
-            result = [(prop, cl.get(itemid, prop)) for prop in props]
-        finally:
-            r.close()
-        return dict(result)
+            return cl.set(itemid, **props)
+        except (TypeError, IndexError, ValueError), message:
+            raise UsageError, message
 
-    def create(self, username, password, classname, *args):
-        r = RoundupRequest(self.tracker, username, password)
-        try:
-            if not r.db.security.hasPermission('Create', r.userid, classname):
-                raise Unauthorised('Permission to create %s denied'%classname)
 
-            cl = r.get_class(classname)
+class RoundupDispatcher(SimpleXMLRPCDispatcher):
+    """RoundupDispatcher bridges from cgi.client to RoundupInstance.
+    It expects user authentication to be done."""
 
-            # convert types
-            props = r.props_from_args(cl, args)
+    def __init__(self, db, userid, translator,
+                 allow_none=False, encoding=None):
 
-            # check for the key property
-            key = cl.getkey()
-            if key and not props.has_key(key):
-                raise UsageError, 'you must provide the "%s" property.'%key
+        SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
+        self.register_instance(RoundupInstance(db, userid, translator))
+                 
 
-            # do the actual create
-            try:
-                result = cl.create(**props)
-            except (TypeError, IndexError, ValueError), message:
-                raise UsageError, message
-        finally:
-            r.close()
-        return result
+    def dispatch(self, input):
+        return self._marshaled_dispatch(input)
 
-    def set(self, username, password, designator, *args):
-        r = RoundupRequest(self.tracker, username, password)
-        try:
-            classname, itemid = hyperdb.splitDesignator(designator)
-            cl = r.get_class(classname)
-            props = r.props_from_args(cl, args, itemid) # convert types
-            for p in props.iterkeys ():
-                if not r.db.security.hasPermission('Edit', r.userid,
-                        classname, p, itemid):
-                    raise Unauthorised('Permission to edit %s of %s denied'%
-                        (p, designator))
-            try:
-                return cl.set(itemid, **props)
-            except (TypeError, IndexError, ValueError), message:
-                raise UsageError, message
-        finally:
-            r.close()
+    def _dispatch(self, method, params):
 
-# vim: set et sts=4 sw=4 :
+        retn = SimpleXMLRPCDispatcher._dispatch(self, method, params)
+        retn = translate(retn)
+        return retn
+