Merge pull request #1479 from vector-im/dbkr/refactor_urlparams

Small refactor of URLParams stuff
This commit is contained in:
David Baker
2023-09-18 20:40:31 +01:00
committed by GitHub
5 changed files with 154 additions and 112 deletions

View File

@@ -21,10 +21,21 @@ import { Config } from "./config/Config";
export const PASSWORD_STRING = "password="; export const PASSWORD_STRING = "password=";
interface UrlParams { interface RoomIdentifier {
roomAlias: string | null; roomAlias: string | null;
roomId: string | null; roomId: string | null;
viaServers: string[]; viaServers: string[];
}
interface UrlParams {
/**
* 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;
/** /**
* Whether the app is running in embedded mode, and should keep the user * Whether the app is running in embedded mode, and should keep the user
* confined to the current room. * confined to the current room.
@@ -106,25 +117,92 @@ export function editFragmentQuery(
)}?${fragmentParams.toString()}`; )}?${fragmentParams.toString()}`;
} }
class ParamParser {
private fragmentParams: URLSearchParams;
private queryParams: URLSearchParams;
constructor(search: string, hash: string) {
this.queryParams = new URLSearchParams(search);
const fragmentQueryStart = hash.indexOf("?");
this.fragmentParams = new URLSearchParams(
fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart)
);
}
// 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.
hasParam(name: string): boolean {
return this.fragmentParams.has(name) || this.queryParams.has(name);
}
getParam(name: string): string | null {
return this.fragmentParams.get(name) ?? this.queryParams.get(name);
}
getAllParams(name: string): string[] {
return [
...this.fragmentParams.getAll(name),
...this.queryParams.getAll(name),
];
}
}
/** /**
* Gets the app parameters for the current URL. * Gets the app parameters for the current URL.
* @param ignoreRoomAlias If true, does not try to parse a room alias from the URL
* @param search The URL search string * @param search The URL search string
* @param pathname The URL path name
* @param hash The URL hash * @param hash The URL hash
* @returns The app parameters encoded in the URL * @returns The app parameters encoded in the URL
*/ */
export const getUrlParams = ( export const getUrlParams = (
ignoreRoomAlias?: boolean,
search = window.location.search, search = window.location.search,
pathname = window.location.pathname,
hash = window.location.hash hash = window.location.hash
): UrlParams => { ): UrlParams => {
// This is legacy code - we're moving away from using aliases const parser = new ParamParser(search, hash);
const fontScale = parseFloat(parser.getParam("fontScale") ?? "");
return {
// 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"),
isEmbedded: parser.hasParam("embed"),
preload: parser.hasParam("preload"),
hideHeader: parser.hasParam("hideHeader"),
hideScreensharing: parser.hasParam("hideScreensharing"),
e2eEnabled: parser.getParam("enableE2e") !== "false", // Defaults to true
userId: parser.getParam("userId"),
displayName: parser.getParam("displayName"),
deviceId: parser.getParam("deviceId"),
baseUrl: parser.getParam("baseUrl"),
lang: parser.getParam("lang"),
fonts: parser.getAllParams("font"),
fontScale: Number.isNaN(fontScale) ? null : fontScale,
analyticsID: parser.getParam("analyticsID"),
allowIceFallback: parser.hasParam("allowIceFallback"),
};
};
/**
* 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,
hash: string
): RoomIdentifier {
let roomAlias: string | null = null; let roomAlias: string | null = null;
if (!ignoreRoomAlias) {
// Here we handle the beginning of the alias and make sure it starts with a // Here we handle the beginning of the alias and make sure it starts with a "#"
// "#"
if (hash === "" || hash.startsWith("#?")) { if (hash === "" || hash.startsWith("#?")) {
roomAlias = pathname.substring(1); // Strip the "/" roomAlias = pathname.substring(1); // Strip the "/"
@@ -152,30 +230,11 @@ export const getUrlParams = (
roomAlias = `${roomAlias}:${Config.defaultServerName()}`; roomAlias = `${roomAlias}:${Config.defaultServerName()}`;
} }
} }
}
const fragmentQueryStart = hash.indexOf("?"); const parser = new ParamParser(search, hash);
const fragmentParams = new URLSearchParams(
fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart)
);
const queryParams = new URLSearchParams(search);
// 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.
const hasParam = (name: string): boolean =>
fragmentParams.has(name) || queryParams.has(name);
const getParam = (name: string): string | null =>
fragmentParams.get(name) ?? queryParams.get(name);
const getAllParams = (name: string): string[] => [
...fragmentParams.getAll(name),
...queryParams.getAll(name),
];
const fontScale = parseFloat(getParam("fontScale") ?? "");
// Make sure roomId is valid // Make sure roomId is valid
let roomId: string | null = getParam("roomId"); let roomId: string | null = parser.getParam("roomId");
if (!roomId?.startsWith("!")) { if (!roomId?.startsWith("!")) {
roomId = null; roomId = null;
} else if (!roomId.includes("")) { } else if (!roomId.includes("")) {
@@ -185,33 +244,14 @@ export const getUrlParams = (
return { return {
roomAlias, roomAlias,
roomId, roomId,
password: getParam("password"), viaServers: parser.getAllParams("viaServers"),
viaServers: getAllParams("via"),
isEmbedded: hasParam("embed"),
preload: hasParam("preload"),
hideHeader: hasParam("hideHeader"),
hideScreensharing: hasParam("hideScreensharing"),
e2eEnabled: getParam("enableE2e") !== "false", // Defaults to true
userId: getParam("userId"),
displayName: getParam("displayName"),
deviceId: getParam("deviceId"),
baseUrl: getParam("baseUrl"),
lang: getParam("lang"),
fonts: getAllParams("font"),
fontScale: Number.isNaN(fontScale) ? null : fontScale,
analyticsID: getParam("analyticsID"),
allowIceFallback: hasParam("allowIceFallback"),
}; };
}; }
/** export const useRoomIdentifier = (): RoomIdentifier => {
* Hook to simplify use of getUrlParams. const { pathname, search, hash } = useLocation();
* @returns The app parameters for the current URL
*/
export const useUrlParams = (): UrlParams => {
const { search, pathname, hash } = useLocation();
return useMemo( return useMemo(
() => getUrlParams(false, search, pathname, hash), () => getRoomIdentifierFromUrl(pathname, search, hash),
[search, pathname, hash] [pathname, search, hash]
); );
}; };

View File

@@ -62,7 +62,7 @@ export class Initializer {
languageDetector.addDetector({ languageDetector.addDetector({
name: "urlFragment", name: "urlFragment",
// Look for a language code in the URL's fragment // Look for a language code in the URL's fragment
lookup: () => getUrlParams(true).lang ?? undefined, lookup: () => getUrlParams().lang ?? undefined,
}); });
i18n i18n
@@ -95,7 +95,7 @@ export class Initializer {
} }
// Custom fonts // Custom fonts
const { fonts, fontScale } = getUrlParams(true); const { fonts, fontScale } = getUrlParams();
if (fontScale !== null) { if (fontScale !== null) {
document.documentElement.style.setProperty( document.documentElement.style.setProperty(
"--font-scale", "--font-scale",

View File

@@ -22,7 +22,7 @@ import { ErrorView, LoadingView } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView"; import { RoomAuthView } from "./RoomAuthView";
import { GroupCallLoader } from "./GroupCallLoader"; import { GroupCallLoader } from "./GroupCallLoader";
import { GroupCallView } from "./GroupCallView"; import { GroupCallView } from "./GroupCallView";
import { useUrlParams } from "../UrlParams"; import { useRoomIdentifier, useUrlParams } from "../UrlParams";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
import { useOptInAnalytics } from "../settings/useSetting"; import { useOptInAnalytics } from "../settings/useSetting";
import { HomePage } from "../home/HomePage"; import { HomePage } from "../home/HomePage";
@@ -30,15 +30,10 @@ import { platform } from "../Platform";
import { AppSelectionModal } from "./AppSelectionModal"; import { AppSelectionModal } from "./AppSelectionModal";
export const RoomPage: FC = () => { export const RoomPage: FC = () => {
const { const { isEmbedded, preload, hideHeader, displayName } = useUrlParams();
roomAlias,
roomId, const { roomAlias, roomId, viaServers } = useRoomIdentifier();
viaServers,
isEmbedded,
preload,
hideHeader,
displayName,
} = useUrlParams();
const roomIdOrAlias = roomId ?? roomAlias; const roomIdOrAlias = roomId ?? roomAlias;
if (!roomIdOrAlias) { if (!roomIdOrAlias) {
console.error("No room specified"); console.error("No room specified");

View File

@@ -109,7 +109,7 @@ export const widget: WidgetHelpers | null = (() => {
baseUrl, baseUrl,
e2eEnabled, e2eEnabled,
allowIceFallback, allowIceFallback,
} = getUrlParams(true); } = getUrlParams();
if (!roomId) throw new Error("Room ID must be supplied"); if (!roomId) throw new Error("Room ID must be supplied");
if (!userId) throw new Error("User ID must be supplied"); if (!userId) throw new Error("User ID must be supplied");
if (!deviceId) throw new Error("Device ID must be supplied"); if (!deviceId) throw new Error("Device ID must be supplied");

View File

@@ -15,7 +15,8 @@ limitations under the License.
*/ */
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import { getUrlParams } from "../src/UrlParams";
import { getRoomIdentifierFromUrl } from "../src/UrlParams";
import { Config } from "../src/config/Config"; import { Config } from "../src/config/Config";
const ROOM_NAME = "roomNameHere"; const ROOM_NAME = "roomNameHere";
@@ -32,27 +33,28 @@ describe("UrlParams", () => {
describe("handles URL with /room/", () => { describe("handles URL with /room/", () => {
it("and nothing else", () => { it("and nothing else", () => {
expect(getUrlParams(false, "", `/room/${ROOM_NAME}`, "").roomAlias).toBe( expect(
`#${ROOM_NAME}:${HOMESERVER}` getRoomIdentifierFromUrl(`/room/${ROOM_NAME}`, "", "").roomAlias
); ).toBe(`#${ROOM_NAME}:${HOMESERVER}`);
}); });
it("and #", () => { it("and #", () => {
expect( expect(
getUrlParams(false, "", `${ORIGIN}/room/`, `#${ROOM_NAME}`).roomAlias getRoomIdentifierFromUrl("", `${ORIGIN}/room/`, `#${ROOM_NAME}`)
.roomAlias
).toBe(`#${ROOM_NAME}:${HOMESERVER}`); ).toBe(`#${ROOM_NAME}:${HOMESERVER}`);
}); });
it("and # and server part", () => { it("and # and server part", () => {
expect( expect(
getUrlParams(false, "", `/room/`, `#${ROOM_NAME}:${HOMESERVER}`) getRoomIdentifierFromUrl("", `/room/`, `#${ROOM_NAME}:${HOMESERVER}`)
.roomAlias .roomAlias
).toBe(`#${ROOM_NAME}:${HOMESERVER}`); ).toBe(`#${ROOM_NAME}:${HOMESERVER}`);
}); });
it("and server part", () => { it("and server part", () => {
expect( expect(
getUrlParams(false, "", `/room/${ROOM_NAME}:${HOMESERVER}`, "") getRoomIdentifierFromUrl(`/room/${ROOM_NAME}:${HOMESERVER}`, "", "")
.roomAlias .roomAlias
).toBe(`#${ROOM_NAME}:${HOMESERVER}`); ).toBe(`#${ROOM_NAME}:${HOMESERVER}`);
}); });
@@ -60,39 +62,44 @@ describe("UrlParams", () => {
describe("handles URL without /room/", () => { describe("handles URL without /room/", () => {
it("and nothing else", () => { it("and nothing else", () => {
expect(getUrlParams(false, "", `/${ROOM_NAME}`, "").roomAlias).toBe( expect(getRoomIdentifierFromUrl(`/${ROOM_NAME}`, "", "").roomAlias).toBe(
`#${ROOM_NAME}:${HOMESERVER}` `#${ROOM_NAME}:${HOMESERVER}`
); );
}); });
it("and with #", () => { it("and with #", () => {
expect(getUrlParams(false, "", "", `#${ROOM_NAME}`).roomAlias).toBe( expect(getRoomIdentifierFromUrl("", "", `#${ROOM_NAME}`).roomAlias).toBe(
`#${ROOM_NAME}:${HOMESERVER}` `#${ROOM_NAME}:${HOMESERVER}`
); );
}); });
it("and with # and server part", () => { it("and with # and server part", () => {
expect( expect(
getUrlParams(false, "", "", `#${ROOM_NAME}:${HOMESERVER}`).roomAlias getRoomIdentifierFromUrl("", "", `#${ROOM_NAME}:${HOMESERVER}`)
.roomAlias
).toBe(`#${ROOM_NAME}:${HOMESERVER}`); ).toBe(`#${ROOM_NAME}:${HOMESERVER}`);
}); });
it("and with server part", () => { it("and with server part", () => {
expect( expect(
getUrlParams(false, "", `/${ROOM_NAME}:${HOMESERVER}`, "").roomAlias getRoomIdentifierFromUrl(`/${ROOM_NAME}:${HOMESERVER}`, "", "")
.roomAlias
).toBe(`#${ROOM_NAME}:${HOMESERVER}`); ).toBe(`#${ROOM_NAME}:${HOMESERVER}`);
}); });
}); });
describe("handles search params", () => { describe("handles search params", () => {
it("(roomId)", () => { it("(roomId)", () => {
expect(getUrlParams(true, `?roomId=${ROOM_ID}`).roomId).toBe(ROOM_ID); expect(
getRoomIdentifierFromUrl("", `?roomId=${ROOM_ID}`, "").roomId
).toBe(ROOM_ID);
}); });
}); });
it("ignores room alias", () => { it("ignores room alias", () => {
expect( expect(
getUrlParams(true, "", `/room/${ROOM_NAME}:${HOMESERVER}`).roomAlias getRoomIdentifierFromUrl("", `/room/${ROOM_NAME}:${HOMESERVER}`, "")
.roomAlias
).toBeFalsy(); ).toBeFalsy();
}); });
}); });