| 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 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 | def jsonify(func, *args, **kwargs): |
|---|
| 27 | """Action decorator that formats output for JSON |
|---|
| 28 | |
|---|
| 29 | Given a function that will return content, this decorator will turn |
|---|
| 30 | the result into JSON, with a content-type of 'application/json' and |
|---|
| 31 | output it. |
|---|
| 32 | |
|---|
| 33 | """ |
|---|
| 34 | pylons = get_pylons(args) |
|---|
| 35 | pylons.response.headers['Content-Type'] = 'application/json' |
|---|
| 36 | data = func(*args, **kwargs) |
|---|
| 37 | if isinstance(data, (list, tuple)): |
|---|
| 38 | msg = "JSON responses with Array envelopes are susceptible to " \ |
|---|
| 39 | "cross-site data leak attacks, see " \ |
|---|
| 40 | "http://pylonshq.com/warnings/JSONArray" |
|---|
| 41 | warnings.warn(msg, Warning, 2) |
|---|
| 42 | log.warning(msg) |
|---|
| 43 | log.debug("Returning JSON wrapped action output") |
|---|
| 44 | return simplejson.dumps(data) |
|---|
| 45 | jsonify = decorator(jsonify) |
|---|
| 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 | is_unicode_params = isinstance(params, UnicodeMultiDict) |
|---|
| 124 | params = params.mixed() |
|---|
| 125 | if variable_decode: |
|---|
| 126 | log.debug("Running variable_decode on params") |
|---|
| 127 | decoded = variabledecode.variable_decode(params, dict_char, |
|---|
| 128 | list_char) |
|---|
| 129 | else: |
|---|
| 130 | decoded = params |
|---|
| 131 | |
|---|
| 132 | if schema: |
|---|
| 133 | log.debug("Validating against a schema") |
|---|
| 134 | try: |
|---|
| 135 | self.form_result = schema.to_python(decoded, state) |
|---|
| 136 | except formencode.Invalid, e: |
|---|
| 137 | errors = e.unpack_errors(variable_decode, dict_char, list_char) |
|---|
| 138 | if validators: |
|---|
| 139 | log.debug("Validating against provided validators") |
|---|
| 140 | if isinstance(validators, dict): |
|---|
| 141 | if not hasattr(self, 'form_result'): |
|---|
| 142 | self.form_result = {} |
|---|
| 143 | for field, validator in validators.iteritems(): |
|---|
| 144 | try: |
|---|
| 145 | self.form_result[field] = \ |
|---|
| 146 | validator.to_python(decoded.get(field), state) |
|---|
| 147 | except formencode.Invalid, error: |
|---|
| 148 | errors[field] = error |
|---|
| 149 | if errors: |
|---|
| 150 | log.debug("Errors found in validation, parsing form with htmlfill " |
|---|
| 151 | "for errors") |
|---|
| 152 | request.environ['REQUEST_METHOD'] = 'GET' |
|---|
| 153 | self._py_object.tmpl_context.form_errors = errors |
|---|
| 154 | |
|---|
| 155 | # If there's no form supplied, just continue with the current |
|---|
| 156 | # function call. |
|---|
| 157 | if not form: |
|---|
| 158 | return func(self, *args, **kwargs) |
|---|
| 159 | |
|---|
| 160 | request.environ['pylons.routes_dict']['action'] = form |
|---|
| 161 | response = self._dispatch_call() |
|---|
| 162 | # XXX: Legacy WSGIResponse support |
|---|
| 163 | legacy_response = False |
|---|
| 164 | if hasattr(response, 'content'): |
|---|
| 165 | form_content = ''.join(response.content) |
|---|
| 166 | legacy_response = True |
|---|
| 167 | else: |
|---|
| 168 | form_content = response |
|---|
| 169 | response = self._py_object.response |
|---|
| 170 | |
|---|
| 171 | # If the form_content is an exception response, return it |
|---|
| 172 | if hasattr(form_content, '_exception'): |
|---|
| 173 | return form_content |
|---|
| 174 | |
|---|
| 175 | # Ensure htmlfill can safely combine the form_content, params and |
|---|
| 176 | # errors variables (that they're all of the same string type) |
|---|
| 177 | if not is_unicode_params: |
|---|
| 178 | log.debug("Raw string form params: ensuring the '%s' form and " |
|---|
| 179 | "FormEncode errors are converted to raw strings for " |
|---|
| 180 | "htmlfill", form) |
|---|
| 181 | encoding = determine_response_charset(response) |
|---|
| 182 | |
|---|
| 183 | # WSGIResponse's content may (unlikely) be unicode |
|---|
| 184 | if isinstance(form_content, unicode): |
|---|
| 185 | form_content = form_content.encode(encoding, |
|---|
| 186 | response.errors) |
|---|
| 187 | |
|---|
| 188 | # FormEncode>=0.7 errors are unicode (due to being localized |
|---|
| 189 | # via ugettext). Convert any of the possible formencode |
|---|
| 190 | # unpack_errors formats to contain raw strings |
|---|
| 191 | errors = encode_formencode_errors(errors, encoding, |
|---|
| 192 | response.errors) |
|---|
| 193 | elif not isinstance(form_content, unicode): |
|---|
| 194 | log.debug("Unicode form params: ensuring the '%s' form is " |
|---|
| 195 | "converted to unicode for htmlfill", form) |
|---|
| 196 | encoding = determine_response_charset(response) |
|---|
| 197 | form_content = form_content.decode(encoding) |
|---|
| 198 | |
|---|
| 199 | form_content = htmlfill.render(form_content, defaults=params, |
|---|
| 200 | errors=errors, **htmlfill_kwargs) |
|---|
| 201 | if legacy_response: |
|---|
| 202 | # Let the Controller merge the legacy response |
|---|
| 203 | response.content = form_content |
|---|
| 204 | return response |
|---|
| 205 | else: |
|---|
| 206 | return form_content |
|---|
| 207 | return func(self, *args, **kwargs) |
|---|
| 208 | return decorator(wrapper) |
|---|
| 209 | |
|---|
| 210 | |
|---|
| 211 | def determine_response_charset(response): |
|---|
| 212 | """Determine the charset of the specified Response object, |
|---|
| 213 | returning the default system encoding when none is set""" |
|---|
| 214 | charset = response.charset |
|---|
| 215 | if charset is None: |
|---|
| 216 | charset = sys.getdefaultencoding() |
|---|
| 217 | log.debug("Determined result charset to be: %s", charset) |
|---|
| 218 | return charset |
|---|
| 219 | |
|---|
| 220 | |
|---|
| 221 | def encode_formencode_errors(errors, encoding, encoding_errors='strict'): |
|---|
| 222 | """Encode any unicode values contained in a FormEncode errors dict |
|---|
| 223 | to raw strings of the specified encoding""" |
|---|
| 224 | if errors is None or isinstance(errors, str): |
|---|
| 225 | # None or Just incase this is FormEncode<=0.7 |
|---|
| 226 | pass |
|---|
| 227 | elif isinstance(errors, unicode): |
|---|
| 228 | errors = errors.encode(encoding, encoding_errors) |
|---|
| 229 | elif isinstance(errors, dict): |
|---|
| 230 | for key, value in errors.iteritems(): |
|---|
| 231 | errors[key] = encode_formencode_errors(value, encoding, |
|---|
| 232 | encoding_errors) |
|---|
| 233 | else: |
|---|
| 234 | # Fallback to an iterable (a list) |
|---|
| 235 | errors = [encode_formencode_errors(error, encoding, encoding_errors) \ |
|---|
| 236 | for error in errors] |
|---|
| 237 | return errors |
|---|
| 238 | |
|---|
| 239 | |
|---|
| 240 | def pylons_formencode_gettext(value): |
|---|
| 241 | """Translates a string ``value`` using pylons gettext first and if |
|---|
| 242 | that fails, formencode gettext. |
|---|
| 243 | |
|---|
| 244 | This allows to "merge" localized error messages from built-in |
|---|
| 245 | FormEncode's validators with application-specific validators. |
|---|
| 246 | |
|---|
| 247 | """ |
|---|
| 248 | trans = pylons_gettext(value) |
|---|
| 249 | if trans == value: |
|---|
| 250 | # translation failed, try formencode |
|---|
| 251 | trans = api._stdtrans(value) |
|---|
| 252 | return trans |
|---|
| 253 | |
|---|
| 254 | |
|---|
| 255 | class PylonsFormEncodeState(object): |
|---|
| 256 | """A ``state`` for FormEncode validate API that includes smart |
|---|
| 257 | ``_`` hook. |
|---|
| 258 | |
|---|
| 259 | The FormEncode library used by validate() decorator has some |
|---|
| 260 | provision for localizing error messages. In particular, it looks |
|---|
| 261 | for attribute ``_`` in the application-specific state object that |
|---|
| 262 | gets passed to every ``.to_python()`` call. If it is found, the |
|---|
| 263 | ``_`` is assumed to be a gettext-like function and is called to |
|---|
| 264 | localize error messages. |
|---|
| 265 | |
|---|
| 266 | One complication is that FormEncode ships with localized error |
|---|
| 267 | messages for standard validators so the user may want to re-use |
|---|
| 268 | them instead of gathering and translating everything from scratch. |
|---|
| 269 | To allow this, we pass as ``_`` a function which looks up |
|---|
| 270 | translation both in application and formencode message catalogs. |
|---|
| 271 | |
|---|
| 272 | """ |
|---|
| 273 | _ = staticmethod(pylons_formencode_gettext) |
|---|