def do_get_streams( user_profile: UserProfile, include_public: bool = True, include_web_public: bool = False, include_subscribed: bool = True, include_all_active: bool = False, include_default: bool = False, include_owner_subscribed: bool = False, ) -> List[Dict[str, Any]]: # This function is only used by API clients now. if include_all_active and not user_profile.is_realm_admin: raise JsonableError(_("User not authorized for this query")) include_public = include_public and user_profile.can_access_public_streams( ) # Start out with all active streams in the realm. query = Stream.objects.filter(realm=user_profile.realm, deactivated=False) if include_all_active: streams = Stream.get_client_data(query) else: # We construct a query as the or (|) of the various sources # this user requested streams from. query_filter: Optional[Q] = None def add_filter_option(option: Q) -> None: nonlocal query_filter if query_filter is None: query_filter = option else: query_filter |= option if include_subscribed: subscribed_stream_ids = get_subscribed_stream_ids_for_user( user_profile) recipient_check = Q(id__in=set(subscribed_stream_ids)) add_filter_option(recipient_check) if include_public: invite_only_check = Q(invite_only=False) add_filter_option(invite_only_check) if include_web_public: # This should match get_web_public_streams_queryset web_public_check = Q( is_web_public=True, invite_only=False, history_public_to_subscribers=True, deactivated=False, ) add_filter_option(web_public_check) if include_owner_subscribed and user_profile.is_bot: bot_owner = user_profile.bot_owner assert bot_owner is not None owner_stream_ids = get_subscribed_stream_ids_for_user(bot_owner) owner_subscribed_check = Q(id__in=set(owner_stream_ids)) add_filter_option(owner_subscribed_check) if query_filter is not None: query = query.filter(query_filter) streams = Stream.get_client_data(query) else: # Don't bother going to the database with no valid sources streams = [] streams.sort(key=lambda elt: elt["name"]) if include_default: is_default = {} default_streams = get_default_streams_for_realm(user_profile.realm_id) for default_stream in default_streams: is_default[default_stream.id] = True for stream in streams: stream["is_default"] = is_default.get(stream["stream_id"], False) return streams
def patch_bot_backend( request: HttpRequest, user_profile: UserProfile, bot_id: int, full_name: Optional[str] = REQ(default=None), role: Optional[int] = REQ( default=None, json_validator=check_int_in(UserProfile.ROLE_TYPES, ), ), bot_owner_id: Optional[int] = REQ(json_validator=check_int, default=None), config_data: Optional[Dict[str, str]] = REQ( default=None, json_validator=check_dict(value_validator=check_string)), service_payload_url: Optional[str] = REQ(json_validator=check_url, default=None), service_interface: int = REQ(json_validator=check_int, default=1), default_sending_stream: Optional[str] = REQ(default=None), default_events_register_stream: Optional[str] = REQ(default=None), default_all_public_streams: Optional[bool] = REQ( default=None, json_validator=check_bool), ) -> HttpResponse: bot = access_bot_by_id(user_profile, bot_id) if full_name is not None: check_change_bot_full_name(bot, full_name, user_profile) if role is not None and bot.role != role: # Logic duplicated from update_user_backend. if UserProfile.ROLE_REALM_OWNER in [ role, bot.role ] and not user_profile.is_realm_owner: raise OrganizationOwnerRequired() do_change_user_role(bot, role, acting_user=user_profile) if bot_owner_id is not None: try: owner = get_user_profile_by_id_in_realm(bot_owner_id, user_profile.realm) except UserProfile.DoesNotExist: raise JsonableError(_("Failed to change owner, no such user")) if not owner.is_active: raise JsonableError( _("Failed to change owner, user is deactivated")) if owner.is_bot: raise JsonableError( _("Failed to change owner, bots can't own other bots")) previous_owner = bot.bot_owner if previous_owner != owner: do_change_bot_owner(bot, owner, user_profile) if default_sending_stream is not None: if default_sending_stream == "": stream: Optional[Stream] = None else: (stream, sub) = access_stream_by_name(user_profile, default_sending_stream) do_change_default_sending_stream(bot, stream, acting_user=user_profile) if default_events_register_stream is not None: if default_events_register_stream == "": stream = None else: (stream, sub) = access_stream_by_name(user_profile, default_events_register_stream) do_change_default_events_register_stream(bot, stream, acting_user=user_profile) if default_all_public_streams is not None: do_change_default_all_public_streams(bot, default_all_public_streams, acting_user=user_profile) if service_payload_url is not None: check_valid_interface_type(service_interface) assert service_interface is not None do_update_outgoing_webhook_service(bot, service_interface, service_payload_url) if config_data is not None: do_update_bot_config_data(bot, config_data) if len(request.FILES) == 0: pass elif len(request.FILES) == 1: user_file = list(request.FILES.values())[0] assert isinstance(user_file, UploadedFile) assert user_file.size is not None upload_avatar_image(user_file, user_profile, bot) avatar_source = UserProfile.AVATAR_FROM_USER do_change_avatar_fields(bot, avatar_source, acting_user=user_profile) else: raise JsonableError(_("You may only upload one file at a time")) json_result = dict( full_name=bot.full_name, avatar_url=avatar_url(bot), service_interface=service_interface, service_payload_url=service_payload_url, config_data=config_data, 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(request, data=json_result)
def add_subscriptions_backend( request: HttpRequest, user_profile: UserProfile, streams_raw: Iterable[Mapping[str, str]] = REQ( "subscriptions", json_validator=add_subscriptions_schema), invite_only: bool = REQ(json_validator=check_bool, default=False), stream_post_policy: int = REQ( json_validator=check_int_in(Stream.STREAM_POST_POLICY_TYPES), default=Stream.STREAM_POST_POLICY_EVERYONE, ), history_public_to_subscribers: Optional[bool] = REQ( json_validator=check_bool, default=None), message_retention_days: Union[str, int] = REQ( json_validator=check_string_or_int, default=RETENTION_DEFAULT), announce: bool = REQ(json_validator=check_bool, default=False), principals: Union[Sequence[str], Sequence[int]] = REQ( json_validator=check_principals, default=EMPTY_PRINCIPALS, ), authorization_errors_fatal: bool = REQ(json_validator=check_bool, default=True), ) -> HttpResponse: realm = user_profile.realm stream_dicts = [] color_map = {} for stream_dict in streams_raw: # 'color' field is optional # check for its presence in the streams_raw first if "color" in stream_dict: color_map[stream_dict["name"]] = stream_dict["color"] stream_dict_copy: StreamDict = {} stream_dict_copy["name"] = stream_dict["name"].strip() # We don't allow newline characters in stream descriptions. if "description" in stream_dict: stream_dict_copy["description"] = stream_dict[ "description"].replace("\n", " ") stream_dict_copy["invite_only"] = invite_only stream_dict_copy["stream_post_policy"] = stream_post_policy stream_dict_copy[ "history_public_to_subscribers"] = history_public_to_subscribers stream_dict_copy[ "message_retention_days"] = parse_message_retention_days( message_retention_days, Stream.MESSAGE_RETENTION_SPECIAL_VALUES_MAP) stream_dicts.append(stream_dict_copy) # Validation of the streams arguments, including enforcement of # can_create_streams policy and check_stream_name policy is inside # list_to_streams. existing_streams, created_streams = list_to_streams(stream_dicts, user_profile, autocreate=True) authorized_streams, unauthorized_streams = filter_stream_authorization( user_profile, existing_streams) if len(unauthorized_streams) > 0 and authorization_errors_fatal: return json_error( _("Unable to access stream ({stream_name}).").format( stream_name=unauthorized_streams[0].name, )) # Newly created streams are also authorized for the creator streams = authorized_streams + created_streams if len(principals) > 0: if realm.is_zephyr_mirror_realm and not all(stream.invite_only for stream in streams): return json_error( _("You can only invite other Zephyr mirroring users to private streams." )) if not user_profile.can_subscribe_other_users(): # Guest users case will not be handled here as it will # be handled by the decorator above. raise JsonableError(_("Insufficient permission")) subscribers = { principal_to_user_profile(user_profile, principal) for principal in principals } else: subscribers = {user_profile} (subscribed, already_subscribed) = bulk_add_subscriptions(realm, streams, subscribers, acting_user=user_profile, color_map=color_map) # We can assume unique emails here for now, but we should eventually # convert this function to be more id-centric. email_to_user_profile: Dict[str, UserProfile] = {} result: Dict[str, Any] = dict(subscribed=defaultdict(list), already_subscribed=defaultdict(list)) for sub_info in subscribed: subscriber = sub_info.user stream = sub_info.stream result["subscribed"][subscriber.email].append(stream.name) email_to_user_profile[subscriber.email] = subscriber for sub_info in already_subscribed: subscriber = sub_info.user stream = sub_info.stream result["already_subscribed"][subscriber.email].append(stream.name) result["subscribed"] = dict(result["subscribed"]) result["already_subscribed"] = dict(result["already_subscribed"]) send_messages_for_new_subscribers( user_profile=user_profile, subscribers=subscribers, new_subscriptions=result["subscribed"], email_to_user_profile=email_to_user_profile, created_streams=created_streams, announce=announce, ) result["subscribed"] = dict(result["subscribed"]) result["already_subscribed"] = dict(result["already_subscribed"]) if not authorization_errors_fatal: result["unauthorized"] = [s.name for s in unauthorized_streams] return json_success(result)
def update_subscription_properties_backend( request: HttpRequest, user_profile: UserProfile, subscription_data: List[Dict[str, Any]] = REQ(json_validator=check_list( check_dict([ ("stream_id", check_int), ("property", check_string), ("value", check_union([check_string, check_bool])), ]), ), ), ) -> HttpResponse: """ This is the entry point to changing subscription properties. This is a bulk endpoint: requestors always provide a subscription_data list containing dictionaries for each stream of interest. Requests are of the form: [{"stream_id": "1", "property": "is_muted", "value": False}, {"stream_id": "1", "property": "color", "value": "#c2c2c2"}] """ property_converters = { "color": check_color, "in_home_view": check_bool, "is_muted": check_bool, "desktop_notifications": check_bool, "audible_notifications": check_bool, "push_notifications": check_bool, "email_notifications": check_bool, "pin_to_top": check_bool, "wildcard_mentions_notify": check_bool, } for change in subscription_data: stream_id = change["stream_id"] property = change["property"] value = change["value"] if property not in property_converters: raise JsonableError( _("Unknown subscription property: {}").format(property)) (stream, sub) = access_stream_by_id(user_profile, stream_id) if sub is None: raise JsonableError( _("Not subscribed to stream id {}").format(stream_id)) try: value = property_converters[property](property, value) except ValidationError as error: raise JsonableError(error.message) do_change_subscription_property(user_profile, sub, stream, property, value, acting_user=user_profile) # TODO: Do this more generally, see update_realm_user_settings_defaults.realm.py from zerver.lib.request import RequestNotes request_notes = RequestNotes.get_notes(request) for req_var in request.POST: if req_var not in request_notes.processed_parameters: request_notes.ignored_parameters.add(req_var) result: Dict[str, Any] = {} if len(request_notes.ignored_parameters) > 0: result["ignored_parameters_unsupported"] = list( request_notes.ignored_parameters) return json_success(result)
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=""), 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), email_notifications_batching_period_seconds: Optional[int] = REQ( json_validator=check_int, default=None), enable_drafts_synchronization: Optional[bool] = REQ( json_validator=check_bool, default=None), 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_in( UserProfile.DESKTOP_ICON_COUNT_DISPLAY_CHOICES), 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), enter_sends: Optional[bool] = REQ(json_validator=check_bool, default=None), ) -> HttpResponse: if (default_language is not None or notification_sound is not None or email_notifications_batching_period_seconds is not None): check_settings_values(notification_sound, email_notifications_batching_period_seconds, default_language) if new_password != "": return_data: Dict[str, Any] = {} if email_belongs_to_ldap(user_profile.realm, user_profile.delivery_email): raise JsonableError(_("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, ): raise JsonableError(_("Wrong password!")) except RateLimited as e: assert e.secs_to_freedom is not None secs_to_freedom = int(e.secs_to_freedom) raise JsonableError( _("You're making too many attempts! Try again in {} seconds."). format(secs_to_freedom), ) if not check_password_strength(new_password): raise JsonableError(_("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: 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) do_start_email_change_process(user_profile, new_email) 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 check_change_full_name(user_profile, full_name, user_profile) # Loop over user_profile.property_types request_settings = { k: v for k, v in list(locals().items()) if k in user_profile.property_types } for k, v in list(request_settings.items()): if v is not None and getattr(user_profile, k) != v: do_change_user_setting(user_profile, k, v, acting_user=user_profile) if timezone is not None and user_profile.timezone != timezone: do_change_user_setting(user_profile, "timezone", timezone, acting_user=user_profile) # TODO: Do this more generally. from zerver.lib.request import RequestNotes request_notes = RequestNotes.get_notes(request) for req_var in request.POST: if req_var not in request_notes.processed_parameters: request_notes.ignored_parameters.add(req_var) if len(request_notes.ignored_parameters) > 0: result["ignored_parameters_unsupported"] = list( request_notes.ignored_parameters) return json_success(result)
def _wrapped_view_func(request: HttpRequest, user_profile: UserProfile, *args: object, **kwargs: object) -> HttpResponse: if not user_profile.is_staff: raise JsonableError(_("Must be an server administrator")) return view_func(request, user_profile, *args, **kwargs)
def validate_entity(entity: Union[UserProfile, RemoteZulipServer]) -> RemoteZulipServer: if not isinstance(entity, RemoteZulipServer): raise JsonableError(err_("Must validate with valid Zulip server API key")) return entity
def send_to_push_bouncer( method: str, endpoint: str, post_data: Union[bytes, Mapping[str, Union[str, int, None, bytes]]], extra_headers: Mapping[str, str] = {}, ) -> Dict[str, object]: """While it does actually send the notice, this function has a lot of code and comments around error handling for the push notifications bouncer. There are several classes of failures, each with its own potential solution: * Network errors with requests.request. We raise an exception to signal it to the callers. * 500 errors from the push bouncer or other unexpected responses; we don't try to parse the response, but do make clear the cause. * 400 errors from the push bouncer. Here there are 2 categories: Our server failed to connect to the push bouncer (should throw) vs. client-side errors like an invalid token. """ assert settings.PUSH_NOTIFICATION_BOUNCER_URL is not None url = urllib.parse.urljoin(settings.PUSH_NOTIFICATION_BOUNCER_URL, "/api/v1/remotes/" + endpoint) api_auth = requests.auth.HTTPBasicAuth(settings.ZULIP_ORG_ID, settings.ZULIP_ORG_KEY) headers = {"User-agent": f"ZulipServer/{ZULIP_VERSION}"} headers.update(extra_headers) try: res = PushBouncerSession().request(method, url, data=post_data, auth=api_auth, verify=True, headers=headers) except ( requests.exceptions.Timeout, requests.exceptions.SSLError, requests.exceptions.ConnectionError, ) as e: raise PushNotificationBouncerRetryLaterError( f"{e.__class__.__name__} while trying to connect to push notification bouncer" ) if res.status_code >= 500: # 500s should be resolved by the people who run the push # notification bouncer service, and they'll get an appropriate # error notification from the server. We raise an exception to signal # to the callers that the attempt failed and they can retry. error_msg = "Received 500 from push notification bouncer" logging.warning(error_msg) raise PushNotificationBouncerRetryLaterError(error_msg) elif res.status_code >= 400: # If JSON parsing errors, just let that exception happen result_dict = orjson.loads(res.content) msg = result_dict["msg"] if "code" in result_dict and result_dict[ "code"] == "INVALID_ZULIP_SERVER": # Invalid Zulip server credentials should email this server's admins raise PushNotificationBouncerException( _("Push notifications bouncer error: {}").format(msg)) else: # But most other errors coming from the push bouncer # server are client errors (e.g. never-registered token) # and should be handled as such. raise JsonableError(msg) elif res.status_code != 200: # Anything else is unexpected and likely suggests a bug in # this version of Zulip, so we throw an exception that will # email the server admins. raise PushNotificationBouncerException( f"Push notification bouncer returned unexpected status code {res.status_code}" ) # If we don't throw an exception, it's a successful bounce! return orjson.loads(res.content)
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[MentionData] = None if content is not None: if content.rstrip() == "": content = "(deleted)" content = normalize_body(content) mention_data = 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 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")) new_stream = get_stream_by_id(stream_id) 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": list(links_for_embed), } queue_json_publish("embed_links", event_data) return json_success()
def events_register_backend( request: HttpRequest, user_profile: UserProfile, apply_markdown: bool = REQ(default=False, json_validator=check_bool), client_gravatar: bool = REQ(default=True, json_validator=check_bool), slim_presence: bool = REQ(default=False, json_validator=check_bool), all_public_streams: Optional[bool] = REQ(default=None, json_validator=check_bool), include_subscribers: bool = REQ(default=False, json_validator=check_bool), client_capabilities: Optional[Dict[str, bool]] = REQ( json_validator=check_dict( [ # This field was accidentally made required when it was added in v2.0.0-781; # this was not realized until after the release of Zulip 2.1.2. (It remains # required to help ensure backwards compatibility of client code.) ("notification_settings_null", check_bool), ], [ # Any new fields of `client_capabilities` should be optional. Add them here. ("bulk_message_deletion", check_bool), ("user_avatar_url_field_optional", check_bool), ("stream_typing_notifications", check_bool), ("user_settings_object", check_bool), ], value_validator=check_bool, ), default=None, ), event_types: Optional[Sequence[str]] = REQ( json_validator=check_list(check_string), default=None ), fetch_event_types: Optional[Sequence[str]] = REQ( json_validator=check_list(check_string), default=None ), narrow: NarrowT = REQ( json_validator=check_list(check_list(check_string, length=2)), default=[] ), queue_lifespan_secs: int = REQ(converter=int, default=0, documentation_pending=True), ) -> HttpResponse: if all_public_streams and not user_profile.can_access_public_streams(): raise JsonableError(_("User not authorized for this query")) all_public_streams = _default_all_public_streams(user_profile, all_public_streams) narrow = _default_narrow(user_profile, narrow) if client_capabilities is None: client_capabilities = {} client = RequestNotes.get_notes(request).client assert client is not None ret = do_events_register( user_profile, client, apply_markdown, client_gravatar, slim_presence, event_types, queue_lifespan_secs, all_public_streams, narrow=narrow, include_subscribers=include_subscribers, client_capabilities=client_capabilities, fetch_event_types=fetch_event_types, ) return json_success(request, data=ret)
def _wrapped_view_func(request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: for param in post_params: func_var_name = param.func_var_name if param.path_only: # For path_only parameters, they should already have # been passed via the URL, so there's no need for REQ # to do anything. # # TODO: Either run validators for path_only parameters # or don't declare them using REQ. assert func_var_name in kwargs if func_var_name in kwargs: continue assert func_var_name is not None if param.argument_type == 'body': try: val = ujson.loads(request.body) except ValueError: raise InvalidJSONError(_("Malformed JSON")) kwargs[func_var_name] = val continue elif param.argument_type is not None: # This is a view bug, not a user error, and thus should throw a 500. raise Exception(_("Invalid argument type")) post_var_names = [param.post_var_name] if param.aliases: post_var_names += param.aliases default_assigned = False post_var_name = None # type: Optional[str] query_params = request.GET.copy() query_params.update(request.POST) for req_var in post_var_names: try: val = query_params[req_var] except KeyError: continue if post_var_name is not None: assert req_var is not None raise RequestConfusingParmsError(post_var_name, req_var) post_var_name = req_var if post_var_name is None: post_var_name = param.post_var_name assert post_var_name is not None if param.default is _REQ.NotSpecified: raise RequestVariableMissingError(post_var_name) val = param.default default_assigned = True if param.converter is not None and not default_assigned: try: val = param.converter(val) except JsonableError: raise except Exception: raise RequestVariableConversionError(post_var_name, val) # Validators are like converters, but they don't handle JSON parsing; we do. if param.validator is not None and not default_assigned: try: val = ujson.loads(val) except Exception: raise JsonableError(_('Argument "%s" is not valid JSON.') % (post_var_name,)) error = param.validator(post_var_name, val) if error: raise JsonableError(error) # str_validators is like validator, but for direct strings (no JSON parsing). if param.str_validator is not None and not default_assigned: error = param.str_validator(post_var_name, val) if error: raise JsonableError(error) kwargs[func_var_name] = val return view_func(request, *args, **kwargs)
def send_message_backend( request: HttpRequest, user_profile: UserProfile, message_type_name: str = REQ("type"), req_to: Optional[str] = REQ("to", default=None), req_sender: Optional[str] = REQ("sender", default=None, documentation_pending=True), 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), time: Optional[float] = REQ(default=None, converter=to_float, 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 = RequestNotes.get_notes(request).client assert client is not None can_forge_sender = user_profile.can_forge_sender if forged and not can_forge_sender: raise JsonableError(_("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. raise JsonableError(_("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 req_sender is None: raise JsonableError(_("Missing sender")) if message_type_name != "private" and not can_forge_sender: raise JsonableError(_("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): raise JsonableError( _("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( client, user_profile, message_to, req_sender, message_type_name) except InvalidMirrorInput: raise JsonableError(_("Invalid mirrored message")) if client.name == "zephyr_mirror" and not user_profile.realm.is_zephyr_mirror_realm: raise JsonableError( _("Zephyr mirroring is not allowed in this organization")) sender = mirror_sender else: if req_sender is not None: raise JsonableError(_("Invalid mirrored message")) sender = user_profile if (delivery_type == "send_later" or delivery_type == "remind") and defer_until is None: raise JsonableError( _("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: deliver_at = 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, ) return json_success(request, data={"deliver_at": deliver_at}) ret = check_send_message( sender, client, message_type_name, message_to, topic_name, message_content, forged=forged, forged_timestamp=time, forwarder_user_profile=user_profile, realm=realm, local_id=local_id, sender_queue_id=queue_id, widget_content=widget_content, ) return json_success(request, data={"id": ret})
def process_zcommands(content: str, user_profile: UserProfile) -> Dict[str, Any]: def change_mode_setting(setting_name: str, switch_command: str, setting: str, setting_value: int) -> str: msg = ("Changed to {setting_name}! To revert " "{setting_name}, type `/{switch_command}`.".format( setting_name=setting_name, switch_command=switch_command, )) do_change_user_setting( user_profile=user_profile, setting_name=setting, setting_value=setting_value, acting_user=user_profile, ) return msg if not content.startswith("/"): raise JsonableError( _("There should be a leading slash in the zcommand.")) command = content[1:] if command == "ping": return {} elif command == "night": if user_profile.color_scheme == UserProfile.COLOR_SCHEME_NIGHT: return dict(msg="You are still in dark theme.") return dict(msg=change_mode_setting( setting_name="dark theme", switch_command="light", setting="color_scheme", setting_value=UserProfile.COLOR_SCHEME_NIGHT, )) elif command == "day": if user_profile.color_scheme == UserProfile.COLOR_SCHEME_LIGHT: return dict(msg="You are still in light theme.") return dict(msg=change_mode_setting( setting_name="light theme", switch_command="dark", setting="color_scheme", setting_value=UserProfile.COLOR_SCHEME_LIGHT, )) elif command == "fluid-width": if user_profile.fluid_layout_width: return dict(msg="You are still in fluid width mode.") return dict(msg=change_mode_setting( setting_name="fluid-width mode", switch_command="fixed-width", setting="fluid_layout_width", setting_value=True, )) elif command == "fixed-width": if not user_profile.fluid_layout_width: return dict(msg="You are still in fixed width mode.") return dict(msg=change_mode_setting( setting_name="fixed-width mode", switch_command="fluid-width", setting="fluid_layout_width", setting_value=False, )) raise JsonableError(_("No such command: {}").format(command))
def access_user_group_by_id(user_group_id: int, realm: Realm) -> UserGroup: try: user_group = UserGroup.objects.get(id=user_group_id, realm=realm) except UserGroup.DoesNotExist: raise JsonableError(_("Invalid user group")) return user_group
def wrapper(request: HttpRequest, user_profile: UserProfile, *args: Any, **kwargs: Any) -> HttpResponse: if not user_profile.is_realm_admin: raise JsonableError(_("Must be a realm administrator")) return func(request, user_profile, *args, **kwargs)
def check_valid_bot_type(user_profile: UserProfile, bot_type: int) -> None: if bot_type not in user_profile.allowed_bot_types: raise JsonableError(_("Invalid bot type"))
def wrapper(request: HttpRequest, user_profile: UserProfile, *args: object, **kwargs: object) -> HttpResponse: if not user_profile.has_billing_access: raise JsonableError( _("Must be a billing administrator or an organization owner")) return func(request, user_profile, *args, **kwargs)
def check_valid_interface_type(interface_type: Optional[int]) -> None: if interface_type not in Service.ALLOWED_INTERFACE_TYPES: raise JsonableError(_("Invalid interface type"))
def _wrapped_view_func(request: HttpRequest, user_profile: UserProfile, *args: object, **kwargs: object) -> HttpResponse: if user_profile.is_guest: raise JsonableError(_("Not allowed for guest users")) return view_func(request, user_profile, *args, **kwargs)
def check_short_name(short_name_raw: str) -> str: short_name = short_name_raw.strip() if len(short_name) == 0: raise JsonableError(_("Bad name or username")) return short_name
def update_stream_backend( request: HttpRequest, user_profile: UserProfile, stream_id: int, description: Optional[str] = REQ(str_validator=check_capped_string( Stream.MAX_DESCRIPTION_LENGTH), default=None), is_private: Optional[bool] = REQ(json_validator=check_bool, default=None), is_announcement_only: Optional[bool] = REQ(json_validator=check_bool, default=None), stream_post_policy: Optional[int] = REQ(json_validator=check_int_in( Stream.STREAM_POST_POLICY_TYPES), default=None), history_public_to_subscribers: Optional[bool] = REQ( json_validator=check_bool, default=None), is_web_public: Optional[bool] = REQ(json_validator=check_bool, default=None), new_name: Optional[str] = REQ(default=None), message_retention_days: Optional[Union[int, str]] = REQ( json_validator=check_string_or_int, default=None), ) -> HttpResponse: # We allow realm administrators to to update the stream name and # description even for private streams. (stream, sub) = access_stream_for_delete_or_update(user_profile, stream_id) if message_retention_days is not None: if not user_profile.is_realm_owner: raise OrganizationOwnerRequired() user_profile.realm.ensure_not_on_limited_plan() new_message_retention_days_value = parse_message_retention_days( message_retention_days, Stream.MESSAGE_RETENTION_SPECIAL_VALUES_MAP) do_change_stream_message_retention_days( stream, user_profile, new_message_retention_days_value) if description is not None: if "\n" in description: # We don't allow newline characters in stream descriptions. description = description.replace("\n", " ") do_change_stream_description(stream, description, acting_user=user_profile) if new_name is not None: new_name = new_name.strip() if stream.name == new_name: raise JsonableError(_("Stream already has that name!")) if stream.name.lower() != new_name.lower(): # Check that the stream name is available (unless we are # are only changing the casing of the stream name). check_stream_name_available(user_profile.realm, new_name) do_rename_stream(stream, new_name, user_profile) if is_announcement_only is not None: # is_announcement_only is a legacy way to specify # stream_post_policy. We can probably just delete this code, # since we're not aware of clients that used it, but we're # keeping it for backwards-compatibility for now. stream_post_policy = Stream.STREAM_POST_POLICY_EVERYONE if is_announcement_only: stream_post_policy = Stream.STREAM_POST_POLICY_ADMINS if stream_post_policy is not None: do_change_stream_post_policy(stream, stream_post_policy) # But we require even realm administrators to be actually # subscribed to make a private stream public. if is_private is not None: default_stream_ids = { s.id for s in get_default_streams_for_realm(stream.realm_id) } (stream, sub) = access_stream_by_id(user_profile, stream_id) if is_private and stream.id in default_stream_ids: raise JsonableError(_("Default streams cannot be made private.")) if is_web_public: # Enforce restrictions on creating web-public streams. if not user_profile.realm.web_public_streams_enabled(): raise JsonableError(_("Web public streams are not enabled.")) if not user_profile.can_create_web_public_streams(): raise JsonableError(_("Insufficient permission")) # Forbid parameter combinations that are inconsistent if is_private or history_public_to_subscribers is False: raise JsonableError(_("Invalid parameters")) if is_private is not None or is_web_public is not None: do_change_stream_permission(stream, is_private, history_public_to_subscribers, is_web_public) return json_success()
def update_user_status_backend( request: HttpRequest, user_profile: UserProfile, away: Optional[bool] = REQ(json_validator=check_bool, default=None), status_text: Optional[str] = REQ(str_validator=check_capped_string(60), default=None), emoji_name: Optional[str] = REQ(default=None), emoji_code: Optional[str] = REQ(default=None), # TODO: emoji_type is the more appropriate name for this parameter, but changing # that requires nontrivial work on the API documentation, since it's not clear # that the reactions endpoint would prefer such a change. emoji_type: Optional[str] = REQ("reaction_type", default=None), ) -> HttpResponse: if status_text is not None: status_text = status_text.strip() if (away is None) and (status_text is None) and (emoji_name is None): raise JsonableError(_("Client did not pass any new values.")) if emoji_name == "": # Reset the emoji_code and reaction_type if emoji_name is empty. # This should clear the user's configured emoji. emoji_code = "" emoji_type = UserStatus.UNICODE_EMOJI elif emoji_name is not None: if emoji_code is None: # The emoji_code argument is only required for rare corner # cases discussed in the long block comment below. For simple # API clients, we allow specifying just the name, and just # look up the code using the current name->code mapping. emoji_code = emoji_name_to_emoji_code(user_profile.realm, emoji_name)[0] if emoji_type is None: emoji_type = emoji_name_to_emoji_code(user_profile.realm, emoji_name)[1] elif emoji_type or emoji_code: raise JsonableError( _("Client must pass emoji_name if they pass either emoji_code or reaction_type.") ) # If we're asking to set an emoji (not clear it ("") or not adjust # it (None)), we need to verify the emoji is valid. if emoji_name not in ["", None]: assert emoji_name is not None assert emoji_code is not None assert emoji_type is not None check_emoji_request(user_profile.realm, emoji_name, emoji_code, emoji_type) client = RequestNotes.get_notes(request).client assert client is not None do_update_user_status( user_profile=user_profile, away=away, status_text=status_text, client_id=client.id, emoji_name=emoji_name, emoji_code=emoji_code, reaction_type=emoji_type, ) return json_success()
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=lambda var_name, s: get_client(s), default=None, intentionally_undocumented=True ), last_event_id: Optional[int] = REQ(json_validator=check_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, json_validator=check_bool, intentionally_undocumented=True ), client_gravatar: bool = REQ( default=False, json_validator=check_bool, intentionally_undocumented=True ), slim_presence: bool = REQ( default=False, json_validator=check_bool, intentionally_undocumented=True ), all_public_streams: bool = REQ( default=False, json_validator=check_bool, intentionally_undocumented=True ), event_types: Optional[Sequence[str]] = REQ( default=None, json_validator=check_list(check_string), intentionally_undocumented=True ), dont_block: bool = REQ(default=False, json_validator=check_bool), narrow: Sequence[Sequence[str]] = REQ( default=[], json_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, json_validator=check_bool, intentionally_undocumented=True ), stream_typing_notifications: bool = REQ( default=False, json_validator=check_bool, intentionally_undocumented=True ), user_settings_object: bool = REQ( default=False, json_validator=check_bool, intentionally_undocumented=True ), ) -> HttpResponse: if all_public_streams and not user_profile.can_access_public_streams(): raise JsonableError(_("User not authorized for this query")) # Extract the Tornado handler from the request tornado_handler = RequestNotes.get_notes(request).tornado_handler assert tornado_handler is not None handler = tornado_handler() assert handler is not None if user_client is None: valid_user_client = RequestNotes.get_notes(request).client assert valid_user_client is not None 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, stream_typing_notifications=stream_typing_notifications, user_settings_object=user_settings_object, ) result = in_tornado_thread(lambda: fetch_events(events_query)) if "extra_log_data" in result: log_data = RequestNotes.get_notes(request).log_data assert log_data is not None 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(request) response.asynchronous = True return response if result["type"] == "error": raise result["exception"] return json_success(request, data=result["response"])
def validate_bouncer_token_request(entity: Union[UserProfile, RemoteZulipServer], token: bytes, kind: int) -> None: if kind not in [RemotePushDeviceToken.APNS, RemotePushDeviceToken.GCM]: raise JsonableError(err_("Invalid token type")) validate_entity(entity) validate_token(token, kind)
def update_user_backend( request: HttpRequest, user_profile: UserProfile, user_id: int, full_name: Optional[str] = REQ(default=None), role: Optional[int] = REQ( default=None, json_validator=check_int_in(UserProfile.ROLE_TYPES, ), ), profile_data: Optional[List[Dict[str, Optional[Union[ int, ProfileDataElementValue]]]]] = REQ( default=None, json_validator=check_profile_data, ), ) -> HttpResponse: target = access_user_by_id(user_profile, user_id, allow_deactivated=True, allow_bots=True, for_admin=True) if role is not None and target.role != role: # Require that the current user has permissions to # grant/remove the role in question. access_user_by_id has # already verified we're an administrator; here we enforce # that only owners can toggle the is_realm_owner flag. # # Logic replicated in patch_bot_backend. if UserProfile.ROLE_REALM_OWNER in [ role, target.role ] and not user_profile.is_realm_owner: raise OrganizationOwnerRequired() if target.role == UserProfile.ROLE_REALM_OWNER and check_last_owner( target): raise JsonableError( _("The owner permission cannot be removed from the only organization owner." )) do_change_user_role(target, role, acting_user=user_profile) if full_name is not None and target.full_name != full_name and full_name.strip( ) != "": # We don't respect `name_changes_disabled` here because the request # is on behalf of the administrator. check_change_full_name(target, full_name, user_profile) if profile_data is not None: clean_profile_data = [] for entry in profile_data: assert isinstance(entry["id"], int) if entry["value"] is None or not entry["value"]: field_id = entry["id"] check_remove_custom_profile_field_value(target, field_id) else: clean_profile_data.append({ "id": entry["id"], "value": entry["value"], }) validate_user_custom_profile_data(target.realm.id, clean_profile_data) do_update_user_custom_profile_data_if_changed(target, clean_profile_data) return json_success(request)
def support(request: HttpRequest) -> HttpResponse: context: Dict[str, Any] = {} if "success_message" in request.session: context["success_message"] = request.session["success_message"] del request.session["success_message"] if settings.BILLING_ENABLED and request.method == "POST": # We check that request.POST only has two keys in it: The # realm_id and a field to change. keys = set(request.POST.keys()) if "csrfmiddlewaretoken" in keys: keys.remove("csrfmiddlewaretoken") if len(keys) != 2: raise JsonableError(_("Invalid parameters")) realm_id = request.POST.get("realm_id") realm = Realm.objects.get(id=realm_id) if request.POST.get("plan_type", None) is not None: new_plan_type = int(request.POST.get("plan_type")) current_plan_type = realm.plan_type do_change_plan_type(realm, new_plan_type, acting_user=request.user) msg = f"Plan type of {realm.string_id} changed from {get_plan_name(current_plan_type)} to {get_plan_name(new_plan_type)} " context["success_message"] = msg elif request.POST.get("discount", None) is not None: new_discount = Decimal(request.POST.get("discount")) current_discount = get_discount_for_realm(realm) or 0 attach_discount_to_realm(realm, new_discount, acting_user=request.user) context[ "success_message"] = f"Discount of {realm.string_id} changed to {new_discount}% from {current_discount}%." elif request.POST.get("new_subdomain", None) is not None: new_subdomain = request.POST.get("new_subdomain") old_subdomain = realm.string_id try: check_subdomain_available(new_subdomain) except ValidationError as error: context["error_message"] = error.message else: do_change_realm_subdomain(realm, new_subdomain, acting_user=request.user) request.session[ "success_message"] = f"Subdomain changed from {old_subdomain} to {new_subdomain}" return HttpResponseRedirect( reverse("support") + "?" + urlencode({"q": new_subdomain})) elif request.POST.get("status", None) is not None: status = request.POST.get("status") if status == "active": do_send_realm_reactivation_email(realm, acting_user=request.user) context[ "success_message"] = f"Realm reactivation email sent to admins of {realm.string_id}." elif status == "deactivated": do_deactivate_realm(realm, acting_user=request.user) context["success_message"] = f"{realm.string_id} deactivated." elif request.POST.get("billing_method", None) is not None: billing_method = request.POST.get("billing_method") if billing_method == "send_invoice": update_billing_method_of_current_plan( realm, charge_automatically=False, acting_user=request.user) context[ "success_message"] = f"Billing method of {realm.string_id} updated to pay by invoice." elif billing_method == "charge_automatically": update_billing_method_of_current_plan( realm, charge_automatically=True, acting_user=request.user) context[ "success_message"] = f"Billing method of {realm.string_id} updated to charge automatically." elif request.POST.get("sponsorship_pending", None) is not None: sponsorship_pending = request.POST.get("sponsorship_pending") if sponsorship_pending == "true": update_sponsorship_status(realm, True, acting_user=request.user) context[ "success_message"] = f"{realm.string_id} marked as pending sponsorship." elif sponsorship_pending == "false": update_sponsorship_status(realm, False, acting_user=request.user) context[ "success_message"] = f"{realm.string_id} is no longer pending sponsorship." elif request.POST.get("approve_sponsorship") is not None: if request.POST.get( "approve_sponsorship") == "approve_sponsorship": approve_sponsorship(realm, acting_user=request.user) context[ "success_message"] = f"Sponsorship approved for {realm.string_id}" elif request.POST.get("downgrade_method", None) is not None: downgrade_method = request.POST.get("downgrade_method") if downgrade_method == "downgrade_at_billing_cycle_end": downgrade_at_the_end_of_billing_cycle(realm) context[ "success_message"] = f"{realm.string_id} marked for downgrade at the end of billing cycle" elif downgrade_method == "downgrade_now_without_additional_licenses": downgrade_now_without_creating_additional_invoices(realm) context[ "success_message"] = f"{realm.string_id} downgraded without creating additional invoices" elif downgrade_method == "downgrade_now_void_open_invoices": downgrade_now_without_creating_additional_invoices(realm) voided_invoices_count = void_all_open_invoices(realm) context[ "success_message"] = f"{realm.string_id} downgraded and voided {voided_invoices_count} open invoices" elif request.POST.get("scrub_realm", None) is not None: if request.POST.get("scrub_realm") == "scrub_realm": do_scrub_realm(realm, acting_user=request.user) context["success_message"] = f"{realm.string_id} scrubbed." query = request.GET.get("q", None) if query: key_words = get_invitee_emails_set(query) users = set(UserProfile.objects.filter(delivery_email__in=key_words)) realms = set(Realm.objects.filter(string_id__in=key_words)) for key_word in key_words: try: URLValidator()(key_word) parse_result = urllib.parse.urlparse(key_word) hostname = parse_result.hostname assert hostname is not None if parse_result.port: hostname = f"{hostname}:{parse_result.port}" subdomain = get_subdomain_from_hostname(hostname) try: realms.add(get_realm(subdomain)) except Realm.DoesNotExist: pass except ValidationError: users.update( UserProfile.objects.filter(full_name__iexact=key_word)) for realm in realms: realm.customer = get_customer_by_realm(realm) current_plan = get_current_plan_by_realm(realm) if current_plan is not None: new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed( current_plan, timezone_now()) if last_ledger_entry is not None: if new_plan is not None: realm.current_plan = new_plan else: realm.current_plan = current_plan realm.current_plan.licenses = last_ledger_entry.licenses realm.current_plan.licenses_used = get_latest_seat_count( realm) # full_names can have , in them users.update(UserProfile.objects.filter(full_name__iexact=query)) context["users"] = users context["realms"] = realms confirmations: List[Dict[str, Any]] = [] preregistration_users = PreregistrationUser.objects.filter( email__in=key_words) confirmations += get_confirmations( [ Confirmation.USER_REGISTRATION, Confirmation.INVITATION, Confirmation.REALM_CREATION ], preregistration_users, hostname=request.get_host(), ) multiuse_invites = MultiuseInvite.objects.filter(realm__in=realms) confirmations += get_confirmations([Confirmation.MULTIUSE_INVITE], multiuse_invites) confirmations += get_confirmations([Confirmation.REALM_REACTIVATION], [realm.id for realm in realms]) context["confirmations"] = confirmations def get_realm_owner_emails_as_string(realm: Realm) -> str: return ", ".join(realm.get_human_owner_users().order_by( "delivery_email").values_list("delivery_email", flat=True)) def get_realm_admin_emails_as_string(realm: Realm) -> str: return ", ".join( realm.get_human_admin_users(include_realm_owners=False).order_by( "delivery_email").values_list("delivery_email", flat=True)) context[ "get_realm_owner_emails_as_string"] = get_realm_owner_emails_as_string context[ "get_realm_admin_emails_as_string"] = get_realm_admin_emails_as_string context["get_discount_for_realm"] = get_discount_for_realm context["realm_icon_url"] = realm_icon_url context["Confirmation"] = Confirmation return render(request, "analytics/support.html", context=context)
def add_bot_backend( request: HttpRequest, user_profile: UserProfile, full_name_raw: str = REQ("full_name"), short_name_raw: str = REQ("short_name"), bot_type: int = REQ(json_validator=check_int, default=UserProfile.DEFAULT_BOT), payload_url: str = REQ(json_validator=check_url, default=""), service_name: Optional[str] = REQ(default=None), config_data: Dict[str, str] = REQ( default={}, json_validator=check_dict(value_validator=check_string)), interface_type: int = REQ(json_validator=check_int, default=Service.GENERIC), default_sending_stream_name: Optional[str] = REQ("default_sending_stream", default=None), default_events_register_stream_name: Optional[str] = REQ( "default_events_register_stream", default=None), default_all_public_streams: Optional[bool] = REQ(json_validator=check_bool, default=None), ) -> HttpResponse: short_name = check_short_name(short_name_raw) if bot_type != UserProfile.INCOMING_WEBHOOK_BOT: service_name = service_name or short_name short_name += "-bot" full_name = check_full_name(full_name_raw) try: email = f"{short_name}@{user_profile.realm.get_bot_domain()}" except InvalidFakeEmailDomain: raise JsonableError( _("Can't create bots until FAKE_EMAIL_DOMAIN is correctly configured.\n" "Please contact your server administrator.")) form = CreateUserForm({"full_name": full_name, "email": email}) if bot_type == UserProfile.EMBEDDED_BOT: if not settings.EMBEDDED_BOTS_ENABLED: raise JsonableError(_("Embedded bots are not enabled.")) if service_name not in [bot.name for bot in EMBEDDED_BOTS]: raise JsonableError(_("Invalid embedded bot name.")) if not form.is_valid(): # We validate client-side as well raise JsonableError(_("Bad name or username")) try: get_user_by_delivery_email(email, user_profile.realm) raise JsonableError(_("Username already in use")) except UserProfile.DoesNotExist: pass check_bot_name_available( realm_id=user_profile.realm_id, full_name=full_name, ) check_bot_creation_policy(user_profile, bot_type) check_valid_bot_type(user_profile, bot_type) check_valid_interface_type(interface_type) if len(request.FILES) == 0: avatar_source = UserProfile.AVATAR_FROM_GRAVATAR elif len(request.FILES) != 1: raise JsonableError(_("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_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_sub) = access_stream_by_name( user_profile, default_events_register_stream_name) if bot_type in (UserProfile.INCOMING_WEBHOOK_BOT, UserProfile.EMBEDDED_BOT) and service_name: check_valid_bot_config(bot_type, service_name, config_data) bot_profile = do_create_user( email=email, password=None, realm=user_profile.realm, full_name=full_name, bot_type=bot_type, 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, acting_user=user_profile, ) if len(request.FILES) == 1: user_file = list(request.FILES.values())[0] assert isinstance(user_file, UploadedFile) assert user_file.size is not None upload_avatar_image(user_file, user_profile, bot_profile) if bot_type in (UserProfile.OUTGOING_WEBHOOK_BOT, UserProfile.EMBEDDED_BOT): assert isinstance(service_name, str) add_service( name=service_name, user_profile=bot_profile, base_url=payload_url, interface=interface_type, token=generate_api_key(), ) if bot_type == UserProfile.INCOMING_WEBHOOK_BOT and service_name: set_bot_config(bot_profile, "integration_id", service_name) if bot_type in (UserProfile.INCOMING_WEBHOOK_BOT, UserProfile.EMBEDDED_BOT): for key, value in config_data.items(): set_bot_config(bot_profile, key, value) notify_created_bot(bot_profile) api_key = get_api_key(bot_profile) json_result = dict( user_id=bot_profile.id, api_key=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(request, data=json_result)
def wrapper(request: HttpRequest, user_profile: UserProfile, *args: Any, **kwargs: Any) -> HttpResponse: if not user_profile.is_realm_admin and not user_profile.is_billing_admin: raise JsonableError(_("Must be a billing administrator or an organization administrator")) return func(request, user_profile, *args, **kwargs)
def _wrapped_view_func(request: HttpRequest, *args: object, **kwargs: object) -> HttpResponse: request_notes = RequestNotes.get_notes(request) for param in post_params: func_var_name = param.func_var_name if param.path_only: # For path_only parameters, they should already have # been passed via the URL, so there's no need for REQ # to do anything. # # TODO: Either run validators for path_only parameters # or don't declare them using REQ. assert func_var_name in kwargs if func_var_name in kwargs: continue assert func_var_name is not None post_var_name: Optional[str] if param.argument_type == "body": post_var_name = "request" try: val = request.body.decode(request.encoding or "utf-8") except UnicodeDecodeError: raise JsonableError(_("Malformed payload")) else: # This is a view bug, not a user error, and thus should throw a 500. assert param.argument_type is None, "Invalid argument type" post_var_names = [param.post_var_name] post_var_names += param.aliases post_var_name = None for req_var in post_var_names: assert req_var is not None if req_var in request.POST: val = request.POST[req_var] request_notes.processed_parameters.add(req_var) elif req_var in request.GET: val = request.GET[req_var] request_notes.processed_parameters.add(req_var) else: # This is covered by test_REQ_aliases, but coverage.py # fails to recognize this for some reason. continue # nocoverage if post_var_name is not None: raise RequestConfusingParmsError( post_var_name, req_var) post_var_name = req_var if post_var_name is None: post_var_name = param.post_var_name assert post_var_name is not None if param.default is _REQ.NotSpecified: raise RequestVariableMissingError(post_var_name) kwargs[func_var_name] = param.default continue if param.converter is not None: try: val = param.converter(post_var_name, val) except JsonableError: raise except Exception: raise RequestVariableConversionError(post_var_name, val) # json_validator is like converter, but doesn't handle JSON parsing; we do. if param.json_validator is not None: try: val = orjson.loads(val) except orjson.JSONDecodeError: if param.argument_type == "body": raise InvalidJSONError(_("Malformed JSON")) raise JsonableError( _('Argument "{}" is not valid JSON.').format( post_var_name)) try: val = param.json_validator(post_var_name, val) except ValidationError as error: raise JsonableError(error.message) # str_validators is like json_validator, but for direct strings (no JSON parsing). if param.str_validator is not None: try: val = param.str_validator(post_var_name, val) except ValidationError as error: raise JsonableError(error.message) kwargs[func_var_name] = val return view_func(request, *args, **kwargs)
def list_to_streams( streams_raw: Collection[StreamDict], user_profile: UserProfile, autocreate: bool = False, admin_access_required: bool = False, ) -> Tuple[List[Stream], List[Stream]]: """Converts list of dicts to a list of Streams, validating input in the process For each stream name, we validate it to ensure it meets our requirements for a proper stream name using check_stream_name. This function in autocreate mode should be atomic: either an exception will be raised during a precheck, or all the streams specified will have been created if applicable. @param streams_raw The list of stream dictionaries to process; names should already be stripped of whitespace by the caller. @param user_profile The user for whom we are retrieving the streams @param autocreate Whether we should create streams if they don't already exist """ # Validate all streams, getting extant ones, then get-or-creating the rest. stream_set = {stream_dict["name"] for stream_dict in streams_raw} for stream_name in stream_set: # Stream names should already have been stripped by the # caller, but it makes sense to verify anyway. assert stream_name == stream_name.strip() check_stream_name(stream_name) existing_streams: List[Stream] = [] missing_stream_dicts: List[StreamDict] = [] existing_stream_map = bulk_get_streams(user_profile.realm, stream_set) if admin_access_required: existing_recipient_ids = [ stream.recipient_id for stream in existing_stream_map.values() ] subs = Subscription.objects.filter( user_profile=user_profile, recipient_id__in=existing_recipient_ids, active=True) sub_map = {sub.recipient_id: sub for sub in subs} for stream in existing_stream_map.values(): sub = sub_map.get(stream.recipient_id, None) check_stream_access_for_delete_or_update(user_profile, stream, sub) message_retention_days_not_none = False web_public_stream_requested = False for stream_dict in streams_raw: stream_name = stream_dict["name"] stream = existing_stream_map.get(stream_name.lower()) if stream is None: if stream_dict.get("message_retention_days", None) is not None: message_retention_days_not_none = True missing_stream_dicts.append(stream_dict) if autocreate and stream_dict["is_web_public"]: web_public_stream_requested = True else: existing_streams.append(stream) if len(missing_stream_dicts) == 0: # This is the happy path for callers who expected all of these # streams to exist already. created_streams: List[Stream] = [] else: # autocreate=True path starts here for stream_dict in missing_stream_dicts: invite_only = stream_dict.get("invite_only", False) if invite_only and not user_profile.can_create_private_streams(): raise JsonableError(_("Insufficient permission")) if not invite_only and not user_profile.can_create_public_streams( ): raise JsonableError(_("Insufficient permission")) if not autocreate: raise JsonableError( _("Stream(s) ({}) do not exist").format( ", ".join(stream_dict["name"] for stream_dict in missing_stream_dicts), )) if web_public_stream_requested: if not user_profile.realm.web_public_streams_enabled(): raise JsonableError(_("Web-public streams are not enabled.")) if not user_profile.can_create_web_public_streams(): # We set create_web_public_stream_policy to allow only organization owners # to create web-public streams, because of their sensitive nature. raise JsonableError(_("Insufficient permission")) if message_retention_days_not_none: if not user_profile.is_realm_owner: raise OrganizationOwnerRequired() user_profile.realm.ensure_not_on_limited_plan() # We already filtered out existing streams, so dup_streams # will normally be an empty list below, but we protect against somebody # else racing to create the same stream. (This is not an entirely # paranoid approach, since often on Zulip two people will discuss # creating a new stream, and both people eagerly do it.) created_streams, dup_streams = create_streams_if_needed( realm=user_profile.realm, stream_dicts=missing_stream_dicts, acting_user=user_profile) existing_streams += dup_streams return existing_streams, created_streams