0001"""The base WSGI XMLRPCController"""
0002import inspect
0003import logging
0004import sys
0005import xmlrpclib
0006
0007from paste.response import replace_header
0008from paste.wsgiwrappers import WSGIResponse
0009
0010from pylons.controllers import WSGIController
0011from pylons.controllers.util import abort
0012
0013__all__ = ['XMLRPCController']
0014
0015log = logging.getLogger(__name__)
0016
0017XMLRPC_MAPPING = ((basestring, 'string'), (list, 'array'), (bool, 'boolean'),
0018 (int, 'int'), (float, 'double'), (dict, 'struct'),
0019 (xmlrpclib.DateTime, 'dateTime.iso8601'),
0020 (xmlrpclib.Binary, 'base64'))
0021
0022def xmlrpc_sig(args):
0023 """Returns a list of the function signature in string format based on a
0024 tuple provided by xmlrpclib."""
0025 signature = []
0026 for param in args:
0027 for type, xml_name in XMLRPC_MAPPING:
0028 if isinstance(param, type):
0029 signature.append(xml_name)
0030 break
0031 return signature
0032
0033
0034def xmlrpc_fault(code, message):
0035 """Convienence method to return a Pylons response XMLRPC Fault"""
0036 fault = xmlrpclib.Fault(code, message)
0037 return WSGIResponse(xmlrpclib.dumps(fault, methodresponse=True))
0038
0039
0040class XMLRPCController(WSGIController):
0041 """XML-RPC Controller that speaks WSGI
0042
0043 This controller handles XML-RPC responses and complies with the
0044 `XML-RPC Specification <http://www.xmlrpc.com/spec>`_ as well as the
0045 `XML-RPC Introspection
0046 <http://scripts.incutio.com/xmlrpc/introspection.html>`_ specification.
0047
0048 By default, methods with names containing a dot are translated to use an
0049 underscore. For example, the `system.methodHelp` is handled by the method
0050 `system_methodHelp`.
0051
0052 Methods in the XML-RPC controller will be called with the method given in
0053 the XMLRPC body. Methods may be annotated with a signature attribute to
0054 declare the valid arguments and return types.
0055
0056 For example::
0057
0058 class MyXML(XMLRPCController):
0059 def userstatus(self):
0060 return 'basic string'
0061 userstatus.signature = [ ['string'] ]
0062
0063 def userinfo(self, username, age=None):
0064 user = LookUpUser(username)
0065 response = {'username':user.name}
0066 if age and age > 10:
0067 response['age'] = age
0068 return response
0069 userinfo.signature = [['struct', 'string'],
0070 ['struct', 'string', 'int']]
0071
0072 Since XML-RPC methods can take different sets of data, each set of valid
0073 arguments is its own list. The first value in the list is the type of the
0074 return argument. The rest of the arguments are the types of the data that
0075 must be passed in.
0076
0077 In the last method in the example above, since the method can optionally
0078 take an integer value both sets of valid parameter lists should be
0079 provided.
0080
0081 Valid types that can be checked in the signature and their corresponding
0082 Python types::
0083
0084 'string' - str
0085 'array' - list
0086 'boolean' - bool
0087 'int' - int
0088 'double' - float
0089 'struct' - dict
0090 'dateTime.iso8601' - xmlrpclib.DateTime
0091 'base64' - xmlrpclib.Binary
0092
0093 The class variable ``allow_none`` is passed to xmlrpclib.dumps; enabling it
0094 allows translating ``None`` to XML (an extension to the XML-RPC
0095 specification)
0096
0097 Note::
0098
0099 Requiring a signature is optional.
0100 """
0101 allow_none = False
0102 max_body_length = 4194304
0103
0104 def _get_method_args(self):
0105 return self.rpc_kargs
0106
0107 def __call__(self, environ, start_response):
0108 """Parse an XMLRPC body for the method, and call it with the
0109 appropriate arguments"""
0110 # Pull out the length, return an error if there is no valid
0111 # length or if the length is larger than the max_body_length.
0112 length = environ.get('CONTENT_LENGTH')
0113 if length:
0114 length = int(length)
0115 else:
0116 # No valid Content-Length header found
0117 log.debug("No Content-Length found, returning 411 error")
0118 abort(411)
0119 if length > self.max_body_length or length == 0:
0120 log.debug("Content-Length larger than max body length. Max: %s,"
0121 " Sent: %s. Returning 413 error", self.max_body_length,
0122 length)
0123 abort(413, "XML body too large")
0124
0125 body = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))
0126 rpc_args, orig_method = xmlrpclib.loads(body)
0127
0128 method = self._find_method_name(orig_method)
0129 func = self._find_method(method)
0130 if not func:
0131 log.debug("Method: %r not found, returning xmlrpc fault", method)
0132 return xmlrpc_fault(0, "No such method name")(environ,
0133 start_response)
0134
0135 # Signature checking for params
0136 if hasattr(func, 'signature'):
0137 log.debug("Checking XMLRPC argument signature")
0138 valid_args = False
0139 params = xmlrpc_sig(rpc_args)
0140 for sig in func.signature:
0141 # Next sig if we don't have the same amount of args
0142 if len(sig)-1 != len(rpc_args):
0143 continue
0144
0145 # If the params match, we're valid
0146 if params == sig[1:]:
0147 valid_args = True
0148 break
0149
0150 if not valid_args:
0151 log.debug("Bad argument signature recieved, returning xmlrpc"
0152 " fault")
0153 msg = ("Incorrect argument signature. %r recieved does not "
0154 "match %r signature for method %r" % (params, func.signature, orig_method))
0156 return xmlrpc_fault(0, msg)(environ, start_response)
0157
0158 # Change the arg list into a keyword dict based off the arg
0159 # names in the functions definition
0160 arglist = inspect.getargspec(func)[0][1:]
0161 kargs = dict(zip(arglist, rpc_args))
0162 kargs['action'], kargs['environ'] = method, environ
0163 kargs['start_response'] = start_response
0164 self.rpc_kargs = kargs
0165 self._func = func
0166
0167 # Now that we know the method is valid, and the args are valid,
0168 # we can dispatch control to the default WSGIController
0169 status = []
0170 headers = []
0171 exc_info = []
0172 def change_content(new_status, new_headers, new_exc_info=None):
0173 status.append(new_status)
0174 headers.extend(new_headers)
0175 exc_info.append(new_exc_info)
0176 output = WSGIController.__call__(self, environ, change_content)
0177 replace_header(headers, 'Content-Type', 'text/xml')
0178 start_response(status[0], headers, exc_info[0])
0179 return output
0180
0181 def _dispatch_call(self):
0182 """Dispatch the call to the function chosen by __call__"""
0183 raw_response = self._inspect_call(self._func)
0184 if not isinstance(raw_response, xmlrpclib.Fault):
0185 raw_response = (raw_response,)
0186
0187 response = xmlrpclib.dumps(raw_response, methodresponse=True,
0188 allow_none=self.allow_none)
0189 return WSGIResponse(response)
0190
0191 def _find_method(self, name):
0192 """Locate a method in the controller by the specified name and return
0193 it
0194 """
0195 log.debug("Looking for XMLRPC method: %r", name)
0196 try:
0197 return getattr(self, name, None)
0198 except UnicodeEncodeError:
0199 return None
0200
0201 def _find_method_name(self, name):
0202 """Locate a method in the controller by the appropriate name
0203
0204 By default, this translates method names like 'system.methodHelp' into
0205 'system_methodHelp'.
0206 """
0207 return name.replace('.', '_')
0208
0209 def _publish_method_name(self, name):
0210 """Translate an internal method name to a publicly viewable one
0211
0212 By default, this translates internal method names like 'blog_view' into
0213 'blog.view'.
0214 """
0215 return name.replace('_', '.')
0216
0217 def system_listMethods(self):
0218 """Returns a list of XML-RPC methods for this XML-RPC resource"""
0219 methods = []
0220 for method in dir(self):
0221 meth = getattr(self, method)
0222
0223 # Only methods have this attribute
0224 if not method.startswith('_') and hasattr(meth, 'im_self'):
0225 methods.append(self._publish_method_name(method))
0226 return methods
0227 system_listMethods.signature = [['array']]
0228
0229 def system_methodSignature(self, name):
0230 """Returns an array of array's for the valid signatures for a method.
0231
0232 The first value of each array is the return value of the method. The
0233 result is an array to indicate multiple signatures a method may be
0234 capable of.
0235 """
0236 method = self._find_method(self._find_method_name(name))
0237 if method:
0238 return getattr(method, 'signature', '')
0239 else:
0240 return xmlrpclib.Fault(0, 'No such method name')
0241 system_methodSignature.signature = [['array', 'string'],
0242 ['string', 'string']]
0243
0244 def system_methodHelp(self, name):
0245 """Returns the documentation for a method"""
0246 method = self._find_method(self._find_method_name(name))
0247 if method:
0248 help = MethodHelp.getdoc(method)
0249 sig = getattr(method, 'signature', None)
0250 if sig:
0251 help += "\n\nMethod signature: %s" % sig
0252 return help
0253 return xmlrpclib.Fault(0, "No such method name")
0254 system_methodHelp.signature = [['string', 'string']]
0255
0256
0257class MethodHelp(object):
0258 """Wrapper for formatting doc strings from XMLRPCController methods"""
0259 def __init__(self, doc):
0260 self.__doc__ = doc
0261
0262 def getdoc(method):
0263 """Return a formatted doc string, via inspect.getdoc, from the
0264 specified XMLRPCController method
0265
0266 The method's help attribute is used if it exists, otherwise the
0267 method's doc string is used.
0268 """
0269 help = getattr(method, 'help', None)
0270 if help is None:
0271 help = method.__doc__
0272 doc = inspect.getdoc(MethodHelp(help))
0273 if doc is None:
0274 return ''
0275 return doc
0276 getdoc = staticmethod(getdoc)