From 11785fc243c7fab7b6c997111b6b932c785afd8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 Jul 2023 14:59:26 +0200 Subject: [PATCH] Avoid redirects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/App.tsx | 3 +- src/UrlParams.ts | 64 ++++++++++++----- src/initializer.tsx | 4 +- src/room/RoomRedirect.tsx | 44 ------------ test/UrlParams-test.ts | 146 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 197 insertions(+), 64 deletions(-) delete mode 100644 src/room/RoomRedirect.tsx create mode 100644 test/UrlParams-test.ts diff --git a/src/App.tsx b/src/App.tsx index d020655d..0822849d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,7 +23,6 @@ import { HomePage } from "./home/HomePage"; import { LoginPage } from "./auth/LoginPage"; import { RegisterPage } from "./auth/RegisterPage"; import { RoomPage } from "./room/RoomPage"; -import { RoomRedirect } from "./room/RoomRedirect"; import { ClientProvider } from "./ClientContext"; import { usePageFocusStyle } from "./usePageFocusStyle"; import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage"; @@ -75,7 +74,7 @@ export default function App({ history }: AppProps) { - + diff --git a/src/UrlParams.ts b/src/UrlParams.ts index eb2b1795..55d8ee10 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 New Vector Ltd +Copyright 2022 - 2023 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,9 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useMemo } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useLocation } from "react-router-dom"; +import { Config } from "./config/Config"; + interface UrlParams { roomAlias: string | null; roomId: string | null; @@ -93,14 +95,38 @@ interface UrlParams { * @returns The app parameters encoded in the URL */ export const getUrlParams = ( - query: string = window.location.search, - fragment: string = window.location.hash + ignoreRoomAlias?: boolean, + location: Location = window.location ): UrlParams => { - const fragmentQueryStart = fragment.indexOf("?"); + const { href, origin, search, hash } = location; + + let roomAlias: string | undefined; + if (!ignoreRoomAlias) { + roomAlias = href.substring(origin.length + 1); + + // If we have none, we throw + if (!roomAlias) { + throw Error("No pathname"); + } + // Delete "/room/" and "?", if present + if (roomAlias.startsWith("room/")) { + roomAlias = roomAlias.substring("room/".length).split("?")[0]; + } + // Add "#", if not present + if (!roomAlias.includes("#")) { + roomAlias = `#${roomAlias}`; + } + // Add server part, if missing + if (!roomAlias.includes(":")) { + roomAlias = `${roomAlias}:${Config.defaultServerName()}`; + } + } + + const fragmentQueryStart = hash.indexOf("?"); const fragmentParams = new URLSearchParams( - fragmentQueryStart === -1 ? "" : fragment.substring(fragmentQueryStart) + fragmentQueryStart === -1 ? "" : hash.substring(fragmentQueryStart) ); - const queryParams = new URLSearchParams(query); + 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 @@ -114,16 +140,10 @@ export const getUrlParams = ( ...queryParams.getAll(name), ]; - // The part of the fragment before the ? - const fragmentRoute = - fragmentQueryStart === -1 - ? fragment - : fragment.substring(0, fragmentQueryStart); - const fontScale = parseFloat(getParam("fontScale") ?? ""); return { - roomAlias: fragmentRoute.length > 1 ? fragmentRoute : null, + roomAlias: !roomAlias || roomAlias.includes("!") ? null : roomAlias, roomId: getParam("roomId"), viaServers: getAllParams("via"), isEmbedded: hasParam("embed"), @@ -149,6 +169,18 @@ export const getUrlParams = ( * @returns The app parameters for the current URL */ export const useUrlParams = (): UrlParams => { - const { hash, search } = useLocation(); - return useMemo(() => getUrlParams(search, hash), [search, hash]); + const getParams = useCallback(() => { + return getUrlParams(false, window.location); + }, []); + + const reactDomLocation = useLocation(); + const [urlParams, setUrlParams] = useState(getParams()); + + useEffect(() => { + if (window.location !== reactDomLocation) { + setUrlParams(getParams()); + } + }, [getParams, reactDomLocation]); + + return urlParams; }; diff --git a/src/initializer.tsx b/src/initializer.tsx index 37e659e7..f8a6c985 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -55,7 +55,7 @@ export class Initializer { languageDetector.addDetector({ name: "urlFragment", // Look for a language code in the URL's fragment - lookup: () => getUrlParams().lang ?? undefined, + lookup: () => getUrlParams(true).lang ?? undefined, }); i18n @@ -136,7 +136,7 @@ export class Initializer { } // Custom fonts - const { fonts, fontScale } = getUrlParams(); + const { fonts, fontScale } = getUrlParams(true); if (fontScale !== null) { document.documentElement.style.setProperty( "--font-scale", diff --git a/src/room/RoomRedirect.tsx b/src/room/RoomRedirect.tsx deleted file mode 100644 index 3d45086d..00000000 --- a/src/room/RoomRedirect.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2022 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { useEffect } from "react"; -import { useLocation, useHistory } from "react-router-dom"; - -import { Config } from "../config/Config"; -import { LoadingView } from "../FullScreenView"; - -// A component that, when loaded, redirects the client to a full room URL -// based on the current URL being an abbreviated room URL -export function RoomRedirect() { - const { pathname } = useLocation(); - const history = useHistory(); - - useEffect(() => { - let roomId = pathname; - - if (pathname.startsWith("/")) { - roomId = roomId.substring(1, roomId.length); - } - - if (!roomId.startsWith("#") && !roomId.startsWith("!")) { - roomId = `#${roomId}:${Config.defaultServerName()}`; - } - - history.replace(`/room/${roomId.toLowerCase()}`); - }, [pathname, history]); - - return ; -} diff --git a/test/UrlParams-test.ts b/test/UrlParams-test.ts new file mode 100644 index 00000000..9f691326 --- /dev/null +++ b/test/UrlParams-test.ts @@ -0,0 +1,146 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { mocked } from "jest-mock"; +import { getUrlParams } from "../src/UrlParams"; +import { Config } from "../src/config/Config"; + +const ROOM_NAME = "roomNameHere"; +const ROOM_ID = "d45f138fsd"; +const ORIGIN = "https://call.element.io"; +const HOMESERVER = "call.ems.host"; + +jest.mock("../src/config/Config"); + +describe("UrlParams", () => { + beforeAll(() => { + mocked(Config.defaultServerName).mockReturnValue("call.ems.host"); + }); + + describe("handles URL with /room/", () => { + it("and nothing else", () => { + expect( + getUrlParams(false, { + origin: ORIGIN, + href: `${ORIGIN}/room/${ROOM_NAME}`, + search: "", + hash: "", + } as Location).roomAlias + ).toBe(`#${ROOM_NAME}:${HOMESERVER}`); + }); + + it("and #", () => { + expect( + getUrlParams(false, { + origin: ORIGIN, + href: `${ORIGIN}/room/#${ROOM_NAME}`, + search: "", + hash: "", + } as Location).roomAlias + ).toBe(`#${ROOM_NAME}:${HOMESERVER}`); + }); + + it("and # and server part", () => { + expect( + getUrlParams(false, { + origin: ORIGIN, + href: `${ORIGIN}/room/#${ROOM_NAME}:${HOMESERVER}`, + search: "", + hash: "", + } as Location).roomAlias + ).toBe(`#${ROOM_NAME}:${HOMESERVER}`); + }); + + it("and server part", () => { + expect( + getUrlParams(false, { + origin: ORIGIN, + href: `${ORIGIN}/room/${ROOM_NAME}:${HOMESERVER}`, + search: "", + hash: "", + } as Location).roomAlias + ).toBe(`#${ROOM_NAME}:${HOMESERVER}`); + }); + }); + + describe("handles URL without /room/", () => { + it("and nothing else", () => { + expect( + getUrlParams(false, { + origin: ORIGIN, + href: `${ORIGIN}/${ROOM_NAME}`, + search: "", + hash: "", + } as Location).roomAlias + ).toBe(`#${ROOM_NAME}:${HOMESERVER}`); + }); + + it("and with #", () => { + expect( + getUrlParams(false, { + origin: ORIGIN, + href: `${ORIGIN}/room/#${ROOM_NAME}`, + search: "", + hash: "", + } as Location).roomAlias + ).toBe(`#${ROOM_NAME}:${HOMESERVER}`); + }); + + it("and with # and server part", () => { + expect( + getUrlParams(false, { + origin: ORIGIN, + href: `${ORIGIN}/room/#${ROOM_NAME}:${HOMESERVER}`, + search: "", + hash: "", + } as Location).roomAlias + ).toBe(`#${ROOM_NAME}:${HOMESERVER}`); + }); + + it("and with server part", () => { + expect( + getUrlParams(false, { + origin: ORIGIN, + href: `${ORIGIN}/room/${ROOM_NAME}:${HOMESERVER}`, + search: "", + hash: "", + } as Location).roomAlias + ).toBe(`#${ROOM_NAME}:${HOMESERVER}`); + }); + }); + + describe("handles search params", () => { + it("(roomId)", () => { + expect( + getUrlParams(true, { + search: `?roomId=${ROOM_ID}`, + hash: "", + } as Location).roomId + ).toBe(ROOM_ID); + }); + }); + + it("ignores room alias", () => { + expect( + getUrlParams(true, { + origin: ORIGIN, + href: `${ORIGIN}/room/${ROOM_NAME}:${HOMESERVER}`, + hash: "", + search: "", + } as Location).roomAlias + ).toBeFalsy(); + }); +});