@designcrowd/library.brand-page 5.10.20 → 5.10.31
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/lib/src/brand-page/constants/font.constants.d.ts +4 -4
- package/lib/src/brand-page/models/index.d.ts +5 -0
- package/lib/src/brand-page/models/index.js +4 -0
- package/lib/src/brand-page/models/styles.model.d.ts +5 -5
- package/lib/src/brand-page/utils/style.util.d.ts +8 -1
- package/lib/src/brand-page/utils/style.util.js +58 -5
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.stories.js +2 -2
- package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.js +1 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/content-settings-items.fixture.js +1 -0
- package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/page-content-items.d.ts +1 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/page-content-items.js +2 -1
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +33 -0
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.js +35 -1
- package/lib/src/brand-page-maker/components/navigation/Navigation.mixin.d.ts +2 -2
- package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +3 -3
- package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +1 -0
- package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.js +17 -0
- package/lib/src/brand-page-maker/components/upgrade-to-premium-button/UpgradeToPremiumButton.mixin.d.ts +2 -2
- package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.d.ts +19 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.js +31 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/AutoLink.d.ts +2 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/AutoLink.js +161 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/extensions/MaxLength.js +3 -3
- package/lib/src/brand-page-maker/composables/rich-text-editor/font.d.ts +1 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/font.js +0 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/formatting.d.ts +1 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/formatting.js +0 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/hyperlink.d.ts +19 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/hyperlink.js +87 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/spacing.d.ts +1 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/spacing.js +0 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/textAlign.d.ts +1 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/textAlign.js +0 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +24 -3
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +214 -33
- package/lib/src/brand-page-maker/constants/brandpage-meta-constant.d.ts +5 -5
- package/lib/src/brand-page-maker/constants/index.d.ts +2 -2
- package/lib/src/brand-page-maker/models/button-group.model.d.ts +2 -0
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +19 -2
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +340 -8
- package/lib/src/bundled-translations-BXxQYq2i.js +119 -0
- package/lib/src/bundled-translations.de-DE-BXcluMZ2.js +119 -0
- package/lib/src/{bundled-translations.de-DE-O4sKkzBX.js → bundled-translations.de-DE-DjQFwukR.js} +1 -1
- package/lib/src/{bundled-translations.es-ES-C4lGaf08.js → bundled-translations.es-ES--EbGRI7e.js} +1 -1
- package/lib/src/bundled-translations.es-ES-DSd-wV16.js +119 -0
- package/lib/src/{bundled-translations.fr-FR-CIy8KjAV.js → bundled-translations.fr-FR-VtC2JYUC.js} +1 -1
- package/lib/src/bundled-translations.fr-FR-jKR_dxWR.js +119 -0
- package/lib/src/bundled-translations.pt-PT-CwvYqXXo.js +119 -0
- package/lib/src/{bundled-translations.pt-PT-C8EcSdyX.js → bundled-translations.pt-PT-l9cEldEo.js} +1 -1
- package/lib/src/bundles/bundled-translations.de-DE.json +11 -0
- package/lib/src/bundles/bundled-translations.es-ES.json +11 -0
- package/lib/src/bundles/bundled-translations.fr-FR.json +13 -2
- package/lib/src/bundles/bundled-translations.json +11 -0
- package/lib/src/bundles/bundled-translations.pt-PT.json +11 -0
- package/lib/src/composables/useTranslate.d.ts +64 -62
- package/lib/src/composables/useTranslate.js +18 -7
- package/lib/src/css/library.brand-page-brandCrowd.min.css +101 -75
- package/lib/src/css/library.brand-page-brandPage.min.css +101 -75
- package/lib/src/css/library.brand-page-designCom.min.css +101 -75
- package/lib/src/css/library.brand-page-preview.min.css +101 -75
- package/lib/src/index.d.ts +3 -1
- package/lib/src/index.mjs +30209 -27738
- package/lib/src/lib.js +2 -1
- package/lib/src/mockServiceWorker.js +91 -54
- package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/color-field/ColorField.mixin.d.ts +11 -0
- package/lib/src/shared/content-settings/components/color-field/ColorField.mixin.js +5 -0
- package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.mixin.d.ts +11 -1
- package/lib/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.mixin.js +4 -0
- package/lib/src/shared/content-settings/components/shape-field/ShapeField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/testimonial-items-field/TestimonialItemsField.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/text-image-field/TextImageField.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +4 -2
- package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.js +12 -7
- package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +2 -2
- package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.d.ts +3 -3
- package/lib/src/shared/my-uploads/MyUploads.mixin.d.ts +1 -1
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +1 -1
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +4 -2
- package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +4 -4
- package/lib/src/shared/shape-library/ShapeLibrary.mixin.d.ts +1 -1
- package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +2 -2
- package/lib/src/shared/template-settings/models.d.ts +3 -3
- package/package.json +6 -3
- package/src/brand-page/components/banner-section/BannerSection.vue +6 -0
- package/src/brand-page/components/sensitive-content/SensitiveContentModal.vue +28 -11
- package/src/brand-page/components/sensitive-content/i18n/sensitive-content-modal.de-DE.json +13 -0
- package/src/brand-page/components/sensitive-content/i18n/sensitive-content-modal.es-ES.json +13 -0
- package/src/brand-page/components/sensitive-content/i18n/sensitive-content-modal.fr-FR.json +13 -0
- package/src/brand-page/components/sensitive-content/i18n/sensitive-content-modal.json +13 -0
- package/src/brand-page/components/sensitive-content/i18n/sensitive-content-modal.pt-PT.json +13 -0
- package/src/brand-page/components/text/Text.vue +2 -0
- package/src/brand-page/components/text-wrapper/TextWrapper.vue +31 -11
- package/src/brand-page/models/index.ts +6 -0
- package/src/brand-page/utils/style.util.ts +105 -5
- package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.stories.ts +2 -2
- package/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.ts +1 -1
- package/src/brand-page-maker/components/brand-page-maker/__fixtures__/content-settings-items.fixture.ts +1 -0
- package/src/brand-page-maker/components/brand-page-maker/__fixtures__/page-content-items.ts +2 -0
- package/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.ts +44 -1
- package/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.vue +1 -0
- package/src/brand-page-maker/components/brandpage-settings/i18n/brand-page-settings.fr-FR.json +1 -1
- package/src/brand-page-maker/components/google-analytics/i18n/google-analytics.fr-FR.json +1 -1
- package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +539 -50
- package/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.ts +22 -0
- package/src/brand-page-maker/components/template-preview/TemplatePreview.vue +1 -1
- package/src/brand-page-maker/composables/rich-text-editor/bubbleMenu.ts +42 -0
- package/src/brand-page-maker/composables/rich-text-editor/extensions/AutoLink.ts +227 -0
- package/src/brand-page-maker/composables/rich-text-editor/extensions/MaxLength.test.ts +1 -1
- package/src/brand-page-maker/composables/rich-text-editor/extensions/MaxLength.ts +4 -15
- package/src/brand-page-maker/composables/rich-text-editor/font.ts +1 -1
- package/src/brand-page-maker/composables/rich-text-editor/formatting.ts +1 -1
- package/src/brand-page-maker/composables/rich-text-editor/hyperlink.ts +114 -0
- package/src/brand-page-maker/composables/rich-text-editor/spacing.ts +1 -1
- package/src/brand-page-maker/composables/rich-text-editor/textAlign.ts +1 -1
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +299 -41
- package/src/brand-page-maker/models/button-group.model.ts +2 -0
- package/src/brand-page-maker/stores/brandpage-maker/__snapshots__/brandpage-maker-store.test.ts.snap +1 -0
- package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +418 -0
- package/src/brand-page-maker/utils/rich-text-editor.util.ts +470 -25
- package/src/bundles/bundled-translations.de-DE.json +11 -0
- package/src/bundles/bundled-translations.es-ES.json +11 -0
- package/src/bundles/bundled-translations.fr-FR.json +13 -2
- package/src/bundles/bundled-translations.json +11 -0
- package/src/bundles/bundled-translations.pt-PT.json +11 -0
- package/src/composables/useTranslate.ts +25 -8
- package/src/lib.ts +7 -1
- package/src/shared/content-settings/components/color-field/ColorField.mixin.ts +5 -0
- package/src/shared/content-settings/components/color-field/ColorField.vue +1 -1
- package/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.mixin.ts +4 -0
- package/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.vue +2 -2
- package/src/shared/content-settings/components/text-field/TextField.vue +1 -1
- package/src/shared/content-settings/components/text-items-field/TextItemsField.vue +2 -2
- package/src/shared/content-settings/components/textarea-field/TextareaField.vue +3 -3
- package/src/shared/content-settings/components/url-field/UrlField.mixin.ts +15 -10
- package/src/shared/content-settings/components/url-field/UrlField.vue +1 -0
- package/lib/src/bundled-translations-DaK60m67.js +0 -117
- package/lib/src/bundled-translations.de-DE-848cpWPB.js +0 -117
- package/lib/src/bundled-translations.es-ES-CtPI5G7I.js +0 -117
- package/lib/src/bundled-translations.fr-FR-CTnDppmf.js +0 -117
- package/lib/src/bundled-translations.pt-PT-Balnne-7.js +0 -117
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
skipStyleDetection,
|
|
7
7
|
hasRTEContent,
|
|
8
8
|
getUpdatedHTMLFromEditor,
|
|
9
|
+
sanitiseClientSideRTE,
|
|
10
|
+
getRTEColor,
|
|
9
11
|
} from './rich-text-editor.util';
|
|
10
12
|
import type { TextStyleDeclaration } from '../../brand-page';
|
|
11
13
|
|
|
@@ -138,3 +140,419 @@ describe('getUpdatedHTMLFromEditor', () => {
|
|
|
138
140
|
expect(result).toBe('<div>styled</div>');
|
|
139
141
|
});
|
|
140
142
|
});
|
|
143
|
+
|
|
144
|
+
describe('sanitiseClientSideRTE', () => {
|
|
145
|
+
it('removes forbidden tags entirely', () => {
|
|
146
|
+
const input =
|
|
147
|
+
'<p>Hello</p><script>alert(1)</script><iframe src="evil"></iframe><img src="x" />';
|
|
148
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
149
|
+
|
|
150
|
+
expect(output).toContain('<p>Hello</p>');
|
|
151
|
+
expect(output).not.toContain('<script>');
|
|
152
|
+
expect(output).not.toContain('alert(1)');
|
|
153
|
+
expect(output).not.toContain('<iframe>');
|
|
154
|
+
expect(output).not.toContain('<img');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('strips dangerous attributes', () => {
|
|
158
|
+
const input = '<p onclick="alert(1)" onload="evil()">Hello</p>';
|
|
159
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
160
|
+
|
|
161
|
+
expect(output).toContain('<p>');
|
|
162
|
+
expect(output).toContain('Hello');
|
|
163
|
+
expect(output).not.toContain('onclick=');
|
|
164
|
+
expect(output).not.toContain('onload=');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('preserves text content', () => {
|
|
168
|
+
const input = '<div>alert();</div> <div>function(){alert();}()</div>';
|
|
169
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
170
|
+
|
|
171
|
+
expect(output).toContain(
|
|
172
|
+
'<div>alert();</div> <div>function(){alert();}()</div>',
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('preserves allowed tags', () => {
|
|
177
|
+
const input = `
|
|
178
|
+
<p>Paragraph</p>
|
|
179
|
+
<span>Span</span>
|
|
180
|
+
<strong>Strong</strong>
|
|
181
|
+
<em>Emphasis</em>
|
|
182
|
+
<u>Underline</u>
|
|
183
|
+
<s>Strike</s>
|
|
184
|
+
<mark>Mark</mark>
|
|
185
|
+
<div>Div</div>
|
|
186
|
+
<br>
|
|
187
|
+
<ul><li>List item</li></ul>
|
|
188
|
+
<ol><li>Ordered item</li></ol>
|
|
189
|
+
<h1>Heading 1</h1>
|
|
190
|
+
<h2>Heading 2</h2>
|
|
191
|
+
<h3>Heading 3</h3>
|
|
192
|
+
<h4>Heading 4</h4>
|
|
193
|
+
<h5>Heading 5</h5>
|
|
194
|
+
<h6>Heading 6</h6>
|
|
195
|
+
<blockquote>Quote</blockquote>
|
|
196
|
+
<code>Code</code>
|
|
197
|
+
<pre>Preformatted</pre>
|
|
198
|
+
<hr>
|
|
199
|
+
`;
|
|
200
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
201
|
+
|
|
202
|
+
expect(output).toContain('<p>Paragraph</p>');
|
|
203
|
+
expect(output).toContain('<span>Span</span>');
|
|
204
|
+
expect(output).toContain('<strong>Strong</strong>');
|
|
205
|
+
expect(output).toContain('<em>Emphasis</em>');
|
|
206
|
+
expect(output).toContain('<u>Underline</u>');
|
|
207
|
+
expect(output).toContain('<s>Strike</s>');
|
|
208
|
+
expect(output).toContain('<mark>Mark</mark>');
|
|
209
|
+
expect(output).toContain('<div>Div</div>');
|
|
210
|
+
expect(output).toContain('<br>');
|
|
211
|
+
expect(output).toContain('<ul><li>List item</li></ul>');
|
|
212
|
+
expect(output).toContain('<ol><li>Ordered item</li></ol>');
|
|
213
|
+
expect(output).toContain('<h1>Heading 1</h1>');
|
|
214
|
+
expect(output).toContain('<h2>Heading 2</h2>');
|
|
215
|
+
expect(output).toContain('<h3>Heading 3</h3>');
|
|
216
|
+
expect(output).toContain('<h4>Heading 4</h4>');
|
|
217
|
+
expect(output).toContain('<h5>Heading 5</h5>');
|
|
218
|
+
expect(output).toContain('<h6>Heading 6</h6>');
|
|
219
|
+
expect(output).toContain('<blockquote>Quote</blockquote>');
|
|
220
|
+
expect(output).toContain('<code>Code</code>');
|
|
221
|
+
expect(output).toContain('<pre>Preformatted</pre>');
|
|
222
|
+
expect(output).toContain('<hr>');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('preserves allowed attributes and removes disallowed ones', () => {
|
|
226
|
+
const input =
|
|
227
|
+
'<p class="foo" data-type="bar" data-color="red" style="color: blue; display: none;">Text</p>';
|
|
228
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
229
|
+
|
|
230
|
+
expect(output).not.toContain('class="foo"');
|
|
231
|
+
expect(output).not.toContain('data-type="bar"');
|
|
232
|
+
expect(output).toContain('data-color="red"');
|
|
233
|
+
expect(output).toContain('style="color: blue"');
|
|
234
|
+
expect(output).not.toContain('display: none');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('retains style attribute', () => {
|
|
238
|
+
const input = '<span style="color:red">Test</span>';
|
|
239
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
240
|
+
|
|
241
|
+
expect(output).toContain(input);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('removes href/javascript links', () => {
|
|
245
|
+
const input = '<a href="javascript:alert(1)">Link</a>';
|
|
246
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
247
|
+
|
|
248
|
+
expect(output).not.toContain('alert(1)');
|
|
249
|
+
expect(output).toContain('Link');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('sanitizes CSS properties to only allow TipTap extension properties', () => {
|
|
253
|
+
const input =
|
|
254
|
+
'<span style="color: red; font-size: 16px; display: block; position: absolute; background-color: blue;">Text</span>';
|
|
255
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
256
|
+
|
|
257
|
+
expect(output).toContain('color: red');
|
|
258
|
+
expect(output).toContain('font-size: 16px');
|
|
259
|
+
expect(output).toContain('background-color: blue');
|
|
260
|
+
expect(output).not.toContain('display: block');
|
|
261
|
+
expect(output).not.toContain('position: absolute');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('removes empty style attributes after CSS property filtering', () => {
|
|
265
|
+
const input =
|
|
266
|
+
'<span style="display: block; position: absolute;">Text</span>';
|
|
267
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
268
|
+
|
|
269
|
+
expect(output).not.toContain('style=');
|
|
270
|
+
expect(output).toContain('<span>Text</span>');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('preserves all TipTap extension CSS properties', () => {
|
|
274
|
+
const input =
|
|
275
|
+
'<span style="font-size: 16px; font-family: Arial; color: red; line-height: 1.5; letter-spacing: 0.1em; text-align: center; background-color: yellow; font-weight: bold; font-style: italic; text-decoration: underline;">Text</span>';
|
|
276
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
277
|
+
|
|
278
|
+
expect(output).toContain('font-size: 16px');
|
|
279
|
+
expect(output).toContain('font-family: Arial');
|
|
280
|
+
expect(output).toContain('color: red');
|
|
281
|
+
expect(output).toContain('line-height: 1.5');
|
|
282
|
+
expect(output).toContain('letter-spacing: 0.1em');
|
|
283
|
+
expect(output).toContain('text-align: center');
|
|
284
|
+
expect(output).toContain('background-color: yellow');
|
|
285
|
+
expect(output).toContain('font-weight: bold');
|
|
286
|
+
expect(output).toContain('font-style: italic');
|
|
287
|
+
expect(output).toContain('text-decoration: underline');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('handles malformed CSS gracefully', () => {
|
|
291
|
+
const input =
|
|
292
|
+
'<span style="color: red; ; font-size: ; font-family: Arial; malformed: property;">Text</span>';
|
|
293
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
294
|
+
|
|
295
|
+
expect(output).toContain('color: red');
|
|
296
|
+
expect(output).toContain('font-family: Arial');
|
|
297
|
+
expect(output).not.toContain('malformed: property');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('removes XSS attempts in style attributes', () => {
|
|
301
|
+
const input =
|
|
302
|
+
'<span style="color: red; expression(alert(1)); background: url(javascript:alert(1));">Text</span>';
|
|
303
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
304
|
+
|
|
305
|
+
expect(output).toContain('color: red');
|
|
306
|
+
expect(output).not.toContain('expression');
|
|
307
|
+
expect(output).not.toContain('alert(1)');
|
|
308
|
+
expect(output).not.toContain('javascript:');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('Anchor tag support', () => {
|
|
312
|
+
it('allows anchor tags with valid href attributes', () => {
|
|
313
|
+
const input = '<a href="https://example.com">Valid link</a>';
|
|
314
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
315
|
+
|
|
316
|
+
expect(output).toContain('<a href="https://example.com"');
|
|
317
|
+
expect(output).toContain('target="_blank"');
|
|
318
|
+
expect(output).toContain('rel="noopener noreferrer"');
|
|
319
|
+
expect(output).toContain('Valid link</a>');
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('allows relative path links', () => {
|
|
323
|
+
const input = '<a href="/internal-page">Internal link</a>';
|
|
324
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
325
|
+
|
|
326
|
+
expect(output).toContain('<a href="/internal-page">Internal link</a>');
|
|
327
|
+
expect(output).not.toContain('target="_blank"');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('allows relative path links with ./ prefix', () => {
|
|
331
|
+
const input = '<a href="./page.html">Relative link</a>';
|
|
332
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
333
|
+
|
|
334
|
+
expect(output).toContain('<a href="./page.html">Relative link</a>');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('allows relative path links with ../ prefix', () => {
|
|
338
|
+
const input = '<a href="../parent-page">Parent link</a>';
|
|
339
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
340
|
+
|
|
341
|
+
expect(output).toContain('<a href="../parent-page">Parent link</a>');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('allows mailto links with valid email', () => {
|
|
345
|
+
const input = '<a href="mailto:test@example.com">Email link</a>';
|
|
346
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
347
|
+
|
|
348
|
+
expect(output).toContain(
|
|
349
|
+
'<a href="mailto:test@example.com">Email link</a>',
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('allows tel links with valid phone number', () => {
|
|
354
|
+
const input = '<a href="tel:+1-234-567-8900">Phone link</a>';
|
|
355
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
356
|
+
|
|
357
|
+
expect(output).toContain('<a href="tel:+1-234-567-8900">Phone link</a>');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('removes dangerous javascript: hrefs', () => {
|
|
361
|
+
const input = '<a href="javascript:alert(1)">Dangerous link</a>';
|
|
362
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
363
|
+
|
|
364
|
+
expect(output).toContain('<a>Dangerous link</a>');
|
|
365
|
+
expect(output).not.toContain('javascript:');
|
|
366
|
+
expect(output).not.toContain('href=');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('removes dangerous data: hrefs', () => {
|
|
370
|
+
const input =
|
|
371
|
+
'<a href="data:text/html,<script>alert(1)</script>">Data link</a>';
|
|
372
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
373
|
+
|
|
374
|
+
expect(output).toContain('<a>Data link</a>');
|
|
375
|
+
expect(output).not.toContain('data:');
|
|
376
|
+
expect(output).not.toContain('href=');
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('removes invalid mailto links', () => {
|
|
380
|
+
const input = '<a href="mailto:invalid-email">Invalid email</a>';
|
|
381
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
382
|
+
|
|
383
|
+
expect(output).toContain('<a>Invalid email</a>');
|
|
384
|
+
expect(output).not.toContain('href=');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('removes invalid tel links', () => {
|
|
388
|
+
const input = '<a href="tel:invalid@phone">Invalid phone</a>';
|
|
389
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
390
|
+
|
|
391
|
+
expect(output).toContain('<a>Invalid phone</a>');
|
|
392
|
+
expect(output).not.toContain('href=');
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('removes hrefs with dangerous characters', () => {
|
|
396
|
+
const input =
|
|
397
|
+
'<a href="path<script>alert(1)</script>">Dangerous path</a>';
|
|
398
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
399
|
+
|
|
400
|
+
expect(output).toContain('<a>Dangerous path</a>');
|
|
401
|
+
expect(output).not.toContain('href=');
|
|
402
|
+
expect(output).not.toContain('<script>');
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('validates URLs properly', () => {
|
|
406
|
+
const input = '<a href="https://malformed..url">Invalid URL</a>';
|
|
407
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
408
|
+
|
|
409
|
+
expect(output).toContain('<a>Invalid URL</a>');
|
|
410
|
+
expect(output).not.toContain('href=');
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('allows simple relative paths without dangerous characters', () => {
|
|
414
|
+
const input = '<a href="simple-path">Simple link</a>';
|
|
415
|
+
const output = sanitiseClientSideRTE(input).sanitizedHtml;
|
|
416
|
+
|
|
417
|
+
expect(output).toContain('<a href="simple-path">Simple link</a>');
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
describe('Sanitization result tracking', () => {
|
|
422
|
+
it('returns sanitization info with removed elements', () => {
|
|
423
|
+
const input =
|
|
424
|
+
'<script>alert(1)</script><a href="javascript:void(0)">Link</a><p class="forbidden">Text</p>';
|
|
425
|
+
const result = sanitiseClientSideRTE(input);
|
|
426
|
+
|
|
427
|
+
expect(result.sanitizedHtml).toContain('<a>Link</a>');
|
|
428
|
+
expect(result.sanitizedHtml).toContain('<p>Text</p>');
|
|
429
|
+
expect(result.sanitizedHtml).not.toContain('<script>');
|
|
430
|
+
expect(result.sanitizedHtml).not.toContain('javascript:');
|
|
431
|
+
expect(result.sanitizedHtml).not.toContain('class=');
|
|
432
|
+
|
|
433
|
+
// Should track the removed href attribute
|
|
434
|
+
expect(result.removedElements).toContain('href="javascript:void(0)"');
|
|
435
|
+
// Should track some removed elements (script tag or attributes)
|
|
436
|
+
expect(result.removedElements.length).toBeGreaterThan(0);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('tracks multiple removed elements', () => {
|
|
440
|
+
const input =
|
|
441
|
+
'<img src="test.jpg"><iframe src="evil.com"></iframe><a href="data:text/html,bad">Link</a>';
|
|
442
|
+
const result = sanitiseClientSideRTE(input);
|
|
443
|
+
|
|
444
|
+
expect(result.removedElements).toContain('href="data:text/html,bad"');
|
|
445
|
+
// Some tags might be removed by DOMPurify depending on configuration
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('returns minimal removal info for safe content', () => {
|
|
449
|
+
const input =
|
|
450
|
+
'<p>Safe content</p><a href="https://example.com">Safe link</a>';
|
|
451
|
+
const result = sanitiseClientSideRTE(input);
|
|
452
|
+
|
|
453
|
+
expect(result.sanitizedHtml).toContain('<p>Safe content</p>');
|
|
454
|
+
expect(result.sanitizedHtml).toContain('<a href="https://example.com"');
|
|
455
|
+
// DOMPurify might add some tags like 'body' during processing
|
|
456
|
+
// We expect minimal removed elements for safe content
|
|
457
|
+
expect(
|
|
458
|
+
result.removedElements.filter((item) => !item.includes('body')).length,
|
|
459
|
+
).toBe(0);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('tracks href removals correctly', () => {
|
|
463
|
+
const input =
|
|
464
|
+
'<a href="javascript:1">Link1</a><a href="javascript:2">Link2</a>';
|
|
465
|
+
const result = sanitiseClientSideRTE(input);
|
|
466
|
+
|
|
467
|
+
expect(
|
|
468
|
+
result.removedElements.filter((attr) => attr.includes('javascript:'))
|
|
469
|
+
.length,
|
|
470
|
+
).toBeGreaterThan(0);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('tracks removed CSS properties in removedElements', () => {
|
|
474
|
+
const input =
|
|
475
|
+
'<span style="color: red; display: block; position: absolute; background-color: blue;">Text</span>';
|
|
476
|
+
const result = sanitiseClientSideRTE(input);
|
|
477
|
+
|
|
478
|
+
expect(result.sanitizedHtml).toContain('color: red');
|
|
479
|
+
expect(result.sanitizedHtml).toContain('background-color: blue');
|
|
480
|
+
expect(result.sanitizedHtml).not.toContain('display: block');
|
|
481
|
+
expect(result.sanitizedHtml).not.toContain('position: absolute');
|
|
482
|
+
|
|
483
|
+
// Check that removed CSS properties are tracked
|
|
484
|
+
expect(result.removedElements).toContain('display: block');
|
|
485
|
+
expect(result.removedElements).toContain('position: absolute');
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('does not duplicate removed CSS properties', () => {
|
|
489
|
+
const input =
|
|
490
|
+
'<span style="display: block;">Text1</span><div style="display: none;">Text2</div>';
|
|
491
|
+
const result = sanitiseClientSideRTE(input);
|
|
492
|
+
|
|
493
|
+
const displayCount = result.removedElements.filter((el) =>
|
|
494
|
+
el.startsWith('display:'),
|
|
495
|
+
).length;
|
|
496
|
+
|
|
497
|
+
expect(displayCount).toBe(2); // Should have both "display: block" and "display: none"
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
describe('getRTEColor', () => {
|
|
503
|
+
it('returns default text color for null/undefined color', () => {
|
|
504
|
+
expect(getRTEColor(null, 'text')).toBe('#000000');
|
|
505
|
+
expect(getRTEColor(undefined, 'text')).toBe('#000000');
|
|
506
|
+
expect(getRTEColor('', 'text')).toBe('#000000');
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('returns default background color for null/undefined color', () => {
|
|
510
|
+
expect(getRTEColor(null, 'background')).toBe('transparent');
|
|
511
|
+
expect(getRTEColor(undefined, 'background')).toBe('transparent');
|
|
512
|
+
expect(getRTEColor('', 'background')).toBe('transparent');
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('returns custom default value when provided', () => {
|
|
516
|
+
expect(getRTEColor(null, 'text', '#ff0000')).toBe('#ff0000');
|
|
517
|
+
expect(getRTEColor(undefined, 'background', '#00ff00')).toBe('#00ff00');
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('returns CSS variable for logo theme colors', () => {
|
|
521
|
+
expect(getRTEColor('logo-text-color', 'text')).toBe(
|
|
522
|
+
'var(--logo-text-color)',
|
|
523
|
+
);
|
|
524
|
+
expect(getRTEColor('logo-background-color', 'background')).toBe(
|
|
525
|
+
'var(--logo-background-color)',
|
|
526
|
+
);
|
|
527
|
+
expect(getRTEColor('logo-text-multi-color', 'text')).toBe(
|
|
528
|
+
'var(--logo-text-multi-color)',
|
|
529
|
+
);
|
|
530
|
+
expect(getRTEColor('logo-background-multi-color', 'background')).toBe(
|
|
531
|
+
'var(--logo-background-multi-color)',
|
|
532
|
+
);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('returns regular color as-is for non-theme colors', () => {
|
|
536
|
+
expect(getRTEColor('#ff0000', 'text')).toBe('#ff0000');
|
|
537
|
+
expect(getRTEColor('rgb(255, 0, 0)', 'background')).toBe('rgb(255, 0, 0)');
|
|
538
|
+
expect(getRTEColor('blue', 'text')).toBe('blue');
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('handles colors containing theme keys', () => {
|
|
542
|
+
// Should NOT wrap colors that contain theme keys but don't start with them
|
|
543
|
+
expect(getRTEColor('some-logo-text-color-variant', 'text')).toBe(
|
|
544
|
+
'some-logo-text-color-variant',
|
|
545
|
+
);
|
|
546
|
+
expect(
|
|
547
|
+
getRTEColor('custom-logo-background-color-style', 'background'),
|
|
548
|
+
).toBe('custom-logo-background-color-style');
|
|
549
|
+
|
|
550
|
+
// Should NOT double-wrap already processed CSS variables
|
|
551
|
+
expect(getRTEColor('var(--logo-text-color)', 'text')).toBe(
|
|
552
|
+
'var(--logo-text-color)',
|
|
553
|
+
);
|
|
554
|
+
expect(getRTEColor('var(--logo-background-color)', 'background')).toBe(
|
|
555
|
+
'var(--logo-background-color)',
|
|
556
|
+
);
|
|
557
|
+
});
|
|
558
|
+
});
|