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

      6

      7

    from paste.response import replace_header

      8

      9

    from pylons.controllers import WSGIController

     10

    from pylons.controllers.util import abort, Response

     11

     12

    __all__ = ['XMLRPCController']

     13

     14

    log = logging.getLogger(__name__)

     15

     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'))

     20

     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

     31

     32

     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))

     37

     38

     39

    class XMLRPCController(WSGIController):

     40

        """XML-RPC Controller that speaks WSGI

     41

     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.

     47

     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`.

     51

     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.

     55

     56

        For example::

     57

     58

            class MyXML(XMLRPCController):

     59

                def userstatus(self):

     60

                    return 'basic string'

     61

                userstatus.signature = [ ['string'] ]

     62

     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']]

     71

     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.

     76

     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.

     80

     81

        Valid types that can be checked in the signature and their

     82

        corresponding Python types::

     83

     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

     92

     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)

     96

     97

        .. note::

     98

     99

            Requiring a signature is optional.

    100

    101

        """

    102

        allow_none = False

    103

        max_body_length = 4194304

    104

    105

        def _get_method_args(self):

    106

            return self.rpc_kargs

    107

    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')

    115

            if length:

    116

                length = int(length)

    117

            else:

    118

                # No valid Content-Length header found

    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")

    128

    129

            body = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))

    130

            rpc_args, orig_method = xmlrpclib.loads(body)

    131

    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)

    140

    141

            # Signature checking for params

    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:

    148

                    # Next sig if we don't have the same amount of args

    149

                    if len(sig)-1 != len(rpc_args):

    150

                        continue

    151

    152

                    # If the params match, we're valid

    153

                    if params == sig[1:]:

    154

                        valid_args = True

    155

                        break

    156

    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)

    165

    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

    173

            self._func = func

    174

    175

            # Now that we know the method is valid, and the args are valid,

    176

            # we can dispatch control to the default WSGIController

    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

    190

    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,)

    196

    197

            response = xmlrpclib.dumps(raw_response, methodresponse=True,

    198

                                       allow_none=self.allow_none)

    199

            return response

    200

    201

        def _find_method(self, name):

    202

            """Locate a method in the controller by the specified name and

    203

            return it"""

    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")

    208

                return

    209

    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

    218

    219

        def _find_method_name(self, name):

    220

            """Locate a method in the controller by the appropriate name

    221

    222

            By default, this translates method names like

    223

            'system.methodHelp' into 'system_methodHelp'.

    224

    225

            """

    226

            return name.replace('.', '_')

    227

    228

        def _publish_method_name(self, name):

    229

            """Translate an internal method name to a publicly viewable one

    230

    231

            By default, this translates internal method names like

    232

            'blog_view' into 'blog.view'.

    233

    234

            """

    235

            return name.replace('_', '.')

    236

    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)

    242

    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']]

    248

    249

        def system_methodSignature(self, name):

    250

            """Returns an array of array's for the valid signatures for a

    251

            method.

    252

    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.

    256

    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']]

    265

    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']]

    277

    278

    279

    class MethodHelp(object):

    280

        """Wrapper for formatting doc strings from XMLRPCController

    281

        methods"""

    282

        def __init__(self, doc):

    283

            self.__doc__ = doc

    284

    285

        def getdoc(method):

    286

            """Return a formatted doc string, via inspect.getdoc, from the

    287

            specified XMLRPCController method

    288

    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)

    300