keycloakify 10.1.3 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/account/KcContext/kcContextMocks.js +18 -6
- package/account/KcContext/kcContextMocks.js.map +1 -1
- package/account/Template.js +8 -26
- package/account/Template.js.map +1 -1
- package/account/Template.useInitialize.d.ts +12 -0
- package/account/Template.useInitialize.js +20 -0
- package/account/Template.useInitialize.js.map +1 -0
- package/account/i18n/index.d.ts +5 -5
- package/account/i18n/index.js +1 -1
- package/account/i18n/index.js.map +1 -1
- package/account/i18n/messages_defaultSet/types.d.ts +3 -0
- package/account/i18n/messages_defaultSet/types.js +26 -0
- package/account/i18n/messages_defaultSet/types.js.map +1 -0
- package/account/i18n/noJsx/GenericI18n_noJsx.d.ts +63 -0
- package/account/i18n/noJsx/GenericI18n_noJsx.js +2 -0
- package/account/i18n/noJsx/GenericI18n_noJsx.js.map +1 -0
- package/account/i18n/noJsx/getI18n.d.ts +41 -0
- package/account/i18n/noJsx/getI18n.js +207 -0
- package/account/i18n/noJsx/getI18n.js.map +1 -0
- package/account/i18n/noJsx/i18nBuilder.d.ts +18 -0
- package/account/i18n/noJsx/i18nBuilder.js +27 -0
- package/account/i18n/noJsx/i18nBuilder.js.map +1 -0
- package/account/i18n/noJsx/index.d.ts +3 -0
- package/account/i18n/noJsx/index.js +2 -0
- package/account/i18n/noJsx/index.js.map +1 -0
- package/account/i18n/withJsx/GenericI18n.d.ts +72 -0
- package/account/i18n/withJsx/GenericI18n.js +5 -0
- package/account/i18n/withJsx/GenericI18n.js.map +1 -0
- package/account/i18n/withJsx/i18nBuilder.d.ts +18 -0
- package/account/i18n/withJsx/i18nBuilder.js +27 -0
- package/account/i18n/withJsx/i18nBuilder.js.map +1 -0
- package/account/i18n/withJsx/index.d.ts +3 -0
- package/account/i18n/withJsx/index.js +2 -0
- package/account/i18n/withJsx/index.js.map +1 -0
- package/account/i18n/withJsx/useI18n.d.ts +27 -0
- package/account/i18n/{useI18n.js → withJsx/useI18n.js} +6 -4
- package/account/i18n/withJsx/useI18n.js.map +1 -0
- package/account/index.d.ts +1 -1
- package/account/index.js +1 -1
- package/account/index.js.map +1 -1
- package/account/pages/Totp.js +3 -2
- package/account/pages/Totp.js.map +1 -1
- package/bin/246.index.js +191 -191
- package/bin/499.index.js +252 -68
- package/bin/main.js +1 -1
- package/lib/kcSanitize/HtmlPolicyBuilder.d.ts +28 -0
- package/lib/kcSanitize/HtmlPolicyBuilder.js +209 -0
- package/lib/kcSanitize/HtmlPolicyBuilder.js.map +1 -0
- package/lib/kcSanitize/KcSanitizer.d.ts +12 -0
- package/lib/kcSanitize/KcSanitizer.js +46 -0
- package/lib/kcSanitize/KcSanitizer.js.map +1 -0
- package/lib/kcSanitize/KcSanitizerPolicy.d.ts +24 -0
- package/lib/kcSanitize/KcSanitizerPolicy.js +149 -0
- package/lib/kcSanitize/KcSanitizerPolicy.js.map +1 -0
- package/lib/kcSanitize/index.d.ts +1 -0
- package/lib/kcSanitize/index.js +5 -0
- package/lib/kcSanitize/index.js.map +1 -0
- package/login/KcContext/kcContextMocks.js +24 -6
- package/login/KcContext/kcContextMocks.js.map +1 -1
- package/login/Template.js +7 -7
- package/login/Template.js.map +1 -1
- package/login/{Template.useStylesAndScripts.d.ts → Template.useInitialize.d.ts} +1 -4
- package/login/{Template.useStylesAndScripts.js → Template.useInitialize.js} +5 -23
- package/login/Template.useInitialize.js.map +1 -0
- package/login/i18n/index.d.ts +5 -5
- package/login/i18n/index.js +1 -1
- package/login/i18n/index.js.map +1 -1
- package/login/i18n/messages_defaultSet/types.d.ts +3 -0
- package/login/i18n/messages_defaultSet/types.js +33 -0
- package/login/i18n/messages_defaultSet/types.js.map +1 -0
- package/login/i18n/noJsx/GenericI18n_noJsx.d.ts +63 -0
- package/login/i18n/noJsx/GenericI18n_noJsx.js +2 -0
- package/login/i18n/noJsx/GenericI18n_noJsx.js.map +1 -0
- package/login/i18n/noJsx/getI18n.d.ts +41 -0
- package/login/i18n/noJsx/getI18n.js +207 -0
- package/login/i18n/noJsx/getI18n.js.map +1 -0
- package/login/i18n/noJsx/i18nBuilder.d.ts +18 -0
- package/login/i18n/noJsx/i18nBuilder.js +27 -0
- package/login/i18n/noJsx/i18nBuilder.js.map +1 -0
- package/login/i18n/noJsx/index.d.ts +3 -0
- package/login/i18n/noJsx/index.js +2 -0
- package/login/i18n/noJsx/index.js.map +1 -0
- package/login/i18n/withJsx/GenericI18n.d.ts +72 -0
- package/login/i18n/withJsx/GenericI18n.js +5 -0
- package/login/i18n/withJsx/GenericI18n.js.map +1 -0
- package/login/i18n/withJsx/i18nBuilder.d.ts +18 -0
- package/login/i18n/withJsx/i18nBuilder.js +27 -0
- package/login/i18n/withJsx/i18nBuilder.js.map +1 -0
- package/login/i18n/withJsx/index.d.ts +3 -0
- package/login/i18n/withJsx/index.js +2 -0
- package/login/i18n/withJsx/index.js.map +1 -0
- package/login/i18n/withJsx/useI18n.d.ts +27 -0
- package/login/i18n/{useI18n.js → withJsx/useI18n.js} +6 -4
- package/login/i18n/withJsx/useI18n.js.map +1 -0
- package/login/index.d.ts +1 -1
- package/login/index.js +1 -1
- package/login/index.js.map +1 -1
- package/login/lib/useUserProfileForm.d.ts +1 -1
- package/login/lib/useUserProfileForm.js +2 -1
- package/login/lib/useUserProfileForm.js.map +1 -1
- package/login/pages/Error.js +2 -1
- package/login/pages/Error.js.map +1 -1
- package/login/pages/Info.js +4 -3
- package/login/pages/Info.js.map +1 -1
- package/login/pages/Login.js +4 -3
- package/login/pages/Login.js.map +1 -1
- package/login/pages/LoginConfigTotp.js +3 -2
- package/login/pages/LoginConfigTotp.js.map +1 -1
- package/login/pages/LoginOtp.js +2 -1
- package/login/pages/LoginOtp.js.map +1 -1
- package/login/pages/LoginPassword.js +2 -1
- package/login/pages/LoginPassword.js.map +1 -1
- package/login/pages/LoginRecoveryAuthnCodeInput.js +2 -1
- package/login/pages/LoginRecoveryAuthnCodeInput.js.map +1 -1
- package/login/pages/LoginResetPassword.js +2 -1
- package/login/pages/LoginResetPassword.js.map +1 -1
- package/login/pages/LoginUpdatePassword.js +3 -2
- package/login/pages/LoginUpdatePassword.js.map +1 -1
- package/login/pages/Register.js +2 -1
- package/login/pages/Register.js.map +1 -1
- package/package.json +110 -31
- package/src/account/KcContext/kcContextMocks.ts +49 -29
- package/src/account/Template.tsx +11 -32
- package/src/account/Template.useInitialize.ts +35 -0
- package/src/account/i18n/index.ts +5 -5
- package/src/account/i18n/messages_defaultSet/types.ts +30 -0
- package/src/account/i18n/noJsx/GenericI18n_noJsx.ts +64 -0
- package/src/account/i18n/noJsx/getI18n.tsx +341 -0
- package/src/account/i18n/noJsx/i18nBuilder.ts +117 -0
- package/src/account/i18n/noJsx/index.ts +3 -0
- package/src/account/i18n/withJsx/GenericI18n.tsx +81 -0
- package/src/account/i18n/withJsx/i18nBuilder.ts +117 -0
- package/src/account/i18n/withJsx/index.ts +3 -0
- package/src/{login/i18n → account/i18n/withJsx}/useI18n.tsx +43 -11
- package/src/account/index.ts +1 -1
- package/src/account/pages/Totp.tsx +3 -2
- package/src/bin/initialize-account-theme/src/multi-page/i18n.ts +10 -3
- package/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl +15 -0
- package/src/bin/keycloakify/generateResources/generateMessageProperties.ts +371 -121
- package/src/bin/keycloakify/generateResources/generateResources.ts +8 -6
- package/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts +61 -29
- package/src/bin/keycloakify/generateResources/generateResourcesForThemeVariant.ts +15 -9
- package/src/lib/kcSanitize/HtmlPolicyBuilder.ts +252 -0
- package/src/lib/kcSanitize/KcSanitizer.ts +60 -0
- package/src/lib/kcSanitize/KcSanitizerPolicy.ts +294 -0
- package/src/lib/kcSanitize/index.ts +5 -0
- package/src/login/KcContext/kcContextMocks.ts +54 -29
- package/src/login/Template.tsx +11 -17
- package/src/login/{Template.useStylesAndScripts.ts → Template.useInitialize.ts} +5 -29
- package/src/login/i18n/index.ts +5 -5
- package/src/login/i18n/messages_defaultSet/types.ts +37 -0
- package/src/login/i18n/noJsx/GenericI18n_noJsx.ts +64 -0
- package/src/login/i18n/noJsx/getI18n.tsx +341 -0
- package/src/login/i18n/noJsx/i18nBuilder.ts +117 -0
- package/src/login/i18n/noJsx/index.ts +3 -0
- package/src/login/i18n/withJsx/GenericI18n.tsx +81 -0
- package/src/login/i18n/withJsx/i18nBuilder.ts +117 -0
- package/src/login/i18n/withJsx/index.ts +3 -0
- package/src/{account/i18n → login/i18n/withJsx}/useI18n.tsx +43 -11
- package/src/login/index.ts +1 -1
- package/src/login/lib/useUserProfileForm.tsx +3 -2
- package/src/login/pages/Error.tsx +2 -1
- package/src/login/pages/Info.tsx +13 -10
- package/src/login/pages/Login.tsx +4 -3
- package/src/login/pages/LoginConfigTotp.tsx +3 -2
- package/src/login/pages/LoginOtp.tsx +2 -1
- package/src/login/pages/LoginPassword.tsx +2 -1
- package/src/login/pages/LoginRecoveryAuthnCodeInput.tsx +2 -1
- package/src/login/pages/LoginResetPassword.tsx +2 -1
- package/src/login/pages/LoginUpdatePassword.tsx +3 -2
- package/src/login/pages/Register.tsx +2 -1
- package/src/tools/useInsertScriptTags.ts +1 -1
- package/src/tools/vendor/dompurify.ts +3 -0
- package/stories/intro/intro.stories.tsx +0 -1
- package/tools/useInsertScriptTags.d.ts +1 -1
- package/tools/vendor/dompurify.d.ts +2 -0
- package/tools/vendor/dompurify.js +2 -0
- package/account/i18n/GenericI18n.d.ts +0 -6
- package/account/i18n/GenericI18n.js +0 -2
- package/account/i18n/GenericI18n.js.map +0 -1
- package/account/i18n/i18n.d.ts +0 -87
- package/account/i18n/i18n.js +0 -111
- package/account/i18n/i18n.js.map +0 -1
- package/account/i18n/useI18n.d.ts +0 -14
- package/account/i18n/useI18n.js.map +0 -1
- package/login/Template.useStylesAndScripts.js.map +0 -1
- package/login/i18n/GenericI18n.d.ts +0 -6
- package/login/i18n/GenericI18n.js +0 -2
- package/login/i18n/GenericI18n.js.map +0 -1
- package/login/i18n/i18n.d.ts +0 -87
- package/login/i18n/i18n.js +0 -113
- package/login/i18n/i18n.js.map +0 -1
- package/login/i18n/useI18n.d.ts +0 -14
- package/login/i18n/useI18n.js.map +0 -1
- package/src/account/i18n/GenericI18n.tsx +0 -6
- package/src/account/i18n/i18n.tsx +0 -250
- package/src/login/i18n/GenericI18n.tsx +0 -6
- package/src/login/i18n/i18n.tsx +0 -252
@@ -17,7 +17,10 @@ import type { BuildContext } from "../../shared/buildContext";
|
|
17
17
|
import { assert, type Equals } from "tsafe/assert";
|
18
18
|
import { readFieldNameUsage } from "./readFieldNameUsage";
|
19
19
|
import { readExtraPagesNames } from "./readExtraPageNames";
|
20
|
-
import {
|
20
|
+
import {
|
21
|
+
generateMessageProperties,
|
22
|
+
type BuildContextLike as BuildContextLike_generateMessageProperties
|
23
|
+
} from "./generateMessageProperties";
|
21
24
|
import { rmSync } from "../../tools/fs.rmSync";
|
22
25
|
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
|
23
26
|
import {
|
@@ -29,24 +32,30 @@ import { escapeStringForPropertiesFile } from "../../tools/escapeStringForProper
|
|
29
32
|
import * as child_process from "child_process";
|
30
33
|
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
31
34
|
|
32
|
-
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
35
|
+
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
36
|
+
BuildContextLike_generateMessageProperties & {
|
37
|
+
extraThemeProperties: string[] | undefined;
|
38
|
+
projectDirPath: string;
|
39
|
+
projectBuildDirPath: string;
|
40
|
+
environmentVariables: { name: string; default: string }[];
|
41
|
+
implementedThemeTypes: BuildContext["implementedThemeTypes"];
|
42
|
+
themeSrcDirPath: string;
|
43
|
+
bundler: "vite" | "webpack";
|
44
|
+
packageJsonFilePath: string;
|
45
|
+
};
|
42
46
|
|
43
47
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
44
48
|
|
45
49
|
export async function generateResourcesForMainTheme(params: {
|
50
|
+
buildContext: BuildContextLike;
|
46
51
|
themeName: string;
|
47
52
|
resourcesDirPath: string;
|
48
|
-
|
49
|
-
|
53
|
+
}): Promise<{
|
54
|
+
writeMessagePropertiesFilesForThemeVariant: (params: {
|
55
|
+
getMessageDirPath: (params: { themeType: ThemeType }) => string;
|
56
|
+
themeName: string;
|
57
|
+
}) => void;
|
58
|
+
}> {
|
50
59
|
const { themeName, resourcesDirPath, buildContext } = params;
|
51
60
|
|
52
61
|
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
|
@@ -54,6 +63,10 @@ export async function generateResourcesForMainTheme(params: {
|
|
54
63
|
return pathJoin(resourcesDirPath, "theme", themeName, themeType);
|
55
64
|
};
|
56
65
|
|
66
|
+
const writeMessagePropertiesFilesByThemeType: Partial<
|
67
|
+
Record<ThemeType, (params: { messageDirPath: string; themeName: string }) => void>
|
68
|
+
> = {};
|
69
|
+
|
57
70
|
for (const themeType of ["login", "account"] as const) {
|
58
71
|
if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
|
59
72
|
continue;
|
@@ -187,30 +200,27 @@ export async function generateResourcesForMainTheme(params: {
|
|
187
200
|
);
|
188
201
|
});
|
189
202
|
|
203
|
+
let languageTags: string[] | undefined = undefined;
|
204
|
+
|
190
205
|
i18n_messages_generation: {
|
191
206
|
if (isForAccountSpa) {
|
192
207
|
break i18n_messages_generation;
|
193
208
|
}
|
194
209
|
|
195
|
-
generateMessageProperties({
|
196
|
-
|
210
|
+
const wrap = generateMessageProperties({
|
211
|
+
buildContext,
|
197
212
|
themeType
|
198
|
-
})
|
199
|
-
const messagesDirPath = pathJoin(themeTypeDirPath, "messages");
|
213
|
+
});
|
200
214
|
|
201
|
-
|
202
|
-
|
203
|
-
});
|
215
|
+
languageTags = wrap.languageTags;
|
216
|
+
const { writeMessagePropertiesFiles } = wrap;
|
204
217
|
|
205
|
-
|
206
|
-
|
207
|
-
`messages_${languageTag}.properties`
|
208
|
-
);
|
218
|
+
writeMessagePropertiesFilesByThemeType[themeType] =
|
219
|
+
writeMessagePropertiesFiles;
|
209
220
|
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
);
|
221
|
+
writeMessagePropertiesFiles({
|
222
|
+
messageDirPath: pathJoin(themeTypeDirPath, "messages"),
|
223
|
+
themeName
|
214
224
|
});
|
215
225
|
}
|
216
226
|
|
@@ -281,7 +291,10 @@ export async function generateResourcesForMainTheme(params: {
|
|
281
291
|
...buildContext.environmentVariables.map(
|
282
292
|
({ name, default: defaultValue }) =>
|
283
293
|
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
|
284
|
-
)
|
294
|
+
),
|
295
|
+
...(languageTags === undefined
|
296
|
+
? []
|
297
|
+
: [`locales=${languageTags.join(",")}`])
|
285
298
|
].join("\n\n"),
|
286
299
|
"utf8"
|
287
300
|
)
|
@@ -338,4 +351,23 @@ export async function generateResourcesForMainTheme(params: {
|
|
338
351
|
getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
|
339
352
|
});
|
340
353
|
}
|
354
|
+
|
355
|
+
return {
|
356
|
+
writeMessagePropertiesFilesForThemeVariant: ({
|
357
|
+
getMessageDirPath,
|
358
|
+
themeName
|
359
|
+
}) => {
|
360
|
+
objectEntries(writeMessagePropertiesFilesByThemeType).forEach(
|
361
|
+
([themeType, writeMessagePropertiesFiles]) => {
|
362
|
+
if (writeMessagePropertiesFiles === undefined) {
|
363
|
+
return;
|
364
|
+
}
|
365
|
+
writeMessagePropertiesFiles({
|
366
|
+
messageDirPath: getMessageDirPath({ themeType }),
|
367
|
+
themeName
|
368
|
+
});
|
369
|
+
}
|
370
|
+
);
|
371
|
+
}
|
372
|
+
};
|
341
373
|
}
|
@@ -1,27 +1,27 @@
|
|
1
1
|
import { join as pathJoin, extname as pathExtname, sep as pathSep } from "path";
|
2
2
|
import { transformCodebase } from "../../tools/transformCodebase";
|
3
|
-
import type { BuildContext } from "../../shared/buildContext";
|
4
3
|
import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
|
5
4
|
import { assert } from "tsafe/assert";
|
6
|
-
|
7
|
-
export type BuildContextLike = {
|
8
|
-
keycloakifyBuildDirPath: string;
|
9
|
-
};
|
10
|
-
|
11
|
-
assert<BuildContext extends BuildContextLike ? true : false>();
|
5
|
+
import type { ThemeType } from "../../shared/constants";
|
12
6
|
|
13
7
|
export function generateResourcesForThemeVariant(params: {
|
14
8
|
resourcesDirPath: string;
|
15
9
|
themeName: string;
|
16
10
|
themeVariantName: string;
|
11
|
+
writeMessagePropertiesFiles: (params: {
|
12
|
+
getMessageDirPath: (params: { themeType: ThemeType }) => string;
|
13
|
+
themeName: string;
|
14
|
+
}) => void;
|
17
15
|
}) {
|
18
|
-
const { resourcesDirPath, themeName, themeVariantName } =
|
16
|
+
const { resourcesDirPath, themeName, themeVariantName, writeMessagePropertiesFiles } =
|
17
|
+
params;
|
19
18
|
|
20
19
|
const mainThemeDirPath = pathJoin(resourcesDirPath, "theme", themeName);
|
20
|
+
const themeVariantDirPath = pathJoin(mainThemeDirPath, "..", themeVariantName);
|
21
21
|
|
22
22
|
transformCodebase({
|
23
23
|
srcDirPath: mainThemeDirPath,
|
24
|
-
destDirPath:
|
24
|
+
destDirPath: themeVariantDirPath,
|
25
25
|
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
26
26
|
if (
|
27
27
|
pathExtname(fileRelativePath) === ".ftl" &&
|
@@ -67,4 +67,10 @@ export function generateResourcesForThemeVariant(params: {
|
|
67
67
|
return newMetaInfKeycloakTheme;
|
68
68
|
}
|
69
69
|
});
|
70
|
+
|
71
|
+
writeMessagePropertiesFiles({
|
72
|
+
getMessageDirPath: ({ themeType }) =>
|
73
|
+
pathJoin(themeVariantDirPath, themeType, "messages"),
|
74
|
+
themeName: themeVariantName
|
75
|
+
});
|
70
76
|
}
|
@@ -0,0 +1,252 @@
|
|
1
|
+
import { DOMPurify } from "keycloakify/tools/vendor/dompurify";
|
2
|
+
|
3
|
+
type TagType = {
|
4
|
+
name: string;
|
5
|
+
attributes: AttributeType[];
|
6
|
+
};
|
7
|
+
type AttributeType = {
|
8
|
+
name: string;
|
9
|
+
matchRegex?: RegExp;
|
10
|
+
matchFunction?: (value: string) => boolean;
|
11
|
+
};
|
12
|
+
|
13
|
+
// implementation for org.owasp.html.HtmlPolicyBuilder
|
14
|
+
// https://www.javadoc.io/static/com.googlecode.owasp-java-html-sanitizer/owasp-java-html-sanitizer/20160628.1/index.html?org/owasp/html/HtmlPolicyBuilder.html
|
15
|
+
// It supports the methods that KCSanitizerPolicy needs and nothing more
|
16
|
+
|
17
|
+
export class HtmlPolicyBuilder {
|
18
|
+
private globalAttributesAllowed: Set<AttributeType> = new Set();
|
19
|
+
private tagsAllowed: Map<string, TagType> = new Map();
|
20
|
+
private tagsAllowedWithNoAttribute: Set<string> = new Set();
|
21
|
+
private currentAttribute: AttributeType | null = null;
|
22
|
+
private isStylingAllowed: boolean = false;
|
23
|
+
private allowedProtocols: Set<string> = new Set();
|
24
|
+
private enforceRelNofollow: boolean = false;
|
25
|
+
private DOMPurify: typeof DOMPurify;
|
26
|
+
|
27
|
+
// add a constructor
|
28
|
+
constructor(
|
29
|
+
dependencyInjections: Partial<{
|
30
|
+
DOMPurify: typeof DOMPurify;
|
31
|
+
}>
|
32
|
+
) {
|
33
|
+
this.DOMPurify = dependencyInjections.DOMPurify ?? DOMPurify;
|
34
|
+
}
|
35
|
+
|
36
|
+
allowWithoutAttributes(tag: string): this {
|
37
|
+
this.tagsAllowedWithNoAttribute.add(tag);
|
38
|
+
return this;
|
39
|
+
}
|
40
|
+
|
41
|
+
// Adds the attributes for validation
|
42
|
+
allowAttributes(...args: string[]): this {
|
43
|
+
if (args.length) {
|
44
|
+
const attr = args[0];
|
45
|
+
this.currentAttribute = { name: attr }; // Default regex, will be set later
|
46
|
+
}
|
47
|
+
return this;
|
48
|
+
}
|
49
|
+
|
50
|
+
// Matching regex for value of allowed attributes
|
51
|
+
matching(matchingPattern: RegExp | ((value: string) => boolean)): this {
|
52
|
+
if (this.currentAttribute) {
|
53
|
+
if (matchingPattern instanceof RegExp) {
|
54
|
+
this.currentAttribute.matchRegex = matchingPattern;
|
55
|
+
} else {
|
56
|
+
this.currentAttribute.matchFunction = matchingPattern;
|
57
|
+
}
|
58
|
+
}
|
59
|
+
return this;
|
60
|
+
}
|
61
|
+
|
62
|
+
// Make attributes in prev call global
|
63
|
+
globally(): this {
|
64
|
+
if (this.currentAttribute) {
|
65
|
+
this.currentAttribute.matchRegex = /.*/;
|
66
|
+
this.globalAttributesAllowed.add(this.currentAttribute);
|
67
|
+
this.currentAttribute = null; // Reset after global application
|
68
|
+
}
|
69
|
+
return this;
|
70
|
+
}
|
71
|
+
|
72
|
+
// Allow styling globally
|
73
|
+
allowStyling(): this {
|
74
|
+
this.isStylingAllowed = true;
|
75
|
+
return this;
|
76
|
+
}
|
77
|
+
|
78
|
+
// Save attributes for specific tag
|
79
|
+
onElements(...tags: string[]): this {
|
80
|
+
if (this.currentAttribute) {
|
81
|
+
tags.forEach(tag => {
|
82
|
+
const element = this.tagsAllowed.get(tag) || {
|
83
|
+
name: tag,
|
84
|
+
attributes: []
|
85
|
+
};
|
86
|
+
element.attributes.push(this.currentAttribute!);
|
87
|
+
this.tagsAllowed.set(tag, element);
|
88
|
+
});
|
89
|
+
this.currentAttribute = null; // Reset after applying to elements
|
90
|
+
}
|
91
|
+
return this;
|
92
|
+
}
|
93
|
+
|
94
|
+
// Make specific tag allowed
|
95
|
+
allowElements(...tags: string[]): this {
|
96
|
+
tags.forEach(tag => {
|
97
|
+
if (!this.tagsAllowed.has(tag)) {
|
98
|
+
this.tagsAllowed.set(tag, { name: tag, attributes: [] });
|
99
|
+
}
|
100
|
+
});
|
101
|
+
return this;
|
102
|
+
}
|
103
|
+
|
104
|
+
// Handle rel=nofollow on links
|
105
|
+
requireRelNofollowOnLinks(): this {
|
106
|
+
this.enforceRelNofollow = true;
|
107
|
+
return this;
|
108
|
+
}
|
109
|
+
|
110
|
+
// Allow standard URL protocols (could include further implementation)
|
111
|
+
allowStandardUrlProtocols(): this {
|
112
|
+
this.allowedProtocols.add("http");
|
113
|
+
this.allowedProtocols.add("https");
|
114
|
+
this.allowedProtocols.add("mailto");
|
115
|
+
return this;
|
116
|
+
}
|
117
|
+
|
118
|
+
apply(html: string): string {
|
119
|
+
//Clear all previous configs first ( in case we used DOMPurify somewhere else )
|
120
|
+
this.DOMPurify.clearConfig();
|
121
|
+
this.DOMPurify.removeAllHooks();
|
122
|
+
this.setupHooks();
|
123
|
+
return this.DOMPurify.sanitize(html, {
|
124
|
+
ALLOWED_TAGS: Array.from(this.tagsAllowed.keys()),
|
125
|
+
ALLOWED_ATTR: this.getAllowedAttributes(),
|
126
|
+
ALLOWED_URI_REGEXP: this.getAllowedUriRegexp(),
|
127
|
+
ADD_TAGS: this.isStylingAllowed ? ["style"] : [],
|
128
|
+
ADD_ATTR: this.isStylingAllowed ? ["style"] : []
|
129
|
+
});
|
130
|
+
}
|
131
|
+
|
132
|
+
private setupHooks(): void {
|
133
|
+
// Check allowed attribute and global attributes and it doesnt exist in them remove it
|
134
|
+
this.DOMPurify.addHook("uponSanitizeAttribute", (currentNode, hookEvent) => {
|
135
|
+
if (!hookEvent) return;
|
136
|
+
|
137
|
+
const tagName = currentNode.tagName.toLowerCase();
|
138
|
+
const allowedAttributes = this.tagsAllowed.get(tagName)?.attributes || [];
|
139
|
+
|
140
|
+
//Add global attributes to allowed attributes
|
141
|
+
this.globalAttributesAllowed.forEach(attribute => {
|
142
|
+
allowedAttributes.push(attribute);
|
143
|
+
});
|
144
|
+
|
145
|
+
//Add style attribute to allowed attributes
|
146
|
+
if (this.isStylingAllowed) {
|
147
|
+
let styleAttribute: AttributeType = { name: "style", matchRegex: /.*/ };
|
148
|
+
allowedAttributes.push(styleAttribute);
|
149
|
+
}
|
150
|
+
|
151
|
+
// Check if the attribute is allowed
|
152
|
+
if (!allowedAttributes.some(attr => attr.name === hookEvent.attrName)) {
|
153
|
+
hookEvent.forceKeepAttr = false;
|
154
|
+
hookEvent.keepAttr = false;
|
155
|
+
currentNode.removeAttribute(hookEvent.attrName);
|
156
|
+
return;
|
157
|
+
} else {
|
158
|
+
const attributeType = allowedAttributes.find(
|
159
|
+
attr => attr.name === hookEvent.attrName
|
160
|
+
);
|
161
|
+
if (attributeType) {
|
162
|
+
//Check if attribute value is allowed
|
163
|
+
if (
|
164
|
+
attributeType.matchRegex &&
|
165
|
+
!attributeType.matchRegex.test(hookEvent.attrValue)
|
166
|
+
) {
|
167
|
+
hookEvent.forceKeepAttr = false;
|
168
|
+
hookEvent.keepAttr = false;
|
169
|
+
currentNode.removeAttribute(hookEvent.attrName);
|
170
|
+
return;
|
171
|
+
}
|
172
|
+
if (
|
173
|
+
attributeType.matchFunction &&
|
174
|
+
!attributeType.matchFunction(hookEvent.attrValue)
|
175
|
+
) {
|
176
|
+
hookEvent.forceKeepAttr = false;
|
177
|
+
hookEvent.keepAttr = false;
|
178
|
+
currentNode.removeAttribute(hookEvent.attrName);
|
179
|
+
return;
|
180
|
+
}
|
181
|
+
}
|
182
|
+
}
|
183
|
+
// both attribute and value already checked so they should be ok
|
184
|
+
// set forceKeep to true to make sure next hooks won't delete them
|
185
|
+
// except for href that we will check later
|
186
|
+
if (hookEvent.attrName !== "href") {
|
187
|
+
hookEvent.keepAttr = true;
|
188
|
+
hookEvent.forceKeepAttr = true;
|
189
|
+
}
|
190
|
+
});
|
191
|
+
|
192
|
+
this.DOMPurify.addHook("afterSanitizeAttributes", currentNode => {
|
193
|
+
// if tag is not allowed to have no attribute then remove it completely
|
194
|
+
if (
|
195
|
+
currentNode.attributes.length == 0 &&
|
196
|
+
currentNode.childNodes.length == 0
|
197
|
+
) {
|
198
|
+
if (!this.tagsAllowedWithNoAttribute.has(currentNode.tagName)) {
|
199
|
+
currentNode.remove();
|
200
|
+
}
|
201
|
+
} else {
|
202
|
+
//in case of <a> or <img> if we have no attribute we need to remove them even if they have child
|
203
|
+
if (currentNode.tagName === "A" || currentNode.tagName === "IMG") {
|
204
|
+
if (currentNode.attributes.length == 0) {
|
205
|
+
//add currentNode children to parent node
|
206
|
+
while (currentNode.firstChild) {
|
207
|
+
currentNode?.parentNode?.insertBefore(
|
208
|
+
currentNode.firstChild,
|
209
|
+
currentNode
|
210
|
+
);
|
211
|
+
}
|
212
|
+
// Remove the currentNode itself
|
213
|
+
currentNode.remove();
|
214
|
+
}
|
215
|
+
}
|
216
|
+
//
|
217
|
+
if (currentNode.tagName === "A") {
|
218
|
+
if (this.enforceRelNofollow) {
|
219
|
+
if (!currentNode.hasAttribute("rel")) {
|
220
|
+
currentNode.setAttribute("rel", "nofollow");
|
221
|
+
} else if (
|
222
|
+
!currentNode.getAttribute("rel")?.includes("nofollow")
|
223
|
+
) {
|
224
|
+
currentNode.setAttribute(
|
225
|
+
"rel",
|
226
|
+
currentNode.getAttribute("rel") + " nofollow"
|
227
|
+
);
|
228
|
+
}
|
229
|
+
}
|
230
|
+
}
|
231
|
+
}
|
232
|
+
});
|
233
|
+
}
|
234
|
+
|
235
|
+
private getAllowedAttributes(): string[] {
|
236
|
+
const allowedAttributes: Set<string> = new Set();
|
237
|
+
this.tagsAllowed.forEach(element => {
|
238
|
+
element.attributes.forEach(attribute => {
|
239
|
+
allowedAttributes.add(attribute.name);
|
240
|
+
});
|
241
|
+
});
|
242
|
+
this.globalAttributesAllowed.forEach(attribute => {
|
243
|
+
allowedAttributes.add(attribute.name);
|
244
|
+
});
|
245
|
+
return Array.from(allowedAttributes);
|
246
|
+
}
|
247
|
+
|
248
|
+
private getAllowedUriRegexp(): RegExp {
|
249
|
+
const protocols = Array.from(this.allowedProtocols).join("|");
|
250
|
+
return new RegExp(`^(?:${protocols})://`, "i");
|
251
|
+
}
|
252
|
+
}
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import { KcSanitizerPolicy } from "./KcSanitizerPolicy";
|
2
|
+
import type { DOMPurify as ofTypeDomPurify } from "keycloakify/tools/vendor/dompurify";
|
3
|
+
|
4
|
+
// implementation of keycloak java sanitize method ( KeycloakSanitizerMethod )
|
5
|
+
// https://github.com/keycloak/keycloak/blob/8ce8a4ba089eef25a0e01f58e09890399477b9ef/services/src/main/java/org/keycloak/theme/KeycloakSanitizerMethod.java#L33
|
6
|
+
export class KcSanitizer {
|
7
|
+
private static HREF_PATTERN = /\s+href="([^"]*)"/g;
|
8
|
+
private static textarea: HTMLTextAreaElement | null = null;
|
9
|
+
|
10
|
+
public static sanitize(
|
11
|
+
html: string,
|
12
|
+
dependencyInjections: Partial<{
|
13
|
+
DOMPurify: typeof ofTypeDomPurify;
|
14
|
+
htmlEntitiesDecode: (html: string) => string;
|
15
|
+
}>
|
16
|
+
): string {
|
17
|
+
if (html === "") return "";
|
18
|
+
|
19
|
+
html =
|
20
|
+
dependencyInjections?.htmlEntitiesDecode !== undefined
|
21
|
+
? dependencyInjections.htmlEntitiesDecode(html)
|
22
|
+
: this.decodeHtml(html);
|
23
|
+
const sanitized = KcSanitizerPolicy.sanitize(html, dependencyInjections);
|
24
|
+
return this.fixURLs(sanitized);
|
25
|
+
}
|
26
|
+
|
27
|
+
private static decodeHtml(html: string): string {
|
28
|
+
if (!KcSanitizer.textarea) {
|
29
|
+
KcSanitizer.textarea = document.createElement("textarea");
|
30
|
+
}
|
31
|
+
KcSanitizer.textarea.innerHTML = html;
|
32
|
+
return KcSanitizer.textarea.value;
|
33
|
+
}
|
34
|
+
|
35
|
+
// This will remove unwanted characters from url
|
36
|
+
private static fixURLs(msg: string): string {
|
37
|
+
const HREF_PATTERN = this.HREF_PATTERN;
|
38
|
+
const result = [];
|
39
|
+
let last = 0;
|
40
|
+
let match: RegExpExecArray | null;
|
41
|
+
|
42
|
+
do {
|
43
|
+
match = HREF_PATTERN.exec(msg);
|
44
|
+
if (match) {
|
45
|
+
const href = match[0]
|
46
|
+
.replace(/=/g, "=")
|
47
|
+
.replace(/\.\./g, ".")
|
48
|
+
.replace(/&/g, "&");
|
49
|
+
|
50
|
+
result.push(msg.substring(last, match.index!));
|
51
|
+
result.push(href);
|
52
|
+
|
53
|
+
last = HREF_PATTERN.lastIndex;
|
54
|
+
}
|
55
|
+
} while (match);
|
56
|
+
|
57
|
+
result.push(msg.substring(last));
|
58
|
+
return result.join("");
|
59
|
+
}
|
60
|
+
}
|