def update_display_settings_backend( request: HttpRequest, user_profile: UserProfile, twenty_four_hour_time: Optional[bool] = REQ(validator=check_bool, default=None), high_contrast_mode: Optional[bool] = REQ(validator=check_bool, default=None), night_mode: Optional[bool] = REQ(validator=check_bool, default=None), default_language: Optional[bool] = REQ(validator=check_string, default=None), left_side_userlist: Optional[bool] = REQ(validator=check_bool, default=None), emoji_alt_code: Optional[bool] = REQ(validator=check_bool, default=None), emojiset: Optional[str] = REQ(validator=check_string, default=None), timezone: Optional[str] = REQ(validator=check_string, default=None) ) -> HttpResponse: if (default_language is not None and default_language not in get_available_language_codes()): raise JsonableError(_("Invalid language '%s'" % (default_language, ))) if (timezone is not None and timezone not in get_all_timezones()): raise JsonableError(_("Invalid timezone '%s'" % (timezone, ))) if (emojiset is not None and emojiset not in UserProfile.emojiset_choices()): raise JsonableError(_("Invalid emojiset '%s'" % (emojiset, ))) request_settings = { k: v for k, v in list(locals().items()) if k in user_profile.property_types } result = {} # type: Dict[str, Any] 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 return json_success(result)
def get_message_edit_history( request: HttpRequest, user_profile: UserProfile, message_id: int = REQ(converter=to_non_negative_int, path_only=True) ) -> HttpResponse: if not user_profile.realm.allow_edit_history: return json_error( _("Message edit history is disabled in this organization")) message, ignored_user_message = access_message(user_profile, message_id) # Extract the message edit history from the message if message.edit_history is not None: message_edit_history = orjson.loads(message.edit_history) else: message_edit_history = [] # Fill in all the extra data that will make it usable fill_edit_history_entries(message_edit_history, message) return json_success( {"message_history": list(reversed(message_edit_history))})
def api_pivotal_webhook(request, user_profile, client, stream=REQ()): # type: (HttpRequest, UserProfile, Client, Text) -> HttpResponse subject = content = None try: subject, content = api_pivotal_webhook_v3(request, user_profile, stream) except AttributeError: return json_error(_("Failed to extract data from Pivotal XML response")) except Exception: # Attempt to parse v5 JSON payload try: subject, content = api_pivotal_webhook_v5(request, user_profile, stream) except AttributeError: return json_error(_("Failed to extract data from Pivotal V5 JSON response")) if subject is None or content is None: return json_error(_("Unable to handle Pivotal payload")) check_send_message(user_profile, client, "stream", [stream], subject, content) return json_success()
def api_heroku_webhook(request, user_profile, stream=REQ(default="heroku"), head=REQ(), app=REQ(), user=REQ(), url=REQ(), git_log=REQ()): # type: (HttpRequest, UserProfile, Text, Text, Text, Text, Text, Text) -> HttpResponse template = "{} deployed version {} of [{}]({})\n> {}" content = template.format(user, head, app, url, git_log) check_send_message(user_profile, request.client, "stream", [stream], app, content) return json_success()
def api_transifex_webhook(request, user_profile, client, project=REQ(), resource=REQ(), language=REQ(), translated=REQ(default=None), reviewed=REQ(default=None), stream=REQ(default='transifex')): # type: (HttpRequest, UserProfile, Client, str, str, str, Optional[int], Optional[int], str) -> HttpResponse subject = "{} in {}".format(project, language) if translated: body = "Resource {} fully translated.".format(resource) elif reviewed: body = "Resource {} fully reviewed.".format(resource) else: return json_error(_("Transifex wrong request")) check_send_message(user_profile, client, 'stream', [stream], subject, body) return json_success()
def update_user_custom_profile_data( request, user_profile, data=REQ(validator=check_list(check_dict([('id', check_int)])))): # type: (HttpRequest, UserProfile, List[Dict[str, Union[int, Text]]]) -> HttpResponse for item in data: field_id = item['id'] try: field = CustomProfileField.objects.get(id=field_id) except CustomProfileField.DoesNotExist: return json_error( _('Field id {id} not found.').format(id=field_id)) validator = CustomProfileField.FIELD_VALIDATORS[field.field_type] result = validator('value[{}]'.format(field_id), item['value']) if result is not None: return json_error(result) do_update_user_custom_profile_data(user_profile, data) # We need to call this explicitly otherwise constraints are not check return json_success()
def json_report_error(request, user_profile, message=REQ(), stacktrace=REQ(), ui_message=REQ(validator=check_bool), user_agent=REQ(), href=REQ(), log=REQ(), more_info=REQ(validator=check_dict([]), default=None)): # type: (HttpRequest, UserProfile, Text, Text, bool, Text, Text, Text, Dict[str, Any]) -> HttpResponse """Accepts an error report and stores in a queue for processing. The actual error reports are later handled by do_report_error (below)""" if not settings.BROWSER_ERROR_REPORTING: return json_success() if js_source_map: stacktrace = js_source_map.annotate_stacktrace(stacktrace) try: version = subprocess.check_output( ["git", "log", "HEAD^..HEAD", "--oneline"], universal_newlines=True) except Exception: version = None queue_json_publish( 'error_reports', dict(type="browser", report=dict( user_email=user_profile.email, user_full_name=user_profile.full_name, user_visible=ui_message, server_path=settings.DEPLOY_ROOT, version=version, user_agent=user_agent, href=href, message=message, stacktrace=stacktrace, log=log, more_info=more_info, )), lambda x: None) return json_success()
def api_zabbix_webhook( request: HttpRequest, user_profile: UserProfile, payload: Dict[str, Any] = REQ(argument_type="body"), ) -> HttpResponse: try: body = get_body_for_http_request(payload) subject = get_subject_for_http_request(payload) except KeyError: message = MISCONFIGURED_PAYLOAD_ERROR_MESSAGE.format( bot_name=user_profile.full_name, support_email=FromAddress.SUPPORT, ).strip() send_rate_limited_pm_notification_to_bot_owner(user_profile, user_profile.realm, message) raise JsonableError(_("Invalid payload")) check_send_webhook_message(request, user_profile, subject, body) return json_success()
def json_report_error(request, user_profile, message=REQ(), stacktrace=REQ(), ui_message=REQ(validator=check_bool), user_agent=REQ(), href=REQ(), log=REQ(), more_info=REQ(validator=check_dict([]), default=None)): if not settings.ERROR_REPORTING: return json_success() if js_source_map: stacktrace = js_source_map.annotate_stacktrace(stacktrace) try: version = subprocess.check_output( ["git", "log", "HEAD^..HEAD", "--oneline"]) except Exception: version = None queue_json_publish( 'error_reports', dict(type="browser", report=dict( user_email=user_profile.email, user_full_name=user_profile.full_name, user_visible=ui_message, server_path=settings.DEPLOY_ROOT, version=version, user_agent=user_agent, href=href, message=message, stacktrace=stacktrace, log=log, more_info=more_info, )), lambda x: None) return json_success()
def update_message_flags(request, user_profile, messages=REQ(validator=check_list(check_int)), operation=REQ('op'), flag=REQ(), all=REQ(validator=check_bool, default=False), stream_name=REQ(default=None), topic_name=REQ(default=None)): # type: (HttpRequest, UserProfile, List[int], text_type, text_type, bool, Optional[text_type], Optional[text_type]) -> HttpResponse if all: target_count_str = "all" else: target_count_str = str(len(messages)) log_data_str = "[%s %s/%s]" % (operation, flag, target_count_str) request._log_data["extra"] = log_data_str stream = None if stream_name is not None: stream = get_stream(stream_name, user_profile.realm) if not stream: raise JsonableError(_('No such stream \'%s\'') % (stream_name, )) if topic_name: topic_exists = UserMessage.objects.filter( user_profile=user_profile, message__recipient__type_id=stream.id, message__recipient__type=Recipient.STREAM, message__subject__iexact=topic_name).exists() if not topic_exists: raise JsonableError(_('No such topic \'%s\'') % (topic_name, )) count = do_update_message_flags(user_profile, operation, flag, messages, all, stream, topic_name) # If we succeed, update log data str with the actual count for how # many messages were updated. if count != len(messages): log_data_str = "[%s %s/%s] actually %s" % (operation, flag, target_count_str, count) request._log_data["extra"] = log_data_str return json_success({'result': 'success', 'messages': messages, 'msg': ''})
def update_message_flags(request, user_profile, messages=REQ('messages', validator=check_list(check_int)), operation=REQ('op'), flag=REQ('flag'), all=REQ('all', validator=check_bool, default=False), stream_name=REQ('stream_name', default=None), topic_name=REQ('topic_name', default=None)): request._log_data["extra"] = "[%s %s]" % (operation, flag) stream = None if stream_name is not None: stream = get_stream(stream_name, user_profile.realm) if not stream: raise JsonableError(_('No such stream \'%s\'') % (stream_name,)) if topic_name: topic_exists = UserMessage.objects.filter(user_profile=user_profile, message__recipient__type_id=stream.id, message__recipient__type=Recipient.STREAM, message__subject__iexact=topic_name).exists() if not topic_exists: raise JsonableError(_('No such topic \'%s\'') % (topic_name,)) do_update_message_flags(user_profile, operation, flag, messages, all, stream, topic_name) return json_success({'result': 'success', 'messages': messages, 'msg': ''})
def change_enter_sends(request, user_profile, enter_sends=REQ(validator=check_bool)): # type: (HttpRequest, UserProfile, bool) -> HttpResponse do_change_enter_sends(user_profile, enter_sends) return json_success()
def json_change_settings( request: HttpRequest, user_profile: UserProfile, full_name: str = REQ(default=""), email: str = REQ(default=""), old_password: str = REQ(default=""), new_password: str = REQ(default=""), ) -> HttpResponse: if not (full_name or new_password or email): return json_error(_("Please fill out all fields.")) if new_password != "": return_data: Dict[str, Any] = {} if email_belongs_to_ldap(user_profile.realm, user_profile.delivery_email): return json_error(_("Your Zulip password is managed in LDAP")) try: if not authenticate( request, username=user_profile.delivery_email, password=old_password, realm=user_profile.realm, return_data=return_data, ): return json_error(_("Wrong password!")) except RateLimited as e: assert e.secs_to_freedom is not None secs_to_freedom = int(e.secs_to_freedom) return json_error( _("You're making too many attempts! Try again in {} seconds."). format(secs_to_freedom), ) if not check_password_strength(new_password): return json_error(_("New password is too weak!")) do_change_password(user_profile, new_password) # In Django 1.10, password changes invalidates sessions, see # https://docs.djangoproject.com/en/1.10/topics/auth/default/#session-invalidation-on-password-change # for details. To avoid this logging the user out of their own # session (which would provide a confusing UX at best), we # update the session hash here. update_session_auth_hash(request, user_profile) # 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() result: Dict[str, Any] = {} new_email = email.strip() if user_profile.delivery_email != new_email and new_email != "": if user_profile.realm.email_changes_disabled and not user_profile.is_realm_admin: return json_error( _("Email address changes are disabled in this organization.")) error = validate_email_is_valid( new_email, get_realm_email_validator(user_profile.realm), ) if error: return json_error(error) try: validate_email_not_already_in_realm( user_profile.realm, new_email, verbose=False, ) except ValidationError as e: return json_error(e.message) do_start_email_change_process(user_profile, new_email) result["account_email"] = _( "Check your email for a confirmation link. ") if user_profile.full_name != full_name and full_name.strip() != "": if name_changes_disabled( user_profile.realm) and not user_profile.is_realm_admin: # 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: # Note that check_change_full_name strips the passed name automatically result["full_name"] = check_change_full_name( user_profile, full_name, user_profile) return json_success(result)
def update_display_settings_backend( request: HttpRequest, user_profile: UserProfile, twenty_four_hour_time: Optional[bool] = REQ(json_validator=check_bool, default=None), dense_mode: Optional[bool] = REQ(json_validator=check_bool, default=None), starred_message_counts: Optional[bool] = REQ(json_validator=check_bool, default=None), fluid_layout_width: Optional[bool] = REQ(json_validator=check_bool, default=None), high_contrast_mode: Optional[bool] = REQ(json_validator=check_bool, default=None), color_scheme: Optional[int] = REQ(json_validator=check_int_in( UserProfile.COLOR_SCHEME_CHOICES), default=None), translate_emoticons: Optional[bool] = REQ(json_validator=check_bool, default=None), default_language: Optional[str] = REQ(default=None), default_view: Optional[str] = REQ( str_validator=check_string_in(default_view_options), default=None), left_side_userlist: Optional[bool] = REQ(json_validator=check_bool, default=None), emojiset: Optional[str] = REQ( str_validator=check_string_in(emojiset_choices), default=None), demote_inactive_streams: Optional[int] = REQ(json_validator=check_int_in( UserProfile.DEMOTE_STREAMS_CHOICES), default=None), timezone: Optional[str] = REQ(str_validator=check_string_in( pytz.all_timezones_set), default=None), ) -> HttpResponse: # We can't use REQ for this widget because # 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")) request_settings = { k: v for k, v in list(locals().items()) if k in user_profile.property_types } result: Dict[str, Any] = {} 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 return json_success(result)
def get_events_backend( request: HttpRequest, user_profile: UserProfile, # user_client is intended only for internal Django=>Tornado requests # and thus shouldn't be documented for external use. user_client: Optional[Client] = REQ(converter=get_client, default=None, intentionally_undocumented=True), last_event_id: Optional[int] = REQ(converter=int, default=None), queue_id: Optional[str] = REQ(default=None), # apply_markdown, client_gravatar, all_public_streams, and various # other parameters are only used when registering a new queue via this # endpoint. This is a feature used primarily by get_events_internal # and not expected to be used by third-party clients. apply_markdown: bool = REQ(default=False, validator=check_bool, intentionally_undocumented=True), client_gravatar: bool = REQ(default=False, validator=check_bool, intentionally_undocumented=True), slim_presence: bool = REQ(default=False, validator=check_bool, intentionally_undocumented=True), all_public_streams: bool = REQ(default=False, validator=check_bool, intentionally_undocumented=True), event_types: Optional[Sequence[str]] = REQ( default=None, validator=check_list(check_string), intentionally_undocumented=True), dont_block: bool = REQ(default=False, validator=check_bool), narrow: Iterable[Sequence[str]] = REQ(default=[], validator=check_list( check_list(check_string)), intentionally_undocumented=True), lifespan_secs: int = REQ(default=0, converter=to_non_negative_int, intentionally_undocumented=True), bulk_message_deletion: bool = REQ(default=False, validator=check_bool, intentionally_undocumented=True), ) -> HttpResponse: # Extract the Tornado handler from the request handler: AsyncDjangoHandler = request._tornado_handler if user_client is None: valid_user_client = request.client else: valid_user_client = user_client events_query = dict( user_profile_id=user_profile.id, queue_id=queue_id, last_event_id=last_event_id, event_types=event_types, client_type_name=valid_user_client.name, all_public_streams=all_public_streams, lifespan_secs=lifespan_secs, narrow=narrow, dont_block=dont_block, handler_id=handler.handler_id, ) if queue_id is None: events_query["new_queue_data"] = dict( user_profile_id=user_profile.id, realm_id=user_profile.realm_id, event_types=event_types, client_type_name=valid_user_client.name, apply_markdown=apply_markdown, client_gravatar=client_gravatar, slim_presence=slim_presence, all_public_streams=all_public_streams, queue_timeout=lifespan_secs, last_connection_time=time.time(), narrow=narrow, bulk_message_deletion=bulk_message_deletion, ) result = fetch_events(events_query) if "extra_log_data" in result: request._log_data["extra"] = result["extra_log_data"] if result["type"] == "async": # Mark this response with .asynchronous; this will result in # Tornado discarding the response and instead long-polling the # request. See zulip_finish for more design details. handler._request = request response = json_success() response.asynchronous = True return response if result["type"] == "error": raise result["exception"] return json_success(result["response"])
def add_bot_backend(request, user_profile, full_name_raw=REQ("full_name"), short_name=REQ(), default_sending_stream_name=REQ('default_sending_stream', default=None), default_events_register_stream_name=REQ( 'default_events_register_stream', default=None), default_all_public_streams=REQ(validator=check_bool, default=None)): # type: (HttpRequest, UserProfile, Text, Text, Optional[Text], Optional[Text], Optional[bool]) -> HttpResponse short_name += "-bot" full_name = check_full_name(full_name_raw) email = '%s@%s' % (short_name, user_profile.realm.get_bot_domain()) form = CreateUserForm({'full_name': full_name, 'email': email}) if not form.is_valid(): # We validate client-side as well return json_error(_('Bad name or username')) try: get_user_profile_by_email(email) return json_error(_("Username already in use")) except UserProfile.DoesNotExist: pass if len(request.FILES) == 0: avatar_source = UserProfile.AVATAR_FROM_GRAVATAR elif len(request.FILES) != 1: return json_error(_("You may only upload one file at a time")) else: avatar_source = UserProfile.AVATAR_FROM_USER default_sending_stream = None if default_sending_stream_name is not None: (default_sending_stream, ignored_rec, ignored_sub) = access_stream_by_name(user_profile, default_sending_stream_name) default_events_register_stream = None if default_events_register_stream_name is not None: (default_events_register_stream, ignored_rec, ignored_sub) = access_stream_by_name( user_profile, default_events_register_stream_name) bot_profile = do_create_user( email=email, password='', realm=user_profile.realm, full_name=full_name, short_name=short_name, active=True, bot_type=UserProfile.DEFAULT_BOT, bot_owner=user_profile, avatar_source=avatar_source, default_sending_stream=default_sending_stream, default_events_register_stream=default_events_register_stream, default_all_public_streams=default_all_public_streams) if len(request.FILES) == 1: user_file = list(request.FILES.values())[0] upload_avatar_image(user_file, user_profile, bot_profile) json_result = dict( api_key=bot_profile.api_key, avatar_url=avatar_url(bot_profile), default_sending_stream=get_stream_name( bot_profile.default_sending_stream), default_events_register_stream=get_stream_name( bot_profile.default_events_register_stream), default_all_public_streams=bot_profile.default_all_public_streams, ) return json_success(json_result)
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), enable_stream_sounds: Optional[bool]=REQ(validator=check_bool, default=None), 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), pm_content_in_desktop_notifications: Optional[bool]=REQ(validator=check_bool, default=None), realm_name_in_notifications: Optional[bool]=REQ(validator=check_bool, default=None)) \ -> HttpResponse: result = {} # Stream notification settings. req_vars = { k: v for k, v in list(locals().items()) if k in user_profile.notification_setting_types } 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 return json_success(result)
def remove_alert_words(request, user_profile, alert_words=REQ(validator=check_list(check_string), default=[])): do_remove_alert_words(user_profile, alert_words) return json_success()
def get_events_internal(request: HttpRequest, handler: BaseHandler, user_profile_id: int=REQ()) -> Union[HttpResponse, _RespondAsynchronously]: user_profile = get_user_profile_by_id(user_profile_id) request._email = user_profile.email process_client(request, user_profile, client_name="internal") return get_events_backend(request, user_profile, handler)
def get_events_backend(request: HttpRequest, user_profile: UserProfile, handler: BaseHandler, # user_client is intended only for internal Django=>Tornado requests # and thus shouldn't be documented for external use. user_client: Optional[Client]=REQ(converter=get_client, default=None, intentionally_undocumented=True), last_event_id: Optional[int]=REQ(converter=int, default=None), queue_id: Optional[str]=REQ(default=None), # apply_markdown, client_gravatar, all_public_streams, and various # other parameters are only used when registering a new queue via this # endpoint. This is a feature used primarily by get_events_internal # and not expected to be used by third-party clients. apply_markdown: bool=REQ(default=False, validator=check_bool, intentionally_undocumented=True), client_gravatar: bool=REQ(default=False, validator=check_bool, intentionally_undocumented=True), all_public_streams: bool=REQ(default=False, validator=check_bool, intentionally_undocumented=True), event_types: Optional[str]=REQ(default=None, validator=check_list(check_string), intentionally_undocumented=True), dont_block: bool=REQ(default=False, validator=check_bool), narrow: Iterable[Sequence[str]]=REQ(default=[], validator=check_list(None), intentionally_undocumented=True), lifespan_secs: int=REQ(default=0, converter=to_non_negative_int, intentionally_undocumented=True) ) -> Union[HttpResponse, _RespondAsynchronously]: if user_client is None: valid_user_client = request.client else: valid_user_client = user_client events_query = dict( user_profile_id = user_profile.id, user_profile_email = user_profile.email, queue_id = queue_id, last_event_id = last_event_id, event_types = event_types, client_type_name = valid_user_client.name, all_public_streams = all_public_streams, lifespan_secs = lifespan_secs, narrow = narrow, dont_block = dont_block, handler_id = handler.handler_id) if queue_id is None: events_query['new_queue_data'] = dict( user_profile_id = user_profile.id, realm_id = user_profile.realm_id, user_profile_email = user_profile.email, event_types = event_types, client_type_name = valid_user_client.name, apply_markdown = apply_markdown, client_gravatar = client_gravatar, all_public_streams = all_public_streams, queue_timeout = lifespan_secs, last_connection_time = time.time(), narrow = narrow) result = fetch_events(events_query) if "extra_log_data" in result: request._log_data['extra'] = result["extra_log_data"] if result["type"] == "async": handler._request = request return RespondAsynchronously if result["type"] == "error": raise result["exception"] return json_success(result["response"])
def json_change_notify_settings( request, user_profile, enable_stream_desktop_notifications=REQ(validator=check_bool, default=None), enable_stream_sounds=REQ(validator=check_bool, default=None), enable_desktop_notifications=REQ(validator=check_bool, default=None), enable_sounds=REQ(validator=check_bool, default=None), enable_offline_email_notifications=REQ(validator=check_bool, default=None), enable_offline_push_notifications=REQ(validator=check_bool, default=None), enable_online_push_notifications=REQ(validator=check_bool, default=None), enable_digest_emails=REQ(validator=check_bool, default=None), pm_content_in_desktop_notifications=REQ(validator=check_bool, default=None)): # type: (HttpRequest, UserProfile, Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool]) -> HttpResponse result = {} # Stream notification settings. if enable_stream_desktop_notifications is not None and \ user_profile.enable_stream_desktop_notifications != enable_stream_desktop_notifications: do_change_enable_stream_desktop_notifications( user_profile, enable_stream_desktop_notifications) result[ 'enable_stream_desktop_notifications'] = enable_stream_desktop_notifications if enable_stream_sounds is not None and \ user_profile.enable_stream_sounds != enable_stream_sounds: do_change_enable_stream_sounds(user_profile, enable_stream_sounds) result['enable_stream_sounds'] = enable_stream_sounds # PM and @-mention settings. if enable_desktop_notifications is not None and \ user_profile.enable_desktop_notifications != enable_desktop_notifications: do_change_enable_desktop_notifications(user_profile, enable_desktop_notifications) result['enable_desktop_notifications'] = enable_desktop_notifications if enable_sounds is not None and \ user_profile.enable_sounds != enable_sounds: do_change_enable_sounds(user_profile, enable_sounds) result['enable_sounds'] = enable_sounds if enable_offline_email_notifications is not None and \ user_profile.enable_offline_email_notifications != enable_offline_email_notifications: do_change_enable_offline_email_notifications( user_profile, enable_offline_email_notifications) result[ 'enable_offline_email_notifications'] = enable_offline_email_notifications if enable_offline_push_notifications is not None and \ user_profile.enable_offline_push_notifications != enable_offline_push_notifications: do_change_enable_offline_push_notifications( user_profile, enable_offline_push_notifications) result[ 'enable_offline_push_notifications'] = enable_offline_push_notifications if enable_online_push_notifications is not None and \ user_profile.enable_online_push_notifications != enable_online_push_notifications: do_change_enable_online_push_notifications( user_profile, enable_online_push_notifications) result[ 'enable_online_push_notifications'] = enable_online_push_notifications if enable_digest_emails is not None and \ user_profile.enable_digest_emails != enable_digest_emails: do_change_enable_digest_emails(user_profile, enable_digest_emails) result['enable_digest_emails'] = enable_digest_emails if pm_content_in_desktop_notifications is not None and \ user_profile.pm_content_in_desktop_notifications != pm_content_in_desktop_notifications: do_change_pm_content_in_desktop_notifications( user_profile, pm_content_in_desktop_notifications) result[ 'pm_content_in_desktop_notifications'] = pm_content_in_desktop_notifications return json_success(result)
def send_message_backend( request: HttpRequest, user_profile: UserProfile, message_type_name: str = REQ("type"), req_to: Optional[str] = REQ("to", default=None), forged_str: Optional[str] = REQ("forged", default=None, documentation_pending=True), topic_name: Optional[str] = REQ_topic(), message_content: str = REQ("content"), widget_content: Optional[str] = REQ(default=None, documentation_pending=True), realm_str: Optional[str] = REQ("realm_str", default=None, documentation_pending=True), local_id: Optional[str] = REQ(default=None), queue_id: Optional[str] = REQ(default=None), delivery_type: str = REQ("delivery_type", default="send_now", documentation_pending=True), defer_until: Optional[str] = REQ("deliver_at", default=None, documentation_pending=True), tz_guess: Optional[str] = REQ("tz_guess", default=None, documentation_pending=True), ) -> HttpResponse: # If req_to is None, then we default to an # empty list of recipients. message_to: Union[Sequence[int], Sequence[str]] = [] if req_to is not None: if message_type_name == "stream": stream_indicator = extract_stream_indicator(req_to) # For legacy reasons check_send_message expects # a list of streams, instead of a single stream. # # Also, mypy can't detect that a single-item # list populated from a Union[int, str] is actually # a Union[Sequence[int], Sequence[str]]. if isinstance(stream_indicator, int): message_to = [stream_indicator] else: message_to = [stream_indicator] else: message_to = extract_private_recipients(req_to) # Temporary hack: We're transitioning `forged` from accepting # `yes` to accepting `true` like all of our normal booleans. forged = forged_str is not None and forged_str in ["yes", "true"] client = request.client can_forge_sender = request.user.can_forge_sender if forged and not can_forge_sender: return json_error(_("User not authorized for this query")) realm = None if realm_str and realm_str != user_profile.realm.string_id: # The realm_str parameter does nothing, because it has to match # the user's realm - but we keep it around for backward compatibility. return json_error(_("User not authorized for this query")) if client.name in [ "zephyr_mirror", "irc_mirror", "jabber_mirror", "JabberMirror" ]: # Here's how security works for mirroring: # # For private messages, the message must be (1) both sent and # received exclusively by users in your realm, and (2) # received by the forwarding user. # # For stream messages, the message must be (1) being forwarded # by an API superuser for your realm and (2) being sent to a # mirrored stream. # # The most important security checks are in # `create_mirrored_message_users` below, which checks the # same-realm constraint. if "sender" not in request.POST: return json_error(_("Missing sender")) if message_type_name != "private" and not can_forge_sender: return json_error(_("User not authorized for this query")) # For now, mirroring only works with recipient emails, not for # recipient user IDs. if not all(isinstance(to_item, str) for to_item in message_to): return json_error( _("Mirroring not allowed with recipient user IDs")) # We need this manual cast so that mypy doesn't complain about # create_mirrored_message_users not being able to accept a Sequence[int] # type parameter. message_to = cast(Sequence[str], message_to) try: mirror_sender = create_mirrored_message_users( request, user_profile, message_to) except InvalidMirrorInput: return json_error(_("Invalid mirrored message")) if client.name == "zephyr_mirror" and not user_profile.realm.is_zephyr_mirror_realm: return json_error( _("Zephyr mirroring is not allowed in this organization")) sender = mirror_sender else: if "sender" in request.POST: return json_error(_("Invalid mirrored message")) sender = user_profile if (delivery_type == "send_later" or delivery_type == "remind") and defer_until is None: return json_error( _("Missing deliver_at in a request for delayed message delivery")) if (delivery_type == "send_later" or delivery_type == "remind") and defer_until is not None: return handle_deferred_message( sender, client, message_type_name, message_to, topic_name, message_content, delivery_type, defer_until, tz_guess, forwarder_user_profile=user_profile, realm=realm, ) ret = check_send_message( sender, client, message_type_name, message_to, topic_name, message_content, forged=forged, forged_timestamp=request.POST.get("time"), forwarder_user_profile=user_profile, realm=realm, local_id=local_id, sender_queue_id=queue_id, widget_content=widget_content, ) return json_success({"id": ret})
def json_change_notify_settings( request: HttpRequest, user_profile: UserProfile, enable_stream_desktop_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), enable_stream_email_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), enable_stream_push_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), enable_stream_audible_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), wildcard_mentions_notify: Optional[bool] = REQ(json_validator=check_bool, default=None), notification_sound: Optional[str] = REQ(default=None), enable_desktop_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), enable_sounds: Optional[bool] = REQ(json_validator=check_bool, default=None), enable_offline_email_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), enable_offline_push_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), enable_online_push_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), enable_digest_emails: Optional[bool] = REQ(json_validator=check_bool, default=None), enable_login_emails: Optional[bool] = REQ(json_validator=check_bool, default=None), enable_marketing_emails: Optional[bool] = REQ(json_validator=check_bool, default=None), message_content_in_email_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), pm_content_in_desktop_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), desktop_icon_count_display: Optional[int] = REQ(json_validator=check_int, default=None), realm_name_in_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), presence_enabled: Optional[bool] = REQ(json_validator=check_bool, default=None), ) -> HttpResponse: result = {} # Stream notification settings. if (notification_sound is not None and notification_sound not in get_available_notification_sounds() and notification_sound != "none"): raise JsonableError( _("Invalid notification sound '{}'").format(notification_sound)) req_vars = { k: v for k, v in list(locals().items()) if k in user_profile.notification_setting_types } 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, acting_user=user_profile) result[k] = v return json_success(result)
def zcommand_backend( request: HttpRequest, user_profile: UserProfile, command: str = REQ("command")) -> HttpResponse: return json_success(process_zcommands(command, user_profile))
def update_message_backend( request: HttpRequest, user_profile: UserMessage, message_id: int = REQ(converter=to_non_negative_int, path_only=True), stream_id: Optional[int] = REQ(converter=to_non_negative_int, default=None), topic_name: Optional[str] = REQ_topic(), propagate_mode: Optional[str] = REQ( default="change_one", str_validator=check_string_in(PROPAGATE_MODE_VALUES)), send_notification_to_old_thread: bool = REQ(default=True, validator=check_bool), send_notification_to_new_thread: bool = REQ(default=True, validator=check_bool), content: Optional[str] = REQ(default=None) ) -> HttpResponse: if not user_profile.realm.allow_message_editing: return json_error( _("Your organization has turned off message editing")) if propagate_mode != "change_one" and topic_name is None and stream_id is None: return json_error(_("Invalid propagate_mode without topic edit")) message, ignored_user_message = access_message(user_profile, message_id) is_no_topic_msg = (message.topic_name() == "(no topic)") # You only have permission to edit a message if: # you change this value also change those two parameters in message_edit.js. # 1. You sent it, OR: # 2. This is a topic-only edit for a (no topic) message, OR: # 3. This is a topic-only edit and you are an admin, OR: # 4. This is a topic-only edit and your realm allows users to edit topics. if message.sender == user_profile: pass elif (content is None) and (is_no_topic_msg or user_profile.is_realm_admin or user_profile.realm.allow_community_topic_editing): pass else: raise JsonableError( _("You don't have permission to edit this message")) # If there is a change to the content, check that it hasn't been too long # Allow an extra 20 seconds since we potentially allow editing 15 seconds # past the limit, and in case there are network issues, etc. The 15 comes # from (min_seconds_to_edit + seconds_left_buffer) in message_edit.js; if # you change this value also change those two parameters in message_edit.js. edit_limit_buffer = 20 if content is not None and user_profile.realm.message_content_edit_limit_seconds > 0: deadline_seconds = user_profile.realm.message_content_edit_limit_seconds + edit_limit_buffer if (timezone_now() - message.date_sent) > datetime.timedelta( seconds=deadline_seconds): raise JsonableError( _("The time limit for editing this message has passed")) # If there is a change to the topic, check that the user is allowed to # edit it and that it has not been too long. If this is not the user who # sent the message, they are not the admin, and the time limit for editing # topics is passed, raise an error. if content is None and message.sender != user_profile and not user_profile.is_realm_admin and \ not is_no_topic_msg: deadline_seconds = Realm.DEFAULT_COMMUNITY_TOPIC_EDITING_LIMIT_SECONDS + edit_limit_buffer if (timezone_now() - message.date_sent) > datetime.timedelta( seconds=deadline_seconds): raise JsonableError( _("The time limit for editing this message has passed")) if topic_name is None and content is None and stream_id is None: return json_error(_("Nothing to change")) if topic_name is not None: topic_name = topic_name.strip() if topic_name == "": raise JsonableError(_("Topic can't be empty")) rendered_content = None links_for_embed: Set[str] = set() prior_mention_user_ids: Set[int] = set() mention_user_ids: Set[int] = set() mention_data: Optional[bugdown.MentionData] = None if content is not None: content = content.strip() if content == "": content = "(deleted)" content = truncate_body(content) mention_data = bugdown.MentionData( realm_id=user_profile.realm.id, content=content, ) user_info = get_user_info_for_message_updates(message.id) prior_mention_user_ids = user_info['mention_user_ids'] # We render the message using the current user's realm; since # the cross-realm bots never edit messages, this should be # always correct. # Note: If rendering fails, the called code will raise a JsonableError. rendered_content = render_incoming_message( message, content, user_info['message_user_ids'], user_profile.realm, mention_data=mention_data) links_for_embed |= message.links_for_preview mention_user_ids = message.mentions_user_ids new_stream = None old_stream = None number_changed = 0 if stream_id is not None: if not user_profile.is_realm_admin: raise JsonableError( _("You don't have permission to move this message")) if content is not None: raise JsonableError( _("Cannot change message content while changing stream")) old_stream = get_stream_by_id(message.recipient.type_id) new_stream = get_stream_by_id(stream_id) if not (old_stream.is_public() and new_stream.is_public()): # We'll likely decide to relax this condition in the # future; it just requires more care with details like the # breadcrumb messages. raise JsonableError(_("Streams must be public")) number_changed = do_update_message( user_profile, message, new_stream, topic_name, propagate_mode, send_notification_to_old_thread, send_notification_to_new_thread, content, rendered_content, prior_mention_user_ids, mention_user_ids, mention_data) # Include the number of messages changed in the logs request._log_data['extra'] = f"[{number_changed}]" if links_for_embed: event_data = { 'message_id': message.id, 'message_content': message.content, # The choice of `user_profile.realm_id` rather than # `sender.realm_id` must match the decision made in the # `render_incoming_message` call earlier in this function. 'message_realm_id': user_profile.realm_id, 'urls': links_for_embed } queue_json_publish('embed_links', event_data) return json_success()
def update_display_settings_backend( request: HttpRequest, user_profile: UserProfile, twenty_four_hour_time: Optional[bool]=REQ(validator=check_bool, default=None), dense_mode: Optional[bool]=REQ(validator=check_bool, default=None), starred_message_counts: Optional[bool]=REQ(validator=check_bool, default=None), fluid_layout_width: Optional[bool]=REQ(validator=check_bool, default=None), high_contrast_mode: Optional[bool]=REQ(validator=check_bool, default=None), night_mode: Optional[bool]=REQ(validator=check_bool, default=None), translate_emoticons: Optional[bool]=REQ(validator=check_bool, default=None), 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), demote_inactive_streams: Optional[int]=REQ(validator=check_int, default=None), timezone: Optional[str]=REQ(validator=check_string, default=None)) -> HttpResponse: if (default_language is not None and default_language not in get_available_language_codes()): raise JsonableError(_("Invalid language '%s'") % (default_language,)) if (timezone is not None and timezone not in get_all_timezones()): raise JsonableError(_("Invalid timezone '%s'") % (timezone,)) if (emojiset is not None and emojiset not in [emojiset_choice['key'] for emojiset_choice in UserProfile.emojiset_choices()]): raise JsonableError(_("Invalid emojiset '%s'") % (emojiset,)) 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,)) request_settings = {k: v for k, v in list(locals().items()) if k in user_profile.property_types} result = {} # type: Dict[str, Any] 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 return json_success(result)
def add_bot_backend(request, user_profile, full_name=REQ(), short_name=REQ(), default_sending_stream_name=REQ('default_sending_stream', default=None), default_events_register_stream_name=REQ( 'default_events_register_stream', default=None), default_all_public_streams=REQ(validator=check_bool, default=None)): # type: (HttpRequest, UserProfile, text_type, text_type, Optional[text_type], Optional[text_type], Optional[bool]) -> HttpResponse short_name += "-bot" email = short_name + "@" + user_profile.realm.domain form = CreateUserForm({'full_name': full_name, 'email': email}) if not form.is_valid(): # We validate client-side as well return json_error(_('Bad name or username')) try: get_user_profile_by_email(email) return json_error(_("Username already in use")) except UserProfile.DoesNotExist: pass if len(request.FILES) == 0: avatar_source = UserProfile.AVATAR_FROM_GRAVATAR elif len(request.FILES) != 1: return json_error(_("You may only upload one file at a time")) else: user_file = list(request.FILES.values())[0] upload_avatar_image(user_file, user_profile, email) avatar_source = UserProfile.AVATAR_FROM_USER default_sending_stream = None if default_sending_stream_name is not None: default_sending_stream = stream_or_none(default_sending_stream_name, user_profile.realm) if (default_sending_stream and not default_sending_stream.is_public() and not subscribed_to_stream(user_profile, default_sending_stream)): return json_error(_('Insufficient permission')) default_events_register_stream = None if default_events_register_stream_name is not None: default_events_register_stream = stream_or_none( default_events_register_stream_name, user_profile.realm) if (default_events_register_stream and not default_events_register_stream.is_public() and not subscribed_to_stream(user_profile, default_events_register_stream)): return json_error(_('Insufficient permission')) bot_profile = do_create_user( email=email, password='', realm=user_profile.realm, full_name=full_name, short_name=short_name, active=True, bot_type=UserProfile.DEFAULT_BOT, bot_owner=user_profile, avatar_source=avatar_source, default_sending_stream=default_sending_stream, default_events_register_stream=default_events_register_stream, default_all_public_streams=default_all_public_streams) json_result = dict( api_key=bot_profile.api_key, avatar_url=avatar_url(bot_profile), default_sending_stream=get_stream_name( bot_profile.default_sending_stream), default_events_register_stream=get_stream_name( bot_profile.default_events_register_stream), default_all_public_streams=bot_profile.default_all_public_streams, ) return json_success(json_result)
def change_enter_sends(request: HttpRequest, user_profile: UserProfile, enter_sends: bool=REQ(validator=check_bool)) -> HttpResponse: do_change_enter_sends(user_profile, enter_sends) return json_success()
def get_messages_backend(request: HttpRequest, user_profile: UserProfile, anchor_val: Optional[str]=REQ( 'anchor', str_validator=check_string, default=None), num_before: int=REQ(converter=to_non_negative_int), num_after: int=REQ(converter=to_non_negative_int), narrow: OptionalNarrowListT=REQ('narrow', converter=narrow_parameter, default=None), use_first_unread_anchor_val: bool=REQ('use_first_unread_anchor', validator=check_bool, default=False), client_gravatar: bool=REQ(validator=check_bool, default=False), apply_markdown: bool=REQ(validator=check_bool, default=True)) -> HttpResponse: anchor = parse_anchor_value(anchor_val, use_first_unread_anchor_val) if num_before + num_after > MAX_MESSAGES_PER_FETCH: return json_error(_("Too many messages requested (maximum {}).").format( MAX_MESSAGES_PER_FETCH, )) if user_profile.realm.email_address_visibility != Realm.EMAIL_ADDRESS_VISIBILITY_EVERYONE: # If email addresses are only available to administrators, # clients cannot compute gravatars, so we force-set it to false. client_gravatar = False include_history = ok_to_include_history(narrow, user_profile) if include_history: # The initial query in this case doesn't use `zerver_usermessage`, # and isn't yet limited to messages the user is entitled to see! # # This is OK only because we've made sure this is a narrow that # will cause us to limit the query appropriately later. # See `ok_to_include_history` for details. need_message = True need_user_message = False elif narrow is None: # We need to limit to messages the user has received, but we don't actually # need any fields from Message need_message = False need_user_message = True else: need_message = True need_user_message = True query, inner_msg_id_col = get_base_query_for_search( user_profile=user_profile, need_message=need_message, need_user_message=need_user_message, ) query, is_search = add_narrow_conditions( user_profile=user_profile, inner_msg_id_col=inner_msg_id_col, query=query, narrow=narrow, ) if narrow is not None: # Add some metadata to our logging data for narrows verbose_operators = [] for term in narrow: if term['operator'] == "is": verbose_operators.append("is:" + term['operand']) else: verbose_operators.append(term['operator']) request._log_data['extra'] = "[{}]".format(",".join(verbose_operators)) sa_conn = get_sqlalchemy_connection() if anchor is None: # The use_first_unread_anchor code path anchor = find_first_unread_anchor( sa_conn, user_profile, narrow, ) anchored_to_left = (anchor == 0) # Set value that will be used to short circuit the after_query # altogether and avoid needless conditions in the before_query. anchored_to_right = (anchor >= LARGER_THAN_MAX_MESSAGE_ID) if anchored_to_right: num_after = 0 first_visible_message_id = get_first_visible_message_id(user_profile.realm) query = limit_query_to_range( query=query, num_before=num_before, num_after=num_after, anchor=anchor, anchored_to_left=anchored_to_left, anchored_to_right=anchored_to_right, id_col=inner_msg_id_col, first_visible_message_id=first_visible_message_id, ) main_query = alias(query) query = select(main_query.c, None, main_query).order_by(column("message_id").asc()) # This is a hack to tag the query we use for testing query = query.prefix_with("/* get_messages */") rows = list(sa_conn.execute(query).fetchall()) query_info = post_process_limited_query( rows=rows, num_before=num_before, num_after=num_after, anchor=anchor, anchored_to_left=anchored_to_left, anchored_to_right=anchored_to_right, first_visible_message_id=first_visible_message_id, ) rows = query_info['rows'] # The following is a little messy, but ensures that the code paths # are similar regardless of the value of include_history. The # 'user_messages' dictionary maps each message to the user's # UserMessage object for that message, which we will attach to the # rendered message dict before returning it. We attempt to # bulk-fetch rendered message dicts from remote cache using the # 'messages' list. message_ids: List[int] = [] user_message_flags: Dict[int, List[str]] = {} if include_history: message_ids = [row[0] for row in rows] # TODO: This could be done with an outer join instead of two queries um_rows = UserMessage.objects.filter(user_profile=user_profile, message__id__in=message_ids) user_message_flags = {um.message_id: um.flags_list() for um in um_rows} for message_id in message_ids: if message_id not in user_message_flags: user_message_flags[message_id] = ["read", "historical"] else: for row in rows: message_id = row[0] flags = row[1] user_message_flags[message_id] = UserMessage.flags_list_for_flags(flags) message_ids.append(message_id) search_fields: Dict[int, Dict[str, str]] = dict() if is_search: for row in rows: message_id = row[0] (topic_name, rendered_content, content_matches, topic_matches) = row[-4:] try: search_fields[message_id] = get_search_fields(rendered_content, topic_name, content_matches, topic_matches) except UnicodeDecodeError as err: # nocoverage # No coverage for this block since it should be # impossible, and we plan to remove it once we've # debugged the case that makes it happen. raise Exception(str(err), message_id, narrow) message_list = messages_for_ids( message_ids=message_ids, user_message_flags=user_message_flags, search_fields=search_fields, apply_markdown=apply_markdown, client_gravatar=client_gravatar, allow_edit_history=user_profile.realm.allow_edit_history, ) statsd.incr('loaded_old_messages', len(message_list)) ret = dict( messages=message_list, result='success', msg='', found_anchor=query_info['found_anchor'], found_oldest=query_info['found_oldest'], found_newest=query_info['found_newest'], history_limited=query_info['history_limited'], anchor=anchor, ) return json_success(ret)
def patch_bot_backend(request, user_profile, email, full_name=REQ(default=None), bot_owner=REQ(default=None), default_sending_stream=REQ(default=None), default_events_register_stream=REQ(default=None), default_all_public_streams=REQ(default=None, validator=check_bool)): # type: (HttpRequest, UserProfile, Text, Optional[Text], Optional[Text], Optional[Text], Optional[Text], Optional[bool]) -> HttpResponse try: bot = get_user_profile_by_email(email) except UserProfile.DoesNotExist: return json_error(_('No such user')) if not user_profile.can_admin_user(bot): return json_error(_('Insufficient permission')) if full_name is not None: check_change_full_name(bot, full_name) if bot_owner is not None: owner = get_user_profile_by_email(bot_owner) do_change_bot_owner(bot, owner, user_profile) if default_sending_stream is not None: if default_sending_stream == "": stream = None # type: Optional[Stream] else: (stream, recipient, sub) = access_stream_by_name(user_profile, default_sending_stream) do_change_default_sending_stream(bot, stream) if default_events_register_stream is not None: if default_events_register_stream == "": stream = None else: (stream, recipient, sub) = access_stream_by_name(user_profile, default_events_register_stream) do_change_default_events_register_stream(bot, stream) if default_all_public_streams is not None: do_change_default_all_public_streams(bot, default_all_public_streams) if len(request.FILES) == 0: pass elif len(request.FILES) == 1: user_file = list(request.FILES.values())[0] upload_avatar_image(user_file, user_profile, bot) avatar_source = UserProfile.AVATAR_FROM_USER do_change_avatar_fields(bot, avatar_source) else: return json_error(_("You may only upload one file at a time")) json_result = dict( full_name=bot.full_name, avatar_url=avatar_url(bot), default_sending_stream=get_stream_name(bot.default_sending_stream), default_events_register_stream=get_stream_name( bot.default_events_register_stream), default_all_public_streams=bot.default_all_public_streams, ) # Don't include the bot owner in case it is not set. # Default bots have no owner. if bot.bot_owner is not None: json_result['bot_owner'] = bot.bot_owner.email return json_success(json_result)