You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
			
				
					285 lines
				
				10 KiB
			
		
		
			
		
	
	
					285 lines
				
				10 KiB
			| 
								 
											8 years ago
										 
									 | 
							
								# -*- coding: utf-8 -*-
							 | 
						||
| 
								 | 
							
								"""
							 | 
						||
| 
								 | 
							
								    werkzeug.contrib.wrappers
							 | 
						||
| 
								 | 
							
								    ~~~~~~~~~~~~~~~~~~~~~~~~~
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    Extra wrappers or mixins contributed by the community.  These wrappers can
							 | 
						||
| 
								 | 
							
								    be mixed in into request objects to add extra functionality.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    Example::
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        from werkzeug.wrappers import Request as RequestBase
							 | 
						||
| 
								 | 
							
								        from werkzeug.contrib.wrappers import JSONRequestMixin
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        class Request(RequestBase, JSONRequestMixin):
							 | 
						||
| 
								 | 
							
								            pass
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    Afterwards this request object provides the extra functionality of the
							 | 
						||
| 
								 | 
							
								    :class:`JSONRequestMixin`.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    :copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
							 | 
						||
| 
								 | 
							
								    :license: BSD, see LICENSE for more details.
							 | 
						||
| 
								 | 
							
								"""
							 | 
						||
| 
								 | 
							
								import codecs
							 | 
						||
| 
								 | 
							
								try:
							 | 
						||
| 
								 | 
							
								    from simplejson import loads
							 | 
						||
| 
								 | 
							
								except ImportError:
							 | 
						||
| 
								 | 
							
								    from json import loads
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								from werkzeug.exceptions import BadRequest
							 | 
						||
| 
								 | 
							
								from werkzeug.utils import cached_property
							 | 
						||
| 
								 | 
							
								from werkzeug.http import dump_options_header, parse_options_header
							 | 
						||
| 
								 | 
							
								from werkzeug._compat import wsgi_decoding_dance
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def is_known_charset(charset):
							 | 
						||
| 
								 | 
							
								    """Checks if the given charset is known to Python."""
							 | 
						||
| 
								 | 
							
								    try:
							 | 
						||
| 
								 | 
							
								        codecs.lookup(charset)
							 | 
						||
| 
								 | 
							
								    except LookupError:
							 | 
						||
| 
								 | 
							
								        return False
							 | 
						||
| 
								 | 
							
								    return True
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class JSONRequestMixin(object):
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    """Add json method to a request object.  This will parse the input data
							 | 
						||
| 
								 | 
							
								    through simplejson if possible.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    :exc:`~werkzeug.exceptions.BadRequest` will be raised if the content-type
							 | 
						||
| 
								 | 
							
								    is not json or if the data itself cannot be parsed as json.
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @cached_property
							 | 
						||
| 
								 | 
							
								    def json(self):
							 | 
						||
| 
								 | 
							
								        """Get the result of simplejson.loads if possible."""
							 | 
						||
| 
								 | 
							
								        if 'json' not in self.environ.get('CONTENT_TYPE', ''):
							 | 
						||
| 
								 | 
							
								            raise BadRequest('Not a JSON request')
							 | 
						||
| 
								 | 
							
								        try:
							 | 
						||
| 
								 | 
							
								            return loads(self.data.decode(self.charset, self.encoding_errors))
							 | 
						||
| 
								 | 
							
								        except Exception:
							 | 
						||
| 
								 | 
							
								            raise BadRequest('Unable to read JSON request')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class ProtobufRequestMixin(object):
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    """Add protobuf parsing method to a request object.  This will parse the
							 | 
						||
| 
								 | 
							
								    input data through `protobuf`_ if possible.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    :exc:`~werkzeug.exceptions.BadRequest` will be raised if the content-type
							 | 
						||
| 
								 | 
							
								    is not protobuf or if the data itself cannot be parsed property.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    .. _protobuf: http://code.google.com/p/protobuf/
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #: by default the :class:`ProtobufRequestMixin` will raise a
							 | 
						||
| 
								 | 
							
								    #: :exc:`~werkzeug.exceptions.BadRequest` if the object is not
							 | 
						||
| 
								 | 
							
								    #: initialized.  You can bypass that check by setting this
							 | 
						||
| 
								 | 
							
								    #: attribute to `False`.
							 | 
						||
| 
								 | 
							
								    protobuf_check_initialization = True
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def parse_protobuf(self, proto_type):
							 | 
						||
| 
								 | 
							
								        """Parse the data into an instance of proto_type."""
							 | 
						||
| 
								 | 
							
								        if 'protobuf' not in self.environ.get('CONTENT_TYPE', ''):
							 | 
						||
| 
								 | 
							
								            raise BadRequest('Not a Protobuf request')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        obj = proto_type()
							 | 
						||
| 
								 | 
							
								        try:
							 | 
						||
| 
								 | 
							
								            obj.ParseFromString(self.data)
							 | 
						||
| 
								 | 
							
								        except Exception:
							 | 
						||
| 
								 | 
							
								            raise BadRequest("Unable to parse Protobuf request")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Fail if not all required fields are set
							 | 
						||
| 
								 | 
							
								        if self.protobuf_check_initialization and not obj.IsInitialized():
							 | 
						||
| 
								 | 
							
								            raise BadRequest("Partial Protobuf request")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return obj
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class RoutingArgsRequestMixin(object):
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    """This request mixin adds support for the wsgiorg routing args
							 | 
						||
| 
								 | 
							
								    `specification`_.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    .. _specification: https://wsgi.readthedocs.io/en/latest/specifications/routing_args.html
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def _get_routing_args(self):
							 | 
						||
| 
								 | 
							
								        return self.environ.get('wsgiorg.routing_args', (()))[0]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def _set_routing_args(self, value):
							 | 
						||
| 
								 | 
							
								        if self.shallow:
							 | 
						||
| 
								 | 
							
								            raise RuntimeError('A shallow request tried to modify the WSGI '
							 | 
						||
| 
								 | 
							
								                               'environment.  If you really want to do that, '
							 | 
						||
| 
								 | 
							
								                               'set `shallow` to False.')
							 | 
						||
| 
								 | 
							
								        self.environ['wsgiorg.routing_args'] = (value, self.routing_vars)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    routing_args = property(_get_routing_args, _set_routing_args, doc='''
							 | 
						||
| 
								 | 
							
								        The positional URL arguments as `tuple`.''')
							 | 
						||
| 
								 | 
							
								    del _get_routing_args, _set_routing_args
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def _get_routing_vars(self):
							 | 
						||
| 
								 | 
							
								        rv = self.environ.get('wsgiorg.routing_args')
							 | 
						||
| 
								 | 
							
								        if rv is not None:
							 | 
						||
| 
								 | 
							
								            return rv[1]
							 | 
						||
| 
								 | 
							
								        rv = {}
							 | 
						||
| 
								 | 
							
								        if not self.shallow:
							 | 
						||
| 
								 | 
							
								            self.routing_vars = rv
							 | 
						||
| 
								 | 
							
								        return rv
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def _set_routing_vars(self, value):
							 | 
						||
| 
								 | 
							
								        if self.shallow:
							 | 
						||
| 
								 | 
							
								            raise RuntimeError('A shallow request tried to modify the WSGI '
							 | 
						||
| 
								 | 
							
								                               'environment.  If you really want to do that, '
							 | 
						||
| 
								 | 
							
								                               'set `shallow` to False.')
							 | 
						||
| 
								 | 
							
								        self.environ['wsgiorg.routing_args'] = (self.routing_args, value)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    routing_vars = property(_get_routing_vars, _set_routing_vars, doc='''
							 | 
						||
| 
								 | 
							
								        The keyword URL arguments as `dict`.''')
							 | 
						||
| 
								 | 
							
								    del _get_routing_vars, _set_routing_vars
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class ReverseSlashBehaviorRequestMixin(object):
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    """This mixin reverses the trailing slash behavior of :attr:`script_root`
							 | 
						||
| 
								 | 
							
								    and :attr:`path`.  This makes it possible to use :func:`~urlparse.urljoin`
							 | 
						||
| 
								 | 
							
								    directly on the paths.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    Because it changes the behavior or :class:`Request` this class has to be
							 | 
						||
| 
								 | 
							
								    mixed in *before* the actual request class::
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        class MyRequest(ReverseSlashBehaviorRequestMixin, Request):
							 | 
						||
| 
								 | 
							
								            pass
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    This example shows the differences (for an application mounted on
							 | 
						||
| 
								 | 
							
								    `/application` and the request going to `/application/foo/bar`):
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        +---------------+-------------------+---------------------+
							 | 
						||
| 
								 | 
							
								        |               | normal behavior   | reverse behavior    |
							 | 
						||
| 
								 | 
							
								        +===============+===================+=====================+
							 | 
						||
| 
								 | 
							
								        | `script_root` | ``/application``  | ``/application/``   |
							 | 
						||
| 
								 | 
							
								        +---------------+-------------------+---------------------+
							 | 
						||
| 
								 | 
							
								        | `path`        | ``/foo/bar``      | ``foo/bar``         |
							 | 
						||
| 
								 | 
							
								        +---------------+-------------------+---------------------+
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @cached_property
							 | 
						||
| 
								 | 
							
								    def path(self):
							 | 
						||
| 
								 | 
							
								        """Requested path as unicode.  This works a bit like the regular path
							 | 
						||
| 
								 | 
							
								        info in the WSGI environment but will not include a leading slash.
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        path = wsgi_decoding_dance(self.environ.get('PATH_INFO') or '',
							 | 
						||
| 
								 | 
							
								                                   self.charset, self.encoding_errors)
							 | 
						||
| 
								 | 
							
								        return path.lstrip('/')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @cached_property
							 | 
						||
| 
								 | 
							
								    def script_root(self):
							 | 
						||
| 
								 | 
							
								        """The root path of the script includling a trailing slash."""
							 | 
						||
| 
								 | 
							
								        path = wsgi_decoding_dance(self.environ.get('SCRIPT_NAME') or '',
							 | 
						||
| 
								 | 
							
								                                   self.charset, self.encoding_errors)
							 | 
						||
| 
								 | 
							
								        return path.rstrip('/') + '/'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class DynamicCharsetRequestMixin(object):
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    """"If this mixin is mixed into a request class it will provide
							 | 
						||
| 
								 | 
							
								    a dynamic `charset` attribute.  This means that if the charset is
							 | 
						||
| 
								 | 
							
								    transmitted in the content type headers it's used from there.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    Because it changes the behavior or :class:`Request` this class has
							 | 
						||
| 
								 | 
							
								    to be mixed in *before* the actual request class::
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        class MyRequest(DynamicCharsetRequestMixin, Request):
							 | 
						||
| 
								 | 
							
								            pass
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    By default the request object assumes that the URL charset is the
							 | 
						||
| 
								 | 
							
								    same as the data charset.  If the charset varies on each request
							 | 
						||
| 
								 | 
							
								    based on the transmitted data it's not a good idea to let the URLs
							 | 
						||
| 
								 | 
							
								    change based on that.  Most browsers assume either utf-8 or latin1
							 | 
						||
| 
								 | 
							
								    for the URLs if they have troubles figuring out.  It's strongly
							 | 
						||
| 
								 | 
							
								    recommended to set the URL charset to utf-8::
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        class MyRequest(DynamicCharsetRequestMixin, Request):
							 | 
						||
| 
								 | 
							
								            url_charset = 'utf-8'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    .. versionadded:: 0.6
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #: the default charset that is assumed if the content type header
							 | 
						||
| 
								 | 
							
								    #: is missing or does not contain a charset parameter.  The default
							 | 
						||
| 
								 | 
							
								    #: is latin1 which is what HTTP specifies as default charset.
							 | 
						||
| 
								 | 
							
								    #: You may however want to set this to utf-8 to better support
							 | 
						||
| 
								 | 
							
								    #: browsers that do not transmit a charset for incoming data.
							 | 
						||
| 
								 | 
							
								    default_charset = 'latin1'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def unknown_charset(self, charset):
							 | 
						||
| 
								 | 
							
								        """Called if a charset was provided but is not supported by
							 | 
						||
| 
								 | 
							
								        the Python codecs module.  By default latin1 is assumed then
							 | 
						||
| 
								 | 
							
								        to not lose any information, you may override this method to
							 | 
						||
| 
								 | 
							
								        change the behavior.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        :param charset: the charset that was not found.
							 | 
						||
| 
								 | 
							
								        :return: the replacement charset.
							 | 
						||
| 
								 | 
							
								        """
							 | 
						||
| 
								 | 
							
								        return 'latin1'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    @cached_property
							 | 
						||
| 
								 | 
							
								    def charset(self):
							 | 
						||
| 
								 | 
							
								        """The charset from the content type."""
							 | 
						||
| 
								 | 
							
								        header = self.environ.get('CONTENT_TYPE')
							 | 
						||
| 
								 | 
							
								        if header:
							 | 
						||
| 
								 | 
							
								            ct, options = parse_options_header(header)
							 | 
						||
| 
								 | 
							
								            charset = options.get('charset')
							 | 
						||
| 
								 | 
							
								            if charset:
							 | 
						||
| 
								 | 
							
								                if is_known_charset(charset):
							 | 
						||
| 
								 | 
							
								                    return charset
							 | 
						||
| 
								 | 
							
								                return self.unknown_charset(charset)
							 | 
						||
| 
								 | 
							
								        return self.default_charset
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class DynamicCharsetResponseMixin(object):
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    """If this mixin is mixed into a response class it will provide
							 | 
						||
| 
								 | 
							
								    a dynamic `charset` attribute.  This means that if the charset is
							 | 
						||
| 
								 | 
							
								    looked up and stored in the `Content-Type` header and updates
							 | 
						||
| 
								 | 
							
								    itself automatically.  This also means a small performance hit but
							 | 
						||
| 
								 | 
							
								    can be useful if you're working with different charsets on
							 | 
						||
| 
								 | 
							
								    responses.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    Because the charset attribute is no a property at class-level, the
							 | 
						||
| 
								 | 
							
								    default value is stored in `default_charset`.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    Because it changes the behavior or :class:`Response` this class has
							 | 
						||
| 
								 | 
							
								    to be mixed in *before* the actual response class::
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        class MyResponse(DynamicCharsetResponseMixin, Response):
							 | 
						||
| 
								 | 
							
								            pass
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    .. versionadded:: 0.6
							 | 
						||
| 
								 | 
							
								    """
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #: the default charset.
							 | 
						||
| 
								 | 
							
								    default_charset = 'utf-8'
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def _get_charset(self):
							 | 
						||
| 
								 | 
							
								        header = self.headers.get('content-type')
							 | 
						||
| 
								 | 
							
								        if header:
							 | 
						||
| 
								 | 
							
								            charset = parse_options_header(header)[1].get('charset')
							 | 
						||
| 
								 | 
							
								            if charset:
							 | 
						||
| 
								 | 
							
								                return charset
							 | 
						||
| 
								 | 
							
								        return self.default_charset
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def _set_charset(self, charset):
							 | 
						||
| 
								 | 
							
								        header = self.headers.get('content-type')
							 | 
						||
| 
								 | 
							
								        ct, options = parse_options_header(header)
							 | 
						||
| 
								 | 
							
								        if not ct:
							 | 
						||
| 
								 | 
							
								            raise TypeError('Cannot set charset if Content-Type '
							 | 
						||
| 
								 | 
							
								                            'header is missing.')
							 | 
						||
| 
								 | 
							
								        options['charset'] = charset
							 | 
						||
| 
								 | 
							
								        self.headers['Content-Type'] = dump_options_header(ct, options)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    charset = property(_get_charset, _set_charset, doc="""
							 | 
						||
| 
								 | 
							
								        The charset for the response.  It's stored inside the
							 | 
						||
| 
								 | 
							
								        Content-Type header as a parameter.""")
							 | 
						||
| 
								 | 
							
								    del _get_charset, _set_charset
							 |