Files
element-call/src/UrlParams.ts
T

414 lines
13 KiB
TypeScript
Raw Normal View History

2022-07-27 16:14:05 -04:00
/*
Copyright 2022-2024 New Vector Ltd.
2022-07-27 16:14:05 -04:00
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
2022-07-27 16:14:05 -04:00
2023-07-03 20:05:08 +02:00
import { useMemo } from "react";
2022-07-27 16:14:05 -04:00
import { useLocation } from "react-router-dom";
2025-03-13 13:58:43 +01:00
import { logger } from "matrix-js-sdk/lib/logger";
2022-07-27 16:14:05 -04:00
2023-07-03 14:59:26 +02:00
import { Config } from "./config/Config";
import { type EncryptionSystem } from "./e2ee/sharedKeyManagement";
2024-04-23 15:15:13 +02:00
import { E2eeType } from "./e2ee/e2eeType";
2023-08-04 13:39:59 +02:00
2023-09-18 17:49:10 +01:00
interface RoomIdentifier {
2022-07-27 16:14:05 -04:00
roomAlias: string | null;
roomId: string | null;
viaServers: string[];
2023-09-18 17:49:10 +01:00
}
export enum UserIntent {
StartNewCall = "start_call",
JoinExistingCall = "join_existing",
Unknown = "unknown",
}
2023-09-18 20:47:47 -04:00
// If you need to add a new flag to this interface, prefer a name that describes
// a specific behavior (such as 'confineToRoom'), rather than one that describes
// the situations that call for this behavior ('isEmbedded'). This makes it
// clearer what each flag means, and helps us avoid coupling Element Call's
// behavior to the needs of specific consumers.
export interface UrlParams {
// Widget api related params
widgetId: string | null;
parentUrl: string | null;
2023-09-18 17:49:10 +01:00
/**
* Anything about what room we're pointed to should be from useRoomIdentifier which
* parses the path and resolves alias with respect to the default server name, however
* roomId is an exception as we need the room ID in embedded (matroyska) mode, and not
* the room alias (or even the via params because we are not trying to join it). This
* is also not validated, where it is in useRoomIdentifier().
*/
roomId: string | null;
2022-12-09 14:25:02 -05:00
/**
2023-09-18 20:47:47 -04:00
* Whether the app should keep the user confined to the current call/room.
2022-12-09 14:25:02 -05:00
*/
2023-09-18 20:47:47 -04:00
confineToRoom: boolean;
/**
* Whether upon entering a room, the user should be prompted to launch the
* native mobile app. (Affects only Android and iOS.)
*
* The app prompt must also be enabled in the config for this to take effect.
2023-09-18 20:47:47 -04:00
*/
appPrompt: boolean;
2022-12-09 14:25:02 -05:00
/**
* Whether the app should pause before joining the call until it sees an
* io.element.join widget action, allowing it to be preloaded.
*/
preload: boolean;
2022-12-09 14:25:02 -05:00
/**
* Whether to hide the room header when in a call.
*/
hideHeader: boolean;
/**
* Whether the controls should be shown. For screen recording no controls can be desired.
*/
showControls: boolean;
2022-12-09 14:25:02 -05:00
/**
* Whether to hide the screen-sharing button.
*/
2022-10-14 16:17:50 +02:00
hideScreensharing: boolean;
2022-12-09 14:25:02 -05:00
/**
* Whether to use end-to-end encryption.
*/
2022-07-27 16:14:05 -04:00
e2eEnabled: boolean;
2022-12-09 14:25:02 -05:00
/**
* The user's ID (only used in matryoshka mode).
*/
2022-07-27 16:14:05 -04:00
userId: string | null;
2022-12-09 14:25:02 -05:00
/**
* The display name to use for auto-registration.
*/
2022-07-27 16:14:05 -04:00
displayName: string | null;
2022-12-09 14:25:02 -05:00
/**
* The device's ID (only used in matryoshka mode).
*/
2022-07-27 16:14:05 -04:00
deviceId: string | null;
2022-12-09 14:25:02 -05:00
/**
* The base URL of the homeserver to use for media lookups in matryoshka mode.
*/
2022-10-17 01:46:44 -04:00
baseUrl: string | null;
2022-12-09 14:25:02 -05:00
/**
* The BCP 47 code of the language the app should use.
*/
2022-10-10 09:19:10 -04:00
lang: string | null;
2022-12-09 14:25:02 -05:00
/**
* The fonts which the interface should use, if not empty.
*/
fonts: string[];
/**
* The factor by which to scale the interface's font size.
*/
fontScale: number | null;
2022-12-19 12:16:59 +01:00
/**
* The Posthog analytics ID. It is only available if the user has given consent for sharing telemetry in element web.
*/
posthogUserId: string | null;
/**
* The Posthog API host. This is only used in the embedded package of Element Call.
*/
posthogApiHost: string | null;
/**
* The Posthog API key. This is only used in the embedded package of Element Call.
*/
posthogApiKey: string | null;
/**
* Whether the app is allowed to use fallback STUN servers for ICE in case the
* user's homeserver doesn't provide any.
*/
allowIceFallback: boolean;
2023-08-04 13:39:59 +02:00
/**
* E2EE password
*/
password: string | null;
/**
2025-04-29 22:12:07 +02:00
* Whether the app should use per participant keys for E2EE.
*/
perParticipantE2EE: boolean;
2025-04-29 22:12:07 +02:00
/**
* Whether the global JS controls for audio output devices should be enabled,
* allowing the list of output devices to be controlled by the app hosting
* Element Call.
*/
controlledOutput: boolean;
2023-10-25 13:49:18 +02:00
/**
* Setting this flag skips the lobby and brings you in the call directly.
* In the widget this can be combined with preload to pass the device settings
* with the join widget action.
*/
skipLobby: boolean;
2024-01-26 10:03:08 +01:00
/**
* Setting this flag makes element call show the lobby after leaving a call.
* This is useful for video rooms.
*/
returnToLobby: boolean;
2024-02-21 21:52:31 +01:00
/**
* The theme to use for element call.
* can be "light", "dark", "light-high-contrast" or "dark-high-contrast".
*/
theme: string | null;
/** This defines the homeserver that is going to be used when joining a room.
2024-02-21 21:52:31 +01:00
* It has to be set to a non default value for links to rooms
* that are not on the default homeserver,
* that is in use for the current user.
*/
viaServers: string | null;
/**
* This defines the homeserver that is going to be used when registering
* a new (guest) user.
* This can be user to configure a non default guest user server when
* creating a spa link.
*/
homeserver: string | null;
/**
* The user's intent with respect to the call.
* e.g. if they clicked a Start Call button, this would be `start_call`.
* If it was a Join Call button, it would be `join_existing`.
*/
intent: string | null;
/**
* The rageshake submit URL. This is only used in the embedded package of Element Call.
*/
rageshakeSubmitUrl: string | null;
/**
* The Sentry DSN. This is only used in the embedded package of Element Call.
*/
sentryDsn: string | null;
/**
* The Sentry environment. This is only used in the embedded package of Element Call.
*/
sentryEnvironment: string | null;
2022-07-27 16:14:05 -04:00
}
2023-09-18 20:47:47 -04:00
// This is here as a stopgap, but what would be far nicer is a function that
// takes a UrlParams and returns a query string. That would enable us to
// consolidate all the data about URL parameters and their meanings to this one
// file.
2023-09-17 17:48:03 -04:00
export function editFragmentQuery(
hash: string,
2023-10-11 10:42:04 -04:00
edit: (params: URLSearchParams) => URLSearchParams,
2023-09-17 17:48:03 -04:00
): string {
const fragmentQueryStart = hash.indexOf("?");
const fragmentParams = edit(
new URLSearchParams(
2023-10-11 10:42:04 -04:00
fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart),
),
2023-09-17 17:48:03 -04:00
);
return `${hash.substring(
0,
2023-10-11 10:42:04 -04:00
fragmentQueryStart,
2023-09-17 17:48:03 -04:00
)}?${fragmentParams.toString()}`;
}
2023-09-18 17:49:10 +01:00
class ParamParser {
private fragmentParams: URLSearchParams;
private queryParams: URLSearchParams;
2023-09-22 18:05:13 -04:00
public constructor(search: string, hash: string) {
2023-09-18 17:49:10 +01:00
this.queryParams = new URLSearchParams(search);
const fragmentQueryStart = hash.indexOf("?");
this.fragmentParams = new URLSearchParams(
2023-10-11 10:42:04 -04:00
fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart),
2023-09-18 17:49:10 +01:00
);
}
// Normally, URL params should be encoded in the fragment so as to avoid
// leaking them to the server. However, we also check the normal query
// string for backwards compatibility with versions that only used that.
2023-09-22 18:05:13 -04:00
public getParam(name: string): string | null {
2023-09-18 17:49:10 +01:00
return this.fragmentParams.get(name) ?? this.queryParams.get(name);
}
2023-09-22 18:05:13 -04:00
public getAllParams(name: string): string[] {
2023-09-18 17:49:10 +01:00
return [
...this.fragmentParams.getAll(name),
...this.queryParams.getAll(name),
];
}
2023-09-19 07:11:39 -04:00
2023-09-22 18:05:13 -04:00
public getFlagParam(name: string, defaultValue = false): boolean {
2023-09-19 07:11:39 -04:00
const param = this.getParam(name);
return param === null ? defaultValue : param !== "false";
}
2023-09-18 17:49:10 +01:00
}
2022-07-27 16:14:05 -04:00
/**
2022-10-10 09:19:10 -04:00
* Gets the app parameters for the current URL.
2023-07-17 19:22:22 +01:00
* @param search The URL search string
* @param hash The URL hash
2022-10-10 09:19:10 -04:00
* @returns The app parameters encoded in the URL
2022-07-27 16:14:05 -04:00
*/
2022-10-10 09:19:10 -04:00
export const getUrlParams = (
2023-07-03 20:05:08 +02:00
search = window.location.search,
2023-10-11 10:42:04 -04:00
hash = window.location.hash,
2022-10-10 09:19:10 -04:00
): UrlParams => {
2023-09-18 17:49:10 +01:00
const parser = new ParamParser(search, hash);
const fontScale = parseFloat(parser.getParam("fontScale") ?? "");
let intent = parser.getParam("intent");
if (!intent || !Object.values(UserIntent).includes(intent as UserIntent)) {
intent = UserIntent.Unknown;
}
const widgetId = parser.getParam("widgetId");
const parentUrl = parser.getParam("parentUrl");
const isWidget = !!widgetId && !!parentUrl;
2023-09-18 17:49:10 +01:00
return {
widgetId,
parentUrl,
2023-09-18 17:49:10 +01:00
// NB. we don't validate roomId here as we do in getRoomIdentifierFromUrl:
// what would we do if it were invalid? If the widget API says that's what
// the room ID is, then that's what it is.
roomId: parser.getParam("roomId"),
password: parser.getParam("password"),
2023-09-18 20:47:47 -04:00
// This flag has 'embed' as an alias for historical reasons
2023-09-19 07:11:39 -04:00
confineToRoom:
parser.getFlagParam("confineToRoom") || parser.getFlagParam("embed"),
appPrompt: parser.getFlagParam("appPrompt", true),
preload: isWidget ? parser.getFlagParam("preload") : false,
2023-09-19 07:11:39 -04:00
hideHeader: parser.getFlagParam("hideHeader"),
2023-10-31 13:47:24 +01:00
showControls: parser.getFlagParam("showControls", true),
2023-09-19 07:11:39 -04:00
hideScreensharing: parser.getFlagParam("hideScreensharing"),
e2eEnabled: parser.getFlagParam("enableE2EE", true),
userId: isWidget ? parser.getParam("userId") : null,
2023-09-18 17:49:10 +01:00
displayName: parser.getParam("displayName"),
deviceId: isWidget ? parser.getParam("deviceId") : null,
baseUrl: isWidget ? parser.getParam("baseUrl") : null,
2023-09-18 17:49:10 +01:00
lang: parser.getParam("lang"),
fonts: parser.getAllParams("font"),
fontScale: Number.isNaN(fontScale) ? null : fontScale,
2023-09-19 07:11:39 -04:00
allowIceFallback: parser.getFlagParam("allowIceFallback"),
2023-10-16 17:58:21 +01:00
perParticipantE2EE: parser.getFlagParam("perParticipantE2EE"),
2025-05-14 19:58:34 +02:00
// TODO this should not default to true!
controlledOutput: parser.getFlagParam("controlledMediaDevices", true),
skipLobby: parser.getFlagParam(
"skipLobby",
isWidget && intent === UserIntent.StartNewCall,
),
2025-03-05 10:40:37 -05:00
// In SPA mode the user should always exit to the home screen when hanging
// up, rather than being sent back to the lobby
returnToLobby: isWidget ? parser.getFlagParam("returnToLobby") : false,
theme: parser.getParam("theme"),
viaServers: !isWidget ? parser.getParam("viaServers") : null,
homeserver: !isWidget ? parser.getParam("homeserver") : null,
intent,
posthogApiHost: parser.getParam("posthogApiHost"),
posthogApiKey: parser.getParam("posthogApiKey"),
posthogUserId:
parser.getParam("posthogUserId") ?? parser.getParam("analyticsID"),
rageshakeSubmitUrl: parser.getParam("rageshakeSubmitUrl"),
sentryDsn: parser.getParam("sentryDsn"),
sentryEnvironment: parser.getParam("sentryEnvironment"),
2023-09-18 17:49:10 +01:00
};
};
/**
* Hook to simplify use of getUrlParams.
* @returns The app parameters for the current URL
*/
export const useUrlParams = (): UrlParams => {
const { search, hash } = useLocation();
return useMemo(() => getUrlParams(search, hash), [search, hash]);
};
export function getRoomIdentifierFromUrl(
pathname: string,
search: string,
2023-10-11 10:42:04 -04:00
hash: string,
2023-09-18 17:49:10 +01:00
): RoomIdentifier {
2023-07-29 20:31:18 +02:00
let roomAlias: string | null = null;
pathname = pathname.substring(1); // Strip the "/"
const pathComponents = pathname.split("/");
const pathHasRoom = pathComponents[0] == "room";
const hasRoomAlias = pathComponents.length > 1;
2023-07-03 20:05:08 +02:00
// What type is our url: roomAlias in hash, room alias as the search path, roomAlias after /room/
2023-09-18 17:49:10 +01:00
if (hash === "" || hash.startsWith("#?")) {
if (hasRoomAlias && pathHasRoom) {
roomAlias = pathComponents[1];
2023-09-18 17:49:10 +01:00
}
if (!pathHasRoom) {
roomAlias = pathComponents[0];
2023-07-29 20:31:18 +02:00
}
2023-09-18 17:49:10 +01:00
} else {
roomAlias = hash;
2023-07-03 14:59:26 +02:00
}
2023-09-18 17:49:10 +01:00
// Delete "?" and what comes afterwards
roomAlias = roomAlias?.split("?")[0] ?? null;
2022-07-27 16:14:05 -04:00
if (roomAlias) {
2023-09-18 17:49:10 +01:00
// Make roomAlias is null, if it only is a "#"
if (roomAlias.length <= 1) {
roomAlias = null;
} else {
// Add "#", if not present
if (!roomAlias.startsWith("#")) {
roomAlias = `#${roomAlias}`;
}
// Add server part, if not present
if (!roomAlias.includes(":")) {
roomAlias = `${roomAlias}:${Config.defaultServerName()}`;
}
2023-09-18 17:49:10 +01:00
}
}
2022-07-27 16:14:05 -04:00
2023-09-18 17:49:10 +01:00
const parser = new ParamParser(search, hash);
2022-12-09 14:25:02 -05:00
2023-07-29 20:31:18 +02:00
// Make sure roomId is valid
2023-09-18 17:49:10 +01:00
let roomId: string | null = parser.getParam("roomId");
2023-07-29 20:31:18 +02:00
if (!roomId?.startsWith("!")) {
roomId = null;
} else if (!roomId.includes("")) {
roomId = null;
}
2022-07-27 16:14:05 -04:00
return {
2023-07-29 20:31:18 +02:00
roomAlias,
roomId,
2023-09-18 17:49:10 +01:00
viaServers: parser.getAllParams("viaServers"),
2022-07-27 16:14:05 -04:00
};
2023-09-18 17:49:10 +01:00
}
2022-07-27 16:14:05 -04:00
2023-09-18 17:49:10 +01:00
export const useRoomIdentifier = (): RoomIdentifier => {
const { pathname, search, hash } = useLocation();
2023-07-03 20:05:08 +02:00
return useMemo(
2023-09-18 17:49:10 +01:00
() => getRoomIdentifierFromUrl(pathname, search, hash),
2023-10-11 10:42:04 -04:00
[pathname, search, hash],
2023-07-03 20:05:08 +02:00
);
2022-07-27 16:14:05 -04:00
};
2024-04-23 15:15:13 +02:00
export function generateUrlSearchParams(
roomId: string,
encryptionSystem: EncryptionSystem,
viaServers?: string[],
): URLSearchParams {
const params = new URLSearchParams();
// The password shouldn't need URL encoding here (we generate URL-safe ones) but encode
// it in case it came from another client that generated a non url-safe one
switch (encryptionSystem?.kind) {
case E2eeType.SHARED_KEY: {
const encodedPassword = encodeURIComponent(encryptionSystem.secret);
if (encodedPassword !== encryptionSystem.secret) {
logger.info(
"Encoded call password used non URL-safe chars: buggy client?",
);
}
params.set("password", encodedPassword);
break;
}
case E2eeType.PER_PARTICIPANT:
params.set("perParticipantE2EE", "true");
break;
}
params.set("roomId", roomId);
viaServers?.forEach((s) => params.set("viaServers", s));
return params;
}