pylons.controllers.xmlrpc
Covered: 235 lines
Missed: 0 lines
Skipped 65 lines
Percent: 100 %
  1
"""The base WSGI XMLRPCController"""
  2
import inspect
  3
import logging
  4
import types
  5
import xmlrpclib
  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'))
 21
def xmlrpc_sig(args):
 22
    """Returns a list of the function signature in string format based on a 
 23
    tuple provided by xmlrpclib."""
 24
    signature = []
 25
    for param in args:
 26
        for type, xml_name in XMLRPC_MAPPING:
 27
            if isinstance(param, type):
 28
                signature.append(xml_name)
 29
                break
 30
    return signature
 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>`_ 
 46
    specification.
 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.
 56
    For example::
 58
        class MyXML(XMLRPCController):
 59
            def userstatus(self):
 60
                return 'basic string'
 61
            userstatus.signature = [ ['string'] ]
 63
            def userinfo(self, username, age=None):
 64
                user = LookUpUser(username)
 65
                response = {'username':user.name}
 66
                if age and age > 10:
 67
                    response['age'] = age
 68
                return response
 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
 79
    should be provided.
 81
    Valid types that can be checked in the signature and their
 82
    corresponding Python types::
 84
        'string' - str
 85
        'array' - list
 86
        'boolean' - bool
 87
        'int' - int
 88
        'double' - float
 89
        'struct' - dict
 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)
 97
    .. note::
 99
        Requiring a signature is optional.
101
    """
102
    allow_none = False
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"""
113
        log_debug = self._pylons_log_debug
114
        length = environ.get('CONTENT_LENGTH')
115
        if length:
116
            length = int(length)
117
        else:
119
            if log_debug:
120
                log.debug("No Content-Length found, returning 411 error")
121
            abort(411)
122
        if length > self.max_body_length or length == 0:
123
            if log_debug:
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)
134
        if not func:
135
            if log_debug:
136
                log.debug("Method: %r not found, returning xmlrpc fault",
137
                          method)
138
            return xmlrpc_fault(0, "No such method name %r" %
139
                                method)(environ, start_response)
142
        if hasattr(func, 'signature'):
143
            if log_debug:
144
                log.debug("Checking XMLRPC argument signature")
145
            valid_args = False
146
            params = xmlrpc_sig(rpc_args)
147
            for sig in func.signature:
149
                if len(sig)-1 != len(rpc_args):
150
                    continue
153
                if params == sig[1:]:
154
                    valid_args = True
155
                    break
157
            if not valid_args:
158
                if log_debug:
159
                    log.debug("Bad argument signature recieved, returning "
160
                              "xmlrpc fault")
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)
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
173
        self._func = func
177
        status = []
178
        headers = []
179
        exc_info = []
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])
189
        return output
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)
199
        return response
201
    def _find_method(self, name):
202
        """Locate a method in the controller by the specified name and
203
        return it"""
205
        if name.startswith('_'):
206
            if self._pylons_log_debug:
207
                log.debug("Action starts with _, private action not allowed")
208
            return
210
        if self._pylons_log_debug:
211
            log.debug("Looking for XMLRPC method: %r", name)
212
        try:
213
            func = getattr(self, name, None)
214
        except UnicodeEncodeError:
215
            return
216
        if isinstance(func, types.MethodType):
217
            return func
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'.
225
        """
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'.
234
        """
235
        return name.replace('_', '.')
237
    def system_listMethods(self):
238
        """Returns a list of XML-RPC methods for this XML-RPC resource"""
239
        methods = []
240
        for method in dir(self):
241
            meth = getattr(self, method)
243
            if not method.startswith('_') and isinstance(meth,
244
                                                         types.MethodType):
245
                methods.append(self._publish_method_name(method))
246
        return methods
247
    system_listMethods.signature = [['array']]
249
    def system_methodSignature(self, name):
250
        """Returns an array of array's for the valid signatures for a
251
        method.
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.
257
        """
258
        method = self._find_method(self._find_method_name(name))
259
        if method:
260
            return getattr(method, 'signature', '')
261
        else:
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))
269
        if method:
270
            help = MethodHelp.getdoc(method)
271
            sig = getattr(method, 'signature', None)
272
            if sig:
273
                help += "\n\nMethod signature: %s" % sig
274
            return help
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
281
    methods"""
282
    def __init__(self, doc):
283
        self.__doc__ = doc
285
    def getdoc(method):
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.
291
        """
292
        help = getattr(method, 'help', None)
293
        if help is None:
294
            help = method.__doc__
295
        doc = inspect.getdoc(MethodHelp(help))
296
        if doc is None:
297
            return ''
298
        return doc
299
    getdoc = staticmethod(getdoc)