Exemple #1
0
def send_notification_backend(
    request: HttpRequest,
    user_profile: UserProfile,
    message_type: str = REQ(
        "type", str_validator=check_string_in(VALID_MESSAGE_TYPES), default="private"
    ),
    operator: str = REQ("op", str_validator=check_string_in(VALID_OPERATOR_TYPES)),
    notification_to: List[int] = REQ("to", json_validator=check_list(check_int)),
    topic: Optional[str] = REQ("topic", default=None),
) -> HttpResponse:
    to_length = len(notification_to)

    if to_length == 0:
        raise JsonableError(_("Empty 'to' list"))

    if message_type == "stream":
        if to_length > 1:
            raise JsonableError(_("Cannot send to multiple streams"))

        if topic is None:
            raise JsonableError(_("Missing topic"))

        stream_id = notification_to[0]
        # Verify that the user has access to the stream and has
        # permission to send messages to it.
        stream = access_stream_by_id(user_profile, stream_id)[0]
        access_stream_for_send_message(user_profile, stream, forwarder_user_profile=None)
        do_send_stream_typing_notification(user_profile, operator, stream, topic)
    else:
        user_ids = notification_to
        check_send_typing_notification(user_profile, user_ids, operator)

    return json_success()
Exemple #2
0
def update_display_settings_backend(
        request: HttpRequest, user_profile: UserProfile,
        twenty_four_hour_time: Optional[bool]=REQ(validator=check_bool, default=None),
        dense_mode: Optional[bool]=REQ(validator=check_bool, default=None),
        starred_message_counts: Optional[bool]=REQ(validator=check_bool, default=None),
        fluid_layout_width: Optional[bool]=REQ(validator=check_bool, default=None),
        high_contrast_mode: Optional[bool]=REQ(validator=check_bool, default=None),
        color_scheme: Optional[int]=REQ(validator=check_int_in(
            UserProfile.COLOR_SCHEME_CHOICES), default=None),
        translate_emoticons: Optional[bool]=REQ(validator=check_bool, default=None),
        default_language: Optional[str]=REQ(validator=check_string, default=None),
        left_side_userlist: Optional[bool]=REQ(validator=check_bool, default=None),
        emojiset: Optional[str]=REQ(validator=check_string_in(
            emojiset_choices), default=None),
        demote_inactive_streams: Optional[int]=REQ(validator=check_int_in(
            UserProfile.DEMOTE_STREAMS_CHOICES), default=None),
        timezone: Optional[str]=REQ(validator=check_string_in(all_timezones),
                                    default=None)) -> HttpResponse:

    # We can't use REQ for this widget because
    # get_available_language_codes requires provisioning to be
    # complete.
    if (default_language is not None and
            default_language not in get_available_language_codes()):
        raise JsonableError(_("Invalid default_language"))

    request_settings = {k: v for k, v in list(locals().items()) if k in user_profile.property_types}
    result: Dict[str, Any] = {}
    for k, v in list(request_settings.items()):
        if v is not None and getattr(user_profile, k) != v:
            do_set_user_display_setting(user_profile, k, v)
            result[k] = v

    return json_success(result)
Exemple #3
0
def upgrade(
    request: HttpRequest,
    user: UserProfile,
    billing_modality: str = REQ(str_validator=check_string_in(VALID_BILLING_MODALITY_VALUES)),
    schedule: str = REQ(str_validator=check_string_in(VALID_BILLING_SCHEDULE_VALUES)),
    signed_seat_count: str = REQ(),
    salt: str = REQ(),
    license_management: Optional[str] = REQ(
        default=None, str_validator=check_string_in(VALID_LICENSE_MANAGEMENT_VALUES)
    ),
    licenses: Optional[int] = REQ(json_validator=check_int, default=None),
    stripe_token: Optional[str] = REQ(default=None),
) -> HttpResponse:

    try:
        seat_count = unsign_seat_count(signed_seat_count, salt)
        if billing_modality == "charge_automatically" and license_management == "automatic":
            licenses = seat_count
        if billing_modality == "send_invoice":
            schedule = "annual"
            license_management = "manual"
        check_upgrade_parameters(
            billing_modality,
            schedule,
            license_management,
            licenses,
            stripe_token is not None,
            seat_count,
        )
        assert licenses is not None
        automanage_licenses = license_management == "automatic"

        billing_schedule = {"annual": CustomerPlan.ANNUAL, "monthly": CustomerPlan.MONTHLY}[
            schedule
        ]
        process_initial_upgrade(user, licenses, automanage_licenses, billing_schedule, stripe_token)
    except BillingError as e:
        if not settings.TEST_SUITE:  # nocoverage
            billing_logger.warning(
                "BillingError during upgrade: %s. user=%s, realm=%s (%s), billing_modality=%s, "
                "schedule=%s, license_management=%s, licenses=%s, has stripe_token: %s",
                e.error_description,
                user.id,
                user.realm.id,
                user.realm.string_id,
                billing_modality,
                schedule,
                license_management,
                licenses,
                stripe_token is not None,
            )
        raise
    except Exception:
        billing_logger.exception("Uncaught exception in billing:", stack_info=True)
        error_message = BillingError.CONTACT_SUPPORT.format(email=settings.ZULIP_ADMINISTRATOR)
        error_description = "uncaught exception during upgrade"
        raise BillingError(error_description, error_message)
    else:
        return json_success()
Exemple #4
0
def render_block(block: WildValue) -> str:
    # https://api.slack.com/reference/block-kit/blocks
    block_type = block["type"].tame(
        check_string_in([
            "actions", "context", "divider", "header", "image", "input",
            "section"
        ]))
    if block_type == "actions":
        # Unhandled
        return ""
    elif block_type == "context" and block.get("elements"):
        pieces = []
        # Slack renders these pieces left-to-right, packed in as
        # closely as possible.  We just render them above each other,
        # for simplicity.
        for element in block["elements"]:
            element_type = element["type"].tame(
                check_string_in(["image", "plain_text", "mrkdwn"]))
            if element_type == "image":
                pieces.append(render_block_element(element))
            else:
                pieces.append(element.tame(check_text_block()))
        return "\n\n".join(piece.strip() for piece in pieces
                           if piece.strip() != "")
    elif block_type == "divider":
        return "----"
    elif block_type == "header":
        return "## " + block["text"].tame(
            check_text_block(plain_text_only=True))
    elif block_type == "image":
        image_url = block["image_url"].tame(check_url)
        alt_text = block["alt_text"].tame(check_string)
        if "title" in block:
            alt_text = block["title"].tame(
                check_text_block(plain_text_only=True))
        return f"[{alt_text}]({image_url})"
    elif block_type == "input":
        # Unhandled
        pass
    elif block_type == "section":
        pieces = []
        if "text" in block:
            pieces.append(block["text"].tame(check_text_block()))

        if "accessory" in block:
            pieces.append(render_block_element(block["accessory"]))

        if "fields" in block:
            # TODO -- these should be rendered in two columns,
            # left-to-right.  We could render them sequentially,
            # except some may be Title1 / Title2 / value1 / value2,
            # which would be nonsensical when rendered sequentially.
            pass

        return "\n\n".join(piece.strip() for piece in pieces
                           if piece.strip() != "")

    return ""
Exemple #5
0
def send_notification_backend(
    request: HttpRequest,
    user_profile: UserProfile,
    message_type: str = REQ(
        "type", str_validator=check_string_in(VALID_MESSAGE_TYPES), default="private"
    ),
    operator: str = REQ("op", str_validator=check_string_in(VALID_OPERATOR_TYPES)),
    user_ids: List[int] = REQ("to", validator=check_list(check_int)),
) -> HttpResponse:
    if len(user_ids) == 0:
        return json_error(_("Missing parameter: 'to' (recipient)"))

    check_send_typing_notification(user_profile, user_ids, operator)
    return json_success()
Exemple #6
0
def update_message_backend(
    request: HttpRequest,
    user_profile: UserProfile,
    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: str = REQ(
        default="change_one", str_validator=check_string_in(PROPAGATE_MODE_VALUES)
    ),
    send_notification_to_old_thread: bool = REQ(default=True, json_validator=check_bool),
    send_notification_to_new_thread: bool = REQ(default=True, json_validator=check_bool),
    content: Optional[str] = REQ(default=None),
) -> HttpResponse:
    number_changed = check_update_message(
        user_profile,
        message_id,
        stream_id,
        topic_name,
        propagate_mode,
        send_notification_to_old_thread,
        send_notification_to_new_thread,
        content,
    )

    # Include the number of messages changed in the logs
    log_data = get_request_notes(request).log_data
    assert log_data is not None
    log_data["extra"] = f"[{number_changed}]"

    return json_success()
Exemple #7
0
def check_text_block(plain_text_only: bool = False) -> Validator[str]:
    if plain_text_only:
        type_validator = check_string_in(["plain_text"])
    else:
        type_validator = check_string

    def f(var_name: str, val: object) -> str:
        block = check_dict([
            ("type", type_validator),
            ("text", check_string),
        ], )(var_name, val)

        # We can't use `value_validator=check_string` above to let
        # mypy know this is a str, because there's an optional boolean
        # `emoji` key which can appear -- hence the assert.
        text = block["text"]
        assert isinstance(text, str)

        # Ideally we would escape the content if it was plain text,
        # but out flavor of Markdown doesn't support escapes. :(
        return text

    return f
Exemple #8
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[bugdown.MentionData] = None
    if content is not None:
        content = content.strip()
        if content == "":
            content = "(deleted)"
        content = truncate_body(content)

        mention_data = bugdown.MentionData(
            realm_id=user_profile.realm.id,
            content=content,
        )
        user_info = get_user_info_for_message_updates(message.id)
        prior_mention_user_ids = user_info['mention_user_ids']

        # We render the message using the current user's realm; since
        # the cross-realm bots never edit messages, this should be
        # always correct.
        # Note: If rendering fails, the called code will raise a JsonableError.
        rendered_content = render_incoming_message(
            message,
            content,
            user_info['message_user_ids'],
            user_profile.realm,
            mention_data=mention_data)
        links_for_embed |= message.links_for_preview

        mention_user_ids = message.mentions_user_ids

    new_stream = None
    old_stream = None
    number_changed = 0

    if stream_id is not None:
        if not user_profile.is_realm_admin:
            raise JsonableError(
                _("You don't have permission to move this message"))
        if content is not None:
            raise JsonableError(
                _("Cannot change message content while changing stream"))

        old_stream = get_stream_by_id(message.recipient.type_id)
        new_stream = get_stream_by_id(stream_id)

        if not (old_stream.is_public() and new_stream.is_public()):
            # We'll likely decide to relax this condition in the
            # future; it just requires more care with details like the
            # breadcrumb messages.
            raise JsonableError(_("Streams must be public"))

    number_changed = do_update_message(
        user_profile, message, new_stream, topic_name, propagate_mode,
        send_notification_to_old_thread, send_notification_to_new_thread,
        content, rendered_content, prior_mention_user_ids, mention_user_ids,
        mention_data)

    # Include the number of messages changed in the logs
    request._log_data['extra'] = f"[{number_changed}]"
    if links_for_embed:
        event_data = {
            'message_id': message.id,
            'message_content': message.content,
            # The choice of `user_profile.realm_id` rather than
            # `sender.realm_id` must match the decision made in the
            # `render_incoming_message` call earlier in this function.
            'message_realm_id': user_profile.realm_id,
            'urls': links_for_embed
        }
        queue_json_publish('embed_links', event_data)
    return json_success()
Exemple #9
0
def update_realm_user_settings_defaults(
    request: HttpRequest,
    user_profile: UserProfile,
    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_view: Optional[str] = REQ(
        str_validator=check_string_in(default_view_options), default=None),
    escape_navigates_to_default_view: Optional[bool] = REQ(
        json_validator=check_bool, 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),
    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 is not included here, because we don't want
    # security-related settings to be controlled by organization administrators.
    # enable_marketing_emails is not included here, since we don't at
    # present allow organizations to customize this. (The user's selection
    # in the signup form takes precedence over RealmUserDefault).
    #
    # We may want to change this model in the future, since some SSO signups
    # do not offer an opportunity to prompt the user at all during signup.
    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),
    enable_drafts_synchronization: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    email_notifications_batching_period_seconds: Optional[int] = REQ(
        json_validator=check_int, default=None),
    twenty_four_hour_time: Optional[bool] = REQ(json_validator=check_bool,
                                                default=None),
    send_stream_typing_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    send_private_typing_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    send_read_receipts: Optional[bool] = REQ(json_validator=check_bool,
                                             default=None),
) -> HttpResponse:
    if notification_sound is not None or email_notifications_batching_period_seconds is not None:
        check_settings_values(notification_sound,
                              email_notifications_batching_period_seconds)

    realm_user_default = RealmUserDefault.objects.get(realm=user_profile.realm)
    request_settings = {
        k: v
        for k, v in list(locals().items())
        if (k in RealmUserDefault.property_types)
    }
    for k, v in list(request_settings.items()):
        if v is not None and getattr(realm_user_default, k) != v:
            do_set_realm_user_default_setting(realm_user_default,
                                              k,
                                              v,
                                              acting_user=user_profile)

    # TODO: Extract `ignored_parameters_unsupported` to be a common feature of the REQ framework.
    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 #10
0
def upgrade(
    request: HttpRequest,
    user: UserProfile,
    billing_modality: str = REQ(
        str_validator=check_string_in(VALID_BILLING_MODALITY_VALUES)),
    schedule: str = REQ(
        str_validator=check_string_in(VALID_BILLING_SCHEDULE_VALUES)),
    signed_seat_count: str = REQ(),
    salt: str = REQ(),
    onboarding: bool = REQ(default=False, json_validator=check_bool),
    license_management: Optional[str] = REQ(
        default=None,
        str_validator=check_string_in(VALID_LICENSE_MANAGEMENT_VALUES)),
    licenses: Optional[int] = REQ(json_validator=check_int, default=None),
) -> HttpResponse:
    ensure_realm_does_not_have_active_plan(user.realm)
    try:
        seat_count = unsign_seat_count(signed_seat_count, salt)
        if billing_modality == "charge_automatically" and license_management == "automatic":
            licenses = seat_count
        if billing_modality == "send_invoice":
            schedule = "annual"
            license_management = "manual"
        check_upgrade_parameters(billing_modality, schedule,
                                 license_management, licenses, seat_count)
        assert licenses is not None and license_management is not None
        automanage_licenses = license_management == "automatic"
        charge_automatically = billing_modality == "charge_automatically"

        billing_schedule = {
            "annual": CustomerPlan.ANNUAL,
            "monthly": CustomerPlan.MONTHLY
        }[schedule]
        if charge_automatically:
            stripe_checkout_session = setup_upgrade_checkout_session_and_payment_intent(
                user,
                seat_count,
                licenses,
                license_management,
                billing_schedule,
                billing_modality,
                onboarding,
            )
            return json_success(
                request,
                data={
                    "stripe_session_url": stripe_checkout_session.url,
                    "stripe_session_id": stripe_checkout_session.id,
                },
            )
        else:
            process_initial_upgrade(
                user,
                licenses,
                automanage_licenses,
                billing_schedule,
                False,
                is_free_trial_offer_enabled(),
            )
            return json_success(request)

    except BillingError as e:
        billing_logger.warning(
            "BillingError during upgrade: %s. user=%s, realm=%s (%s), billing_modality=%s, "
            "schedule=%s, license_management=%s, licenses=%s",
            e.error_description,
            user.id,
            user.realm.id,
            user.realm.string_id,
            billing_modality,
            schedule,
            license_management,
            licenses,
        )
        raise e
    except Exception:
        billing_logger.exception("Uncaught exception in billing:",
                                 stack_info=True)
        error_message = BillingError.CONTACT_SUPPORT.format(
            email=settings.ZULIP_ADMINISTRATOR)
        error_description = "uncaught exception during upgrade"
        raise BillingError(error_description, error_message)
Exemple #11
0
    check_list,
    check_required_string,
    check_string,
    check_string_in,
    check_union,
)
from zerver.models import Draft, UserProfile
from zerver.tornado.django_api import send_event

VALID_DRAFT_TYPES: Set[str] = {"", "private", "stream"}

# A validator to verify if the structure (syntax) of a dictionary
# meets the requirements to be a draft dictionary:
draft_dict_validator = check_dict_only(
    required_keys=[
        ("type", check_string_in(VALID_DRAFT_TYPES)),
        ("to", check_list(check_int)
         ),  # The ID of the stream to send to, or a list of user IDs.
        ("topic", check_string
         ),  # This string can simply be empty for private type messages.
        ("content", check_required_string),
    ],
    optional_keys=[
        ("timestamp", check_union([check_int,
                                   check_float])),  # A Unix timestamp.
    ],
)


def further_validated_draft_dict(draft_dict: Dict[str, Any],
                                 user_profile: UserProfile) -> Dict[str, Any]:
Exemple #12
0
def support(
    request: HttpRequest,
    realm_id: Optional[int] = REQ(default=None, converter=to_non_negative_int),
    plan_type: Optional[int] = REQ(default=None,
                                   converter=to_non_negative_int),
    discount: Optional[Decimal] = REQ(default=None, converter=to_decimal),
    new_subdomain: Optional[str] = REQ(default=None),
    status: Optional[str] = REQ(
        default=None, str_validator=check_string_in(VALID_STATUS_VALUES)),
    billing_method: Optional[str] = REQ(
        default=None, str_validator=check_string_in(VALID_BILLING_METHODS)),
    sponsorship_pending: Optional[bool] = REQ(default=None,
                                              json_validator=check_bool),
    approve_sponsorship: Optional[bool] = REQ(default=None,
                                              json_validator=check_bool),
    downgrade_method: Optional[str] = REQ(
        default=None, str_validator=check_string_in(VALID_DOWNGRADE_METHODS)),
    scrub_realm: Optional[bool] = REQ(default=None, json_validator=check_bool),
    query: Optional[str] = REQ("q", default=None),
    org_type: Optional[int] = REQ(default=None, converter=to_non_negative_int),
) -> 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 = Realm.objects.get(id=realm_id)

        acting_user = request.user
        assert isinstance(acting_user, UserProfile)
        if plan_type is not None:
            current_plan_type = realm.plan_type
            do_change_plan_type(realm, plan_type, acting_user=acting_user)
            msg = f"Plan type of {realm.string_id} changed from {get_plan_name(current_plan_type)} to {get_plan_name(plan_type)} "
            context["success_message"] = msg
        elif org_type is not None:
            current_realm_type = realm.org_type
            do_change_realm_org_type(realm, org_type, acting_user=acting_user)
            msg = f"Org type of {realm.string_id} changed from {get_org_type_display_name(current_realm_type)} to {get_org_type_display_name(org_type)} "
            context["success_message"] = msg
        elif discount is not None:
            current_discount = get_discount_for_realm(realm) or 0
            attach_discount_to_realm(realm, discount, acting_user=acting_user)
            context[
                "success_message"] = f"Discount of {realm.string_id} changed to {discount}% from {current_discount}%."
        elif new_subdomain is not None:
            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=acting_user)
                request.session[
                    "success_message"] = f"Subdomain changed from {old_subdomain} to {new_subdomain}"
                return HttpResponseRedirect(
                    reverse("support") + "?" + urlencode({"q": new_subdomain}))
        elif status is not None:
            if status == "active":
                do_send_realm_reactivation_email(realm,
                                                 acting_user=acting_user)
                context[
                    "success_message"] = f"Realm reactivation email sent to admins of {realm.string_id}."
            elif status == "deactivated":
                do_deactivate_realm(realm, acting_user=acting_user)
                context["success_message"] = f"{realm.string_id} deactivated."
        elif billing_method is not None:
            if billing_method == "send_invoice":
                update_billing_method_of_current_plan(
                    realm, charge_automatically=False, acting_user=acting_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=acting_user)
                context[
                    "success_message"] = f"Billing method of {realm.string_id} updated to charge automatically."
        elif sponsorship_pending is not None:
            if sponsorship_pending:
                update_sponsorship_status(realm, True, acting_user=acting_user)
                context[
                    "success_message"] = f"{realm.string_id} marked as pending sponsorship."
            else:
                update_sponsorship_status(realm,
                                          False,
                                          acting_user=acting_user)
                context[
                    "success_message"] = f"{realm.string_id} is no longer pending sponsorship."
        elif approve_sponsorship:
            do_approve_sponsorship(realm, acting_user=acting_user)
            context[
                "success_message"] = f"Sponsorship approved for {realm.string_id}"
        elif downgrade_method is not None:
            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 scrub_realm:
            do_scrub_realm(realm, acting_user=acting_user)
            context["success_message"] = f"{realm.string_id} scrubbed."

    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["get_org_type_display_name"] = get_org_type_display_name
    context["realm_icon_url"] = realm_icon_url
    context["Confirmation"] = Confirmation
    context["sorted_realm_types"] = sorted(Realm.ORG_TYPES.values(),
                                           key=lambda d: d["display_order"])

    return render(request, "analytics/support.html", context=context)
Exemple #13
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_stream_desktop_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    enable_stream_email_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    enable_stream_push_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    enable_stream_audible_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    wildcard_mentions_notify: Optional[bool] = REQ(json_validator=check_bool,
                                                   default=None),
    notification_sound: Optional[str] = REQ(default=None),
    enable_desktop_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    enable_sounds: Optional[bool] = REQ(json_validator=check_bool,
                                        default=None),
    enable_offline_email_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    enable_offline_push_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    enable_online_push_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    enable_digest_emails: Optional[bool] = REQ(json_validator=check_bool,
                                               default=None),
    enable_login_emails: Optional[bool] = REQ(json_validator=check_bool,
                                              default=None),
    enable_marketing_emails: Optional[bool] = REQ(json_validator=check_bool,
                                                  default=None),
    message_content_in_email_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    pm_content_in_desktop_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    desktop_icon_count_display: Optional[int] = REQ(json_validator=check_int,
                                                    default=None),
    realm_name_in_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    presence_enabled: Optional[bool] = REQ(json_validator=check_bool,
                                           default=None),
    enter_sends: Optional[bool] = REQ(json_validator=check_bool, default=None),
) -> HttpResponse:
    # We can't use REQ for this widget because
    # get_available_language_codes requires provisioning to be
    # complete.
    if default_language is not None and default_language not in get_available_language_codes(
    ):
        raise JsonableError(_("Invalid default_language"))

    if (notification_sound is not None
            and notification_sound not in get_available_notification_sounds()
            and notification_sound != "none"):
        raise JsonableError(
            _("Invalid notification sound '{}'").format(notification_sound))

    if email_notifications_batching_period_seconds is not None and (
            email_notifications_batching_period_seconds <= 0
            or email_notifications_batching_period_seconds > 7 * 24 * 60 * 60):
        # We set a limit of one week for the batching period
        raise JsonableError(
            _("Invalid email batching period: {} seconds").format(
                email_notifications_batching_period_seconds))

    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_set_user_display_setting(user_profile, k, v)

    req_vars = {
        k: v
        for k, v in list(locals().items())
        if k in user_profile.notification_setting_types
    }

    for k, v in list(req_vars.items()):
        if v is not None and getattr(user_profile, k) != v:
            do_change_notification_settings(user_profile,
                                            k,
                                            v,
                                            acting_user=user_profile)

    if timezone is not None and user_profile.timezone != timezone:
        do_set_user_display_setting(user_profile, "timezone", timezone)

    # TODO: Do this more generally.
    from zerver.lib.request import get_request_notes

    request_notes = get_request_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)