From 66b480f83722fc1d05d901f5c05293c891781b72 Mon Sep 17 00:00:00 2001 From: uchar Date: Wed, 18 Sep 2024 11:13:49 +0330 Subject: [PATCH] use textarea on client for decode --- src/tools/kcSanitize/KcSanitizer.ts | 26 +- test/kcSanitize/KcSanitizer.spec.ts | 356 +++++++++++++++++----------- 2 files changed, 241 insertions(+), 141 deletions(-) diff --git a/src/tools/kcSanitize/KcSanitizer.ts b/src/tools/kcSanitize/KcSanitizer.ts index 471a8751..1be68d42 100644 --- a/src/tools/kcSanitize/KcSanitizer.ts +++ b/src/tools/kcSanitize/KcSanitizer.ts @@ -1,26 +1,44 @@ import { KcSanitizerPolicy } from "keycloakify/tools/kcSanitize/KcSanitizerPolicy"; -import { decode } from "html-entities"; // implementation of keycloak java sanitize method ( KeycloakSanitizerMethod ) // https://github.com/keycloak/keycloak/blob/8ce8a4ba089eef25a0e01f58e09890399477b9ef/services/src/main/java/org/keycloak/theme/KeycloakSanitizerMethod.java#L33 export class KcSanitizer { private static HREF_PATTERN = /\s+href="([^"]*)"/g; + private static textarea: HTMLTextAreaElement | null = null; - public static sanitize(html: string | null): string { + public static async sanitize(html: string | null): Promise { if (html == null) { throw new Error("Cannot escape null value."); } if (html === "") return ""; - html = this.decodeHtmlFull(html); + html = await this.decodeHtmlFull(html); const sanitized = KcSanitizerPolicy.sanitize(html); return this.fixURLs(sanitized); } - private static decodeHtmlFull(html: string): string { + private static async decodeHtmlFull(html: string): Promise { + if (typeof window !== "undefined" && typeof document !== "undefined") { + return KcSanitizer.decodeHtmlOnClient(html); + } else { + return await KcSanitizer.decodeHtmlOnServer(html); + } + } + + private static async decodeHtmlOnServer(html: string): Promise { + // Dynamically import html-entities only on the server side + const { decode } = await import("html-entities"); return decode(html); } + private static decodeHtmlOnClient(html: string): string { + if (!KcSanitizer.textarea) { + KcSanitizer.textarea = document.createElement("textarea"); + } + KcSanitizer.textarea.innerHTML = html; + return KcSanitizer.textarea.value; + } + // This will remove unwanted characters from url private static fixURLs(msg: string): string { const HREF_PATTERN = this.HREF_PATTERN; diff --git a/test/kcSanitize/KcSanitizer.spec.ts b/test/kcSanitize/KcSanitizer.spec.ts index 5260b17f..09a72a70 100644 --- a/test/kcSanitize/KcSanitizer.spec.ts +++ b/test/kcSanitize/KcSanitizer.spec.ts @@ -1,146 +1,228 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, vi, beforeAll } from "vitest"; import { KcSanitizer } from "keycloakify/tools/kcSanitize/KcSanitizer"; // Implementation of Keycloak Java method KeycloakSanitizerTest with bunch of more test for p tag styling // https://github.com/keycloak/keycloak/blob/8ce8a4ba089eef25a0e01f58e09890399477b9ef/services/src/test/java/org/keycloak/theme/KeycloakSanitizerTest.java#L32 -describe("KeycloakSanitizerMethod", () => { - it("should handle escapes correctly", () => { - let html: string | null = ""; - let expectedResult: string | null; - - html = - "
Keycloak
"; - expectedResult = '
Keycloak
'; - assertResult(expectedResult, html); - - html = "

Foo

"; - expectedResult = "

Foo

"; - assertResult(expectedResult, html); - - html = - '
Keycloak
'; - expectedResult = '
Keycloak
'; - assertResult(expectedResult, html); - - html = null; // Type assertion to handle null - expectedResult = null; - expect(() => assertResult(expectedResult, html)).toThrow( +const testCases = [ + { + description: "should handle escapes correctly", + cases: [ + { + html: "
Keycloak
", + expectedResult: '
Keycloak
' + }, + { + html: "

Foo

", + expectedResult: "

Foo

" + }, + { + html: '
Keycloak
', + expectedResult: '
Keycloak
' + }, + { + html: null, + expectedResult: null + }, + { + html: "", + expectedResult: "" + } + ] + }, + { + description: "should handle URLs correctly", + cases: [ + { + html: "

link

", + expectedResult: + '

link

' + }, + { + html: '

link

', + expectedResult: "

link

" + }, + { + html: "

link

", + expectedResult: "

link

" + }, + { + html: '

link

', + expectedResult: "

link

" + }, + { + html: '

link

', + expectedResult: "

link

" + }, + { + html: '

link

', + expectedResult: "

link

" + }, + { + html: '

link

', + expectedResult: "

link

" + }, + { + html: '

link

', + expectedResult: + '

link

' + }, + { + html: '

link2

', + expectedResult: + '

link2

' + } + ] + }, + { + description: "should handle ordinary texts correctly", + cases: [ + { + html: "Some text", + expectedResult: "Some text" + }, + { + html: `text with "double quotation"`, + expectedResult: `text with "double quotation"` + }, + { + html: `text with 'single quotation'`, + expectedResult: `text with 'single quotation'` + } + ] + }, + { + description: "should handle text styles correctly", + cases: [ + { + html: "

text

", + expectedResult: "

text

" + }, + { + html: "

text

", + expectedResult: "

text

" + }, + { + html: '

red text

', + expectedResult: '

red text

' + }, + { + html: '

red text

', + expectedResult: '

red text

' + }, + { + html: '

Case-insensitive

', + expectedResult: '

Case-insensitive

' + }, + { + html: '

wrong value for align

', + expectedResult: "

wrong value for align

" + }, + { + html: '

wrong value for align

', + expectedResult: "

wrong value for align

" + }, + { + html: '

This is a paragraph with larger text.

', + expectedResult: + '

This is a paragraph with larger text.

' + }, + { + html: "

או נושא שתבחר

", + expectedResult: "

או נושא שתבחר

" + } + ] + }, + { + description: "should handle styles correctly", + cases: [ + { + html: '
', + expectedResult: '
' + }, + { + html: '
', + expectedResult: "
" + }, + { + html: ' Content ', + expectedResult: ' Content ' + } + ] + } +]; +const assertResult = async ( + expectedResult: string | null, + html: string | null +): Promise => { + if (html === null) { + await expect(KcSanitizer.sanitize(html)).rejects.toThrow( "Cannot escape null value." ); + } else { + const result = await KcSanitizer.sanitize(html); + expect(result).toBe(expectedResult); + } +}; - html = ""; - expectedResult = ""; - assertResult(expectedResult, html); - }); - - it("should handle URLs correctly", () => { - let html: string = ""; - - html = "

link

"; - assertResult('

link

', html); - - html = '

link

'; - assertResult("

link

", html); - - html = "

link

"; - assertResult("

link

", html); - - html = '

link

'; - assertResult("

link

", html); - - html = '

link

'; - assertResult("

link

", html); - - html = '

link

'; - assertResult("

link

", html); - - html = - '

link

'; - assertResult("

link

", html); - - html = - '

link

'; - assertResult("

link

", html); - - html = '

link

'; - assertResult( - '

link

', - html - ); - - html = - "

link1link2

"; - assertResult( - '

link1link2

', - html - ); - }); - - it("should handle ordinary texts correctly", () => { - let html: string = ""; - - html = "Some text"; - assertResult("Some text", html); - - html = `text with "double quotation"`; - assertResult(`text with "double quotation"`, html); - - html = `text with 'single quotation'`; - assertResult(`text with 'single quotation'`, html); - }); - - it("should handle text styles correctly", () => { - let html: string = ""; - - html = "

text

"; - assertResult("

text

", html); - - html = "

text

"; - assertResult("

text

", html); - - html = `

red text

`; - assertResult(`

red text

`, html); - - html = `

red text

`; - assertResult(`

red text

`, html); - - html = `

Case-insensitive

`; - assertResult(`

Case-insensitive

`, html); - - html = `

wrong value for align

`; - assertResult(`

wrong value for align

`, html); - - html = `

wrong value for align

`; - assertResult(`

wrong value for align

`, html); - - html = `

This is a paragraph with larger text.

`; - assertResult( - `

This is a paragraph with larger text.

`, - html - ); - - html = `

או נושא שתבחר

`; - assertResult(`

או נושא שתבחר

`, html); - }); - - it("should handle styles correctly", () => { - let html = ""; - html = `
`; - assertResult(`
`, html); - - html = `
`; - assertResult(`
`, html); - - html = ` Content `; - assertResult(` Content `, html); - }); - - function assertResult(expectedResult: string | null, html: string | null): void { - if (expectedResult === null) { - expect(KcSanitizer.sanitize(html)).toThrow("Cannot escape null value."); - } else { - const result = KcSanitizer.sanitize(html); - expect(result).toBe(expectedResult); - } +// Server-side tests +describe("KcSanitizer - Server Side", () => { + for (const group of testCases) { + describe(group.description, () => { + for (const test of group.cases) { + it(`should handle ${test.html}`, async () => { + await assertResult(test.expectedResult, test.html); + }); + } + }); + } +}); + +// Client-side tests +describe("KcSanitizer - Client Side (jsdom)", () => { + const decodeHtmlEntities = (html: string): string => { + const entitiesMap: { [key: string]: string } = { + "&": "&", + "<": "<", + ">": ">", + """: '"', + "'": "'" + }; + + return html.replace( + /&|<|>|"|'/g, + entity => entitiesMap[entity] || entity + ); + }; + + beforeAll(() => { + // Mocking the `document.createElement` to simulate textarea behavior + vi.stubGlobal("document", { + createElement: (tagName: string) => { + if (tagName === "textarea") { + let _innerHTML = ""; + return { + get innerHTML() { + return _innerHTML; + }, + set innerHTML(html) { + _innerHTML = html; + this.value = decodeHtmlEntities(html); // Simulate decoding + }, + value: "" // Mimic the textarea behavior where innerHTML -> value + }; + } + throw new Error("Unsupported element"); + } + }); + }); + + for (const group of testCases) { + describe(group.description, () => { + for (const test of group.cases) { + it(`should handle ${test.html}`, async () => { + await assertResult(test.expectedResult, test.html); + }); + } + }); } });