From a4938d3760d5d9c67fb3c4f72274d8f775321931 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Fri, 16 Feb 2024 13:56:36 -0800 Subject: [PATCH] page_params: Parse page_params and state_data with Zod. This establishes a runtime check that their types continue to reflect reality going forward. Signed-off-by: Anders Kaseorg --- analytics/views/stats.py | 2 + corporate/lib/stripe.py | 16 +- corporate/views/portico.py | 2 + tools/test-js-with-node | 1 + web/src/base_page_params.ts | 101 +++++++++++ web/src/billing/page_params.ts | 24 +-- web/src/blueslip.ts | 2 +- web/src/filter.ts | 7 +- web/src/group_permission_settings.ts | 2 +- web/src/i18n.ts | 6 +- web/src/narrow_state.ts | 2 +- web/src/page_params.ts | 49 +---- web/src/portico/google-analytics.js | 2 +- web/src/portico/landing-page.js | 4 +- web/src/sentry.ts | 8 +- web/src/settings_config.ts | 2 +- web/src/state_data.ts | 260 ++++++++++++++------------- web/src/stats/page_params.ts | 16 +- web/src/types.ts | 12 -- web/src/ui_init.js | 7 +- web/src/zulip_test.js | 2 +- web/tests/lib/index.js | 2 + zerver/context_processors.py | 2 + zerver/lib/home.py | 3 + zerver/tests/test_home.py | 2 + 25 files changed, 312 insertions(+), 224 deletions(-) create mode 100644 web/src/base_page_params.ts diff --git a/analytics/views/stats.py b/analytics/views/stats.py index bab13f53d0..dad1a67b9c 100644 --- a/analytics/views/stats.py +++ b/analytics/views/stats.py @@ -80,7 +80,9 @@ def render_stats( translation.get_language_from_path(request.path_info), ) + # Sync this with stats_params_schema in base_page_params.ts. page_params = dict( + page_type="stats", data_url_suffix=data_url_suffix, upload_space_used=space_used, guest_users=guest_users, diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 384b46130f..e7dd7e0720 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -8,7 +8,18 @@ from datetime import datetime, timedelta, timezone from decimal import Decimal from enum import Enum from functools import wraps -from typing import Any, Callable, Dict, Generator, Optional, Tuple, TypedDict, TypeVar, Union +from typing import ( + Any, + Callable, + Dict, + Generator, + Literal, + Optional, + Tuple, + TypedDict, + TypeVar, + Union, +) from urllib.parse import urlencode, urljoin import stripe @@ -615,7 +626,9 @@ class BillingSessionAuditLogEventError(Exception): super().__init__(self.message) +# Sync this with upgrade_params_schema in base_page_params.ts. class UpgradePageParams(TypedDict): + page_type: Literal["upgrade"] annual_price: int demo_organization_scheduled_deletion_date: Optional[datetime] monthly_price: int @@ -2395,6 +2408,7 @@ class BillingSession(ABC): "remote_server_legacy_plan_end_date": remote_server_legacy_plan_end_date, "manual_license_management": initial_upgrade_request.manual_license_management, "page_params": { + "page_type": "upgrade", "annual_price": get_price_per_license( tier, CustomerPlan.BILLING_SCHEDULE_ANNUAL, percent_off ), diff --git a/corporate/views/portico.py b/corporate/views/portico.py index 9ec6b969c9..9d914f20d8 100644 --- a/corporate/views/portico.py +++ b/corporate/views/portico.py @@ -278,7 +278,9 @@ def team_view(request: HttpRequest) -> HttpResponse: request, "corporate/team.html", context={ + # Sync this with team_params_schema in base_page_params.ts. "page_params": { + "page_type": "team", "contributors": data["contributors"], }, "date": data["date"], diff --git a/tools/test-js-with-node b/tools/test-js-with-node index f7348bc37f..c5d0f5c759 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -56,6 +56,7 @@ EXEMPT_FILES = make_set( "web/src/attachments_ui.ts", "web/src/audible_notifications.ts", "web/src/avatar.ts", + "web/src/base_page_params.ts", "web/src/billing/event_status.ts", "web/src/billing/helpers.ts", "web/src/blueslip.ts", diff --git a/web/src/base_page_params.ts b/web/src/base_page_params.ts new file mode 100644 index 0000000000..250e427efb --- /dev/null +++ b/web/src/base_page_params.ts @@ -0,0 +1,101 @@ +import $ from "jquery"; +import {z} from "zod"; + +import {state_data_schema, term_schema} from "./state_data"; + +const t1 = performance.now(); + +// Sync this with zerver.context_processors.zulip_default_context. +const default_params_schema = z.object({ + page_type: z.literal("default"), + development_environment: z.boolean(), + realm_sentry_key: z.optional(z.string()), + request_language: z.string(), + server_sentry_dsn: z.nullable(z.string()), + server_sentry_environment: z.optional(z.string()), + server_sentry_sample_rate: z.optional(z.number()), + server_sentry_trace_rate: z.optional(z.number()), +}); + +// These parameters are sent in #page-params for both users and spectators. +// +// Sync this with zerver.lib.home.build_page_params_for_home_page_load. +const home_params_schema = default_params_schema + .extend({ + page_type: z.literal("home"), + apps_page_url: z.string(), + bot_types: z.array( + z.object({ + type_id: z.number(), + name: z.string(), + allowed: z.boolean(), + }), + ), + corporate_enabled: z.boolean(), + furthest_read_time: z.nullable(z.number()), + is_spectator: z.boolean(), + language_list: z.array( + z.object({ + code: z.string(), + locale: z.string(), + name: z.string(), + percent_translated: z.optional(z.number()), + }), + ), + login_page: z.string(), + narrow: z.optional(z.array(term_schema)), + narrow_stream: z.optional(z.string()), + needs_tutorial: z.boolean(), + promote_sponsoring_zulip: z.boolean(), + show_billing: z.boolean(), + show_plans: z.boolean(), + show_webathena: z.boolean(), + sponsorship_pending: z.boolean(), + state_data: state_data_schema.optional(), + translation_data: z.record(z.string()), + }) + // TODO/typescript: Remove .passthrough() when all consumers have been + // converted to TypeScript and the schema is complete. + .passthrough(); + +// Sync this with analytics.views.stats.render_stats. +const stats_params_schema = default_params_schema.extend({ + page_type: z.literal("stats"), + data_url_suffix: z.string(), + upload_space_used: z.nullable(z.number()), + guest_users: z.nullable(z.number()), + translation_data: z.record(z.string()), +}); + +// Sync this with corporate.views.portico.team_view. +const team_params_schema = default_params_schema.extend({ + page_type: z.literal("team"), + contributors: z.unknown(), +}); + +// Sync this with corporate.lib.stripe.UpgradePageParams. +const upgrade_params_schema = default_params_schema.extend({ + page_type: z.literal("upgrade"), + annual_price: z.number(), + demo_organization_scheduled_deletion_date: z.nullable(z.number()), + monthly_price: z.number(), + seat_count: z.number(), + billing_base_url: z.string(), + tier: z.number(), + flat_discount: z.number(), + flat_discounted_months: z.number(), + fixed_price: z.number().nullable(), +}); + +const page_params_schema = z.discriminatedUnion("page_type", [ + default_params_schema, + home_params_schema, + stats_params_schema, + team_params_schema, + upgrade_params_schema, +]); + +export const page_params = page_params_schema.parse($("#page-params").remove().data("params")); + +const t2 = performance.now(); +export const page_params_parse_time = t2 - t1; diff --git a/web/src/billing/page_params.ts b/web/src/billing/page_params.ts index 240520ce6d..5a3734db3c 100644 --- a/web/src/billing/page_params.ts +++ b/web/src/billing/page_params.ts @@ -1,19 +1,9 @@ -import $ from "jquery"; +import assert from "minimalistic-assert"; -// Don't remove page_params here yet, since we still use them later. -// For example, "#page_params" is used again through `sentry.ts`, which -// imports the main `src/page_params` module. -export const page_params: { - annual_price: number; - monthly_price: number; - seat_count: number; - billing_base_url: string; - tier: number; - flat_discount: number; - flat_discounted_months: number; - fixed_price: number | null; -} = $("#page-params").data("params"); +import {page_params as base_page_params} from "../base_page_params"; -if (!page_params) { - throw new Error("Missing page-params"); -} +assert(base_page_params.page_type === "upgrade"); + +// We need to export with a narrowed TypeScript type +// eslint-disable-next-line unicorn/prefer-export-from +export const page_params = base_page_params; diff --git a/web/src/blueslip.ts b/web/src/blueslip.ts index bdeeedf27b..7a79a3e1af 100644 --- a/web/src/blueslip.ts +++ b/web/src/blueslip.ts @@ -9,8 +9,8 @@ import * as Sentry from "@sentry/browser"; import $ from "jquery"; +import {page_params} from "./base_page_params"; import {BlueslipError, display_stacktrace} from "./blueslip_stacktrace"; -import {page_params} from "./page_params"; if (Error.stackTraceLimit !== undefined) { Error.stackTraceLimit = 100000; diff --git a/web/src/filter.ts b/web/src/filter.ts index 9c7aeb53aa..908cee2856 100644 --- a/web/src/filter.ts +++ b/web/src/filter.ts @@ -12,6 +12,7 @@ import type {Message} from "./message_store"; import {page_params} from "./page_params"; import * as people from "./people"; import {realm} from "./state_data"; +import type {Term} from "./state_data"; import * as stream_data from "./stream_data"; import type {StreamSubscription} from "./sub_store"; import * as unread from "./unread"; @@ -234,12 +235,6 @@ function message_matches_search_term(message: Message, operator: string, operand return true; // unknown operators return true (effectively ignored) } -export type Term = { - negated?: boolean; - operator: string; - operand: string; -}; - export class Filter { _terms: Term[]; _sub?: StreamSubscription; diff --git a/web/src/group_permission_settings.ts b/web/src/group_permission_settings.ts index a3f0c70b98..81b6fb2af2 100644 --- a/web/src/group_permission_settings.ts +++ b/web/src/group_permission_settings.ts @@ -1,5 +1,5 @@ import {realm} from "./state_data"; -import type {GroupPermissionSetting} from "./types"; +import type {GroupPermissionSetting} from "./state_data"; export function get_group_permission_setting_config( setting_name: string, diff --git a/web/src/i18n.ts b/web/src/i18n.ts index 3e6fb161cb..f528aaf87e 100644 --- a/web/src/i18n.ts +++ b/web/src/i18n.ts @@ -6,14 +6,14 @@ import {DEFAULT_INTL_CONFIG, IntlErrorCode, createIntl, createIntlCache} from "@ import type {FormatXMLElementFn, PrimitiveType} from "intl-messageformat"; import _ from "lodash"; -import {page_params} from "./page_params"; +import {page_params} from "./base_page_params"; const cache = createIntlCache(); export const intl = createIntl( { locale: page_params.request_language, defaultLocale: "en", - messages: page_params.translation_data, + messages: "translation_data" in page_params ? page_params.translation_data : {}, /* istanbul ignore next */ onError(error) { // Ignore complaints about untranslated strings that were @@ -50,7 +50,7 @@ export function $t_html( }); } -export let language_list: (typeof page_params)["language_list"]; +export let language_list: (typeof page_params & {page_type: "home"})["language_list"]; export function get_language_name(language_code: string): string { const language_list_map: Record = {}; diff --git a/web/src/narrow_state.ts b/web/src/narrow_state.ts index 2145bcdba2..983f29f731 100644 --- a/web/src/narrow_state.ts +++ b/web/src/narrow_state.ts @@ -1,10 +1,10 @@ import * as blueslip from "./blueslip"; import {Filter} from "./filter"; -import type {Term} from "./filter"; import * as inbox_util from "./inbox_util"; import {page_params} from "./page_params"; import * as people from "./people"; import * as recent_view_util from "./recent_view_util"; +import type {Term} from "./state_data"; import * as stream_data from "./stream_data"; import type {StreamSubscription} from "./sub_store"; import * as unread from "./unread"; diff --git a/web/src/page_params.ts b/web/src/page_params.ts index c42a40456b..a5150da4f9 100644 --- a/web/src/page_params.ts +++ b/web/src/page_params.ts @@ -1,44 +1,9 @@ -import $ from "jquery"; +import assert from "minimalistic-assert"; -import type {Term} from "./filter"; +import {page_params as base_page_params} from "./base_page_params"; -const t1 = performance.now(); -export const page_params: { - apps_page_url: string; - bot_types: { - type_id: number; - name: string; - allowed: boolean; - }[]; - corporate_enabled: boolean; - development_environment: boolean; - furthest_read_time: number | null; - is_spectator: boolean; - language_list: { - code: string; - locale: string; - name: string; - percent_translated?: number; - }[]; - login_page: string; - narrow?: Term[]; - narrow_stream?: string; - needs_tutorial: boolean; - promote_sponsoring_zulip: boolean; - realm_sentry_key?: string; - request_language: string; - server_sentry_dsn: string | null; - server_sentry_environment?: string; - server_sentry_sample_rate?: number; - server_sentry_trace_rate?: number; - show_billing: boolean; - show_plans: boolean; - show_webathena: boolean; - sponsorship_pending: boolean; - translation_data: Record; -} = $("#page-params").remove().data("params"); -const t2 = performance.now(); -export const page_params_parse_time = t2 - t1; -if (!page_params) { - throw new Error("Missing page-params"); -} +assert(base_page_params.page_type === "home"); + +// We need to export with a narrowed TypeScript type. +// eslint-disable-next-line unicorn/prefer-export-from +export const page_params = base_page_params; diff --git a/web/src/portico/google-analytics.js b/web/src/portico/google-analytics.js index f24497cd29..ad0171f17a 100644 --- a/web/src/portico/google-analytics.js +++ b/web/src/portico/google-analytics.js @@ -1,6 +1,6 @@ import {gtag, install} from "ga-gtag"; -import {page_params} from "../page_params"; +import {page_params} from "../base_page_params"; export let config; diff --git a/web/src/portico/landing-page.js b/web/src/portico/landing-page.js index c8a0f36b04..c30adae598 100644 --- a/web/src/portico/landing-page.js +++ b/web/src/portico/landing-page.js @@ -1,6 +1,7 @@ import $ from "jquery"; +import assert from "minimalistic-assert"; -import {page_params} from "../page_params"; +import {page_params} from "../base_page_params"; import {detect_user_os} from "./tabbed-instructions"; import render_tabs from "./team"; @@ -119,6 +120,7 @@ $(() => { events(); if (window.location.pathname === "/team/") { + assert(page_params.page_type === "team"); const contributors = page_params.contributors; delete page_params.contributors; render_tabs(contributors); diff --git a/web/src/sentry.ts b/web/src/sentry.ts index b35108e445..4fb25a067e 100644 --- a/web/src/sentry.ts +++ b/web/src/sentry.ts @@ -1,6 +1,6 @@ import * as Sentry from "@sentry/browser"; -import {page_params} from "./page_params"; +import {page_params} from "./base_page_params"; import {current_user, realm} from "./state_data"; type UserInfo = { @@ -76,7 +76,11 @@ if (page_params.server_sentry_dsn) { const user_info: UserInfo = { realm: sentry_key, }; - if (sentry_key !== "www" && current_user !== undefined) { + if ( + sentry_key !== "www" && + page_params.page_type === "home" && + current_user !== undefined + ) { user_info.role = current_user.is_owner ? "Organization owner" : current_user.is_admin diff --git a/web/src/settings_config.ts b/web/src/settings_config.ts index 6b8fc06a68..0264381b02 100644 --- a/web/src/settings_config.ts +++ b/web/src/settings_config.ts @@ -1,7 +1,7 @@ import Handlebars from "handlebars/runtime"; +import {page_params} from "./base_page_params"; import {$t, $t_html} from "./i18n"; -import {page_params} from "./page_params"; import type {RealmDefaultSettings} from "./realm_user_settings_defaults"; import {realm} from "./state_data"; import type {StreamSpecificNotificationSettings} from "./sub_store"; diff --git a/web/src/state_data.ts b/web/src/state_data.ts index cac1d07165..84b46fa7d4 100644 --- a/web/src/state_data.ts +++ b/web/src/state_data.ts @@ -1,131 +1,145 @@ -import type {GroupPermissionSetting} from "./types"; +import {z} from "zod"; -export let current_user: { - avatar_source: string; - delivery_email: string; - is_admin: boolean; - is_billing_admin: boolean; - is_guest: boolean; - is_moderator: boolean; - is_owner: boolean; - user_id: number; -}; +const group_permission_setting_schema = z.object({ + require_system_group: z.boolean(), + allow_internet_group: z.boolean(), + allow_owners_group: z.boolean(), + allow_nobody_group: z.boolean(), + allow_everyone_group: z.boolean(), + default_group_name: z.string(), + id_field_name: z.string(), + default_for_system_groups: z.nullable(z.string()), + allowed_system_groups: z.array(z.string()), +}); +export type GroupPermissionSetting = z.output; -export let realm: { - custom_profile_fields: { - display_in_profile_summary?: boolean; - field_data: string; - hint: string; - id: number; - name: string; - order: number; - type: number; - }[]; - custom_profile_field_types: { - SHORT_TEXT: { - id: number; - name: string; - }; - LONG_TEXT: { - id: number; - name: string; - }; - DATE: { - id: number; - name: string; - }; - SELECT: { - id: number; - name: string; - }; - URL: { - id: number; - name: string; - }; - EXTERNAL_ACCOUNT: { - id: number; - name: string; - }; - USER: { - id: number; - name: string; - }; - PRONOUNS: { - id: number; - name: string; - }; - }; - max_avatar_file_size_mib: number; - max_icon_file_size_mib: number; - max_logo_file_size_mib: number; - realm_add_custom_emoji_policy: number; - realm_available_video_chat_providers: { - disabled: {name: string; id: number}; - jitsi_meet: {name: string; id: number}; - zoom?: {name: string; id: number}; - big_blue_button?: {name: string; id: number}; - }; - realm_avatar_changes_disabled: boolean; - realm_bot_domain: string; - realm_can_access_all_users_group: number; - realm_create_multiuse_invite_group: number; - realm_create_private_stream_policy: number; - realm_create_public_stream_policy: number; - realm_create_web_public_stream_policy: number; - realm_delete_own_message_policy: number; - realm_description: string; - realm_domains: {domain: string; allow_subdomains: boolean}[]; - realm_edit_topic_policy: number; - realm_email_changes_disabled: boolean; - realm_enable_guest_user_indicator: boolean; - realm_enable_spectator_access: boolean; - realm_icon_source: string; - realm_icon_url: string; - realm_invite_to_realm_policy: number; - realm_invite_to_stream_policy: number; - realm_is_zephyr_mirror_realm: boolean; - realm_jitsi_server_url: string | null; - realm_logo_source: string; - realm_logo_url: string; - realm_move_messages_between_streams_policy: number; - realm_name_changes_disabled: boolean; - realm_name: string; - realm_night_logo_source: string; - realm_night_logo_url: string; - realm_notifications_stream_id: number; - realm_org_type: number; - realm_plan_type: number; - realm_private_message_policy: number; - realm_push_notifications_enabled: boolean; - realm_upload_quota_mib: number | null; - realm_uri: string; - realm_user_group_edit_policy: number; - realm_video_chat_provider: number; - realm_waiting_period_threshold: number; - server_avatar_changes_disabled: boolean; - server_jitsi_server_url: string | null; - server_name_changes_disabled: boolean; - server_needs_upgrade: boolean; - server_presence_offline_threshold_seconds: number; - server_supported_permission_settings: { - realm: Record; - stream: Record; - group: Record; - }; - server_typing_started_expiry_period_milliseconds: number; - server_typing_started_wait_period_milliseconds: number; - server_typing_stopped_wait_period_milliseconds: number; - server_web_public_streams_enabled: boolean; - stop_words: string[]; - zulip_merge_base: string; - zulip_plan_is_not_limited: boolean; - zulip_version: string; -}; +export const term_schema = z.object({ + negated: z.optional(z.boolean()), + operator: z.string(), + operand: z.string(), +}); +export type Term = z.output; +// Sync this with zerver.lib.events.do_events_register. -export function set_current_user(initial_current_user: typeof current_user): void { +export const current_user_schema = z.object({ + avatar_source: z.string(), + delivery_email: z.string(), + is_admin: z.boolean(), + is_billing_admin: z.boolean(), + is_guest: z.boolean(), + is_moderator: z.boolean(), + is_owner: z.boolean(), + user_id: z.number(), +}); +// Sync this with zerver.lib.events.do_events_register. + +export const realm_schema = z.object({ + custom_profile_fields: z.array( + z.object({ + display_in_profile_summary: z.optional(z.boolean()), + field_data: z.string(), + hint: z.string(), + id: z.number(), + name: z.string(), + order: z.number(), + type: z.number(), + }), + ), + custom_profile_field_types: z.object({ + SHORT_TEXT: z.object({id: z.number(), name: z.string()}), + LONG_TEXT: z.object({id: z.number(), name: z.string()}), + DATE: z.object({id: z.number(), name: z.string()}), + SELECT: z.object({id: z.number(), name: z.string()}), + URL: z.object({id: z.number(), name: z.string()}), + EXTERNAL_ACCOUNT: z.object({id: z.number(), name: z.string()}), + USER: z.object({id: z.number(), name: z.string()}), + PRONOUNS: z.object({id: z.number(), name: z.string()}), + }), + max_avatar_file_size_mib: z.number(), + max_icon_file_size_mib: z.number(), + max_logo_file_size_mib: z.number(), + realm_add_custom_emoji_policy: z.number(), + realm_available_video_chat_providers: z.object({ + disabled: z.object({name: z.string(), id: z.number()}), + jitsi_meet: z.object({name: z.string(), id: z.number()}), + zoom: z.optional(z.object({name: z.string(), id: z.number()})), + big_blue_button: z.optional(z.object({name: z.string(), id: z.number()})), + }), + realm_avatar_changes_disabled: z.boolean(), + realm_bot_domain: z.string(), + realm_can_access_all_users_group: z.number(), + realm_create_multiuse_invite_group: z.number(), + realm_create_private_stream_policy: z.number(), + realm_create_public_stream_policy: z.number(), + realm_create_web_public_stream_policy: z.number(), + realm_delete_own_message_policy: z.number(), + realm_description: z.string(), + realm_domains: z.array( + z.object({ + domain: z.string(), + allow_subdomains: z.boolean(), + }), + ), + realm_edit_topic_policy: z.number(), + realm_email_changes_disabled: z.boolean(), + realm_enable_guest_user_indicator: z.boolean(), + realm_enable_spectator_access: z.boolean(), + realm_icon_source: z.string(), + realm_icon_url: z.string(), + realm_invite_to_realm_policy: z.number(), + realm_invite_to_stream_policy: z.number(), + realm_is_zephyr_mirror_realm: z.boolean(), + realm_jitsi_server_url: z.nullable(z.string()), + realm_logo_source: z.string(), + realm_logo_url: z.string(), + realm_move_messages_between_streams_policy: z.number(), + realm_name_changes_disabled: z.boolean(), + realm_name: z.string(), + realm_night_logo_source: z.string(), + realm_night_logo_url: z.string(), + realm_notifications_stream_id: z.number(), + realm_org_type: z.number(), + realm_plan_type: z.number(), + realm_private_message_policy: z.number(), + realm_push_notifications_enabled: z.boolean(), + realm_upload_quota_mib: z.nullable(z.number()), + realm_uri: z.string(), + realm_user_group_edit_policy: z.number(), + realm_video_chat_provider: z.number(), + realm_waiting_period_threshold: z.number(), + server_avatar_changes_disabled: z.boolean(), + server_jitsi_server_url: z.nullable(z.string()), + server_name_changes_disabled: z.boolean(), + server_needs_upgrade: z.boolean(), + server_presence_offline_threshold_seconds: z.number(), + server_supported_permission_settings: z.object({ + realm: z.record(group_permission_setting_schema), + stream: z.record(group_permission_setting_schema), + group: z.record(group_permission_setting_schema), + }), + server_typing_started_expiry_period_milliseconds: z.number(), + server_typing_started_wait_period_milliseconds: z.number(), + server_typing_stopped_wait_period_milliseconds: z.number(), + server_web_public_streams_enabled: z.boolean(), + stop_words: z.array(z.string()), + zulip_merge_base: z.string(), + zulip_plan_is_not_limited: z.boolean(), + zulip_version: z.string(), +}); + +export const state_data_schema = current_user_schema + .merge(realm_schema) + // TODO/typescript: Remove .passthrough() when all consumers have been + // converted to TypeScript and the schema is complete. + .passthrough(); + +export let current_user: z.infer; +export let realm: z.infer; + +export function set_current_user(initial_current_user: z.infer): void { current_user = initial_current_user; } -export function set_realm(initial_realm: typeof realm): void { +export function set_realm(initial_realm: z.infer): void { realm = initial_realm; } diff --git a/web/src/stats/page_params.ts b/web/src/stats/page_params.ts index f51d89a576..389a1f1f5c 100644 --- a/web/src/stats/page_params.ts +++ b/web/src/stats/page_params.ts @@ -1,11 +1,9 @@ -import $ from "jquery"; +import assert from "minimalistic-assert"; -export const page_params: { - data_url_suffix: string; - guest_users: number | null; - upload_space_used: number | null; -} = $("#page-params").data("params"); +import {page_params as base_page_params} from "../base_page_params"; -if (!page_params) { - throw new Error("Missing page-params"); -} +assert(base_page_params.page_type === "stats"); + +// We need to export with a narrowed TypeScript type +// eslint-disable-next-line unicorn/prefer-export-from +export const page_params = base_page_params; diff --git a/web/src/types.ts b/web/src/types.ts index fee9048a39..a5a3cf974a 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -52,15 +52,3 @@ export type UpdateMessageEvent = { // This will not be set until it gets fixed. topic?: string; }; - -export type GroupPermissionSetting = { - require_system_group: boolean; - allow_internet_group: boolean; - allow_owners_group: boolean; - allow_nobody_group: boolean; - allow_everyone_group: boolean; - default_group_name: string; - id_field_name: string; - default_for_system_groups: string | null; - allowed_system_groups: string[]; -}; diff --git a/web/src/ui_init.js b/web/src/ui_init.js index a9e51f8f43..61b332c981 100644 --- a/web/src/ui_init.js +++ b/web/src/ui_init.js @@ -1,5 +1,6 @@ import $ from "jquery"; import _ from "lodash"; +import assert from "minimalistic-assert"; import generated_emoji_codes from "../../static/generated/emoji/emoji_codes.json"; import * as fenced_code from "../shared/src/fenced_code"; @@ -114,7 +115,7 @@ import * as sidebar_ui from "./sidebar_ui"; import * as spoilers from "./spoilers"; import * as starred_messages from "./starred_messages"; import * as starred_messages_ui from "./starred_messages_ui"; -import {current_user, realm, set_current_user, set_realm} from "./state_data"; +import {current_user, realm, set_current_user, set_realm, state_data_schema} from "./state_data"; import * as stream_data from "./stream_data"; import * as stream_edit from "./stream_edit"; import * as stream_edit_subscribers from "./stream_edit_subscribers"; @@ -874,7 +875,8 @@ $(async () => { url: "/json/register", data, success(response_data) { - initialize_everything(response_data); + const state_data = state_data_schema.parse(response_data); + initialize_everything(state_data); }, error() { $("#app-loading-middle-content").hide(); @@ -884,6 +886,7 @@ $(async () => { }, }); } else { + assert(page_params.state_data !== undefined); initialize_everything(page_params.state_data); } }); diff --git a/web/src/zulip_test.js b/web/src/zulip_test.js index 1ee2a0d2b9..e3ea7e75e3 100644 --- a/web/src/zulip_test.js +++ b/web/src/zulip_test.js @@ -9,7 +9,7 @@ export {get_stream_id, get_sub, get_subscriber_count} from "./stream_data"; export {get_by_user_id as get_person_by_user_id, get_user_id_from_name} from "./people"; export {last_visible as last_visible_row, id as row_id} from "./rows"; export {cancel as cancel_compose} from "./compose_actions"; -export {page_params, page_params_parse_time} from "./page_params"; +export {page_params, page_params_parse_time} from "./base_page_params"; export {initiate as initiate_reload} from "./reload"; export {page_load_time} from "./setup"; export {current_user, realm} from "./state_data"; diff --git a/web/tests/lib/index.js b/web/tests/lib/index.js index f1c710778a..4b50e0cac7 100644 --- a/web/tests/lib/index.js +++ b/web/tests/lib/index.js @@ -118,6 +118,8 @@ test.set_verbose(files.length === 1); require("../../src/blueslip"); namespace.mock_esm("../../src/i18n", stub_i18n); require("../../src/i18n"); + namespace.mock_esm("../../src/base_page_params", zpage_params); + require("../../src/base_page_params"); namespace.mock_esm("../../src/billing/page_params", zpage_billing_params); require("../../src/billing/page_params"); namespace.mock_esm("../../src/page_params", zpage_params); diff --git a/zerver/context_processors.py b/zerver/context_processors.py index 604b529828..e64cb57380 100644 --- a/zerver/context_processors.py +++ b/zerver/context_processors.py @@ -32,6 +32,7 @@ from zproject.backends import ( from zproject.config import get_config DEFAULT_PAGE_PARAMS: Mapping[str, Any] = { + "page_type": "default", "development_environment": settings.DEVELOPMENT, } @@ -165,6 +166,7 @@ def zulip_default_context(request: HttpRequest) -> Dict[str, Any]: f'{escape(support_email)}' ) + # Sync this with default_params_schema in base_page_params.ts. default_page_params: Dict[str, Any] = { **DEFAULT_PAGE_PARAMS, "server_sentry_dsn": settings.SENTRY_FRONTEND_DSN, diff --git a/zerver/lib/home.py b/zerver/lib/home.py index c6a96130c7..cd886d717e 100644 --- a/zerver/lib/home.py +++ b/zerver/lib/home.py @@ -195,7 +195,10 @@ def build_page_params_for_home_page_load( # Pass parameters to the client-side JavaScript code. # These end up in a JavaScript Object named 'page_params'. + # + # Sync this with home_params_schema in base_page_params.ts. page_params: Dict[str, object] = dict( + page_type="home", ## Server settings. test_suite=settings.TEST_SUITE, insecure_desktop_app=insecure_desktop_app, diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index 313436d6d4..9963c85565 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -52,6 +52,7 @@ class HomeTest(ZulipTestCase): "narrow_stream", "needs_tutorial", "no_event_queue", + "page_type", "promote_sponsoring_zulip", "request_language", "server_sentry_dsn", @@ -347,6 +348,7 @@ class HomeTest(ZulipTestCase): "login_page", "needs_tutorial", "no_event_queue", + "page_type", "promote_sponsoring_zulip", "realm_rendered_description", "request_language",