wrappers.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. # -*- coding: utf-8 -*-
  2. """
  3. werkzeug.contrib.wrappers
  4. ~~~~~~~~~~~~~~~~~~~~~~~~~
  5. Extra wrappers or mixins contributed by the community. These wrappers can
  6. be mixed in into request objects to add extra functionality.
  7. Example::
  8. from werkzeug.wrappers import Request as RequestBase
  9. from werkzeug.contrib.wrappers import JSONRequestMixin
  10. class Request(RequestBase, JSONRequestMixin):
  11. pass
  12. Afterwards this request object provides the extra functionality of the
  13. :class:`JSONRequestMixin`.
  14. :copyright: 2007 Pallets
  15. :license: BSD-3-Clause
  16. """
  17. import codecs
  18. import warnings
  19. from .._compat import wsgi_decoding_dance
  20. from ..exceptions import BadRequest
  21. from ..http import dump_options_header
  22. from ..http import parse_options_header
  23. from ..utils import cached_property
  24. from ..wrappers.json import JSONMixin as _JSONMixin
  25. def is_known_charset(charset):
  26. """Checks if the given charset is known to Python."""
  27. try:
  28. codecs.lookup(charset)
  29. except LookupError:
  30. return False
  31. return True
  32. class JSONRequestMixin(_JSONMixin):
  33. """
  34. .. deprecated:: 0.15
  35. Moved to :class:`werkzeug.wrappers.json.JSONMixin`. This old
  36. import will be removed in version 1.0.
  37. """
  38. @property
  39. def json(self):
  40. warnings.warn(
  41. "'werkzeug.contrib.wrappers.JSONRequestMixin' has moved to"
  42. " 'werkzeug.wrappers.json.JSONMixin'. This old import will"
  43. " be removed in version 1.0.",
  44. DeprecationWarning,
  45. stacklevel=2,
  46. )
  47. return super(JSONRequestMixin, self).json
  48. class ProtobufRequestMixin(object):
  49. """Add protobuf parsing method to a request object. This will parse the
  50. input data through `protobuf`_ if possible.
  51. :exc:`~werkzeug.exceptions.BadRequest` will be raised if the content-type
  52. is not protobuf or if the data itself cannot be parsed property.
  53. .. _protobuf: https://github.com/protocolbuffers/protobuf
  54. .. deprecated:: 0.15
  55. This mixin will be removed in version 1.0.
  56. """
  57. #: by default the :class:`ProtobufRequestMixin` will raise a
  58. #: :exc:`~werkzeug.exceptions.BadRequest` if the object is not
  59. #: initialized. You can bypass that check by setting this
  60. #: attribute to `False`.
  61. protobuf_check_initialization = True
  62. def parse_protobuf(self, proto_type):
  63. """Parse the data into an instance of proto_type."""
  64. warnings.warn(
  65. "'werkzeug.contrib.wrappers.ProtobufRequestMixin' is"
  66. " deprecated as of version 0.15 and will be removed in"
  67. " version 1.0.",
  68. DeprecationWarning,
  69. stacklevel=2,
  70. )
  71. if "protobuf" not in self.environ.get("CONTENT_TYPE", ""):
  72. raise BadRequest("Not a Protobuf request")
  73. obj = proto_type()
  74. try:
  75. obj.ParseFromString(self.data)
  76. except Exception:
  77. raise BadRequest("Unable to parse Protobuf request")
  78. # Fail if not all required fields are set
  79. if self.protobuf_check_initialization and not obj.IsInitialized():
  80. raise BadRequest("Partial Protobuf request")
  81. return obj
  82. class RoutingArgsRequestMixin(object):
  83. """This request mixin adds support for the wsgiorg routing args
  84. `specification`_.
  85. .. _specification: https://wsgi.readthedocs.io/en/latest/
  86. specifications/routing_args.html
  87. .. deprecated:: 0.15
  88. This mixin will be removed in version 1.0.
  89. """
  90. def _get_routing_args(self):
  91. warnings.warn(
  92. "'werkzeug.contrib.wrappers.RoutingArgsRequestMixin' is"
  93. " deprecated as of version 0.15 and will be removed in"
  94. " version 1.0.",
  95. DeprecationWarning,
  96. stacklevel=2,
  97. )
  98. return self.environ.get("wsgiorg.routing_args", (()))[0]
  99. def _set_routing_args(self, value):
  100. warnings.warn(
  101. "'werkzeug.contrib.wrappers.RoutingArgsRequestMixin' is"
  102. " deprecated as of version 0.15 and will be removed in"
  103. " version 1.0.",
  104. DeprecationWarning,
  105. stacklevel=2,
  106. )
  107. if self.shallow:
  108. raise RuntimeError(
  109. "A shallow request tried to modify the WSGI "
  110. "environment. If you really want to do that, "
  111. "set `shallow` to False."
  112. )
  113. self.environ["wsgiorg.routing_args"] = (value, self.routing_vars)
  114. routing_args = property(
  115. _get_routing_args,
  116. _set_routing_args,
  117. doc="""
  118. The positional URL arguments as `tuple`.""",
  119. )
  120. del _get_routing_args, _set_routing_args
  121. def _get_routing_vars(self):
  122. warnings.warn(
  123. "'werkzeug.contrib.wrappers.RoutingArgsRequestMixin' is"
  124. " deprecated as of version 0.15 and will be removed in"
  125. " version 1.0.",
  126. DeprecationWarning,
  127. stacklevel=2,
  128. )
  129. rv = self.environ.get("wsgiorg.routing_args")
  130. if rv is not None:
  131. return rv[1]
  132. rv = {}
  133. if not self.shallow:
  134. self.routing_vars = rv
  135. return rv
  136. def _set_routing_vars(self, value):
  137. warnings.warn(
  138. "'werkzeug.contrib.wrappers.RoutingArgsRequestMixin' is"
  139. " deprecated as of version 0.15 and will be removed in"
  140. " version 1.0.",
  141. DeprecationWarning,
  142. stacklevel=2,
  143. )
  144. if self.shallow:
  145. raise RuntimeError(
  146. "A shallow request tried to modify the WSGI "
  147. "environment. If you really want to do that, "
  148. "set `shallow` to False."
  149. )
  150. self.environ["wsgiorg.routing_args"] = (self.routing_args, value)
  151. routing_vars = property(
  152. _get_routing_vars,
  153. _set_routing_vars,
  154. doc="""
  155. The keyword URL arguments as `dict`.""",
  156. )
  157. del _get_routing_vars, _set_routing_vars
  158. class ReverseSlashBehaviorRequestMixin(object):
  159. """This mixin reverses the trailing slash behavior of :attr:`script_root`
  160. and :attr:`path`. This makes it possible to use :func:`~urlparse.urljoin`
  161. directly on the paths.
  162. Because it changes the behavior or :class:`Request` this class has to be
  163. mixed in *before* the actual request class::
  164. class MyRequest(ReverseSlashBehaviorRequestMixin, Request):
  165. pass
  166. This example shows the differences (for an application mounted on
  167. `/application` and the request going to `/application/foo/bar`):
  168. +---------------+-------------------+---------------------+
  169. | | normal behavior | reverse behavior |
  170. +===============+===================+=====================+
  171. | `script_root` | ``/application`` | ``/application/`` |
  172. +---------------+-------------------+---------------------+
  173. | `path` | ``/foo/bar`` | ``foo/bar`` |
  174. +---------------+-------------------+---------------------+
  175. .. deprecated:: 0.15
  176. This mixin will be removed in version 1.0.
  177. """
  178. @cached_property
  179. def path(self):
  180. """Requested path as unicode. This works a bit like the regular path
  181. info in the WSGI environment but will not include a leading slash.
  182. """
  183. warnings.warn(
  184. "'werkzeug.contrib.wrappers.ReverseSlashBehaviorRequestMixin'"
  185. " is deprecated as of version 0.15 and will be removed in"
  186. " version 1.0.",
  187. DeprecationWarning,
  188. stacklevel=2,
  189. )
  190. path = wsgi_decoding_dance(
  191. self.environ.get("PATH_INFO") or "", self.charset, self.encoding_errors
  192. )
  193. return path.lstrip("/")
  194. @cached_property
  195. def script_root(self):
  196. """The root path of the script includling a trailing slash."""
  197. warnings.warn(
  198. "'werkzeug.contrib.wrappers.ReverseSlashBehaviorRequestMixin'"
  199. " is deprecated as of version 0.15 and will be removed in"
  200. " version 1.0.",
  201. DeprecationWarning,
  202. stacklevel=2,
  203. )
  204. path = wsgi_decoding_dance(
  205. self.environ.get("SCRIPT_NAME") or "", self.charset, self.encoding_errors
  206. )
  207. return path.rstrip("/") + "/"
  208. class DynamicCharsetRequestMixin(object):
  209. """"If this mixin is mixed into a request class it will provide
  210. a dynamic `charset` attribute. This means that if the charset is
  211. transmitted in the content type headers it's used from there.
  212. Because it changes the behavior or :class:`Request` this class has
  213. to be mixed in *before* the actual request class::
  214. class MyRequest(DynamicCharsetRequestMixin, Request):
  215. pass
  216. By default the request object assumes that the URL charset is the
  217. same as the data charset. If the charset varies on each request
  218. based on the transmitted data it's not a good idea to let the URLs
  219. change based on that. Most browsers assume either utf-8 or latin1
  220. for the URLs if they have troubles figuring out. It's strongly
  221. recommended to set the URL charset to utf-8::
  222. class MyRequest(DynamicCharsetRequestMixin, Request):
  223. url_charset = 'utf-8'
  224. .. deprecated:: 0.15
  225. This mixin will be removed in version 1.0.
  226. .. versionadded:: 0.6
  227. """
  228. #: the default charset that is assumed if the content type header
  229. #: is missing or does not contain a charset parameter. The default
  230. #: is latin1 which is what HTTP specifies as default charset.
  231. #: You may however want to set this to utf-8 to better support
  232. #: browsers that do not transmit a charset for incoming data.
  233. default_charset = "latin1"
  234. def unknown_charset(self, charset):
  235. """Called if a charset was provided but is not supported by
  236. the Python codecs module. By default latin1 is assumed then
  237. to not lose any information, you may override this method to
  238. change the behavior.
  239. :param charset: the charset that was not found.
  240. :return: the replacement charset.
  241. """
  242. return "latin1"
  243. @cached_property
  244. def charset(self):
  245. """The charset from the content type."""
  246. warnings.warn(
  247. "'werkzeug.contrib.wrappers.DynamicCharsetRequestMixin'"
  248. " is deprecated as of version 0.15 and will be removed in"
  249. " version 1.0.",
  250. DeprecationWarning,
  251. stacklevel=2,
  252. )
  253. header = self.environ.get("CONTENT_TYPE")
  254. if header:
  255. ct, options = parse_options_header(header)
  256. charset = options.get("charset")
  257. if charset:
  258. if is_known_charset(charset):
  259. return charset
  260. return self.unknown_charset(charset)
  261. return self.default_charset
  262. class DynamicCharsetResponseMixin(object):
  263. """If this mixin is mixed into a response class it will provide
  264. a dynamic `charset` attribute. This means that if the charset is
  265. looked up and stored in the `Content-Type` header and updates
  266. itself automatically. This also means a small performance hit but
  267. can be useful if you're working with different charsets on
  268. responses.
  269. Because the charset attribute is no a property at class-level, the
  270. default value is stored in `default_charset`.
  271. Because it changes the behavior or :class:`Response` this class has
  272. to be mixed in *before* the actual response class::
  273. class MyResponse(DynamicCharsetResponseMixin, Response):
  274. pass
  275. .. deprecated:: 0.15
  276. This mixin will be removed in version 1.0.
  277. .. versionadded:: 0.6
  278. """
  279. #: the default charset.
  280. default_charset = "utf-8"
  281. def _get_charset(self):
  282. warnings.warn(
  283. "'werkzeug.contrib.wrappers.DynamicCharsetResponseMixin'"
  284. " is deprecated as of version 0.15 and will be removed in"
  285. " version 1.0.",
  286. DeprecationWarning,
  287. stacklevel=2,
  288. )
  289. header = self.headers.get("content-type")
  290. if header:
  291. charset = parse_options_header(header)[1].get("charset")
  292. if charset:
  293. return charset
  294. return self.default_charset
  295. def _set_charset(self, charset):
  296. warnings.warn(
  297. "'werkzeug.contrib.wrappers.DynamicCharsetResponseMixin'"
  298. " is deprecated as of version 0.15 and will be removed in"
  299. " version 1.0.",
  300. DeprecationWarning,
  301. stacklevel=2,
  302. )
  303. header = self.headers.get("content-type")
  304. ct, options = parse_options_header(header)
  305. if not ct:
  306. raise TypeError("Cannot set charset if Content-Type header is missing.")
  307. options["charset"] = charset
  308. self.headers["Content-Type"] = dump_options_header(ct, options)
  309. charset = property(
  310. _get_charset,
  311. _set_charset,
  312. doc="""
  313. The charset for the response. It's stored inside the
  314. Content-Type header as a parameter.""",
  315. )
  316. del _get_charset, _set_charset