messages: Return shallow copy of message object.

When more than one outgoing webhook is configured,
the message which is send to the webhook bot passes
through finalize_payload function multiple times,
which mutated the message dict in a way that many keys
were lost from the dict obj.

This commit fixes that problem by having
`finalize_payload` return a shallow copy of the
incoming dict, instead of mutating it.  We still
mutate dicts inside of `post_process_dicts`, though,
for performance reasons.

This was slightly modified by @showell to fix the
`test_both_codepaths` test that was added concurrently
to this work.  (I used a slightly verbose style in the
tests to emphasize the transformation from `wide_dict`
to `narrow_dict`.)

I also removed a deepcopy call inside
`get_client_payload`, since we now no longer mutate
in `finalize_payload`.

Finally, I added some comments here and there.

For testing, I mostly protect against the root
cause of the bug happening again, by adding a line
to make sure that `sender_realm_id` does not get
wiped out from the "wide" dictionary.

A better test would exercise the actual code that
exposed the bug here by sending a message to a bot
with two or more services attached to it.  I will
do that in a future commit.

Fixes #14384
This commit is contained in:
Udit107710
2020-03-26 22:16:23 +00:00
committed by Tim Abbott
parent 4c51a94bcd
commit ef741bf317
6 changed files with 51 additions and 19 deletions

View File

@@ -2,6 +2,7 @@ import datetime
import ujson
import zlib
import ahocorasick
import copy
from django.utils.translation import ugettext as _
from django.utils.timezone import now as timezone_now
@@ -190,17 +191,46 @@ class MessageDict:
@staticmethod
def post_process_dicts(objs: List[Dict[str, Any]], apply_markdown: bool, client_gravatar: bool) -> None:
'''
NOTE: This function mutates the objects in
the `objs` list, rather than making
shallow copies. It might be safer to
make shallow copies here, but performance
is somewhat important here, as we are
often fetching several messages.
'''
MessageDict.bulk_hydrate_sender_info(objs)
MessageDict.bulk_hydrate_recipient_info(objs)
for obj in objs:
MessageDict.finalize_payload(obj, apply_markdown, client_gravatar)
MessageDict._finalize_payload(obj, apply_markdown, client_gravatar)
@staticmethod
def finalize_payload(obj: Dict[str, Any],
apply_markdown: bool,
client_gravatar: bool,
keep_rendered_content: bool=False) -> None:
keep_rendered_content: bool=False) -> Dict[str, Any]:
'''
Make a shallow copy of the incoming dict to avoid
mutation-related bugs. This function is often
called when we're sending out message events to
multiple clients, who often want the final dictionary
to have different shapes here based on the parameters.
'''
new_obj = copy.copy(obj)
# Next call our worker, which mutates the record in place.
MessageDict._finalize_payload(
new_obj,
apply_markdown=apply_markdown,
client_gravatar=client_gravatar,
keep_rendered_content=keep_rendered_content
)
return new_obj
@staticmethod
def _finalize_payload(obj: Dict[str, Any], apply_markdown: bool, client_gravatar: bool,
keep_rendered_content: bool=False) -> None:
MessageDict.set_sender_avatar(obj, client_gravatar)
if apply_markdown:
obj['content_type'] = 'text/html'