Beispiel #1
0
 def wrapper(request: HttpRequest, *args: object, **kwargs: object) -> HttpResponse:
     if request.method != "POST":
         err_method = request.method
         logging.warning(
             "Method Not Allowed (%s): %s",
             err_method,
             request.path,
             extra={"status_code": 405, "request": request},
         )
         if RequestNotes.get_notes(request).error_format == "JSON":
             return json_method_not_allowed(["POST"])
         else:
             return TemplateResponse(
                 request, "404.html", context={"status_code": 405}, status=405
             )
     return func(request, *args, **kwargs)
Beispiel #2
0
def report_narrow_times(
    request: HttpRequest,
    user_profile: Union[UserProfile, AnonymousUser],
    initial_core: int = REQ(converter=to_non_negative_int),
    initial_free: int = REQ(converter=to_non_negative_int),
    network: int = REQ(converter=to_non_negative_int),
) -> HttpResponse:
    log_data = RequestNotes.get_notes(request).log_data
    assert log_data is not None
    log_data["extra"] = f"[{initial_core}ms/{initial_free}ms/{network}ms]"
    realm = get_valid_realm_from_request(request)
    base_key = statsd_key(realm.string_id, clean_periods=True)
    statsd.timing(f"narrow.initial_core.{base_key}", initial_core)
    statsd.timing(f"narrow.initial_free.{base_key}", initial_free)
    statsd.timing(f"narrow.network.{base_key}", network)
    return json_success()
Beispiel #3
0
    def process_request(self, request: HttpRequest) -> None:
        maybe_tracemalloc_listen()
        request_notes = RequestNotes.get_notes(request)

        if request_notes.log_data is not None:
            # Sanity check to ensure this is being called from the
            # Tornado code path that returns responses asynchronously.
            assert request_notes.saved_response is not None

            # Avoid re-initializing request_notes.log_data if it's already there.
            return

        request_notes.client_name, request_notes.client_version = parse_client(
            request)
        request_notes.log_data = {}
        record_request_start_data(request_notes.log_data)
Beispiel #4
0
 def _wrapped_func_arguments(request: HttpRequest, *args: object,
                             **kwargs: object) -> HttpResponse:
     if not authenticate_notify(request):
         raise AccessDeniedError()
     request_notes = RequestNotes.get_notes(request)
     is_tornado_request = request_notes.tornado_handler is not None
     # These next 2 are not security checks; they are internal
     # assertions to help us find bugs.
     if is_tornado_view and not is_tornado_request:
         raise RuntimeError(
             "Tornado notify view called with no Tornado handler")
     if not is_tornado_view and is_tornado_request:
         raise RuntimeError(
             "Django notify view called with Tornado handler")
     request_notes.requestor_for_logs = "internal"
     return view_func(request, *args, **kwargs)
Beispiel #5
0
    def format(self, record: logging.LogRecord) -> str:
        from zerver.lib.request import get_current_request

        request = get_current_request()
        if not request:
            setattr(record, "user", None)
            setattr(record, "client", None)
            setattr(record, "url", None)
            setattr(record, "content_type", None)
            setattr(record, "custom_headers", None)
            setattr(record, "payload", None)
            return super().format(record)

        if request.content_type == "application/json":
            payload = request.body
        else:
            payload = request.POST.get("payload")

        try:
            payload = orjson.dumps(orjson.loads(payload),
                                   option=orjson.OPT_INDENT_2).decode()
        except orjson.JSONDecodeError:
            pass

        custom_header_template = "{header}: {value}\n"

        header_text = ""
        for header in request.META.keys():
            if header.lower().startswith("http_x"):
                header_text += custom_header_template.format(
                    header=header, value=request.META[header])

        header_message = header_text if header_text else None
        from zerver.lib.request import RequestNotes

        client = RequestNotes.get_notes(request).client
        assert client is not None

        setattr(
            record, "user",
            f"{request.user.delivery_email} ({request.user.realm.string_id})")
        setattr(record, "client", client.name)
        setattr(record, "url", request.META.get("PATH_INFO", None))
        setattr(record, "content_type", request.content_type)
        setattr(record, "custom_headers", header_message)
        setattr(record, "payload", payload)
        return super().format(record)
Beispiel #6
0
    def process_response(self, request: HttpRequest,
                         response: HttpResponseBase) -> HttpResponseBase:
        if getattr(response, "asynchronous", False):
            # This special Tornado "asynchronous" response is
            # discarded after going through this code path as Tornado
            # intends to block, so we stop here to avoid unnecessary work.
            return response

        remote_ip = request.META["REMOTE_ADDR"]

        # Get the requestor's identifier and client, if available.
        request_notes = RequestNotes.get_notes(request)
        requestor_for_logs = request_notes.requestor_for_logs
        if requestor_for_logs is None:
            # Note that request.user is a Union[RemoteZulipServer, UserProfile, AnonymousUser],
            # if it is present.
            if hasattr(request, "user") and hasattr(
                    request.user, "format_requestor_for_logs"):
                requestor_for_logs = request.user.format_requestor_for_logs()
            else:
                requestor_for_logs = "unauth@{}".format(
                    get_subdomain(request) or "root")

        if response.streaming:
            assert isinstance(response, StreamingHttpResponse)
            content_iter: Optional[
                Iterator[bytes]] = response.streaming_content
            content = None
        else:
            content = response.content
            content_iter = None

        assert request_notes.client_name is not None and request_notes.log_data is not None
        write_log_line(
            request_notes.log_data,
            request.path,
            request.method,
            remote_ip,
            requestor_for_logs,
            request_notes.client_name,
            client_version=request_notes.client_version,
            status_code=response.status_code,
            error_content=content,
            error_content_iter=content_iter,
        )
        return response
Beispiel #7
0
def update_message_flags(
        request: HttpRequest,
        user_profile: UserProfile,
        messages: List[int] = REQ(json_validator=check_list(check_int)),
        operation: str = REQ("op"),
        flag: str = REQ(),
) -> HttpResponse:
    request_notes = RequestNotes.get_notes(request)
    assert request_notes.log_data is not None

    count = do_update_message_flags(user_profile, operation, flag, messages)

    target_count_str = str(len(messages))
    log_data_str = f"[{operation} {flag}/{target_count_str}] actually {count}"
    request_notes.log_data["extra"] = log_data_str

    return json_success(request, data={"messages": messages})
Beispiel #8
0
def get_and_set_request_language(
    request: HttpRequest, user_configured_language: str, testing_url_language: Optional[str] = None
) -> str:
    # We pick a language for the user as follows:
    # * First priority is the language in the URL, for debugging.
    # * If not in the URL, we use the language from the user's settings.
    request_language = testing_url_language
    if request_language is None:
        request_language = user_configured_language
    translation.activate(request_language)

    # We also want to save the language to the user's cookies, so that
    # something reasonable will happen in logged-in portico pages.
    # We accomplish that by setting a flag on the request which signals
    # to LocaleMiddleware to set the cookie on the response.
    RequestNotes.get_notes(request).set_language = translation.get_language()

    return request_language
Beispiel #9
0
    def handle(self, *args: Any, **options: Any) -> None:
        realm = self.get_realm(options)
        user = self.get_user(options["email"], realm)
        anchor = UserMessage.objects.filter(
            user_profile=user).order_by("-message")[200].message_id
        mock_request = HostRequestMock(
            post_data={
                "anchor": anchor,
                "num_before": 1200,
                "num_after": 200,
            },
            user_profile=user,
            meta_data={"REMOTE_ADDR": "127.0.0.1"},
            path="/",
        )
        mock_request.session = MockSession()
        RequestNotes.get_notes(mock_request).log_data = None

        profile_request(mock_request)
Beispiel #10
0
def api_fetch_api_key(
    request: HttpRequest, username: str = REQ(), password: str = REQ()
) -> HttpResponse:
    return_data: Dict[str, bool] = {}

    realm = get_realm_from_request(request)
    if realm is None:
        raise InvalidSubdomainError()

    if not ldap_auth_enabled(realm=realm):
        # In case we don't authenticate against LDAP, check for a valid
        # email. LDAP backend can authenticate against a non-email.
        validate_login_email(username)
    user_profile = authenticate(
        request=request, username=username, password=password, realm=realm, return_data=return_data
    )
    if return_data.get("inactive_user"):
        raise UserDeactivatedError()
    if return_data.get("inactive_realm"):
        raise RealmDeactivatedError()
    if return_data.get("password_auth_disabled"):
        raise PasswordAuthDisabledError()
    if return_data.get("password_reset_needed"):
        raise PasswordResetRequiredError()
    if user_profile is None:
        raise AuthenticationFailedError()

    assert user_profile.is_authenticated

    # Maybe sending 'user_logged_in' signal is the better approach:
    #   user_logged_in.send(sender=user_profile.__class__, request=request, user=user_profile)
    # Not doing this only because over here we don't add the user information
    # in the session. If the signal receiver assumes that we do then that
    # would cause problems.
    email_on_new_login(sender=user_profile.__class__, request=request, user=user_profile)

    # Mark this request as having a logged-in user for our server logs.
    process_client(request, user_profile)
    RequestNotes.get_notes(request).requestor_for_logs = user_profile.format_requestor_for_logs()

    api_key = get_api_key(user_profile)
    return json_success({"api_key": api_key, "email": user_profile.delivery_email})
Beispiel #11
0
def finish_handler(handler_id: int, event_queue_id: str,
                   contents: List[Dict[str,
                                       Any]], apply_markdown: bool) -> None:
    err_msg = f"Got error finishing handler for queue {event_queue_id}"
    try:
        # We do the import during runtime to avoid cyclic dependency
        # with zerver.lib.request
        from zerver.lib.request import RequestNotes
        from zerver.middleware import async_request_timer_restart

        # We call async_request_timer_restart here in case we are
        # being finished without any events (because another
        # get_events request has supplanted this request)
        handler = get_handler_by_id(handler_id)
        request = handler._request
        async_request_timer_restart(request)
        log_data = RequestNotes.get_notes(request).log_data
        assert log_data is not None
        if len(contents) != 1:
            log_data["extra"] = f"[{event_queue_id}/1]"
        else:
            log_data["extra"] = "[{}/1/{}]".format(event_queue_id,
                                                   contents[0]["type"])

        tornado.ioloop.IOLoop.current().add_callback(
            handler.zulip_finish,
            dict(result="success",
                 msg="",
                 events=contents,
                 queue_id=event_queue_id),
            request,
            apply_markdown=apply_markdown,
        )
    except OSError as e:
        if str(e) != "Stream is closed":
            logging.exception(err_msg, stack_info=True)
    except AssertionError as e:
        if str(e) != "Request closed":
            logging.exception(err_msg, stack_info=True)
    except Exception:
        logging.exception(err_msg, stack_info=True)
Beispiel #12
0
    def format(self, record: logging.LogRecord) -> str:
        from zerver.lib.request import get_current_request

        request = get_current_request()
        if not request:
            setattr(record, "user", None)
            setattr(record, "client", None)
            setattr(record, "url", None)
            setattr(record, "content_type", None)
            setattr(record, "custom_headers", None)
            setattr(record, "payload", None)
            return super().format(record)

        if request.content_type == "application/json":
            payload: Union[str, bytes] = request.body
        else:
            payload = request.POST["payload"]

        try:
            payload = orjson.dumps(orjson.loads(payload), option=orjson.OPT_INDENT_2).decode()
        except orjson.JSONDecodeError:
            pass

        header_text = "".join(
            f"{header}: {value}\n"
            for header, value in request.headers.items()
            if header.lower().startswith("x-")
        )

        from zerver.lib.request import RequestNotes

        client = RequestNotes.get_notes(request).client
        assert client is not None

        setattr(record, "user", f"{request.user.delivery_email} ({request.user.realm.string_id})")
        setattr(record, "client", client.name)
        setattr(record, "url", request.META.get("PATH_INFO", None))
        setattr(record, "content_type", request.content_type)
        setattr(record, "custom_headers", header_text or None)
        setattr(record, "payload", payload)
        return super().format(record)
Beispiel #13
0
def add_context(event: "Event", hint: "Hint") -> Optional["Event"]:
    if "exc_info" in hint:
        _, exc_value, _ = hint["exc_info"]
        # Ignore GeneratorExit, KeyboardInterrupt, and SystemExit exceptions
        if not isinstance(exc_value, Exception):
            return None
    from django.conf import settings

    from zerver.lib.request import RequestNotes, get_current_request
    from zerver.models import get_user_profile_by_id

    with capture_internal_exceptions():
        # event.user is the user context, from Sentry, which is
        # pre-populated with some keys via its Django integration:
        # https://docs.sentry.io/platforms/python/guides/django/enriching-error-data/additional-data/identify-user/
        event.setdefault("tags", {})
        user_info = event.get("user", {})
        if user_info.get("id"):
            user_profile = get_user_profile_by_id(user_info["id"])
            event["tags"]["realm"] = user_info[
                "realm"] = user_profile.realm.string_id or "root"
            with override_language(settings.LANGUAGE_CODE):
                # str() to force the lazy-translation to apply now,
                # since it won't serialize into json for Sentry otherwise
                user_info["role"] = str(user_profile.get_role_name())

        # These are PII, and should be scrubbed
        if "username" in user_info:
            del user_info["username"]
        if "email" in user_info:
            del user_info["email"]

        request = get_current_request()
        if request:
            request_notes = RequestNotes.get_notes(request)
            if request_notes.client is not None:
                event["tags"]["client"] = request_notes.client.name
            if request_notes.realm is not None:
                event["tags"].setdefault("realm",
                                         request_notes.realm.string_id)
    return event
    def get_extras(
            cls,
            q: str,
            request: Optional[HttpRequest] = None) -> Tuple[str, List[object]]:
        """
        Return extra SQL and params to be attached to end of current Query's
        SQL and params. The return format matches the format that should be used
        for providing raw SQL with params to Django's .raw():
        https://docs.djangoproject.com/en/3.2/topics/db/sql/#passing-parameters-into-raw

        Here we ensure that results are limited to the subdomain of the request
        and also exclude bots, as we currently don't want them to be managed by SCIM2.
        """
        assert request is not None
        realm = RequestNotes.get_notes(request).realm
        assert realm is not None

        return (
            "AND zerver_realm.id = %s AND zerver_userprofile.is_bot = False ORDER BY zerver_userprofile.id",
            [realm.id],
        )
Beispiel #15
0
def create_mirrored_message_users(request: HttpRequest,
                                  user_profile: UserProfile,
                                  recipients: Iterable[str]) -> UserProfile:
    if "sender" not in request.POST:
        raise InvalidMirrorInput("No sender")

    sender_email = request.POST["sender"].strip().lower()
    referenced_users = {sender_email}
    if request.POST["type"] == "private":
        for email in recipients:
            referenced_users.add(email.lower())

    client = RequestNotes.get_notes(request).client
    assert client is not None

    if client.name == "zephyr_mirror":
        user_check = same_realm_zephyr_user
        fullname_function = compute_mit_user_fullname
    elif client.name == "irc_mirror":
        user_check = same_realm_irc_user
        fullname_function = compute_irc_user_fullname
    elif client.name in ("jabber_mirror", "JabberMirror"):
        user_check = same_realm_jabber_user
        fullname_function = compute_jabber_user_fullname
    else:
        raise InvalidMirrorInput("Unrecognized mirroring client")

    for email in referenced_users:
        # Check that all referenced users are in our realm:
        if not user_check(user_profile, email):
            raise InvalidMirrorInput("At least one user cannot be mirrored")

    # Create users for the referenced users, if needed.
    for email in referenced_users:
        create_mirror_user_if_needed(user_profile.realm, email,
                                     fullname_function)

    sender = get_user_including_cross_realm(sender_email, user_profile.realm)
    return sender
Beispiel #16
0
    def rate_limit_request(self, request: HttpRequest) -> None:
        from zerver.lib.request import RequestNotes

        ratelimited, time = self.rate_limit()
        request_notes = RequestNotes.get_notes(request)

        request_notes.ratelimits_applied.append(
            RateLimitResult(
                entity=self,
                secs_to_freedom=time,
                remaining=0,
                over_limit=ratelimited,
            ))
        # Abort this request if the user is over their rate limits
        if ratelimited:
            # Pass information about what kind of entity got limited in the exception:
            raise RateLimited(time)

        calls_remaining, seconds_until_reset = self.api_calls_left()

        request_notes.ratelimits_applied[-1].remaining = calls_remaining
        request_notes.ratelimits_applied[
            -1].secs_to_freedom = seconds_until_reset
Beispiel #17
0
def update_user_activity(request: HttpRequest, user_profile: UserProfile,
                         query: Optional[str]) -> None:
    # update_active_status also pushes to RabbitMQ, and it seems
    # redundant to log that here as well.
    if request.META["PATH_INFO"] == "/json/users/me/presence":
        return

    request_notes = RequestNotes.get_notes(request)
    if query is not None:
        pass
    elif request_notes.query is not None:
        query = request_notes.query
    else:
        query = request.META["PATH_INFO"]

    assert request_notes.client is not None
    event = {
        "query": query,
        "user_profile_id": user_profile.id,
        "time": datetime_to_timestamp(timezone_now()),
        "client_id": request_notes.client.id,
    }
    queue_json_publish("user_activity", event, lambda event: None)
Beispiel #18
0
def finish_mobile_flow(request: HttpRequest, user_profile: UserProfile, otp: str) -> HttpResponse:
    # For the mobile OAuth flow, we send the API key and other
    # necessary details in a redirect to a zulip:// URI scheme.
    api_key = get_api_key(user_profile)
    response = create_response_for_otp_flow(
        api_key, otp, user_profile, encrypted_key_field_name="otp_encrypted_api_key"
    )

    # Since we are returning an API key instead of going through
    # the Django login() function (which creates a browser
    # session, etc.), the "new login" signal handler (which
    # triggers an email notification new logins) will not run
    # automatically.  So we call it manually here.
    #
    # Arguably, sending a fake 'user_logged_in' signal would be a better approach:
    #   user_logged_in.send(sender=user_profile.__class__, request=request, user=user_profile)
    email_on_new_login(sender=user_profile.__class__, request=request, user=user_profile)

    # Mark this request as having a logged-in user for our server logs.
    process_client(request, user_profile)
    RequestNotes.get_notes(request).requestor_for_logs = user_profile.format_requestor_for_logs()

    return response
Beispiel #19
0
def update_active_status_backend(
    request: HttpRequest,
    user_profile: UserProfile,
    status: str = REQ(),
    ping_only: bool = REQ(json_validator=check_bool, default=False),
    new_user_input: bool = REQ(json_validator=check_bool, default=False),
    slim_presence: bool = REQ(json_validator=check_bool, default=False),
) -> HttpResponse:
    status_val = UserPresence.status_from_string(status)
    if status_val is None:
        raise JsonableError(_("Invalid status: {}").format(status))
    elif user_profile.presence_enabled:
        client = RequestNotes.get_notes(request).client
        assert client is not None
        update_user_presence(user_profile, client, timezone_now(), status_val, new_user_input)

    if ping_only:
        ret: Dict[str, Any] = {}
    else:
        ret = get_presence_response(user_profile, slim_presence)

    if user_profile.realm.is_zephyr_mirror_realm:
        # In zephyr mirroring realms, users can't see the presence of other
        # users, but each user **is** interested in whether their mirror bot
        # (running as their user) has been active.
        try:
            activity = UserActivity.objects.get(
                user_profile=user_profile, query="get_events", client__name="zephyr_mirror"
            )

            ret["zephyr_mirror_active"] = activity.last_visit > timezone_now() - datetime.timedelta(
                minutes=5
            )
        except UserActivity.DoesNotExist:
            ret["zephyr_mirror_active"] = False

    return json_success(request, data=ret)
Beispiel #20
0
def api_slack_webhook(
    request: HttpRequest,
    user_profile: UserProfile,
    user_name: str = REQ(),
    text: str = REQ(),
    channel_name: str = REQ(),
    stream: str = REQ(default="slack"),
    channels_map_to_topics: str = REQ(default="1"),
) -> HttpResponse:

    if channels_map_to_topics not in list(VALID_OPTIONS.values()):
        raise JsonableError(_("Error: channels_map_to_topics parameter other than 0 or 1"))

    if channels_map_to_topics == VALID_OPTIONS["SHOULD_BE_MAPPED"]:
        subject = f"channel: {channel_name}"
    else:
        stream = channel_name
        subject = _("Message from Slack")

    content = ZULIP_MESSAGE_TEMPLATE.format(message_sender=user_name, text=text)
    client = RequestNotes.get_notes(request).client
    assert client is not None
    check_send_stream_message(user_profile, client, stream, subject, content)
    return json_success()
Beispiel #21
0
    def convert_tornado_request_to_django_request(self) -> HttpRequest:
        # This takes the WSGI environment that Tornado received (which
        # fully describes the HTTP request that was sent to Tornado)
        # and pass it to Django's WSGIRequest to generate a Django
        # HttpRequest object with the original Tornado request's HTTP
        # headers, parameters, etc.
        environ = WSGIContainer.environ(self.request)
        environ["PATH_INFO"] = urllib.parse.unquote(environ["PATH_INFO"])

        # Django WSGIRequest setup code that should match logic from
        # Django's WSGIHandler.__call__ before the call to
        # `get_response()`.
        set_script_prefix(get_script_name(environ))
        signals.request_started.send(sender=self.__class__)
        request = WSGIRequest(environ)

        # We do the import during runtime to avoid cyclic dependency
        from zerver.lib.request import RequestNotes

        # Provide a way for application code to access this handler
        # given the HttpRequest object.
        RequestNotes.get_notes(request).tornado_handler = weakref.ref(self)

        return request
Beispiel #22
0
    def process_request(self, request: HttpRequest) -> Optional[HttpResponse]:
        # Match against ALLOWED_HOSTS, which is rather permissive;
        # failure will raise DisallowedHost, which is a 400.
        request.get_host()

        # This check is important to avoid doing the extra work of
        # `get_realm` (which does a database query that could be
        # problematic for Tornado).  Also the error page below is only
        # appropriate for a page visited in a browser, not the API.
        #
        # API authentication will end up checking for an invalid
        # realm, and throw a JSON-format error if appropriate.
        if request.path.startswith(("/static/", "/api/", "/json/")):
            return None

        subdomain = get_subdomain(request)
        if subdomain != Realm.SUBDOMAIN_FOR_ROOT_DOMAIN:
            request_notes = RequestNotes.get_notes(request)
            try:
                request_notes.realm = get_realm(subdomain)
            except Realm.DoesNotExist:
                return render(request, "zerver/invalid_realm.html", status=404)
            request_notes.has_fetched_realm = True
        return None
Beispiel #23
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(request, data=result)
Beispiel #24
0
def check_send_webhook_message(
    request: HttpRequest,
    user_profile: UserProfile,
    topic: str,
    body: str,
    complete_event_type: Optional[str] = None,
    stream: Optional[str] = REQ(default=None),
    user_specified_topic: Optional[str] = REQ("topic", default=None),
    only_events: Optional[List[str]] = REQ(
        default=None, json_validator=check_list(check_string)),
    exclude_events: Optional[List[str]] = REQ(
        default=None, json_validator=check_list(check_string)),
    unquote_url_parameters: bool = False,
) -> None:
    if complete_event_type is not None:
        # Here, we implement Zulip's generic support for filtering
        # events sent by the third-party service.
        #
        # If complete_event_type is passed to this function, we will check the event
        # type against user configured lists of only_events and exclude events.
        # If the event does not satisfy the configuration, the function will return
        # without sending any messages.
        #
        # We match items in only_events and exclude_events using Unix
        # shell-style wildcards.
        if (only_events is not None
                and all(not fnmatch.fnmatch(complete_event_type, pattern)
                        for pattern in only_events)) or (
                            exclude_events is not None and any(
                                fnmatch.fnmatch(complete_event_type, pattern)
                                for pattern in exclude_events)):
            return

    client = RequestNotes.get_notes(request).client
    assert client is not None
    if stream is None:
        assert user_profile.bot_owner is not None
        check_send_private_message(user_profile, client,
                                   user_profile.bot_owner, body)
    else:
        # Some third-party websites (such as Atlassian's Jira), tend to
        # double escape their URLs in a manner that escaped space characters
        # (%20) are never properly decoded. We work around that by making sure
        # that the URL parameters are decoded on our end.
        if unquote_url_parameters:
            stream = unquote(stream)

        if user_specified_topic is not None:
            topic = user_specified_topic
            if unquote_url_parameters:
                topic = unquote(topic)

        try:
            if stream.isdecimal():
                check_send_stream_message_by_id(user_profile, client,
                                                int(stream), topic, body)
            else:
                check_send_stream_message(user_profile, client, stream, topic,
                                          body)
        except StreamDoesNotExistError:
            # A PM will be sent to the bot_owner by check_message, notifying
            # that the webhook bot just tried to send a message to a non-existent
            # stream, so we don't need to re-raise it since it clutters up
            # webhook-errors.log
            pass
Beispiel #25
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(request)
Beispiel #26
0
    def save(self) -> None:
        """
        This method is called at the end of operations modifying a user,
        and is responsible for actually applying the requested changes,
        writing them to the database.
        """
        realm = RequestNotes.get_notes(self._request).realm
        assert realm is not None

        email_new_value = getattr(self, "_email_new_value", None)
        is_active_new_value = getattr(self, "_is_active_new_value", None)
        full_name_new_value = getattr(self, "_full_name_new_value", None)
        password = getattr(self, "_password_set_to", None)

        # Clean up the internal "pending change" state, now that we've
        # fetched the values:
        self._email_new_value = None
        self._is_active_new_value = None
        self._full_name_new_value = None
        self._password_set_to = None

        if email_new_value:
            try:
                # Note that the validate_email check that usually
                # appears adjacent to email_allowed_for_realm is
                # present in save().
                email_allowed_for_realm(email_new_value, realm)
            except DomainNotAllowedForRealmError:
                raise scim_exceptions.BadRequestError(
                    "This email domain isn't allowed in this organization.")
            except DisposableEmailError:  # nocoverage
                raise scim_exceptions.BadRequestError(
                    "Disposable email domains are not allowed for this realm.")
            except EmailContainsPlusError:  # nocoverage
                raise scim_exceptions.BadRequestError(
                    "Email address can't contain + characters.")

            try:
                validate_email_not_already_in_realm(realm, email_new_value)
            except ValidationError as e:
                raise ConflictError("Email address already in use: " + str(e))

        if self.is_new_user():
            assert full_name_new_value is not None
            self.obj = do_create_user(
                email_new_value,
                password,
                realm,
                full_name_new_value,
                acting_user=None,
            )
            return

        # TODO: The below operations should ideally be executed in a single
        # atomic block to avoid failing with partial changes getting saved.
        # This can be fixed once we figure out how do_deactivate_user can be run
        # inside an atomic block.

        # We process full_name first here, since it's the only one that can fail.
        if full_name_new_value:
            check_change_full_name(self.obj,
                                   full_name_new_value,
                                   acting_user=None)

        if email_new_value:
            do_change_user_delivery_email(self.obj, email_new_value)

        if is_active_new_value is not None and is_active_new_value:
            do_reactivate_user(self.obj, acting_user=None)
        elif is_active_new_value is not None and not is_active_new_value:
            do_deactivate_user(self.obj, acting_user=None)
Beispiel #27
0
def events_register_backend(
    request: HttpRequest,
    maybe_user_profile: Union[UserProfile, AnonymousUser],
    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(json_validator=check_int, default=0, documentation_pending=True),
) -> HttpResponse:
    if maybe_user_profile.is_authenticated:
        user_profile = maybe_user_profile
        assert isinstance(user_profile, UserProfile)
        realm = user_profile.realm

        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)
    else:
        user_profile = None
        realm = get_valid_realm_from_request(request)

        if not realm.allow_web_public_streams_access():
            raise MissingAuthenticationError()

        all_public_streams = False

    if client_capabilities is None:
        client_capabilities = {}

    client = RequestNotes.get_notes(request).client
    assert client is not None

    ret = do_events_register(
        user_profile,
        realm,
        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)
Beispiel #28
0
def build_page_params_for_home_page_load(
    request: HttpRequest,
    user_profile: Optional[UserProfile],
    realm: Realm,
    insecure_desktop_app: bool,
    narrow: List[List[str]],
    narrow_stream: Optional[Stream],
    narrow_topic: Optional[str],
    first_in_realm: bool,
    prompt_for_invites: bool,
    needs_tutorial: bool,
) -> Tuple[int, Dict[str, Any]]:
    """
    This function computes page_params for when we load the home page.

    The page_params data structure gets sent to the client.
    """
    client_capabilities = {
        "notification_settings_null": True,
        "bulk_message_deletion": True,
        "user_avatar_url_field_optional": True,
        "stream_typing_notifications": False,  # Set this to True when frontend support is implemented.
        "user_settings_object": True,
    }

    if user_profile is not None:
        client = RequestNotes.get_notes(request).client
        assert client is not None
        register_ret = do_events_register(
            user_profile,
            client,
            apply_markdown=True,
            client_gravatar=True,
            slim_presence=True,
            client_capabilities=client_capabilities,
            narrow=narrow,
            include_streams=False,
        )
    else:
        # Since events for spectator is not implemented, we only fetch the data
        # at the time of request and don't register for any events.
        # TODO: Implement events for spectator.
        from zerver.lib.events import fetch_initial_state_data, post_process_state

        register_ret = fetch_initial_state_data(
            user_profile,
            realm=realm,
            event_types=None,
            queue_id=None,
            client_gravatar=False,
            user_avatar_url_field_optional=client_capabilities["user_avatar_url_field_optional"],
            user_settings_object=client_capabilities["user_settings_object"],
            slim_presence=False,
            include_subscribers=False,
            include_streams=False,
        )

        post_process_state(user_profile, register_ret, False)

    furthest_read_time = get_furthest_read_time(user_profile)

    request_language = get_and_set_request_language(
        request,
        register_ret["user_settings"]["default_language"],
        translation.get_language_from_path(request.path_info),
    )

    two_fa_enabled = settings.TWO_FACTOR_AUTHENTICATION_ENABLED and user_profile is not None
    billing_info = get_billing_info(user_profile)
    user_permission_info = get_user_permission_info(user_profile)

    # Pass parameters to the client-side JavaScript code.
    # These end up in a JavaScript Object named 'page_params'.
    page_params = dict(
        ## Server settings.
        test_suite=settings.TEST_SUITE,
        insecure_desktop_app=insecure_desktop_app,
        login_page=settings.HOME_NOT_LOGGED_IN,
        warn_no_email=settings.WARN_NO_EMAIL,
        search_pills_enabled=settings.SEARCH_PILLS_ENABLED,
        # Only show marketing email settings if on Zulip Cloud
        corporate_enabled=settings.CORPORATE_ENABLED,
        ## Misc. extra data.
        language_list=get_language_list(),
        needs_tutorial=needs_tutorial,
        first_in_realm=first_in_realm,
        prompt_for_invites=prompt_for_invites,
        furthest_read_time=furthest_read_time,
        bot_types=get_bot_types(user_profile),
        two_fa_enabled=two_fa_enabled,
        apps_page_url=get_apps_page_url(),
        show_billing=billing_info.show_billing,
        promote_sponsoring_zulip=promote_sponsoring_zulip_in_realm(realm),
        show_plans=billing_info.show_plans,
        show_webathena=user_permission_info.show_webathena,
        # Adding two_fa_enabled as condition saves us 3 queries when
        # 2FA is not enabled.
        two_fa_enabled_user=two_fa_enabled and bool(default_device(user_profile)),
        is_spectator=user_profile is None,
        # There is no event queue for spectators since
        # events support for spectators is not implemented yet.
        no_event_queue=user_profile is None,
    )

    for field_name in register_ret.keys():
        page_params[field_name] = register_ret[field_name]

    if narrow_stream is not None:
        # In narrow_stream context, initial pointer is just latest message
        recipient = narrow_stream.recipient
        try:
            max_message_id = (
                Message.objects.filter(recipient=recipient).order_by("id").reverse()[0].id
            )
        except IndexError:
            max_message_id = -1
        page_params["narrow_stream"] = narrow_stream.name
        if narrow_topic is not None:
            page_params["narrow_topic"] = narrow_topic
        page_params["narrow"] = [dict(operator=term[0], operand=term[1]) for term in narrow]
        page_params["max_message_id"] = max_message_id
        assert isinstance(page_params["user_settings"], dict)
        page_params["user_settings"]["enable_desktop_notifications"] = False

    page_params["translation_data"] = get_language_translation_data(request_language)

    return register_ret["queue_id"], page_params
Beispiel #29
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)
Beispiel #30
0
 def get_extra_filter_kwargs(request: HttpRequest, *args: Any,
                             **kwargs: Any) -> Dict[str, object]:
     realm = RequestNotes.get_notes(request).realm
     assert realm is not None
     return {"realm_id": realm.id, "is_bot": False}