0001"""Pylons Decorators: ``jsonify``, ``validate``, REST, and Cache decorators"""
0002import logging
0003import sys
0004import warnings
0005
0006import formencode
0007import formencode.variabledecode as variabledecode
0008import simplejson
0009from decorator import decorator
0010from formencode import htmlfill
0011from paste.util.multidict import UnicodeMultiDict
0012
0013import pylons
0014
0015__all__ = ['jsonify', 'validate']
0016
0017log = logging.getLogger(__name__)
0018
0019def jsonify(func, *args, **kwargs):
0020 """Action decorator that formats output for JSON
0021
0022 Given a function that will return content, this decorator will
0023 turn the result into JSON, with a content-type of 'text/javascript'
0024 and output it.
0025 """
0026 pylons.response.headers['Content-Type'] = 'text/javascript'
0027 data = func(*args, **kwargs)
0028 if isinstance(data, list):
0029 msg = "JSON responses with Array envelopes are susceptible to " "cross-site data leak attacks, see " "http://pylonshq.com/warnings/JSONArray"
0032 warnings.warn(msg, Warning, 2)
0033 log.warning(msg)
0034 log.debug("Returning JSON wrapped action output")
0035 return simplejson.dumps(data)
0036jsonify = decorator(jsonify)
0037
0038def validate(schema=None, validators=None, form=None, variable_decode=False,
0039 dict_char='.', list_char='-', post_only=True, state=None,
0040 **htmlfill_kwargs):
0041 """Validate input either for a FormEncode schema, or individual validators
0042
0043 Given a form schema or dict of validators, validate will attempt to
0044 validate the schema or validator list.
0045
0046 If validation was succesfull, the valid result dict will be saved
0047 as ``self.form_result``. Otherwise, the action will be re-run as if it was
0048 a GET, and the output will be filled by FormEncode's htmlfill to fill in
0049 the form field errors.
0050
0051 If you'd like validate to also check GET (query) variables (**not** GET
0052 requests!) during its validation, set the ``post_only`` keyword argument
0053 to False.
0054
0055 .. warning::
0056 ``post_only`` applies to *where* the arguments to be validated come
0057 from. It does *not* restrict the form to only working with post, merely
0058 only checking POST vars.
0059
0060 Example:
0061
0062 .. code-block:: Python
0063
0064 class SomeController(BaseController):
0065
0066 def create(self, id):
0067 return render('/myform.mako')
0068
0069 @validate(schema=model.forms.myshema(), form='create')
0070 def update(self, id):
0071 # Do something with self.form_result
0072 pass
0073 """
0074 def wrapper(func, self, *args, **kwargs):
0075 """Decorator Wrapper function"""
0076 request = pylons.request._current_obj()
0077 errors = {}
0078 if post_only:
0079 params = request.POST
0080 else:
0081 params = request.params
0082 is_unicode_params = isinstance(params, UnicodeMultiDict)
0083 params = params.mixed()
0084 if variable_decode:
0085 log.debug("Running variable_decode on params")
0086 decoded = variabledecode.variable_decode(params, dict_char,
0087 list_char)
0088 else:
0089 decoded = params
0090
0091 if schema:
0092 log.debug("Validating against a schema")
0093 try:
0094 self.form_result = schema.to_python(decoded, state)
0095 except formencode.Invalid, e:
0096 errors = e.unpack_errors(variable_decode, dict_char, list_char)
0097 if validators:
0098 log.debug("Validating against provided validators")
0099 if isinstance(validators, dict):
0100 if not hasattr(self, 'form_result'):
0101 self.form_result = {}
0102 for field, validator in validators.iteritems():
0103 try:
0104 self.form_result[field] = validator.to_python(decoded.get(field), state)
0106 except formencode.Invalid, error:
0107 errors[field] = error
0108 if errors:
0109 log.debug("Errors found in validation, parsing form with htmlfill "
0110 "for errors")
0111 request.environ['REQUEST_METHOD'] = 'GET'
0112 pylons.c.form_errors = errors
0113
0114 # If there's no form supplied, just continue with the current
0115 # function call.
0116 if not form:
0117 return func(self, *args, **kwargs)
0118
0119 request.environ['pylons.routes_dict']['action'] = form
0120 response = self._dispatch_call()
0121 # XXX: Legacy WSGIResponse support
0122 legacy_response = False
0123 if hasattr(response, 'wsgi_response'):
0124 form_content = ''.join(response.content)
0125 legacy_response = True
0126 else:
0127 form_content = response
0128 response = pylons.response._current_obj()
0129
0130 # Ensure htmlfill can safely combine the form_content, params and
0131 # errors variables (that they're all of the same string type)
0132 if not is_unicode_params:
0133 log.debug("Raw string form params: ensuring the '%s' form and "
0134 "FormEncode errors are converted to raw strings for "
0135 "htmlfill", form)
0136 encoding = determine_response_charset(response)
0137
0138 # WSGIResponse's content may (unlikely) be unicode
0139 if isinstance(form_content, unicode):
0140 form_content = form_content.encode(encoding,
0141 response.errors)
0142
0143 # FormEncode>=0.7 errors are unicode (due to being localized
0144 # via ugettext). Convert any of the possible formencode
0145 # unpack_errors formats to contain raw strings
0146 errors = encode_formencode_errors(errors, encoding,
0147 response.errors)
0148 elif not isinstance(form_content, unicode):
0149 log.debug("Unicode form params: ensuring the '%s' form is "
0150 "converted to unicode for htmlfill", form)
0151 encoding = determine_response_charset(response)
0152 form_content = form_content.decode(encoding)
0153
0154 form_content = htmlfill.render(form_content, defaults=params,
0155 errors=errors, **htmlfill_kwargs)
0156 if legacy_response:
0157 # Let the Controller merge the legacy response
0158 response.content = form_content
0159 return response
0160 else:
0161 return form_content
0162 return func(self, *args, **kwargs)
0163 return decorator(wrapper)
0164
0165def determine_response_charset(response):
0166 """Determine the charset of the specified Response object, returning the
0167 default system encoding when none is set"""
0168 charset = response.determine_charset()
0169 if charset is None:
0170 charset = sys.getdefaultencoding()
0171 log.debug("Determined result charset to be: %s", charset)
0172 return charset
0173
0174def encode_formencode_errors(errors, encoding, encoding_errors='strict'):
0175 """Encode any unicode values contained in a FormEncode errors dict to raw
0176 strings of the specified encoding"""
0177 if errors is None or isinstance(errors, str):
0178 # None or Just incase this is FormEncode<=0.7
0179 pass
0180 elif isinstance(errors, unicode):
0181 errors = errors.encode(encoding, encoding_errors)
0182 elif isinstance(errors, dict):
0183 for key, value in errors.iteritems():
0184 errors[key] = encode_formencode_errors(value, encoding,
0185 encoding_errors)
0186 else:
0187 # Fallback to an iterable (a list)
0188 errors = [encode_formencode_errors(error, encoding, encoding_errors) for error in errors]
0190 return errors