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
@@ -0,0 +1,294 @@
1
+ import { HtmlPolicyBuilder } from "./HtmlPolicyBuilder";
2
+ import type { DOMPurify as ofTypeDomPurify } from "keycloakify/tools/vendor/dompurify";
3
+
4
+ //implementation of java Sanitizer policy ( KeycloakSanitizerPolicy )
5
+ // All regex directly copied from the keycloak source but some of them changed slightly to work with typescript(ONSITE_URL and OFFSITE_URL)
6
+ // Also replaced ?i with "i" tag as second parameter of RegExp
7
+ //https://github.com/keycloak/keycloak/blob/8ce8a4ba089eef25a0e01f58e09890399477b9ef/services/src/main/java/org/keycloak/theme/KeycloakSanitizerPolicy.java#L29
8
+ export class KcSanitizerPolicy {
9
+ public static readonly COLOR_NAME = new RegExp(
10
+ "(?:aqua|black|blue|fuchsia|gray|grey|green|lime|maroon|navy|olive|purple|red|silver|teal|white|yellow)"
11
+ );
12
+
13
+ public static readonly COLOR_CODE = new RegExp(
14
+ "(?:#(?:[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?))"
15
+ );
16
+
17
+ public static readonly NUMBER_OR_PERCENT = new RegExp("[0-9]+%?");
18
+
19
+ public static readonly PARAGRAPH = new RegExp(
20
+ "(?:[\\p{L}\\p{N},'\\.\\s\\-_\\(\\)]|&[0-9]{2};)*",
21
+ "u" // Unicode flag for \p{L} and \p{N} in the pattern
22
+ );
23
+
24
+ public static readonly HTML_ID = new RegExp("[a-zA-Z0-9\\:\\-_\\.]+");
25
+
26
+ public static readonly HTML_TITLE = new RegExp(
27
+ "[\\p{L}\\p{N}\\s\\-_',:\\[\\]!\\./\\\\\\(\\)&]*",
28
+ "u" // Unicode flag for \p{L} and \p{N} in the pattern
29
+ );
30
+
31
+ public static readonly HTML_CLASS = new RegExp("[a-zA-Z0-9\\s,\\-_]+");
32
+
33
+ public static readonly ONSITE_URL = new RegExp(
34
+ "(?:[\\p{L}\\p{N}.#@\\$%+&;\\-_~,?=/!]+|#(\\w)+)",
35
+ "u" // Unicode flag for \p{L} and \p{N} in the pattern
36
+ );
37
+
38
+ public static readonly OFFSITE_URL = new RegExp(
39
+ "\\s*(?:(?:ht|f)tps?://|mailto:)[\\p{L}\\p{N}]+" +
40
+ "[\\p{L}\\p{N}\\p{Zs}.#@\\$%+&;:\\-_~,?=/!()]*\\s*",
41
+ "u" // Unicode flag for \p{L} and \p{N} in the pattern
42
+ );
43
+
44
+ public static readonly NUMBER = new RegExp(
45
+ "[+-]?(?:(?:[0-9]+(?:\\.[0-9]*)?)|\\.[0-9]+)"
46
+ );
47
+ public static readonly NAME = new RegExp("[a-zA-Z0-9\\-_\\$]+");
48
+
49
+ public static readonly ALIGN = new RegExp(
50
+ "\\b(center|left|right|justify|char)\\b",
51
+ "i" // Case-insensitive flag
52
+ );
53
+
54
+ public static readonly VALIGN = new RegExp(
55
+ "\\b(baseline|bottom|middle|top)\\b",
56
+ "i" // Case-insensitive flag
57
+ );
58
+
59
+ public static readonly HISTORY_BACK = new RegExp(
60
+ "(?:javascript:)?\\Qhistory.go(-1)\\E"
61
+ );
62
+
63
+ public static readonly ONE_CHAR = new RegExp(
64
+ ".?",
65
+ "s" // Dotall flag for . to match newlines
66
+ );
67
+
68
+ private static COLOR_NAME_OR_COLOR_CODE(s: string): boolean {
69
+ return (
70
+ KcSanitizerPolicy.COLOR_NAME.test(s) || KcSanitizerPolicy.COLOR_CODE.test(s)
71
+ );
72
+ }
73
+
74
+ private static ONSITE_OR_OFFSITE_URL(s: string): boolean {
75
+ return (
76
+ KcSanitizerPolicy.ONSITE_URL.test(s) || KcSanitizerPolicy.OFFSITE_URL.test(s)
77
+ );
78
+ }
79
+
80
+ public static sanitize(
81
+ html: string,
82
+ dependencyInjections: Partial<{
83
+ DOMPurify: typeof ofTypeDomPurify;
84
+ }>
85
+ ): string {
86
+ return new HtmlPolicyBuilder(dependencyInjections)
87
+ .allowWithoutAttributes("span")
88
+
89
+ .allowAttributes("id")
90
+ .matching(this.HTML_ID)
91
+ .globally()
92
+
93
+ .allowAttributes("class")
94
+ .matching(this.HTML_CLASS)
95
+ .globally()
96
+
97
+ .allowAttributes("lang")
98
+ .matching(/[a-zA-Z]{2,20}/)
99
+ .globally()
100
+
101
+ .allowAttributes("title")
102
+ .matching(this.HTML_TITLE)
103
+ .globally()
104
+
105
+ .allowStyling()
106
+
107
+ .allowAttributes("align")
108
+ .matching(this.ALIGN)
109
+ .onElements("p")
110
+
111
+ .allowAttributes("for")
112
+ .matching(this.HTML_ID)
113
+ .onElements("label")
114
+
115
+ .allowAttributes("color")
116
+ .matching(this.COLOR_NAME_OR_COLOR_CODE)
117
+ .onElements("font")
118
+
119
+ .allowAttributes("face")
120
+ .matching(/[\w;, \-]+/)
121
+ .onElements("font")
122
+
123
+ .allowAttributes("size")
124
+ .matching(this.NUMBER)
125
+ .onElements("font")
126
+
127
+ .allowAttributes("href")
128
+ .matching(this.ONSITE_OR_OFFSITE_URL)
129
+ .onElements("a")
130
+
131
+ .allowStandardUrlProtocols()
132
+ .allowAttributes("nohref")
133
+ .onElements("a")
134
+
135
+ .allowAttributes("name")
136
+ .matching(this.NAME)
137
+ .onElements("a")
138
+
139
+ .allowAttributes("onfocus", "onblur", "onclick", "onmousedown", "onmouseup")
140
+ .matching(this.HISTORY_BACK)
141
+ .onElements("a")
142
+
143
+ .requireRelNofollowOnLinks()
144
+ .allowAttributes("src")
145
+ .matching(this.ONSITE_OR_OFFSITE_URL)
146
+ .onElements("img")
147
+
148
+ .allowAttributes("name")
149
+ .matching(this.NAME)
150
+ .onElements("img")
151
+
152
+ .allowAttributes("alt")
153
+ .matching(this.PARAGRAPH)
154
+ .onElements("img")
155
+
156
+ .allowAttributes("border", "hspace", "vspace")
157
+ .matching(this.NUMBER)
158
+ .onElements("img")
159
+
160
+ .allowAttributes("border", "cellpadding", "cellspacing")
161
+ .matching(this.NUMBER)
162
+ .onElements("table")
163
+
164
+ .allowAttributes("bgcolor")
165
+ .matching(this.COLOR_NAME_OR_COLOR_CODE)
166
+ .onElements("table")
167
+
168
+ .allowAttributes("background")
169
+ .matching(this.ONSITE_URL)
170
+ .onElements("table")
171
+
172
+ .allowAttributes("align")
173
+ .matching(this.ALIGN)
174
+ .onElements("table")
175
+
176
+ .allowAttributes("noresize")
177
+ .matching(new RegExp("noresize", "i"))
178
+ .onElements("table")
179
+
180
+ .allowAttributes("background")
181
+ .matching(this.ONSITE_URL)
182
+ .onElements("td", "th", "tr")
183
+
184
+ .allowAttributes("bgcolor")
185
+ .matching(this.COLOR_NAME_OR_COLOR_CODE)
186
+ .onElements("td", "th")
187
+
188
+ .allowAttributes("abbr")
189
+ .matching(this.PARAGRAPH)
190
+ .onElements("td", "th")
191
+
192
+ .allowAttributes("axis", "headers")
193
+ .matching(this.NAME)
194
+ .onElements("td", "th")
195
+
196
+ .allowAttributes("scope")
197
+ .matching(new RegExp("(?:row|col)(?:group)?", "i"))
198
+ .onElements("td", "th")
199
+
200
+ .allowAttributes("nowrap")
201
+ .onElements("td", "th")
202
+
203
+ .allowAttributes("height", "width")
204
+ .matching(this.NUMBER_OR_PERCENT)
205
+ .onElements("table", "td", "th", "tr", "img")
206
+
207
+ .allowAttributes("align")
208
+ .matching(this.ALIGN)
209
+ .onElements(
210
+ "thead",
211
+ "tbody",
212
+ "tfoot",
213
+ "img",
214
+ "td",
215
+ "th",
216
+ "tr",
217
+ "colgroup",
218
+ "col"
219
+ )
220
+
221
+ .allowAttributes("valign")
222
+ .matching(this.VALIGN)
223
+ .onElements("thead", "tbody", "tfoot", "td", "th", "tr", "colgroup", "col")
224
+
225
+ .allowAttributes("charoff")
226
+ .matching(this.NUMBER_OR_PERCENT)
227
+ .onElements("td", "th", "tr", "colgroup", "col", "thead", "tbody", "tfoot")
228
+
229
+ .allowAttributes("char")
230
+ .matching(this.ONE_CHAR)
231
+ .onElements("td", "th", "tr", "colgroup", "col", "thead", "tbody", "tfoot")
232
+
233
+ .allowAttributes("colspan", "rowspan")
234
+ .matching(this.NUMBER)
235
+ .onElements("td", "th")
236
+
237
+ .allowAttributes("span", "width")
238
+ .matching(this.NUMBER_OR_PERCENT)
239
+ .onElements("colgroup", "col")
240
+ .allowElements(
241
+ "a",
242
+ "label",
243
+ "noscript",
244
+ "h1",
245
+ "h2",
246
+ "h3",
247
+ "h4",
248
+ "h5",
249
+ "h6",
250
+ "p",
251
+ "i",
252
+ "b",
253
+ "u",
254
+ "strong",
255
+ "em",
256
+ "small",
257
+ "big",
258
+ "pre",
259
+ "code",
260
+ "cite",
261
+ "samp",
262
+ "sub",
263
+ "sup",
264
+ "strike",
265
+ "center",
266
+ "blockquote",
267
+ "hr",
268
+ "br",
269
+ "col",
270
+ "font",
271
+ "map",
272
+ "span",
273
+ "div",
274
+ "img",
275
+ "ul",
276
+ "ol",
277
+ "li",
278
+ "dd",
279
+ "dt",
280
+ "dl",
281
+ "tbody",
282
+ "thead",
283
+ "tfoot",
284
+ "table",
285
+ "td",
286
+ "th",
287
+ "tr",
288
+ "colgroup",
289
+ "fieldset",
290
+ "legend"
291
+ )
292
+ .apply(html);
293
+ }
294
+ }
@@ -0,0 +1,5 @@
1
+ import { KcSanitizer } from "./KcSanitizer";
2
+
3
+ export function kcSanitize(html: string): string {
4
+ return KcSanitizer.sanitize(html, {});
5
+ }
@@ -7,6 +7,7 @@ import {
7
7
  import { id } from "tsafe/id";
8
8
  import { assert, type Equals } from "tsafe/assert";
9
9
  import { BASE_URL } from "keycloakify/lib/BASE_URL";
10
+ import type { LanguageTag } from "keycloakify/login/i18n/messages_defaultSet/types";
10
11
 
11
12
  const attributesByName = Object.fromEntries(
12
13
  id<Attribute[]>([
@@ -116,35 +117,59 @@ export const kcContextCommonMock: KcContext.Common = {
116
117
  }
117
118
  },
118
119
  locale: {
119
- supported: [
120
- /* spell-checker: disable */
121
- ["de", "Deutsch"],
122
- ["no", "Norsk"],
123
- ["ru", "Русский"],
124
- ["sv", "Svenska"],
125
- ["pt-BR", "Português (Brasil)"],
126
- ["lt", "Lietuvių"],
127
- ["en", "English"],
128
- ["it", "Italiano"],
129
- ["fr", "Français"],
130
- ["zh-CN", "中文简体"],
131
- ["es", "Español"],
132
- ["cs", "Čeština"],
133
- ["ja", "日本語"],
134
- ["sk", "Slovenčina"],
135
- ["pl", "Polski"],
136
- ["ca", "Català"],
137
- ["nl", "Nederlands"],
138
- ["tr", "Türkçe"]
139
- /* spell-checker: enable */
140
- ].map(
141
- ([languageTag, label]) =>
142
- ({
143
- languageTag,
144
- label,
145
- url: "https://gist.github.com/garronej/52baaca1bb925f2296ab32741e062b8e"
146
- }) as const
147
- ),
120
+ supported: (
121
+ [
122
+ /* spell-checker: disable */
123
+ ["de", "Deutsch"],
124
+ ["no", "Norsk"],
125
+ ["ru", "Русский"],
126
+ ["sv", "Svenska"],
127
+ ["pt-BR", "Português (Brasil)"],
128
+ ["lt", "Lietuvių"],
129
+ ["en", "English"],
130
+ ["it", "Italiano"],
131
+ ["fr", "Français"],
132
+ ["zh-CN", "中文简体"],
133
+ ["es", "Español"],
134
+ ["cs", "Čeština"],
135
+ ["ja", "日本語"],
136
+ ["sk", "Slovenčina"],
137
+ ["pl", "Polski"],
138
+ ["ca", "Català"],
139
+ ["nl", "Nederlands"],
140
+ ["tr", "Türkçe"],
141
+ ["ar", "العربية"],
142
+ ["da", "Dansk"],
143
+ ["el", "Ελληνικά"],
144
+ ["fa", "فارسی"],
145
+ ["fi", "Suomi"],
146
+ ["hu", "Magyar"],
147
+ ["ka", "ქართული"],
148
+ ["lv", "Latviešu"],
149
+ ["pt", "Português"],
150
+ ["th", "ไทย"],
151
+ ["uk", "Українська"],
152
+ ["zh-TW", "中文繁體"]
153
+ /* spell-checker: enable */
154
+ ] as const
155
+ ).map(([languageTag, label]) => {
156
+ {
157
+ type Got = typeof languageTag;
158
+ type Expected = LanguageTag;
159
+
160
+ type Missing = Exclude<Expected, Got>;
161
+ type Unexpected = Exclude<Got, Expected>;
162
+
163
+ assert<Equals<Missing, never>>;
164
+ assert<Equals<Unexpected, never>>;
165
+ }
166
+
167
+ return {
168
+ languageTag,
169
+ label,
170
+ url: "https://gist.github.com/garronej/52baaca1bb925f2296ab32741e062b8e"
171
+ } as const;
172
+ }),
148
173
 
149
174
  currentLanguageTag: "en"
150
175
  },
@@ -1,10 +1,10 @@
1
1
  import { useEffect } from "react";
2
- import { assert } from "keycloakify/tools/assert";
3
2
  import { clsx } from "keycloakify/tools/clsx";
3
+ import { kcSanitize } from "keycloakify/lib/kcSanitize";
4
4
  import type { TemplateProps } from "keycloakify/login/TemplateProps";
5
5
  import { getKcClsx } from "keycloakify/login/lib/kcClsx";
6
6
  import { useSetClassName } from "keycloakify/tools/useSetClassName";
7
- import { useStylesAndScripts } from "keycloakify/login/Template.useStylesAndScripts";
7
+ import { useInitialize } from "keycloakify/login/Template.useInitialize";
8
8
  import type { I18n } from "./i18n";
9
9
  import type { KcContext } from "./KcContext";
10
10
 
@@ -27,9 +27,9 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
27
27
 
28
28
  const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
29
29
 
30
- const { msg, msgStr, getChangeLocaleUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
30
+ const { msg, msgStr, currentLanguage, enabledLanguages } = i18n;
31
31
 
32
- const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext;
32
+ const { realm, auth, url, message, isAppInitiatedAction } = kcContext;
33
33
 
34
34
  useEffect(() => {
35
35
  document.title = documentTitle ?? msgStr("loginTitle", kcContext.realm.displayName);
@@ -45,7 +45,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
45
45
  className: bodyClassName ?? kcClsx("kcBodyClass")
46
46
  });
47
47
 
48
- const { isReadyToRender } = useStylesAndScripts({ kcContext, doUseDefaultCss });
48
+ const { isReadyToRender } = useInitialize({ kcContext, doUseDefaultCss });
49
49
 
50
50
  if (!isReadyToRender) {
51
51
  return null;
@@ -58,10 +58,9 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
58
58
  {msg("loginTitleHtml", realm.displayNameHtml)}
59
59
  </div>
60
60
  </div>
61
-
62
61
  <div className={kcClsx("kcFormCardClass")}>
63
62
  <header className={kcClsx("kcFormHeaderClass")}>
64
- {realm.internationalizationEnabled && (assert(locale !== undefined), locale.supported.length > 1) && (
63
+ {enabledLanguages.length > 1 && (
65
64
  <div className={kcClsx("kcLocaleMainClass")} id="kc-locale">
66
65
  <div id="kc-locale-wrapper" className={kcClsx("kcLocaleWrapperClass")}>
67
66
  <div id="kc-locale-dropdown" className={clsx("menu-button-links", kcClsx("kcLocaleDropDownClass"))}>
@@ -73,7 +72,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
73
72
  aria-expanded="false"
74
73
  aria-controls="language-switch1"
75
74
  >
76
- {labelBySupportedLanguageTag[currentLanguageTag]}
75
+ {currentLanguage.label}
77
76
  </button>
78
77
  <ul
79
78
  role="menu"
@@ -83,15 +82,10 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
83
82
  id="language-switch1"
84
83
  className={kcClsx("kcLocaleListClass")}
85
84
  >
86
- {locale.supported.map(({ languageTag }, i) => (
85
+ {enabledLanguages.map(({ languageTag, label, href }, i) => (
87
86
  <li key={languageTag} className={kcClsx("kcLocaleListItemClass")} role="none">
88
- <a
89
- role="menuitem"
90
- id={`language-${i + 1}`}
91
- className={kcClsx("kcLocaleItemClass")}
92
- href={getChangeLocaleUrl(languageTag)}
93
- >
94
- {labelBySupportedLanguageTag[languageTag]}
87
+ <a role="menuitem" id={`language-${i + 1}`} className={kcClsx("kcLocaleItemClass")} href={href}>
88
+ {label}
95
89
  </a>
96
90
  </li>
97
91
  ))}
@@ -152,7 +146,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
152
146
  <span
153
147
  className={kcClsx("kcAlertTitleClass")}
154
148
  dangerouslySetInnerHTML={{
155
- __html: message.summary
149
+ __html: kcSanitize(message.summary)
156
150
  }}
157
151
  />
158
152
  </div>
@@ -2,7 +2,7 @@ import { useEffect } from "react";
2
2
  import { assert } from "keycloakify/tools/assert";
3
3
  import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
4
4
  import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
5
- import { KcContext } from "keycloakify/login/KcContext/KcContext";
5
+ import type { KcContext } from "keycloakify/login/KcContext";
6
6
 
7
7
  export type KcContextLike = {
8
8
  url: {
@@ -10,34 +10,19 @@ export type KcContextLike = {
10
10
  resourcesPath: string;
11
11
  ssoLoginInOtherTabsUrl: string;
12
12
  };
13
- locale?: {
14
- currentLanguageTag: string;
15
- };
16
13
  scripts: string[];
17
14
  };
18
15
 
19
16
  assert<keyof KcContextLike extends keyof KcContext ? true : false>();
20
17
  assert<KcContext extends KcContextLike ? true : false>();
21
18
 
22
- export function useStylesAndScripts(params: {
19
+ export function useInitialize(params: {
23
20
  kcContext: KcContextLike;
24
21
  doUseDefaultCss: boolean;
25
22
  }) {
26
23
  const { kcContext, doUseDefaultCss } = params;
27
24
 
28
- const { url, locale, scripts } = kcContext;
29
-
30
- useEffect(() => {
31
- const { currentLanguageTag } = locale ?? {};
32
-
33
- if (currentLanguageTag === undefined) {
34
- return;
35
- }
36
-
37
- const html = document.querySelector("html");
38
- assert(html !== null);
39
- html.lang = currentLanguageTag;
40
- }, []);
25
+ const { url, scripts } = kcContext;
41
26
 
42
27
  const { areAllStyleSheetsLoaded } = useInsertLinkTags({
43
28
  componentOrHookName: "Template",
@@ -55,14 +40,7 @@ export function useStylesAndScripts(params: {
55
40
  const { insertScriptTags } = useInsertScriptTags({
56
41
  componentOrHookName: "Template",
57
42
  scriptTags: [
58
- {
59
- type: "importmap",
60
- textContent: JSON.stringify({
61
- imports: {
62
- rfc4648: `${url.resourcesCommonPath}/node_modules/rfc4648/lib/rfc4648.js`
63
- }
64
- })
65
- },
43
+ // NOTE: The importmap is added in by the FTL script because it's too late to add it here.
66
44
  {
67
45
  type: "module",
68
46
  src: `${url.resourcesPath}/js/menu-button-links.js`
@@ -76,9 +54,7 @@ export function useStylesAndScripts(params: {
76
54
  textContent: `
77
55
  import { checkCookiesAndSetTimer } from "${url.resourcesPath}/js/authChecker.js";
78
56
 
79
- checkCookiesAndSetTimer(
80
- "${url.ssoLoginInOtherTabsUrl}"
81
- );
57
+ checkCookiesAndSetTimer("${url.ssoLoginInOtherTabsUrl}");
82
58
  `
83
59
  }
84
60
  ]
@@ -1,5 +1,5 @@
1
- import type { GenericI18n } from "./GenericI18n";
2
- import type { MessageKey_defaultSet, KcContextLike } from "./i18n";
3
- export type { MessageKey_defaultSet, KcContextLike };
4
- export type I18n = GenericI18n<MessageKey_defaultSet>;
5
- export { createUseI18n } from "./useI18n";
1
+ export * from "./withJsx";
2
+ import type { GenericI18n } from "./withJsx/GenericI18n";
3
+ import type { MessageKey as MessageKey_defaultSet } from "./messages_defaultSet/types";
4
+ /** INTERNAL: DO NOT IMPORT THIS */
5
+ export type I18n = GenericI18n<MessageKey_defaultSet, string>;
@@ -0,0 +1,37 @@
1
+
2
+ export const languageTags = [
3
+ "ar",
4
+ "ca",
5
+ "cs",
6
+ "da",
7
+ "de",
8
+ "el",
9
+ "en",
10
+ "es",
11
+ "fa",
12
+ "fi",
13
+ "fr",
14
+ "hu",
15
+ "it",
16
+ "ja",
17
+ "ka",
18
+ "lt",
19
+ "lv",
20
+ "nl",
21
+ "no",
22
+ "pl",
23
+ "pt",
24
+ "pt-BR",
25
+ "ru",
26
+ "sk",
27
+ "sv",
28
+ "th",
29
+ "tr",
30
+ "uk",
31
+ "zh-CN",
32
+ "zh-TW"
33
+ ] as const;
34
+
35
+ export type LanguageTag = typeof languageTags[number];
36
+
37
+ export type MessageKey = keyof typeof import("./en")["default"];
@@ -0,0 +1,64 @@
1
+ export type GenericI18n_noJsx<MessageKey extends string, LanguageTag extends string> = {
2
+ currentLanguage: {
3
+ /**
4
+ * e.g: "en", "fr", "zh-CN"
5
+ *
6
+ * The current language
7
+ */
8
+ languageTag: LanguageTag;
9
+ /**
10
+ * e.g: "English", "Français", "中文(简体)"
11
+ *
12
+ * The current language
13
+ */
14
+ label: string;
15
+ };
16
+ /**
17
+ * Array of languages enabled on the realm.
18
+ */
19
+ enabledLanguages: {
20
+ languageTag: LanguageTag;
21
+ label: string;
22
+ href: string;
23
+ }[];
24
+ /**
25
+ *
26
+ * Examples assuming currentLanguageTag === "en"
27
+ * {
28
+ * en: {
29
+ * "access-denied": "Access denied",
30
+ * "impersonateTitleHtml": "<strong>{0}</strong> Impersonate User",
31
+ * "bar": "Bar {0}"
32
+ * }
33
+ * }
34
+ *
35
+ * msgStr("access-denied") === "Access denied"
36
+ * msgStr("not-a-message-key") Throws an error
37
+ * msgStr("impersonateTitleHtml", "Foo") === "<strong>Foo</strong> Impersonate User"
38
+ * msgStr("${bar}", "<strong>c</strong>") === "Bar &lt;strong&gt;XXX&lt;/strong&gt;"
39
+ * The html in the arg is partially escaped for security reasons, it might come from an untrusted source, it's not safe to render it as html.
40
+ */
41
+ msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string;
42
+ /**
43
+ * This is meant to be used when the key argument is variable, something that might have been configured by the user
44
+ * in the Keycloak admin for example.
45
+ *
46
+ * Examples assuming currentLanguageTag === "en"
47
+ * {
48
+ * en: {
49
+ * "access-denied": "Access denied",
50
+ * }
51
+ * }
52
+ *
53
+ * advancedMsgStr("${access-denied}") === advancedMsgStr("access-denied") === msgStr("access-denied") === "Access denied"
54
+ * advancedMsgStr("${not-a-message-key}") === advancedMsgStr("not-a-message-key") === "not-a-message-key"
55
+ */
56
+ advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
57
+
58
+ /**
59
+ * Initially the messages are in english (fallback language).
60
+ * The translations in the current language are being fetched dynamically.
61
+ * This property is true while the translations are being fetched.
62
+ */
63
+ isFetchingTranslations: boolean;
64
+ };