From 9d0c878abf9249da6e16f1acfec311498dc9f368 Mon Sep 17 00:00:00 2001 From: David Wobrock Date: Mon, 27 Feb 2023 09:07:02 +0100 Subject: [PATCH] Refs #28948 -- Precomputed once serialized cookie messages. When the cookie size is too long, the same messages were serialized over and over again. --- django/contrib/messages/storage/cookie.py | 55 +++++++++++++++++------ tests/messages_tests/test_cookie.py | 4 +- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/django/contrib/messages/storage/cookie.py b/django/contrib/messages/storage/cookie.py index 0fd7ab60b6..b493b207c8 100644 --- a/django/contrib/messages/storage/cookie.py +++ b/django/contrib/messages/storage/cookie.py @@ -47,14 +47,28 @@ class MessageDecoder(json.JSONDecoder): return self.process_messages(decoded) -class MessageSerializer: +class MessagePartSerializer: def dumps(self, obj): - return json.dumps( - obj, - separators=(",", ":"), - cls=MessageEncoder, - ).encode("latin-1") + return [ + json.dumps( + o, + separators=(",", ":"), + cls=MessageEncoder, + ) + for o in obj + ] + +class MessagePartGatherSerializer: + def dumps(self, obj): + """ + The parameter is an already serialized list of Message objects. No need + to serialize it again, only join the list together and encode it. + """ + return ("[" + ",".join(obj) + "]").encode("latin-1") + + +class MessageSerializer: def loads(self, data): return json.loads(data.decode("latin-1"), cls=MessageDecoder) @@ -70,6 +84,7 @@ class CookieStorage(BaseStorage): # restrict the session cookie to 1/2 of 4kb. See #18781. max_cookie_size = 2048 not_finished = "__messagesnotfinished__" + not_finished_json = json.dumps("__messagesnotfinished__") key_salt = "django.contrib.messages" def __init__(self, *args, **kwargs): @@ -122,7 +137,8 @@ class CookieStorage(BaseStorage): returned), and add the not_finished sentinel value to indicate as much. """ unstored_messages = [] - encoded_data = self._encode(messages) + serialized_messages = MessagePartSerializer().dumps(messages) + encoded_data = self._encode_parts(serialized_messages) if self.max_cookie_size: # data is going to be stored eventually by SimpleCookie, which # adds its own overhead, which we must account for. @@ -134,27 +150,40 @@ class CookieStorage(BaseStorage): while encoded_data and stored_length(encoded_data) > self.max_cookie_size: if remove_oldest: unstored_messages.append(messages.pop(0)) + serialized_messages.pop(0) else: unstored_messages.insert(0, messages.pop()) - encoded_data = self._encode( - messages + [self.not_finished], encode_empty=unstored_messages + serialized_messages.pop() + encoded_data = self._encode_parts( + serialized_messages + [self.not_finished_json], + encode_empty=bool(unstored_messages), ) self._update_cookie(encoded_data, response) return unstored_messages - def _encode(self, messages, encode_empty=False): + def _encode_parts(self, messages, encode_empty=False): """ - Return an encoded version of the messages list which can be stored as - plain text. + Return an encoded version of the serialized 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: return self.signer.sign_object( - messages, serializer=MessageSerializer, compress=True + messages, serializer=MessagePartGatherSerializer, compress=True ) + def _encode(self, messages, encode_empty=False): + """ + Return an encoded version of the messages list which can be stored as + plain text. + + Proxies MessagePartSerializer.dumps and _encoded_parts. + """ + serialized_messages = MessagePartSerializer().dumps(messages) + return self._encode_parts(serialized_messages, encode_empty=encode_empty) + def _decode(self, data): """ Safely decode an encoded text stream back into a list of messages. diff --git a/tests/messages_tests/test_cookie.py b/tests/messages_tests/test_cookie.py index 913611c0fe..0fd2ed34d8 100644 --- a/tests/messages_tests/test_cookie.py +++ b/tests/messages_tests/test_cookie.py @@ -60,9 +60,9 @@ class CookieTests(BaseTests, SimpleTestCase): def encode_decode(self, *args, **kwargs): storage = self.get_storage() - message = Message(constants.DEBUG, *args, **kwargs) + message = [Message(constants.DEBUG, *args, **kwargs)] encoded = storage._encode(message) - return storage._decode(encoded) + return storage._decode(encoded)[0] def test_get(self): storage = self.storage_class(self.get_request())