@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.
- package/assets/Android.png +0 -0
- package/assets/iOS.png +0 -0
- package/package.json +16 -2
- package/v2Components/HtmlEditor/HTMLEditor.js +508 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1809 -0
- package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +532 -0
- package/v2Components/HtmlEditor/_htmlEditor.scss +304 -0
- package/v2Components/HtmlEditor/_index.lazy.scss +26 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +376 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +331 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/__tests__/index.test.js +314 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +244 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/index.js +111 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/PreviewModeGroup.js +72 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/__tests__/PreviewModeGroup.test.js +1594 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +113 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/_previewModeGroup.scss +82 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/index.js +115 -0
- package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +57 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/ContentOverlay.js +90 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +60 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/LayoutSelector.js +58 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/ContentOverlay.test.js +403 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +424 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/LayoutSelector.test.js +248 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +253 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +104 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +179 -0
- package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +220 -0
- package/v2Components/HtmlEditor/components/PreviewPane/index.js +229 -0
- package/v2Components/HtmlEditor/components/SplitContainer/SplitContainer.js +276 -0
- package/v2Components/HtmlEditor/components/SplitContainer/__tests__/SplitContainer.test.js +295 -0
- package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +257 -0
- package/v2Components/HtmlEditor/components/SplitContainer/index.js +7 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +31 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +70 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/__tests__/index.test.js +98 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +311 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/index.js +297 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/messages.js +57 -0
- package/v2Components/HtmlEditor/components/common/EditorContext.js +84 -0
- package/v2Components/HtmlEditor/components/common/__tests__/EditorContext.test.js +660 -0
- package/v2Components/HtmlEditor/constants.js +241 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useEditorContent.test.js +450 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +785 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useLayoutState.test.js +580 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.enhanced.test.js +768 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +590 -0
- package/v2Components/HtmlEditor/hooks/useEditorContent.js +274 -0
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +407 -0
- package/v2Components/HtmlEditor/hooks/useLayoutState.js +247 -0
- package/v2Components/HtmlEditor/hooks/useValidation.js +325 -0
- package/v2Components/HtmlEditor/index.js +29 -0
- package/v2Components/HtmlEditor/index.lazy.js +114 -0
- package/v2Components/HtmlEditor/messages.js +389 -0
- package/v2Components/HtmlEditor/utils/__tests__/contentSanitizer.test.js +741 -0
- package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +1042 -0
- package/v2Components/HtmlEditor/utils/__tests__/liquidTemplateSupport.test.js +515 -0
- package/v2Components/HtmlEditor/utils/__tests__/properSyntaxHighlighting.test.js +473 -0
- package/v2Components/HtmlEditor/utils/__tests__/simplePerformance.test.js +1109 -0
- package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +240 -0
- package/v2Components/HtmlEditor/utils/contentSanitizer.js +433 -0
- package/v2Components/HtmlEditor/utils/htmlValidator.js +508 -0
- package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +524 -0
- package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +163 -0
- package/v2Components/HtmlEditor/utils/simplePerformance.js +145 -0
- package/v2Components/HtmlEditor/utils/validationAdapter.js +130 -0
- package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +200 -0
- package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +545 -0
- package/v2Containers/EmailWrapper/index.js +8 -1
- package/v2Containers/Rcs/index.js +2 -0
- package/v2Containers/Templates/constants.js +8 -0
- package/v2Containers/Templates/index.js +56 -28
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -14
- 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
|
+
};
|