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": " 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
+
+
+`;