@capillarytech/creatives-library 8.0.271 → 8.0.272
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/constants/unified.js +2 -1
- package/initialReducer.js +2 -0
- package/package.json +1 -1
- package/services/api.js +10 -0
- package/services/tests/api.test.js +34 -0
- package/tests/integration/TemplateCreation/TemplateCreation.integration.test.js +17 -35
- package/tests/integration/TemplateCreation/api-response.js +31 -1
- package/tests/integration/TemplateCreation/msw-handler.js +2 -0
- package/utils/common.js +5 -0
- package/utils/commonUtils.js +28 -5
- package/utils/tests/commonUtil.test.js +224 -0
- package/utils/transformTemplateConfig.js +0 -10
- package/v2Components/CapDeviceContent/index.js +61 -56
- package/v2Components/CapTagList/index.js +6 -1
- package/v2Components/CapTagListWithInput/index.js +5 -1
- package/v2Components/CapTagListWithInput/messages.js +1 -1
- package/v2Components/CapWhatsappCTA/tests/index.test.js +5 -0
- package/v2Components/ErrorInfoNote/constants.js +1 -0
- package/v2Components/ErrorInfoNote/index.js +402 -72
- package/v2Components/ErrorInfoNote/messages.js +32 -6
- package/v2Components/ErrorInfoNote/style.scss +278 -6
- package/v2Components/FormBuilder/tests/index.test.js +13 -4
- package/v2Components/HtmlEditor/HTMLEditor.js +418 -99
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +870 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1882 -133
- package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +27 -16
- package/v2Components/HtmlEditor/_htmlEditor.scss +108 -45
- package/v2Components/HtmlEditor/_index.lazy.scss +0 -1
- package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +23 -102
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +148 -140
- package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +2 -1
- package/v2Components/HtmlEditor/components/DeviceToggle/index.js +3 -3
- package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +9 -1
- package/v2Components/HtmlEditor/components/EditorToolbar/index.js +31 -6
- package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +22 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +4 -7
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +35 -45
- package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +1 -3
- package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +33 -33
- package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +7 -6
- package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +7 -10
- package/v2Components/HtmlEditor/components/PreviewPane/index.js +22 -43
- package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +1 -1
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +18 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +36 -31
- package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +46 -34
- package/v2Components/HtmlEditor/components/ValidationPanel/constants.js +6 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/index.js +52 -46
- package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +277 -0
- package/v2Components/HtmlEditor/components/ValidationTabs/index.js +295 -0
- package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +51 -0
- package/v2Components/HtmlEditor/constants.js +45 -20
- package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +373 -16
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +351 -16
- package/v2Components/HtmlEditor/hooks/useEditorContent.js +5 -2
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +88 -146
- package/v2Components/HtmlEditor/hooks/useValidation.js +213 -56
- package/v2Components/HtmlEditor/index.js +1 -1
- package/v2Components/HtmlEditor/messages.js +102 -94
- package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +214 -45
- package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +134 -0
- package/v2Components/HtmlEditor/utils/contentSanitizer.js +40 -41
- package/v2Components/HtmlEditor/utils/htmlValidator.js +71 -72
- package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +158 -124
- package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +23 -25
- package/v2Components/HtmlEditor/utils/validationAdapter.js +66 -41
- package/v2Components/HtmlEditor/utils/validationConstants.js +38 -0
- package/v2Components/MobilePushPreviewV2/constants.js +6 -0
- package/v2Components/MobilePushPreviewV2/index.js +33 -7
- package/v2Components/TemplatePreview/_templatePreview.scss +55 -24
- package/v2Components/TemplatePreview/index.js +47 -32
- package/v2Components/TemplatePreview/messages.js +4 -0
- package/v2Components/TestAndPreviewSlidebox/_testAndPreviewSlidebox.scss +1 -0
- package/v2Containers/BeeEditor/index.js +172 -90
- package/v2Containers/BeePopupEditor/_beePopupEditor.scss +14 -0
- package/v2Containers/BeePopupEditor/constants.js +10 -0
- package/v2Containers/BeePopupEditor/index.js +194 -0
- package/v2Containers/BeePopupEditor/tests/index.test.js +627 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +127 -51
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +156 -13
- package/v2Containers/CreativesContainer/SlideBoxHeader.js +2 -1
- package/v2Containers/CreativesContainer/constants.js +1 -0
- package/v2Containers/CreativesContainer/index.js +251 -47
- package/v2Containers/CreativesContainer/messages.js +8 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +11 -2
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +38 -50
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +103 -0
- package/v2Containers/Email/actions.js +7 -0
- package/v2Containers/Email/constants.js +5 -1
- package/v2Containers/Email/index.js +234 -29
- package/v2Containers/Email/messages.js +32 -0
- package/v2Containers/Email/reducer.js +12 -1
- package/v2Containers/Email/sagas.js +61 -7
- package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +2 -0
- package/v2Containers/Email/tests/reducer.test.js +46 -0
- package/v2Containers/Email/tests/sagas.test.js +320 -29
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +1246 -0
- package/v2Containers/EmailWrapper/components/EmailWrapperView.js +212 -21
- package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +40 -74
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +2472 -0
- package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +520 -0
- package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +2 -67
- package/v2Containers/EmailWrapper/constants.js +2 -0
- package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +627 -79
- package/v2Containers/EmailWrapper/index.js +103 -23
- package/v2Containers/EmailWrapper/messages.js +65 -1
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +955 -0
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +596 -82
- package/v2Containers/InApp/__tests__/InAppHTMLEditor.test.js +376 -0
- package/v2Containers/InApp/__tests__/sagas.test.js +363 -0
- package/v2Containers/InApp/actions.js +7 -0
- package/v2Containers/InApp/constants.js +20 -4
- package/v2Containers/InApp/index.js +802 -360
- package/v2Containers/InApp/index.scss +4 -3
- package/v2Containers/InApp/messages.js +7 -3
- package/v2Containers/InApp/reducer.js +21 -3
- package/v2Containers/InApp/sagas.js +29 -9
- package/v2Containers/InApp/selectors.js +25 -5
- package/v2Containers/InApp/tests/index.test.js +154 -50
- package/v2Containers/InApp/tests/reducer.test.js +34 -0
- package/v2Containers/InApp/tests/sagas.test.js +61 -9
- package/v2Containers/InApp/tests/selectors.test.js +612 -0
- package/v2Containers/InAppWrapper/components/InAppWrapperView.js +151 -0
- package/v2Containers/InAppWrapper/components/__tests__/InAppWrapperView.test.js +267 -0
- package/v2Containers/InAppWrapper/components/inAppWrapperView.scss +23 -0
- package/v2Containers/InAppWrapper/constants.js +16 -0
- package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +473 -0
- package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +198 -0
- package/v2Containers/InAppWrapper/index.js +148 -0
- package/v2Containers/InAppWrapper/messages.js +49 -0
- package/v2Containers/InappAdvance/index.js +1099 -0
- package/v2Containers/InappAdvance/index.scss +10 -0
- package/v2Containers/InappAdvance/tests/index.test.js +448 -0
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +3 -0
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +2 -0
- package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +2 -0
- package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +9 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +12 -0
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4 -0
- package/v2Containers/TagList/index.js +62 -19
- package/v2Containers/Templates/_templates.scss +60 -1
- package/v2Containers/Templates/index.js +89 -4
- package/v2Containers/Templates/messages.js +4 -0
- package/v2Containers/TemplatesV2/TemplatesV2.style.js +4 -2
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +0 -152
- package/v2Containers/EmailWrapper/tests/EmailWrapperView.test.js +0 -214
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Validation Hook for HTML Editor
|
|
3
|
-
* Manages real-time validation and error display
|
|
3
|
+
* Manages real-time validation and error display.
|
|
4
|
+
* UI gating: only Rule Group #1 (Input & Sanitization) blocks Save/Update/Preview/Test.
|
|
5
|
+
* All other rules are warnings for backward compatibility with CKEditor legacy templates.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
useState, useEffect, useCallback, useRef,
|
|
10
|
+
} from 'react';
|
|
7
11
|
import { validateHTML, extractAndValidateCSS } from '../utils/htmlValidator';
|
|
8
12
|
import { sanitizeHTML, isContentSafe, findUnsafeContent } from '../utils/contentSanitizer';
|
|
13
|
+
import { BLOCKING_ERROR_RULE_IDS, VALIDATION_SEVERITY } from '../constants';
|
|
14
|
+
import { ISSUE_SOURCES } from '../utils/validationConstants';
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
17
|
* Custom hook for managing HTML/CSS validation
|
|
@@ -16,12 +22,61 @@ import { sanitizeHTML, isContentSafe, findUnsafeContent } from '../utils/content
|
|
|
16
22
|
* @param {Function} formatValidatorMessage - Message formatter function for validator internationalization
|
|
17
23
|
* @returns {Object} Validation state and methods
|
|
18
24
|
*/
|
|
25
|
+
/**
|
|
26
|
+
* Get line number for a character position in text
|
|
27
|
+
*/
|
|
28
|
+
const getLineNumberFromPosition = (text, position) => {
|
|
29
|
+
if (position === undefined || position < 0) return 1;
|
|
30
|
+
return text.substring(0, position).split('\n').length;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get 1-based line and column from a character position in text
|
|
35
|
+
*/
|
|
36
|
+
const getLineAndColumnFromPosition = (text, position) => {
|
|
37
|
+
if (!text || position === undefined || position < 0) {
|
|
38
|
+
return { line: 1, column: 1 };
|
|
39
|
+
}
|
|
40
|
+
const before = text.substring(0, position);
|
|
41
|
+
const lines = before.split('\n');
|
|
42
|
+
const line = lines.length;
|
|
43
|
+
const lastLine = lines[lines.length - 1] || '';
|
|
44
|
+
const column = lastLine.length + 1;
|
|
45
|
+
return { line, column };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Find line number for a tag or pattern in content
|
|
50
|
+
* Used for API errors to locate where the error occurs
|
|
51
|
+
*/
|
|
52
|
+
const findLineNumberForTag = (content, tagName) => {
|
|
53
|
+
if (!content || !tagName) return null;
|
|
54
|
+
|
|
55
|
+
// Try to find the tag in the content
|
|
56
|
+
// Look for patterns like {{ tagName }}, {{tagName}}, etc.
|
|
57
|
+
const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
58
|
+
const patterns = [
|
|
59
|
+
new RegExp(`\\{\\{\\s*${escapedTagName}\\s*\\}\\}`, 'g'),
|
|
60
|
+
new RegExp(`\\{%[^%]*${escapedTagName}[^%]*%\\}`, 'g'),
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
for (const pattern of patterns) {
|
|
64
|
+
const match = pattern.exec(content);
|
|
65
|
+
if (match) {
|
|
66
|
+
return getLineNumberFromPosition(content, match.index);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
};
|
|
72
|
+
|
|
19
73
|
export const useValidation = (content, variant = 'email', options = {}, formatSanitizerMessage = null, formatValidatorMessage = null) => {
|
|
20
74
|
const {
|
|
21
75
|
enableRealTime = true,
|
|
22
76
|
debounceMs = 500,
|
|
23
77
|
enableSanitization = true,
|
|
24
|
-
securityLevel = 'standard'
|
|
78
|
+
securityLevel = 'standard',
|
|
79
|
+
apiValidationErrors = null, // API validation errors from validateLiquidTemplateContent
|
|
25
80
|
} = options;
|
|
26
81
|
|
|
27
82
|
// Validation state
|
|
@@ -42,8 +97,8 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
42
97
|
totalErrors: 0,
|
|
43
98
|
totalWarnings: 0,
|
|
44
99
|
totalInfo: 0,
|
|
45
|
-
hasSecurityIssues: false
|
|
46
|
-
}
|
|
100
|
+
hasSecurityIssues: false,
|
|
101
|
+
},
|
|
47
102
|
});
|
|
48
103
|
|
|
49
104
|
// Refs for debouncing
|
|
@@ -55,7 +110,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
55
110
|
*/
|
|
56
111
|
const performValidation = useCallback(async (htmlContent) => {
|
|
57
112
|
if (!htmlContent) {
|
|
58
|
-
setValidationState(prev => ({
|
|
113
|
+
setValidationState((prev) => ({
|
|
59
114
|
...prev,
|
|
60
115
|
isValidating: false,
|
|
61
116
|
htmlErrors: [],
|
|
@@ -72,13 +127,13 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
72
127
|
totalErrors: 0,
|
|
73
128
|
totalWarnings: 0,
|
|
74
129
|
totalInfo: 0,
|
|
75
|
-
hasSecurityIssues: false
|
|
76
|
-
}
|
|
130
|
+
hasSecurityIssues: false,
|
|
131
|
+
},
|
|
77
132
|
}));
|
|
78
133
|
return;
|
|
79
134
|
}
|
|
80
135
|
|
|
81
|
-
setValidationState(prev => ({ ...prev, isValidating: true }));
|
|
136
|
+
setValidationState((prev) => ({ ...prev, isValidating: true }));
|
|
82
137
|
|
|
83
138
|
try {
|
|
84
139
|
// 1. HTML Validation
|
|
@@ -91,15 +146,21 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
91
146
|
const isSecure = isContentSafe(htmlContent);
|
|
92
147
|
const securityIssues = isSecure ? [] : findUnsafeContent(htmlContent);
|
|
93
148
|
|
|
94
|
-
// 4. Sanitization (if enabled)
|
|
149
|
+
// 4. Sanitization (if enabled) – Rule Group #1 issues come from here
|
|
95
150
|
let sanitizationResult = null;
|
|
96
151
|
if (enableSanitization) {
|
|
97
152
|
sanitizationResult = sanitizeHTML(htmlContent, variant, securityLevel, formatSanitizerMessage);
|
|
98
153
|
}
|
|
99
154
|
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
const
|
|
155
|
+
const sanitizationWarnings = sanitizationResult?.warnings || [];
|
|
156
|
+
const blockingSanitizerCount = sanitizationWarnings.filter((w) => BLOCKING_ERROR_RULE_IDS.includes(w.rule)).length;
|
|
157
|
+
const protocolSecurityCount = (securityIssues || []).filter((s) => ['JavaScript Protocol', 'Data URL', 'VBScript Protocol'].includes(s?.type)).length;
|
|
158
|
+
|
|
159
|
+
// Summary: totalErrors/totalWarnings are for display; blocking count is for gating
|
|
160
|
+
const totalErrors = htmlValidation.errors.length + cssValidation.errors.length + blockingSanitizerCount + protocolSecurityCount;
|
|
161
|
+
const totalWarnings = htmlValidation.warnings.length + cssValidation.warnings.length
|
|
162
|
+
+ (sanitizationWarnings.length - blockingSanitizerCount)
|
|
163
|
+
+ (securityIssues.length - protocolSecurityCount);
|
|
103
164
|
const totalInfo = htmlValidation.info.length + cssValidation.info.length;
|
|
104
165
|
const hasSecurityIssues = securityIssues.length > 0;
|
|
105
166
|
|
|
@@ -114,20 +175,19 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
114
175
|
cssWarnings: cssValidation.warnings,
|
|
115
176
|
cssInfo: cssValidation.info,
|
|
116
177
|
securityIssues,
|
|
117
|
-
sanitizationWarnings
|
|
178
|
+
sanitizationWarnings,
|
|
118
179
|
isValid: htmlValidation.isValid && cssValidation.isValid,
|
|
119
180
|
isSecure,
|
|
120
181
|
summary: {
|
|
121
182
|
totalErrors,
|
|
122
183
|
totalWarnings,
|
|
123
184
|
totalInfo,
|
|
124
|
-
hasSecurityIssues
|
|
125
|
-
}
|
|
185
|
+
hasSecurityIssues,
|
|
186
|
+
},
|
|
126
187
|
});
|
|
127
|
-
|
|
128
188
|
} catch (error) {
|
|
129
189
|
console.error('Validation error:', error);
|
|
130
|
-
setValidationState(prev => ({
|
|
190
|
+
setValidationState((prev) => ({
|
|
131
191
|
...prev,
|
|
132
192
|
isValidating: false,
|
|
133
193
|
htmlErrors: [{
|
|
@@ -137,13 +197,13 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
137
197
|
column: 1,
|
|
138
198
|
rule: 'validation-error',
|
|
139
199
|
severity: 'error',
|
|
140
|
-
source: 'validator'
|
|
200
|
+
source: 'validator',
|
|
141
201
|
}],
|
|
142
202
|
isValid: false,
|
|
143
203
|
summary: {
|
|
144
204
|
...prev.summary,
|
|
145
|
-
totalErrors: 1
|
|
146
|
-
}
|
|
205
|
+
totalErrors: 1,
|
|
206
|
+
},
|
|
147
207
|
}));
|
|
148
208
|
}
|
|
149
209
|
}, [variant, enableSanitization, securityLevel, formatSanitizerMessage, formatValidatorMessage]);
|
|
@@ -208,8 +268,8 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
208
268
|
totalErrors: 0,
|
|
209
269
|
totalWarnings: 0,
|
|
210
270
|
totalInfo: 0,
|
|
211
|
-
hasSecurityIssues: false
|
|
212
|
-
}
|
|
271
|
+
hasSecurityIssues: false,
|
|
272
|
+
},
|
|
213
273
|
});
|
|
214
274
|
}, []);
|
|
215
275
|
|
|
@@ -223,10 +283,10 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
223
283
|
...validationState.htmlInfo,
|
|
224
284
|
...validationState.cssErrors,
|
|
225
285
|
...validationState.cssWarnings,
|
|
226
|
-
...validationState.cssInfo
|
|
286
|
+
...validationState.cssInfo,
|
|
227
287
|
];
|
|
228
288
|
|
|
229
|
-
return allErrors.filter(error => error.severity === severity);
|
|
289
|
+
return allErrors.filter((error) => error.severity === severity);
|
|
230
290
|
}, [validationState]);
|
|
231
291
|
|
|
232
292
|
/**
|
|
@@ -239,32 +299,116 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
239
299
|
...validationState.htmlInfo,
|
|
240
300
|
...validationState.cssErrors,
|
|
241
301
|
...validationState.cssWarnings,
|
|
242
|
-
...validationState.cssInfo
|
|
302
|
+
...validationState.cssInfo,
|
|
243
303
|
];
|
|
244
304
|
|
|
245
|
-
return allErrors.filter(error => error.source === source);
|
|
305
|
+
return allErrors.filter((error) => error.source === source);
|
|
246
306
|
}, [validationState]);
|
|
247
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Extract line number from error message if present
|
|
310
|
+
* API errors might contain line numbers in messages like "Error at line 5" or "Line 10: error"
|
|
311
|
+
* Also tries to find line number by searching for the problematic tag in content
|
|
312
|
+
*/
|
|
313
|
+
const extractLineNumberFromMessage = useCallback((message) => {
|
|
314
|
+
if (!message || typeof message !== 'string') {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
// Try to match patterns like "line 5", "Line 10", "at line 15", "line: 20", etc.
|
|
318
|
+
const lineMatch = message.match(/\b(?:line|Line|LINE)\s*:?\s*(\d+)\b/i);
|
|
319
|
+
if (lineMatch && lineMatch[1]) {
|
|
320
|
+
return parseInt(lineMatch[1], 10);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Try to extract tag name from error message (e.g., "Unsupported tags: test" -> "test")
|
|
324
|
+
const tagMatch = message.match(/(?:tag|tags|Tag|Tags)[\s:]+([a-zA-Z_][a-zA-Z0-9_.]*)/i);
|
|
325
|
+
if (tagMatch && tagMatch[1] && content) {
|
|
326
|
+
const tagName = tagMatch[1];
|
|
327
|
+
const lineNumber = findLineNumberForTag(content, tagName);
|
|
328
|
+
if (lineNumber) {
|
|
329
|
+
return lineNumber;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return null;
|
|
334
|
+
}, [content]);
|
|
335
|
+
|
|
248
336
|
/**
|
|
249
337
|
* Get all errors and warnings combined
|
|
338
|
+
* Includes both client-side validation errors and API validation errors
|
|
250
339
|
*/
|
|
251
340
|
const getAllIssues = useCallback(() => {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
341
|
+
// API errors (liquid + standard) are blocking – they block Save/Update/Preview/Test
|
|
342
|
+
const apiLiquidErrors = (apiValidationErrors?.liquidErrors || []).map((errorMessage) => {
|
|
343
|
+
const extractedLine = extractLineNumberFromMessage(errorMessage);
|
|
344
|
+
return {
|
|
345
|
+
type: VALIDATION_SEVERITY.ERROR,
|
|
346
|
+
message: errorMessage,
|
|
347
|
+
line: extractedLine,
|
|
348
|
+
column: null,
|
|
349
|
+
rule: 'liquid-api-validation',
|
|
350
|
+
severity: VALIDATION_SEVERITY.ERROR,
|
|
351
|
+
source: ISSUE_SOURCES.LIQUID,
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const apiStandardErrors = (apiValidationErrors?.standardErrors || []).map((errorMessage) => {
|
|
356
|
+
const extractedLine = extractLineNumberFromMessage(errorMessage);
|
|
357
|
+
return {
|
|
358
|
+
type: VALIDATION_SEVERITY.ERROR,
|
|
359
|
+
message: errorMessage,
|
|
360
|
+
line: extractedLine,
|
|
361
|
+
column: null,
|
|
362
|
+
rule: 'standard-api-validation',
|
|
363
|
+
severity: VALIDATION_SEVERITY.ERROR,
|
|
364
|
+
source: 'api-validator',
|
|
365
|
+
};
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Security: protocol types are Rule Group #1 (blocking); others are warnings
|
|
369
|
+
// Use issue.position (from findUnsafeContent) to show real line/column when available
|
|
370
|
+
const PROTOCOL_TYPES = ['JavaScript Protocol', 'Data URL', 'VBScript Protocol'];
|
|
371
|
+
const contentStr = typeof content === 'string' ? content : '';
|
|
372
|
+
const securityAsIssues = (validationState.securityIssues || []).map((issue) => {
|
|
373
|
+
const isBlocking = PROTOCOL_TYPES.includes(issue?.type);
|
|
374
|
+
const { line, column } = (issue?.position !== undefined && contentStr)
|
|
375
|
+
? getLineAndColumnFromPosition(contentStr, issue.position)
|
|
376
|
+
: { line: 1, column: 1 };
|
|
377
|
+
return {
|
|
378
|
+
type: isBlocking ? VALIDATION_SEVERITY.ERROR : VALIDATION_SEVERITY.WARNING,
|
|
261
379
|
message: `Security issue: ${issue.type}`,
|
|
262
|
-
line
|
|
263
|
-
column
|
|
264
|
-
rule: 'security-violation',
|
|
265
|
-
severity:
|
|
266
|
-
source: 'security'
|
|
267
|
-
}
|
|
380
|
+
line,
|
|
381
|
+
column,
|
|
382
|
+
rule: isBlocking ? 'sanitizer.dangerousProtocolDetected' : 'security-violation',
|
|
383
|
+
severity: isBlocking ? VALIDATION_SEVERITY.ERROR : VALIDATION_SEVERITY.WARNING,
|
|
384
|
+
source: 'security',
|
|
385
|
+
};
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Sanitization warnings (Rule Group #1 entries have rule set by contentSanitizer)
|
|
389
|
+
const sanitizationAsIssues = (validationState.sanitizationWarnings || []).map((w) => {
|
|
390
|
+
const sev = BLOCKING_ERROR_RULE_IDS.includes(w.rule) ? VALIDATION_SEVERITY.ERROR : VALIDATION_SEVERITY.WARNING;
|
|
391
|
+
return {
|
|
392
|
+
...w,
|
|
393
|
+
severity: sev,
|
|
394
|
+
rule: w.rule || 'sanitizer.unknown',
|
|
395
|
+
line: w.line ?? 1,
|
|
396
|
+
column: w.column ?? 1,
|
|
397
|
+
source: w.source || 'sanitizer',
|
|
398
|
+
};
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const allIssues = [
|
|
402
|
+
...(validationState.htmlErrors || []),
|
|
403
|
+
...(validationState.htmlWarnings || []),
|
|
404
|
+
...(validationState.htmlInfo || []),
|
|
405
|
+
...(validationState.cssErrors || []),
|
|
406
|
+
...(validationState.cssWarnings || []),
|
|
407
|
+
...(validationState.cssInfo || []),
|
|
408
|
+
...securityAsIssues,
|
|
409
|
+
...sanitizationAsIssues,
|
|
410
|
+
...apiLiquidErrors,
|
|
411
|
+
...apiStandardErrors,
|
|
268
412
|
].sort((a, b) => {
|
|
269
413
|
// Sort by severity (error > warning > info) then by line number
|
|
270
414
|
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
@@ -273,16 +417,22 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
273
417
|
}
|
|
274
418
|
return (a.line || 0) - (b.line || 0);
|
|
275
419
|
});
|
|
276
|
-
|
|
420
|
+
|
|
421
|
+
// Ensure we always return an array
|
|
422
|
+
return Array.isArray(allIssues) ? allIssues : [];
|
|
423
|
+
}, [validationState, apiValidationErrors, extractLineNumberFromMessage, content]);
|
|
277
424
|
|
|
278
425
|
/**
|
|
279
426
|
* Check if validation is clean (no errors or warnings)
|
|
427
|
+
* Includes API validation errors in the check
|
|
280
428
|
*/
|
|
281
429
|
const isClean = useCallback(() => {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
430
|
+
const hasApiErrors = (apiValidationErrors?.liquidErrors?.length || 0) + (apiValidationErrors?.standardErrors?.length || 0) > 0;
|
|
431
|
+
return validationState.summary.totalErrors === 0
|
|
432
|
+
&& validationState.summary.totalWarnings === 0
|
|
433
|
+
&& !validationState.summary.hasSecurityIssues
|
|
434
|
+
&& !hasApiErrors;
|
|
435
|
+
}, [validationState.summary, apiValidationErrors]);
|
|
286
436
|
|
|
287
437
|
// Effect to validate content when it changes
|
|
288
438
|
useEffect(() => {
|
|
@@ -292,14 +442,19 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
292
442
|
}, [content, validateContent, enableRealTime]);
|
|
293
443
|
|
|
294
444
|
// Cleanup on unmount
|
|
295
|
-
useEffect(() => {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
};
|
|
445
|
+
useEffect(() => () => {
|
|
446
|
+
if (debounceRef.current) {
|
|
447
|
+
clearTimeout(debounceRef.current);
|
|
448
|
+
}
|
|
301
449
|
}, []);
|
|
302
450
|
|
|
451
|
+
const hasApiErrors = (apiValidationErrors?.liquidErrors?.length || 0) + (apiValidationErrors?.standardErrors?.length || 0) > 0;
|
|
452
|
+
|
|
453
|
+
const protocolTypes = ['JavaScript Protocol', 'Data URL', 'VBScript Protocol'];
|
|
454
|
+
// Client-side Liquid validation errors are blocking (genuine syntax errors)
|
|
455
|
+
const hasClientSideLiquidErrors = (validationState.htmlErrors || []).some((e) => e.source === ISSUE_SOURCES.LIQUID && e.severity === VALIDATION_SEVERITY.ERROR);
|
|
456
|
+
const hasBlockingErrors = (validationState.sanitizationWarnings || []).some((w) => BLOCKING_ERROR_RULE_IDS.includes(w.rule)) || (validationState.securityIssues || []).some((s) => protocolTypes.includes(s?.type)) || hasApiErrors || hasClientSideLiquidErrors;
|
|
457
|
+
|
|
303
458
|
return {
|
|
304
459
|
// Validation state
|
|
305
460
|
...validationState,
|
|
@@ -316,10 +471,12 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
316
471
|
isClean,
|
|
317
472
|
|
|
318
473
|
// Computed properties
|
|
319
|
-
hasErrors: validationState.summary.totalErrors > 0,
|
|
474
|
+
hasErrors: validationState.summary.totalErrors > 0 || hasApiErrors,
|
|
320
475
|
hasWarnings: validationState.summary.totalWarnings > 0,
|
|
321
|
-
hasSecurityIssues: validationState.summary.hasSecurityIssues
|
|
476
|
+
hasSecurityIssues: validationState.summary.hasSecurityIssues,
|
|
477
|
+
/** True only when Rule Group #1 (Input & Sanitization) issues exist. Use for UI gating. */
|
|
478
|
+
hasBlockingErrors,
|
|
322
479
|
};
|
|
323
480
|
};
|
|
324
481
|
|
|
325
|
-
export default useValidation;
|
|
482
|
+
export default useValidation;
|