@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,220 @@
1
+ /**
2
+ * Preview Pane Styles
3
+ */
4
+
5
+ @import '~@capillarytech/cap-ui-library/styles/_variables.scss';
6
+
7
+ .preview-pane {
8
+ height: 100%;
9
+ max-height: 31.25rem;
10
+ position: relative;
11
+ background-color: $CAP_G09;
12
+ padding: 1rem;
13
+ overflow: auto;
14
+
15
+ &__mode-controls {
16
+ position: absolute;
17
+ top: 0.5rem;
18
+ right: 0.5rem;
19
+ z-index: 10;
20
+ background-color: $CAP_G07;
21
+ border-radius: 0.5rem;
22
+ padding: 0.25rem;
23
+ display: flex;
24
+ gap: 0.25rem;
25
+ align-items: center;
26
+
27
+ .preview-mode-group {
28
+ margin: 0;
29
+
30
+ .ant-btn {
31
+ border: none;
32
+ box-shadow: none;
33
+ padding: 0.25rem;
34
+ min-width: 1.75rem;
35
+ height: 1.75rem;
36
+ border-radius: 0.25rem;
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+
41
+ &.ant-btn-primary {
42
+ background-color: $CAP_WHITE;
43
+ color: $CAP_G01;
44
+
45
+ .anticon {
46
+ color: $CAP_G01;
47
+ font-size: 1.25rem;
48
+ }
49
+ }
50
+
51
+ &:not(.ant-btn-primary) {
52
+ background-color: transparent;
53
+ color: $CAP_G04;
54
+
55
+ .anticon {
56
+ color: $CAP_G04;
57
+ font-size: 1.25rem;
58
+ }
59
+
60
+ &:hover,
61
+ &.preview-mode-group__btn--active {
62
+ background-color: rgba($CAP_WHITE, 0.5);
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ &__content {
70
+ height: calc(100% - 2rem);
71
+ max-height: 28.125rem;
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ position: relative;
76
+ overflow: auto;
77
+ }
78
+
79
+ &__iframe {
80
+ border: none;
81
+ border-radius: 0.25rem;
82
+ box-shadow: 0 0.0625rem 0.1875rem rgba($CAP_G01, 0.1);
83
+ background-color: $CAP_WHITE;
84
+ width: 100%;
85
+ height: 28.125rem;
86
+ max-height: 31.25rem;
87
+
88
+ &--mobile {
89
+ // Width and height now controlled by React inline styles
90
+ border-radius: 0.75rem;
91
+ box-shadow: 0 0.25rem 1.25rem rgba($CAP_G01, 0.1);
92
+ }
93
+ }
94
+
95
+ iframe {
96
+ border: none;
97
+ border-radius: 0.25rem;
98
+ box-shadow: 0 0.0625rem 0.1875rem rgba($CAP_G01, 0.1);
99
+ background-color: $CAP_WHITE;
100
+ }
101
+
102
+ .device-frame {
103
+ position: relative;
104
+ display: inline-block;
105
+
106
+ .home-indicator {
107
+ position: absolute;
108
+ bottom: -0.25rem;
109
+ left: 50%;
110
+ transform: translateX(-50%);
111
+ width: 8.375rem;
112
+ height: 0.3125rem;
113
+ background-color: $CAP_G02;
114
+ border-radius: 0.1875rem;
115
+ z-index: 1070;
116
+ }
117
+
118
+ &::before {
119
+ content: '';
120
+ position: absolute;
121
+ top: -1.25rem;
122
+ left: 50%;
123
+ transform: translateX(-50%);
124
+ width: 3.75rem;
125
+ height: 0.375rem;
126
+ background-color: $CAP_G02;
127
+ border-radius: 0.1875rem;
128
+ z-index: 1070 + 1;
129
+ }
130
+
131
+ &::after {
132
+ content: '';
133
+ position: absolute;
134
+ top: -0.9375rem;
135
+ right: 1.25rem;
136
+ width: 0.75rem;
137
+ height: 0.75rem;
138
+ background-color: $CAP_G02;
139
+ border-radius: 50%;
140
+ z-index: 1070 + 1;
141
+ }
142
+ }
143
+
144
+ // Media queries for responsive behavior - iframe sizing now controlled by React
145
+ // @media (max-width: 768px) - Removed: sizing now handled by React inline styles
146
+
147
+ @media (max-width: 576px) {
148
+
149
+ .device-frame,
150
+ .preview-pane .device-frame {
151
+ transform: scale(0.8);
152
+ transform-origin: center top;
153
+ // iframe sizing now controlled by React inline styles
154
+ }
155
+ }
156
+
157
+ .preview-loading {
158
+ display: flex;
159
+ align-items: center;
160
+ justify-content: center;
161
+ height: 100%;
162
+ color: $CAP_G10;
163
+
164
+ .loading-content {
165
+ text-align: center;
166
+
167
+ .loading-icon {
168
+ font-size: 3rem;
169
+ margin-bottom: 1rem;
170
+ animation: pulse 2s infinite;
171
+ }
172
+ }
173
+ }
174
+
175
+ @keyframes pulse {
176
+
177
+ 0%,
178
+ 100% {
179
+ opacity: 1;
180
+ }
181
+
182
+ 50% {
183
+ opacity: 0.5;
184
+ }
185
+ }
186
+
187
+ .preview-empty {
188
+ display: flex;
189
+ align-items: center;
190
+ justify-content: center;
191
+ height: calc(100% - 2rem);
192
+ color: $CAP_G10;
193
+ text-align: center;
194
+ background-color: $CAP_WHITE;
195
+ border-radius: 0.25rem;
196
+ box-shadow: 0 0.0625rem 0.1875rem rgba($CAP_G01, 0.1);
197
+ margin: 0;
198
+ width: 100%;
199
+
200
+ .empty-content {
201
+ padding: 2.5rem 1.25rem;
202
+
203
+ .empty-icon {
204
+ font-size: 3rem;
205
+ margin-bottom: 1rem;
206
+ }
207
+
208
+ p {
209
+ margin: 0.25rem 0;
210
+ color: $CAP_G10;
211
+ font-size: 0.875rem;
212
+ }
213
+
214
+ .preview-mode-info {
215
+ font-size: 0.75rem;
216
+ margin-top: 0.5rem;
217
+ }
218
+ }
219
+ }
220
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * PreviewPane - Live preview panel with responsive device frames
3
+ *
4
+ * Features:
5
+ * - Live HTML/CSS/JavaScript preview
6
+ * - Responsive device frames (Desktop/Mobile/Mobile Device)
7
+ * - Sandboxed iframe for security
8
+ * - Real-time content updates
9
+ *
10
+ * Security:
11
+ * - Uses DOMPurify-based contentSanitizer.js for comprehensive XSS protection
12
+ * - Sanitizes all HTML content before rendering in iframe
13
+ * - Removes dangerous elements: <script>, <iframe>, <object>, <embed>, <applet>
14
+ * - Strips event handler attributes (onclick, onload, etc.)
15
+ * - Filters dangerous protocols (javascript:, data:, vbscript:)
16
+ * - Variant-aware sanitization (EMAIL vs INAPP with different security levels)
17
+ * - Iframe sandbox uses "allow-scripts" only (no allow-same-origin for security)
18
+ * - Content delivered via blob URLs to isolate iframe from parent origin
19
+ *
20
+ * Previous Security Issues (FIXED):
21
+ * - Removed insecure manual sanitizeJavaScript function that only escaped </script> tags
22
+ * - Replaced with robust DOMPurify-based sanitization for full XSS protection
23
+ * - Removed "allow-same-origin" from iframe sandbox to prevent same-origin access
24
+ * - Implemented blob URL delivery to maintain isolation while allowing script execution
25
+ */
26
+
27
+ import React, { useMemo, useEffect, useRef } from 'react';
28
+ import PropTypes from 'prop-types';
29
+ import { injectIntl, intlShape } from 'react-intl';
30
+
31
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
32
+
33
+ // Security utilities
34
+ import { sanitizeHTML, SANITIZER_VARIANTS } from '../../utils/contentSanitizer';
35
+
36
+ // Components
37
+ import PreviewModeGroup from '../EditorToolbar/PreviewModeGroup';
38
+ import InAppPreviewPane from '../InAppPreviewPane';
39
+
40
+ // Context
41
+ import { useEditorContext } from '../common/EditorContext';
42
+
43
+ // Constants
44
+ import { PREVIEW_MODES, HTML_EDITOR_VARIANTS } from '../../constants';
45
+ import { LAYOUT_TYPES } from '../InAppPreviewPane/constants';
46
+
47
+ // Messages
48
+ import messages from '../../messages';
49
+
50
+ // Styles
51
+ import './_previewPane.scss';
52
+
53
+ const PreviewPane = ({
54
+ intl,
55
+ className = '',
56
+ isFullscreenMode = false,
57
+ isModalContext = false,
58
+ layoutType = LAYOUT_TYPES.MODAL
59
+ }) => {
60
+ const { content, layout, variant } = useEditorContext();
61
+
62
+ // Destructure for better readability
63
+ const { content: contentValue } = content || {};
64
+ const { viewMode, setViewMode, mobileWidth } = layout || {};
65
+
66
+ /**
67
+ * Sanitize content for secure preview rendering
68
+ *
69
+ * This function uses the robust contentSanitizer.js utility which employs DOMPurify
70
+ * for comprehensive XSS protection. The sanitizer handles:
71
+ * - Script tag removal/escaping
72
+ * - Event handler attribute removal
73
+ * - Dangerous protocol filtering (javascript:, data:, etc.)
74
+ * - Malicious HTML structure prevention
75
+ *
76
+ * The variant is determined based on the editor context - EMAIL for email templates,
77
+ * INAPP for in-app content with more permissive multimedia support.
78
+ */
79
+ const combinedContent = useMemo(() => {
80
+ if (!contentValue) {
81
+ return '';
82
+ }
83
+
84
+ // Determine sanitization variant based on editor variant
85
+ // Default to EMAIL for stricter security if variant is not available
86
+ const sanitizationVariant = variant === HTML_EDITOR_VARIANTS.INAPP
87
+ ? SANITIZER_VARIANTS.INAPP
88
+ : SANITIZER_VARIANTS.EMAIL;
89
+
90
+ // Use the robust contentSanitizer with DOMPurify for comprehensive XSS protection
91
+ const sanitizationResult = sanitizeHTML(contentValue, sanitizationVariant);
92
+
93
+ // Log security warnings in development for debugging
94
+ if (process.env.NODE_ENV === 'development' && sanitizationResult.warnings.length > 0) {
95
+ console.warn('PreviewPane: Content sanitization warnings:', sanitizationResult.warnings);
96
+ }
97
+
98
+ // Log removed elements in development for security awareness
99
+ if (process.env.NODE_ENV === 'development' && sanitizationResult.removedElements.length > 0) {
100
+ console.warn('PreviewPane: Removed potentially unsafe elements:', sanitizationResult.removedElements);
101
+ }
102
+
103
+ return sanitizationResult.sanitized;
104
+ }, [contentValue, variant]);
105
+
106
+ // Create blob URL for secure iframe content delivery
107
+ // This isolates the iframe content from the parent origin while allowing script execution
108
+ const blobUrlRef = useRef(null);
109
+ const blobUrl = useMemo(() => {
110
+ // Clean up previous blob URL to prevent memory leaks
111
+ if (blobUrlRef.current) {
112
+ URL.revokeObjectURL(blobUrlRef.current);
113
+ blobUrlRef.current = null;
114
+ }
115
+
116
+ // Create new blob URL if we have content
117
+ if (combinedContent) {
118
+ const blob = new Blob([combinedContent], { type: 'text/html' });
119
+ blobUrlRef.current = URL.createObjectURL(blob);
120
+ return blobUrlRef.current;
121
+ }
122
+
123
+ return null;
124
+ }, [combinedContent]);
125
+
126
+ // Cleanup blob URL on unmount
127
+ useEffect(() => {
128
+ return () => {
129
+ if (blobUrlRef.current) {
130
+ URL.revokeObjectURL(blobUrlRef.current);
131
+ blobUrlRef.current = null;
132
+ }
133
+ };
134
+ }, []);
135
+
136
+ // Generate CSS classes based on view mode
137
+ const getIframeClasses = () => {
138
+ const baseClass = 'preview-pane__iframe';
139
+ const classes = [baseClass];
140
+
141
+ if (viewMode === PREVIEW_MODES.MOBILE) {
142
+ classes.push(`${baseClass}--mobile`);
143
+ }
144
+
145
+ return classes.join(' ');
146
+ };
147
+
148
+ // Generate inline styles for responsive sizing based on view mode
149
+ const getPreviewStyles = () => {
150
+ const baseStyles = {};
151
+
152
+ // Apply sizing based on current view mode
153
+ if (viewMode === PREVIEW_MODES.MOBILE || viewMode === PREVIEW_MODES.MOBILE_DEVICE) {
154
+ // Use layout.mobileWidth or fallback to 375px
155
+ const width = mobileWidth || 375;
156
+ baseStyles.width = `${width}px`;
157
+ baseStyles.height = '100%'; // Fill container height
158
+ }
159
+
160
+ return baseStyles;
161
+ };
162
+
163
+ const renderDeviceFrame = () => (
164
+ <iframe
165
+ src={blobUrl || 'about:blank'}
166
+ title={intl.formatMessage(messages.htmlPreview)}
167
+ className={getIframeClasses()}
168
+ style={getPreviewStyles()}
169
+ sandbox="allow-scripts"
170
+ />
171
+ );
172
+
173
+ const renderEmptyState = () => (
174
+ <div className="preview-empty">
175
+ <div className="empty-content">
176
+ <div className="empty-icon">📝</div>
177
+ <p>{intl.formatMessage(messages.startTypingHtml)}</p>
178
+ <p className="preview-mode-info">
179
+ {intl.formatMessage(messages.previewMode)}: {
180
+ viewMode === PREVIEW_MODES.DESKTOP
181
+ ? intl.formatMessage(messages.desktop)
182
+ : viewMode === PREVIEW_MODES.MOBILE
183
+ ? intl.formatMessage(messages.mobile)
184
+ : intl.formatMessage(messages.mobileDevice)
185
+ }
186
+ </p>
187
+ </div>
188
+ </div>
189
+ );
190
+
191
+ // Render InApp preview for InApp variant
192
+ if (variant === HTML_EDITOR_VARIANTS.INAPP) {
193
+ return (
194
+ <InAppPreviewPane
195
+ intl={intl}
196
+ className={className}
197
+ isFullscreenMode={isFullscreenMode}
198
+ isModalContext={isModalContext}
199
+ layoutType={layoutType}
200
+ />
201
+ );
202
+ }
203
+
204
+ // Render Email preview for Email variant
205
+ return (
206
+ <div className={`preview-pane ${className}`}>
207
+ <CapRow className="preview-pane__mode-controls">
208
+ <PreviewModeGroup
209
+ value={viewMode}
210
+ onChange={setViewMode}
211
+ />
212
+ </CapRow>
213
+
214
+ <CapRow className="preview-pane__content">
215
+ {blobUrl ? renderDeviceFrame() : renderEmptyState()}
216
+ </CapRow>
217
+ </div>
218
+ );
219
+ };
220
+
221
+ PreviewPane.propTypes = {
222
+ intl: intlShape.isRequired,
223
+ className: PropTypes.string,
224
+ isFullscreenMode: PropTypes.bool,
225
+ isModalContext: PropTypes.bool,
226
+ layoutType: PropTypes.oneOf(Object.values(LAYOUT_TYPES))
227
+ };
228
+
229
+ export default injectIntl(PreviewPane);