web/lib/django/contrib/messages/storage/cookie.py
changeset 29 cc9b7e14412b
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/lib/django/contrib/messages/storage/cookie.py	Tue May 25 02:43:45 2010 +0200
@@ -0,0 +1,152 @@
+import hmac
+
+from django.conf import settings
+from django.contrib.messages import constants
+from django.contrib.messages.storage.base import BaseStorage, Message
+from django.http import CompatCookie
+from django.utils import simplejson as json
+from django.utils.hashcompat import sha_hmac
+
+
+class MessageEncoder(json.JSONEncoder):
+    """
+    Compactly serializes instances of the ``Message`` class as JSON.
+    """
+    message_key = '__json_message'
+
+    def default(self, obj):
+        if isinstance(obj, Message):
+            message = [self.message_key, obj.level, obj.message]
+            if obj.extra_tags:
+                message.append(obj.extra_tags)
+            return message
+        return super(MessageEncoder, self).default(obj)
+
+
+class MessageDecoder(json.JSONDecoder):
+    """
+    Decodes JSON that includes serialized ``Message`` instances.
+    """
+
+    def process_messages(self, obj):
+        if isinstance(obj, list) and obj:
+            if obj[0] == MessageEncoder.message_key:
+                return Message(*obj[1:])
+            return [self.process_messages(item) for item in obj]
+        if isinstance(obj, dict):
+            return dict([(key, self.process_messages(value))
+                         for key, value in obj.iteritems()])
+        return obj
+
+    def decode(self, s, **kwargs):
+        decoded = super(MessageDecoder, self).decode(s, **kwargs)
+        return self.process_messages(decoded)
+
+class CookieStorage(BaseStorage):
+    """
+    Stores messages in a cookie.
+    """
+    cookie_name = 'messages'
+    # We should be able to store 4K in a cookie, but Internet Explorer
+    # imposes 4K as the *total* limit for a domain.  To allow other
+    # cookies, we go for 3/4 of 4K.
+    max_cookie_size = 3072
+    not_finished = '__messagesnotfinished__'
+
+    def _get(self, *args, **kwargs):
+        """
+        Retrieves a list of messages from the messages cookie.  If the
+        not_finished sentinel value is found at the end of the message list,
+        remove it and return a result indicating that not all messages were
+        retrieved by this storage.
+        """
+        data = self.request.COOKIES.get(self.cookie_name)
+        messages = self._decode(data)
+        all_retrieved = not (messages and messages[-1] == self.not_finished)
+        if messages and not all_retrieved:
+            # remove the sentinel value
+            messages.pop()
+        return messages, all_retrieved
+
+    def _update_cookie(self, encoded_data, response):
+        """
+        Either sets the cookie with the encoded data if there is any data to
+        store, or deletes the cookie.
+        """
+        if encoded_data:
+            response.set_cookie(self.cookie_name, encoded_data)
+        else:
+            response.delete_cookie(self.cookie_name)
+
+    def _store(self, messages, response, remove_oldest=True, *args, **kwargs):
+        """
+        Stores the messages to a cookie, returning a list of any messages which
+        could not be stored.
+
+        If the encoded data is larger than ``max_cookie_size``, removes
+        messages until the data fits (these are the messages which are
+        returned), and add the not_finished sentinel value to indicate as much.
+        """
+        unstored_messages = []
+        encoded_data = self._encode(messages)
+        if self.max_cookie_size:
+            # data is going to be stored eventually by CompatCookie, which
+            # adds it's own overhead, which we must account for.
+            cookie = CompatCookie() # create outside the loop
+            def stored_length(val):
+                return len(cookie.value_encode(val)[1])
+
+            while encoded_data and stored_length(encoded_data) > self.max_cookie_size:
+                if remove_oldest:
+                    unstored_messages.append(messages.pop(0))
+                else:
+                    unstored_messages.insert(0, messages.pop())
+                encoded_data = self._encode(messages + [self.not_finished],
+                                            encode_empty=unstored_messages)
+        self._update_cookie(encoded_data, response)
+        return unstored_messages
+
+    def _hash(self, value):
+        """
+        Creates an HMAC/SHA1 hash based on the value and the project setting's
+        SECRET_KEY, modified to make it unique for the present purpose.
+        """
+        key = 'django.contrib.messages' + settings.SECRET_KEY
+        return hmac.new(key, value, sha_hmac).hexdigest()
+
+    def _encode(self, messages, encode_empty=False):
+        """
+        Returns an encoded version of the messages list which can be stored as
+        plain text.
+
+        Since the data will be retrieved from the client-side, the encoded data
+        also contains a hash to ensure that the data was not tampered with.
+        """
+        if messages or encode_empty:
+            encoder = MessageEncoder(separators=(',', ':'))
+            value = encoder.encode(messages)
+            return '%s$%s' % (self._hash(value), value)
+
+    def _decode(self, data):
+        """
+        Safely decodes a encoded text stream back into a list of messages.
+
+        If the encoded text stream contained an invalid hash or was in an
+        invalid format, ``None`` is returned.
+        """
+        if not data:
+            return None
+        bits = data.split('$', 1)
+        if len(bits) == 2:
+            hash, value = bits
+            if hash == self._hash(value):
+                try:
+                    # If we get here (and the JSON decode works), everything is
+                    # good. In any other case, drop back and return None.
+                    return json.loads(value, cls=MessageDecoder)
+                except ValueError:
+                    pass
+        # Mark the data as used (so it gets removed) since something was wrong
+        # with the data.
+        self.used = True
+        return None