From d77534c1de1ae1da9ca66f2c07c4ba4ae2441ffa Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 14 Oct 2024 23:24:17 +0300 Subject: [PATCH] web/util: move identifier validation functions to separate file --- web/src/api/media.ts | 25 +++++-------- web/src/ui/timeline/TimelineEvent.tsx | 4 +-- web/src/util/validation.ts | 51 +++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 19 deletions(-) create mode 100644 web/src/util/validation.ts diff --git a/web/src/api/media.ts b/web/src/api/media.ts index 0de06ad..933c654 100644 --- a/web/src/api/media.ts +++ b/web/src/api/media.ts @@ -13,20 +13,15 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - +import { parseMXC } from "@/util/validation.ts" import { UserID } from "./types" -const mediaRegex = /^mxc:\/\/([a-zA-Z0-9.:-]+)\/([a-zA-Z0-9_-]+)$/ - export const getMediaURL = (mxc?: string, encrypted: boolean = false): string | undefined => { - if (!mxc) { + const [server, mediaID] = parseMXC(mxc) + if (!mediaID) { return undefined } - const match = mxc.match(mediaRegex) - if (!match) { - return undefined - } - return `_gomuks/media/${match[1]}/${match[2]}?encrypted=${encrypted}` + return `_gomuks/media/${server}/${mediaID}?encrypted=${encrypted}` } export const getEncryptedMediaURL = (mxc?: string): string | undefined => { @@ -34,15 +29,11 @@ export const getEncryptedMediaURL = (mxc?: string): string | undefined => { } export const getAvatarURL = (_userID: UserID, mxc?: string): string | undefined => { - if (!mxc) { + const [server, mediaID] = parseMXC(mxc) + if (!mediaID) { return undefined // return `_gomuks/avatar/${encodeURIComponent(userID)}` } - const match = mxc.match(mediaRegex) - if (!match) { - return undefined - // return `_gomuks/avatar/${encodeURIComponent(userID)}` - } - return `_gomuks/media/${match[1]}/${match[2]}` - // return `_gomuks/avatar/${encodeURIComponent(userID)}/${match[1]}/${match[2]}` + return `_gomuks/media/${server}/${mediaID}` + // return `_gomuks/avatar/${encodeURIComponent(userID)}/${server}/${mediaID}` } diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx index 33ba74f..d608467 100644 --- a/web/src/ui/timeline/TimelineEvent.tsx +++ b/web/src/ui/timeline/TimelineEvent.tsx @@ -17,6 +17,7 @@ import React, { use, useCallback } from "react" import { getAvatarURL } from "@/api/media.ts" import { RoomStateStore } from "@/api/statestore" import { MemDBEvent, MemberEventContent } from "@/api/types" +import { isEventID } from "@/util/validation.ts" import { ClientContext } from "../ClientContext.ts" import { LightboxContext } from "../Lightbox.tsx" import { ReplyIDBody } from "./ReplyBody.tsx" @@ -152,8 +153,7 @@ const TimelineEvent = ({ room, evt, prevEvt, setReplyTo }: TimelineEventProps) = {shortTime}
- {typeof replyTo === "string" && BodyType !== HiddenEvent - ? : null} + {isEventID(replyTo) && BodyType !== HiddenEvent ? : null} {evt.reactions ? : null}
diff --git a/web/src/util/validation.ts b/web/src/util/validation.ts new file mode 100644 index 0000000..5917575 --- /dev/null +++ b/web/src/util/validation.ts @@ -0,0 +1,51 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { ContentURI, EventID, RoomAlias, RoomID, UserID } from "@/api/types" + +const simpleHomeserverRegex = /^[a-zA-Z0-9.:-]+$/ +const mediaRegex = /^mxc:\/\/([a-zA-Z0-9.:-]+)\/([a-zA-Z0-9_-]+)$/ + +function isIdentifier(identifier: unknown, sigil: string, requiresServer: boolean): identifier is T { + if (typeof identifier !== "string" || !identifier.startsWith(sigil)) { + return false + } + if (requiresServer) { + const idx = identifier.indexOf(":") + return idx > 0 && simpleHomeserverRegex.test(identifier.slice(idx+1)) + } + return true +} + +export function validated(value: T | undefined, validator: (value: T) => boolean): value is T { + return value !== undefined && validator(value) +} + +export const isEventID = (eventID: unknown) => isIdentifier(eventID, "$", false) +export const isUserID = (userID: unknown) => isIdentifier(userID, "@", true) +export const isRoomID = (roomID: unknown) => isIdentifier(roomID, "!", true) +export const isRoomAlias = (roomAlias: unknown) => isIdentifier(roomAlias, "#", true) +export const isMXC = (mxc: unknown): mxc is ContentURI => typeof mxc === "string" && mediaRegex.test(mxc) + +export function parseMXC(mxc: unknown): [string, string] | [] { + if (typeof mxc !== "string") { + return [] + } + const match = mxc.match(mediaRegex) + if (!match) { + return [] + } + return [match[1], match[2]] +}