2016-12-28 18:37:28 +01:00
|
|
|
# When adding new functions/classes to this file, you need to also add
|
|
|
|
# their types to request.pyi in this directory (the mypy stubs file to
|
|
|
|
# make REQ be treated properly by mypy) so that mypy understands them.
|
2016-05-30 22:26:12 +02:00
|
|
|
from __future__ import absolute_import
|
2016-05-29 16:52:55 +02:00
|
|
|
from functools import wraps
|
|
|
|
import ujson
|
2016-05-30 22:26:12 +02:00
|
|
|
from six.moves import zip
|
2016-05-29 16:52:55 +02:00
|
|
|
|
2016-05-25 15:02:02 +02:00
|
|
|
from django.utils.translation import ugettext as _
|
|
|
|
|
JsonableError: Move into a normally-typed file.
The file `zerver/lib/request.py` doesn't have type annotations
of its own; if they did, they would duplicate the annotations that
exist in its stub file `zerver/lib/request.pyi`. The latter exists
so that we can provide types for the highly dynamic `REQ` and
`has_request_variables`, which are beyond the type-checker's ken
to type-check, but we should minimize the scope of code that gets
that kind of treatment and `JsonableError` is not at all the sort of
code that needs it.
So move the definition of `JsonableError` into a file that does
get type-checked.
In doing so, the type-checker points out one issue already:
`__str__` should return a `str`, but we had it returning a `Text`,
which on Python 2 is not the same thing. Indeed, because the
message we pass to the `JsonableError` constructor is generally
translated, it may well be a Unicode string stuffed full of
non-ASCII characters. This is potentially a bit of a landmine.
But (a) it can only possibly matter in Python 2 which we intend to
be off before long, and (b) AFAIK it hasn't been biting us in
practice, so we've probably reasonably well worked around it where
it could matter. Leave it as is.
2017-07-20 00:51:54 +02:00
|
|
|
from zerver.lib.exceptions import JsonableError
|
2016-05-29 16:52:55 +02:00
|
|
|
|
|
|
|
class RequestVariableMissingError(JsonableError):
|
2017-07-20 01:23:12 +02:00
|
|
|
def __init__(self, var_name):
|
2016-05-29 16:52:55 +02:00
|
|
|
self.var_name = var_name
|
|
|
|
|
|
|
|
def to_json_error_msg(self):
|
2016-05-25 15:02:02 +02:00
|
|
|
return _("Missing '%s' argument") % (self.var_name,)
|
2016-05-29 16:52:55 +02:00
|
|
|
|
|
|
|
class RequestVariableConversionError(JsonableError):
|
2017-07-20 01:23:12 +02:00
|
|
|
def __init__(self, var_name, bad_value):
|
2016-05-29 16:52:55 +02:00
|
|
|
self.var_name = var_name
|
|
|
|
self.bad_value = bad_value
|
|
|
|
|
|
|
|
def to_json_error_msg(self):
|
2016-05-25 15:02:02 +02:00
|
|
|
return (_("Bad value for '%(var_name)s': %(value)s") %
|
|
|
|
{'var_name': self.var_name, 'value': self.bad_value})
|
2016-05-29 16:52:55 +02:00
|
|
|
|
|
|
|
# Used in conjunction with @has_request_variables, below
|
|
|
|
class REQ(object):
|
|
|
|
# NotSpecified is a sentinel value for determining whether a
|
|
|
|
# default value was specified for a request variable. We can't
|
|
|
|
# use None because that could be a valid, user-specified default
|
|
|
|
class _NotSpecified(object):
|
|
|
|
pass
|
|
|
|
NotSpecified = _NotSpecified()
|
|
|
|
|
|
|
|
def __init__(self, whence=None, converter=None, default=NotSpecified,
|
|
|
|
validator=None, argument_type=None):
|
|
|
|
"""whence: the name of the request variable that should be used
|
|
|
|
for this parameter. Defaults to a request variable of the
|
|
|
|
same name as the parameter.
|
|
|
|
|
|
|
|
converter: a function that takes a string and returns a new
|
|
|
|
value. If specified, this will be called on the request
|
|
|
|
variable value before passing to the function
|
|
|
|
|
|
|
|
default: a value to be used for the argument if the parameter
|
|
|
|
is missing in the request
|
|
|
|
|
|
|
|
validator: similar to converter, but takes an already parsed JSON
|
|
|
|
data structure. If specified, we will parse the JSON request
|
|
|
|
variable value before passing to the function
|
|
|
|
|
|
|
|
argument_type: pass 'body' to extract the parsed JSON
|
|
|
|
corresponding to the request body
|
|
|
|
"""
|
|
|
|
|
|
|
|
self.post_var_name = whence
|
2017-05-07 17:09:28 +02:00
|
|
|
self.func_var_name = None # type: str
|
2016-05-29 16:52:55 +02:00
|
|
|
self.converter = converter
|
|
|
|
self.validator = validator
|
|
|
|
self.default = default
|
|
|
|
self.argument_type = argument_type
|
|
|
|
|
|
|
|
if converter and validator:
|
2017-03-09 09:16:15 +01:00
|
|
|
# Not user-facing, so shouldn't be tagged for translation
|
|
|
|
raise AssertionError('converter and validator are mutually exclusive')
|
2016-05-29 16:52:55 +02:00
|
|
|
|
|
|
|
# Extracts variables from the request object and passes them as
|
|
|
|
# named function arguments. The request object must be the first
|
|
|
|
# argument to the function.
|
|
|
|
#
|
|
|
|
# To use, assign a function parameter a default value that is an
|
2016-10-09 14:33:17 +02:00
|
|
|
# instance of the REQ class. That parameter will then be automatically
|
2016-05-29 16:52:55 +02:00
|
|
|
# populated from the HTTP request. The request object must be the
|
|
|
|
# first argument to the decorated function.
|
|
|
|
#
|
|
|
|
# This should generally be the innermost (syntactically bottommost)
|
|
|
|
# decorator applied to a view, since other decorators won't preserve
|
|
|
|
# the default parameter values used by has_request_variables.
|
|
|
|
#
|
|
|
|
# Note that this can't be used in helper functions which are not
|
|
|
|
# expected to call json_error or json_success, as it uses json_error
|
|
|
|
# internally when it encounters an error
|
|
|
|
def has_request_variables(view_func):
|
|
|
|
num_params = view_func.__code__.co_argcount
|
|
|
|
if view_func.__defaults__ is None:
|
|
|
|
num_default_params = 0
|
|
|
|
else:
|
|
|
|
num_default_params = len(view_func.__defaults__)
|
|
|
|
default_param_names = view_func.__code__.co_varnames[num_params - num_default_params:]
|
|
|
|
default_param_values = view_func.__defaults__
|
|
|
|
if default_param_values is None:
|
|
|
|
default_param_values = []
|
|
|
|
|
|
|
|
post_params = []
|
|
|
|
|
|
|
|
for (name, value) in zip(default_param_names, default_param_values):
|
|
|
|
if isinstance(value, REQ):
|
|
|
|
value.func_var_name = name
|
|
|
|
if value.post_var_name is None:
|
|
|
|
value.post_var_name = name
|
|
|
|
post_params.append(value)
|
|
|
|
|
|
|
|
@wraps(view_func)
|
|
|
|
def _wrapped_view_func(request, *args, **kwargs):
|
|
|
|
for param in post_params:
|
|
|
|
if param.func_var_name in kwargs:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if param.argument_type == 'body':
|
|
|
|
try:
|
|
|
|
val = ujson.loads(request.body)
|
|
|
|
except ValueError:
|
2016-05-25 15:02:02 +02:00
|
|
|
raise JsonableError(_('Malformed JSON'))
|
2016-05-29 16:52:55 +02:00
|
|
|
kwargs[param.func_var_name] = val
|
|
|
|
continue
|
|
|
|
elif param.argument_type is not None:
|
|
|
|
# This is a view bug, not a user error, and thus should throw a 500.
|
2016-05-25 15:02:02 +02:00
|
|
|
raise Exception(_("Invalid argument type"))
|
2016-05-29 16:52:55 +02:00
|
|
|
|
|
|
|
default_assigned = False
|
|
|
|
try:
|
2016-11-03 13:00:18 +01:00
|
|
|
query_params = request.GET.copy()
|
|
|
|
query_params.update(request.POST)
|
|
|
|
val = query_params[param.post_var_name]
|
2016-05-29 16:52:55 +02:00
|
|
|
except KeyError:
|
|
|
|
if param.default is REQ.NotSpecified:
|
|
|
|
raise RequestVariableMissingError(param.post_var_name)
|
|
|
|
val = param.default
|
|
|
|
default_assigned = True
|
|
|
|
|
|
|
|
if param.converter is not None and not default_assigned:
|
|
|
|
try:
|
|
|
|
val = param.converter(val)
|
|
|
|
except JsonableError:
|
|
|
|
raise
|
2017-03-05 10:25:27 +01:00
|
|
|
except Exception:
|
2016-05-29 16:52:55 +02:00
|
|
|
raise RequestVariableConversionError(param.post_var_name, val)
|
|
|
|
|
|
|
|
# Validators are like converters, but they don't handle JSON parsing; we do.
|
|
|
|
if param.validator is not None and not default_assigned:
|
|
|
|
try:
|
|
|
|
val = ujson.loads(val)
|
2017-03-05 10:25:27 +01:00
|
|
|
except Exception:
|
2017-06-26 13:12:20 +02:00
|
|
|
raise JsonableError(_('Argument "%s" is not valid JSON.') % (param.post_var_name,))
|
2016-05-29 16:52:55 +02:00
|
|
|
|
|
|
|
error = param.validator(param.post_var_name, val)
|
|
|
|
if error:
|
|
|
|
raise JsonableError(error)
|
|
|
|
|
|
|
|
kwargs[param.func_var_name] = val
|
|
|
|
|
|
|
|
return view_func(request, *args, **kwargs)
|
|
|
|
|
|
|
|
return _wrapped_view_func
|