Skip to content

Commit e9b4eda

Browse files
committed
XMLRPC improvements:
* Add support for actions to XMLRPC interface. * Provide bridge so user actions may be executed either via CGI or XMLRPC. * Adjust XMLRPC tests to recent work. * Cleanup.
1 parent 0aba97f commit e9b4eda

File tree

9 files changed

+207
-53
lines changed

9 files changed

+207
-53
lines changed

roundup/actions.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#
2+
# Copyright (C) 2009 Stefan Seefeld
3+
# All rights reserved.
4+
# For license terms see the file COPYING.txt.
5+
#
6+
7+
from roundup.exceptions import *
8+
from roundup import hyperdb
9+
from roundup.i18n import _
10+
11+
class Action:
12+
def __init__(self, db, translator):
13+
self.db = db
14+
self.translator = translator
15+
16+
def handle(self, *args):
17+
"""Action handler procedure"""
18+
raise NotImplementedError
19+
20+
def execute(self, *args):
21+
"""Execute the action specified by this object."""
22+
23+
self.permission(*args)
24+
return self.handle(*args)
25+
26+
27+
def permission(self, *args):
28+
"""Check whether the user has permission to execute this action.
29+
30+
If not, raise Unauthorised."""
31+
32+
pass
33+
34+
35+
def gettext(self, msgid):
36+
"""Return the localized translation of msgid"""
37+
return self.translator.gettext(msgid)
38+
39+
40+
_ = gettext
41+
42+
43+
class Retire(Action):
44+
45+
def handle(self, designator):
46+
47+
classname, itemid = hyperdb.splitDesignator(designator)
48+
49+
# make sure we don't try to retire admin or anonymous
50+
if (classname == 'user' and
51+
self.db.user.get(itemid, 'username') in ('admin', 'anonymous')):
52+
raise ValueError, self._(
53+
'You may not retire the admin or anonymous user')
54+
55+
# do the retire
56+
self.db.getclass(classname).retire(itemid)
57+
self.db.commit()
58+
59+
60+
def permission(self, designator):
61+
62+
classname, itemid = hyperdb.splitDesignator(designator)
63+
64+
if not self.db.security.hasPermission('Edit', self.db.getuid(),
65+
classname=classname, itemid=itemid):
66+
raise Unauthorised(self._('You do not have permission to '
67+
'%(action)s the %(classname)s class.')%info)
68+

roundup/cgi/actions.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
#$Id: actions.py,v 1.73 2008-08-18 05:04:01 richard Exp $
2-
31
import re, cgi, StringIO, urllib, time, random, csv, codecs
42

53
from roundup import hyperdb, token, date, password
4+
from roundup.actions import Action as BaseAction
65
from roundup.i18n import _
76
import roundup.exceptions
87
from roundup.cgi import exceptions, templating
@@ -991,4 +990,46 @@ def handle(self):
991990

992991
return '\n'
993992

993+
994+
class Bridge(BaseAction):
995+
"""Make roundup.actions.Action executable via CGI request.
996+
997+
Using this allows users to write actions executable from multiple frontends.
998+
CGI Form content is translated into a dictionary, which then is passed as
999+
argument to 'handle()'. XMLRPC requests have to pass this dictionary
1000+
directly.
1001+
"""
1002+
1003+
def __init__(self, *args):
1004+
1005+
# As this constructor is callable from multiple frontends, each with
1006+
# different Action interfaces, we have to look at the arguments to
1007+
# figure out how to complete construction.
1008+
if (len(args) == 1 and
1009+
hasattr(args[0], '__class__') and
1010+
args[0].__class__.__name__ == 'Client'):
1011+
self.cgi = True
1012+
self.execute = self.execute_cgi
1013+
self.client = args[0]
1014+
self.form = self.client.form
1015+
else:
1016+
self.cgi = False
1017+
1018+
def execute_cgi(self):
1019+
args = {}
1020+
for key in self.form.keys():
1021+
args[key] = self.form.getvalue(key)
1022+
self.permission(args)
1023+
return self.handle(args)
1024+
1025+
def permission(self, args):
1026+
"""Raise Unauthorised if the current user is not allowed to execute
1027+
this action. Users may override this method."""
1028+
1029+
pass
1030+
1031+
def handle(self, args):
1032+
1033+
raise NotImplementedError
1034+
9941035
# vim: set filetype=python sts=4 sw=4 et si :

roundup/cgi/client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,9 @@ def handle_xmlrpc(self):
382382
self.determine_user()
383383

384384
# Call the appropriate XML-RPC method.
385-
handler = xmlrpc.RoundupDispatcher(self.db, self.userid, self.translator,
385+
handler = xmlrpc.RoundupDispatcher(self.db,
386+
self.instance.actions,
387+
self.translator,
386388
allow_none=True)
387389
output = handler.dispatch(input)
388390
self.db.commit()

roundup/cgi/exceptions.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
1-
#$Id: exceptions.py,v 1.6 2004-11-18 14:10:27 a1s Exp $
2-
'''Exceptions for use in Roundup's web interface.
3-
'''
1+
"""Exceptions for use in Roundup's web interface.
2+
"""
43

54
__docformat__ = 'restructuredtext'
65

6+
from roundup.exceptions import LoginError, Unauthorised
77
import cgi
88

99
class HTTPException(Exception):
1010
pass
1111

12-
class LoginError(HTTPException):
13-
pass
14-
15-
class Unauthorised(HTTPException):
16-
pass
17-
1812
class Redirect(HTTPException):
1913
pass
2014

@@ -50,13 +44,13 @@ class SeriousError(Exception):
5044
escaped.
5145
"""
5246
def __str__(self):
53-
return '''
47+
return """
5448
<html><head><title>Roundup issue tracker: An error has occurred</title>
5549
<link rel="stylesheet" type="text/css" href="@@file/style.css">
5650
</head>
5751
<body class="body" marginwidth="0" marginheight="0">
5852
<p class="error-message">%s</p>
5953
</body></html>
60-
'''%cgi.escape(self.args[0])
54+
"""%cgi.escape(self.args[0])
6155

6256
# vim: set filetype=python sts=4 sw=4 et si :

roundup/exceptions.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
1-
#$Id: exceptions.py,v 1.1 2004-03-26 00:44:11 richard Exp $
2-
'''Exceptions for use across all Roundup components.
3-
'''
1+
"""Exceptions for use across all Roundup components.
2+
"""
43

54
__docformat__ = 'restructuredtext'
65

6+
class LoginError(Exception):
7+
pass
8+
9+
class Unauthorised(Exception):
10+
pass
11+
712
class Reject(Exception):
8-
'''An auditor may raise this exception when the current create or set
13+
"""An auditor may raise this exception when the current create or set
914
operation should be stopped.
1015
1116
It is up to the specific interface invoking the create or set to
1217
handle this exception sanely. For example:
1318
1419
- mailgw will trap and ignore Reject for file attachments and messages
1520
- cgi will trap and present the exception in a nice format
16-
'''
21+
"""
1722
pass
1823

1924
class UsageError(ValueError):

roundup/instance.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@
1515
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
1616
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
1717
#
18-
# $Id: instance.py,v 1.37 2006-12-11 23:36:15 richard Exp $
1918

20-
'''Tracker handling (open tracker).
19+
"""Tracker handling (open tracker).
2120
2221
Backwards compatibility for the old-style "imported" trackers.
23-
'''
22+
"""
2423
__docformat__ = 'restructuredtext'
2524

2625
import os
2726
import sys
2827
from roundup import configuration, mailgw
29-
from roundup import hyperdb, backends
28+
from roundup import hyperdb, backends, actions
3029
from roundup.cgi import client, templating
30+
from roundup.cgi import actions as cgi_actions
3131

3232
class Vars:
3333
def __init__(self, vars):
@@ -47,6 +47,7 @@ def __init__(self, tracker_home, optimize=0):
4747
self.tracker_home = tracker_home
4848
self.optimize = optimize
4949
self.config = configuration.CoreConfig(tracker_home)
50+
self.actions = {}
5051
self.cgi_actions = {}
5152
self.templating_utils = {}
5253
self.load_interfaces()
@@ -182,7 +183,20 @@ def _load_python(self, file, vars):
182183
return vars
183184

184185
def registerAction(self, name, action):
185-
self.cgi_actions[name] = action
186+
187+
# The logic here is this:
188+
# * if `action` derives from actions.Action,
189+
# it is executable as a generic action.
190+
# * if, moreover, it also derives from cgi.actions.Bridge,
191+
# it may in addition be called via CGI
192+
# * in all other cases we register it as a CGI action, without
193+
# any check (for backward compatibility).
194+
if issubclass(action, actions.Action):
195+
self.actions[name] = action
196+
if issubclass(action, cgi_actions.Bridge):
197+
self.cgi_actions[name] = action
198+
else:
199+
self.cgi_actions[name] = action
186200

187201
def registerUtil(self, name, function):
188202
self.templating_utils[name] = function

roundup/scripts/roundup_xmlrpc_server.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ def authenticate(self, tracker):
5151

5252
if scheme.lower() == 'basic':
5353
decoded = base64.decodestring(challenge)
54-
username, password = decoded.split(':')
54+
if ':' in decoded:
55+
username, password = decoded.split(':')
56+
else:
57+
username = decoded
5558
if not username:
5659
username = 'anonymous'
5760
db = tracker.open('admin')
@@ -79,7 +82,7 @@ def do_POST(self):
7982
tracker = self.get_tracker(tracker_name)
8083
db = self.authenticate(tracker)
8184

82-
instance = RoundupInstance(db, None)
85+
instance = RoundupInstance(db, tracker.actions, None)
8386
self.server.register_instance(instance)
8487
SimpleXMLRPCRequestHandler.do_POST(self)
8588
except Unauthorised, message:

roundup/xmlrpc.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from roundup.cgi.exceptions import *
99
from roundup.exceptions import UsageError
1010
from roundup.date import Date, Range, Interval
11+
from roundup import actions
1112
from SimpleXMLRPCServer import *
1213

1314
def translate(value):
@@ -52,10 +53,10 @@ class RoundupInstance:
5253
"""The RoundupInstance provides the interface accessible through
5354
the Python XMLRPC mapping."""
5455

55-
def __init__(self, db, translator):
56+
def __init__(self, db, actions, translator):
5657

5758
self.db = db
58-
self.userid = db.getuid()
59+
self.actions = actions
5960
self.translator = translator
6061

6162
def list(self, classname, propname=None):
@@ -125,15 +126,30 @@ def set(self, designator, *args):
125126
raise UsageError, message
126127

127128

129+
builtin_actions = {'retire': actions.Retire}
130+
131+
def action(self, name, *args):
132+
""""""
133+
134+
if name in self.actions:
135+
action_type = self.actions[name]
136+
elif name in self.builtin_actions:
137+
action_type = self.builtin_actions[name]
138+
else:
139+
raise Exception('action "%s" is not supported %s' % (name, ','.join(self.actions.keys())))
140+
action = action_type(self.db, self.translator)
141+
return action.execute(*args)
142+
143+
128144
class RoundupDispatcher(SimpleXMLRPCDispatcher):
129145
"""RoundupDispatcher bridges from cgi.client to RoundupInstance.
130146
It expects user authentication to be done."""
131147

132-
def __init__(self, db, userid, translator,
148+
def __init__(self, db, actions, translator,
133149
allow_none=False, encoding=None):
134150

135151
SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
136-
self.register_instance(RoundupInstance(db, userid, translator))
152+
self.register_instance(RoundupInstance(db, actions, translator))
137153

138154

139155
def dispatch(self, input):

0 commit comments

Comments
 (0)