2018-04-24 03:47:28 +02:00
|
|
|
from typing import Optional, Any, Dict
|
2015-11-23 17:09:21 +01:00
|
|
|
|
2016-05-25 15:02:02 +02:00
|
|
|
from django.utils.translation import ugettext as _
|
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
|
2016-06-05 02:15:26 +02:00
|
|
|
from django.http import HttpRequest, HttpResponse
|
2019-02-02 23:53:22 +01:00
|
|
|
from django.shortcuts import render
|
2015-11-23 17:09:21 +01:00
|
|
|
|
2017-11-04 00:59:22 +01:00
|
|
|
from zerver.decorator import has_request_variables, \
|
2019-02-02 23:53:22 +01:00
|
|
|
REQ, human_users_only
|
2017-12-27 13:17:58 +01:00
|
|
|
from zerver.lib.actions import do_change_password, do_change_notification_settings, \
|
2018-04-28 22:35:18 +02:00
|
|
|
do_change_enter_sends, do_regenerate_api_key, do_change_avatar_fields, \
|
2018-08-02 08:47:13 +02:00
|
|
|
do_set_user_display_setting, validate_email, do_change_user_delivery_email, \
|
2019-02-02 23:53:22 +01:00
|
|
|
do_start_email_change_process, check_change_full_name, \
|
2018-01-11 21:36:11 +01:00
|
|
|
get_available_notification_sounds
|
2015-11-23 17:09:21 +01:00
|
|
|
from zerver.lib.avatar import avatar_url
|
2017-07-11 06:13:23 +02:00
|
|
|
from zerver.lib.send_email import send_email, FromAddress
|
2016-08-05 22:28:25 +02:00
|
|
|
from zerver.lib.i18n import get_available_language_codes
|
2015-11-23 17:09:21 +01:00
|
|
|
from zerver.lib.response import json_success, json_error
|
|
|
|
from zerver.lib.upload import upload_avatar_image
|
2019-03-17 14:48:51 +01:00
|
|
|
from zerver.lib.validator import check_bool, check_string, check_int
|
2016-07-31 10:02:42 +02:00
|
|
|
from zerver.lib.request import JsonableError
|
2017-03-14 10:53:09 +01:00
|
|
|
from zerver.lib.timezone import get_all_timezones
|
2019-04-23 04:51:04 +02:00
|
|
|
from zerver.models import UserProfile, name_changes_disabled, avatar_changes_disabled
|
2017-07-22 00:27:45 +02:00
|
|
|
from confirmation.models import get_object_from_key, render_confirmation_key_error, \
|
2017-11-01 21:07:39 +01:00
|
|
|
ConfirmationKeyException, Confirmation
|
2018-05-29 07:25:08 +02:00
|
|
|
from zproject.backends import email_belongs_to_ldap
|
2017-01-20 12:27:38 +01:00
|
|
|
|
2019-04-23 04:51:04 +02:00
|
|
|
AVATAR_CHANGES_DISABLED_ERROR = _("Avatar changes are disabled in this organization.")
|
|
|
|
|
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:
|
2017-11-07 20:45:11 +01:00
|
|
|
email_change_object = get_object_from_key(confirmation_key, Confirmation.EMAIL_CHANGE)
|
2017-07-22 00:27:45 +02:00
|
|
|
except ConfirmationKeyException as exception:
|
|
|
|
return render_confirmation_key_error(request, exception)
|
2017-01-20 12:27:38 +01:00
|
|
|
|
2017-11-07 20:45:11 +01:00
|
|
|
new_email = email_change_object.new_email
|
|
|
|
old_email = email_change_object.old_email
|
|
|
|
user_profile = email_change_object.user_profile
|
2017-01-20 12:27:38 +01:00
|
|
|
|
2018-02-02 16:54:26 +01:00
|
|
|
if user_profile.realm.email_changes_disabled and not user_profile.is_realm_admin:
|
2017-11-07 20:48:32 +01:00
|
|
|
raise JsonableError(_("Email address changes are disabled in this organization."))
|
2018-08-02 08:47:13 +02:00
|
|
|
|
|
|
|
do_change_user_delivery_email(user_profile, new_email)
|
2017-07-22 00:27:45 +02:00
|
|
|
|
2018-04-30 17:35:21 +02:00
|
|
|
context = {'realm_name': user_profile.realm.name, 'new_email': new_email}
|
2018-12-03 23:26:51 +01:00
|
|
|
send_email('zerver/emails/notify_change_in_email', to_emails=[old_email],
|
2017-07-22 00:27:45 +02:00
|
|
|
from_name="Zulip Account Security", from_address=FromAddress.SUPPORT,
|
2018-12-14 08:41:42 +01:00
|
|
|
language=user_profile.default_language, context=context)
|
2017-01-20 12:27:38 +01:00
|
|
|
|
|
|
|
ctx = {
|
|
|
|
'new_email': new_email,
|
|
|
|
'old_email': old_email,
|
|
|
|
}
|
|
|
|
return render(request, 'confirmation/confirm_email_change.html', context=ctx)
|
2015-11-23 17:09:21 +01:00
|
|
|
|
2017-07-31 20:44:52 +02:00
|
|
|
@human_users_only
|
2015-11-23 17:09:21 +01:00
|
|
|
@has_request_variables
|
2017-12-04 09:56:07 +01:00
|
|
|
def json_change_settings(request: HttpRequest, user_profile: UserProfile,
|
2018-04-24 03:47:28 +02:00
|
|
|
full_name: str=REQ(default=""),
|
|
|
|
email: str=REQ(default=""),
|
|
|
|
old_password: str=REQ(default=""),
|
|
|
|
new_password: str=REQ(default="")) -> HttpResponse:
|
2017-01-20 12:27:38 +01:00
|
|
|
if not (full_name or new_password or email):
|
2018-01-11 20:28:44 +01:00
|
|
|
return json_error(_("Please fill out all fields."))
|
2016-07-23 22:22:25 +02:00
|
|
|
|
2018-01-11 20:28:18 +01:00
|
|
|
if new_password != "":
|
2018-05-29 07:25:08 +02:00
|
|
|
return_data = {} # type: Dict[str, Any]
|
2018-08-02 08:47:13 +02:00
|
|
|
if email_belongs_to_ldap(user_profile.realm, user_profile.delivery_email):
|
2018-05-29 07:25:08 +02:00
|
|
|
return json_error(_("Your Zulip password is managed in LDAP"))
|
2018-08-02 08:47:13 +02:00
|
|
|
if not authenticate(username=user_profile.delivery_email, password=old_password,
|
2018-05-29 07:25:08 +02:00
|
|
|
realm=user_profile.realm, return_data=return_data):
|
2016-05-25 15:02:02 +02:00
|
|
|
return json_error(_("Wrong password!"))
|
2015-11-23 17:09:21 +01:00
|
|
|
do_change_password(user_profile, new_password)
|
2016-11-17 08:56:01 +01:00
|
|
|
# In Django 1.10, password changes invalidates sessions, see
|
|
|
|
# https://docs.djangoproject.com/en/1.10/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
|
|
|
|
2017-05-22 21:07:35 +02:00
|
|
|
result = {} # type: Dict[str, Any]
|
2017-01-20 12:27:38 +01:00
|
|
|
new_email = email.strip()
|
2018-08-02 08:47:13 +02:00
|
|
|
if user_profile.delivery_email != new_email and new_email != '':
|
2018-02-02 16:54:26 +01:00
|
|
|
if user_profile.realm.email_changes_disabled and not user_profile.is_realm_admin:
|
2017-03-05 02:18:42 +01:00
|
|
|
return json_error(_("Email address changes are disabled in this organization."))
|
2017-01-20 12:27:38 +01:00
|
|
|
error, skipped = validate_email(user_profile, new_email)
|
2017-09-25 07:18:42 +02:00
|
|
|
if error:
|
|
|
|
return json_error(error)
|
|
|
|
if skipped:
|
|
|
|
return json_error(skipped)
|
2017-01-20 12:27:38 +01:00
|
|
|
|
|
|
|
do_start_email_change_process(user_profile, new_email)
|
2017-10-02 23:27:22 +02:00
|
|
|
result['account_email'] = _("Check your email for a confirmation link. ")
|
2017-01-20 12:27:38 +01:00
|
|
|
|
2015-11-23 17:09:21 +01:00
|
|
|
if user_profile.full_name != full_name and full_name.strip() != "":
|
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
|
2017-04-07 07:28:28 +02:00
|
|
|
result['full_name'] = check_change_full_name(user_profile, full_name, user_profile)
|
2015-11-23 17:09:21 +01:00
|
|
|
|
|
|
|
return json_success(result)
|
|
|
|
|
2017-04-16 22:10:56 +02:00
|
|
|
@human_users_only
|
2016-06-23 11:32:45 +02:00
|
|
|
@has_request_variables
|
2017-11-16 02:37:54 +01:00
|
|
|
def update_display_settings_backend(
|
|
|
|
request: HttpRequest, user_profile: UserProfile,
|
|
|
|
twenty_four_hour_time: Optional[bool]=REQ(validator=check_bool, default=None),
|
2018-05-24 20:53:26 +02:00
|
|
|
dense_mode: Optional[bool]=REQ(validator=check_bool, default=None),
|
2018-08-17 08:09:07 +02:00
|
|
|
starred_message_counts: Optional[bool]=REQ(validator=check_bool, default=None),
|
2019-04-16 18:46:17 +02:00
|
|
|
fluid_layout_width: Optional[bool]=REQ(validator=check_bool, default=None),
|
2017-11-16 02:37:54 +01:00
|
|
|
high_contrast_mode: Optional[bool]=REQ(validator=check_bool, default=None),
|
|
|
|
night_mode: Optional[bool]=REQ(validator=check_bool, default=None),
|
2018-01-15 19:36:32 +01:00
|
|
|
translate_emoticons: Optional[bool]=REQ(validator=check_bool, default=None),
|
2017-11-16 02:37:54 +01:00
|
|
|
default_language: Optional[bool]=REQ(validator=check_string, default=None),
|
|
|
|
left_side_userlist: Optional[bool]=REQ(validator=check_bool, default=None),
|
|
|
|
emojiset: Optional[str]=REQ(validator=check_string, default=None),
|
2019-03-17 14:48:51 +01:00
|
|
|
demote_inactive_streams: Optional[int]=REQ(validator=check_int, default=None),
|
2017-11-16 02:37:54 +01:00
|
|
|
timezone: Optional[str]=REQ(validator=check_string, default=None)) -> HttpResponse:
|
|
|
|
|
2016-12-22 15:58:18 +01:00
|
|
|
if (default_language is not None and
|
|
|
|
default_language not in get_available_language_codes()):
|
2019-04-20 03:49:03 +02:00
|
|
|
raise JsonableError(_("Invalid language '%s'") % (default_language,))
|
2016-12-22 15:58:18 +01:00
|
|
|
|
2017-03-14 10:53:09 +01:00
|
|
|
if (timezone is not None and
|
|
|
|
timezone not in get_all_timezones()):
|
2019-04-20 03:49:03 +02:00
|
|
|
raise JsonableError(_("Invalid timezone '%s'") % (timezone,))
|
2017-03-14 10:53:09 +01:00
|
|
|
|
2017-04-02 21:05:33 +02:00
|
|
|
if (emojiset is not None and
|
2019-09-28 01:06:46 +02:00
|
|
|
emojiset not in [emojiset_choice['key'] for emojiset_choice in UserProfile.emojiset_choices()]):
|
2019-04-20 03:49:03 +02:00
|
|
|
raise JsonableError(_("Invalid emojiset '%s'") % (emojiset,))
|
2017-04-02 21:05:33 +02:00
|
|
|
|
2019-03-17 14:48:51 +01:00
|
|
|
if (demote_inactive_streams is not None and
|
|
|
|
demote_inactive_streams not in UserProfile.DEMOTE_STREAMS_CHOICES):
|
|
|
|
raise JsonableError(_("Invalid setting value '%s'") % (demote_inactive_streams,))
|
|
|
|
|
2017-05-22 21:07:35 +02:00
|
|
|
request_settings = {k: v for k, v in list(locals().items()) if k in user_profile.property_types}
|
2017-05-17 22:16:38 +02:00
|
|
|
result = {} # type: Dict[str, Any]
|
2017-05-22 21:07:35 +02:00
|
|
|
for k, v in list(request_settings.items()):
|
|
|
|
if v is not None and getattr(user_profile, k) != v:
|
|
|
|
do_set_user_display_setting(user_profile, k, v)
|
|
|
|
result[k] = v
|
2017-03-14 10:53:09 +01:00
|
|
|
|
2016-06-23 11:32:45 +02:00
|
|
|
return json_success(result)
|
|
|
|
|
2017-04-16 22:10:56 +02:00
|
|
|
@human_users_only
|
2015-11-23 17:09:21 +01:00
|
|
|
@has_request_variables
|
2017-12-04 09:56:07 +01:00
|
|
|
def json_change_notify_settings(
|
|
|
|
request: HttpRequest, user_profile: UserProfile,
|
|
|
|
enable_stream_desktop_notifications: Optional[bool]=REQ(validator=check_bool, default=None),
|
|
|
|
enable_stream_email_notifications: Optional[bool]=REQ(validator=check_bool, default=None),
|
|
|
|
enable_stream_push_notifications: Optional[bool]=REQ(validator=check_bool, default=None),
|
2019-06-11 08:47:49 +02:00
|
|
|
enable_stream_audible_notifications: Optional[bool]=REQ(validator=check_bool, default=None),
|
2019-09-03 23:27:45 +02:00
|
|
|
wildcard_mentions_notify: Optional[bool]=REQ(validator=check_bool, default=None),
|
2018-01-11 21:36:11 +01:00
|
|
|
notification_sound: Optional[str]=REQ(validator=check_string, default=None),
|
2017-12-04 09:56:07 +01:00
|
|
|
enable_desktop_notifications: Optional[bool]=REQ(validator=check_bool, default=None),
|
|
|
|
enable_sounds: Optional[bool]=REQ(validator=check_bool, default=None),
|
|
|
|
enable_offline_email_notifications: Optional[bool]=REQ(validator=check_bool, default=None),
|
|
|
|
enable_offline_push_notifications: Optional[bool]=REQ(validator=check_bool, default=None),
|
|
|
|
enable_online_push_notifications: Optional[bool]=REQ(validator=check_bool, default=None),
|
|
|
|
enable_digest_emails: Optional[bool]=REQ(validator=check_bool, default=None),
|
2018-08-24 07:28:51 +02:00
|
|
|
enable_login_emails: Optional[bool]=REQ(validator=check_bool, default=None),
|
2017-11-29 13:42:39 +01:00
|
|
|
message_content_in_email_notifications: Optional[bool]=REQ(validator=check_bool, default=None),
|
2018-01-06 23:30:43 +01:00
|
|
|
pm_content_in_desktop_notifications: Optional[bool]=REQ(validator=check_bool, default=None),
|
2019-06-29 22:00:44 +02:00
|
|
|
desktop_icon_count_display: Optional[int]=REQ(validator=check_int, default=None),
|
2017-11-29 13:42:39 +01:00
|
|
|
realm_name_in_notifications: Optional[bool]=REQ(validator=check_bool, default=None)
|
|
|
|
) -> HttpResponse:
|
2015-11-23 17:09:21 +01:00
|
|
|
result = {}
|
|
|
|
|
|
|
|
# Stream notification settings.
|
|
|
|
|
2018-01-11 21:36:11 +01:00
|
|
|
if (notification_sound is not None and
|
|
|
|
notification_sound not in get_available_notification_sounds()):
|
|
|
|
raise JsonableError(_("Invalid notification sound '%s'") % (notification_sound,))
|
|
|
|
|
2017-05-23 03:19:21 +02:00
|
|
|
req_vars = {k: v for k, v in list(locals().items()) if k in user_profile.notification_setting_types}
|
2015-11-23 17:09:21 +01:00
|
|
|
|
2017-05-23 03:19:21 +02:00
|
|
|
for k, v in list(req_vars.items()):
|
|
|
|
if v is not None and getattr(user_profile, k) != v:
|
|
|
|
do_change_notification_settings(user_profile, k, v)
|
|
|
|
result[k] = v
|
2016-12-07 17:29:12 +01:00
|
|
|
|
2015-11-23 17:09:21 +01:00
|
|
|
return json_success(result)
|
|
|
|
|
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:
|
2016-05-25 15:02:02 +02:00
|
|
|
return json_error(_("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:
|
|
|
|
return json_error(AVATAR_CHANGES_DISABLED_ERROR)
|
|
|
|
|
2016-01-25 01:27:18 +01:00
|
|
|
user_file = list(request.FILES.values())[0]
|
2017-03-06 06:22:28 +01:00
|
|
|
if ((settings.MAX_AVATAR_FILE_SIZE * 1024 * 1024) < user_file.size):
|
|
|
|
return json_error(_("Uploaded file is larger than the allowed limit of %s MB") % (
|
|
|
|
settings.MAX_AVATAR_FILE_SIZE))
|
2017-03-02 16:21:46 +01:00
|
|
|
upload_avatar_image(user_file, user_profile, user_profile)
|
2017-01-28 19:05:20 +01:00
|
|
|
do_change_avatar_fields(user_profile, UserProfile.AVATAR_FROM_USER)
|
2015-11-23 17:09:21 +01:00
|
|
|
user_avatar_url = avatar_url(user_profile)
|
|
|
|
|
|
|
|
json_result = dict(
|
|
|
|
avatar_url = user_avatar_url
|
|
|
|
)
|
|
|
|
return json_success(json_result)
|
|
|
|
|
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:
|
|
|
|
return json_error(AVATAR_CHANGES_DISABLED_ERROR)
|
|
|
|
|
2017-01-28 19:05:20 +01:00
|
|
|
do_change_avatar_fields(user_profile, UserProfile.AVATAR_FROM_GRAVATAR)
|
2016-12-21 18:34:03 +01:00
|
|
|
gravatar_url = avatar_url(user_profile)
|
|
|
|
|
|
|
|
json_result = dict(
|
|
|
|
avatar_url = gravatar_url
|
|
|
|
)
|
|
|
|
return json_success(json_result)
|
|
|
|
|
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.
|
2015-11-23 17:09:21 +01:00
|
|
|
@has_request_variables
|
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(
|
2018-08-10 21:03:32 +02:00
|
|
|
api_key = new_api_key
|
2015-11-23 17:09:21 +01:00
|
|
|
)
|
|
|
|
return json_success(json_result)
|
|
|
|
|
2017-10-28 00:16:13 +02:00
|
|
|
@human_users_only
|
2015-11-23 17:09:21 +01:00
|
|
|
@has_request_variables
|
2017-12-04 09:56:07 +01:00
|
|
|
def change_enter_sends(request: HttpRequest, user_profile: UserProfile,
|
|
|
|
enter_sends: bool=REQ(validator=check_bool)) -> HttpResponse:
|
2015-11-23 17:09:21 +01:00
|
|
|
do_change_enter_sends(user_profile, enter_sends)
|
|
|
|
return json_success()
|