2022-11-27 23:09:11 +01:00
|
|
|
from email.headerregistry import Address
|
2024-07-30 10:17:33 +02:00
|
|
|
from typing import Annotated, Any
|
2015-11-23 17:09:21 +01:00
|
|
|
|
|
|
|
from django.conf import settings
|
2016-11-17 08:56:01 +01:00
|
|
|
from django.contrib.auth import authenticate, update_session_auth_hash
|
2023-07-20 21:05:37 +02:00
|
|
|
from django.contrib.auth.tokens import default_token_generator
|
2020-06-11 00:54:34 +02:00
|
|
|
from django.core.exceptions import ValidationError
|
2022-06-15 04:23:40 +02:00
|
|
|
from django.core.files.uploadedfile import UploadedFile
|
2023-11-28 18:24:30 +01:00
|
|
|
from django.db import transaction
|
2023-07-20 21:05:37 +02:00
|
|
|
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
2019-02-02 23:53:22 +01:00
|
|
|
from django.shortcuts import render
|
2020-09-16 19:30:05 +02:00
|
|
|
from django.utils.html import escape
|
|
|
|
from django.utils.safestring import SafeString
|
2021-04-16 00:57:30 +02:00
|
|
|
from django.utils.translation import gettext as _
|
|
|
|
from django.utils.translation import gettext_lazy
|
2024-07-30 10:17:33 +02:00
|
|
|
from pydantic import Json
|
2015-11-23 17:09:21 +01:00
|
|
|
|
2020-06-11 00:54:34 +02:00
|
|
|
from confirmation.models import (
|
|
|
|
Confirmation,
|
2022-11-17 09:30:48 +01:00
|
|
|
ConfirmationKeyError,
|
2020-06-11 00:54:34 +02:00
|
|
|
get_object_from_key,
|
|
|
|
render_confirmation_key_error,
|
|
|
|
)
|
2022-04-14 23:49:26 +02:00
|
|
|
from zerver.actions.user_settings import (
|
2020-06-11 00:54:34 +02:00
|
|
|
check_change_full_name,
|
|
|
|
do_change_avatar_fields,
|
|
|
|
do_change_password,
|
|
|
|
do_change_user_delivery_email,
|
2021-08-13 16:18:53 +02:00
|
|
|
do_change_user_setting,
|
2020-06-11 00:54:34 +02:00
|
|
|
do_regenerate_api_key,
|
|
|
|
do_start_email_change_process,
|
|
|
|
)
|
2024-09-23 20:24:45 +02:00
|
|
|
from zerver.actions.users import generate_password_reset_url
|
2022-04-14 23:49:26 +02:00
|
|
|
from zerver.decorator import human_users_only
|
2015-11-23 17:09:21 +01:00
|
|
|
from zerver.lib.avatar import avatar_url
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.lib.email_validation import (
|
|
|
|
get_realm_email_validator,
|
2021-07-16 22:11:10 +02:00
|
|
|
validate_email_is_valid,
|
2020-06-11 00:54:34 +02:00
|
|
|
validate_email_not_already_in_realm,
|
|
|
|
)
|
2024-07-18 21:29:40 +02:00
|
|
|
from zerver.lib.exceptions import (
|
|
|
|
IncompatibleParameterValuesError,
|
|
|
|
JsonableError,
|
|
|
|
RateLimitedError,
|
|
|
|
UserDeactivatedError,
|
|
|
|
)
|
2021-09-09 20:19:08 +02:00
|
|
|
from zerver.lib.i18n import get_available_language_codes
|
2021-11-03 23:20:55 +01:00
|
|
|
from zerver.lib.rate_limiter import RateLimitedUser
|
2021-06-30 18:35:50 +02:00
|
|
|
from zerver.lib.response import json_success
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.lib.send_email import FromAddress, send_email
|
2022-04-14 23:26:40 +02:00
|
|
|
from zerver.lib.sounds import get_available_notification_sounds
|
2024-07-30 10:17:33 +02:00
|
|
|
from zerver.lib.typed_endpoint import typed_endpoint, typed_endpoint_without_parameters
|
|
|
|
from zerver.lib.typed_endpoint_validators import (
|
|
|
|
check_int_in_validator,
|
|
|
|
check_string_in_validator,
|
|
|
|
timezone_validator,
|
2022-06-28 00:43:57 +02:00
|
|
|
)
|
2024-07-30 10:17:33 +02:00
|
|
|
from zerver.lib.upload import upload_avatar_image
|
2024-07-18 11:05:37 +02:00
|
|
|
from zerver.models import EmailChangeStatus, RealmUserDefault, UserBaseSettings, UserProfile
|
2023-12-15 02:14:24 +01:00
|
|
|
from zerver.models.realms import avatar_changes_disabled, name_changes_disabled
|
2021-11-04 00:18:32 +01:00
|
|
|
from zerver.views.auth import redirect_to_deactivation_notice
|
2020-06-11 00:54:34 +02:00
|
|
|
from zproject.backends import check_password_strength, email_belongs_to_ldap
|
2017-01-20 12:27:38 +01:00
|
|
|
|
2021-04-16 00:57:30 +02:00
|
|
|
AVATAR_CHANGES_DISABLED_ERROR = gettext_lazy("Avatar changes are disabled in this organization.")
|
2019-04-23 04:51:04 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2023-11-28 18:25:28 +01:00
|
|
|
def validate_email_change_request(user_profile: UserProfile, new_email: str) -> None:
|
|
|
|
if not user_profile.is_active:
|
|
|
|
# TODO: Make this into a user-facing error, not JSON
|
|
|
|
raise UserDeactivatedError
|
|
|
|
|
|
|
|
if user_profile.realm.email_changes_disabled and not user_profile.is_realm_admin:
|
|
|
|
raise JsonableError(_("Email address changes are disabled in this organization."))
|
|
|
|
|
|
|
|
error = validate_email_is_valid(
|
|
|
|
new_email,
|
|
|
|
get_realm_email_validator(user_profile.realm),
|
|
|
|
)
|
|
|
|
if error:
|
|
|
|
raise JsonableError(error)
|
|
|
|
|
|
|
|
try:
|
|
|
|
validate_email_not_already_in_realm(
|
|
|
|
user_profile.realm,
|
|
|
|
new_email,
|
|
|
|
verbose=False,
|
|
|
|
)
|
|
|
|
except ValidationError as e:
|
|
|
|
raise JsonableError(e.message)
|
|
|
|
|
|
|
|
|
2017-11-27 09:28:57 +01:00
|
|
|
def confirm_email_change(request: HttpRequest, confirmation_key: str) -> HttpResponse:
|
2017-07-22 00:27:45 +02:00
|
|
|
try:
|
2022-07-21 15:26:09 +02:00
|
|
|
email_change_object = get_object_from_key(
|
|
|
|
confirmation_key, [Confirmation.EMAIL_CHANGE], mark_object_used=True
|
|
|
|
)
|
2022-11-17 09:30:48 +01:00
|
|
|
except ConfirmationKeyError as exception:
|
2017-07-22 00:27:45 +02:00
|
|
|
return render_confirmation_key_error(request, exception)
|
2017-01-20 12:27:38 +01:00
|
|
|
|
2022-06-15 04:23:40 +02:00
|
|
|
assert isinstance(email_change_object, EmailChangeStatus)
|
2017-11-07 20:45:11 +01:00
|
|
|
new_email = email_change_object.new_email
|
|
|
|
old_email = email_change_object.old_email
|
2023-11-28 18:24:30 +01:00
|
|
|
with transaction.atomic():
|
|
|
|
user_profile = UserProfile.objects.select_for_update().get(
|
|
|
|
id=email_change_object.user_profile_id
|
|
|
|
)
|
2017-01-20 12:27:38 +01:00
|
|
|
|
2023-11-28 18:25:06 +01:00
|
|
|
if user_profile.delivery_email != old_email:
|
|
|
|
# This is not expected to be possible, since we deactivate
|
|
|
|
# any previous email changes when we create a new one, but
|
|
|
|
# double-check.
|
|
|
|
return render_confirmation_key_error(
|
|
|
|
request, ConfirmationKeyError(ConfirmationKeyError.EXPIRED)
|
|
|
|
) # nocoverage
|
|
|
|
|
2023-11-28 18:24:30 +01:00
|
|
|
if user_profile.realm.deactivated:
|
|
|
|
return redirect_to_deactivation_notice()
|
2021-11-04 00:18:32 +01:00
|
|
|
|
2023-11-28 18:25:28 +01:00
|
|
|
validate_email_change_request(user_profile, new_email)
|
2024-08-11 22:38:04 +02:00
|
|
|
do_change_user_delivery_email(user_profile, new_email, acting_user=user_profile)
|
2017-07-22 00:27:45 +02:00
|
|
|
|
2023-11-28 18:24:30 +01:00
|
|
|
user_profile = UserProfile.objects.get(id=email_change_object.user_profile_id)
|
2021-02-12 08:20:45 +01:00
|
|
|
context = {"realm_name": user_profile.realm.name, "new_email": new_email}
|
2020-02-14 13:58:58 +01:00
|
|
|
language = user_profile.default_language
|
2023-07-20 21:05:37 +02:00
|
|
|
|
|
|
|
if old_email == "":
|
|
|
|
# The assertions here are to help document the only circumstance under which
|
|
|
|
# this condition should be possible.
|
|
|
|
assert (
|
|
|
|
user_profile.realm.demo_organization_scheduled_deletion_date is not None
|
|
|
|
and user_profile.is_realm_owner
|
|
|
|
)
|
|
|
|
# Because demo organizations are created without setting an email and password
|
|
|
|
# we want to redirect to setting a password after configuring and confirming
|
|
|
|
# an email for the owner's account.
|
|
|
|
reset_password_url = generate_password_reset_url(user_profile, default_token_generator)
|
|
|
|
return HttpResponseRedirect(reset_password_url)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
send_email(
|
2021-02-12 08:20:45 +01:00
|
|
|
"zerver/emails/notify_change_in_email",
|
2021-02-12 08:19:30 +01:00
|
|
|
to_emails=[old_email],
|
|
|
|
from_name=FromAddress.security_email_from_name(user_profile=user_profile),
|
|
|
|
from_address=FromAddress.SUPPORT,
|
|
|
|
language=language,
|
|
|
|
context=context,
|
|
|
|
realm=user_profile.realm,
|
|
|
|
)
|
2022-11-27 23:09:11 +01:00
|
|
|
old_email_address = Address(addr_spec=old_email)
|
|
|
|
new_email_address = Address(addr_spec=new_email)
|
2017-01-20 12:27:38 +01:00
|
|
|
ctx = {
|
2021-02-12 08:20:45 +01:00
|
|
|
"new_email_html_tag": SafeString(
|
2022-11-27 23:09:11 +01:00
|
|
|
f'<a href="mailto:{escape(new_email)}">{escape(new_email_address.username)}@<wbr>{escape(new_email_address.domain)}</wbr></a>'
|
2021-02-12 08:19:30 +01:00
|
|
|
),
|
2021-02-12 08:20:45 +01:00
|
|
|
"old_email_html_tag": SafeString(
|
2022-11-27 23:09:11 +01:00
|
|
|
f'<a href="mailto:{escape(old_email)}">{escape(old_email_address.username)}@<wbr>{escape(old_email_address.domain)}</wbr></a>'
|
2021-02-12 08:19:30 +01:00
|
|
|
),
|
2017-01-20 12:27:38 +01:00
|
|
|
}
|
2021-02-12 08:20:45 +01:00
|
|
|
return render(request, "confirmation/confirm_email_change.html", context=ctx)
|
2015-11-23 17:09:21 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2021-07-07 22:08:11 +02:00
|
|
|
emojiset_choices = {emojiset["key"] for emojiset in UserProfile.emojiset_choices()}
|
2023-10-23 09:02:57 +02:00
|
|
|
web_home_view_options = ["recent_topics", "inbox", "all_messages"]
|
2024-07-21 15:23:56 +02:00
|
|
|
web_animate_image_previews_options = ["always", "on_hover", "never"]
|
2021-07-07 22:08:11 +02:00
|
|
|
|
|
|
|
|
2021-09-09 20:19:08 +02:00
|
|
|
def check_settings_values(
|
2024-07-12 02:30:23 +02:00
|
|
|
notification_sound: str | None,
|
|
|
|
email_notifications_batching_period_seconds: int | None,
|
|
|
|
default_language: str | None = None,
|
2021-09-09 20:19:08 +02:00
|
|
|
) -> None:
|
2024-09-03 18:08:36 +02:00
|
|
|
# We can't use typed_endpoint for this widget because
|
2021-09-09 20:19:08 +02:00
|
|
|
# get_available_language_codes requires provisioning to be
|
|
|
|
# complete.
|
|
|
|
if default_language is not None and default_language not in get_available_language_codes():
|
|
|
|
raise JsonableError(_("Invalid default_language"))
|
|
|
|
|
|
|
|
if (
|
|
|
|
notification_sound is not None
|
|
|
|
and notification_sound not in get_available_notification_sounds()
|
|
|
|
and notification_sound != "none"
|
|
|
|
):
|
2023-07-17 22:40:33 +02:00
|
|
|
raise JsonableError(
|
|
|
|
_("Invalid notification sound '{notification_sound}'").format(
|
|
|
|
notification_sound=notification_sound
|
|
|
|
)
|
|
|
|
)
|
2021-09-09 20:19:08 +02:00
|
|
|
|
|
|
|
if email_notifications_batching_period_seconds is not None and (
|
|
|
|
email_notifications_batching_period_seconds <= 0
|
|
|
|
or email_notifications_batching_period_seconds > 7 * 24 * 60 * 60
|
|
|
|
):
|
|
|
|
# We set a limit of one week for the batching period
|
|
|
|
raise JsonableError(
|
2023-07-17 22:40:33 +02:00
|
|
|
_("Invalid email batching period: {seconds} seconds").format(
|
|
|
|
seconds=email_notifications_batching_period_seconds
|
2021-09-09 20:19:08 +02:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-07-18 11:05:37 +02:00
|
|
|
def check_information_density_setting_values(
|
|
|
|
setting_object: UserProfile | RealmUserDefault,
|
|
|
|
dense_mode: bool | None,
|
|
|
|
web_font_size_px: int | None,
|
|
|
|
web_line_height_percent: int | None,
|
|
|
|
) -> None:
|
|
|
|
dense_mode = dense_mode if dense_mode is not None else setting_object.dense_mode
|
|
|
|
web_font_size_px = (
|
|
|
|
web_font_size_px if web_font_size_px is not None else setting_object.web_font_size_px
|
|
|
|
)
|
|
|
|
web_line_height_percent = (
|
|
|
|
web_line_height_percent
|
|
|
|
if web_line_height_percent is not None
|
|
|
|
else setting_object.web_line_height_percent
|
|
|
|
)
|
|
|
|
|
|
|
|
if dense_mode:
|
2024-07-18 02:07:12 +02:00
|
|
|
if web_font_size_px != UserBaseSettings.WEB_FONT_SIZE_PX_COMPACT:
|
2024-07-18 21:29:40 +02:00
|
|
|
raise IncompatibleParameterValuesError("dense_mode", "web_font_size_px")
|
2024-07-18 11:05:37 +02:00
|
|
|
|
2024-07-18 02:07:12 +02:00
|
|
|
if web_line_height_percent != UserBaseSettings.WEB_LINE_HEIGHT_PERCENT_COMPACT:
|
2024-07-18 21:29:40 +02:00
|
|
|
raise IncompatibleParameterValuesError("dense_mode", "web_line_height_percent")
|
2024-07-18 11:05:37 +02:00
|
|
|
|
|
|
|
|
2017-07-31 20:44:52 +02:00
|
|
|
@human_users_only
|
2024-07-30 10:17:33 +02:00
|
|
|
@typed_endpoint
|
2021-02-12 08:19:30 +01:00
|
|
|
def json_change_settings(
|
|
|
|
request: HttpRequest,
|
|
|
|
user_profile: UserProfile,
|
2024-07-30 10:17:33 +02:00
|
|
|
*,
|
|
|
|
full_name: str | None = None,
|
|
|
|
email: str | None = None,
|
|
|
|
old_password: str | None = None,
|
|
|
|
new_password: str | None = None,
|
|
|
|
twenty_four_hour_time: Json[bool] | None = None,
|
|
|
|
dense_mode: Json[bool] | None = None,
|
|
|
|
web_mark_read_on_scroll_policy: Annotated[
|
|
|
|
Json[int], check_int_in_validator(UserProfile.WEB_MARK_READ_ON_SCROLL_POLICY_CHOICES)
|
|
|
|
]
|
|
|
|
| None = None,
|
|
|
|
web_channel_default_view: Annotated[
|
|
|
|
Json[int], check_int_in_validator(UserProfile.WEB_CHANNEL_DEFAULT_VIEW_CHOICES)
|
|
|
|
]
|
|
|
|
| None = None,
|
|
|
|
starred_message_counts: Json[bool] | None = None,
|
|
|
|
receives_typing_notifications: Json[bool] | None = None,
|
|
|
|
fluid_layout_width: Json[bool] | None = None,
|
|
|
|
high_contrast_mode: Json[bool] | None = None,
|
|
|
|
color_scheme: Annotated[Json[int], check_int_in_validator(UserProfile.COLOR_SCHEME_CHOICES)]
|
|
|
|
| None = None,
|
|
|
|
web_font_size_px: Json[int] | None = None,
|
|
|
|
web_line_height_percent: Json[int] | None = None,
|
|
|
|
translate_emoticons: Json[bool] | None = None,
|
|
|
|
display_emoji_reaction_users: Json[bool] | None = None,
|
|
|
|
default_language: str | None = None,
|
|
|
|
web_home_view: Annotated[str, check_string_in_validator(web_home_view_options)] | None = None,
|
|
|
|
web_escape_navigates_to_home_view: Json[bool] | None = None,
|
|
|
|
left_side_userlist: Json[bool] | None = None,
|
|
|
|
emojiset: Annotated[str, check_string_in_validator(emojiset_choices)] | None = None,
|
|
|
|
demote_inactive_streams: Annotated[
|
|
|
|
Json[int], check_int_in_validator(UserProfile.DEMOTE_STREAMS_CHOICES)
|
|
|
|
]
|
|
|
|
| None = None,
|
|
|
|
web_stream_unreads_count_display_policy: Annotated[
|
|
|
|
Json[int],
|
|
|
|
check_int_in_validator(UserProfile.WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_CHOICES),
|
|
|
|
]
|
|
|
|
| None = None,
|
|
|
|
timezone: Annotated[str, timezone_validator()] | None = None,
|
|
|
|
email_notifications_batching_period_seconds: Json[int] | None = None,
|
|
|
|
enable_drafts_synchronization: Json[bool] | None = None,
|
|
|
|
enable_stream_desktop_notifications: Json[bool] | None = None,
|
|
|
|
enable_stream_email_notifications: Json[bool] | None = None,
|
|
|
|
enable_stream_push_notifications: Json[bool] | None = None,
|
|
|
|
enable_stream_audible_notifications: Json[bool] | None = None,
|
|
|
|
wildcard_mentions_notify: Json[bool] | None = None,
|
|
|
|
enable_followed_topic_desktop_notifications: Json[bool] | None = None,
|
|
|
|
enable_followed_topic_email_notifications: Json[bool] | None = None,
|
|
|
|
enable_followed_topic_push_notifications: Json[bool] | None = None,
|
|
|
|
enable_followed_topic_audible_notifications: Json[bool] | None = None,
|
|
|
|
enable_followed_topic_wildcard_mentions_notify: Json[bool] | None = None,
|
|
|
|
notification_sound: str | None = None,
|
|
|
|
enable_desktop_notifications: Json[bool] | None = None,
|
|
|
|
enable_sounds: Json[bool] | None = None,
|
|
|
|
enable_offline_email_notifications: Json[bool] | None = None,
|
|
|
|
enable_offline_push_notifications: Json[bool] | None = None,
|
|
|
|
enable_online_push_notifications: Json[bool] | None = None,
|
|
|
|
enable_digest_emails: Json[bool] | None = None,
|
|
|
|
enable_login_emails: Json[bool] | None = None,
|
|
|
|
enable_marketing_emails: Json[bool] | None = None,
|
|
|
|
message_content_in_email_notifications: Json[bool] | None = None,
|
|
|
|
pm_content_in_desktop_notifications: Json[bool] | None = None,
|
|
|
|
desktop_icon_count_display: Annotated[
|
|
|
|
Json[int], check_int_in_validator(UserProfile.DESKTOP_ICON_COUNT_DISPLAY_CHOICES)
|
|
|
|
]
|
|
|
|
| None = None,
|
|
|
|
realm_name_in_email_notifications_policy: Annotated[
|
|
|
|
Json[int],
|
|
|
|
check_int_in_validator(UserProfile.REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_CHOICES),
|
|
|
|
]
|
|
|
|
| None = None,
|
|
|
|
automatically_follow_topics_policy: Annotated[
|
|
|
|
Json[int],
|
|
|
|
check_int_in_validator(UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES),
|
|
|
|
]
|
|
|
|
| None = None,
|
|
|
|
automatically_unmute_topics_in_muted_streams_policy: Annotated[
|
|
|
|
Json[int],
|
|
|
|
check_int_in_validator(UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES),
|
|
|
|
]
|
|
|
|
| None = None,
|
|
|
|
automatically_follow_topics_where_mentioned: Json[bool] | None = None,
|
|
|
|
presence_enabled: Json[bool] | None = None,
|
|
|
|
enter_sends: Json[bool] | None = None,
|
|
|
|
send_private_typing_notifications: Json[bool] | None = None,
|
|
|
|
send_stream_typing_notifications: Json[bool] | None = None,
|
|
|
|
send_read_receipts: Json[bool] | None = None,
|
2024-09-09 12:51:38 +02:00
|
|
|
allow_private_data_export: Json[bool] | None = None,
|
2024-07-30 10:17:33 +02:00
|
|
|
user_list_style: Annotated[
|
|
|
|
Json[int], check_int_in_validator(UserProfile.USER_LIST_STYLE_CHOICES)
|
|
|
|
]
|
|
|
|
| None = None,
|
|
|
|
web_animate_image_previews: Annotated[
|
|
|
|
str, check_string_in_validator(web_animate_image_previews_options)
|
|
|
|
]
|
|
|
|
| None = None,
|
|
|
|
email_address_visibility: Annotated[
|
|
|
|
Json[int], check_int_in_validator(UserProfile.EMAIL_ADDRESS_VISIBILITY_TYPES)
|
|
|
|
]
|
|
|
|
| None = None,
|
|
|
|
web_navigate_to_sent_message: Json[bool] | None = None,
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> HttpResponse:
|
2024-07-16 21:00:59 +02:00
|
|
|
# UserProfile object is being refetched here to make sure that we
|
|
|
|
# do not use stale object from cache which can happen when a
|
|
|
|
# previous request tried updating multiple settings in a single
|
|
|
|
# request.
|
|
|
|
#
|
|
|
|
# TODO: Change the cache flushing strategy to make sure cache
|
|
|
|
# does not contain stale objects.
|
|
|
|
user_profile = UserProfile.objects.get(id=user_profile.id)
|
2021-07-07 22:08:11 +02:00
|
|
|
if (
|
2021-09-09 16:18:00 +02:00
|
|
|
default_language is not None
|
|
|
|
or notification_sound is not None
|
|
|
|
or email_notifications_batching_period_seconds is not None
|
2021-07-07 22:08:11 +02:00
|
|
|
):
|
2021-09-09 16:18:00 +02:00
|
|
|
check_settings_values(
|
|
|
|
notification_sound, email_notifications_batching_period_seconds, default_language
|
2021-07-22 10:05:04 +02:00
|
|
|
)
|
|
|
|
|
2022-08-08 01:39:32 +02:00
|
|
|
if new_password is not None:
|
2024-07-12 02:30:17 +02:00
|
|
|
return_data: dict[str, Any] = {}
|
2018-08-02 08:47:13 +02:00
|
|
|
if email_belongs_to_ldap(user_profile.realm, user_profile.delivery_email):
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(_("Your Zulip password is managed in LDAP"))
|
2019-12-30 02:21:51 +01:00
|
|
|
|
|
|
|
try:
|
2021-02-12 08:19:30 +01:00
|
|
|
if not authenticate(
|
|
|
|
request,
|
|
|
|
username=user_profile.delivery_email,
|
|
|
|
password=old_password,
|
|
|
|
realm=user_profile.realm,
|
|
|
|
return_data=return_data,
|
|
|
|
):
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(_("Wrong password!"))
|
2022-11-17 09:30:48 +01:00
|
|
|
except RateLimitedError as e:
|
2020-11-27 16:33:01 +01:00
|
|
|
assert e.secs_to_freedom is not None
|
|
|
|
secs_to_freedom = int(e.secs_to_freedom)
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(
|
2023-07-17 22:40:33 +02:00
|
|
|
_("You're making too many attempts! Try again in {seconds} seconds.").format(
|
|
|
|
seconds=secs_to_freedom
|
2021-02-12 08:19:30 +01:00
|
|
|
),
|
2019-12-30 02:21:51 +01:00
|
|
|
)
|
|
|
|
|
auth: Use zxcvbn to ensure password strength on server side.
For a long time, we've been only doing the zxcvbn password strength
checks on the browser, which is helpful, but means users could through
hackery (or a bug in the frontend validation code) manage to set a
too-weak password. We fix this by running our password strength
validation on the backend as well, using python-zxcvbn.
In theory, a bug in python-zxcvbn could result in it producing a
different opinion than the frontend version; if so, it'd be a pretty
bad bug in the library, and hopefully we'd hear about it from users,
report upstream, and get it fixed that way. Alternatively, we can
switch to shelling out to node like we do for KaTeX.
Fixes #6880.
2019-11-18 08:11:03 +01:00
|
|
|
if not check_password_strength(new_password):
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(_("New password is too weak!"))
|
2019-12-30 02:21:51 +01:00
|
|
|
|
2015-11-23 17:09:21 +01:00
|
|
|
do_change_password(user_profile, new_password)
|
2021-11-05 20:26:37 +01:00
|
|
|
# Password changes invalidates sessions, see
|
2024-05-24 16:57:31 +02:00
|
|
|
# https://docs.djangoproject.com/en/5.0/topics/auth/default/#session-invalidation-on-password-change
|
2017-07-05 11:47:21 +02:00
|
|
|
# for details. To avoid this logging the user out of their own
|
2016-11-17 08:56:01 +01:00
|
|
|
# session (which would provide a confusing UX at best), we
|
|
|
|
# update the session hash here.
|
|
|
|
update_session_auth_hash(request, user_profile)
|
2016-12-16 11:38:21 +01:00
|
|
|
# We also save the session to the DB immediately to mitigate
|
|
|
|
# race conditions. In theory, there is still a race condition
|
|
|
|
# and to completely avoid it we will have to use some kind of
|
|
|
|
# mutex lock in `django.contrib.auth.get_user` where session
|
|
|
|
# is verified. To make that lock work we will have to control
|
|
|
|
# the AuthenticationMiddleware which is currently controlled
|
|
|
|
# by Django,
|
|
|
|
request.session.save()
|
2015-11-23 17:09:21 +01:00
|
|
|
|
2024-07-12 02:30:17 +02:00
|
|
|
result: dict[str, Any] = {}
|
2020-03-04 14:40:30 +01:00
|
|
|
|
2022-08-08 01:39:32 +02:00
|
|
|
if email is not None:
|
|
|
|
new_email = email.strip()
|
|
|
|
if user_profile.delivery_email != new_email:
|
2023-11-28 18:25:28 +01:00
|
|
|
validate_email_change_request(user_profile, new_email)
|
2017-01-20 12:27:38 +01:00
|
|
|
|
2022-08-08 01:39:32 +02:00
|
|
|
ratelimited, time_until_free = RateLimitedUser(
|
|
|
|
user_profile, domain="email_change_by_user"
|
|
|
|
).rate_limit()
|
|
|
|
if ratelimited:
|
2022-11-17 09:30:48 +01:00
|
|
|
raise RateLimitedError(time_until_free)
|
2021-11-03 23:20:55 +01:00
|
|
|
|
2022-08-08 01:39:32 +02:00
|
|
|
do_start_email_change_process(user_profile, new_email)
|
2017-01-20 12:27:38 +01:00
|
|
|
|
2022-08-08 01:39:32 +02:00
|
|
|
if full_name is not None and user_profile.full_name != full_name:
|
2018-02-02 16:54:26 +01:00
|
|
|
if name_changes_disabled(user_profile.realm) and not user_profile.is_realm_admin:
|
2015-11-23 17:09:21 +01:00
|
|
|
# Failingly silently is fine -- they can't do it through the UI, so
|
|
|
|
# they'd have to be trying to break the rules.
|
|
|
|
pass
|
|
|
|
else:
|
2017-02-08 04:39:55 +01:00
|
|
|
# Note that check_change_full_name strips the passed name automatically
|
2021-07-15 18:31:34 +02:00
|
|
|
check_change_full_name(user_profile, full_name, user_profile)
|
2015-11-23 17:09:21 +01:00
|
|
|
|
2024-07-15 12:12:17 +02:00
|
|
|
if (
|
2024-07-18 11:05:37 +02:00
|
|
|
dense_mode is not None
|
|
|
|
or web_font_size_px is not None
|
|
|
|
or web_line_height_percent is not None
|
2024-07-15 12:12:17 +02:00
|
|
|
):
|
2024-07-18 11:05:37 +02:00
|
|
|
check_information_density_setting_values(
|
|
|
|
user_profile, dense_mode, web_font_size_px, web_line_height_percent
|
2024-07-15 12:12:17 +02:00
|
|
|
)
|
|
|
|
|
2021-07-07 22:08:11 +02:00
|
|
|
# Loop over user_profile.property_types
|
2023-09-12 23:19:57 +02:00
|
|
|
request_settings = {k: v for k, v in locals().items() if k in user_profile.property_types}
|
|
|
|
for k, v in request_settings.items():
|
2017-05-22 21:07:35 +02:00
|
|
|
if v is not None and getattr(user_profile, k) != v:
|
2021-09-08 13:25:50 +02:00
|
|
|
do_change_user_setting(user_profile, k, v, acting_user=user_profile)
|
2018-01-11 21:36:11 +01:00
|
|
|
|
2021-07-07 22:08:11 +02:00
|
|
|
if timezone is not None and user_profile.timezone != timezone:
|
2021-09-08 13:25:50 +02:00
|
|
|
do_change_user_setting(user_profile, "timezone", timezone, acting_user=user_profile)
|
2021-07-07 22:08:11 +02:00
|
|
|
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request, data=result)
|
2015-11-23 17:09:21 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2017-11-27 09:28:57 +01:00
|
|
|
def set_avatar_backend(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
|
2015-11-23 17:09:21 +01:00
|
|
|
if len(request.FILES) != 1:
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(_("You must upload exactly one avatar."))
|
2015-11-23 17:09:21 +01:00
|
|
|
|
2019-04-23 04:51:04 +02:00
|
|
|
if avatar_changes_disabled(user_profile.realm) and not user_profile.is_realm_admin:
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(str(AVATAR_CHANGES_DISABLED_ERROR))
|
2019-04-23 04:51:04 +02:00
|
|
|
|
2023-07-22 00:34:11 +02:00
|
|
|
[user_file] = request.FILES.values()
|
2022-06-15 04:23:40 +02:00
|
|
|
assert isinstance(user_file, UploadedFile)
|
|
|
|
assert user_file.size is not None
|
2024-01-29 00:52:43 +01:00
|
|
|
if user_file.size > settings.MAX_AVATAR_FILE_SIZE_MIB * 1024 * 1024:
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(
|
2023-07-17 22:40:33 +02:00
|
|
|
_("Uploaded file is larger than the allowed limit of {max_size} MiB").format(
|
|
|
|
max_size=settings.MAX_AVATAR_FILE_SIZE_MIB,
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
|
|
|
)
|
2024-06-25 23:27:26 +02:00
|
|
|
upload_avatar_image(user_file, user_profile)
|
2020-06-29 12:47:44 +02:00
|
|
|
do_change_avatar_fields(user_profile, UserProfile.AVATAR_FROM_USER, acting_user=user_profile)
|
2015-11-23 17:09:21 +01:00
|
|
|
user_avatar_url = avatar_url(user_profile)
|
|
|
|
|
|
|
|
json_result = dict(
|
2021-02-12 08:19:30 +01:00
|
|
|
avatar_url=user_avatar_url,
|
2015-11-23 17:09:21 +01:00
|
|
|
)
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request, data=json_result)
|
2015-11-23 17:09:21 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2017-11-27 09:28:57 +01:00
|
|
|
def delete_avatar_backend(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
|
2019-04-23 04:51:04 +02:00
|
|
|
if avatar_changes_disabled(user_profile.realm) and not user_profile.is_realm_admin:
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(str(AVATAR_CHANGES_DISABLED_ERROR))
|
2019-04-23 04:51:04 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
do_change_avatar_fields(
|
|
|
|
user_profile, UserProfile.AVATAR_FROM_GRAVATAR, acting_user=user_profile
|
|
|
|
)
|
2016-12-21 18:34:03 +01:00
|
|
|
gravatar_url = avatar_url(user_profile)
|
|
|
|
|
|
|
|
json_result = dict(
|
2021-02-12 08:19:30 +01:00
|
|
|
avatar_url=gravatar_url,
|
2016-12-21 18:34:03 +01:00
|
|
|
)
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request, data=json_result)
|
2016-12-21 18:34:03 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2017-10-28 00:16:13 +02:00
|
|
|
# We don't use @human_users_only here, because there are use cases for
|
|
|
|
# a bot regenerating its own API key.
|
2024-07-30 10:17:33 +02:00
|
|
|
@typed_endpoint_without_parameters
|
2017-11-27 09:28:57 +01:00
|
|
|
def regenerate_api_key(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
|
2018-08-10 21:03:32 +02:00
|
|
|
new_api_key = do_regenerate_api_key(user_profile, user_profile)
|
2015-11-23 17:09:21 +01:00
|
|
|
json_result = dict(
|
2021-02-12 08:19:30 +01:00
|
|
|
api_key=new_api_key,
|
2015-11-23 17:09:21 +01:00
|
|
|
)
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request, data=json_result)
|