Vendor dompurify, use isomorphic-dompurify only for tests
This commit is contained in:
parent
b6e9043d91
commit
ddb0af1dcb
@ -111,6 +111,11 @@
|
||||
"evt": "^2.5.7",
|
||||
"tsx": "^4.15.5",
|
||||
"html-entities": "^2.5.2",
|
||||
"isomorphic-dompurify": "^2.15.0"
|
||||
"isomorphic-dompurify": "^2.15.0",
|
||||
"dompurify": "^3.1.6",
|
||||
"@types/dompurify": "^2.0.0",
|
||||
"webpack": "5.93.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"@babel/preset-env": "7.24.8"
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,4 @@
|
||||
import * as child_process from "child_process";
|
||||
import { run } from "./shared/run";
|
||||
|
||||
run("yarn build");
|
||||
run("npx build-storybook");
|
||||
|
||||
function run(command: string, options?: { env?: NodeJS.ProcessEnv }) {
|
||||
console.log(`$ ${command}`);
|
||||
|
||||
child_process.execSync(command, { stdio: "inherit", ...options });
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { join } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
@ -6,6 +5,8 @@ import { transformCodebase } from "../../src/bin/tools/transformCodebase";
|
||||
import { createPublicKeycloakifyDevResourcesDir } from "./createPublicKeycloakifyDevResourcesDir";
|
||||
import { createAccountV1Dir } from "./createAccountV1Dir";
|
||||
import chalk from "chalk";
|
||||
import { run } from "../shared/run";
|
||||
import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
|
||||
|
||||
(async () => {
|
||||
console.log(chalk.cyan("Building Keycloakify..."));
|
||||
@ -88,6 +89,7 @@ import chalk from "chalk";
|
||||
|
||||
run(`npx tsc -p ${join("src", "tsconfig.json")}`);
|
||||
run(`npx tsc-alias -p ${join("src", "tsconfig.json")}`);
|
||||
vendorFrontendDependencies({ distDirPath: join("dist") });
|
||||
|
||||
if (fs.existsSync(join("dist", "vite-plugin", "index.original.js"))) {
|
||||
fs.renameSync(
|
||||
@ -164,12 +166,6 @@ import chalk from "chalk";
|
||||
);
|
||||
})();
|
||||
|
||||
function run(command: string) {
|
||||
console.log(chalk.grey(`$ ${command}`));
|
||||
|
||||
child_process.execSync(command, { stdio: "inherit" });
|
||||
}
|
||||
|
||||
function patchDeprecatedBufferApiUsage(filePath: string) {
|
||||
const before = fs.readFileSync(filePath).toString("utf8");
|
||||
|
||||
|
100
scripts/build/vendorFrontendDependencies.ts
Normal file
100
scripts/build/vendorFrontendDependencies.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import * as fs from "fs";
|
||||
import {
|
||||
join as pathJoin,
|
||||
relative as pathRelative,
|
||||
basename as pathBasename,
|
||||
dirname as pathDirname
|
||||
} from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { run } from "../shared/run";
|
||||
import { cacheDirPath as cacheDirPath_base } from "../shared/cacheDirPath";
|
||||
|
||||
export function vendorFrontendDependencies(params: { distDirPath: string }) {
|
||||
const { distDirPath } = params;
|
||||
|
||||
const vendorDirPath = pathJoin(distDirPath, "tools", "vendor");
|
||||
const cacheDirPath = pathJoin(cacheDirPath_base, "vendorFrontendDependencies");
|
||||
|
||||
const extraBundleFileBasenames = new Set<string>();
|
||||
|
||||
fs.readdirSync(vendorDirPath)
|
||||
.filter(fileBasename => fileBasename.endsWith(".js"))
|
||||
.map(fileBasename => pathJoin(vendorDirPath, fileBasename))
|
||||
.forEach(filePath => {
|
||||
{
|
||||
const mapFilePath = `${filePath}.map`;
|
||||
|
||||
if (fs.existsSync(mapFilePath)) {
|
||||
fs.unlinkSync(mapFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(cacheDirPath)) {
|
||||
fs.mkdirSync(cacheDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
const webpackConfigJsFilePath = pathJoin(cacheDirPath, "webpack.config.js");
|
||||
const webpackOutputDirPath = pathJoin(cacheDirPath, "webpack_output");
|
||||
const webpackOutputFilePath = pathJoin(webpackOutputDirPath, "index.js");
|
||||
|
||||
fs.writeFileSync(
|
||||
webpackConfigJsFilePath,
|
||||
Buffer.from(
|
||||
[
|
||||
`const path = require('path');`,
|
||||
``,
|
||||
`module.exports = {`,
|
||||
` mode: 'production',`,
|
||||
` entry: '${filePath}',`,
|
||||
` output: {`,
|
||||
` path: '${webpackOutputDirPath}',`,
|
||||
` filename: '${pathBasename(webpackOutputFilePath)}',`,
|
||||
` libraryTarget: 'module',`,
|
||||
` },`,
|
||||
` target: "web",`,
|
||||
` module: {`,
|
||||
` rules: [`,
|
||||
` {`,
|
||||
` test: /\.js$/,`,
|
||||
` use: {`,
|
||||
` loader: 'babel-loader',`,
|
||||
` options: {`,
|
||||
` presets: ['@babel/preset-env'],`,
|
||||
` }`,
|
||||
` }`,
|
||||
` }`,
|
||||
` ]`,
|
||||
` }`,
|
||||
`};`
|
||||
].join("\n")
|
||||
)
|
||||
);
|
||||
|
||||
run(
|
||||
`npx webpack --config ${pathRelative(process.cwd(), webpackConfigJsFilePath)}`
|
||||
);
|
||||
|
||||
fs.readdirSync(webpackOutputDirPath)
|
||||
.filter(fileBasename => !fileBasename.endsWith(".txt"))
|
||||
.map(fileBasename => pathJoin(webpackOutputDirPath, fileBasename))
|
||||
.forEach(bundleFilePath => {
|
||||
assert(bundleFilePath.endsWith(".js"));
|
||||
|
||||
if (pathBasename(bundleFilePath) === "index.js") {
|
||||
fs.renameSync(webpackOutputFilePath, filePath);
|
||||
} else {
|
||||
const bundleFileBasename = pathBasename(bundleFilePath);
|
||||
|
||||
assert(!extraBundleFileBasenames.has(bundleFileBasename));
|
||||
extraBundleFileBasenames.add(bundleFileBasename);
|
||||
|
||||
fs.renameSync(
|
||||
bundleFilePath,
|
||||
pathJoin(pathDirname(filePath), bundleFileBasename)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
fs.rmSync(webpackOutputDirPath, { recursive: true });
|
||||
});
|
||||
}
|
@ -6,6 +6,7 @@ import chalk from "chalk";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { is } from "tsafe/is";
|
||||
import { run } from "./shared/run";
|
||||
|
||||
(async () => {
|
||||
{
|
||||
@ -84,9 +85,3 @@ import { is } from "tsafe/is";
|
||||
|
||||
console.log(`${chalk.green(`✓ Exported realm to`)} ${chalk.bold(targetFilePath)}`);
|
||||
})();
|
||||
|
||||
function run(command: string) {
|
||||
console.log(chalk.grey(`$ ${command}`));
|
||||
|
||||
return child_process.execSync(command, { stdio: "inherit" });
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import "minimal-polyfills/Object.fromEntries";
|
||||
import * as fs from "fs";
|
||||
import {
|
||||
join as pathJoin,
|
||||
|
@ -1,8 +1,8 @@
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { join } from "path";
|
||||
import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange";
|
||||
import { crawl } from "../src/bin/tools/crawl";
|
||||
import { run } from "./shared/run";
|
||||
|
||||
{
|
||||
const dirPath = "node_modules";
|
||||
@ -47,9 +47,3 @@ run("yarn install", { cwd: join("..", starterName) });
|
||||
run(`npx tsx ${join("scripts", "link-in-app.ts")} ${starterName}`);
|
||||
|
||||
startRebuildOnSrcChange();
|
||||
|
||||
function run(command: string, options?: { cwd: string }) {
|
||||
console.log(`$ ${command}`);
|
||||
|
||||
child_process.execSync(command, { stdio: "inherit", ...options });
|
||||
}
|
||||
|
9
scripts/shared/cacheDirPath.ts
Normal file
9
scripts/shared/cacheDirPath.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { join as pathJoin } from "path";
|
||||
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
|
||||
export const cacheDirPath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"node_modules",
|
||||
".cache",
|
||||
"scripts"
|
||||
);
|
@ -2,8 +2,9 @@ import { relative as pathRelative } from "path";
|
||||
import { downloadAndExtractArchive } from "../../src/bin/tools/downloadAndExtractArchive";
|
||||
import { getProxyFetchOptions } from "../../src/bin/tools/fetchProxyOptions";
|
||||
import { join as pathJoin } from "path";
|
||||
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { cacheDirPath } from "./cacheDirPath";
|
||||
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
|
||||
const KEYCLOAK_VERSION = {
|
||||
FOR_LOGIN_THEME: "25.0.4",
|
||||
@ -22,12 +23,7 @@ export async function downloadKeycloakDefaultTheme(params: {
|
||||
|
||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
||||
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
||||
cacheDirPath: pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"node_modules",
|
||||
".cache",
|
||||
"scripts"
|
||||
),
|
||||
cacheDirPath,
|
||||
fetchOptions: getProxyFetchOptions({
|
||||
npmConfigGetCwd: getThisCodebaseRootDirPath()
|
||||
}),
|
||||
|
8
scripts/shared/run.ts
Normal file
8
scripts/shared/run.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import * as child_process from "child_process";
|
||||
import chalk from "chalk";
|
||||
|
||||
export function run(command: string, options?: { cwd: string }) {
|
||||
console.log(chalk.grey(`$ ${command}`));
|
||||
|
||||
child_process.execSync(command, { stdio: "inherit", ...options });
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import * as child_process from "child_process";
|
||||
import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange";
|
||||
import { run } from "./shared/run";
|
||||
|
||||
(async () => {
|
||||
run("yarn build");
|
||||
@ -18,9 +19,3 @@ import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange";
|
||||
|
||||
startRebuildOnSrcChange();
|
||||
})();
|
||||
|
||||
function run(command: string, options?: { env?: NodeJS.ProcessEnv }) {
|
||||
console.log(`$ ${command}`);
|
||||
|
||||
child_process.execSync(command, { stdio: "inherit", ...options });
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DOMPurify } from "keycloakify/lib/vendor/isomorphic-dompurify";
|
||||
import { DOMPurify } from "keycloakify/tools/vendor/dompurify";
|
||||
|
||||
type TagType = {
|
||||
name: string;
|
||||
@ -22,6 +22,16 @@ export class HtmlPolicyBuilder {
|
||||
private isStylingAllowed: boolean = false;
|
||||
private allowedProtocols: Set<string> = new Set();
|
||||
private enforceRelNofollow: boolean = false;
|
||||
private DOMPurify: typeof DOMPurify;
|
||||
|
||||
// add a constructor
|
||||
constructor(
|
||||
dependencyInjections: Partial<{
|
||||
DOMPurify: typeof DOMPurify;
|
||||
}>
|
||||
) {
|
||||
this.DOMPurify = dependencyInjections.DOMPurify ?? DOMPurify;
|
||||
}
|
||||
|
||||
allowWithoutAttributes(tag: string): this {
|
||||
this.tagsAllowedWithNoAttribute.add(tag);
|
||||
@ -69,7 +79,10 @@ export class HtmlPolicyBuilder {
|
||||
onElements(...tags: string[]): this {
|
||||
if (this.currentAttribute) {
|
||||
tags.forEach(tag => {
|
||||
const element = this.tagsAllowed.get(tag) || { name: tag, attributes: [] };
|
||||
const element = this.tagsAllowed.get(tag) || {
|
||||
name: tag,
|
||||
attributes: []
|
||||
};
|
||||
element.attributes.push(this.currentAttribute!);
|
||||
this.tagsAllowed.set(tag, element);
|
||||
});
|
||||
@ -104,10 +117,10 @@ export class HtmlPolicyBuilder {
|
||||
|
||||
apply(html: string): string {
|
||||
//Clear all previous configs first ( in case we used DOMPurify somewhere else )
|
||||
DOMPurify.clearConfig();
|
||||
DOMPurify.removeAllHooks();
|
||||
this.DOMPurify.clearConfig();
|
||||
this.DOMPurify.removeAllHooks();
|
||||
this.setupHooks();
|
||||
return DOMPurify.sanitize(html, {
|
||||
return this.DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: Array.from(this.tagsAllowed.keys()),
|
||||
ALLOWED_ATTR: this.getAllowedAttributes(),
|
||||
ALLOWED_URI_REGEXP: this.getAllowedUriRegexp(),
|
||||
@ -118,7 +131,7 @@ export class HtmlPolicyBuilder {
|
||||
|
||||
private setupHooks(): void {
|
||||
// Check allowed attribute and global attributes and it doesnt exist in them remove it
|
||||
DOMPurify.addHook("uponSanitizeAttribute", (currentNode, hookEvent) => {
|
||||
this.DOMPurify.addHook("uponSanitizeAttribute", (currentNode, hookEvent) => {
|
||||
if (!hookEvent) return;
|
||||
|
||||
const tagName = currentNode.tagName.toLowerCase();
|
||||
@ -142,16 +155,24 @@ export class HtmlPolicyBuilder {
|
||||
currentNode.removeAttribute(hookEvent.attrName);
|
||||
return;
|
||||
} else {
|
||||
const attributeType = allowedAttributes.find(attr => attr.name === hookEvent.attrName);
|
||||
const attributeType = allowedAttributes.find(
|
||||
attr => attr.name === hookEvent.attrName
|
||||
);
|
||||
if (attributeType) {
|
||||
//Check if attribute value is allowed
|
||||
if (attributeType.matchRegex && !attributeType.matchRegex.test(hookEvent.attrValue)) {
|
||||
if (
|
||||
attributeType.matchRegex &&
|
||||
!attributeType.matchRegex.test(hookEvent.attrValue)
|
||||
) {
|
||||
hookEvent.forceKeepAttr = false;
|
||||
hookEvent.keepAttr = false;
|
||||
currentNode.removeAttribute(hookEvent.attrName);
|
||||
return;
|
||||
}
|
||||
if (attributeType.matchFunction && !attributeType.matchFunction(hookEvent.attrValue)) {
|
||||
if (
|
||||
attributeType.matchFunction &&
|
||||
!attributeType.matchFunction(hookEvent.attrValue)
|
||||
) {
|
||||
hookEvent.forceKeepAttr = false;
|
||||
hookEvent.keepAttr = false;
|
||||
currentNode.removeAttribute(hookEvent.attrName);
|
||||
@ -168,9 +189,12 @@ export class HtmlPolicyBuilder {
|
||||
}
|
||||
});
|
||||
|
||||
DOMPurify.addHook("afterSanitizeAttributes", currentNode => {
|
||||
this.DOMPurify.addHook("afterSanitizeAttributes", currentNode => {
|
||||
// if tag is not allowed to have no attribute then remove it completely
|
||||
if (currentNode.attributes.length == 0 && currentNode.childNodes.length == 0) {
|
||||
if (
|
||||
currentNode.attributes.length == 0 &&
|
||||
currentNode.childNodes.length == 0
|
||||
) {
|
||||
if (!this.tagsAllowedWithNoAttribute.has(currentNode.tagName)) {
|
||||
currentNode.remove();
|
||||
}
|
||||
@ -180,7 +204,10 @@ export class HtmlPolicyBuilder {
|
||||
if (currentNode.attributes.length == 0) {
|
||||
//add currentNode children to parent node
|
||||
while (currentNode.firstChild) {
|
||||
currentNode?.parentNode?.insertBefore(currentNode.firstChild, currentNode);
|
||||
currentNode?.parentNode?.insertBefore(
|
||||
currentNode.firstChild,
|
||||
currentNode
|
||||
);
|
||||
}
|
||||
// Remove the currentNode itself
|
||||
currentNode.remove();
|
||||
@ -191,8 +218,13 @@ export class HtmlPolicyBuilder {
|
||||
if (this.enforceRelNofollow) {
|
||||
if (!currentNode.hasAttribute("rel")) {
|
||||
currentNode.setAttribute("rel", "nofollow");
|
||||
} else if (!currentNode.getAttribute("rel")?.includes("nofollow")) {
|
||||
currentNode.setAttribute("rel", currentNode.getAttribute("rel") + " nofollow");
|
||||
} else if (
|
||||
!currentNode.getAttribute("rel")?.includes("nofollow")
|
||||
) {
|
||||
currentNode.setAttribute(
|
||||
"rel",
|
||||
currentNode.getAttribute("rel") + " nofollow"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { KcSanitizerPolicy } from "./KcSanitizerPolicy";
|
||||
import type { DOMPurify as ofTypeDomPurify } from "keycloakify/tools/vendor/dompurify";
|
||||
|
||||
// implementation of keycloak java sanitize method ( KeycloakSanitizerMethod )
|
||||
// https://github.com/keycloak/keycloak/blob/8ce8a4ba089eef25a0e01f58e09890399477b9ef/services/src/main/java/org/keycloak/theme/KeycloakSanitizerMethod.java#L33
|
||||
@ -6,11 +7,20 @@ export class KcSanitizer {
|
||||
private static HREF_PATTERN = /\s+href="([^"]*)"/g;
|
||||
private static textarea: HTMLTextAreaElement | null = null;
|
||||
|
||||
public static sanitize(html: string, decodeHtml?: (html: string) => string): string {
|
||||
public static sanitize(
|
||||
html: string,
|
||||
dependencyInjections: Partial<{
|
||||
DOMPurify: typeof ofTypeDomPurify;
|
||||
htmlEntitiesDecode: (html: string) => string;
|
||||
}>
|
||||
): string {
|
||||
if (html === "") return "";
|
||||
|
||||
html = decodeHtml !== undefined ? decodeHtml(html) : this.decodeHtml(html);
|
||||
const sanitized = KcSanitizerPolicy.sanitize(html);
|
||||
html =
|
||||
dependencyInjections?.htmlEntitiesDecode !== undefined
|
||||
? dependencyInjections.htmlEntitiesDecode(html)
|
||||
: this.decodeHtml(html);
|
||||
const sanitized = KcSanitizerPolicy.sanitize(html, dependencyInjections);
|
||||
return this.fixURLs(sanitized);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { HtmlPolicyBuilder } from "keycloakify/tools/kcSanitize/HtmlPolicyBuilder";
|
||||
import { HtmlPolicyBuilder } from "./HtmlPolicyBuilder";
|
||||
import type { DOMPurify as ofTypeDomPurify } from "keycloakify/tools/vendor/dompurify";
|
||||
|
||||
//implementation of java Sanitizer policy ( KeycloakSanitizerPolicy )
|
||||
// All regex directly copied from the keycloak source but some of them changed slightly to work with typescript(ONSITE_URL and OFFSITE_URL)
|
||||
@ -76,8 +77,13 @@ export class KcSanitizerPolicy {
|
||||
);
|
||||
}
|
||||
|
||||
public static sanitize(html: string) {
|
||||
return new HtmlPolicyBuilder()
|
||||
public static sanitize(
|
||||
html: string,
|
||||
dependencyInjections: Partial<{
|
||||
DOMPurify: typeof ofTypeDomPurify;
|
||||
}>
|
||||
): string {
|
||||
return new HtmlPolicyBuilder(dependencyInjections)
|
||||
.allowWithoutAttributes("span")
|
||||
|
||||
.allowAttributes("id")
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { KcSanitizer } from "./KcSanitizer";
|
||||
|
||||
export function kcSanitize(html: string): string {
|
||||
return KcSanitizer.sanitize(html);
|
||||
return KcSanitizer.sanitize(html, {});
|
||||
}
|
||||
|
3
src/lib/vendor/isomorphic-dompurify.ts
vendored
3
src/lib/vendor/isomorphic-dompurify.ts
vendored
@ -1,3 +0,0 @@
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
|
||||
export { DOMPurify };
|
3
src/tools/vendor/dompurify.ts
vendored
Normal file
3
src/tools/vendor/dompurify.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
export { DOMPurify };
|
@ -1,6 +1,7 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { KcSanitizer } from "keycloakify/lib/kcSanitize/KcSanitizer";
|
||||
import { decode } from "html-entities";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
|
||||
// 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
|
||||
@ -131,7 +132,10 @@ describe("KeycloakSanitizerMethod", () => {
|
||||
});
|
||||
|
||||
function assertResult(expectedResult: string, html: string): void {
|
||||
const result = KcSanitizer.sanitize(html, decode);
|
||||
const result = KcSanitizer.sanitize(html, {
|
||||
DOMPurify: DOMPurify as any,
|
||||
htmlEntitiesDecode: decode
|
||||
});
|
||||
expect(result).toBe(expectedResult);
|
||||
}
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user