web/lib/django/contrib/messages/storage/cookie.py
changeset 29 cc9b7e14412b
equal deleted inserted replaced
28:b758351d191f 29:cc9b7e14412b
       
     1 import hmac
       
     2 
       
     3 from django.conf import settings
       
     4 from django.contrib.messages import constants
       
     5 from django.contrib.messages.storage.base import BaseStorage, Message
       
     6 from django.http import CompatCookie
       
     7 from django.utils import simplejson as json
       
     8 from django.utils.hashcompat import sha_hmac
       
     9 
       
    10 
       
    11 class MessageEncoder(json.JSONEncoder):
       
    12     """
       
    13     Compactly serializes instances of the ``Message`` class as JSON.
       
    14     """
       
    15     message_key = '__json_message'
       
    16 
       
    17     def default(self, obj):
       
    18         if isinstance(obj, Message):
       
    19             message = [self.message_key, obj.level, obj.message]
       
    20             if obj.extra_tags:
       
    21                 message.append(obj.extra_tags)
       
    22             return message
       
    23         return super(MessageEncoder, self).default(obj)
       
    24 
       
    25 
       
    26 class MessageDecoder(json.JSONDecoder):
       
    27     """
       
    28     Decodes JSON that includes serialized ``Message`` instances.
       
    29     """
       
    30 
       
    31     def process_messages(self, obj):
       
    32         if isinstance(obj, list) and obj:
       
    33             if obj[0] == MessageEncoder.message_key:
       
    34                 return Message(*obj[1:])
       
    35             return [self.process_messages(item) for item in obj]
       
    36         if isinstance(obj, dict):
       
    37             return dict([(key, self.process_messages(value))
       
    38                          for key, value in obj.iteritems()])
       
    39         return obj
       
    40 
       
    41     def decode(self, s, **kwargs):
       
    42         decoded = super(MessageDecoder, self).decode(s, **kwargs)
       
    43         return self.process_messages(decoded)
       
    44 
       
    45 class CookieStorage(BaseStorage):
       
    46     """
       
    47     Stores messages in a cookie.
       
    48     """
       
    49     cookie_name = 'messages'
       
    50     # We should be able to store 4K in a cookie, but Internet Explorer
       
    51     # imposes 4K as the *total* limit for a domain.  To allow other
       
    52     # cookies, we go for 3/4 of 4K.
       
    53     max_cookie_size = 3072
       
    54     not_finished = '__messagesnotfinished__'
       
    55 
       
    56     def _get(self, *args, **kwargs):
       
    57         """
       
    58         Retrieves a list of messages from the messages cookie.  If the
       
    59         not_finished sentinel value is found at the end of the message list,
       
    60         remove it and return a result indicating that not all messages were
       
    61         retrieved by this storage.
       
    62         """
       
    63         data = self.request.COOKIES.get(self.cookie_name)
       
    64         messages = self._decode(data)
       
    65         all_retrieved = not (messages and messages[-1] == self.not_finished)
       
    66         if messages and not all_retrieved:
       
    67             # remove the sentinel value
       
    68             messages.pop()
       
    69         return messages, all_retrieved
       
    70 
       
    71     def _update_cookie(self, encoded_data, response):
       
    72         """
       
    73         Either sets the cookie with the encoded data if there is any data to
       
    74         store, or deletes the cookie.
       
    75         """
       
    76         if encoded_data:
       
    77             response.set_cookie(self.cookie_name, encoded_data)
       
    78         else:
       
    79             response.delete_cookie(self.cookie_name)
       
    80 
       
    81     def _store(self, messages, response, remove_oldest=True, *args, **kwargs):
       
    82         """
       
    83         Stores the messages to a cookie, returning a list of any messages which
       
    84         could not be stored.
       
    85 
       
    86         If the encoded data is larger than ``max_cookie_size``, removes
       
    87         messages until the data fits (these are the messages which are
       
    88         returned), and add the not_finished sentinel value to indicate as much.
       
    89         """
       
    90         unstored_messages = []
       
    91         encoded_data = self._encode(messages)
       
    92         if self.max_cookie_size:
       
    93             # data is going to be stored eventually by CompatCookie, which
       
    94             # adds it's own overhead, which we must account for.
       
    95             cookie = CompatCookie() # create outside the loop
       
    96             def stored_length(val):
       
    97                 return len(cookie.value_encode(val)[1])
       
    98 
       
    99             while encoded_data and stored_length(encoded_data) > self.max_cookie_size:
       
   100                 if remove_oldest:
       
   101                     unstored_messages.append(messages.pop(0))
       
   102                 else:
       
   103                     unstored_messages.insert(0, messages.pop())
       
   104                 encoded_data = self._encode(messages + [self.not_finished],
       
   105                                             encode_empty=unstored_messages)
       
   106         self._update_cookie(encoded_data, response)
       
   107         return unstored_messages
       
   108 
       
   109     def _hash(self, value):
       
   110         """
       
   111         Creates an HMAC/SHA1 hash based on the value and the project setting's
       
   112         SECRET_KEY, modified to make it unique for the present purpose.
       
   113         """
       
   114         key = 'django.contrib.messages' + settings.SECRET_KEY
       
   115         return hmac.new(key, value, sha_hmac).hexdigest()
       
   116 
       
   117     def _encode(self, messages, encode_empty=False):
       
   118         """
       
   119         Returns an encoded version of the messages list which can be stored as
       
   120         plain text.
       
   121 
       
   122         Since the data will be retrieved from the client-side, the encoded data
       
   123         also contains a hash to ensure that the data was not tampered with.
       
   124         """
       
   125         if messages or encode_empty:
       
   126             encoder = MessageEncoder(separators=(',', ':'))
       
   127             value = encoder.encode(messages)
       
   128             return '%s$%s' % (self._hash(value), value)
       
   129 
       
   130     def _decode(self, data):
       
   131         """
       
   132         Safely decodes a encoded text stream back into a list of messages.
       
   133 
       
   134         If the encoded text stream contained an invalid hash or was in an
       
   135         invalid format, ``None`` is returned.
       
   136         """
       
   137         if not data:
       
   138             return None
       
   139         bits = data.split('$', 1)
       
   140         if len(bits) == 2:
       
   141             hash, value = bits
       
   142             if hash == self._hash(value):
       
   143                 try:
       
   144                     # If we get here (and the JSON decode works), everything is
       
   145                     # good. In any other case, drop back and return None.
       
   146                     return json.loads(value, cls=MessageDecoder)
       
   147                 except ValueError:
       
   148                     pass
       
   149         # Mark the data as used (so it gets removed) since something was wrong
       
   150         # with the data.
       
   151         self.used = True
       
   152         return None