snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
0
zerver/views/development/__init__.py
Normal file
0
zerver/views/development/__init__.py
Normal file
23
zerver/views/development/cache.py
Normal file
23
zerver/views/development/cache.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import os
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from zerver.decorator import require_post
|
||||
from zerver.lib.cache import get_cache_backend
|
||||
from zerver.lib.per_request_cache import flush_per_request_caches
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.models.clients import clear_client_cache
|
||||
|
||||
ZULIP_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../../")
|
||||
|
||||
|
||||
# This is used only by the Puppeteer tests to clear all the cache after each run.
|
||||
@csrf_exempt
|
||||
@require_post
|
||||
def remove_caches(request: HttpRequest) -> HttpResponse: # nocoverage
|
||||
cache = get_cache_backend(None)
|
||||
cache.clear()
|
||||
clear_client_cache()
|
||||
flush_per_request_caches()
|
||||
return json_success(request)
|
||||
20
zerver/views/development/camo.py
Normal file
20
zerver/views/development/camo.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseForbidden
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
|
||||
from zerver.lib.camo import is_camo_url_valid
|
||||
|
||||
|
||||
def handle_camo_url(
|
||||
request: HttpRequest, digest: str, received_url: str
|
||||
) -> HttpResponse: # nocoverage
|
||||
original_url = bytes.fromhex(received_url).decode()
|
||||
if is_camo_url_valid(digest, original_url):
|
||||
original_url = urljoin("/", original_url)
|
||||
if url_has_allowed_host_and_scheme(original_url, allowed_hosts=None):
|
||||
return redirect(original_url)
|
||||
return HttpResponseForbidden("<p>Not a valid URL.</p>")
|
||||
else:
|
||||
return HttpResponseForbidden("<p>Not a valid URL.</p>")
|
||||
165
zerver/views/development/dev_login.py
Normal file
165
zerver/views/development/dev_login.py
Normal file
@@ -0,0 +1,165 @@
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from zerver.context_processors import get_realm_from_request
|
||||
from zerver.decorator import do_login, require_post
|
||||
from zerver.lib.exceptions import (
|
||||
AuthenticationFailedError,
|
||||
InvalidSubdomainError,
|
||||
JsonableError,
|
||||
RealmDeactivatedError,
|
||||
UserDeactivatedError,
|
||||
)
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.lib.subdomains import get_subdomain
|
||||
from zerver.lib.typed_endpoint import typed_endpoint
|
||||
from zerver.lib.validator import validate_login_email
|
||||
from zerver.models import Realm, UserProfile
|
||||
from zerver.models.realms import get_realm
|
||||
from zerver.views.auth import get_safe_redirect_to
|
||||
from zerver.views.errors import config_error
|
||||
from zproject.backends import dev_auth_enabled
|
||||
|
||||
|
||||
def get_dev_users(realm: Realm | None = None, extra_users_count: int = 10) -> list[UserProfile]:
|
||||
# Development environments usually have only a few users, but
|
||||
# it still makes sense to limit how many extra users we render to
|
||||
# support performance testing with DevAuthBackend.
|
||||
if realm is not None:
|
||||
users_query = UserProfile.objects.select_related("realm").filter(
|
||||
is_bot=False, is_active=True, realm=realm
|
||||
)
|
||||
else:
|
||||
users_query = UserProfile.objects.select_related("realm").filter(
|
||||
is_bot=False, is_active=True
|
||||
)
|
||||
|
||||
shakespearian_users = users_query.exclude(email__startswith="extrauser").order_by("email")
|
||||
extra_users = users_query.filter(email__startswith="extrauser").order_by("email")
|
||||
# Limit the number of extra users we offer by default
|
||||
extra_users = extra_users[0:extra_users_count]
|
||||
users = [*shakespearian_users, *extra_users]
|
||||
return users
|
||||
|
||||
|
||||
def add_dev_login_context(realm: Realm | None, context: dict[str, Any]) -> None:
|
||||
users = get_dev_users(realm)
|
||||
context["current_realm"] = realm
|
||||
context["all_realms"] = Realm.objects.filter(deactivated_redirect__isnull=True)
|
||||
|
||||
def sort(lst: list[UserProfile]) -> list[UserProfile]:
|
||||
return sorted(lst, key=lambda u: u.delivery_email)
|
||||
|
||||
context["direct_owners"] = sort([u for u in users if u.is_realm_owner])
|
||||
context["direct_admins"] = sort([u for u in users if u.is_realm_admin and not u.is_realm_owner])
|
||||
context["guest_users"] = sort([u for u in users if u.is_guest])
|
||||
context["direct_moderators"] = sort([u for u in users if u.role == UserProfile.ROLE_MODERATOR])
|
||||
context["direct_users"] = sort([u for u in users if not (u.is_guest or u.is_moderator)])
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@typed_endpoint
|
||||
def dev_direct_login(
|
||||
request: HttpRequest,
|
||||
*,
|
||||
next: str = "/",
|
||||
) -> HttpResponse:
|
||||
# This function allows logging in without a password and should only be called
|
||||
# in development environments. It may be called if the DevAuthBackend is included
|
||||
# in settings.AUTHENTICATION_BACKENDS
|
||||
if (not dev_auth_enabled()) or settings.PRODUCTION:
|
||||
# This check is probably not required, since authenticate would fail without
|
||||
# an enabled DevAuthBackend.
|
||||
return config_error(request, "dev_not_supported")
|
||||
|
||||
subdomain = get_subdomain(request)
|
||||
realm = get_realm(subdomain)
|
||||
|
||||
if request.POST.get("prefers_web_public_view") == "Anonymous login":
|
||||
redirect_to = get_safe_redirect_to(next, realm.url)
|
||||
return HttpResponseRedirect(redirect_to)
|
||||
|
||||
email = request.POST["direct_email"]
|
||||
user_profile = authenticate(dev_auth_username=email, realm=realm)
|
||||
if user_profile is None:
|
||||
return config_error(request, "dev_not_supported")
|
||||
assert isinstance(user_profile, UserProfile)
|
||||
do_login(request, user_profile)
|
||||
|
||||
redirect_to = get_safe_redirect_to(next, user_profile.realm.url)
|
||||
return HttpResponseRedirect(redirect_to)
|
||||
|
||||
|
||||
def check_dev_auth_backend() -> None:
|
||||
if settings.PRODUCTION:
|
||||
raise JsonableError(_("Endpoint not available in production."))
|
||||
if not dev_auth_enabled():
|
||||
raise JsonableError(_("DevAuthBackend not enabled."))
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_post
|
||||
@typed_endpoint
|
||||
def api_dev_fetch_api_key(request: HttpRequest, *, username: str) -> HttpResponse:
|
||||
"""This function allows logging in without a password on the Zulip
|
||||
mobile apps when connecting to a Zulip development environment. It
|
||||
requires DevAuthBackend to be included in settings.AUTHENTICATION_BACKENDS.
|
||||
"""
|
||||
check_dev_auth_backend()
|
||||
|
||||
# Django invokes authenticate methods by matching arguments, and this
|
||||
# authentication flow will not invoke LDAP authentication because of
|
||||
# this condition of Django so no need to check if LDAP backend is
|
||||
# enabled.
|
||||
validate_login_email(username)
|
||||
realm = get_realm_from_request(request)
|
||||
if realm is None:
|
||||
raise InvalidSubdomainError
|
||||
return_data: dict[str, bool] = {}
|
||||
user_profile = authenticate(dev_auth_username=username, realm=realm, return_data=return_data)
|
||||
if return_data.get("inactive_realm"):
|
||||
raise RealmDeactivatedError
|
||||
if return_data.get("inactive_user"):
|
||||
raise UserDeactivatedError
|
||||
if return_data.get("invalid_subdomain"): # nocoverage
|
||||
raise InvalidSubdomainError
|
||||
if user_profile is None:
|
||||
# Since we're not actually checking passwords, this condition
|
||||
# is when one's attempting to send an email address that
|
||||
# doesn't have an account, i.e. it's definitely invalid username.
|
||||
raise AuthenticationFailedError
|
||||
assert isinstance(user_profile, UserProfile)
|
||||
|
||||
do_login(request, user_profile)
|
||||
api_key = user_profile.api_key
|
||||
return json_success(
|
||||
request,
|
||||
data={"api_key": api_key, "email": user_profile.delivery_email, "user_id": user_profile.id},
|
||||
)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def api_dev_list_users(request: HttpRequest) -> HttpResponse:
|
||||
check_dev_auth_backend()
|
||||
|
||||
users = get_dev_users()
|
||||
return json_success(
|
||||
request,
|
||||
data=dict(
|
||||
direct_admins=[
|
||||
dict(email=u.delivery_email, realm_url=u.realm.url)
|
||||
for u in users
|
||||
if u.is_realm_admin
|
||||
],
|
||||
direct_users=[
|
||||
dict(email=u.delivery_email, realm_url=u.realm.url)
|
||||
for u in users
|
||||
if not u.is_realm_admin
|
||||
],
|
||||
),
|
||||
)
|
||||
167
zerver/views/development/email_log.py
Normal file
167
zerver/views/development/email_log.py
Normal file
@@ -0,0 +1,167 @@
|
||||
import os
|
||||
from contextlib import suppress
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import orjson
|
||||
from django.conf import settings
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.views.decorators.http import require_safe
|
||||
|
||||
from confirmation.models import Confirmation
|
||||
from zerver.actions.realm_settings import do_send_realm_reactivation_email
|
||||
from zerver.actions.user_settings import do_change_user_delivery_email
|
||||
from zerver.actions.users import change_user_is_active
|
||||
from zerver.lib.email_notifications import enqueue_welcome_emails, send_account_registered_email
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.lib.typed_endpoint import typed_endpoint
|
||||
from zerver.models import Realm
|
||||
from zerver.models.realms import get_realm
|
||||
from zerver.models.streams import get_realm_stream
|
||||
from zerver.models.users import get_user_by_delivery_email
|
||||
from zerver.views.invite import INVITATION_LINK_VALIDITY_MINUTES
|
||||
from zproject.email_backends import get_forward_address, set_forward_address
|
||||
|
||||
ZULIP_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../")
|
||||
|
||||
|
||||
@typed_endpoint
|
||||
def email_page(request: HttpRequest, *, forward_address: str | None = None) -> HttpResponse:
|
||||
if request.method == "POST":
|
||||
assert forward_address is not None
|
||||
set_forward_address(forward_address)
|
||||
return json_success(request)
|
||||
try:
|
||||
with open(settings.EMAIL_CONTENT_LOG_PATH, "r+") as f:
|
||||
content = f.read()
|
||||
except FileNotFoundError:
|
||||
content = ""
|
||||
return render(
|
||||
request,
|
||||
"zerver/development/email_log.html",
|
||||
{"log": content, "forward_address": get_forward_address()},
|
||||
)
|
||||
|
||||
|
||||
def clear_emails(request: HttpRequest) -> HttpResponse:
|
||||
with suppress(FileNotFoundError):
|
||||
os.remove(settings.EMAIL_CONTENT_LOG_PATH)
|
||||
return redirect(email_page)
|
||||
|
||||
|
||||
@require_safe
|
||||
def generate_all_emails(request: HttpRequest) -> HttpResponse:
|
||||
# We import Django test client and override_settings inside
|
||||
# the view function, because it isn't needed in production
|
||||
# elsewhere, and not importing it saves ~50ms of unnecessary
|
||||
# manage.py startup time.
|
||||
|
||||
from django.test import Client, override_settings
|
||||
|
||||
# override_settings is used as a hacky workaround to disable
|
||||
# rate limiting; this is fine because this view only runs in
|
||||
# the development environment.
|
||||
with override_settings(RATE_LIMITING=False):
|
||||
client = Client()
|
||||
|
||||
# write fake data for all variables
|
||||
registered_email = "hamlet@zulip.com"
|
||||
unregistered_email_1 = "new-person@zulip.com"
|
||||
unregistered_email_2 = "new-person-2@zulip.com"
|
||||
invite_expires_in_minutes = INVITATION_LINK_VALIDITY_MINUTES
|
||||
realm = get_realm("zulip")
|
||||
other_realm = Realm.objects.exclude(string_id="zulip").first()
|
||||
user = get_user_by_delivery_email(registered_email, realm)
|
||||
|
||||
# Password reset emails
|
||||
# active account in realm
|
||||
result = client.post(
|
||||
"/accounts/password/reset/", {"email": registered_email}, HTTP_HOST=realm.host
|
||||
)
|
||||
assert result.status_code == 302
|
||||
# deactivated user
|
||||
change_user_is_active(user, False)
|
||||
result = client.post(
|
||||
"/accounts/password/reset/", {"email": registered_email}, HTTP_HOST=realm.host
|
||||
)
|
||||
assert result.status_code == 302
|
||||
change_user_is_active(user, True)
|
||||
# account on different realm
|
||||
assert other_realm is not None
|
||||
result = client.post(
|
||||
"/accounts/password/reset/", {"email": registered_email}, HTTP_HOST=other_realm.host
|
||||
)
|
||||
assert result.status_code == 302
|
||||
# no account anywhere
|
||||
result = client.post(
|
||||
"/accounts/password/reset/", {"email": unregistered_email_1}, HTTP_HOST=realm.host
|
||||
)
|
||||
assert result.status_code == 302
|
||||
|
||||
# Confirm account email
|
||||
result = client.post(
|
||||
"/accounts/home/", {"email": unregistered_email_1}, HTTP_HOST=realm.host
|
||||
)
|
||||
assert result.status_code == 302
|
||||
|
||||
# Find account email
|
||||
result = client.post("/accounts/find/", {"emails": registered_email}, HTTP_HOST=realm.host)
|
||||
assert result.status_code == 200
|
||||
|
||||
# New login email
|
||||
logged_in = client.login(dev_auth_username=registered_email, realm=realm)
|
||||
assert logged_in
|
||||
|
||||
# New user invite and reminder emails
|
||||
stream = get_realm_stream("Denmark", user.realm.id)
|
||||
result = client.post(
|
||||
"/json/invites",
|
||||
{
|
||||
"invitee_emails": unregistered_email_2,
|
||||
"invite_expires_in_minutes": invite_expires_in_minutes,
|
||||
"stream_ids": orjson.dumps([stream.id]).decode(),
|
||||
},
|
||||
HTTP_HOST=realm.host,
|
||||
)
|
||||
assert result.status_code == 200
|
||||
|
||||
# Verification for new email
|
||||
result = client.patch(
|
||||
"/json/settings",
|
||||
urlencode({"email": "hamlets-new@zulip.com"}),
|
||||
content_type="application/x-www-form-urlencoded",
|
||||
HTTP_HOST=realm.host,
|
||||
)
|
||||
assert result.status_code == 200
|
||||
|
||||
# Email change successful
|
||||
key = (
|
||||
Confirmation.objects.filter(type=Confirmation.EMAIL_CHANGE)
|
||||
.latest("id")
|
||||
.confirmation_key
|
||||
)
|
||||
user_profile = get_user_by_delivery_email(registered_email, realm)
|
||||
result = client.post("/accounts/confirm_new_email/", {"key": key})
|
||||
assert result.status_code == 200
|
||||
|
||||
# Reset the email value so we can run this again
|
||||
do_change_user_delivery_email(user_profile, registered_email, acting_user=None)
|
||||
|
||||
# Initial email with new account information for normal user
|
||||
send_account_registered_email(user_profile)
|
||||
|
||||
# Onboarding emails for normal user
|
||||
enqueue_welcome_emails(user_profile)
|
||||
|
||||
# Initial email with new account information for admin user
|
||||
send_account_registered_email(get_user_by_delivery_email("iago@zulip.com", realm))
|
||||
|
||||
# Onboarding emails for admin user
|
||||
enqueue_welcome_emails(
|
||||
get_user_by_delivery_email("iago@zulip.com", realm), realm_creation=True
|
||||
)
|
||||
|
||||
# Realm reactivation email
|
||||
do_send_realm_reactivation_email(realm, acting_user=None)
|
||||
|
||||
return redirect(email_page)
|
||||
57
zerver/views/development/help.py
Normal file
57
zerver/views/development/help.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import os
|
||||
|
||||
import werkzeug
|
||||
from django.conf import settings
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
def help_dev_mode_view(request: HttpRequest, subpath: str = "") -> HttpResponse:
|
||||
"""
|
||||
Dev only view that displays help information for setting up the
|
||||
help center dev server in the default `run-dev` mode where the
|
||||
help center server is not running. Also serves raw MDX content when
|
||||
`raw` query param is passed is passed.
|
||||
"""
|
||||
|
||||
def read_mdx_file(filename: str) -> HttpResponse:
|
||||
file_path = os.path.join(
|
||||
settings.DEPLOY_ROOT, "starlight_help", "src", "content", "docs", f"{filename}.mdx"
|
||||
)
|
||||
try:
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
return HttpResponse(content, content_type="text/plain")
|
||||
except OSError:
|
||||
return HttpResponse("Error reading MDX file", status=500)
|
||||
|
||||
mdx_file_exists = False
|
||||
is_requesting_raw_file = request.GET.get("raw") == ""
|
||||
|
||||
if subpath:
|
||||
subpath = werkzeug.utils.secure_filename(subpath)
|
||||
raw_url = f"/help/{subpath}?raw"
|
||||
mdx_path = os.path.join(
|
||||
settings.DEPLOY_ROOT, "starlight_help", "src", "content", "docs", f"{subpath}.mdx"
|
||||
)
|
||||
mdx_file_exists = os.path.exists(mdx_path) and "/include/" not in mdx_path
|
||||
if mdx_file_exists and is_requesting_raw_file:
|
||||
return read_mdx_file(subpath)
|
||||
else:
|
||||
if request.path.endswith("/"):
|
||||
raw_url = "/help/?raw"
|
||||
else:
|
||||
raw_url = "/help?raw"
|
||||
mdx_file_exists = True
|
||||
if is_requesting_raw_file:
|
||||
return read_mdx_file("index")
|
||||
|
||||
return render(
|
||||
request,
|
||||
"zerver/development/dev_help.html",
|
||||
{
|
||||
"subpath": subpath,
|
||||
"mdx_file_exists": mdx_file_exists,
|
||||
"raw_url": raw_url,
|
||||
},
|
||||
)
|
||||
158
zerver/views/development/integrations.py
Normal file
158
zerver/views/development/integrations.py
Normal file
@@ -0,0 +1,158 @@
|
||||
import os
|
||||
from contextlib import suppress
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import orjson
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http.response import HttpResponseBase
|
||||
from django.shortcuts import render
|
||||
from django.test import Client
|
||||
from pydantic import Json
|
||||
|
||||
from zerver.lib.exceptions import JsonableError, ResourceNotFoundError
|
||||
from zerver.lib.integrations import INCOMING_WEBHOOK_INTEGRATIONS
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.lib.typed_endpoint import PathOnly, typed_endpoint
|
||||
from zerver.lib.webhooks.common import call_fixture_to_headers, standardize_headers
|
||||
from zerver.models import UserProfile
|
||||
from zerver.models.realms import get_realm
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse
|
||||
|
||||
ZULIP_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../../")
|
||||
|
||||
|
||||
def get_webhook_integrations() -> list[str]:
|
||||
return [integration.name for integration in INCOMING_WEBHOOK_INTEGRATIONS]
|
||||
|
||||
|
||||
def get_valid_integration_name(name: str) -> str | None:
|
||||
for integration_name in get_webhook_integrations():
|
||||
if name == integration_name:
|
||||
return integration_name
|
||||
return None
|
||||
|
||||
|
||||
def dev_panel(request: HttpRequest) -> HttpResponse:
|
||||
integrations = get_webhook_integrations()
|
||||
bots = UserProfile.objects.filter(is_bot=True, bot_type=UserProfile.INCOMING_WEBHOOK_BOT)
|
||||
context = {
|
||||
"integrations": integrations,
|
||||
"bots": bots,
|
||||
# We set isolated_page to avoid clutter from footer/header.
|
||||
"isolated_page": True,
|
||||
}
|
||||
return render(request, "zerver/development/integrations_dev_panel.html", context)
|
||||
|
||||
|
||||
def send_webhook_fixture_message(
|
||||
url: str, body: str, is_json: bool, custom_headers: dict[str, Any]
|
||||
) -> "TestHttpResponse":
|
||||
client = Client()
|
||||
realm = get_realm("zulip")
|
||||
standardized_headers = standardize_headers(custom_headers)
|
||||
http_host = standardized_headers.pop("HTTP_HOST", realm.host)
|
||||
if is_json:
|
||||
content_type = standardized_headers.pop("HTTP_CONTENT_TYPE", "application/json")
|
||||
else:
|
||||
content_type = standardized_headers.pop("HTTP_CONTENT_TYPE", "text/plain")
|
||||
return client.post(
|
||||
url,
|
||||
body,
|
||||
content_type=content_type,
|
||||
follow=False,
|
||||
secure=False,
|
||||
headers=None,
|
||||
query_params=None,
|
||||
HTTP_HOST=http_host,
|
||||
**standardized_headers,
|
||||
)
|
||||
|
||||
|
||||
@typed_endpoint
|
||||
def get_fixtures(request: HttpRequest, *, integration_name: PathOnly[str]) -> HttpResponse:
|
||||
valid_integration_name = get_valid_integration_name(integration_name)
|
||||
if not valid_integration_name:
|
||||
raise ResourceNotFoundError(f'"{integration_name}" is not a valid webhook integration.')
|
||||
|
||||
fixtures = {}
|
||||
fixtures_dir = os.path.join(ZULIP_PATH, f"zerver/webhooks/{valid_integration_name}/fixtures")
|
||||
if not os.path.exists(fixtures_dir):
|
||||
msg = f'The integration "{valid_integration_name}" does not have fixtures.'
|
||||
raise ResourceNotFoundError(msg)
|
||||
|
||||
for fixture in os.listdir(fixtures_dir):
|
||||
fixture_path = os.path.join(fixtures_dir, fixture)
|
||||
with open(fixture_path) as f:
|
||||
body = f.read()
|
||||
# The file extension will be used to determine the type.
|
||||
with suppress(orjson.JSONDecodeError):
|
||||
body = orjson.loads(body)
|
||||
|
||||
headers_raw = call_fixture_to_headers(
|
||||
valid_integration_name, "".join(fixture.split(".")[:-1])
|
||||
)
|
||||
|
||||
def fix_name(header: str) -> str: # nocoverage
|
||||
return header.removeprefix("HTTP_") # HTTP_ is a prefix intended for Django.
|
||||
|
||||
headers = {fix_name(k): v for k, v in headers_raw.items()}
|
||||
fixtures[fixture] = {"body": body, "headers": headers}
|
||||
|
||||
return json_success(request, data={"fixtures": fixtures})
|
||||
|
||||
|
||||
@typed_endpoint
|
||||
def check_send_webhook_fixture_message(
|
||||
request: HttpRequest,
|
||||
*,
|
||||
url: str,
|
||||
body: str,
|
||||
is_json: Json[bool],
|
||||
custom_headers: str,
|
||||
) -> HttpResponseBase:
|
||||
try:
|
||||
custom_headers_dict = orjson.loads(custom_headers)
|
||||
except orjson.JSONDecodeError as ve: # nocoverage
|
||||
raise JsonableError(f"Custom HTTP headers are not in a valid JSON format. {ve}") # nolint
|
||||
|
||||
response = send_webhook_fixture_message(url, body, is_json, custom_headers_dict)
|
||||
if response.status_code == 200:
|
||||
responses = [{"status_code": response.status_code, "message": response.content.decode()}]
|
||||
return json_success(request, data={"responses": responses})
|
||||
else: # nocoverage
|
||||
return response
|
||||
|
||||
|
||||
@typed_endpoint
|
||||
def send_all_webhook_fixture_messages(
|
||||
request: HttpRequest, *, url: str, integration_name: str
|
||||
) -> HttpResponse:
|
||||
valid_integration_name = get_valid_integration_name(integration_name)
|
||||
if not valid_integration_name: # nocoverage
|
||||
raise ResourceNotFoundError(f'"{integration_name}" is not a valid webhook integration.')
|
||||
|
||||
fixtures_dir = os.path.join(ZULIP_PATH, f"zerver/webhooks/{valid_integration_name}/fixtures")
|
||||
if not os.path.exists(fixtures_dir):
|
||||
msg = f'The integration "{valid_integration_name}" does not have fixtures.'
|
||||
raise ResourceNotFoundError(msg)
|
||||
|
||||
responses = []
|
||||
for fixture in os.listdir(fixtures_dir):
|
||||
fixture_path = os.path.join(fixtures_dir, fixture)
|
||||
with open(fixture_path) as f:
|
||||
content = f.read()
|
||||
x = fixture.split(".")
|
||||
fixture_name, fixture_format = "".join(_ for _ in x[:-1]), x[-1]
|
||||
headers = call_fixture_to_headers(valid_integration_name, fixture_name)
|
||||
is_json = fixture_format == "json"
|
||||
response = send_webhook_fixture_message(url, content, is_json, headers)
|
||||
responses.append(
|
||||
{
|
||||
"status_code": response.status_code,
|
||||
"fixture_name": fixture,
|
||||
"message": response.content.decode(),
|
||||
}
|
||||
)
|
||||
return json_success(request, data={"responses": responses})
|
||||
104
zerver/views/development/registration.py
Normal file
104
zerver/views/development/registration.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from confirmation.models import Confirmation, create_confirmation_link
|
||||
from zerver.context_processors import get_realm_from_request
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.models import Realm, UserProfile
|
||||
from zerver.views.auth import (
|
||||
create_preregistration_realm,
|
||||
create_preregistration_user,
|
||||
redirect_and_log_into_subdomain,
|
||||
)
|
||||
from zerver.views.registration import accounts_register, create_demo_helper
|
||||
from zproject.backends import ExternalAuthResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.http.request import _ImmutableQueryDict
|
||||
|
||||
|
||||
# This is used only by the Puppeteer test in realm-creation.ts.
|
||||
def confirmation_key(request: HttpRequest) -> HttpResponse:
|
||||
return json_success(request, data=request.session["confirmation_key"])
|
||||
|
||||
|
||||
def modify_postdata(request: HttpRequest, **kwargs: Any) -> None:
|
||||
new_post = request.POST.copy()
|
||||
for key, value in kwargs.items():
|
||||
new_post[key] = value
|
||||
new_post._mutable = False
|
||||
request.POST = cast("_ImmutableQueryDict", new_post)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def register_development_user(request: HttpRequest) -> HttpResponse:
|
||||
realm = get_realm_from_request(request)
|
||||
if realm is None: # nocoverage
|
||||
return HttpResponseRedirect(
|
||||
f"{settings.EXTERNAL_URI_SCHEME}{settings.REALM_HOSTS['zulip']}/devtools/register_user/",
|
||||
status=307,
|
||||
)
|
||||
|
||||
count = UserProfile.objects.count()
|
||||
name = f"user-{count}"
|
||||
email = f"{name}@zulip.com"
|
||||
prereg = create_preregistration_user(email, realm, password_required=False)
|
||||
activation_url = create_confirmation_link(prereg, Confirmation.USER_REGISTRATION)
|
||||
key = activation_url.split("/")[-1]
|
||||
# Need to add test data to POST request as it doesn't originally contain the required parameters
|
||||
modify_postdata(request, key=key, full_name=name, password="test", terms="true")
|
||||
|
||||
return accounts_register(request)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def register_development_realm(request: HttpRequest) -> HttpResponse:
|
||||
count = UserProfile.objects.count()
|
||||
name = f"user-{count}"
|
||||
email = f"{name}@zulip.com"
|
||||
realm_name = f"realm-{count}"
|
||||
realm_type = Realm.ORG_TYPES["business"]["id"]
|
||||
realm_default_language = "en"
|
||||
realm_subdomain = realm_name
|
||||
prereg_realm = create_preregistration_realm(
|
||||
email, realm_name, realm_subdomain, realm_type, realm_default_language
|
||||
)
|
||||
activation_url = create_confirmation_link(
|
||||
prereg_realm, Confirmation.NEW_REALM_USER_REGISTRATION, no_associated_realm_object=True
|
||||
)
|
||||
key = activation_url.split("/")[-1]
|
||||
# Need to add test data to POST request as it doesn't originally contain the required parameters
|
||||
modify_postdata(
|
||||
request,
|
||||
key=key,
|
||||
realm_name=realm_name,
|
||||
realm_type=realm_type,
|
||||
realm_default_language=realm_default_language,
|
||||
full_name=name,
|
||||
password="test",
|
||||
realm_subdomain=realm_subdomain,
|
||||
terms="true",
|
||||
how_realm_creator_found_zulip="ad",
|
||||
how_realm_creator_found_zulip_extra_context="test",
|
||||
)
|
||||
|
||||
return accounts_register(request)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def register_demo_development_realm(request: HttpRequest) -> HttpResponse:
|
||||
user_profile = create_demo_helper(
|
||||
request,
|
||||
realm_type=Realm.ORG_TYPES["education"]["id"],
|
||||
realm_default_language="en",
|
||||
how_realm_creator_found_zulip="existing_user",
|
||||
how_realm_creator_found_zulip_extra_context="test",
|
||||
timezone="US/Pacific",
|
||||
)
|
||||
|
||||
return redirect_and_log_into_subdomain(
|
||||
ExternalAuthResult(user_profile=user_profile, data_dict={"is_realm_creation": True})
|
||||
)
|
||||
63
zerver/views/development/showroom.py
Normal file
63
zerver/views/development/showroom.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import os
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
|
||||
background_colors = [
|
||||
{"name": "Default background", "css_var": "--color-background"},
|
||||
{"name": "Popover background", "css_var": "--color-background-popover-menu"},
|
||||
{"name": "Modal background", "css_var": "--color-background-modal"},
|
||||
{"name": "Compose background", "css_var": "--color-compose-box-background"},
|
||||
]
|
||||
|
||||
|
||||
def get_svg_filenames() -> list[str]:
|
||||
icons_dir = os.path.join(os.path.dirname(__file__), "../../../web/icons")
|
||||
|
||||
# Get all .svg file names from the directory
|
||||
svg_files = [f for f in os.listdir(icons_dir) if f.endswith(".svg")]
|
||||
|
||||
# Remove the .svg extension from the file names
|
||||
icon_names = [os.path.splitext(f)[0] for f in svg_files]
|
||||
|
||||
# Sort the list alphabetically
|
||||
return sorted(icon_names)
|
||||
|
||||
|
||||
def showroom_component_buttons(request: HttpRequest) -> HttpResponse:
|
||||
context = {
|
||||
"background_colors": background_colors,
|
||||
"icons": get_svg_filenames(),
|
||||
"page_is_showroom": True,
|
||||
"showroom_component": "buttons",
|
||||
"doc_root_title": "Button styles browser",
|
||||
# We set isolated_page to avoid clutter from footer/header.
|
||||
"isolated_page": True,
|
||||
}
|
||||
return render(request, "zerver/development/showroom/buttons.html", context)
|
||||
|
||||
|
||||
def showroom_component_banners(request: HttpRequest) -> HttpResponse:
|
||||
context = {
|
||||
"background_colors": background_colors,
|
||||
"icons": get_svg_filenames(),
|
||||
"page_is_showroom": True,
|
||||
"showroom_component": "banners",
|
||||
"doc_root_title": "Banner styles browser",
|
||||
# We set isolated_page to avoid clutter from footer/header.
|
||||
"isolated_page": True,
|
||||
}
|
||||
return render(request, "zerver/development/showroom/banners.html", context)
|
||||
|
||||
|
||||
def showroom_component_inputs(request: HttpRequest) -> HttpResponse:
|
||||
context = {
|
||||
"background_colors": background_colors,
|
||||
"icons": get_svg_filenames(),
|
||||
"page_is_showroom": True,
|
||||
"showroom_component": "inputs",
|
||||
"doc_root_title": "Input styles browser",
|
||||
# We set isolated_page to avoid clutter from footer/header.
|
||||
"isolated_page": True,
|
||||
}
|
||||
return render(request, "zerver/development/showroom/inputs.html", context)
|
||||
Reference in New Issue
Block a user