Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

cookie.py 6.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import json
  2. from django.conf import settings
  3. from django.contrib.messages.storage.base import BaseStorage, Message
  4. from django.http import SimpleCookie
  5. from django.utils.crypto import constant_time_compare, salted_hmac
  6. from django.utils.safestring import SafeData, mark_safe
  7. class MessageEncoder(json.JSONEncoder):
  8. """
  9. Compactly serialize instances of the ``Message`` class as JSON.
  10. """
  11. message_key = '__json_message'
  12. def default(self, obj):
  13. if isinstance(obj, Message):
  14. # Using 0/1 here instead of False/True to produce more compact json
  15. is_safedata = 1 if isinstance(obj.message, SafeData) else 0
  16. message = [self.message_key, is_safedata, obj.level, obj.message]
  17. if obj.extra_tags:
  18. message.append(obj.extra_tags)
  19. return message
  20. return super().default(obj)
  21. class MessageDecoder(json.JSONDecoder):
  22. """
  23. Decode JSON that includes serialized ``Message`` instances.
  24. """
  25. def process_messages(self, obj):
  26. if isinstance(obj, list) and obj:
  27. if obj[0] == MessageEncoder.message_key:
  28. if len(obj) == 3:
  29. # Compatibility with previously-encoded messages
  30. return Message(*obj[1:])
  31. if obj[1]:
  32. obj[3] = mark_safe(obj[3])
  33. return Message(*obj[2:])
  34. return [self.process_messages(item) for item in obj]
  35. if isinstance(obj, dict):
  36. return {key: self.process_messages(value)
  37. for key, value in obj.items()}
  38. return obj
  39. def decode(self, s, **kwargs):
  40. decoded = super().decode(s, **kwargs)
  41. return self.process_messages(decoded)
  42. class CookieStorage(BaseStorage):
  43. """
  44. Store messages in a cookie.
  45. """
  46. cookie_name = 'messages'
  47. # uwsgi's default configuration enforces a maximum size of 4kb for all the
  48. # HTTP headers. In order to leave some room for other cookies and headers,
  49. # restrict the session cookie to 1/2 of 4kb. See #18781.
  50. max_cookie_size = 2048
  51. not_finished = '__messagesnotfinished__'
  52. def _get(self, *args, **kwargs):
  53. """
  54. Retrieve a list of messages from the messages cookie. If the
  55. not_finished sentinel value is found at the end of the message list,
  56. remove it and return a result indicating that not all messages were
  57. retrieved by this storage.
  58. """
  59. data = self.request.COOKIES.get(self.cookie_name)
  60. messages = self._decode(data)
  61. all_retrieved = not (messages and messages[-1] == self.not_finished)
  62. if messages and not all_retrieved:
  63. # remove the sentinel value
  64. messages.pop()
  65. return messages, all_retrieved
  66. def _update_cookie(self, encoded_data, response):
  67. """
  68. Either set the cookie with the encoded data if there is any data to
  69. store, or delete the cookie.
  70. """
  71. if encoded_data:
  72. response.set_cookie(
  73. self.cookie_name, encoded_data,
  74. domain=settings.SESSION_COOKIE_DOMAIN,
  75. secure=settings.SESSION_COOKIE_SECURE or None,
  76. httponly=settings.SESSION_COOKIE_HTTPONLY or None,
  77. samesite=settings.SESSION_COOKIE_SAMESITE,
  78. )
  79. else:
  80. response.delete_cookie(self.cookie_name, domain=settings.SESSION_COOKIE_DOMAIN)
  81. def _store(self, messages, response, remove_oldest=True, *args, **kwargs):
  82. """
  83. Store the messages to a cookie and return a list of any messages which
  84. could not be stored.
  85. If the encoded data is larger than ``max_cookie_size``, remove
  86. messages until the data fits (these are the messages which are
  87. returned), and add the not_finished sentinel value to indicate as much.
  88. """
  89. unstored_messages = []
  90. encoded_data = self._encode(messages)
  91. if self.max_cookie_size:
  92. # data is going to be stored eventually by SimpleCookie, which
  93. # adds its own overhead, which we must account for.
  94. cookie = SimpleCookie() # create outside the loop
  95. def stored_length(val):
  96. return len(cookie.value_encode(val)[1])
  97. while encoded_data and stored_length(encoded_data) > self.max_cookie_size:
  98. if remove_oldest:
  99. unstored_messages.append(messages.pop(0))
  100. else:
  101. unstored_messages.insert(0, messages.pop())
  102. encoded_data = self._encode(messages + [self.not_finished],
  103. encode_empty=unstored_messages)
  104. self._update_cookie(encoded_data, response)
  105. return unstored_messages
  106. def _hash(self, value):
  107. """
  108. Create an HMAC/SHA1 hash based on the value and the project setting's
  109. SECRET_KEY, modified to make it unique for the present purpose.
  110. """
  111. key_salt = 'django.contrib.messages'
  112. return salted_hmac(key_salt, value).hexdigest()
  113. def _encode(self, messages, encode_empty=False):
  114. """
  115. Return an encoded version of the messages list which can be stored as
  116. plain text.
  117. Since the data will be retrieved from the client-side, the encoded data
  118. also contains a hash to ensure that the data was not tampered with.
  119. """
  120. if messages or encode_empty:
  121. encoder = MessageEncoder(separators=(',', ':'))
  122. value = encoder.encode(messages)
  123. return '%s$%s' % (self._hash(value), value)
  124. def _decode(self, data):
  125. """
  126. Safely decode an encoded text stream back into a list of messages.
  127. If the encoded text stream contained an invalid hash or was in an
  128. invalid format, return None.
  129. """
  130. if not data:
  131. return None
  132. bits = data.split('$', 1)
  133. if len(bits) == 2:
  134. hash, value = bits
  135. if constant_time_compare(hash, self._hash(value)):
  136. try:
  137. # If we get here (and the JSON decode works), everything is
  138. # good. In any other case, drop back and return None.
  139. return json.loads(value, cls=MessageDecoder)
  140. except json.JSONDecodeError:
  141. pass
  142. # Mark the data as used (so it gets removed) since something was wrong
  143. # with the data.
  144. self.used = True
  145. return None