Exemple #1
0
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
Exemple #2
0
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)
Exemple #3
0
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)
Exemple #4
0
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)
Exemple #5
0
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)
Exemple #6
0
 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)
Exemple #7
0
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
Exemple #8
0
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)
Exemple #9
0
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()
Exemple #10
0
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)
Exemple #11
0
    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)
Exemple #12
0
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})
Exemple #13
0
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))
Exemple #14
0
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
Exemple #15
0
 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)
Exemple #16
0
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"))
Exemple #17
0
 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)
Exemple #18
0
def check_valid_interface_type(interface_type: Optional[int]) -> None:
    if interface_type not in Service.ALLOWED_INTERFACE_TYPES:
        raise JsonableError(_("Invalid interface type"))
Exemple #19
0
 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)
Exemple #20
0
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
Exemple #21
0
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()
Exemple #22
0
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()
Exemple #23
0
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"])
Exemple #24
0
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)
Exemple #25
0
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)
Exemple #26
0
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)
Exemple #27
0
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)
Exemple #28
0
 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)
Exemple #29
0
    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)
Exemple #30
0
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