@capillarytech/creatives-library 8.0.208 → 8.0.210

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 (76) hide show
  1. package/assets/Android.png +0 -0
  2. package/assets/iOS.png +0 -0
  3. package/package.json +16 -2
  4. package/v2Components/HtmlEditor/HTMLEditor.js +508 -0
  5. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1809 -0
  6. package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +532 -0
  7. package/v2Components/HtmlEditor/_htmlEditor.scss +304 -0
  8. package/v2Components/HtmlEditor/_index.lazy.scss +26 -0
  9. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +376 -0
  10. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +331 -0
  11. package/v2Components/HtmlEditor/components/DeviceToggle/__tests__/index.test.js +314 -0
  12. package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +244 -0
  13. package/v2Components/HtmlEditor/components/DeviceToggle/index.js +111 -0
  14. package/v2Components/HtmlEditor/components/EditorToolbar/PreviewModeGroup.js +72 -0
  15. package/v2Components/HtmlEditor/components/EditorToolbar/__tests__/PreviewModeGroup.test.js +1594 -0
  16. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +113 -0
  17. package/v2Components/HtmlEditor/components/EditorToolbar/_previewModeGroup.scss +82 -0
  18. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +115 -0
  19. package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +57 -0
  20. package/v2Components/HtmlEditor/components/InAppPreviewPane/ContentOverlay.js +90 -0
  21. package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +60 -0
  22. package/v2Components/HtmlEditor/components/InAppPreviewPane/LayoutSelector.js +58 -0
  23. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/ContentOverlay.test.js +403 -0
  24. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +424 -0
  25. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/LayoutSelector.test.js +248 -0
  26. package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +253 -0
  27. package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +104 -0
  28. package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +179 -0
  29. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +220 -0
  30. package/v2Components/HtmlEditor/components/PreviewPane/index.js +229 -0
  31. package/v2Components/HtmlEditor/components/SplitContainer/SplitContainer.js +276 -0
  32. package/v2Components/HtmlEditor/components/SplitContainer/__tests__/SplitContainer.test.js +295 -0
  33. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +257 -0
  34. package/v2Components/HtmlEditor/components/SplitContainer/index.js +7 -0
  35. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
  36. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +31 -0
  37. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +70 -0
  38. package/v2Components/HtmlEditor/components/ValidationPanel/__tests__/index.test.js +98 -0
  39. package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +311 -0
  40. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +297 -0
  41. package/v2Components/HtmlEditor/components/ValidationPanel/messages.js +57 -0
  42. package/v2Components/HtmlEditor/components/common/EditorContext.js +84 -0
  43. package/v2Components/HtmlEditor/components/common/__tests__/EditorContext.test.js +660 -0
  44. package/v2Components/HtmlEditor/constants.js +241 -0
  45. package/v2Components/HtmlEditor/hooks/__tests__/useEditorContent.test.js +450 -0
  46. package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +785 -0
  47. package/v2Components/HtmlEditor/hooks/__tests__/useLayoutState.test.js +580 -0
  48. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.enhanced.test.js +768 -0
  49. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +590 -0
  50. package/v2Components/HtmlEditor/hooks/useEditorContent.js +274 -0
  51. package/v2Components/HtmlEditor/hooks/useInAppContent.js +407 -0
  52. package/v2Components/HtmlEditor/hooks/useLayoutState.js +247 -0
  53. package/v2Components/HtmlEditor/hooks/useValidation.js +325 -0
  54. package/v2Components/HtmlEditor/index.js +29 -0
  55. package/v2Components/HtmlEditor/index.lazy.js +114 -0
  56. package/v2Components/HtmlEditor/messages.js +389 -0
  57. package/v2Components/HtmlEditor/utils/__tests__/contentSanitizer.test.js +741 -0
  58. package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +1042 -0
  59. package/v2Components/HtmlEditor/utils/__tests__/liquidTemplateSupport.test.js +515 -0
  60. package/v2Components/HtmlEditor/utils/__tests__/properSyntaxHighlighting.test.js +473 -0
  61. package/v2Components/HtmlEditor/utils/__tests__/simplePerformance.test.js +1109 -0
  62. package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +240 -0
  63. package/v2Components/HtmlEditor/utils/contentSanitizer.js +433 -0
  64. package/v2Components/HtmlEditor/utils/htmlValidator.js +508 -0
  65. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +524 -0
  66. package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +163 -0
  67. package/v2Components/HtmlEditor/utils/simplePerformance.js +145 -0
  68. package/v2Components/HtmlEditor/utils/validationAdapter.js +130 -0
  69. package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +200 -0
  70. package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +545 -0
  71. package/v2Containers/EmailWrapper/index.js +8 -1
  72. package/v2Containers/Rcs/index.js +2 -0
  73. package/v2Containers/Templates/constants.js +8 -0
  74. package/v2Containers/Templates/index.js +56 -28
  75. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -14
  76. package/v2Containers/Whatsapp/index.js +1 -0
@@ -0,0 +1,240 @@
1
+ /**
2
+ * ValidationAdapter Tests
3
+ *
4
+ * Tests for the validation adapter utility that transforms HTML Editor
5
+ * validation data to ErrorInfoNote format.
6
+ */
7
+
8
+ import {
9
+ transformValidationToErrorInfo,
10
+ hasValidationErrors,
11
+ getValidationSummary
12
+ } from '../validationAdapter';
13
+
14
+ // Mock validation object
15
+ const createMockValidation = (issues = [], isValidating = false) => ({
16
+ isValidating,
17
+ getAllIssues: jest.fn(() => issues),
18
+ isClean: jest.fn(() => issues.length === 0),
19
+ summary: {
20
+ totalErrors: issues.filter(i => i.severity === 'error').length,
21
+ totalWarnings: issues.filter(i => i.severity === 'warning').length,
22
+ hasSecurityIssues: issues.some(i => i.source === 'security')
23
+ }
24
+ });
25
+
26
+ describe('transformValidationToErrorInfo', () => {
27
+ it('returns empty error messages when validation is null', () => {
28
+ const result = transformValidationToErrorInfo(null);
29
+ expect(result).toEqual({
30
+ errorMessages: {
31
+ LIQUID_ERROR_MSG: [],
32
+ STANDARD_ERROR_MSG: []
33
+ }
34
+ });
35
+ });
36
+
37
+ it('returns empty error messages when validation is validating', () => {
38
+ const validation = createMockValidation([], true);
39
+ const result = transformValidationToErrorInfo(validation);
40
+ expect(result).toEqual({
41
+ errorMessages: {
42
+ LIQUID_ERROR_MSG: [],
43
+ STANDARD_ERROR_MSG: []
44
+ }
45
+ });
46
+ });
47
+
48
+ it('separates liquid and standard errors correctly', () => {
49
+ const issues = [
50
+ {
51
+ message: 'Liquid syntax error',
52
+ source: 'liquid-validator',
53
+ severity: 'error',
54
+ line: 1,
55
+ column: 5
56
+ },
57
+ {
58
+ message: 'HTML validation error',
59
+ source: 'htmlhint',
60
+ severity: 'error',
61
+ line: 2,
62
+ column: 10,
63
+ rule: 'tag-pair'
64
+ },
65
+ {
66
+ message: 'Another liquid error',
67
+ rule: 'liquid-malformed',
68
+ severity: 'warning'
69
+ }
70
+ ];
71
+
72
+ const validation = createMockValidation(issues);
73
+ const result = transformValidationToErrorInfo(validation);
74
+
75
+ expect(result.errorMessages.LIQUID_ERROR_MSG).toHaveLength(2);
76
+ expect(result.errorMessages.STANDARD_ERROR_MSG).toHaveLength(1);
77
+
78
+ // Check liquid error formatting
79
+ expect(result.errorMessages.LIQUID_ERROR_MSG[0]).toContain('Liquid syntax error');
80
+ expect(result.errorMessages.LIQUID_ERROR_MSG[0]).toContain('Line 1, Char 5');
81
+
82
+ // Check standard error formatting
83
+ expect(result.errorMessages.STANDARD_ERROR_MSG[0]).toContain('HTML validation error');
84
+ expect(result.errorMessages.STANDARD_ERROR_MSG[0]).toContain('Line 2, Char 10');
85
+ expect(result.errorMessages.STANDARD_ERROR_MSG[0]).toContain('tag-pair');
86
+ });
87
+
88
+ it('formats error messages with line and column info', () => {
89
+ const issues = [
90
+ {
91
+ message: 'Test error',
92
+ source: 'htmlhint',
93
+ severity: 'error',
94
+ line: 5,
95
+ column: 15,
96
+ rule: 'test-rule'
97
+ }
98
+ ];
99
+
100
+ const validation = createMockValidation(issues);
101
+ const result = transformValidationToErrorInfo(validation);
102
+
103
+ const errorMessage = result.errorMessages.STANDARD_ERROR_MSG[0];
104
+ expect(errorMessage).toBe('Test error Line 5, Char 15. • test-rule');
105
+ });
106
+
107
+ it('handles errors without line/column info', () => {
108
+ const issues = [
109
+ {
110
+ message: 'General error',
111
+ source: 'custom',
112
+ severity: 'error'
113
+ }
114
+ ];
115
+
116
+ const validation = createMockValidation(issues);
117
+ const result = transformValidationToErrorInfo(validation);
118
+
119
+ expect(result.errorMessages.STANDARD_ERROR_MSG[0]).toBe('General error');
120
+ });
121
+
122
+ it('works with both email and inapp variants', () => {
123
+ const issues = [
124
+ {
125
+ message: 'Test error',
126
+ source: 'liquid-validator',
127
+ severity: 'error'
128
+ }
129
+ ];
130
+
131
+ const validation = createMockValidation(issues);
132
+
133
+ const emailResult = transformValidationToErrorInfo(validation, 'email');
134
+ const inappResult = transformValidationToErrorInfo(validation, 'inapp');
135
+
136
+ expect(emailResult.errorMessages.LIQUID_ERROR_MSG).toHaveLength(1);
137
+ expect(inappResult.errorMessages.LIQUID_ERROR_MSG).toHaveLength(1);
138
+ });
139
+ });
140
+
141
+ describe('hasValidationErrors', () => {
142
+ it('returns false when validation is null', () => {
143
+ expect(hasValidationErrors(null)).toBe(false);
144
+ });
145
+
146
+ it('returns false when validation is validating', () => {
147
+ const validation = createMockValidation([], true);
148
+ expect(hasValidationErrors(validation)).toBe(false);
149
+ });
150
+
151
+ it('returns false when there are no issues', () => {
152
+ const validation = createMockValidation([]);
153
+ expect(hasValidationErrors(validation)).toBe(false);
154
+ });
155
+
156
+ it('returns true when there are validation issues', () => {
157
+ const issues = [
158
+ {
159
+ message: 'Test error',
160
+ source: 'htmlhint',
161
+ severity: 'error'
162
+ }
163
+ ];
164
+ const validation = createMockValidation(issues);
165
+ expect(hasValidationErrors(validation)).toBe(true);
166
+ });
167
+ });
168
+
169
+ describe('getValidationSummary', () => {
170
+ it('returns default summary when validation is null', () => {
171
+ const result = getValidationSummary(null);
172
+ expect(result).toEqual({
173
+ totalErrors: 0,
174
+ totalWarnings: 0,
175
+ hasLiquidErrors: false
176
+ });
177
+ });
178
+
179
+ it('calculates summary correctly with mixed issues', () => {
180
+ const issues = [
181
+ {
182
+ message: 'Error 1',
183
+ source: 'htmlhint',
184
+ severity: 'error'
185
+ },
186
+ {
187
+ message: 'Warning 1',
188
+ source: 'css-validator',
189
+ severity: 'warning'
190
+ },
191
+ {
192
+ message: 'Liquid error',
193
+ source: 'liquid-validator',
194
+ severity: 'error'
195
+ }
196
+ ];
197
+
198
+ const validation = createMockValidation(issues);
199
+ const result = getValidationSummary(validation);
200
+
201
+ expect(result).toEqual({
202
+ totalErrors: 2,
203
+ totalWarnings: 1,
204
+ hasLiquidErrors: true,
205
+ totalIssues: 3
206
+ });
207
+ });
208
+
209
+ it('detects liquid errors by rule name', () => {
210
+ const issues = [
211
+ {
212
+ message: 'Liquid rule error',
213
+ source: 'custom',
214
+ severity: 'error',
215
+ rule: 'liquid-malformed'
216
+ }
217
+ ];
218
+
219
+ const validation = createMockValidation(issues);
220
+ const result = getValidationSummary(validation);
221
+
222
+ expect(result.hasLiquidErrors).toBe(true);
223
+ });
224
+
225
+ it('detects liquid errors by message content', () => {
226
+ const issues = [
227
+ {
228
+ message: 'Invalid liquid syntax detected',
229
+ source: 'custom',
230
+ severity: 'error'
231
+ }
232
+ ];
233
+
234
+ const validation = createMockValidation(issues);
235
+ const result = getValidationSummary(validation);
236
+
237
+ expect(result.hasLiquidErrors).toBe(true);
238
+ });
239
+ });
240
+
@@ -0,0 +1,433 @@
1
+ /**
2
+ * Content Sanitization Utility
3
+ * Uses DOMPurify for XSS protection and content cleaning
4
+ */
5
+
6
+ import DOMPurify from 'dompurify';
7
+
8
+ // Default message formatter for when intl is not available
9
+ const defaultMessageFormatter = (messageKey, values = {}) => {
10
+ // Fallback messages for when intl is not available
11
+ const fallbackMessages = {
12
+ 'sanitizer.invalidInput': 'Invalid input: HTML content must be a string',
13
+ 'sanitizer.invalidInputNonEmpty': 'Invalid input: HTML content must be a non-empty string',
14
+ 'sanitizer.sanitizationFailed': `Sanitization failed: ${values.error || 'Unknown error'}`,
15
+ 'sanitizer.dangerousProtocolDetected': 'Dangerous protocol detected',
16
+ 'sanitizer.emailMultimediaNotSupported': 'Video/audio/canvas elements may not be supported in email clients',
17
+ 'sanitizer.emailPositioningNotSupported': 'Fixed/sticky positioning may not work in email clients',
18
+ 'sanitizer.emailModernCssLimited': 'CSS Grid and Flexbox have limited support in email clients',
19
+ 'sanitizer.emailViewportUnitsNotSupported': 'Viewport units and rem may not be supported in all email clients',
20
+ 'sanitizer.mobileTablesNotFriendly': 'Tables may not be mobile-friendly - consider using flexbox or grid',
21
+ 'sanitizer.mobileRelativeFontSizes': 'Consider using relative font sizes (em, rem, %) for better mobile scaling',
22
+ 'sanitizer.mobileFixedWidthsProblematic': 'Large fixed widths may not work well on mobile devices',
23
+ 'sanitizer.mobileRelativeUnits': 'Consider using relative units (rem, em, %) for better mobile responsiveness',
24
+ 'sanitizer.productionValidHtml': 'Provide valid HTML content before deploying to production',
25
+ 'sanitizer.productionSanitized': 'Content has been sanitized for security - review changes before deploying',
26
+ 'sanitizer.productionInlineCss': 'Consider inlining CSS for better email client compatibility',
27
+ 'sanitizer.productionLargeContent': `Content is large (${values.size || 'unknown'} characters) - consider optimizing for mobile performance`
28
+ };
29
+
30
+ return fallbackMessages[messageKey] || messageKey;
31
+ };
32
+
33
+ // Constants for better maintainability
34
+ const SANITIZER_VARIANTS = {
35
+ EMAIL: 'email',
36
+ INAPP: 'inapp'
37
+ };
38
+
39
+ const SECURITY_LEVELS = {
40
+ STANDARD: 'standard',
41
+ STRICT: 'strict'
42
+ };
43
+
44
+ const CONTENT_LIMITS = {
45
+ LARGE_CONTENT_SIZE: 50000,
46
+ MIN_CONTENT_LENGTH: 0
47
+ };
48
+
49
+ const DANGEROUS_PROTOCOLS = ['javascript:', 'data:', 'vbscript:'];
50
+
51
+ const EVENT_HANDLERS = [
52
+ 'onclick', 'onload', 'onerror', 'onmouseover', 'onmouseout',
53
+ 'onmousedown', 'onmouseup', 'onkeydown', 'onkeyup', 'onkeypress',
54
+ 'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset'
55
+ ];
56
+
57
+ // Email-specific sanitization config
58
+ const EMAIL_CONFIG = {
59
+ ALLOWED_TAGS: [
60
+ 'html', 'head', 'title', 'meta', 'style', 'body',
61
+ 'div', 'span', 'p', 'br', 'hr', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
62
+ 'a', 'img', 'table', 'tr', 'td', 'th', 'thead', 'tbody', 'tfoot',
63
+ 'ul', 'ol', 'li', 'strong', 'b', 'em', 'i', 'u', 'center',
64
+ 'font', 'small', 'big', 'sup', 'sub', 'pre', 'code',
65
+ 'blockquote', 'cite', 'abbr', 'acronym', 'address'
66
+ ],
67
+ ALLOWED_ATTR: [
68
+ 'src', 'alt', 'title', 'href', 'target', 'rel',
69
+ 'width', 'height', 'style', 'class', 'id',
70
+ 'align', 'valign', 'bgcolor', 'color', 'border',
71
+ 'cellpadding', 'cellspacing', 'colspan', 'rowspan',
72
+ 'type', 'charset', 'content', 'name', 'http-equiv'
73
+ ],
74
+ FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'applet', 'form', 'input'],
75
+ FORBID_ATTR: ['onclick', 'onload', 'onerror', 'onmouseover'],
76
+ ALLOW_DATA_ATTR: false
77
+ };
78
+
79
+ // InApp-specific sanitization config
80
+ const INAPP_CONFIG = {
81
+ ALLOWED_TAGS: [
82
+ 'html', 'head', 'title', 'meta', 'style', 'body',
83
+ 'div', 'span', 'p', 'br', 'hr', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
84
+ 'a', 'img', 'button', 'ul', 'ol', 'li', 'strong', 'b', 'em', 'i', 'u',
85
+ 'small', 'big', 'sup', 'sub', 'pre', 'code', 'blockquote', 'cite',
86
+ 'video', 'audio', 'source', 'canvas' // Mobile-friendly multimedia
87
+ ],
88
+ ALLOWED_ATTR: [
89
+ 'src', 'alt', 'title', 'href', 'target', 'rel',
90
+ 'width', 'height', 'style', 'class', 'id',
91
+ 'type', 'controls', 'autoplay', 'loop', 'muted',
92
+ 'poster', 'preload'
93
+ ],
94
+ FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'applet', 'form', 'input'],
95
+ FORBID_ATTR: ['onclick', 'onload', 'onerror', 'onmouseover'],
96
+ ALLOW_DATA_ATTR: true
97
+ };
98
+
99
+ // Strict sanitization config for production
100
+ const STRICT_CONFIG = {
101
+ ALLOWED_TAGS: [
102
+ 'div', 'span', 'p', 'br', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
103
+ 'a', 'img', 'strong', 'b', 'em', 'i', 'u', 'ul', 'ol', 'li'
104
+ ],
105
+ ALLOWED_ATTR: ['src', 'alt', 'title', 'href', 'style', 'class'],
106
+ FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'applet', 'form', 'input', 'style'],
107
+ FORBID_ATTR: ['onclick', 'onload', 'onerror', 'onmouseover'],
108
+ ALLOW_DATA_ATTR: false
109
+ };
110
+
111
+ /**
112
+ * Sanitizes HTML content based on variant and security level
113
+ * @param {string} html - HTML content to sanitize
114
+ * @param {string} variant - Editor variant (SANITIZER_VARIANTS.EMAIL or SANITIZER_VARIANTS.INAPP)
115
+ * @param {string} level - Security level (SECURITY_LEVELS.STANDARD or SECURITY_LEVELS.STRICT)
116
+ * @param {Function} formatMessage - Message formatter function for internationalization
117
+ * @returns {Object} Sanitization result with sanitized content, cleanliness status, removed elements, and warnings
118
+ */
119
+ export const sanitizeHTML = (html, variant = SANITIZER_VARIANTS.EMAIL, level = SECURITY_LEVELS.STANDARD, formatMessage = defaultMessageFormatter) => {
120
+ // Input validation with better error handling
121
+ if (html === null || html === undefined || typeof html !== 'string') {
122
+ return {
123
+ sanitized: '',
124
+ isClean: true,
125
+ removedElements: [],
126
+ warnings: html === null || html === undefined ? [] : [{
127
+ type: 'warning',
128
+ message: formatMessage('sanitizer.invalidInput'),
129
+ source: 'sanitizer'
130
+ }]
131
+ };
132
+ }
133
+
134
+ // Handle empty string as valid input (no warnings needed)
135
+ if (html === '') {
136
+ return {
137
+ sanitized: '',
138
+ isClean: true,
139
+ removedElements: [],
140
+ warnings: []
141
+ };
142
+ }
143
+
144
+ // Validate variant parameter
145
+ const validVariants = Object.values(SANITIZER_VARIANTS);
146
+ if (!validVariants.includes(variant)) {
147
+ console.warn(`contentSanitizer: Invalid variant "${variant}". Valid variants are: ${validVariants.join(', ')}`);
148
+ variant = SANITIZER_VARIANTS.EMAIL; // Fallback to email
149
+ }
150
+
151
+ // Validate level parameter
152
+ const validLevels = Object.values(SECURITY_LEVELS);
153
+ if (!validLevels.includes(level)) {
154
+ console.warn(`contentSanitizer: Invalid security level "${level}". Valid levels are: ${validLevels.join(', ')}`);
155
+ level = SECURITY_LEVELS.STANDARD; // Fallback to standard
156
+ }
157
+
158
+ const result = {
159
+ sanitized: '',
160
+ isClean: true,
161
+ removedElements: [],
162
+ warnings: []
163
+ };
164
+
165
+ try {
166
+ // Select configuration based on variant and level using constants
167
+ let config;
168
+ if (level === SECURITY_LEVELS.STRICT) {
169
+ config = STRICT_CONFIG;
170
+ } else {
171
+ config = variant === SANITIZER_VARIANTS.INAPP ? INAPP_CONFIG : EMAIL_CONFIG;
172
+ }
173
+
174
+ // Configure DOMPurify
175
+ const purifyConfig = {
176
+ ALLOWED_TAGS: config.ALLOWED_TAGS,
177
+ ALLOWED_ATTR: config.ALLOWED_ATTR,
178
+ FORBID_TAGS: config.FORBID_TAGS,
179
+ FORBID_ATTR: config.FORBID_ATTR,
180
+ ALLOW_DATA_ATTR: config.ALLOW_DATA_ATTR,
181
+ RETURN_DOM: false,
182
+ RETURN_DOM_FRAGMENT: false,
183
+ RETURN_DOM_IMPORT: false,
184
+ SANITIZE_DOM: true,
185
+ KEEP_CONTENT: true,
186
+ ADD_TAGS: [],
187
+ ADD_ATTR: [],
188
+ CUSTOM_ELEMENT_HANDLING: {
189
+ tagNameCheck: null,
190
+ attributeNameCheck: null,
191
+ allowCustomizedBuiltInElements: false
192
+ }
193
+ };
194
+
195
+ // Sanitize the content directly
196
+ result.sanitized = DOMPurify.sanitize(html, purifyConfig);
197
+
198
+ // Additional checks and warnings
199
+ if (result.sanitized !== html) {
200
+ result.isClean = false;
201
+ }
202
+
203
+ // Add variant-specific warnings (check original HTML before sanitization)
204
+ addVariantWarnings(html, variant, result, formatMessage);
205
+
206
+ } catch (error) {
207
+ result.warnings.push({
208
+ type: 'error',
209
+ message: formatMessage('sanitizer.sanitizationFailed', { error: error.message }),
210
+ source: 'sanitizer'
211
+ });
212
+ result.sanitized = ''; // Return empty content if sanitization fails
213
+ result.isClean = false;
214
+ }
215
+
216
+ return result;
217
+ };
218
+
219
+ /**
220
+ * Adds variant-specific warnings based on content analysis
221
+ * @param {string} html - HTML content to analyze
222
+ * @param {string} variant - Editor variant
223
+ * @param {Object} result - Result object to add warnings to
224
+ * @param {Function} formatMessage - Message formatter function
225
+ */
226
+ const addVariantWarnings = (html, variant, result, formatMessage = defaultMessageFormatter) => {
227
+ if (!html || typeof html !== 'string') return;
228
+
229
+ if (variant === SANITIZER_VARIANTS.EMAIL) {
230
+ // Check for potentially problematic email elements
231
+ const emailProblematicElements = ['<video', '<audio', '<canvas'];
232
+ if (emailProblematicElements.some(element => html.includes(element))) {
233
+ result.warnings.push({
234
+ type: 'warning',
235
+ message: formatMessage('sanitizer.emailMultimediaNotSupported'),
236
+ source: 'email-compatibility'
237
+ });
238
+ }
239
+
240
+ const problematicStyles = ['position: fixed', 'position: sticky', 'position:fixed', 'position:sticky'];
241
+ if (problematicStyles.some(style => html.includes(style))) {
242
+ result.warnings.push({
243
+ type: 'warning',
244
+ message: formatMessage('sanitizer.emailPositioningNotSupported'),
245
+ source: 'email-compatibility'
246
+ });
247
+ }
248
+
249
+ // Check for CSS Grid/Flexbox which may have limited email support
250
+ const modernCssFeatures = ['display: grid', 'display: flex', 'display:grid', 'display:flex'];
251
+ if (modernCssFeatures.some(feature => html.includes(feature))) {
252
+ result.warnings.push({
253
+ type: 'info',
254
+ message: formatMessage('sanitizer.emailModernCssLimited'),
255
+ source: 'email-compatibility'
256
+ });
257
+ }
258
+ } else if (variant === SANITIZER_VARIANTS.INAPP) {
259
+ // Check for potentially problematic mobile elements
260
+ if (html.includes('<table')) {
261
+ result.warnings.push({
262
+ type: 'info',
263
+ message: formatMessage('sanitizer.mobileTablesNotFriendly'),
264
+ source: 'mobile-optimization'
265
+ });
266
+ }
267
+
268
+ // Check for fixed pixel font sizes
269
+ if (html.includes('font-size') && /font-size:\s*[0-9]+px/g.test(html)) {
270
+ result.warnings.push({
271
+ type: 'info',
272
+ message: formatMessage('sanitizer.mobileRelativeFontSizes'),
273
+ source: 'mobile-optimization'
274
+ });
275
+ }
276
+
277
+ // Check for fixed pixel widths that might not be mobile-friendly
278
+ if (/width:\s*[0-9]{3,}px/g.test(html)) {
279
+ result.warnings.push({
280
+ type: 'info',
281
+ message: formatMessage('sanitizer.mobileFixedWidthsProblematic'),
282
+ source: 'mobile-optimization'
283
+ });
284
+ }
285
+ }
286
+ };
287
+
288
+ /**
289
+ * Validates and sanitizes content for production use
290
+ * @param {string} html - HTML content
291
+ * @param {string} variant - Editor variant (SANITIZER_VARIANTS.EMAIL or SANITIZER_VARIANTS.INAPP)
292
+ * @param {Function} formatMessage - Message formatter function for internationalization
293
+ * @returns {Object} Production-ready content with validation results and recommendations
294
+ */
295
+ export const prepareForProduction = (html, variant = SANITIZER_VARIANTS.EMAIL, formatMessage = defaultMessageFormatter) => {
296
+ // Input validation
297
+ if (!html || typeof html !== 'string') {
298
+ return {
299
+ content: '',
300
+ isProductionReady: false,
301
+ securityIssues: [],
302
+ warnings: [{
303
+ type: 'error',
304
+ message: formatMessage('sanitizer.invalidInputNonEmpty'),
305
+ source: 'production-validator'
306
+ }],
307
+ recommendations: [formatMessage('sanitizer.productionValidHtml')]
308
+ };
309
+ }
310
+
311
+ // First, sanitize with strict rules
312
+ const sanitizeResult = sanitizeHTML(html, variant, SECURITY_LEVELS.STRICT, formatMessage);
313
+
314
+ const result = {
315
+ content: sanitizeResult.sanitized,
316
+ isProductionReady: sanitizeResult.isClean,
317
+ securityIssues: sanitizeResult.removedElements,
318
+ warnings: sanitizeResult.warnings,
319
+ recommendations: []
320
+ };
321
+
322
+ // Add production readiness recommendations
323
+ if (!result.isProductionReady) {
324
+ result.recommendations.push(formatMessage('sanitizer.productionSanitized'));
325
+ }
326
+
327
+ if (variant === SANITIZER_VARIANTS.EMAIL && html.includes('<style>')) {
328
+ result.recommendations.push(formatMessage('sanitizer.productionInlineCss'));
329
+ }
330
+
331
+ if (variant === SANITIZER_VARIANTS.INAPP && html.length > CONTENT_LIMITS.LARGE_CONTENT_SIZE) {
332
+ result.recommendations.push(formatMessage('sanitizer.productionLargeContent', { size: html.length }));
333
+ }
334
+
335
+ // Additional variant-specific recommendations
336
+ if (variant === SANITIZER_VARIANTS.EMAIL) {
337
+ if (html.includes('rem') || html.includes('vh') || html.includes('vw')) {
338
+ result.recommendations.push(formatMessage('sanitizer.emailViewportUnitsNotSupported'));
339
+ }
340
+ } else if (variant === SANITIZER_VARIANTS.INAPP) {
341
+ if (html.includes('px') && html.length > CONTENT_LIMITS.LARGE_CONTENT_SIZE / 2) {
342
+ result.recommendations.push(formatMessage('sanitizer.mobileRelativeUnits'));
343
+ }
344
+ }
345
+
346
+ return result;
347
+ };
348
+
349
+ /**
350
+ * Quick security check for content
351
+ * @param {string} html - HTML content to check
352
+ * @returns {boolean} True if content appears safe
353
+ */
354
+ export const isContentSafe = (html) => {
355
+ if (!html || typeof html !== 'string') return true;
356
+
357
+ // Create dynamic patterns from constants
358
+ const protocolPatterns = DANGEROUS_PROTOCOLS.map(protocol =>
359
+ new RegExp(protocol.replace(':', '\\:'), 'gi')
360
+ );
361
+
362
+ const eventHandlerPattern = new RegExp(EVENT_HANDLERS.join('|'), 'gi');
363
+
364
+ const dangerousPatterns = [
365
+ ...protocolPatterns,
366
+ /<script/gi,
367
+ eventHandlerPattern,
368
+ /<iframe/gi,
369
+ /<object/gi,
370
+ /<embed/gi,
371
+ /<applet/gi,
372
+ /<form/gi
373
+ ];
374
+
375
+ return !dangerousPatterns.some(pattern => pattern.test(html));
376
+ };
377
+
378
+ /**
379
+ * Extracts potentially unsafe content for review
380
+ * @param {string} html - HTML content to analyze
381
+ * @returns {Array} List of unsafe patterns found with details
382
+ */
383
+ export const findUnsafeContent = (html) => {
384
+ if (!html || typeof html !== 'string') return [];
385
+
386
+ const unsafeContent = [];
387
+
388
+ // Build patterns dynamically from constants
389
+ const patterns = {
390
+ 'JavaScript Protocol': new RegExp(DANGEROUS_PROTOCOLS[0].replace(':', '\\:'), 'gi'),
391
+ 'Data URL': new RegExp(DANGEROUS_PROTOCOLS[1].replace(':', '\\:'), 'gi'),
392
+ 'VBScript Protocol': new RegExp(DANGEROUS_PROTOCOLS[2].replace(':', '\\:'), 'gi'),
393
+ 'Script Tag': /<script[^>]*>/gi,
394
+ 'Event Handler': new RegExp(`(${EVENT_HANDLERS.join('|')})\\s*=`, 'gi'),
395
+ 'Iframe': /<iframe[^>]*>/gi,
396
+ 'Object/Embed': /<(object|embed)[^>]*>/gi,
397
+ 'Applet': /<applet[^>]*>/gi,
398
+ 'Form': /<form[^>]*>/gi
399
+ };
400
+
401
+ Object.entries(patterns).forEach(([name, pattern]) => {
402
+ let match;
403
+ // Use exec in a loop to find all matches with positions
404
+ while ((match = pattern.exec(html)) !== null) {
405
+ unsafeContent.push({
406
+ type: name,
407
+ content: match[0],
408
+ position: match.index,
409
+ length: match[0].length
410
+ });
411
+
412
+ // Prevent infinite loop for global patterns
413
+ if (!pattern.global) break;
414
+ }
415
+ });
416
+
417
+ // Sort by position for easier review
418
+ return unsafeContent.sort((a, b) => a.position - b.position);
419
+ };
420
+
421
+ // Export constants for external use
422
+ export { SANITIZER_VARIANTS, SECURITY_LEVELS, CONTENT_LIMITS };
423
+
424
+ export default {
425
+ sanitizeHTML,
426
+ prepareForProduction,
427
+ isContentSafe,
428
+ findUnsafeContent,
429
+ // Include constants in default export for convenience
430
+ VARIANTS: SANITIZER_VARIANTS,
431
+ SECURITY_LEVELS,
432
+ CONTENT_LIMITS
433
+ };