diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index f925facaf0..71f3c9e079 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -28,6 +28,7 @@ import { decode } from "html-entities"; import { IContent } from "matrix-js-sdk/src/models/event"; import { Optional } from "matrix-events-sdk"; import _Linkify from "linkify-react"; +import escapeHtml from "escape-html"; import { _linkifyElement, @@ -355,10 +356,10 @@ abstract class BaseHighlighter { public constructor(public highlightClass: string, public highlightLink?: string) {} /** - * apply the highlights to a section of text + * Apply the highlights to a section of text * * @param {string} safeSnippet The snippet of text to apply the highlights - * to. + * to. This input must be sanitised as it will be treated as HTML. * @param {string[]} safeHighlights A list of substrings to highlight, * sorted by descending length. * @@ -367,7 +368,7 @@ abstract class BaseHighlighter { */ public applyHighlights(safeSnippet: string, safeHighlights: string[]): T[] { let lastOffset = 0; - let offset; + let offset: number; let nodes: T[] = []; const safeHighlight = safeHighlights[0]; @@ -440,7 +441,7 @@ interface IOpts { } export interface IOptsReturnNode extends IOpts { - returnString: false | undefined; + returnString?: false | undefined; } export interface IOptsReturnString extends IOpts { @@ -574,7 +575,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op safeBody = formatEmojis(safeBody, true).join(""); } } else if (highlighter) { - safeBody = highlighter.applyHighlights(plainBody, safeHighlights!).join(""); + safeBody = highlighter.applyHighlights(escapeHtml(plainBody), safeHighlights!).join(""); } } finally { delete sanitizeParams.textFilter; diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx index be15ea9694..6ddb7f1067 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -71,7 +71,7 @@ export default class SearchResultTile extends React.Component { for (let j = 0; j < timeline.length; j++) { const mxEv = timeline[j]; - let highlights; + let highlights: string[] | undefined; const contextual = !this.props.ourEventsIndexes.includes(j); if (!contextual) { highlights = this.props.searchHighlights; diff --git a/test/HtmlUtils-test.tsx b/test/HtmlUtils-test.tsx index 8baeea446a..d63470e122 100644 --- a/test/HtmlUtils-test.tsx +++ b/test/HtmlUtils-test.tsx @@ -14,11 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactElement } from "react"; import { mocked } from "jest-mock"; import { render, screen } from "@testing-library/react"; +import { IContent } from "matrix-js-sdk/src/models/event"; -import { topicToHtml } from "../src/HtmlUtils"; +import { bodyToHtml, topicToHtml } from "../src/HtmlUtils"; import SettingsStore from "../src/settings/SettingsStore"; jest.mock("../src/settings/SettingsStore"); @@ -29,7 +30,7 @@ const enableHtmlTopicFeature = () => { }); }; -describe("HtmlUtils", () => { +describe("topicToHtml", () => { function getContent() { return screen.getByRole("contentinfo").children[0].innerHTML; } @@ -62,3 +63,47 @@ describe("HtmlUtils", () => { expect(getContent()).toEqual('pizza 🍕'); }); }); + +describe("bodyToHtml", () => { + function getHtml(content: IContent, highlights?: string[]): string { + return (bodyToHtml(content, highlights, {}) as ReactElement).props.dangerouslySetInnerHTML.__html; + } + + it("should apply highlights to HTML messages", () => { + const html = getHtml( + { + body: "test **foo** bar", + msgtype: "m.text", + formatted_body: "test foo bar", + format: "org.matrix.custom.html", + }, + ["test"], + ); + + expect(html).toMatchInlineSnapshot(`"test foo bar"`); + }); + + it("should apply highlights to plaintext messages", () => { + const html = getHtml( + { + body: "test foo bar", + msgtype: "m.text", + }, + ["test"], + ); + + expect(html).toMatchInlineSnapshot(`"test foo bar"`); + }); + + it("should not respect HTML tags in plaintext message highlighting", () => { + const html = getHtml( + { + body: "test foo bar", + msgtype: "m.text", + }, + ["test"], + ); + + expect(html).toMatchInlineSnapshot(`"test foo <b>bar"`); + }); +});