pylons.decorators
Covered: 146 lines
Missed: 17 lines
Skipped 45 lines
Percent: 89 %

      1

    """Pylons Decorators

      2

      3

    Common decorators intended for use in controllers. Additional

      4

    decorators for use with controllers are in the

      5

    :mod:`~pylons.decorators.cache`, :mod:`~pylons.decorators.rest` and

      6

    :mod:`~pylons.decorators.secure` modules.

      7

      8

    """

      9

    import logging

     10

    import sys

     11

    import warnings

     12

     13

    import formencode

     14

    import simplejson

     15

    from decorator import decorator

     16

    from formencode import api, htmlfill, variabledecode

     17

    from webob.multidict import UnicodeMultiDict

     18

     19

    from pylons.decorators.util import get_pylons

     20

    from pylons.i18n import _ as pylons_gettext

     21

     22

    __all__ = ['jsonify', 'validate']

     23

     24

    log = logging.getLogger(__name__)

     25

     26

    @decorator

     27

    def jsonify(func, *args, **kwargs):

     28

        """Action decorator that formats output for JSON

     29

     30

        Given a function that will return content, this decorator will turn

     31

        the result into JSON, with a content-type of 'application/json' and

     32

        output it.

     33

     34

        """

     35

        pylons = get_pylons(args)

     36

        pylons.response.headers['Content-Type'] = 'application/json'

     37

        data = func(*args, **kwargs)

     38

        if isinstance(data, (list, tuple)):

     39

            msg = "JSON responses with Array envelopes are susceptible to " \

     40

                  "cross-site data leak attacks, see " \

     41

                  "http://pylonshq.com/warnings/JSONArray"

     42

            warnings.warn(msg, Warning, 2)

     43

            log.warning(msg)

     44

        log.debug("Returning JSON wrapped action output")

     45

        return simplejson.dumps(data)

     46

     47

     48

    def validate(schema=None, validators=None, form=None, variable_decode=False,

     49

                 dict_char='.', list_char='-', post_only=True, state=None,

     50

                 on_get=False, **htmlfill_kwargs):

     51

        """Validate input either for a FormEncode schema, or individual

     52

        validators

     53

     54

        Given a form schema or dict of validators, validate will attempt to

     55

        validate the schema or validator list.

     56

     57

        If validation was successful, the valid result dict will be saved

     58

        as ``self.form_result``. Otherwise, the action will be re-run as if

     59

        it was a GET, and the output will be filled by FormEncode's

     60

        htmlfill to fill in the form field errors.

     61

     62

        ``schema``

     63

            Refers to a FormEncode Schema object to use during validation.

     64

        ``form``

     65

            Method used to display the form, which will be used to get the

     66

            HTML representation of the form for error filling.

     67

        ``variable_decode``

     68

            Boolean to indicate whether FormEncode's variable decode

     69

            function should be run on the form input before validation.

     70

        ``dict_char``

     71

            Passed through to FormEncode. Toggles the form field naming

     72

            scheme used to determine what is used to represent a dict. This

     73

            option is only applicable when used with variable_decode=True.

     74

        ``list_char``

     75

            Passed through to FormEncode. Toggles the form field naming

     76

            scheme used to determine what is used to represent a list. This

     77

            option is only applicable when used with variable_decode=True.

     78

        ``post_only``

     79

            Boolean that indicates whether or not GET (query) variables

     80

            should be included during validation.

     81

     82

            .. warning::

     83

                ``post_only`` applies to *where* the arguments to be

     84

                validated come from. It does *not* restrict the form to

     85

                only working with post, merely only checking POST vars.

     86

        ``state``

     87

            Passed through to FormEncode for use in validators that utilize

     88

            a state object.

     89

        ``on_get``

     90

            Whether to validate on GET requests. By default only POST

     91

            requests are validated.

     92

     93

        Example::

     94

     95

            class SomeController(BaseController):

     96

     97

                def create(self, id):

     98

                    return render('/myform.mako')

     99

    100

                @validate(schema=model.forms.myshema(), form='create')

    101

                def update(self, id):

    102

                    # Do something with self.form_result

    103

                    pass

    104

    105

        """

    106

        if state is None:

    107

            state = PylonsFormEncodeState

    108

        def wrapper(func, self, *args, **kwargs):

    109

            """Decorator Wrapper function"""

    110

            request = self._py_object.request

    111

            errors = {}

    112

    113

            # Skip the validation if on_get is False and its a GET

    114

            if not on_get and request.environ['REQUEST_METHOD'] == 'GET':

    115

                return func(self, *args, **kwargs)

    116

    117

            # If they want post args only, use just the post args

    118

            if post_only:

    119

                params = request.POST

    120

            else:

    121

                params = request.params

    122

    123

            params = params.mixed()

    124

            if variable_decode:

    125

                log.debug("Running variable_decode on params")

    126

                decoded = variabledecode.variable_decode(params, dict_char,

    127

                                                         list_char)

    128

            else:

    129

                decoded = params

    130

    131

            if schema:

    132

                log.debug("Validating against a schema")

    133

                try:

    134

                    self.form_result = schema.to_python(decoded, state)

    135

                except formencode.Invalid, e:

    136

                    errors = e.unpack_errors(variable_decode, dict_char, list_char)

    137

            if validators:

    138

                log.debug("Validating against provided validators")

    139

                if isinstance(validators, dict):

    140

                    if not hasattr(self, 'form_result'):

    141

                        self.form_result = {}

    142

                    for field, validator in validators.iteritems():

    143

                        try:

    144

                            self.form_result[field] = \

    145

                                validator.to_python(decoded.get(field), state)

    146

                        except formencode.Invalid, error:

    147

                            errors[field] = error

    148

            if errors:

    149

                log.debug("Errors found in validation, parsing form with htmlfill "

    150

                          "for errors")

    151

                request.environ['REQUEST_METHOD'] = 'GET'

    152

                self._py_object.tmpl_context.form_errors = errors

    153

    154

                # If there's no form supplied, just continue with the current

    155

                # function call.

    156

                if not form:

    157

                    return func(self, *args, **kwargs)

    158

    159

                request.environ['pylons.routes_dict']['action'] = form

    160

                response = self._dispatch_call()

    161

    162

                # If the form_content is an exception response, return it

    163

                if hasattr(response, '_exception'):

    164

                    return response

    165

    166

                htmlfill_kwargs2 = htmlfill_kwargs.copy()

    167

                htmlfill_kwargs2.setdefault('encoding', request.charset)

    168

                return htmlfill.render(response, defaults=params, errors=errors,

    169

                                       **htmlfill_kwargs2)

    170

            return func(self, *args, **kwargs)

    171

        return decorator(wrapper)

    172

    173

    174

    def pylons_formencode_gettext(value):

    175

        """Translates a string ``value`` using pylons gettext first and if

    176

        that fails, formencode gettext.

    177

    178

        This allows to "merge" localized error messages from built-in

    179

        FormEncode's validators with application-specific validators.

    180

    181

        """

    182

        trans = pylons_gettext(value)

    183

        if trans == value:

    184

            # translation failed, try formencode

    185

            trans = api._stdtrans(value)

    186

        return trans

    187

    188

    189

    class PylonsFormEncodeState(object):

    190

        """A ``state`` for FormEncode validate API that includes smart

    191

        ``_`` hook.

    192

    193

        The FormEncode library used by validate() decorator has some

    194

        provision for localizing error messages. In particular, it looks

    195

        for attribute ``_`` in the application-specific state object that

    196

        gets passed to every ``.to_python()`` call. If it is found, the

    197

        ``_`` is assumed to be a gettext-like function and is called to

    198

        localize error messages.

    199

    200

        One complication is that FormEncode ships with localized error

    201

        messages for standard validators so the user may want to re-use

    202

        them instead of gathering and translating everything from scratch.

    203

        To allow this, we pass as ``_`` a function which looks up

    204

        translation both in application and formencode message catalogs.

    205

    206

        """

    207

        _ = staticmethod(pylons_formencode_gettext)

    208