request: Store client information using ZulipRequestNotes.

This concludes the HttpRequest migration to eliminate arbitrary
attributes (except private ones that are belong to django) attached
to the request object during runtime and migrated them to a
separate data structure dedicated for the purpose of adding
information (so called notes) to a HttpRequest.
This commit is contained in:
PIG208 2021-07-10 00:10:51 +08:00 committed by Tim Abbott
parent 26875cdf0b
commit c03b9c95ad
20 changed files with 89 additions and 55 deletions

View File

@ -170,7 +170,7 @@ def zulip_default_context(request: HttpRequest) -> Dict[str, Any]:
"settings_path": settings_path, "settings_path": settings_path,
"secrets_path": secrets_path, "secrets_path": secrets_path,
"settings_comments_path": settings_comments_path, "settings_comments_path": settings_comments_path,
"platform": request.client_name, "platform": get_request_notes(request).client_name,
"allow_search_engine_indexing": allow_search_engine_indexing, "allow_search_engine_indexing": allow_search_engine_indexing,
"landing_page_navbar_message": settings.LANDING_PAGE_NAVBAR_MESSAGE, "landing_page_navbar_message": settings.LANDING_PAGE_NAVBAR_MESSAGE,
"default_page_params": default_page_params, "default_page_params": default_page_params,

View File

@ -92,11 +92,12 @@ def update_user_activity(
else: else:
query = request.META["PATH_INFO"] query = request.META["PATH_INFO"]
assert request_notes.client is not None
event = { event = {
"query": query, "query": query,
"user_profile_id": user_profile.id, "user_profile_id": user_profile.id,
"time": datetime_to_timestamp(timezone_now()), "time": datetime_to_timestamp(timezone_now()),
"client_id": request.client.id, "client_id": request_notes.client.id,
} }
queue_json_publish("user_activity", event, lambda event: None) queue_json_publish("user_activity", event, lambda event: None)
@ -181,8 +182,11 @@ def process_client(
skip_update_user_activity: bool = False, skip_update_user_activity: bool = False,
query: Optional[str] = None, query: Optional[str] = None,
) -> None: ) -> None:
request_notes = get_request_notes(request)
if client_name is None: if client_name is None:
client_name = request.client_name client_name = request_notes.client_name
assert client_name is not None
# We could check for a browser's name being "Mozilla", but # We could check for a browser's name being "Mozilla", but
# e.g. Opera and MobileSafari don't set that, and it seems # e.g. Opera and MobileSafari don't set that, and it seems
@ -192,7 +196,7 @@ def process_client(
# the Zulip desktop apps be themselves. # the Zulip desktop apps be themselves.
client_name = "website" client_name = "website"
request.client = get_client(client_name) request_notes.client = get_client(client_name)
if not skip_update_user_activity and user_profile.is_authenticated: if not skip_update_user_activity and user_profile.is_authenticated:
update_user_activity(request, user_profile, query) update_user_activity(request, user_profile, query)
@ -788,7 +792,8 @@ def client_is_exempt_from_rate_limiting(request: HttpRequest) -> bool:
# Don't rate limit requests from Django that come from our own servers, # Don't rate limit requests from Django that come from our own servers,
# and don't rate-limit dev instances # and don't rate-limit dev instances
return (request.client and request.client.name.lower() == "internal") and ( client = get_request_notes(request).client
return (client is not None and client.name.lower() == "internal") and (
is_local_addr(request.META["REMOTE_ADDR"]) or settings.DEBUG_RATE_LIMITING is_local_addr(request.META["REMOTE_ADDR"]) or settings.DEBUG_RATE_LIMITING
) )

View File

@ -16,6 +16,7 @@ from zerver.lib.i18n import (
get_language_list, get_language_list,
get_language_translation_data, get_language_translation_data,
) )
from zerver.lib.request import get_request_notes
from zerver.models import Message, Realm, Stream, UserProfile from zerver.models import Message, Realm, Stream, UserProfile
from zerver.views.message_flags import get_latest_update_message_flag_activity from zerver.views.message_flags import get_latest_update_message_flag_activity
@ -138,9 +139,11 @@ def build_page_params_for_home_page_load(
} }
if user_profile is not None: if user_profile is not None:
client = get_request_notes(request).client
assert client is not None
register_ret = do_events_register( register_ret = do_events_register(
user_profile, user_profile,
request.client, client,
apply_markdown=True, apply_markdown=True,
client_gravatar=True, client_gravatar=True,
slim_presence=True, slim_presence=True,

View File

@ -276,9 +276,13 @@ class ZulipWebhookFormatter(ZulipFormatter):
) )
header_message = header_text if header_text else None header_message = header_text if header_text else None
from zerver.lib.request import get_request_notes
client = get_request_notes(request).client
assert client is not None
setattr(record, "user", f"{request.user.delivery_email} ({request.user.realm.string_id})") setattr(record, "user", f"{request.user.delivery_email} ({request.user.realm.string_id})")
setattr(record, "client", request.client.name) setattr(record, "client", client.name)
setattr(record, "url", request.META.get("PATH_INFO", None)) setattr(record, "url", request.META.get("PATH_INFO", None))
setattr(record, "content_type", request.content_type) setattr(record, "content_type", request.content_type)
setattr(record, "custom_headers", header_message) setattr(record, "custom_headers", header_message)

View File

@ -300,8 +300,6 @@ class HostRequestMock(HttpRequest):
self.host = host self.host = host
self.GET = QueryDict(mutable=True) self.GET = QueryDict(mutable=True)
self.method = "" self.method = ""
if client_name is not None:
self.client = get_client(client_name)
# Convert any integer parameters passed into strings, even # Convert any integer parameters passed into strings, even
# though of course the HTTP API would do so. Ideally, we'd # though of course the HTTP API would do so. Ideally, we'd
@ -322,11 +320,12 @@ class HostRequestMock(HttpRequest):
self.user = user_profile self.user = user_profile
self._body = b"" self._body = b""
self.content_type = "" self.content_type = ""
self.client_name = ""
request_notes_map[self] = ZulipRequestNotes( request_notes_map[self] = ZulipRequestNotes(
client_name="",
log_data={}, log_data={},
tornado_handler=tornado_handler, tornado_handler=tornado_handler,
client=get_client(client_name) if client_name is not None else None,
) )
@property @property

View File

@ -14,7 +14,7 @@ from zerver.lib.actions import (
send_rate_limited_pm_notification_to_bot_owner, send_rate_limited_pm_notification_to_bot_owner,
) )
from zerver.lib.exceptions import ErrorCode, JsonableError, StreamDoesNotExistError from zerver.lib.exceptions import ErrorCode, JsonableError, StreamDoesNotExistError
from zerver.lib.request import REQ, has_request_variables from zerver.lib.request import REQ, get_request_notes, has_request_variables
from zerver.lib.send_email import FromAddress from zerver.lib.send_email import FromAddress
from zerver.lib.timestamp import timestamp_to_datetime from zerver.lib.timestamp import timestamp_to_datetime
from zerver.lib.validator import check_list, check_string from zerver.lib.validator import check_list, check_string
@ -107,9 +107,11 @@ def check_send_webhook_message(
): ):
return return
client = get_request_notes(request).client
assert client is not None
if stream is None: if stream is None:
assert user_profile.bot_owner is not None assert user_profile.bot_owner is not None
check_send_private_message(user_profile, request.client, user_profile.bot_owner, body) check_send_private_message(user_profile, client, user_profile.bot_owner, body)
else: else:
# Some third-party websites (such as Atlassian's Jira), tend to # Some third-party websites (such as Atlassian's Jira), tend to
# double escape their URLs in a manner that escaped space characters # double escape their URLs in a manner that escaped space characters
@ -125,11 +127,9 @@ def check_send_webhook_message(
try: try:
if stream.isdecimal(): if stream.isdecimal():
check_send_stream_message_by_id( check_send_stream_message_by_id(user_profile, client, int(stream), topic, body)
user_profile, request.client, int(stream), topic, body
)
else: else:
check_send_stream_message(user_profile, request.client, stream, topic, body) check_send_stream_message(user_profile, client, stream, topic, body)
except StreamDoesNotExistError: except StreamDoesNotExistError:
# A PM will be sent to the bot_owner by check_message, notifying # A PM will be sent to the bot_owner by check_message, notifying
# that the webhook bot just tried to send a message to a non-existent # that the webhook bot just tried to send a message to a non-existent

View File

@ -351,7 +351,7 @@ class LogRequests(MiddlewareMixin):
# Avoid re-initializing request_notes.log_data if it's already there. # Avoid re-initializing request_notes.log_data if it's already there.
return return
request.client_name, request.client_version = parse_client(request) request_notes.client_name, request_notes.client_version = parse_client(request)
request_notes.log_data = {} request_notes.log_data = {}
record_request_start_data(request_notes.log_data) record_request_start_data(request_notes.log_data)
@ -401,10 +401,6 @@ class LogRequests(MiddlewareMixin):
requestor_for_logs = request.user.format_requestor_for_logs() requestor_for_logs = request.user.format_requestor_for_logs()
else: else:
requestor_for_logs = "unauth@{}".format(get_subdomain(request) or "root") requestor_for_logs = "unauth@{}".format(get_subdomain(request) or "root")
try:
client = request.client.name
except Exception:
client = request.client_name
if response.streaming: if response.streaming:
content_iter = response.streaming_content content_iter = response.streaming_content
@ -413,15 +409,15 @@ class LogRequests(MiddlewareMixin):
content = response.content content = response.content
content_iter = None content_iter = None
assert request_notes.log_data is not None assert request_notes.client_name is not None and request_notes.log_data is not None
write_log_line( write_log_line(
request_notes.log_data, request_notes.log_data,
request.path, request.path,
request.method, request.method,
remote_ip, remote_ip,
requestor_for_logs, requestor_for_logs,
client, request_notes.client_name,
client_version=request.client_version, client_version=request_notes.client_version,
status_code=response.status_code, status_code=response.status_code,
error_content=content, error_content=content,
error_content_iter=content_iter, error_content_iter=content_iter,

View File

@ -243,9 +243,9 @@ class AsyncDjangoHandler(tornado.web.RequestHandler, base.BaseHandler):
if request_notes.requestor_for_logs is not None: if request_notes.requestor_for_logs is not None:
request_notes.requestor_for_logs = old_request_notes.requestor_for_logs request_notes.requestor_for_logs = old_request_notes.requestor_for_logs
request.user = old_request.user request.user = old_request.user
request.client = old_request.client request_notes.client = old_request_notes.client
request.client_name = old_request.client_name request_notes.client_name = old_request_notes.client_name
request.client_version = old_request.client_version request_notes.client_version = old_request_notes.client_version
# The saved_response attribute, if present, causes # The saved_response attribute, if present, causes
# rest_dispatch to return the response immediately before # rest_dispatch to return the response immediately before

View File

@ -114,7 +114,8 @@ def get_events_backend(
handler: AsyncDjangoHandler = tornado_handler handler: AsyncDjangoHandler = tornado_handler
if user_client is None: if user_client is None:
valid_user_client = request.client valid_user_client = get_request_notes(request).client
assert valid_user_client is not None
else: else:
valid_user_client = user_client valid_user_client = user_client

View File

@ -5,7 +5,7 @@ from django.utils.translation import gettext as _
from zerver.lib.events import do_events_register from zerver.lib.events import do_events_register
from zerver.lib.exceptions import JsonableError from zerver.lib.exceptions import JsonableError
from zerver.lib.request import REQ, has_request_variables from zerver.lib.request import REQ, get_request_notes, has_request_variables
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.lib.validator import check_bool, check_dict, check_list, check_string from zerver.lib.validator import check_bool, check_dict, check_list, check_string
from zerver.models import Stream, UserProfile from zerver.models import Stream, UserProfile
@ -79,9 +79,12 @@ def events_register_backend(
if client_capabilities is None: if client_capabilities is None:
client_capabilities = {} client_capabilities = {}
client = get_request_notes(request).client
assert client is not None
ret = do_events_register( ret = do_events_register(
user_profile, user_profile,
request.client, client,
apply_markdown, apply_markdown,
client_gravatar, client_gravatar,
slim_presence, slim_presence,

View File

@ -37,9 +37,10 @@ def update_message_flags(
flag: str = REQ(), flag: str = REQ(),
) -> HttpResponse: ) -> HttpResponse:
request_notes = get_request_notes(request) request_notes = get_request_notes(request)
assert request_notes.client is not None
assert request_notes.log_data is not None assert request_notes.log_data is not None
count = do_update_message_flags(user_profile, request.client, operation, flag, messages) count = do_update_message_flags(user_profile, request_notes.client, operation, flag, messages)
target_count_str = str(len(messages)) target_count_str = str(len(messages))
log_data_str = f"[{operation} {flag}/{target_count_str}] actually {count}" log_data_str = f"[{operation} {flag}/{target_count_str}] actually {count}"
@ -50,8 +51,9 @@ def update_message_flags(
@has_request_variables @has_request_variables
def mark_all_as_read(request: HttpRequest, user_profile: UserProfile) -> HttpResponse: def mark_all_as_read(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
count = do_mark_all_as_read(user_profile, request.client)
request_notes = get_request_notes(request) request_notes = get_request_notes(request)
assert request_notes.client is not None
count = do_mark_all_as_read(user_profile, request_notes.client)
log_data_str = f"[{count} updated]" log_data_str = f"[{count} updated]"
assert request_notes.log_data is not None assert request_notes.log_data is not None

View File

@ -20,6 +20,7 @@ from zerver.lib.actions import (
) )
from zerver.lib.exceptions import JsonableError from zerver.lib.exceptions import JsonableError
from zerver.lib.message import render_markdown from zerver.lib.message import render_markdown
from zerver.lib.request import get_request_notes
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.lib.timestamp import convert_to_UTC from zerver.lib.timestamp import convert_to_UTC
from zerver.lib.topic import REQ_topic from zerver.lib.topic import REQ_topic
@ -52,13 +53,16 @@ def create_mirrored_message_users(
for email in recipients: for email in recipients:
referenced_users.add(email.lower()) referenced_users.add(email.lower())
if request.client.name == "zephyr_mirror": client = get_request_notes(request).client
assert client is not None
if client.name == "zephyr_mirror":
user_check = same_realm_zephyr_user user_check = same_realm_zephyr_user
fullname_function = compute_mit_user_fullname fullname_function = compute_mit_user_fullname
elif request.client.name == "irc_mirror": elif client.name == "irc_mirror":
user_check = same_realm_irc_user user_check = same_realm_irc_user
fullname_function = compute_irc_user_fullname fullname_function = compute_irc_user_fullname
elif request.client.name in ("jabber_mirror", "JabberMirror"): elif client.name in ("jabber_mirror", "JabberMirror"):
user_check = same_realm_jabber_user user_check = same_realm_jabber_user
fullname_function = compute_jabber_user_fullname fullname_function = compute_jabber_user_fullname
else: else:
@ -221,7 +225,8 @@ def send_message_backend(
# `yes` to accepting `true` like all of our normal booleans. # `yes` to accepting `true` like all of our normal booleans.
forged = forged_str is not None and forged_str in ["yes", "true"] forged = forged_str is not None and forged_str in ["yes", "true"]
client = request.client client = get_request_notes(request).client
assert client is not None
can_forge_sender = request.user.can_forge_sender can_forge_sender = request.user.can_forge_sender
if forged and not can_forge_sender: if forged and not can_forge_sender:
raise JsonableError(_("User not authorized for this query")) raise JsonableError(_("User not authorized for this query"))
@ -324,7 +329,9 @@ def render_message_backend(
message = Message() message = Message()
message.sender = user_profile message.sender = user_profile
message.content = content message.content = content
message.sending_client = request.client client = get_request_notes(request).client
assert client is not None
message.sending_client = client
rendering_result = render_markdown(message, content, realm=user_profile.realm) rendering_result = render_markdown(message, content, realm=user_profile.realm)
return json_success({"rendered": rendering_result.rendered_content}) return json_success({"rendered": rendering_result.rendered_content})

View File

@ -9,7 +9,7 @@ from django.utils.translation import gettext as _
from zerver.decorator import human_users_only from zerver.decorator import human_users_only
from zerver.lib.actions import do_update_user_status, update_user_presence from zerver.lib.actions import do_update_user_status, update_user_presence
from zerver.lib.presence import get_presence_for_user, get_presence_response from zerver.lib.presence import get_presence_for_user, get_presence_response
from zerver.lib.request import REQ, JsonableError, has_request_variables from zerver.lib.request import REQ, JsonableError, get_request_notes, has_request_variables
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.timestamp import datetime_to_timestamp
from zerver.lib.validator import check_bool, check_capped_string from zerver.lib.validator import check_bool, check_capped_string
@ -75,11 +75,13 @@ def update_user_status_backend(
if (away is None) and (status_text is None): if (away is None) and (status_text is None):
raise JsonableError(_("Client did not pass any new values.")) raise JsonableError(_("Client did not pass any new values."))
client = get_request_notes(request).client
assert client is not None
do_update_user_status( do_update_user_status(
user_profile=user_profile, user_profile=user_profile,
away=away, away=away,
status_text=status_text, status_text=status_text,
client_id=request.client.id, client_id=client.id,
) )
return json_success() return json_success()
@ -99,9 +101,9 @@ def update_active_status_backend(
if status_val is None: if status_val is None:
raise JsonableError(_("Invalid status: {}").format(status)) raise JsonableError(_("Invalid status: {}").format(status))
elif user_profile.presence_enabled: elif user_profile.presence_enabled:
update_user_presence( client = get_request_notes(request).client
user_profile, request.client, timezone_now(), status_val, new_user_input assert client is not None
) update_user_presence(user_profile, client, timezone_now(), status_val, new_user_input)
if ping_only: if ping_only:
ret: Dict[str, Any] = {} ret: Dict[str, Any] = {}

View File

@ -50,7 +50,7 @@ from zerver.lib.exceptions import (
OrganizationOwnerRequired, OrganizationOwnerRequired,
ResourceNotFoundError, ResourceNotFoundError,
) )
from zerver.lib.request import REQ, has_request_variables from zerver.lib.request import REQ, get_request_notes, has_request_variables
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.lib.retention import parse_message_retention_days from zerver.lib.retention import parse_message_retention_days
from zerver.lib.streams import ( from zerver.lib.streams import (
@ -404,8 +404,10 @@ def remove_subscriptions_backend(
people_to_unsub = {user_profile} people_to_unsub = {user_profile}
result: Dict[str, List[str]] = dict(removed=[], not_removed=[]) result: Dict[str, List[str]] = dict(removed=[], not_removed=[])
client = get_request_notes(request).client
assert client is not None
(removed, not_subscribed) = bulk_remove_subscriptions( (removed, not_subscribed) = bulk_remove_subscriptions(
people_to_unsub, streams, request.client, acting_user=user_profile people_to_unsub, streams, client, acting_user=user_profile
) )
for (subscriber, removed_stream) in removed: for (subscriber, removed_stream) in removed:

View File

@ -5,7 +5,7 @@ from django.http import HttpRequest, HttpResponse
from zerver.decorator import webhook_view from zerver.decorator import webhook_view
from zerver.lib.actions import check_send_private_message from zerver.lib.actions import check_send_private_message
from zerver.lib.request import REQ, has_request_variables from zerver.lib.request import REQ, get_request_notes, has_request_variables
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.models import UserProfile, get_user from zerver.models import UserProfile, get_user
@ -35,5 +35,7 @@ def api_dialogflow_webhook(
body = f"{status} - {error_status}" body = f"{status} - {error_status}"
receiving_user = get_user(email, user_profile.realm) receiving_user = get_user(email, user_profile.realm)
check_send_private_message(user_profile, request.client, receiving_user, body) client = get_request_notes(request).client
assert client is not None
check_send_private_message(user_profile, client, receiving_user, body)
return json_success() return json_success()

View File

@ -4,7 +4,7 @@ from django.utils.translation import gettext as _
from zerver.decorator import webhook_view from zerver.decorator import webhook_view
from zerver.lib.actions import check_send_stream_message from zerver.lib.actions import check_send_stream_message
from zerver.lib.exceptions import JsonableError from zerver.lib.exceptions import JsonableError
from zerver.lib.request import REQ, has_request_variables from zerver.lib.request import REQ, get_request_notes, has_request_variables
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.models import UserProfile from zerver.models import UserProfile
@ -34,5 +34,7 @@ def api_slack_webhook(
subject = _("Message from Slack") subject = _("Message from Slack")
content = ZULIP_MESSAGE_TEMPLATE.format(message_sender=user_name, text=text) content = ZULIP_MESSAGE_TEMPLATE.format(message_sender=user_name, text=text)
check_send_stream_message(user_profile, request.client, stream, subject, content) client = get_request_notes(request).client
assert client is not None
check_send_stream_message(user_profile, client, stream, subject, content)
return json_success() return json_success()

View File

@ -10,7 +10,7 @@ from zerver.lib.actions import (
check_send_private_message, check_send_private_message,
send_rate_limited_pm_notification_to_bot_owner, send_rate_limited_pm_notification_to_bot_owner,
) )
from zerver.lib.request import REQ, has_request_variables from zerver.lib.request import REQ, get_request_notes, has_request_variables
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.lib.send_email import FromAddress from zerver.lib.send_email import FromAddress
from zerver.lib.webhooks.common import check_send_webhook_message from zerver.lib.webhooks.common import check_send_webhook_message
@ -137,7 +137,9 @@ def api_teamcity_webhook(
return json_success() return json_success()
body = f"Your personal build for {body}" body = f"Your personal build for {body}"
check_send_private_message(user_profile, request.client, teamcity_user, body) client = get_request_notes(request).client
assert client is not None
check_send_private_message(user_profile, client, teamcity_user, body)
return json_success() return json_success()

View File

@ -5,7 +5,7 @@ from django.http import HttpRequest, HttpResponse
from zerver.decorator import webhook_view from zerver.decorator import webhook_view
from zerver.lib.actions import check_send_private_message from zerver.lib.actions import check_send_private_message
from zerver.lib.request import REQ, has_request_variables from zerver.lib.request import REQ, get_request_notes, has_request_variables
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.models import UserProfile, get_user from zerver.models import UserProfile, get_user
@ -22,5 +22,7 @@ def api_yo_app_webhook(
) -> HttpResponse: ) -> HttpResponse:
body = f"Yo from {username}" body = f"Yo from {username}"
receiving_user = get_user(email, user_profile.realm) receiving_user = get_user(email, user_profile.realm)
check_send_private_message(user_profile, request.client, receiving_user, body) client = get_request_notes(request).client
assert client is not None
check_send_private_message(user_profile, client, receiving_user, body)
return json_success() return json_success()

View File

@ -278,7 +278,9 @@ def rate_limit_auth(auth_func: AuthFuncT, *args: Any, **kwargs: Any) -> Optional
request = args[1] request = args[1]
username = kwargs["username"] username = kwargs["username"]
if not hasattr(request, "client") or not client_is_exempt_from_rate_limiting(request): if get_request_notes(request).client is None or not client_is_exempt_from_rate_limiting(
request
):
# Django cycles through enabled authentication backends until one succeeds, # Django cycles through enabled authentication backends until one succeeds,
# or all of them fail. If multiple backends are tried like this, we only want # or all of them fail. If multiple backends are tried like this, we only want
# to execute rate_limit_authentication_* once, on the first attempt: # to execute rate_limit_authentication_* once, on the first attempt:

View File

@ -52,8 +52,8 @@ def add_context(event: "Event", hint: "Hint") -> Optional["Event"]:
request = get_current_request() request = get_current_request()
if request: if request:
request_notes = get_request_notes(request) request_notes = get_request_notes(request)
if hasattr(request, "client"): if request_notes.client is not None:
event["tags"]["client"] = request.client.name event["tags"]["client"] = request_notes.client.name
if request_notes.realm is not None: if request_notes.realm is not None:
event["tags"].setdefault("realm", request_notes.realm.string_id) event["tags"].setdefault("realm", request_notes.realm.string_id)
return event return event