1
"""The base WSGI XMLRPCController"""
7
from paste.response import replace_header
9
from pylons.controllers import WSGIController
10
from pylons.controllers.util import abort, Response
12
__all__ = ['XMLRPCController']
14
log = logging.getLogger(__name__)
16
XMLRPC_MAPPING = ((basestring, 'string'), (list, 'array'), (bool, 'boolean'),
17
(int, 'int'), (float, 'double'), (dict, 'struct'),
18
(xmlrpclib.DateTime, 'dateTime.iso8601'),
19
(xmlrpclib.Binary, 'base64'))
22
"""Returns a list of the function signature in string format based on a
23
tuple provided by xmlrpclib."""
26
for type, xml_name in XMLRPC_MAPPING:
27
if isinstance(param, type):
28
signature.append(xml_name)
33
def xmlrpc_fault(code, message):
34
"""Convienence method to return a Pylons response XMLRPC Fault"""
35
fault = xmlrpclib.Fault(code, message)
36
return Response(body=xmlrpclib.dumps(fault, methodresponse=True))
39
class XMLRPCController(WSGIController):
40
"""XML-RPC Controller that speaks WSGI
42
This controller handles XML-RPC responses and complies with the
43
`XML-RPC Specification <http://www.xmlrpc.com/spec>`_ as well as
44
the `XML-RPC Introspection
45
<http://scripts.incutio.com/xmlrpc/introspection.html>`_
48
By default, methods with names containing a dot are translated to
49
use an underscore. For example, the `system.methodHelp` is handled
50
by the method :meth:`system_methodHelp`.
52
Methods in the XML-RPC controller will be called with the method
53
given in the XMLRPC body. Methods may be annotated with a signature
54
attribute to declare the valid arguments and return types.
58
class MyXML(XMLRPCController):
61
userstatus.signature = [ ['string'] ]
63
def userinfo(self, username, age=None):
64
user = LookUpUser(username)
65
response = {'username':user.name}
69
userinfo.signature = [['struct', 'string'],
70
['struct', 'string', 'int']]
72
Since XML-RPC methods can take different sets of data, each set of
73
valid arguments is its own list. The first value in the list is the
74
type of the return argument. The rest of the arguments are the
75
types of the data that must be passed in.
77
In the last method in the example above, since the method can
78
optionally take an integer value both sets of valid parameter lists
81
Valid types that can be checked in the signature and their
82
corresponding Python types::
90
'dateTime.iso8601' - xmlrpclib.DateTime
91
'base64' - xmlrpclib.Binary
93
The class variable ``allow_none`` is passed to xmlrpclib.dumps;
94
enabling it allows translating ``None`` to XML (an extension to the
95
XML-RPC specification)
99
Requiring a signature is optional.
103
max_body_length = 4194304
105
def _get_method_args(self):
106
return self.rpc_kargs
108
def __call__(self, environ, start_response):
109
"""Parse an XMLRPC body for the method, and call it with the
110
appropriate arguments"""
111
# Pull out the length, return an error if there is no valid
112
# length or if the length is larger than the max_body_length.
113
log_debug = self._pylons_log_debug
114
length = environ.get('CONTENT_LENGTH')
118
# No valid Content-Length header found
120
log.debug("No Content-Length found, returning 411 error")
122
if length > self.max_body_length or length == 0:
124
log.debug("Content-Length larger than max body length. Max: "
125
"%s, Sent: %s. Returning 413 error",
126
self.max_body_length, length)
127
abort(413, "XML body too large")
129
body = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))
130
rpc_args, orig_method = xmlrpclib.loads(body)
132
method = self._find_method_name(orig_method)
133
func = self._find_method(method)
136
log.debug("Method: %r not found, returning xmlrpc fault",
138
return xmlrpc_fault(0, "No such method name %r" %
139
method)(environ, start_response)
141
# Signature checking for params
142
if hasattr(func, 'signature'):
144
log.debug("Checking XMLRPC argument signature")
146
params = xmlrpc_sig(rpc_args)
147
for sig in func.signature:
148
# Next sig if we don't have the same amount of args
149
if len(sig)-1 != len(rpc_args):
152
# If the params match, we're valid
153
if params == sig[1:]:
159
log.debug("Bad argument signature recieved, returning "
161
msg = ("Incorrect argument signature. %r recieved does not "
162
"match %r signature for method %r" % \
163
(params, func.signature, orig_method))
164
return xmlrpc_fault(0, msg)(environ, start_response)
166
# Change the arg list into a keyword dict based off the arg
167
# names in the functions definition
168
arglist = inspect.getargspec(func)[0][1:]
169
kargs = dict(zip(arglist, rpc_args))
170
kargs['action'], kargs['environ'] = method, environ
171
kargs['start_response'] = start_response
172
self.rpc_kargs = kargs
175
# Now that we know the method is valid, and the args are valid,
176
# we can dispatch control to the default WSGIController
180
def change_content(new_status, new_headers, new_exc_info=None):
181
status.append(new_status)
182
headers.extend(new_headers)
183
exc_info.append(new_exc_info)
184
output = WSGIController.__call__(self, environ, change_content)
185
output = list(output)
186
headers.append(('Content-Length', str(len(output[0]))))
187
replace_header(headers, 'Content-Type', 'text/xml')
188
start_response(status[0], headers, exc_info[0])
191
def _dispatch_call(self):
192
"""Dispatch the call to the function chosen by __call__"""
193
raw_response = self._inspect_call(self._func)
194
if not isinstance(raw_response, xmlrpclib.Fault):
195
raw_response = (raw_response,)
197
response = xmlrpclib.dumps(raw_response, methodresponse=True,
198
allow_none=self.allow_none)
201
def _find_method(self, name):
202
"""Locate a method in the controller by the specified name and
204
# Keep private methods private
205
if name.startswith('_'):
206
if self._pylons_log_debug:
207
log.debug("Action starts with _, private action not allowed")
210
if self._pylons_log_debug:
211
log.debug("Looking for XMLRPC method: %r", name)
213
func = getattr(self, name, None)
214
except UnicodeEncodeError:
216
if isinstance(func, types.MethodType):
219
def _find_method_name(self, name):
220
"""Locate a method in the controller by the appropriate name
222
By default, this translates method names like
223
'system.methodHelp' into 'system_methodHelp'.
226
return name.replace('.', '_')
228
def _publish_method_name(self, name):
229
"""Translate an internal method name to a publicly viewable one
231
By default, this translates internal method names like
232
'blog_view' into 'blog.view'.
235
return name.replace('_', '.')
237
def system_listMethods(self):
238
"""Returns a list of XML-RPC methods for this XML-RPC resource"""
240
for method in dir(self):
241
meth = getattr(self, method)
243
if not method.startswith('_') and isinstance(meth,
245
methods.append(self._publish_method_name(method))
247
system_listMethods.signature = [['array']]
249
def system_methodSignature(self, name):
250
"""Returns an array of array's for the valid signatures for a
253
The first value of each array is the return value of the
254
method. The result is an array to indicate multiple signatures
255
a method may be capable of.
258
method = self._find_method(self._find_method_name(name))
260
return getattr(method, 'signature', '')
262
return xmlrpclib.Fault(0, 'No such method name')
263
system_methodSignature.signature = [['array', 'string'],
264
['string', 'string']]
266
def system_methodHelp(self, name):
267
"""Returns the documentation for a method"""
268
method = self._find_method(self._find_method_name(name))
270
help = MethodHelp.getdoc(method)
271
sig = getattr(method, 'signature', None)
273
help += "\n\nMethod signature: %s" % sig
275
return xmlrpclib.Fault(0, "No such method name")
276
system_methodHelp.signature = [['string', 'string']]
279
class MethodHelp(object):
280
"""Wrapper for formatting doc strings from XMLRPCController
282
def __init__(self, doc):
286
"""Return a formatted doc string, via inspect.getdoc, from the
287
specified XMLRPCController method
289
The method's help attribute is used if it exists, otherwise the
290
method's doc string is used.
292
help = getattr(method, 'help', None)
294
help = method.__doc__
295
doc = inspect.getdoc(MethodHelp(help))
299
getdoc = staticmethod(getdoc)