aboutsummaryrefslogtreecommitdiffstats
path: root/python/werkzeug/contrib/wrappers.py
blob: 49b82a71ef3125cdd8ba7d701e40e934971b30cc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# -*- 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: 2007 Pallets
    :license: BSD-3-Clause
"""
import codecs
import warnings

from .._compat import wsgi_decoding_dance
from ..exceptions import BadRequest
from ..http import dump_options_header
from ..http import parse_options_header
from ..utils import cached_property
from ..wrappers.json import JSONMixin as _JSONMixin


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(_JSONMixin):
    """
    .. deprecated:: 0.15
        Moved to :class:`werkzeug.wrappers.json.JSONMixin`. This old
        import will be removed in version 1.0.
    """

    @property
    def json(self):
        warnings.warn(
            "'werkzeug.contrib.wrappers.JSONRequestMixin' has moved to"
            " 'werkzeug.wrappers.json.JSONMixin'. This old import will"
            " be removed in version 1.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        return super(JSONRequestMixin, self).json


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: https://github.com/protocolbuffers/protobuf

    .. deprecated:: 0.15
        This mixin will be removed in version 1.0.
    """

    #: 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."""
        warnings.warn(
            "'werkzeug.contrib.wrappers.ProtobufRequestMixin' is"
            " deprecated as of version 0.15 and will be removed in"
            " version 1.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        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

    .. deprecated:: 0.15
        This mixin will be removed in version 1.0.
    """

    def _get_routing_args(self):
        warnings.warn(
            "'werkzeug.contrib.wrappers.RoutingArgsRequestMixin' is"
            " deprecated as of version 0.15 and will be removed in"
            " version 1.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.environ.get("wsgiorg.routing_args", (()))[0]

    def _set_routing_args(self, value):
        warnings.warn(
            "'werkzeug.contrib.wrappers.RoutingArgsRequestMixin' is"
            " deprecated as of version 0.15 and will be removed in"
            " version 1.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        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):
        warnings.warn(
            "'werkzeug.contrib.wrappers.RoutingArgsRequestMixin' is"
            " deprecated as of version 0.15 and will be removed in"
            " version 1.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        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):
        warnings.warn(
            "'werkzeug.contrib.wrappers.RoutingArgsRequestMixin' is"
            " deprecated as of version 0.15 and will be removed in"
            " version 1.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        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``         |
        +---------------+-------------------+---------------------+

    .. deprecated:: 0.15
        This mixin will be removed in version 1.0.
    """

    @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.
        """
        warnings.warn(
            "'werkzeug.contrib.wrappers.ReverseSlashBehaviorRequestMixin'"
            " is deprecated as of version 0.15 and will be removed in"
            " version 1.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        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."""
        warnings.warn(
            "'werkzeug.contrib.wrappers.ReverseSlashBehaviorRequestMixin'"
            " is deprecated as of version 0.15 and will be removed in"
            " version 1.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        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'

    .. deprecated:: 0.15
        This mixin will be removed in version 1.0.

    .. 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."""
        warnings.warn(
            "'werkzeug.contrib.wrappers.DynamicCharsetRequestMixin'"
            " is deprecated as of version 0.15 and will be removed in"
            " version 1.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        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

    .. deprecated:: 0.15
        This mixin will be removed in version 1.0.

    .. versionadded:: 0.6
    """

    #: the default charset.
    default_charset = "utf-8"

    def _get_charset(self):
        warnings.warn(
            "'werkzeug.contrib.wrappers.DynamicCharsetResponseMixin'"
            " is deprecated as of version 0.15 and will be removed in"
            " version 1.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        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):
        warnings.warn(
            "'werkzeug.contrib.wrappers.DynamicCharsetResponseMixin'"
            " is deprecated as of version 0.15 and will be removed in"
            " version 1.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        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