aboutsummaryrefslogtreecommitdiffstats
path: root/python/werkzeug/contrib
diff options
context:
space:
mode:
Diffstat (limited to 'python/werkzeug/contrib')
-rw-r--r--python/werkzeug/contrib/__init__.py16
-rw-r--r--python/werkzeug/contrib/atom.py362
-rw-r--r--python/werkzeug/contrib/cache.py933
-rw-r--r--python/werkzeug/contrib/fixers.py262
-rw-r--r--python/werkzeug/contrib/iterio.py358
-rw-r--r--python/werkzeug/contrib/lint.py11
-rw-r--r--python/werkzeug/contrib/profiler.py42
-rw-r--r--python/werkzeug/contrib/securecookie.py362
-rw-r--r--python/werkzeug/contrib/sessions.py389
-rw-r--r--python/werkzeug/contrib/wrappers.py385
10 files changed, 3120 insertions, 0 deletions
diff --git a/python/werkzeug/contrib/__init__.py b/python/werkzeug/contrib/__init__.py
new file mode 100644
index 0000000..0e741f0
--- /dev/null
+++ b/python/werkzeug/contrib/__init__.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+"""
+ werkzeug.contrib
+ ~~~~~~~~~~~~~~~~
+
+ Contains user-submitted code that other users may find useful, but which
+ is not part of the Werkzeug core. Anyone can write code for inclusion in
+ the `contrib` package. All modules in this package are distributed as an
+ add-on library and thus are not part of Werkzeug itself.
+
+ This file itself is mostly for informational purposes and to tell the
+ Python interpreter that `contrib` is a package.
+
+ :copyright: 2007 Pallets
+ :license: BSD-3-Clause
+"""
diff --git a/python/werkzeug/contrib/atom.py b/python/werkzeug/contrib/atom.py
new file mode 100644
index 0000000..d079d2b
--- /dev/null
+++ b/python/werkzeug/contrib/atom.py
@@ -0,0 +1,362 @@
+# -*- coding: utf-8 -*-
+"""
+ werkzeug.contrib.atom
+ ~~~~~~~~~~~~~~~~~~~~~
+
+ This module provides a class called :class:`AtomFeed` which can be
+ used to generate feeds in the Atom syndication format (see :rfc:`4287`).
+
+ Example::
+
+ def atom_feed(request):
+ feed = AtomFeed("My Blog", feed_url=request.url,
+ url=request.host_url,
+ subtitle="My example blog for a feed test.")
+ for post in Post.query.limit(10).all():
+ feed.add(post.title, post.body, content_type='html',
+ author=post.author, url=post.url, id=post.uid,
+ updated=post.last_update, published=post.pub_date)
+ return feed.get_response()
+
+ :copyright: 2007 Pallets
+ :license: BSD-3-Clause
+"""
+import warnings
+from datetime import datetime
+
+from .._compat import implements_to_string
+from .._compat import string_types
+from ..utils import escape
+from ..wrappers import BaseResponse
+
+warnings.warn(
+ "'werkzeug.contrib.atom' is deprecated as of version 0.15 and will"
+ " be removed in version 1.0.",
+ DeprecationWarning,
+ stacklevel=2,
+)
+
+XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml"
+
+
+def _make_text_block(name, content, content_type=None):
+ """Helper function for the builder that creates an XML text block."""
+ if content_type == "xhtml":
+ return u'<%s type="xhtml"><div xmlns="%s">%s</div></%s>\n' % (
+ name,
+ XHTML_NAMESPACE,
+ content,
+ name,
+ )
+ if not content_type:
+ return u"<%s>%s</%s>\n" % (name, escape(content), name)
+ return u'<%s type="%s">%s</%s>\n' % (name, content_type, escape(content), name)
+
+
+def format_iso8601(obj):
+ """Format a datetime object for iso8601"""
+ iso8601 = obj.isoformat()
+ if obj.tzinfo:
+ return iso8601
+ return iso8601 + "Z"
+
+
+@implements_to_string
+class AtomFeed(object):
+
+ """A helper class that creates Atom feeds.
+
+ :param title: the title of the feed. Required.
+ :param title_type: the type attribute for the title element. One of
+ ``'html'``, ``'text'`` or ``'xhtml'``.
+ :param url: the url for the feed (not the url *of* the feed)
+ :param id: a globally unique id for the feed. Must be an URI. If
+ not present the `feed_url` is used, but one of both is
+ required.
+ :param updated: the time the feed was modified the last time. Must
+ be a :class:`datetime.datetime` object. If not
+ present the latest entry's `updated` is used.
+ Treated as UTC if naive datetime.
+ :param feed_url: the URL to the feed. Should be the URL that was
+ requested.
+ :param author: the author of the feed. Must be either a string (the
+ name) or a dict with name (required) and uri or
+ email (both optional). Can be a list of (may be
+ mixed, too) strings and dicts, too, if there are
+ multiple authors. Required if not every entry has an
+ author element.
+ :param icon: an icon for the feed.
+ :param logo: a logo for the feed.
+ :param rights: copyright information for the feed.
+ :param rights_type: the type attribute for the rights element. One of
+ ``'html'``, ``'text'`` or ``'xhtml'``. Default is
+ ``'text'``.
+ :param subtitle: a short description of the feed.
+ :param subtitle_type: the type attribute for the subtitle element.
+ One of ``'text'``, ``'html'``, ``'text'``
+ or ``'xhtml'``. Default is ``'text'``.
+ :param links: additional links. Must be a list of dictionaries with
+ href (required) and rel, type, hreflang, title, length
+ (all optional)
+ :param generator: the software that generated this feed. This must be
+ a tuple in the form ``(name, url, version)``. If
+ you don't want to specify one of them, set the item
+ to `None`.
+ :param entries: a list with the entries for the feed. Entries can also
+ be added later with :meth:`add`.
+
+ For more information on the elements see
+ http://www.atomenabled.org/developers/syndication/
+
+ Everywhere where a list is demanded, any iterable can be used.
+ """
+
+ default_generator = ("Werkzeug", None, None)
+
+ def __init__(self, title=None, entries=None, **kwargs):
+ self.title = title
+ self.title_type = kwargs.get("title_type", "text")
+ self.url = kwargs.get("url")
+ self.feed_url = kwargs.get("feed_url", self.url)
+ self.id = kwargs.get("id", self.feed_url)
+ self.updated = kwargs.get("updated")
+ self.author = kwargs.get("author", ())
+ self.icon = kwargs.get("icon")
+ self.logo = kwargs.get("logo")
+ self.rights = kwargs.get("rights")
+ self.rights_type = kwargs.get("rights_type")
+ self.subtitle = kwargs.get("subtitle")
+ self.subtitle_type = kwargs.get("subtitle_type", "text")
+ self.generator = kwargs.get("generator")
+ if self.generator is None:
+ self.generator = self.default_generator
+ self.links = kwargs.get("links", [])
+ self.entries = list(entries) if entries else []
+
+ if not hasattr(self.author, "__iter__") or isinstance(
+ self.author, string_types + (dict,)
+ ):
+ self.author = [self.author]
+ for i, author in enumerate(self.author):
+ if not isinstance(author, dict):
+ self.author[i] = {"name": author}
+
+ if not self.title:
+ raise ValueError("title is required")
+ if not self.id:
+ raise ValueError("id is required")
+ for author in self.author:
+ if "name" not in author:
+ raise TypeError("author must contain at least a name")
+
+ def add(self, *args, **kwargs):
+ """Add a new entry to the feed. This function can either be called
+ with a :class:`FeedEntry` or some keyword and positional arguments
+ that are forwarded to the :class:`FeedEntry` constructor.
+ """
+ if len(args) == 1 and not kwargs and isinstance(args[0], FeedEntry):
+ self.entries.append(args[0])
+ else:
+ kwargs["feed_url"] = self.feed_url
+ self.entries.append(FeedEntry(*args, **kwargs))
+
+ def __repr__(self):
+ return "<%s %r (%d entries)>" % (
+ self.__class__.__name__,
+ self.title,
+ len(self.entries),
+ )
+
+ def generate(self):
+ """Return a generator that yields pieces of XML."""
+ # atom demands either an author element in every entry or a global one
+ if not self.author:
+ if any(not e.author for e in self.entries):
+ self.author = ({"name": "Unknown author"},)
+
+ if not self.updated:
+ dates = sorted([entry.updated for entry in self.entries])
+ self.updated = dates[-1] if dates else datetime.utcnow()
+
+ yield u'<?xml version="1.0" encoding="utf-8"?>\n'
+ yield u'<feed xmlns="http://www.w3.org/2005/Atom">\n'
+ yield " " + _make_text_block("title", self.title, self.title_type)
+ yield u" <id>%s</id>\n" % escape(self.id)
+ yield u" <updated>%s</updated>\n" % format_iso8601(self.updated)
+ if self.url:
+ yield u' <link href="%s" />\n' % escape(self.url)
+ if self.feed_url:
+ yield u' <link href="%s" rel="self" />\n' % escape(self.feed_url)
+ for link in self.links:
+ yield u" <link %s/>\n" % "".join(
+ '%s="%s" ' % (k, escape(link[k])) for k in link
+ )
+ for author in self.author:
+ yield u" <author>\n"
+ yield u" <name>%s</name>\n" % escape(author["name"])
+ if "uri" in author:
+ yield u" <uri>%s</uri>\n" % escape(author["uri"])
+ if "email" in author:
+ yield " <email>%s</email>\n" % escape(author["email"])
+ yield " </author>\n"
+ if self.subtitle:
+ yield " " + _make_text_block("subtitle", self.subtitle, self.subtitle_type)
+ if self.icon:
+ yield u" <icon>%s</icon>\n" % escape(self.icon)
+ if self.logo:
+ yield u" <logo>%s</logo>\n" % escape(self.logo)
+ if self.rights:
+ yield " " + _make_text_block("rights", self.rights, self.rights_type)
+ generator_name, generator_url, generator_version = self.generator
+ if generator_name or generator_url or generator_version:
+ tmp = [u" <generator"]
+ if generator_url:
+ tmp.append(u' uri="%s"' % escape(generator_url))
+ if generator_version:
+ tmp.append(u' version="%s"' % escape(generator_version))
+ tmp.append(u">%s</generator>\n" % escape(generator_name))
+ yield u"".join(tmp)
+ for entry in self.entries:
+ for line in entry.generate():
+ yield u" " + line
+ yield u"</feed>\n"
+
+ def to_string(self):
+ """Convert the feed into a string."""
+ return u"".join(self.generate())
+
+ def get_response(self):
+ """Return a response object for the feed."""
+ return BaseResponse(self.to_string(), mimetype="application/atom+xml")
+
+ def __call__(self, environ, start_response):
+ """Use the class as WSGI response object."""
+ return self.get_response()(environ, start_response)
+
+ def __str__(self):
+ return self.to_string()
+
+
+@implements_to_string
+class FeedEntry(object):
+
+ """Represents a single entry in a feed.
+
+ :param title: the title of the entry. Required.
+ :param title_type: the type attribute for the title element. One of
+ ``'html'``, ``'text'`` or ``'xhtml'``.
+ :param content: the content of the entry.
+ :param content_type: the type attribute for the content element. One
+ of ``'html'``, ``'text'`` or ``'xhtml'``.
+ :param summary: a summary of the entry's content.
+ :param summary_type: the type attribute for the summary element. One
+ of ``'html'``, ``'text'`` or ``'xhtml'``.
+ :param url: the url for the entry.
+ :param id: a globally unique id for the entry. Must be an URI. If
+ not present the URL is used, but one of both is required.
+ :param updated: the time the entry was modified the last time. Must
+ be a :class:`datetime.datetime` object. Treated as
+ UTC if naive datetime. Required.
+ :param author: the author of the entry. Must be either a string (the
+ name) or a dict with name (required) and uri or
+ email (both optional). Can be a list of (may be
+ mixed, too) strings and dicts, too, if there are
+ multiple authors. Required if the feed does not have an
+ author element.
+ :param published: the time the entry was initially published. Must
+ be a :class:`datetime.datetime` object. Treated as
+ UTC if naive datetime.
+ :param rights: copyright information for the entry.
+ :param rights_type: the type attribute for the rights element. One of
+ ``'html'``, ``'text'`` or ``'xhtml'``. Default is
+ ``'text'``.
+ :param links: additional links. Must be a list of dictionaries with
+ href (required) and rel, type, hreflang, title, length
+ (all optional)
+ :param categories: categories for the entry. Must be a list of dictionaries
+ with term (required), scheme and label (all optional)
+ :param xml_base: The xml base (url) for this feed item. If not provided
+ it will default to the item url.
+
+ For more information on the elements see
+ http://www.atomenabled.org/developers/syndication/
+
+ Everywhere where a list is demanded, any iterable can be used.
+ """
+
+ def __init__(self, title=None, content=None, feed_url=None, **kwargs):
+ self.title = title
+ self.title_type = kwargs.get("title_type", "text")
+ self.content = content
+ self.content_type = kwargs.get("content_type", "html")
+ self.url = kwargs.get("url")
+ self.id = kwargs.get("id", self.url)
+ self.updated = kwargs.get("updated")
+ self.summary = kwargs.get("summary")
+ self.summary_type = kwargs.get("summary_type", "html")
+ self.author = kwargs.get("author", ())
+ self.published = kwargs.get("published")
+ self.rights = kwargs.get("rights")
+ self.links = kwargs.get("links", [])
+ self.categories = kwargs.get("categories", [])
+ self.xml_base = kwargs.get("xml_base", feed_url)
+
+ if not hasattr(self.author, "__iter__") or isinstance(
+ self.author, string_types + (dict,)
+ ):
+ self.author = [self.author]
+ for i, author in enumerate(self.author):
+ if not isinstance(author, dict):
+ self.author[i] = {"name": author}
+
+ if not self.title:
+ raise ValueError("title is required")
+ if not self.id:
+ raise ValueError("id is required")
+ if not self.updated:
+ raise ValueError("updated is required")
+
+ def __repr__(self):
+ return "<%s %r>" % (self.__class__.__name__, self.title)
+
+ def generate(self):
+ """Yields pieces of ATOM XML."""
+ base = ""
+ if self.xml_base:
+ base = ' xml:base="%s"' % escape(self.xml_base)
+ yield u"<entry%s>\n" % base
+ yield u" " + _make_text_block("title", self.title, self.title_type)
+ yield u" <id>%s</id>\n" % escape(self.id)
+ yield u" <updated>%s</updated>\n" % format_iso8601(self.updated)
+ if self.published:
+ yield u" <published>%s</published>\n" % format_iso8601(self.published)
+ if self.url:
+ yield u' <link href="%s" />\n' % escape(self.url)
+ for author in self.author:
+ yield u" <author>\n"
+ yield u" <name>%s</name>\n" % escape(author["name"])
+ if "uri" in author:
+ yield u" <uri>%s</uri>\n" % escape(author["uri"])
+ if "email" in author:
+ yield u" <email>%s</email>\n" % escape(author["email"])
+ yield u" </author>\n"
+ for link in self.links:
+ yield u" <link %s/>\n" % "".join(
+ '%s="%s" ' % (k, escape(link[k])) for k in link
+ )
+ for category in self.categories:
+ yield u" <category %s/>\n" % "".join(
+ '%s="%s" ' % (k, escape(category[k])) for k in category
+ )
+ if self.summary:
+ yield u" " + _make_text_block("summary", self.summary, self.summary_type)
+ if self.content:
+ yield u" " + _make_text_block("content", self.content, self.content_type)
+ yield u"</entry>\n"
+
+ def to_string(self):
+ """Convert the feed item into a unicode object."""
+ return u"".join(self.generate())
+
+ def __str__(self):
+ return self.to_string()
diff --git a/python/werkzeug/contrib/cache.py b/python/werkzeug/contrib/cache.py
new file mode 100644
index 0000000..79c749b
--- /dev/null
+++ b/python/werkzeug/contrib/cache.py
@@ -0,0 +1,933 @@
+# -*- coding: utf-8 -*-
+"""
+ werkzeug.contrib.cache
+ ~~~~~~~~~~~~~~~~~~~~~~
+
+ The main problem with dynamic Web sites is, well, they're dynamic. Each
+ time a user requests a page, the webserver executes a lot of code, queries
+ the database, renders templates until the visitor gets the page he sees.
+
+ This is a lot more expensive than just loading a file from the file system
+ and sending it to the visitor.
+
+ For most Web applications, this overhead isn't a big deal but once it
+ becomes, you will be glad to have a cache system in place.
+
+ How Caching Works
+ =================
+
+ Caching is pretty simple. Basically you have a cache object lurking around
+ somewhere that is connected to a remote cache or the file system or
+ something else. When the request comes in you check if the current page
+ is already in the cache and if so, you're returning it from the cache.
+ Otherwise you generate the page and put it into the cache. (Or a fragment
+ of the page, you don't have to cache the full thing)
+
+ Here is a simple example of how to cache a sidebar for 5 minutes::
+
+ def get_sidebar(user):
+ identifier = 'sidebar_for/user%d' % user.id
+ value = cache.get(identifier)
+ if value is not None:
+ return value
+ value = generate_sidebar_for(user=user)
+ cache.set(identifier, value, timeout=60 * 5)
+ return value
+
+ Creating a Cache Object
+ =======================
+
+ To create a cache object you just import the cache system of your choice
+ from the cache module and instantiate it. Then you can start working
+ with that object:
+
+ >>> from werkzeug.contrib.cache import SimpleCache
+ >>> c = SimpleCache()
+ >>> c.set("foo", "value")
+ >>> c.get("foo")
+ 'value'
+ >>> c.get("missing") is None
+ True
+
+ Please keep in mind that you have to create the cache and put it somewhere
+ you have access to it (either as a module global you can import or you just
+ put it into your WSGI application).
+
+ :copyright: 2007 Pallets
+ :license: BSD-3-Clause
+"""
+import errno
+import os
+import platform
+import re
+import tempfile
+import warnings
+from hashlib import md5
+from time import time
+
+from .._compat import integer_types
+from .._compat import iteritems
+from .._compat import string_types
+from .._compat import text_type
+from .._compat import to_native
+from ..posixemulation import rename
+
+try:
+ import cPickle as pickle
+except ImportError: # pragma: no cover
+ import pickle
+
+warnings.warn(
+ "'werkzeug.contrib.cache' is deprecated as of version 0.15 and will"
+ " be removed in version 1.0. It has moved to https://github.com"
+ "/pallets/cachelib.",
+ DeprecationWarning,
+ stacklevel=2,
+)
+
+
+def _items(mappingorseq):
+ """Wrapper for efficient iteration over mappings represented by dicts
+ or sequences::
+
+ >>> for k, v in _items((i, i*i) for i in xrange(5)):
+ ... assert k*k == v
+
+ >>> for k, v in _items(dict((i, i*i) for i in xrange(5))):
+ ... assert k*k == v
+
+ """
+ if hasattr(mappingorseq, "items"):
+ return iteritems(mappingorseq)
+ return mappingorseq
+
+
+class BaseCache(object):
+ """Baseclass for the cache systems. All the cache systems implement this
+ API or a superset of it.
+
+ :param default_timeout: the default timeout (in seconds) that is used if
+ no timeout is specified on :meth:`set`. A timeout
+ of 0 indicates that the cache never expires.
+ """
+
+ def __init__(self, default_timeout=300):
+ self.default_timeout = default_timeout
+
+ def _normalize_timeout(self, timeout):
+ if timeout is None:
+ timeout = self.default_timeout
+ return timeout
+
+ def get(self, key):
+ """Look up key in the cache and return the value for it.
+
+ :param key: the key to be looked up.
+ :returns: The value if it exists and is readable, else ``None``.
+ """
+ return None
+
+ def delete(self, key):
+ """Delete `key` from the cache.
+
+ :param key: the key to delete.
+ :returns: Whether the key existed and has been deleted.
+ :rtype: boolean
+ """
+ return True
+
+ def get_many(self, *keys):
+ """Returns a list of values for the given keys.
+ For each key an item in the list is created::
+
+ foo, bar = cache.get_many("foo", "bar")
+
+ Has the same error handling as :meth:`get`.
+
+ :param keys: The function accepts multiple keys as positional
+ arguments.
+ """
+ return [self.get(k) for k in keys]
+
+ def get_dict(self, *keys):
+ """Like :meth:`get_many` but return a dict::
+
+ d = cache.get_dict("foo", "bar")
+ foo = d["foo"]
+ bar = d["bar"]
+
+ :param keys: The function accepts multiple keys as positional
+ arguments.
+ """
+ return dict(zip(keys, self.get_many(*keys)))
+
+ def set(self, key, value, timeout=None):
+ """Add a new key/value to the cache (overwrites value, if key already
+ exists in the cache).
+
+ :param key: the key to set
+ :param value: the value for the key
+ :param timeout: the cache timeout for the key in seconds (if not
+ specified, it uses the default timeout). A timeout of
+ 0 idicates that the cache never expires.
+ :returns: ``True`` if key has been updated, ``False`` for backend
+ errors. Pickling errors, however, will raise a subclass of
+ ``pickle.PickleError``.
+ :rtype: boolean
+ """
+ return True
+
+ def add(self, key, value, timeout=None):
+ """Works like :meth:`set` but does not overwrite the values of already
+ existing keys.
+
+ :param key: the key to set
+ :param value: the value for the key
+ :param timeout: the cache timeout for the key in seconds (if not
+ specified, it uses the default timeout). A timeout of
+ 0 idicates that the cache never expires.
+ :returns: Same as :meth:`set`, but also ``False`` for already
+ existing keys.
+ :rtype: boolean
+ """
+ return True
+
+ def set_many(self, mapping, timeout=None):
+ """Sets multiple keys and values from a mapping.
+
+ :param mapping: a mapping with the keys/values to set.
+ :param timeout: the cache timeout for the key in seconds (if not
+ specified, it uses the default timeout). A timeout of
+ 0 idicates that the cache never expires.
+ :returns: Whether all given keys have been set.
+ :rtype: boolean
+ """
+ rv = True
+ for key, value in _items(mapping):
+ if not self.set(key, value, timeout):
+ rv = False
+ return rv
+
+ def delete_many(self, *keys):
+ """Deletes multiple keys at once.
+
+ :param keys: The function accepts multiple keys as positional
+ arguments.
+ :returns: Whether all given keys have been deleted.
+ :rtype: boolean
+ """
+ return all(self.delete(key) for key in keys)
+
+ def has(self, key):
+ """Checks if a key exists in the cache without returning it. This is a
+ cheap operation that bypasses loading the actual data on the backend.
+
+ This method is optional and may not be implemented on all caches.
+
+ :param key: the key to check
+ """
+ raise NotImplementedError(
+ "%s doesn't have an efficient implementation of `has`. That "
+ "means it is impossible to check whether a key exists without "
+ "fully loading the key's data. Consider using `self.get` "
+ "explicitly if you don't care about performance."
+ )
+
+ def clear(self):
+ """Clears the cache. Keep in mind that not all caches support
+ completely clearing the cache.
+
+ :returns: Whether the cache has been cleared.
+ :rtype: boolean
+ """
+ return True
+
+ def inc(self, key, delta=1):
+ """Increments the value of a key by `delta`. If the key does
+ not yet exist it is initialized with `delta`.
+
+ For supporting caches this is an atomic operation.
+
+ :param key: the key to increment.
+ :param delta: the delta to add.
+ :returns: The new value or ``None`` for backend errors.
+ """
+ value = (self.get(key) or 0) + delta
+ return value if self.set(key, value) else None
+
+ def dec(self, key, delta=1):
+ """Decrements the value of a key by `delta`. If the key does
+ not yet exist it is initialized with `-delta`.
+
+ For supporting caches this is an atomic operation.
+
+ :param key: the key to increment.
+ :param delta: the delta to subtract.
+ :returns: The new value or `None` for backend errors.
+ """
+ value = (self.get(key) or 0) - delta
+ return value if self.set(key, value) else None
+
+
+class NullCache(BaseCache):
+ """A cache that doesn't cache. This can be useful for unit testing.
+
+ :param default_timeout: a dummy parameter that is ignored but exists
+ for API compatibility with other caches.
+ """
+
+ def has(self, key):
+ return False
+
+
+class SimpleCache(BaseCache):
+ """Simple memory cache for single process environments. This class exists
+ mainly for the development server and is not 100% thread safe. It tries
+ to use as many atomic operations as possible and no locks for simplicity
+ but it could happen under heavy load that keys are added multiple times.
+
+ :param threshold: the maximum number of items the cache stores before
+ it starts deleting some.
+ :param default_timeout: the default timeout that is used if no timeout is
+ specified on :meth:`~BaseCache.set`. A timeout of
+ 0 indicates that the cache never expires.
+ """
+
+ def __init__(self, threshold=500, default_timeout=300):
+ BaseCache.__init__(self, default_timeout)
+ self._cache = {}
+ self.clear = self._cache.clear
+ self._threshold = threshold
+
+ def _prune(self):
+ if len(self._cache) > self._threshold:
+ now = time()
+ toremove = []
+ for idx, (key, (expires, _)) in enumerate(self._cache.items()):
+ if (expires != 0 and expires <= now) or idx % 3 == 0:
+ toremove.append(key)
+ for key in toremove:
+ self._cache.pop(key, None)
+
+ def _normalize_timeout(self, timeout):
+ timeout = BaseCache._normalize_timeout(self, timeout)
+ if timeout > 0:
+ timeout = time() + timeout
+ return timeout
+
+ def get(self, key):
+ try:
+ expires, value = self._cache[key]
+ if expires == 0 or expires > time():
+ return pickle.loads(value)
+ except (KeyError, pickle.PickleError):
+ return None
+
+ def set(self, key, value, timeout=None):
+ expires = self._normalize_timeout(timeout)
+ self._prune()
+ self._cache[key] = (expires, pickle.dumps(value, pickle.HIGHEST_PROTOCOL))
+ return True
+
+ def add(self, key, value, timeout=None):
+ expires = self._normalize_timeout(timeout)
+ self._prune()
+ item = (expires, pickle.dumps(value, pickle.HIGHEST_PROTOCOL))
+ if key in self._cache:
+ return False
+ self._cache.setdefault(key, item)
+ return True
+
+ def delete(self, key):
+ return self._cache.pop(key, None) is not None
+
+ def has(self, key):
+ try:
+ expires, value = self._cache[key]
+ return expires == 0 or expires > time()
+ except KeyError:
+ return False
+
+
+_test_memcached_key = re.compile(r"[^\x00-\x21\xff]{1,250}$").match
+
+
+class MemcachedCache(BaseCache):
+ """A cache that uses memcached as backend.
+
+ The first argument can either be an object that resembles the API of a
+ :class:`memcache.Client` or a tuple/list of server addresses. In the
+ event that a tuple/list is passed, Werkzeug tries to import the best
+ available memcache library.
+
+ This cache looks into the following packages/modules to find bindings for
+ memcached:
+
+ - ``pylibmc``
+ - ``google.appengine.api.memcached``
+ - ``memcached``
+ - ``libmc``
+
+ Implementation notes: This cache backend works around some limitations in
+ memcached to simplify the interface. For example unicode keys are encoded
+ to utf-8 on the fly. Methods such as :meth:`~BaseCache.get_dict` return
+ the keys in the same format as passed. Furthermore all get methods
+ silently ignore key errors to not cause problems when untrusted user data
+ is passed to the get methods which is often the case in web applications.
+
+ :param servers: a list or tuple of server addresses or alternatively
+ a :class:`memcache.Client` or a compatible client.
+ :param default_timeout: the default timeout that is used if no timeout is
+ specified on :meth:`~BaseCache.set`. A timeout of
+ 0 indicates that the cache never expires.
+ :param key_prefix: a prefix that is added before all keys. This makes it
+ possible to use the same memcached server for different
+ applications. Keep in mind that
+ :meth:`~BaseCache.clear` will also clear keys with a
+ different prefix.
+ """
+
+ def __init__(self, servers=None, default_timeout=300, key_prefix=None):
+ BaseCache.__init__(self, default_timeout)
+ if servers is None or isinstance(servers, (list, tuple)):
+ if servers is None:
+ servers = ["127.0.0.1:11211"]
+ self._client = self.import_preferred_memcache_lib(servers)
+ if self._client is None:
+ raise RuntimeError("no memcache module found")
+ else:
+ # NOTE: servers is actually an already initialized memcache
+ # client.
+ self._client = servers
+
+ self.key_prefix = to_native(key_prefix)
+
+ def _normalize_key(self, key):
+ key = to_native(key, "utf-8")
+ if self.key_prefix:
+ key = self.key_prefix + key
+ return key
+
+ def _normalize_timeout(self, timeout):
+ timeout = BaseCache._normalize_timeout(self, timeout)
+ if timeout > 0:
+ timeout = int(time()) + timeout
+ return timeout
+
+ def get(self, key):
+ key = self._normalize_key(key)
+ # memcached doesn't support keys longer than that. Because often
+ # checks for so long keys can occur because it's tested from user
+ # submitted data etc we fail silently for getting.
+ if _test_memcached_key(key):
+ return self._client.get(key)
+
+ def get_dict(self, *keys):
+ key_mapping = {}
+ have_encoded_keys = False
+ for key in keys:
+ encoded_key = self._normalize_key(key)
+ if not isinstance(key, str):
+ have_encoded_keys = True
+ if _test_memcached_key(key):
+ key_mapping[encoded_key] = key
+ _keys = list(key_mapping)
+ d = rv = self._client.get_multi(_keys)
+ if have_encoded_keys or self.key_prefix:
+ rv = {}
+ for key, value in iteritems(d):
+ rv[key_mapping[key]] = value
+ if len(rv) < len(keys):
+ for key in keys:
+ if key not in rv:
+ rv[key] = None
+ return rv
+
+ def add(self, key, value, timeout=None):
+ key = self._normalize_key(key)
+ timeout = self._normalize_timeout(timeout)
+ return self._client.add(key, value, timeout)
+
+ def set(self, key, value, timeout=None):
+ key = self._normalize_key(key)
+ timeout = self._normalize_timeout(timeout)
+ return self._client.set(key, value, timeout)
+
+ def get_many(self, *keys):
+ d = self.get_dict(*keys)
+ return [d[key] for key in keys]
+
+ def set_many(self, mapping, timeout=None):
+ new_mapping = {}
+ for key, value in _items(mapping):
+ key = self._normalize_key(key)
+ new_mapping[key] = value
+
+ timeout = self._normalize_timeout(timeout)
+ failed_keys = self._client.set_multi(new_mapping, timeout)
+ return not failed_keys
+
+ def delete(self, key):
+ key = self._normalize_key(key)
+ if _test_memcached_key(key):
+ return self._client.delete(key)
+
+ def delete_many(self, *keys):
+ new_keys = []
+ for key in keys:
+ key = self._normalize_key(key)
+ if _test_memcached_key(key):
+ new_keys.append(key)
+ return self._client.delete_multi(new_keys)
+
+ def has(self, key):
+ key = self._normalize_key(key)
+ if _test_memcached_key(key):
+ return self._client.append(key, "")
+ return False
+
+ def clear(self):
+ return self._client.flush_all()
+
+ def inc(self, key, delta=1):
+ key = self._normalize_key(key)
+ return self._client.incr(key, delta)
+
+ def dec(self, key, delta=1):
+ key = self._normalize_key(key)
+ return self._client.decr(key, delta)
+
+ def import_preferred_memcache_lib(self, servers):
+ """Returns an initialized memcache client. Used by the constructor."""
+ try:
+ import pylibmc
+ except ImportError:
+ pass
+ else:
+ return pylibmc.Client(servers)
+
+ try:
+ from google.appengine.api import memcache
+ except ImportError:
+ pass
+ else:
+ return memcache.Client()
+
+ try:
+ import memcache
+ except ImportError:
+ pass
+ else:
+ return memcache.Client(servers)
+
+ try:
+ import libmc
+ except ImportError:
+ pass
+ else:
+ return libmc.Client(servers)
+
+
+# backwards compatibility
+GAEMemcachedCache = MemcachedCache
+
+
+class RedisCache(BaseCache):
+ """Uses the Redis key-value store as a cache backend.
+
+ The first argument can be either a string denoting address of the Redis
+ server or an object resembling an instance of a redis.Redis class.
+
+ Note: Python Redis API already takes care of encoding unicode strings on
+ the fly.
+
+ .. versionadded:: 0.7
+
+ .. versionadded:: 0.8
+ `key_prefix` was added.
+
+ .. versionchanged:: 0.8
+ This cache backend now properly serializes objects.
+
+ .. versionchanged:: 0.8.3
+ This cache backend now supports password authentication.
+
+ .. versionchanged:: 0.10
+ ``**kwargs`` is now passed to the redis object.
+
+ :param host: address of the Redis server or an object which API is
+ compatible with the official Python Redis client (redis-py).
+ :param port: port number on which Redis server listens for connections.
+ :param password: password authentication for the Redis server.
+ :param db: db (zero-based numeric index) on Redis Server to connect.
+ :param default_timeout: the default timeout that is used if no timeout is
+ specified on :meth:`~BaseCache.set`. A timeout of
+ 0 indicates that the cache never expires.
+ :param key_prefix: A prefix that should be added to all keys.
+
+ Any additional keyword arguments will be passed to ``redis.Redis``.
+ """
+
+ def __init__(
+ self,
+ host="localhost",
+ port=6379,
+ password=None,
+ db=0,
+ default_timeout=300,
+ key_prefix=None,
+ **kwargs
+ ):
+ BaseCache.__init__(self, default_timeout)
+ if host is None:
+ raise ValueError("RedisCache host parameter may not be None")
+ if isinstance(host, string_types):
+ try:
+ import redis
+ except ImportError:
+ raise RuntimeError("no redis module found")
+ if kwargs.get("decode_responses", None):
+ raise ValueError("decode_responses is not supported by RedisCache.")
+ self._client = redis.Redis(
+ host=host, port=port, password=password, db=db, **kwargs
+ )
+ else:
+ self._client = host
+ self.key_prefix = key_prefix or ""
+
+ def _normalize_timeout(self, timeout):
+ timeout = BaseCache._normalize_timeout(self, timeout)
+ if timeout == 0:
+ timeout = -1
+ return timeout
+
+ def dump_object(self, value):
+ """Dumps an object into a string for redis. By default it serializes
+ integers as regular string and pickle dumps everything else.
+ """
+ t = type(value)
+ if t in integer_types:
+ return str(value).encode("ascii")
+ return b"!" + pickle.dumps(value)
+
+ def load_object(self, value):
+ """The reversal of :meth:`dump_object`. This might be called with
+ None.
+ """
+ if value is None:
+ return None
+ if value.startswith(b"!"):
+ try:
+ return pickle.loads(value[1:])
+ except pickle.PickleError:
+ return None
+ try:
+ return int(value)
+ except ValueError:
+ # before 0.8 we did not have serialization. Still support that.
+ return value
+
+ def get(self, key):
+ return self.load_object(self._client.get(self.key_prefix + key))
+
+ def get_many(self, *keys):
+ if self.key_prefix:
+ keys = [self.key_prefix + key for key in keys]
+ return [self.load_object(x) for x in self._client.mget(keys)]
+
+ def set(self, key, value, timeout=None):
+ timeout = self._normalize_timeout(timeout)
+ dump = self.dump_object(value)
+ if timeout == -1:
+ result = self._client.set(name=self.key_prefix + key, value=dump)
+ else:
+ result = self._client.setex(
+ name=self.key_prefix + key, value=dump, time=timeout
+ )
+ return result
+
+ def add(self, key, value, timeout=None):
+ timeout = self._normalize_timeout(timeout)
+ dump = self.dump_object(value)
+ return self._client.setnx(
+ name=self.key_prefix + key, value=dump
+ ) and self._client.expire(name=self.key_prefix + key, time=timeout)
+
+ def set_many(self, mapping, timeout=None):
+ timeout = self._normalize_timeout(timeout)
+ # Use transaction=False to batch without calling redis MULTI
+ # which is not supported by twemproxy
+ pipe = self._client.pipeline(transaction=False)
+
+ for key, value in _items(mapping):
+ dump = self.dump_object(value)
+ if timeout == -1:
+ pipe.set(name=self.key_prefix + key, value=dump)
+ else:
+ pipe.setex(name=self.key_prefix + key, value=dump, time=timeout)
+ return pipe.execute()
+
+ def delete(self, key):
+ return self._client.delete(self.key_prefix + key)
+
+ def delete_many(self, *keys):
+ if not keys:
+ return
+ if self.key_prefix:
+ keys = [self.key_prefix + key for key in keys]
+ return self._client.delete(*keys)
+
+ def has(self, key):
+ return self._client.exists(self.key_prefix + key)
+
+ def clear(self):
+ status = False
+ if self.key_prefix:
+ keys = self._client.keys(self.key_prefix + "*")
+ if keys:
+ status = self._client.delete(*keys)
+ else:
+ status = self._client.flushdb()
+ return status
+
+ def inc(self, key, delta=1):
+ return self._client.incr(name=self.key_prefix + key, amount=delta)
+
+ def dec(self, key, delta=1):
+ return self._client.decr(name=self.key_prefix + key, amount=delta)
+
+
+class FileSystemCache(BaseCache):
+ """A cache that stores the items on the file system. This cache depends
+ on being the only user of the `cache_dir`. Make absolutely sure that
+ nobody but this cache stores files there or otherwise the cache will
+ randomly delete files therein.
+
+ :param cache_dir: the directory where cache files are stored.
+ :param threshold: the maximum number of items the cache stores before
+ it starts deleting some. A threshold value of 0
+ indicates no threshold.
+ :param default_timeout: the default timeout that is used if no timeout is
+ specified on :meth:`~BaseCache.set`. A timeout of
+ 0 indicates that the cache never expires.
+ :param mode: the file mode wanted for the cache files, default 0600
+ """
+
+ #: used for temporary files by the FileSystemCache
+ _fs_transaction_suffix = ".__wz_cache"
+ #: keep amount of files in a cache element
+ _fs_count_file = "__wz_cache_count"
+
+ def __init__(self, cache_dir, threshold=500, default_timeout=300, mode=0o600):
+ BaseCache.__init__(self, default_timeout)
+ self._path = cache_dir
+ self._threshold = threshold
+ self._mode = mode
+
+ try:
+ os.makedirs(self._path)
+ except OSError as ex:
+ if ex.errno != errno.EEXIST:
+ raise
+
+ self._update_count(value=len(self._list_dir()))
+
+ @property
+ def _file_count(self):
+ return self.get(self._fs_count_file) or 0
+
+ def _update_count(self, delta=None, value=None):
+ # If we have no threshold, don't count files
+ if self._threshold == 0:
+ return
+
+ if delta:
+ new_count = self._file_count + delta
+ else:
+ new_count = value or 0
+ self.set(self._fs_count_file, new_count, mgmt_element=True)
+
+ def _normalize_timeout(self, timeout):
+ timeout = BaseCache._normalize_timeout(self, timeout)
+ if timeout != 0:
+ timeout = time() + timeout
+ return int(timeout)
+
+ def _list_dir(self):
+ """return a list of (fully qualified) cache filenames
+ """
+ mgmt_files = [
+ self._get_filename(name).split("/")[-1] for name in (self._fs_count_file,)
+ ]
+ return [
+ os.path.join(self._path, fn)
+ for fn in os.listdir(self._path)
+ if not fn.endswith(self._fs_transaction_suffix) and fn not in mgmt_files
+ ]
+
+ def _prune(self):
+ if self._threshold == 0 or not self._file_count > self._threshold:
+ return
+
+ entries = self._list_dir()
+ now = time()
+ for idx, fname in enumerate(entries):
+ try:
+ remove = False
+ with open(fname, "rb") as f:
+ expires = pickle.load(f)
+ remove = (expires != 0 and expires <= now) or idx % 3 == 0
+
+ if remove:
+ os.remove(fname)
+ except (IOError, OSError):
+ pass
+ self._update_count(value=len(self._list_dir()))
+
+ def clear(self):
+ for fname in self._list_dir():
+ try:
+ os.remove(fname)
+ except (IOError, OSError):
+ self._update_count(value=len(self._list_dir()))
+ return False
+ self._update_count(value=0)
+ return True
+
+ def _get_filename(self, key):
+ if isinstance(key, text_type):
+ key = key.encode("utf-8") # XXX unicode review
+ hash = md5(key).hexdigest()
+ return os.path.join(self._path, hash)
+
+ def get(self, key):
+ filename = self._get_filename(key)
+ try:
+ with open(filename, "rb") as f:
+ pickle_time = pickle.load(f)
+ if pickle_time == 0 or pickle_time >= time():
+ return pickle.load(f)
+ else:
+ os.remove(filename)
+ return None
+ except (IOError, OSError, pickle.PickleError):
+ return None
+
+ def add(self, key, value, timeout=None):
+ filename = self._get_filename(key)
+ if not os.path.exists(filename):
+ return self.set(key, value, timeout)
+ return False
+
+ def set(self, key, value, timeout=None, mgmt_element=False):
+ # Management elements have no timeout
+ if mgmt_element:
+ timeout = 0
+
+ # Don't prune on management element update, to avoid loop
+ else:
+ self._prune()
+
+ timeout = self._normalize_timeout(timeout)
+ filename = self._get_filename(key)
+ try:
+ fd, tmp = tempfile.mkstemp(
+ suffix=self._fs_transaction_suffix, dir=self._path
+ )
+ with os.fdopen(fd, "wb") as f:
+ pickle.dump(timeout, f, 1)
+ pickle.dump(value, f, pickle.HIGHEST_PROTOCOL)
+ rename(tmp, filename)
+ os.chmod(filename, self._mode)
+ except (IOError, OSError):
+ return False
+ else:
+ # Management elements should not count towards threshold
+ if not mgmt_element:
+ self._update_count(delta=1)
+ return True
+
+ def delete(self, key, mgmt_element=False):
+ try:
+ os.remove(self._get_filename(key))
+ except (IOError, OSError):
+ return False
+ else:
+ # Management elements should not count towards threshold
+ if not mgmt_element:
+ self._update_count(delta=-1)
+ return True
+
+ def has(self, key):
+ filename = self._get_filename(key)
+ try:
+ with open(filename, "rb") as f:
+ pickle_time = pickle.load(f)
+ if pickle_time == 0 or pickle_time >= time():
+ return True
+ else:
+ os.remove(filename)
+ return False
+ except (IOError, OSError, pickle.PickleError):
+ return False
+
+
+class UWSGICache(BaseCache):
+ """Implements the cache using uWSGI's caching framework.
+
+ .. note::
+ This class cannot be used when running under PyPy, because the uWSGI
+ API implementation for PyPy is lacking the needed functionality.
+
+ :param default_timeout: The default timeout in seconds.
+ :param cache: The name of the caching instance to connect to, for
+ example: mycache@localhost:3031, defaults to an empty string, which
+ means uWSGI will cache in the local instance. If the cache is in the
+ same instance as the werkzeug app, you only have to provide the name of
+ the cache.
+ """
+
+ def __init__(self, default_timeout=300, cache=""):
+ BaseCache.__init__(self, default_timeout)
+
+ if platform.python_implementation() == "PyPy":
+ raise RuntimeError(
+ "uWSGI caching does not work under PyPy, see "
+ "the docs for more details."
+ )
+
+ try:
+ import uwsgi
+
+ self._uwsgi = uwsgi
+ except ImportError:
+ raise RuntimeError(
+ "uWSGI could not be imported, are you running under uWSGI?"
+ )
+
+ self.cache = cache
+
+ def get(self, key):
+ rv = self._uwsgi.cache_get(key, self.cache)
+ if rv is None:
+ return
+ return pickle.loads(rv)
+
+ def delete(self, key):
+ return self._uwsgi.cache_del(key, self.cache)
+
+ def set(self, key, value, timeout=None):
+ return self._uwsgi.cache_update(
+ key, pickle.dumps(value), self._normalize_timeout(timeout), self.cache
+ )
+
+ def add(self, key, value, timeout=None):
+ return self._uwsgi.cache_set(
+ key, pickle.dumps(value), self._normalize_timeout(timeout), self.cache
+ )
+
+ def clear(self):
+ return self._uwsgi.cache_clear(self.cache)
+
+ def has(self, key):
+ return self._uwsgi.cache_exists(key, self.cache) is not None
diff --git a/python/werkzeug/contrib/fixers.py b/python/werkzeug/contrib/fixers.py
new file mode 100644
index 0000000..8df0afd
--- /dev/null
+++ b/python/werkzeug/contrib/fixers.py
@@ -0,0 +1,262 @@
+"""
+Fixers
+======
+
+.. warning::
+ .. deprecated:: 0.15
+ ``ProxyFix`` has moved to :mod:`werkzeug.middleware.proxy_fix`.
+ All other code in this module is deprecated and will be removed
+ in version 1.0.
+
+.. versionadded:: 0.5
+
+This module includes various helpers that fix web server behavior.
+
+.. autoclass:: ProxyFix
+ :members:
+
+.. autoclass:: CGIRootFix
+
+.. autoclass:: PathInfoFromRequestUriFix
+
+.. autoclass:: HeaderRewriterFix
+
+.. autoclass:: InternetExplorerFix
+
+:copyright: 2007 Pallets
+:license: BSD-3-Clause
+"""
+import warnings
+
+from ..datastructures import Headers
+from ..datastructures import ResponseCacheControl
+from ..http import parse_cache_control_header
+from ..http import parse_options_header
+from ..http import parse_set_header
+from ..middleware.proxy_fix import ProxyFix as _ProxyFix
+from ..useragents import UserAgent
+
+try:
+ from urllib.parse import unquote
+except ImportError:
+ from urllib import unquote
+
+
+class CGIRootFix(object):
+ """Wrap the application in this middleware if you are using FastCGI
+ or CGI and you have problems with your app root being set to the CGI
+ script's path instead of the path users are going to visit.
+
+ :param app: the WSGI application
+ :param app_root: Defaulting to ``'/'``, you can set this to
+ something else if your app is mounted somewhere else.
+
+ .. deprecated:: 0.15
+ This middleware will be removed in version 1.0.
+
+ .. versionchanged:: 0.9
+ Added `app_root` parameter and renamed from
+ ``LighttpdCGIRootFix``.
+ """
+
+ def __init__(self, app, app_root="/"):
+ warnings.warn(
+ "'CGIRootFix' is deprecated as of version 0.15 and will be"
+ " removed in version 1.0.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ self.app = app
+ self.app_root = app_root.strip("/")
+
+ def __call__(self, environ, start_response):
+ environ["SCRIPT_NAME"] = self.app_root
+ return self.app(environ, start_response)
+
+
+class LighttpdCGIRootFix(CGIRootFix):
+ def __init__(self, *args, **kwargs):
+ warnings.warn(
+ "'LighttpdCGIRootFix' is renamed 'CGIRootFix'. Both will be"
+ " removed in version 1.0.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ super(LighttpdCGIRootFix, self).__init__(*args, **kwargs)
+
+
+class PathInfoFromRequestUriFix(object):
+ """On windows environment variables are limited to the system charset
+ which makes it impossible to store the `PATH_INFO` variable in the
+ environment without loss of information on some systems.
+
+ This is for example a problem for CGI scripts on a Windows Apache.
+
+ This fixer works by recreating the `PATH_INFO` from `REQUEST_URI`,
+ `REQUEST_URL`, or `UNENCODED_URL` (whatever is available). Thus the
+ fix can only be applied if the webserver supports either of these
+ variables.
+
+ :param app: the WSGI application
+
+ .. deprecated:: 0.15
+ This middleware will be removed in version 1.0.
+ """
+
+ def __init__(self, app):
+ warnings.warn(
+ "'PathInfoFromRequestUriFix' is deprecated as of version"
+ " 0.15 and will be removed in version 1.0.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ self.app = app
+
+ def __call__(self, environ, start_response):
+ for key in "REQUEST_URL", "REQUEST_URI", "UNENCODED_URL":
+ if key not in environ:
+ continue
+ request_uri = unquote(environ[key])
+ script_name = unquote(environ.get("SCRIPT_NAME", ""))
+ if request_uri.startswith(script_name):
+ environ["PATH_INFO"] = request_uri[len(script_name) :].split("?", 1)[0]
+ break
+ return self.app(environ, start_response)
+
+
+class ProxyFix(_ProxyFix):
+ """
+ .. deprecated:: 0.15
+ ``werkzeug.contrib.fixers.ProxyFix`` has moved to
+ :mod:`werkzeug.middleware.proxy_fix`. This import will be
+ removed in 1.0.
+ """
+
+ def __init__(self, *args, **kwargs):
+ warnings.warn(
+ "'werkzeug.contrib.fixers.ProxyFix' has moved to 'werkzeug"
+ ".middleware.proxy_fix.ProxyFix'. This import is deprecated"
+ " as of version 0.15 and will be removed in 1.0.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ super(ProxyFix, self).__init__(*args, **kwargs)
+
+
+class HeaderRewriterFix(object):
+ """This middleware can remove response headers and add others. This
+ is for example useful to remove the `Date` header from responses if you
+ are using a server that adds that header, no matter if it's present or
+ not or to add `X-Powered-By` headers::
+
+ app = HeaderRewriterFix(app, remove_headers=['Date'],
+ add_headers=[('X-Powered-By', 'WSGI')])
+
+ :param app: the WSGI application
+ :param remove_headers: a sequence of header keys that should be
+ removed.
+ :param add_headers: a sequence of ``(key, value)`` tuples that should
+ be added.
+
+ .. deprecated:: 0.15
+ This middleware will be removed in 1.0.
+ """
+
+ def __init__(self, app, remove_headers=None, add_headers=None):
+ warnings.warn(
+ "'HeaderRewriterFix' is deprecated as of version 0.15 and"
+ " will be removed in version 1.0.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ self.app = app
+ self.remove_headers = set(x.lower() for x in (remove_headers or ()))
+ self.add_headers = list(add_headers or ())
+
+ def __call__(self, environ, start_response):
+ def rewriting_start_response(status, headers, exc_info=None):
+ new_headers = []
+ for key, value in headers:
+ if key.lower() not in self.remove_headers:
+ new_headers.append((key, value))
+ new_headers += self.add_headers
+ return start_response(status, new_headers, exc_info)
+
+ return self.app(environ, rewriting_start_response)
+
+
+class InternetExplorerFix(object):
+ """This middleware fixes a couple of bugs with Microsoft Internet
+ Explorer. Currently the following fixes are applied:
+
+ - removing of `Vary` headers for unsupported mimetypes which
+ causes troubles with caching. Can be disabled by passing
+ ``fix_vary=False`` to the constructor.
+ see: https://support.microsoft.com/en-us/help/824847
+
+ - removes offending headers to work around caching bugs in
+ Internet Explorer if `Content-Disposition` is set. Can be
+ disabled by passing ``fix_attach=False`` to the constructor.
+
+ If it does not detect affected Internet Explorer versions it won't touch
+ the request / response.
+
+ .. deprecated:: 0.15
+ This middleware will be removed in 1.0.
+ """
+
+ # This code was inspired by Django fixers for the same bugs. The
+ # fix_vary and fix_attach fixers were originally implemented in Django
+ # by Michael Axiak and is available as part of the Django project:
+ # https://code.djangoproject.com/ticket/4148
+
+ def __init__(self, app, fix_vary=True, fix_attach=True):
+ warnings.warn(
+ "'InternetExplorerFix' is deprecated as of version 0.15 and"
+ " will be removed in version 1.0.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ self.app = app
+ self.fix_vary = fix_vary
+ self.fix_attach = fix_attach
+
+ def fix_headers(self, environ, headers, status=None):
+ if self.fix_vary:
+ header = headers.get("content-type", "")
+ mimetype, options = parse_options_header(header)
+ if mimetype not in ("text/html", "text/plain", "text/sgml"):
+ headers.pop("vary", None)
+
+ if self.fix_attach and "content-disposition" in headers:
+ pragma = parse_set_header(headers.get("pragma", ""))
+ pragma.discard("no-cache")
+ header = pragma.to_header()
+ if not header:
+ headers.pop("pragma", "")
+ else:
+ headers["Pragma"] = header
+ header = headers.get("cache-control", "")
+ if header:
+ cc = parse_cache_control_header(header, cls=ResponseCacheControl)
+ cc.no_cache = None
+ cc.no_store = False
+ header = cc.to_header()
+ if not header:
+ headers.pop("cache-control", "")
+ else:
+ headers["Cache-Control"] = header
+
+ def run_fixed(self, environ, start_response):
+ def fixing_start_response(status, headers, exc_info=None):
+ headers = Headers(headers)
+ self.fix_headers(environ, headers, status)
+ return start_response(status, headers.to_wsgi_list(), exc_info)
+
+ return self.app(environ, fixing_start_response)
+
+ def __call__(self, environ, start_response):
+ ua = UserAgent(environ)
+ if ua.browser != "msie":
+ return self.app(environ, start_response)
+ return self.run_fixed(environ, start_response)
diff --git a/python/werkzeug/contrib/iterio.py b/python/werkzeug/contrib/iterio.py
new file mode 100644
index 0000000..b672454
--- /dev/null
+++ b/python/werkzeug/contrib/iterio.py
@@ -0,0 +1,358 @@
+# -*- coding: utf-8 -*-
+r"""
+ werkzeug.contrib.iterio
+ ~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module implements a :class:`IterIO` that converts an iterator into
+ a stream object and the other way round. Converting streams into
+ iterators requires the `greenlet`_ module.
+
+ To convert an iterator into a stream all you have to do is to pass it
+ directly to the :class:`IterIO` constructor. In this example we pass it
+ a newly created generator::
+
+ def foo():
+ yield "something\n"
+ yield "otherthings"
+ stream = IterIO(foo())
+ print stream.read() # read the whole iterator
+
+ The other way round works a bit different because we have to ensure that
+ the code execution doesn't take place yet. An :class:`IterIO` call with a
+ callable as first argument does two things. The function itself is passed
+ an :class:`IterIO` stream it can feed. The object returned by the
+ :class:`IterIO` constructor on the other hand is not an stream object but
+ an iterator::
+
+ def foo(stream):
+ stream.write("some")
+ stream.write("thing")
+ stream.flush()
+ stream.write("otherthing")
+ iterator = IterIO(foo)
+ print iterator.next() # prints something
+ print iterator.next() # prints otherthing
+ iterator.next() # raises StopIteration
+
+ .. _greenlet: https://github.com/python-greenlet/greenlet
+
+ :copyright: 2007 Pallets
+ :license: BSD-3-Clause
+"""
+import warnings
+
+from .._compat import implements_iterator
+
+try:
+ import greenlet
+except ImportError:
+ greenlet = None
+
+warnings.warn(
+ "'werkzeug.contrib.iterio' is deprecated as of version 0.15 and"
+ " will be removed in version 1.0.",
+ DeprecationWarning,
+ stacklevel=2,
+)
+
+
+def _mixed_join(iterable, sentinel):
+ """concatenate any string type in an intelligent way."""
+ iterator = iter(iterable)
+ first_item = next(iterator, sentinel)
+ if isinstance(first_item, bytes):
+ return first_item + b"".join(iterator)
+ return first_item + u"".join(iterator)
+
+
+def _newline(reference_string):
+ if isinstance(reference_string, bytes):
+ return b"\n"
+ return u"\n"
+
+
+@implements_iterator
+class IterIO(object):
+ """Instances of this object implement an interface compatible with the
+ standard Python :class:`file` object. Streams are either read-only or
+ write-only depending on how the object is created.
+
+ If the first argument is an iterable a file like object is returned that
+ returns the contents of the iterable. In case the iterable is empty
+ read operations will return the sentinel value.
+
+ If the first argument is a callable then the stream object will be
+ created and passed to that function. The caller itself however will
+ not receive a stream but an iterable. The function will be executed
+ step by step as something iterates over the returned iterable. Each
+ call to :meth:`flush` will create an item for the iterable. If
+ :meth:`flush` is called without any writes in-between the sentinel
+ value will be yielded.
+
+ Note for Python 3: due to the incompatible interface of bytes and
+ streams you should set the sentinel value explicitly to an empty
+ bytestring (``b''``) if you are expecting to deal with bytes as
+ otherwise the end of the stream is marked with the wrong sentinel
+ value.
+
+ .. versionadded:: 0.9
+ `sentinel` parameter was added.
+ """
+
+ def __new__(cls, obj, sentinel=""):
+ try:
+ iterator = iter(obj)
+ except TypeError:
+ return IterI(obj, sentinel)
+ return IterO(iterator, sentinel)
+
+ def __iter__(self):
+ return self
+
+ def tell(self):
+ if self.closed:
+ raise ValueError("I/O operation on closed file")
+ return self.pos
+
+ def isatty(self):
+ if self.closed:
+ raise ValueError("I/O operation on closed file")
+ return False
+
+ def seek(self, pos, mode=0):
+ if self.closed:
+ raise ValueError("I/O operation on closed file")
+ raise IOError(9, "Bad file descriptor")
+
+ def truncate(self, size=None):
+ if self.closed:
+ raise ValueError("I/O operation on closed file")
+ raise IOError(9, "Bad file descriptor")
+
+ def write(self, s):
+ if self.closed:
+ raise ValueError("I/O operation on closed file")
+ raise IOError(9, "Bad file descriptor")
+
+ def writelines(self, list):
+ if self.closed:
+ raise ValueError("I/O operation on closed file")
+ raise IOError(9, "Bad file descriptor")
+
+ def read(self, n=-1):
+ if self.closed:
+ raise ValueError("I/O operation on closed file")
+ raise IOError(9, "Bad file descriptor")
+
+ def readlines(self, sizehint=0):
+ if self.closed:
+ raise ValueError("I/O operation on closed file")
+ raise IOError(9, "Bad file descriptor")
+
+ def readline(self, length=None):
+ if self.closed:
+ raise ValueError("I/O operation on closed file")
+ raise IOError(9, "Bad file descriptor")
+
+ def flush(self):
+ if self.closed:
+ raise ValueError("I/O operation on closed file")
+ raise IOError(9, "Bad file descriptor")
+
+ def __next__(self):
+ if self.closed:
+ raise StopIteration()
+ line = self.readline()
+ if not line:
+ raise StopIteration()
+ return line
+
+
+class IterI(IterIO):
+ """Convert an stream into an iterator."""
+
+ def __new__(cls, func, sentinel=""):
+ if greenlet is None:
+ raise RuntimeError("IterI requires greenlet support")
+ stream = object.__new__(cls)
+ stream._parent = greenlet.getcurrent()
+ stream._buffer = []
+ stream.closed = False
+ stream.sentinel = sentinel
+ stream.pos = 0
+
+ def run():
+ func(stream)
+ stream.close()
+
+ g = greenlet.greenlet(run, stream._parent)
+ while 1:
+ rv = g.switch()
+ if not rv:
+ return
+ yield rv[0]
+
+ def close(self):
+ if not self.closed:
+ self.closed = True
+ self._flush_impl()
+
+ def write(self, s):
+ if self.closed:
+ raise ValueError("I/O operation on closed file")
+ if s:
+ self.pos += len(s)
+ self._buffer.append(s)
+
+ def writelines(self, list):
+ for item in list:
+ self.write(item)
+
+ def flush(self):
+ if self.closed:
+ raise ValueError("I/O operation on closed file")
+ self._flush_impl()
+
+ def _flush_impl(self):
+ data = _mixed_join(self._buffer, self.sentinel)
+ self._buffer = []
+ if not data and self.closed:
+ self._parent.switch()
+ else:
+ self._parent.switch((data,))
+
+
+class IterO(IterIO):
+ """Iter output. Wrap an iterator and give it a stream like interface."""
+
+ def __new__(cls, gen, sentinel=""):
+ self = object.__new__(cls)
+ self._gen = gen
+ self._buf = None
+ self.sentinel = sentinel
+ self.closed = False
+ self.pos = 0
+ return self
+
+ def __iter__(self):
+ return self
+
+ def _buf_append(self, string):
+ """Replace string directly without appending to an empty string,
+ avoiding type issues."""
+ if not self._buf:
+ self._buf = string
+ else:
+ self._buf += string
+
+ def close(self):
+ if not self.closed:
+ self.closed = True
+ if hasattr(self._gen, "close"):
+ self._gen.close()
+
+ def seek(self, pos, mode=0):
+ if self.closed:
+ raise ValueError("I/O operation on closed file")
+ if mode == 1:
+ pos += self.pos
+ elif mode == 2:
+ self.read()
+ self.pos = min(self.pos, self.pos + pos)
+ return
+ elif mode != 0:
+ raise IOError("Invalid argument")
+ buf = []
+ try:
+ tmp_end_pos = len(self._buf or "")
+ while pos > tmp_end_pos:
+ item = next(self._gen)
+ tmp_end_pos += len(item)
+ buf.append(item)
+ except StopIteration:
+ pass
+ if buf:
+ self._buf_append(_mixed_join(buf, self.sentinel))
+ self.pos = max(0, pos)
+
+ def read(self, n=-1):
+ if self.closed:
+ raise ValueError("I/O operation on closed file")
+ if n < 0:
+ self._buf_append(_mixed_join(self._gen, self.sentinel))
+ result = self._buf[self.pos :]
+ self.pos += len(result)
+ return result
+ new_pos = self.pos + n
+ buf = []
+ try:
+ tmp_end_pos = 0 if self._buf is None else len(self._buf)
+ while new_pos > tmp_end_pos or (self._buf is None and not buf):
+ item = next(self._gen)
+ tmp_end_pos += len(item)
+ buf.append(item)
+ except StopIteration:
+ pass
+ if buf:
+ self._buf_append(_mixed_join(buf, self.sentinel))
+
+ if self._buf is None:
+ return self.sentinel
+
+ new_pos = max(0, new_pos)
+ try:
+ return self._buf[self.pos : new_pos]
+ finally:
+ self.pos = min(new_pos, len(self._buf))
+
+ def readline(self, length=None):
+ if self.closed:
+ raise ValueError("I/O operation on closed file")
+
+ nl_pos = -1
+ if self._buf:
+ nl_pos = self._buf.find(_newline(self._buf), self.pos)
+ buf = []
+ try:
+ if self._buf is None:
+ pos = self.pos
+ else:
+ pos = len(self._buf)
+ while nl_pos < 0:
+ item = next(self._gen)
+ local_pos = item.find(_newline(item))
+ buf.append(item)
+ if local_pos >= 0:
+ nl_pos = pos + local_pos
+ break
+ pos += len(item)
+ except StopIteration:
+ pass
+ if buf:
+ self._buf_append(_mixed_join(buf, self.sentinel))
+
+ if self._buf is None:
+ return self.sentinel
+
+ if nl_pos < 0:
+ new_pos = len(self._buf)
+ else:
+ new_pos = nl_pos + 1
+ if length is not None and self.pos + length < new_pos:
+ new_pos = self.pos + length
+ try:
+ return self._buf[self.pos : new_pos]
+ finally:
+ self.pos = min(new_pos, len(self._buf))
+
+ def readlines(self, sizehint=0):
+ total = 0
+ lines = []
+ line = self.readline()
+ while line:
+ lines.append(line)
+ total += len(line)
+ if 0 < sizehint <= total:
+ break
+ line = self.readline()
+ return lines
diff --git a/python/werkzeug/contrib/lint.py b/python/werkzeug/contrib/lint.py
new file mode 100644
index 0000000..8bd8b8a
--- /dev/null
+++ b/python/werkzeug/contrib/lint.py
@@ -0,0 +1,11 @@
+import warnings
+
+from ..middleware.lint import * # noqa: F401, F403
+
+warnings.warn(
+ "'werkzeug.contrib.lint' has moved to 'werkzeug.middleware.lint'."
+ " This import is deprecated as of version 0.15 and will be removed"
+ " in version 1.0.",
+ DeprecationWarning,
+ stacklevel=2,
+)
diff --git a/python/werkzeug/contrib/profiler.py b/python/werkzeug/contrib/profiler.py
new file mode 100644
index 0000000..b79fe56
--- /dev/null
+++ b/python/werkzeug/contrib/profiler.py
@@ -0,0 +1,42 @@
+import warnings
+
+from ..middleware.profiler import * # noqa: F401, F403
+
+warnings.warn(
+ "'werkzeug.contrib.profiler' has moved to"
+ "'werkzeug.middleware.profiler'. This import is deprecated as of"
+ "version 0.15 and will be removed in version 1.0.",
+ DeprecationWarning,
+ stacklevel=2,
+)
+
+
+class MergeStream(object):
+ """An object that redirects ``write`` calls to multiple streams.
+ Use this to log to both ``sys.stdout`` and a file::
+
+ f = open('profiler.log', 'w')
+ stream = MergeStream(sys.stdout, f)
+ profiler = ProfilerMiddleware(app, stream)
+
+ .. deprecated:: 0.15
+ Use the ``tee`` command in your terminal instead. This class
+ will be removed in 1.0.
+ """
+
+ def __init__(self, *streams):
+ warnings.warn(
+ "'MergeStream' is deprecated as of version 0.15 and will be removed in"
+ " version 1.0. Use your terminal's 'tee' command instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+
+ if not streams:
+ raise TypeError("At least one stream must be given.")
+
+ self.streams = streams
+
+ def write(self, data):
+ for stream in self.streams:
+ stream.write(data)
diff --git a/python/werkzeug/contrib/securecookie.py b/python/werkzeug/contrib/securecookie.py
new file mode 100644
index 0000000..c4c9eee
--- /dev/null
+++ b/python/werkzeug/contrib/securecookie.py
@@ -0,0 +1,362 @@
+# -*- coding: utf-8 -*-
+r"""
+ werkzeug.contrib.securecookie
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module implements a cookie that is not alterable from the client
+ because it adds a checksum the server checks for. You can use it as
+ session replacement if all you have is a user id or something to mark
+ a logged in user.
+
+ Keep in mind that the data is still readable from the client as a
+ normal cookie is. However you don't have to store and flush the
+ sessions you have at the server.
+
+ Example usage:
+
+ >>> from werkzeug.contrib.securecookie import SecureCookie
+ >>> x = SecureCookie({"foo": 42, "baz": (1, 2, 3)}, "deadbeef")
+
+ Dumping into a string so that one can store it in a cookie:
+
+ >>> value = x.serialize()
+
+ Loading from that string again:
+
+ >>> x = SecureCookie.unserialize(value, "deadbeef")
+ >>> x["baz"]
+ (1, 2, 3)
+
+ If someone modifies the cookie and the checksum is wrong the unserialize
+ method will fail silently and return a new empty `SecureCookie` object.
+
+ Keep in mind that the values will be visible in the cookie so do not
+ store data in a cookie you don't want the user to see.
+
+ Application Integration
+ =======================
+
+ If you are using the werkzeug request objects you could integrate the
+ secure cookie into your application like this::
+
+ from werkzeug.utils import cached_property
+ from werkzeug.wrappers import BaseRequest
+ from werkzeug.contrib.securecookie import SecureCookie
+
+ # don't use this key but a different one; you could just use
+ # os.urandom(20) to get something random
+ SECRET_KEY = '\xfa\xdd\xb8z\xae\xe0}4\x8b\xea'
+
+ class Request(BaseRequest):
+
+ @cached_property
+ def client_session(self):
+ data = self.cookies.get('session_data')
+ if not data:
+ return SecureCookie(secret_key=SECRET_KEY)
+ return SecureCookie.unserialize(data, SECRET_KEY)
+
+ def application(environ, start_response):
+ request = Request(environ)
+
+ # get a response object here
+ response = ...
+
+ if request.client_session.should_save:
+ session_data = request.client_session.serialize()
+ response.set_cookie('session_data', session_data,
+ httponly=True)
+ return response(environ, start_response)
+
+ A less verbose integration can be achieved by using shorthand methods::
+
+ class Request(BaseRequest):
+
+ @cached_property
+ def client_session(self):
+ return SecureCookie.load_cookie(self, secret_key=COOKIE_SECRET)
+
+ def application(environ, start_response):
+ request = Request(environ)
+
+ # get a response object here
+ response = ...
+
+ request.client_session.save_cookie(response)
+ return response(environ, start_response)
+
+ :copyright: 2007 Pallets
+ :license: BSD-3-Clause
+"""
+import base64
+import pickle
+import warnings
+from hashlib import sha1 as _default_hash
+from hmac import new as hmac
+from time import time
+
+from .._compat import iteritems
+from .._compat import text_type
+from .._compat import to_bytes
+from .._compat import to_native
+from .._internal import _date_to_unix
+from ..contrib.sessions import ModificationTrackingDict
+from ..security import safe_str_cmp
+from ..urls import url_quote_plus
+from ..urls import url_unquote_plus
+
+warnings.warn(
+ "'werkzeug.contrib.securecookie' is deprecated as of version 0.15"
+ " and will be removed in version 1.0. It has moved to"
+ " https://github.com/pallets/secure-cookie.",
+ DeprecationWarning,
+ stacklevel=2,
+)
+
+
+class UnquoteError(Exception):
+ """Internal exception used to signal failures on quoting."""
+
+
+class SecureCookie(ModificationTrackingDict):
+ """Represents a secure cookie. You can subclass this class and provide
+ an alternative mac method. The import thing is that the mac method
+ is a function with a similar interface to the hashlib. Required
+ methods are update() and digest().
+
+ Example usage:
+
+ >>> x = SecureCookie({"foo": 42, "baz": (1, 2, 3)}, "deadbeef")
+ >>> x["foo"]
+ 42
+ >>> x["baz"]
+ (1, 2, 3)
+ >>> x["blafasel"] = 23
+ >>> x.should_save
+ True
+
+ :param data: the initial data. Either a dict, list of tuples or `None`.
+ :param secret_key: the secret key. If not set `None` or not specified
+ it has to be set before :meth:`serialize` is called.
+ :param new: The initial value of the `new` flag.
+ """
+
+ #: The hash method to use. This has to be a module with a new function
+ #: or a function that creates a hashlib object. Such as `hashlib.md5`
+ #: Subclasses can override this attribute. The default hash is sha1.
+ #: Make sure to wrap this in staticmethod() if you store an arbitrary
+ #: function there such as hashlib.sha1 which might be implemented
+ #: as a function.
+ hash_method = staticmethod(_default_hash)
+
+ #: The module used for serialization. Should have a ``dumps`` and a
+ #: ``loads`` method that takes bytes. The default is :mod:`pickle`.
+ #:
+ #: .. versionchanged:: 0.15
+ #: The default of ``pickle`` will change to :mod:`json` in 1.0.
+ serialization_method = pickle
+
+ #: if the contents should be base64 quoted. This can be disabled if the
+ #: serialization process returns cookie safe strings only.
+ quote_base64 = True
+
+ def __init__(self, data=None, secret_key=None, new=True):
+ ModificationTrackingDict.__init__(self, data or ())
+ # explicitly convert it into a bytestring because python 2.6
+ # no longer performs an implicit string conversion on hmac
+ if secret_key is not None:
+ secret_key = to_bytes(secret_key, "utf-8")
+ self.secret_key = secret_key
+ self.new = new
+
+ if self.serialization_method is pickle:
+ warnings.warn(
+ "The default 'SecureCookie.serialization_method' will"
+ " change from pickle to json in version 1.0. To upgrade"
+ " existing tokens, override 'unquote' to try pickle if"
+ " json fails.",
+ stacklevel=2,
+ )
+
+ def __repr__(self):
+ return "<%s %s%s>" % (
+ self.__class__.__name__,
+ dict.__repr__(self),
+ "*" if self.should_save else "",
+ )
+
+ @property
+ def should_save(self):
+ """True if the session should be saved. By default this is only true
+ for :attr:`modified` cookies, not :attr:`new`.
+ """
+ return self.modified
+
+ @classmethod
+ def quote(cls, value):
+ """Quote the value for the cookie. This can be any object supported
+ by :attr:`serialization_method`.
+
+ :param value: the value to quote.
+ """
+ if cls.serialization_method is not None:
+ value = cls.serialization_method.dumps(value)
+ if cls.quote_base64:
+ value = b"".join(
+ base64.b64encode(to_bytes(value, "utf8")).splitlines()
+ ).strip()
+ return value
+
+ @classmethod
+ def unquote(cls, value):
+ """Unquote the value for the cookie. If unquoting does not work a
+ :exc:`UnquoteError` is raised.
+
+ :param value: the value to unquote.
+ """
+ try:
+ if cls.quote_base64:
+ value = base64.b64decode(value)
+ if cls.serialization_method is not None:
+ value = cls.serialization_method.loads(value)
+ return value
+ except Exception:
+ # unfortunately pickle and other serialization modules can
+ # cause pretty every error here. if we get one we catch it
+ # and convert it into an UnquoteError
+ raise UnquoteError()
+
+ def serialize(self, expires=None):
+ """Serialize the secure cookie into a string.
+
+ If expires is provided, the session will be automatically invalidated
+ after expiration when you unseralize it. This provides better
+ protection against session cookie theft.
+
+ :param expires: an optional expiration date for the cookie (a
+ :class:`datetime.datetime` object)
+ """
+ if self.secret_key is None:
+ raise RuntimeError("no secret key defined")
+ if expires:
+ self["_expires"] = _date_to_unix(expires)
+ result = []
+ mac = hmac(self.secret_key, None, self.hash_method)
+ for key, value in sorted(self.items()):
+ result.append(
+ (
+ "%s=%s" % (url_quote_plus(key), self.quote(value).decode("ascii"))
+ ).encode("ascii")
+ )
+ mac.update(b"|" + result[-1])
+ return b"?".join([base64.b64encode(mac.digest()).strip(), b"&".join(result)])
+
+ @classmethod
+ def unserialize(cls, string, secret_key):
+ """Load the secure cookie from a serialized string.
+
+ :param string: the cookie value to unserialize.
+ :param secret_key: the secret key used to serialize the cookie.
+ :return: a new :class:`SecureCookie`.
+ """
+ if isinstance(string, text_type):
+ string = string.encode("utf-8", "replace")
+ if isinstance(secret_key, text_type):
+ secret_key = secret_key.encode("utf-8", "replace")
+ try:
+ base64_hash, data = string.split(b"?", 1)
+ except (ValueError, IndexError):
+ items = ()
+ else:
+ items = {}
+ mac = hmac(secret_key, None, cls.hash_method)
+ for item in data.split(b"&"):
+ mac.update(b"|" + item)
+ if b"=" not in item:
+ items = None
+ break
+ key, value = item.split(b"=", 1)
+ # try to make the key a string
+ key = url_unquote_plus(key.decode("ascii"))
+ try:
+ key = to_native(key)
+ except UnicodeError:
+ pass
+ items[key] = value
+
+ # no parsing error and the mac looks okay, we can now
+ # sercurely unpickle our cookie.
+ try:
+ client_hash = base64.b64decode(base64_hash)
+ except TypeError:
+ items = client_hash = None
+ if items is not None and safe_str_cmp(client_hash, mac.digest()):
+ try:
+ for key, value in iteritems(items):
+ items[key] = cls.unquote(value)
+ except UnquoteError:
+ items = ()
+ else:
+ if "_expires" in items:
+ if time() > items["_expires"]:
+ items = ()
+ else:
+ del items["_expires"]
+ else:
+ items = ()
+ return cls(items, secret_key, False)
+
+ @classmethod
+ def load_cookie(cls, request, key="session", secret_key=None):
+ """Loads a :class:`SecureCookie` from a cookie in request. If the
+ cookie is not set, a new :class:`SecureCookie` instanced is
+ returned.
+
+ :param request: a request object that has a `cookies` attribute
+ which is a dict of all cookie values.
+ :param key: the name of the cookie.
+ :param secret_key: the secret key used to unquote the cookie.
+ Always provide the value even though it has
+ no default!
+ """
+ data = request.cookies.get(key)
+ if not data:
+ return cls(secret_key=secret_key)
+ return cls.unserialize(data, secret_key)
+
+ def save_cookie(
+ self,
+ response,
+ key="session",
+ expires=None,
+ session_expires=None,
+ max_age=None,
+ path="/",
+ domain=None,
+ secure=None,
+ httponly=False,
+ force=False,
+ ):
+ """Saves the SecureCookie in a cookie on response object. All
+ parameters that are not described here are forwarded directly
+ to :meth:`~BaseResponse.set_cookie`.
+
+ :param response: a response object that has a
+ :meth:`~BaseResponse.set_cookie` method.
+ :param key: the name of the cookie.
+ :param session_expires: the expiration date of the secure cookie
+ stored information. If this is not provided
+ the cookie `expires` date is used instead.
+ """
+ if force or self.should_save:
+ data = self.serialize(session_expires or expires)
+ response.set_cookie(
+ key,
+ data,
+ expires=expires,
+ max_age=max_age,
+ path=path,
+ domain=domain,
+ secure=secure,
+ httponly=httponly,
+ )
diff --git a/python/werkzeug/contrib/sessions.py b/python/werkzeug/contrib/sessions.py
new file mode 100644
index 0000000..866e827
--- /dev/null
+++ b/python/werkzeug/contrib/sessions.py
@@ -0,0 +1,389 @@
+# -*- coding: utf-8 -*-
+r"""
+ werkzeug.contrib.sessions
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module contains some helper classes that help one to add session
+ support to a python WSGI application. For full client-side session
+ storage see :mod:`~werkzeug.contrib.securecookie` which implements a
+ secure, client-side session storage.
+
+
+ Application Integration
+ =======================
+
+ ::
+
+ from werkzeug.contrib.sessions import SessionMiddleware, \
+ FilesystemSessionStore
+
+ app = SessionMiddleware(app, FilesystemSessionStore())
+
+ The current session will then appear in the WSGI environment as
+ `werkzeug.session`. However it's recommended to not use the middleware
+ but the stores directly in the application. However for very simple
+ scripts a middleware for sessions could be sufficient.
+
+ This module does not implement methods or ways to check if a session is
+ expired. That should be done by a cronjob and storage specific. For
+ example to prune unused filesystem sessions one could check the modified
+ time of the files. If sessions are stored in the database the new()
+ method should add an expiration timestamp for the session.
+
+ For better flexibility it's recommended to not use the middleware but the
+ store and session object directly in the application dispatching::
+
+ session_store = FilesystemSessionStore()
+
+ def application(environ, start_response):
+ request = Request(environ)
+ sid = request.cookies.get('cookie_name')
+ if sid is None:
+ request.session = session_store.new()
+ else:
+ request.session = session_store.get(sid)
+ response = get_the_response_object(request)
+ if request.session.should_save:
+ session_store.save(request.session)
+ response.set_cookie('cookie_name', request.session.sid)
+ return response(environ, start_response)
+
+ :copyright: 2007 Pallets
+ :license: BSD-3-Clause
+"""
+import os
+import re
+import tempfile
+import warnings
+from hashlib import sha1
+from os import path
+from pickle import dump
+from pickle import HIGHEST_PROTOCOL
+from pickle import load
+from random import random
+from time import time
+
+from .._compat import PY2
+from .._compat import text_type
+from ..datastructures import CallbackDict
+from ..filesystem import get_filesystem_encoding
+from ..posixemulation import rename
+from ..utils import dump_cookie
+from ..utils import parse_cookie
+from ..wsgi import ClosingIterator
+
+warnings.warn(
+ "'werkzeug.contrib.sessions' is deprecated as of version 0.15 and"
+ " will be removed in version 1.0. It has moved to"
+ " https://github.com/pallets/secure-cookie.",
+ DeprecationWarning,
+ stacklevel=2,
+)
+
+_sha1_re = re.compile(r"^[a-f0-9]{40}$")
+
+
+def _urandom():
+ if hasattr(os, "urandom"):
+ return os.urandom(30)
+ return text_type(random()).encode("ascii")
+
+
+def generate_key(salt=None):
+ if salt is None:
+ salt = repr(salt).encode("ascii")
+ return sha1(b"".join([salt, str(time()).encode("ascii"), _urandom()])).hexdigest()
+
+
+class ModificationTrackingDict(CallbackDict):
+ __slots__ = ("modified",)
+
+ def __init__(self, *args, **kwargs):
+ def on_update(self):
+ self.modified = True
+
+ self.modified = False
+ CallbackDict.__init__(self, on_update=on_update)
+ dict.update(self, *args, **kwargs)
+
+ def copy(self):
+ """Create a flat copy of the dict."""
+ missing = object()
+ result = object.__new__(self.__class__)
+ for name in self.__slots__:
+ val = getattr(self, name, missing)
+ if val is not missing:
+ setattr(result, name, val)
+ return result
+
+ def __copy__(self):
+ return self.copy()
+
+
+class Session(ModificationTrackingDict):
+ """Subclass of a dict that keeps track of direct object changes. Changes
+ in mutable structures are not tracked, for those you have to set
+ `modified` to `True` by hand.
+ """
+
+ __slots__ = ModificationTrackingDict.__slots__ + ("sid", "new")
+
+ def __init__(self, data, sid, new=False):
+ ModificationTrackingDict.__init__(self, data)
+ self.sid = sid
+ self.new = new
+
+ def __repr__(self):
+ return "<%s %s%s>" % (
+ self.__class__.__name__,
+ dict.__repr__(self),
+ "*" if self.should_save else "",
+ )
+
+ @property
+ def should_save(self):
+ """True if the session should be saved.
+
+ .. versionchanged:: 0.6
+ By default the session is now only saved if the session is
+ modified, not if it is new like it was before.
+ """
+ return self.modified
+
+
+class SessionStore(object):
+ """Baseclass for all session stores. The Werkzeug contrib module does not
+ implement any useful stores besides the filesystem store, application
+ developers are encouraged to create their own stores.
+
+ :param session_class: The session class to use. Defaults to
+ :class:`Session`.
+ """
+
+ def __init__(self, session_class=None):
+ if session_class is None:
+ session_class = Session
+ self.session_class = session_class
+
+ def is_valid_key(self, key):
+ """Check if a key has the correct format."""
+ return _sha1_re.match(key) is not None
+
+ def generate_key(self, salt=None):
+ """Simple function that generates a new session key."""
+ return generate_key(salt)
+
+ def new(self):
+ """Generate a new session."""
+ return self.session_class({}, self.generate_key(), True)
+
+ def save(self, session):
+ """Save a session."""
+
+ def save_if_modified(self, session):
+ """Save if a session class wants an update."""
+ if session.should_save:
+ self.save(session)
+
+ def delete(self, session):
+ """Delete a session."""
+
+ def get(self, sid):
+ """Get a session for this sid or a new session object. This method
+ has to check if the session key is valid and create a new session if
+ that wasn't the case.
+ """
+ return self.session_class({}, sid, True)
+
+
+#: used for temporary files by the filesystem session store
+_fs_transaction_suffix = ".__wz_sess"
+
+
+class FilesystemSessionStore(SessionStore):
+ """Simple example session store that saves sessions on the filesystem.
+ This store works best on POSIX systems and Windows Vista / Windows
+ Server 2008 and newer.
+
+ .. versionchanged:: 0.6
+ `renew_missing` was added. Previously this was considered `True`,
+ now the default changed to `False` and it can be explicitly
+ deactivated.
+
+ :param path: the path to the folder used for storing the sessions.
+ If not provided the default temporary directory is used.
+ :param filename_template: a string template used to give the session
+ a filename. ``%s`` is replaced with the
+ session id.
+ :param session_class: The session class to use. Defaults to
+ :class:`Session`.
+ :param renew_missing: set to `True` if you want the store to
+ give the user a new sid if the session was
+ not yet saved.
+ """
+
+ def __init__(
+ self,
+ path=None,
+ filename_template="werkzeug_%s.sess",
+ session_class=None,
+ renew_missing=False,
+ mode=0o644,
+ ):
+ SessionStore.__init__(self, session_class)
+ if path is None:
+ path = tempfile.gettempdir()
+ self.path = path
+ if isinstance(filename_template, text_type) and PY2:
+ filename_template = filename_template.encode(get_filesystem_encoding())
+ assert not filename_template.endswith(_fs_transaction_suffix), (
+ "filename templates may not end with %s" % _fs_transaction_suffix
+ )
+ self.filename_template = filename_template
+ self.renew_missing = renew_missing
+ self.mode = mode
+
+ def get_session_filename(self, sid):
+ # out of the box, this should be a strict ASCII subset but
+ # you might reconfigure the session object to have a more
+ # arbitrary string.
+ if isinstance(sid, text_type) and PY2:
+ sid = sid.encode(get_filesystem_encoding())
+ return path.join(self.path, self.filename_template % sid)
+
+ def save(self, session):
+ fn = self.get_session_filename(session.sid)
+ fd, tmp = tempfile.mkstemp(suffix=_fs_transaction_suffix, dir=self.path)
+ f = os.fdopen(fd, "wb")
+ try:
+ dump(dict(session), f, HIGHEST_PROTOCOL)
+ finally:
+ f.close()
+ try:
+ rename(tmp, fn)
+ os.chmod(fn, self.mode)
+ except (IOError, OSError):
+ pass
+
+ def delete(self, session):
+ fn = self.get_session_filename(session.sid)
+ try:
+ os.unlink(fn)
+ except OSError:
+ pass
+
+ def get(self, sid):
+ if not self.is_valid_key(sid):
+ return self.new()
+ try:
+ f = open(self.get_session_filename(sid), "rb")
+ except IOError:
+ if self.renew_missing:
+ return self.new()
+ data = {}
+ else:
+ try:
+ try:
+ data = load(f)
+ except Exception:
+ data = {}
+ finally:
+ f.close()
+ return self.session_class(data, sid, False)
+
+ def list(self):
+ """Lists all sessions in the store.
+
+ .. versionadded:: 0.6
+ """
+ before, after = self.filename_template.split("%s", 1)
+ filename_re = re.compile(
+ r"%s(.{5,})%s$" % (re.escape(before), re.escape(after))
+ )
+ result = []
+ for filename in os.listdir(self.path):
+ #: this is a session that is still being saved.
+ if filename.endswith(_fs_transaction_suffix):
+ continue
+ match = filename_re.match(filename)
+ if match is not None:
+ result.append(match.group(1))
+ return result
+
+
+class SessionMiddleware(object):
+ """A simple middleware that puts the session object of a store provided
+ into the WSGI environ. It automatically sets cookies and restores
+ sessions.
+
+ However a middleware is not the preferred solution because it won't be as
+ fast as sessions managed by the application itself and will put a key into
+ the WSGI environment only relevant for the application which is against
+ the concept of WSGI.
+
+ The cookie parameters are the same as for the :func:`~dump_cookie`
+ function just prefixed with ``cookie_``. Additionally `max_age` is
+ called `cookie_age` and not `cookie_max_age` because of backwards
+ compatibility.
+ """
+
+ def __init__(
+ self,
+ app,
+ store,
+ cookie_name="session_id",
+ cookie_age=None,
+ cookie_expires=None,
+ cookie_path="/",
+ cookie_domain=None,
+ cookie_secure=None,
+ cookie_httponly=False,
+ cookie_samesite="Lax",
+ environ_key="werkzeug.session",
+ ):
+ self.app = app
+ self.store = store
+ self.cookie_name = cookie_name
+ self.cookie_age = cookie_age
+ self.cookie_expires = cookie_expires
+ self.cookie_path = cookie_path
+ self.cookie_domain = cookie_domain
+ self.cookie_secure = cookie_secure
+ self.cookie_httponly = cookie_httponly
+ self.cookie_samesite = cookie_samesite
+ self.environ_key = environ_key
+
+ def __call__(self, environ, start_response):
+ cookie = parse_cookie(environ.get("HTTP_COOKIE", ""))
+ sid = cookie.get(self.cookie_name, None)
+ if sid is None:
+ session = self.store.new()
+ else:
+ session = self.store.get(sid)
+ environ[self.environ_key] = session
+
+ def injecting_start_response(status, headers, exc_info=None):
+ if session.should_save:
+ self.store.save(session)
+ headers.append(
+ (
+ "Set-Cookie",
+ dump_cookie(
+ self.cookie_name,
+ session.sid,
+ self.cookie_age,
+ self.cookie_expires,
+ self.cookie_path,
+ self.cookie_domain,
+ self.cookie_secure,
+ self.cookie_httponly,
+ samesite=self.cookie_samesite,
+ ),
+ )
+ )
+ return start_response(status, headers, exc_info)
+
+ return ClosingIterator(
+ self.app(environ, injecting_start_response),
+ lambda: self.store.save_if_modified(session),
+ )
diff --git a/python/werkzeug/contrib/wrappers.py b/python/werkzeug/contrib/wrappers.py
new file mode 100644
index 0000000..49b82a7
--- /dev/null
+++ b/python/werkzeug/contrib/wrappers.py
@@ -0,0 +1,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