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.
Files changed (198) hide show
  1. package/account/KcContext/kcContextMocks.js +18 -6
  2. package/account/KcContext/kcContextMocks.js.map +1 -1
  3. package/account/Template.js +8 -26
  4. package/account/Template.js.map +1 -1
  5. package/account/Template.useInitialize.d.ts +12 -0
  6. package/account/Template.useInitialize.js +20 -0
  7. package/account/Template.useInitialize.js.map +1 -0
  8. package/account/i18n/index.d.ts +5 -5
  9. package/account/i18n/index.js +1 -1
  10. package/account/i18n/index.js.map +1 -1
  11. package/account/i18n/messages_defaultSet/types.d.ts +3 -0
  12. package/account/i18n/messages_defaultSet/types.js +26 -0
  13. package/account/i18n/messages_defaultSet/types.js.map +1 -0
  14. package/account/i18n/noJsx/GenericI18n_noJsx.d.ts +63 -0
  15. package/account/i18n/noJsx/GenericI18n_noJsx.js +2 -0
  16. package/account/i18n/noJsx/GenericI18n_noJsx.js.map +1 -0
  17. package/account/i18n/noJsx/getI18n.d.ts +41 -0
  18. package/account/i18n/noJsx/getI18n.js +207 -0
  19. package/account/i18n/noJsx/getI18n.js.map +1 -0
  20. package/account/i18n/noJsx/i18nBuilder.d.ts +18 -0
  21. package/account/i18n/noJsx/i18nBuilder.js +27 -0
  22. package/account/i18n/noJsx/i18nBuilder.js.map +1 -0
  23. package/account/i18n/noJsx/index.d.ts +3 -0
  24. package/account/i18n/noJsx/index.js +2 -0
  25. package/account/i18n/noJsx/index.js.map +1 -0
  26. package/account/i18n/withJsx/GenericI18n.d.ts +72 -0
  27. package/account/i18n/withJsx/GenericI18n.js +5 -0
  28. package/account/i18n/withJsx/GenericI18n.js.map +1 -0
  29. package/account/i18n/withJsx/i18nBuilder.d.ts +18 -0
  30. package/account/i18n/withJsx/i18nBuilder.js +27 -0
  31. package/account/i18n/withJsx/i18nBuilder.js.map +1 -0
  32. package/account/i18n/withJsx/index.d.ts +3 -0
  33. package/account/i18n/withJsx/index.js +2 -0
  34. package/account/i18n/withJsx/index.js.map +1 -0
  35. package/account/i18n/withJsx/useI18n.d.ts +27 -0
  36. package/account/i18n/{useI18n.js → withJsx/useI18n.js} +6 -4
  37. package/account/i18n/withJsx/useI18n.js.map +1 -0
  38. package/account/index.d.ts +1 -1
  39. package/account/index.js +1 -1
  40. package/account/index.js.map +1 -1
  41. package/account/pages/Totp.js +3 -2
  42. package/account/pages/Totp.js.map +1 -1
  43. package/bin/246.index.js +191 -191
  44. package/bin/499.index.js +252 -68
  45. package/bin/main.js +1 -1
  46. package/lib/kcSanitize/HtmlPolicyBuilder.d.ts +28 -0
  47. package/lib/kcSanitize/HtmlPolicyBuilder.js +209 -0
  48. package/lib/kcSanitize/HtmlPolicyBuilder.js.map +1 -0
  49. package/lib/kcSanitize/KcSanitizer.d.ts +12 -0
  50. package/lib/kcSanitize/KcSanitizer.js +46 -0
  51. package/lib/kcSanitize/KcSanitizer.js.map +1 -0
  52. package/lib/kcSanitize/KcSanitizerPolicy.d.ts +24 -0
  53. package/lib/kcSanitize/KcSanitizerPolicy.js +149 -0
  54. package/lib/kcSanitize/KcSanitizerPolicy.js.map +1 -0
  55. package/lib/kcSanitize/index.d.ts +1 -0
  56. package/lib/kcSanitize/index.js +5 -0
  57. package/lib/kcSanitize/index.js.map +1 -0
  58. package/login/KcContext/kcContextMocks.js +24 -6
  59. package/login/KcContext/kcContextMocks.js.map +1 -1
  60. package/login/Template.js +7 -7
  61. package/login/Template.js.map +1 -1
  62. package/login/{Template.useStylesAndScripts.d.ts → Template.useInitialize.d.ts} +1 -4
  63. package/login/{Template.useStylesAndScripts.js → Template.useInitialize.js} +5 -23
  64. package/login/Template.useInitialize.js.map +1 -0
  65. package/login/i18n/index.d.ts +5 -5
  66. package/login/i18n/index.js +1 -1
  67. package/login/i18n/index.js.map +1 -1
  68. package/login/i18n/messages_defaultSet/types.d.ts +3 -0
  69. package/login/i18n/messages_defaultSet/types.js +33 -0
  70. package/login/i18n/messages_defaultSet/types.js.map +1 -0
  71. package/login/i18n/noJsx/GenericI18n_noJsx.d.ts +63 -0
  72. package/login/i18n/noJsx/GenericI18n_noJsx.js +2 -0
  73. package/login/i18n/noJsx/GenericI18n_noJsx.js.map +1 -0
  74. package/login/i18n/noJsx/getI18n.d.ts +41 -0
  75. package/login/i18n/noJsx/getI18n.js +207 -0
  76. package/login/i18n/noJsx/getI18n.js.map +1 -0
  77. package/login/i18n/noJsx/i18nBuilder.d.ts +18 -0
  78. package/login/i18n/noJsx/i18nBuilder.js +27 -0
  79. package/login/i18n/noJsx/i18nBuilder.js.map +1 -0
  80. package/login/i18n/noJsx/index.d.ts +3 -0
  81. package/login/i18n/noJsx/index.js +2 -0
  82. package/login/i18n/noJsx/index.js.map +1 -0
  83. package/login/i18n/withJsx/GenericI18n.d.ts +72 -0
  84. package/login/i18n/withJsx/GenericI18n.js +5 -0
  85. package/login/i18n/withJsx/GenericI18n.js.map +1 -0
  86. package/login/i18n/withJsx/i18nBuilder.d.ts +18 -0
  87. package/login/i18n/withJsx/i18nBuilder.js +27 -0
  88. package/login/i18n/withJsx/i18nBuilder.js.map +1 -0
  89. package/login/i18n/withJsx/index.d.ts +3 -0
  90. package/login/i18n/withJsx/index.js +2 -0
  91. package/login/i18n/withJsx/index.js.map +1 -0
  92. package/login/i18n/withJsx/useI18n.d.ts +27 -0
  93. package/login/i18n/{useI18n.js → withJsx/useI18n.js} +6 -4
  94. package/login/i18n/withJsx/useI18n.js.map +1 -0
  95. package/login/index.d.ts +1 -1
  96. package/login/index.js +1 -1
  97. package/login/index.js.map +1 -1
  98. package/login/lib/useUserProfileForm.d.ts +1 -1
  99. package/login/lib/useUserProfileForm.js +2 -1
  100. package/login/lib/useUserProfileForm.js.map +1 -1
  101. package/login/pages/Error.js +2 -1
  102. package/login/pages/Error.js.map +1 -1
  103. package/login/pages/Info.js +4 -3
  104. package/login/pages/Info.js.map +1 -1
  105. package/login/pages/Login.js +4 -3
  106. package/login/pages/Login.js.map +1 -1
  107. package/login/pages/LoginConfigTotp.js +3 -2
  108. package/login/pages/LoginConfigTotp.js.map +1 -1
  109. package/login/pages/LoginOtp.js +2 -1
  110. package/login/pages/LoginOtp.js.map +1 -1
  111. package/login/pages/LoginPassword.js +2 -1
  112. package/login/pages/LoginPassword.js.map +1 -1
  113. package/login/pages/LoginRecoveryAuthnCodeInput.js +2 -1
  114. package/login/pages/LoginRecoveryAuthnCodeInput.js.map +1 -1
  115. package/login/pages/LoginResetPassword.js +2 -1
  116. package/login/pages/LoginResetPassword.js.map +1 -1
  117. package/login/pages/LoginUpdatePassword.js +3 -2
  118. package/login/pages/LoginUpdatePassword.js.map +1 -1
  119. package/login/pages/Register.js +2 -1
  120. package/login/pages/Register.js.map +1 -1
  121. package/package.json +110 -31
  122. package/src/account/KcContext/kcContextMocks.ts +49 -29
  123. package/src/account/Template.tsx +11 -32
  124. package/src/account/Template.useInitialize.ts +35 -0
  125. package/src/account/i18n/index.ts +5 -5
  126. package/src/account/i18n/messages_defaultSet/types.ts +30 -0
  127. package/src/account/i18n/noJsx/GenericI18n_noJsx.ts +64 -0
  128. package/src/account/i18n/noJsx/getI18n.tsx +341 -0
  129. package/src/account/i18n/noJsx/i18nBuilder.ts +117 -0
  130. package/src/account/i18n/noJsx/index.ts +3 -0
  131. package/src/account/i18n/withJsx/GenericI18n.tsx +81 -0
  132. package/src/account/i18n/withJsx/i18nBuilder.ts +117 -0
  133. package/src/account/i18n/withJsx/index.ts +3 -0
  134. package/src/{login/i18n → account/i18n/withJsx}/useI18n.tsx +43 -11
  135. package/src/account/index.ts +1 -1
  136. package/src/account/pages/Totp.tsx +3 -2
  137. package/src/bin/initialize-account-theme/src/multi-page/i18n.ts +10 -3
  138. package/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl +15 -0
  139. package/src/bin/keycloakify/generateResources/generateMessageProperties.ts +371 -121
  140. package/src/bin/keycloakify/generateResources/generateResources.ts +8 -6
  141. package/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts +61 -29
  142. package/src/bin/keycloakify/generateResources/generateResourcesForThemeVariant.ts +15 -9
  143. package/src/lib/kcSanitize/HtmlPolicyBuilder.ts +252 -0
  144. package/src/lib/kcSanitize/KcSanitizer.ts +60 -0
  145. package/src/lib/kcSanitize/KcSanitizerPolicy.ts +294 -0
  146. package/src/lib/kcSanitize/index.ts +5 -0
  147. package/src/login/KcContext/kcContextMocks.ts +54 -29
  148. package/src/login/Template.tsx +11 -17
  149. package/src/login/{Template.useStylesAndScripts.ts → Template.useInitialize.ts} +5 -29
  150. package/src/login/i18n/index.ts +5 -5
  151. package/src/login/i18n/messages_defaultSet/types.ts +37 -0
  152. package/src/login/i18n/noJsx/GenericI18n_noJsx.ts +64 -0
  153. package/src/login/i18n/noJsx/getI18n.tsx +341 -0
  154. package/src/login/i18n/noJsx/i18nBuilder.ts +117 -0
  155. package/src/login/i18n/noJsx/index.ts +3 -0
  156. package/src/login/i18n/withJsx/GenericI18n.tsx +81 -0
  157. package/src/login/i18n/withJsx/i18nBuilder.ts +117 -0
  158. package/src/login/i18n/withJsx/index.ts +3 -0
  159. package/src/{account/i18n → login/i18n/withJsx}/useI18n.tsx +43 -11
  160. package/src/login/index.ts +1 -1
  161. package/src/login/lib/useUserProfileForm.tsx +3 -2
  162. package/src/login/pages/Error.tsx +2 -1
  163. package/src/login/pages/Info.tsx +13 -10
  164. package/src/login/pages/Login.tsx +4 -3
  165. package/src/login/pages/LoginConfigTotp.tsx +3 -2
  166. package/src/login/pages/LoginOtp.tsx +2 -1
  167. package/src/login/pages/LoginPassword.tsx +2 -1
  168. package/src/login/pages/LoginRecoveryAuthnCodeInput.tsx +2 -1
  169. package/src/login/pages/LoginResetPassword.tsx +2 -1
  170. package/src/login/pages/LoginUpdatePassword.tsx +3 -2
  171. package/src/login/pages/Register.tsx +2 -1
  172. package/src/tools/useInsertScriptTags.ts +1 -1
  173. package/src/tools/vendor/dompurify.ts +3 -0
  174. package/stories/intro/intro.stories.tsx +0 -1
  175. package/tools/useInsertScriptTags.d.ts +1 -1
  176. package/tools/vendor/dompurify.d.ts +2 -0
  177. package/tools/vendor/dompurify.js +2 -0
  178. package/account/i18n/GenericI18n.d.ts +0 -6
  179. package/account/i18n/GenericI18n.js +0 -2
  180. package/account/i18n/GenericI18n.js.map +0 -1
  181. package/account/i18n/i18n.d.ts +0 -87
  182. package/account/i18n/i18n.js +0 -111
  183. package/account/i18n/i18n.js.map +0 -1
  184. package/account/i18n/useI18n.d.ts +0 -14
  185. package/account/i18n/useI18n.js.map +0 -1
  186. package/login/Template.useStylesAndScripts.js.map +0 -1
  187. package/login/i18n/GenericI18n.d.ts +0 -6
  188. package/login/i18n/GenericI18n.js +0 -2
  189. package/login/i18n/GenericI18n.js.map +0 -1
  190. package/login/i18n/i18n.d.ts +0 -87
  191. package/login/i18n/i18n.js +0 -113
  192. package/login/i18n/i18n.js.map +0 -1
  193. package/login/i18n/useI18n.d.ts +0 -14
  194. package/login/i18n/useI18n.js.map +0 -1
  195. package/src/account/i18n/GenericI18n.tsx +0 -6
  196. package/src/account/i18n/i18n.tsx +0 -250
  197. package/src/login/i18n/GenericI18n.tsx +0 -6
  198. 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 { generateMessageProperties } from "./generateMessageProperties";
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
- extraThemeProperties: string[] | undefined;
34
- projectDirPath: string;
35
- projectBuildDirPath: string;
36
- environmentVariables: { name: string; default: string }[];
37
- implementedThemeTypes: BuildContext["implementedThemeTypes"];
38
- themeSrcDirPath: string;
39
- bundler: "vite" | "webpack";
40
- packageJsonFilePath: string;
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
- buildContext: BuildContextLike;
49
- }): Promise<void> {
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
- themeSrcDirPath: buildContext.themeSrcDirPath,
210
+ const wrap = generateMessageProperties({
211
+ buildContext,
197
212
  themeType
198
- }).forEach(({ languageTag, propertiesFileSource }) => {
199
- const messagesDirPath = pathJoin(themeTypeDirPath, "messages");
213
+ });
200
214
 
201
- fs.mkdirSync(pathJoin(themeTypeDirPath, "messages"), {
202
- recursive: true
203
- });
215
+ languageTags = wrap.languageTags;
216
+ const { writeMessagePropertiesFiles } = wrap;
204
217
 
205
- const propertiesFilePath = pathJoin(
206
- messagesDirPath,
207
- `messages_${languageTag}.properties`
208
- );
218
+ writeMessagePropertiesFilesByThemeType[themeType] =
219
+ writeMessagePropertiesFiles;
209
220
 
210
- fs.writeFileSync(
211
- propertiesFilePath,
212
- Buffer.from(propertiesFileSource, "utf8")
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 } = params;
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: pathJoin(mainThemeDirPath, "..", themeVariantName),
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(/&#61;/g, "=")
47
+ .replace(/\.\./g, ".")
48
+ .replace(/&amp;/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
+ }