diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index ec8662eaf5..abc7c74c12 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024, 2025 New Vector Ltd. Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2017, 2018 New Vector Ltd @@ -22,7 +22,7 @@ import { getEmojiFromUnicode } from "@matrix-org/emojibase-bindings"; import SettingsStore from "./settings/SettingsStore"; import { stripHTMLReply, stripPlainReply } from "./utils/Reply"; import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils"; -import { filterImg, sanitizeHtmlParams, transformTags } from "./Linkify"; +import { sanitizeHtmlParams, transformTags } from "./Linkify"; import { graphemeSegmenter } from "./utils/strings"; export { Linkify, linkifyAndSanitizeHtml } from "./Linkify"; @@ -302,8 +302,15 @@ function analyseEvent(content: IContent, highlights: Optional, opts: E if (opts.forComposerQuote) { sanitizeParams = composerSanitizeHtmlParams; } - if (!opts.mediaIsVisible) { - sanitizeParams.exclusiveFilter = filterImg; + + if (opts.mediaIsVisible === false && sanitizeParams.transformTags?.["img"]) { + // Prevent mutating the source of sanitizeParams. + sanitizeParams.transformTags = { + ...sanitizeParams.transformTags, + img: (tagName) => { + return { tagName, attribs: {} }; + }, + }; } try { diff --git a/src/Linkify.tsx b/src/Linkify.tsx index b67d0294fe..ae5447502a 100644 --- a/src/Linkify.tsx +++ b/src/Linkify.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { type ReactElement } from "react"; -import sanitizeHtml, { IFrame, type IOptions } from "sanitize-html"; +import sanitizeHtml, { type IOptions } from "sanitize-html"; import { merge } from "lodash"; import _Linkify from "linkify-react"; @@ -46,8 +46,6 @@ export const transformTags: NonNullable = { // Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag // because transformTags is used _before_ we filter by allowedSchemesByTag and // we don't want to allow images with `https?` `src`s. - // Filtering out images now happens as a exlusive filter so we can conditionally apply this - // based on settings. if (!src) { return { tagName, attribs: {} }; } @@ -76,7 +74,6 @@ export const transformTags: NonNullable = { if (requestedHeight) { attribs.style += "height: 100%;"; } - attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height)!; return { tagName, attribs }; }, @@ -196,7 +193,6 @@ export const sanitizeHtmlParams: IOptions = { nestingLimit: 50, }; - /* Wrapper around linkify-react merging in our default linkify options */ export function Linkify({ as, options, children }: React.ComponentProps): ReactElement { return ( @@ -227,7 +223,3 @@ export function linkifyString(str: string, options = linkifyMatrixOptions): stri export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatrixOptions): string { return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams); } - -export function filterImg(frame: IFrame): boolean { - return frame.tag === "img"; -} \ No newline at end of file diff --git a/test/unit-tests/HtmlUtils-test.tsx b/test/unit-tests/HtmlUtils-test.tsx index 0650db1890..6a28fbad6b 100644 --- a/test/unit-tests/HtmlUtils-test.tsx +++ b/test/unit-tests/HtmlUtils-test.tsx @@ -13,6 +13,7 @@ import parse from "html-react-parser"; import { bodyToHtml, bodyToNode, formatEmojis, topicToHtml } from "../../src/HtmlUtils"; import SettingsStore from "../../src/settings/SettingsStore"; +import { getMockClientWithEventEmitter } from "../test-utils"; jest.mock("../../src/settings/SettingsStore"); @@ -228,4 +229,39 @@ describe("bodyToNode", () => { expect(asFragment()).toMatchSnapshot(); }); + + it.each([[true], [false]])("should handle inline media when mediaIsVisible is %s", (mediaIsVisible) => { + const cli = getMockClientWithEventEmitter({ + mxcUrlToHttp: jest.fn().mockReturnValue("https://example.org/img"), + }); + const { className, formattedBody } = bodyToNode( + { + "body": "![foo](mxc://going/knowwhere) Hello there", + "format": "org.matrix.custom.html", + "formatted_body": `foo Hello there`, + "m.relates_to": { + "m.in_reply_to": { + event_id: "$eventId", + }, + }, + "msgtype": "m.text", + }, + [], + { + mediaIsVisible, + }, + ); + + const { asFragment } = render( + , + ); + expect(asFragment()).toMatchSnapshot(); + // We do not want to download untrusted media. + // eslint-disable-next-line no-restricted-properties + expect(cli.mxcUrlToHttp).toHaveBeenCalledTimes(mediaIsVisible ? 1 : 0); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); }); diff --git a/test/unit-tests/__snapshots__/HtmlUtils-test.tsx.snap b/test/unit-tests/__snapshots__/HtmlUtils-test.tsx.snap index 09ab44bfcb..018a6721c1 100644 --- a/test/unit-tests/__snapshots__/HtmlUtils-test.tsx.snap +++ b/test/unit-tests/__snapshots__/HtmlUtils-test.tsx.snap @@ -64,3 +64,30 @@ exports[`bodyToNode should generate big emoji for an emoji-only reply to a messa `; + +exports[`bodyToNode should handle inline media when mediaIsVisible is false 1`] = ` + + + + foo Hello there + + +`; + +exports[`bodyToNode should handle inline media when mediaIsVisible is true 1`] = ` + + + + foo Hello there + + +`;