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",
|
"evt": "^2.5.7",
|
||||||
"tsx": "^4.15.5",
|
"tsx": "^4.15.5",
|
||||||
"html-entities": "^2.5.2",
|
"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("yarn build");
|
||||||
run("npx build-storybook");
|
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 * as fs from "fs";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
@ -6,6 +5,8 @@ import { transformCodebase } from "../../src/bin/tools/transformCodebase";
|
|||||||
import { createPublicKeycloakifyDevResourcesDir } from "./createPublicKeycloakifyDevResourcesDir";
|
import { createPublicKeycloakifyDevResourcesDir } from "./createPublicKeycloakifyDevResourcesDir";
|
||||||
import { createAccountV1Dir } from "./createAccountV1Dir";
|
import { createAccountV1Dir } from "./createAccountV1Dir";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
import { run } from "../shared/run";
|
||||||
|
import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
console.log(chalk.cyan("Building Keycloakify..."));
|
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 -p ${join("src", "tsconfig.json")}`);
|
||||||
run(`npx tsc-alias -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"))) {
|
if (fs.existsSync(join("dist", "vite-plugin", "index.original.js"))) {
|
||||||
fs.renameSync(
|
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) {
|
function patchDeprecatedBufferApiUsage(filePath: string) {
|
||||||
const before = fs.readFileSync(filePath).toString("utf8");
|
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 { Deferred } from "evt/tools/Deferred";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { is } from "tsafe/is";
|
import { is } from "tsafe/is";
|
||||||
|
import { run } from "./shared/run";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
{
|
{
|
||||||
@ -84,9 +85,3 @@ import { is } from "tsafe/is";
|
|||||||
|
|
||||||
console.log(`${chalk.green(`✓ Exported realm to`)} ${chalk.bold(targetFilePath)}`);
|
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 * as fs from "fs";
|
||||||
import {
|
import {
|
||||||
join as pathJoin,
|
join as pathJoin,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import * as child_process from "child_process";
|
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange";
|
import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange";
|
||||||
import { crawl } from "../src/bin/tools/crawl";
|
import { crawl } from "../src/bin/tools/crawl";
|
||||||
|
import { run } from "./shared/run";
|
||||||
|
|
||||||
{
|
{
|
||||||
const dirPath = "node_modules";
|
const dirPath = "node_modules";
|
||||||
@ -47,9 +47,3 @@ run("yarn install", { cwd: join("..", starterName) });
|
|||||||
run(`npx tsx ${join("scripts", "link-in-app.ts")} ${starterName}`);
|
run(`npx tsx ${join("scripts", "link-in-app.ts")} ${starterName}`);
|
||||||
|
|
||||||
startRebuildOnSrcChange();
|
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 { downloadAndExtractArchive } from "../../src/bin/tools/downloadAndExtractArchive";
|
||||||
import { getProxyFetchOptions } from "../../src/bin/tools/fetchProxyOptions";
|
import { getProxyFetchOptions } from "../../src/bin/tools/fetchProxyOptions";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
|
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
|
import { cacheDirPath } from "./cacheDirPath";
|
||||||
|
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
|
||||||
|
|
||||||
const KEYCLOAK_VERSION = {
|
const KEYCLOAK_VERSION = {
|
||||||
FOR_LOGIN_THEME: "25.0.4",
|
FOR_LOGIN_THEME: "25.0.4",
|
||||||
@ -22,12 +23,7 @@ export async function downloadKeycloakDefaultTheme(params: {
|
|||||||
|
|
||||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
const { extractedDirPath } = await downloadAndExtractArchive({
|
||||||
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
||||||
cacheDirPath: pathJoin(
|
cacheDirPath,
|
||||||
getThisCodebaseRootDirPath(),
|
|
||||||
"node_modules",
|
|
||||||
".cache",
|
|
||||||
"scripts"
|
|
||||||
),
|
|
||||||
fetchOptions: getProxyFetchOptions({
|
fetchOptions: getProxyFetchOptions({
|
||||||
npmConfigGetCwd: getThisCodebaseRootDirPath()
|
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 * as child_process from "child_process";
|
||||||
import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange";
|
import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange";
|
||||||
|
import { run } from "./shared/run";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
run("yarn build");
|
run("yarn build");
|
||||||
@ -18,9 +19,3 @@ import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange";
|
|||||||
|
|
||||||
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 = {
|
type TagType = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -22,6 +22,16 @@ export class HtmlPolicyBuilder {
|
|||||||
private isStylingAllowed: boolean = false;
|
private isStylingAllowed: boolean = false;
|
||||||
private allowedProtocols: Set<string> = new Set();
|
private allowedProtocols: Set<string> = new Set();
|
||||||
private enforceRelNofollow: boolean = false;
|
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 {
|
allowWithoutAttributes(tag: string): this {
|
||||||
this.tagsAllowedWithNoAttribute.add(tag);
|
this.tagsAllowedWithNoAttribute.add(tag);
|
||||||
@ -69,7 +79,10 @@ export class HtmlPolicyBuilder {
|
|||||||
onElements(...tags: string[]): this {
|
onElements(...tags: string[]): this {
|
||||||
if (this.currentAttribute) {
|
if (this.currentAttribute) {
|
||||||
tags.forEach(tag => {
|
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!);
|
element.attributes.push(this.currentAttribute!);
|
||||||
this.tagsAllowed.set(tag, element);
|
this.tagsAllowed.set(tag, element);
|
||||||
});
|
});
|
||||||
@ -104,10 +117,10 @@ export class HtmlPolicyBuilder {
|
|||||||
|
|
||||||
apply(html: string): string {
|
apply(html: string): string {
|
||||||
//Clear all previous configs first ( in case we used DOMPurify somewhere else )
|
//Clear all previous configs first ( in case we used DOMPurify somewhere else )
|
||||||
DOMPurify.clearConfig();
|
this.DOMPurify.clearConfig();
|
||||||
DOMPurify.removeAllHooks();
|
this.DOMPurify.removeAllHooks();
|
||||||
this.setupHooks();
|
this.setupHooks();
|
||||||
return DOMPurify.sanitize(html, {
|
return this.DOMPurify.sanitize(html, {
|
||||||
ALLOWED_TAGS: Array.from(this.tagsAllowed.keys()),
|
ALLOWED_TAGS: Array.from(this.tagsAllowed.keys()),
|
||||||
ALLOWED_ATTR: this.getAllowedAttributes(),
|
ALLOWED_ATTR: this.getAllowedAttributes(),
|
||||||
ALLOWED_URI_REGEXP: this.getAllowedUriRegexp(),
|
ALLOWED_URI_REGEXP: this.getAllowedUriRegexp(),
|
||||||
@ -118,7 +131,7 @@ export class HtmlPolicyBuilder {
|
|||||||
|
|
||||||
private setupHooks(): void {
|
private setupHooks(): void {
|
||||||
// Check allowed attribute and global attributes and it doesnt exist in them remove it
|
// 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;
|
if (!hookEvent) return;
|
||||||
|
|
||||||
const tagName = currentNode.tagName.toLowerCase();
|
const tagName = currentNode.tagName.toLowerCase();
|
||||||
@ -142,16 +155,24 @@ export class HtmlPolicyBuilder {
|
|||||||
currentNode.removeAttribute(hookEvent.attrName);
|
currentNode.removeAttribute(hookEvent.attrName);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
const attributeType = allowedAttributes.find(attr => attr.name === hookEvent.attrName);
|
const attributeType = allowedAttributes.find(
|
||||||
|
attr => attr.name === hookEvent.attrName
|
||||||
|
);
|
||||||
if (attributeType) {
|
if (attributeType) {
|
||||||
//Check if attribute value is allowed
|
//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.forceKeepAttr = false;
|
||||||
hookEvent.keepAttr = false;
|
hookEvent.keepAttr = false;
|
||||||
currentNode.removeAttribute(hookEvent.attrName);
|
currentNode.removeAttribute(hookEvent.attrName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (attributeType.matchFunction && !attributeType.matchFunction(hookEvent.attrValue)) {
|
if (
|
||||||
|
attributeType.matchFunction &&
|
||||||
|
!attributeType.matchFunction(hookEvent.attrValue)
|
||||||
|
) {
|
||||||
hookEvent.forceKeepAttr = false;
|
hookEvent.forceKeepAttr = false;
|
||||||
hookEvent.keepAttr = false;
|
hookEvent.keepAttr = false;
|
||||||
currentNode.removeAttribute(hookEvent.attrName);
|
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 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)) {
|
if (!this.tagsAllowedWithNoAttribute.has(currentNode.tagName)) {
|
||||||
currentNode.remove();
|
currentNode.remove();
|
||||||
}
|
}
|
||||||
@ -180,7 +204,10 @@ export class HtmlPolicyBuilder {
|
|||||||
if (currentNode.attributes.length == 0) {
|
if (currentNode.attributes.length == 0) {
|
||||||
//add currentNode children to parent node
|
//add currentNode children to parent node
|
||||||
while (currentNode.firstChild) {
|
while (currentNode.firstChild) {
|
||||||
currentNode?.parentNode?.insertBefore(currentNode.firstChild, currentNode);
|
currentNode?.parentNode?.insertBefore(
|
||||||
|
currentNode.firstChild,
|
||||||
|
currentNode
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Remove the currentNode itself
|
// Remove the currentNode itself
|
||||||
currentNode.remove();
|
currentNode.remove();
|
||||||
@ -191,8 +218,13 @@ export class HtmlPolicyBuilder {
|
|||||||
if (this.enforceRelNofollow) {
|
if (this.enforceRelNofollow) {
|
||||||
if (!currentNode.hasAttribute("rel")) {
|
if (!currentNode.hasAttribute("rel")) {
|
||||||
currentNode.setAttribute("rel", "nofollow");
|
currentNode.setAttribute("rel", "nofollow");
|
||||||
} else if (!currentNode.getAttribute("rel")?.includes("nofollow")) {
|
} else if (
|
||||||
currentNode.setAttribute("rel", currentNode.getAttribute("rel") + " nofollow");
|
!currentNode.getAttribute("rel")?.includes("nofollow")
|
||||||
|
) {
|
||||||
|
currentNode.setAttribute(
|
||||||
|
"rel",
|
||||||
|
currentNode.getAttribute("rel") + " nofollow"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { KcSanitizerPolicy } from "./KcSanitizerPolicy";
|
import { KcSanitizerPolicy } from "./KcSanitizerPolicy";
|
||||||
|
import type { DOMPurify as ofTypeDomPurify } from "keycloakify/tools/vendor/dompurify";
|
||||||
|
|
||||||
// implementation of keycloak java sanitize method ( KeycloakSanitizerMethod )
|
// implementation of keycloak java sanitize method ( KeycloakSanitizerMethod )
|
||||||
// https://github.com/keycloak/keycloak/blob/8ce8a4ba089eef25a0e01f58e09890399477b9ef/services/src/main/java/org/keycloak/theme/KeycloakSanitizerMethod.java#L33
|
// 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 HREF_PATTERN = /\s+href="([^"]*)"/g;
|
||||||
private static textarea: HTMLTextAreaElement | null = null;
|
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 "";
|
if (html === "") return "";
|
||||||
|
|
||||||
html = decodeHtml !== undefined ? decodeHtml(html) : this.decodeHtml(html);
|
html =
|
||||||
const sanitized = KcSanitizerPolicy.sanitize(html);
|
dependencyInjections?.htmlEntitiesDecode !== undefined
|
||||||
|
? dependencyInjections.htmlEntitiesDecode(html)
|
||||||
|
: this.decodeHtml(html);
|
||||||
|
const sanitized = KcSanitizerPolicy.sanitize(html, dependencyInjections);
|
||||||
return this.fixURLs(sanitized);
|
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 )
|
//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)
|
// 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) {
|
public static sanitize(
|
||||||
return new HtmlPolicyBuilder()
|
html: string,
|
||||||
|
dependencyInjections: Partial<{
|
||||||
|
DOMPurify: typeof ofTypeDomPurify;
|
||||||
|
}>
|
||||||
|
): string {
|
||||||
|
return new HtmlPolicyBuilder(dependencyInjections)
|
||||||
.allowWithoutAttributes("span")
|
.allowWithoutAttributes("span")
|
||||||
|
|
||||||
.allowAttributes("id")
|
.allowAttributes("id")
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { KcSanitizer } from "./KcSanitizer";
|
import { KcSanitizer } from "./KcSanitizer";
|
||||||
|
|
||||||
export function kcSanitize(html: string): string {
|
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 { describe, it, expect } from "vitest";
|
||||||
import { KcSanitizer } from "keycloakify/lib/kcSanitize/KcSanitizer";
|
import { KcSanitizer } from "keycloakify/lib/kcSanitize/KcSanitizer";
|
||||||
import { decode } from "html-entities";
|
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
|
// 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
|
// 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 {
|
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);
|
expect(result).toBe(expectedResult);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user