@pushwoosh/dumb-components 1.1.171 → 1.1.172
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/RichmediaPreview/RichmediaPreview.d.ts +4 -2
- package/RichmediaPreview/RichmediaPreview.js +6 -15
- package/RichmediaPreview/styles.d.ts +2 -1
- package/RichmediaPreview/styles.js +2 -1
- package/SandboxedHtmlFrame/SandboxedHtmlFrame.d.ts +16 -0
- package/SandboxedHtmlFrame/SandboxedHtmlFrame.js +55 -0
- package/SandboxedHtmlFrame/__tests__/SandboxedHtmlFrame.test.d.ts +1 -0
- package/SandboxedHtmlFrame/__tests__/SandboxedHtmlFrame.test.js +57 -0
- package/SandboxedHtmlFrame/index.d.ts +2 -0
- package/SandboxedHtmlFrame/index.js +1 -0
- package/SandboxedHtmlFrame/types.d.ts +33 -0
- package/SandboxedHtmlFrame/types.js +1 -0
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +1 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { type ReactElement } from 'react';
|
|
2
2
|
import type { RichmediaPreviewProps } from './types';
|
|
3
3
|
/**
|
|
4
|
-
* Renders a rich-media (in-app) template in a sandboxed iframe, resolving
|
|
5
|
-
* localization via {@link useRichmediaHtml}.
|
|
4
|
+
* Renders a rich-media (in-app) template in a fully isolated sandboxed iframe, resolving
|
|
5
|
+
* params and localization via {@link useRichmediaHtml}. Template scripts run in an opaque
|
|
6
|
+
* origin (via {@link SandboxedHtmlFrame}) and cannot reach the control-panel session,
|
|
7
|
+
* cookies, storage or parent DOM.
|
|
6
8
|
*
|
|
7
9
|
* @example
|
|
8
10
|
* <RichmediaPreview name="Promo" content={html} params={params} language="en" />
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useRef, useEffect } from 'react';
|
|
3
2
|
import { Color } from '@pushwoosh/kit-constants';
|
|
4
3
|
import { Spinner } from '../Spinner';
|
|
5
4
|
import { useRichmediaHtml } from './hooks';
|
|
6
5
|
import { RichMediaPreviewContainer, RichMediaPreviewIframe } from './styles';
|
|
7
6
|
/**
|
|
8
|
-
* Renders a rich-media (in-app) template in a sandboxed iframe, resolving
|
|
9
|
-
* localization via {@link useRichmediaHtml}.
|
|
7
|
+
* Renders a rich-media (in-app) template in a fully isolated sandboxed iframe, resolving
|
|
8
|
+
* params and localization via {@link useRichmediaHtml}. Template scripts run in an opaque
|
|
9
|
+
* origin (via {@link SandboxedHtmlFrame}) and cannot reach the control-panel session,
|
|
10
|
+
* cookies, storage or parent DOM.
|
|
10
11
|
*
|
|
11
12
|
* @example
|
|
12
13
|
* <RichmediaPreview name="Promo" content={html} params={params} language="en" />
|
|
@@ -23,7 +24,6 @@ export function RichmediaPreview(props) {
|
|
|
23
24
|
height = 480,
|
|
24
25
|
isLoading = false
|
|
25
26
|
} = props;
|
|
26
|
-
const $ref = useRef(null);
|
|
27
27
|
const richmediaHtml = useRichmediaHtml({
|
|
28
28
|
content,
|
|
29
29
|
params,
|
|
@@ -31,15 +31,6 @@ export function RichmediaPreview(props) {
|
|
|
31
31
|
defaultLanguage,
|
|
32
32
|
baseUrl
|
|
33
33
|
});
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
var _$ref$current;
|
|
36
|
-
if (!($ref !== null && $ref !== void 0 && (_$ref$current = $ref.current) !== null && _$ref$current !== void 0 && (_$ref$current = _$ref$current.contentWindow) !== null && _$ref$current !== void 0 && _$ref$current.document)) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
$ref.current.contentWindow.document.open('text/html', 'replace');
|
|
40
|
-
$ref.current.contentWindow.document.write(richmediaHtml);
|
|
41
|
-
$ref.current.contentWindow.document.close();
|
|
42
|
-
}, [$ref, richmediaHtml]);
|
|
43
34
|
return _jsx(RichMediaPreviewContainer, {
|
|
44
35
|
width: width,
|
|
45
36
|
height: height,
|
|
@@ -49,9 +40,9 @@ export function RichmediaPreview(props) {
|
|
|
49
40
|
size: "medium",
|
|
50
41
|
color: Color.LOCKED
|
|
51
42
|
}) : _jsx(RichMediaPreviewIframe, {
|
|
52
|
-
|
|
43
|
+
mode: "isolated",
|
|
53
44
|
title: name,
|
|
54
|
-
|
|
45
|
+
html: richmediaHtml
|
|
55
46
|
})
|
|
56
47
|
});
|
|
57
48
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SandboxedHtmlFrame } from '../SandboxedHtmlFrame';
|
|
1
2
|
export declare const RichMediaPreviewContainer: import("styled-components").StyledComponent<"div", any, import("@pushwoosh/kit-helpers/styled-helpers").CommonBoxProps & {
|
|
2
3
|
$alignContent?: import("@pushwoosh/kit-helpers/styled-helpers").AlignContentValue | undefined;
|
|
3
4
|
$alignItems?: import("@pushwoosh/kit-helpers/styled-helpers").ItemAlignmentValue | undefined;
|
|
@@ -14,4 +15,4 @@ export declare const RichMediaPreviewContainer: import("styled-components").Styl
|
|
|
14
15
|
height: string | number;
|
|
15
16
|
$isLoading?: boolean;
|
|
16
17
|
}, "$gridAutoFlow">;
|
|
17
|
-
export declare const RichMediaPreviewIframe: import("styled-components").StyledComponent<
|
|
18
|
+
export declare const RichMediaPreviewIframe: import("styled-components").StyledComponent<typeof SandboxedHtmlFrame, any, {}, never>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import styled from 'styled-components';
|
|
2
2
|
import { Color, ShapeRadius } from '@pushwoosh/kit-constants';
|
|
3
3
|
import { Vertical, toCssValue } from '@pushwoosh/kit-helpers';
|
|
4
|
+
import { SandboxedHtmlFrame } from '../SandboxedHtmlFrame';
|
|
4
5
|
export const RichMediaPreviewContainer = styled(Vertical).withConfig({
|
|
5
6
|
displayName: "RichMediaPreviewContainer",
|
|
6
7
|
componentId: "sc-1ci6l2b-0"
|
|
@@ -11,7 +12,7 @@ export const RichMediaPreviewContainer = styled(Vertical).withConfig({
|
|
|
11
12
|
}) => toCssValue(height), ShapeRadius.SECTION, ({
|
|
12
13
|
$isLoading
|
|
13
14
|
}) => $isLoading ? Color.FROZEN : Color.CLEAR);
|
|
14
|
-
export const RichMediaPreviewIframe = styled.
|
|
15
|
+
export const RichMediaPreviewIframe = styled(SandboxedHtmlFrame).withConfig({
|
|
15
16
|
displayName: "RichMediaPreviewIframe",
|
|
16
17
|
componentId: "sc-1ci6l2b-1"
|
|
17
18
|
})(["position:absolute;left:-50%;top:-50%;width:200%;height:200%;transform:scale(0.51);"]);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
import type { SandboxedHtmlFrameProps } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Renders untrusted HTML inside a sandboxed iframe. The `mode` fixes the security
|
|
5
|
+
* boundary so scripts and same-origin access can never be granted together:
|
|
6
|
+
*
|
|
7
|
+
* - `isolated` — `sandbox="allow-scripts"`; scripts run in an opaque origin and cannot
|
|
8
|
+
* touch the embedding page. The parent gets no DOM access.
|
|
9
|
+
* - `inspectable` — `sandbox="allow-same-origin"`; scripts are disabled, but the parent
|
|
10
|
+
* keeps read access to the frame document via `onReady`.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* <SandboxedHtmlFrame mode="isolated" html={html} title="Preview" />
|
|
14
|
+
* <SandboxedHtmlFrame mode="inspectable" html={html} title="Stats" onReady={decorate} />
|
|
15
|
+
*/
|
|
16
|
+
export declare function SandboxedHtmlFrame(props: SandboxedHtmlFrameProps): ReactElement;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
const SANDBOX = {
|
|
4
|
+
isolated: 'allow-scripts',
|
|
5
|
+
inspectable: 'allow-same-origin'
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Renders untrusted HTML inside a sandboxed iframe. The `mode` fixes the security
|
|
9
|
+
* boundary so scripts and same-origin access can never be granted together:
|
|
10
|
+
*
|
|
11
|
+
* - `isolated` — `sandbox="allow-scripts"`; scripts run in an opaque origin and cannot
|
|
12
|
+
* touch the embedding page. The parent gets no DOM access.
|
|
13
|
+
* - `inspectable` — `sandbox="allow-same-origin"`; scripts are disabled, but the parent
|
|
14
|
+
* keeps read access to the frame document via `onReady`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* <SandboxedHtmlFrame mode="isolated" html={html} title="Preview" />
|
|
18
|
+
* <SandboxedHtmlFrame mode="inspectable" html={html} title="Stats" onReady={decorate} />
|
|
19
|
+
*/
|
|
20
|
+
export function SandboxedHtmlFrame(props) {
|
|
21
|
+
const {
|
|
22
|
+
mode,
|
|
23
|
+
html,
|
|
24
|
+
title,
|
|
25
|
+
className
|
|
26
|
+
} = props;
|
|
27
|
+
const onReady = mode === 'inspectable' ? props.onReady : undefined;
|
|
28
|
+
const $ref = useRef(null);
|
|
29
|
+
const [isReady, setIsReady] = useState(false);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
setIsReady(false);
|
|
32
|
+
}, [html]);
|
|
33
|
+
const handleLoad = useCallback(() => {
|
|
34
|
+
setIsReady(true);
|
|
35
|
+
}, []);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
var _$ref$current;
|
|
38
|
+
if (!isReady || !onReady) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const frameDocument = (_$ref$current = $ref.current) === null || _$ref$current === void 0 ? void 0 : _$ref$current.contentDocument;
|
|
42
|
+
if (!frameDocument) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
return onReady(frameDocument);
|
|
46
|
+
}, [isReady, onReady]);
|
|
47
|
+
return _jsx("iframe", {
|
|
48
|
+
ref: $ref,
|
|
49
|
+
className: className,
|
|
50
|
+
title: title,
|
|
51
|
+
srcDoc: html,
|
|
52
|
+
sandbox: SANDBOX[mode],
|
|
53
|
+
onLoad: handleLoad
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { SandboxedHtmlFrame } from '../SandboxedHtmlFrame';
|
|
4
|
+
const HTML = '<p>content</p>';
|
|
5
|
+
function getFrame(container) {
|
|
6
|
+
const frame = container.querySelector('iframe');
|
|
7
|
+
if (!frame) {
|
|
8
|
+
throw new Error('iframe not rendered');
|
|
9
|
+
}
|
|
10
|
+
return frame;
|
|
11
|
+
}
|
|
12
|
+
describe('SandboxedHtmlFrame', () => {
|
|
13
|
+
it('isolates scripts in an opaque origin without same-origin access', () => {
|
|
14
|
+
const {
|
|
15
|
+
container
|
|
16
|
+
} = render(_jsx(SandboxedHtmlFrame, {
|
|
17
|
+
mode: "isolated",
|
|
18
|
+
title: "preview",
|
|
19
|
+
html: HTML
|
|
20
|
+
}));
|
|
21
|
+
const frame = getFrame(container);
|
|
22
|
+
expect(frame.getAttribute('sandbox')).toBe('allow-scripts');
|
|
23
|
+
expect(frame.getAttribute('srcdoc')).toBe(HTML);
|
|
24
|
+
expect(frame).toHaveAttribute('title', 'preview');
|
|
25
|
+
});
|
|
26
|
+
it('grants same-origin access but disables scripts in inspectable mode', () => {
|
|
27
|
+
const {
|
|
28
|
+
container
|
|
29
|
+
} = render(_jsx(SandboxedHtmlFrame, {
|
|
30
|
+
mode: "inspectable",
|
|
31
|
+
title: "stats",
|
|
32
|
+
html: HTML
|
|
33
|
+
}));
|
|
34
|
+
const frame = getFrame(container);
|
|
35
|
+
expect(frame.getAttribute('sandbox')).toBe('allow-same-origin');
|
|
36
|
+
expect(frame.getAttribute('sandbox')).not.toContain('allow-scripts');
|
|
37
|
+
});
|
|
38
|
+
it('never emits allow-scripts together with allow-same-origin', () => {
|
|
39
|
+
const isolated = render(_jsx(SandboxedHtmlFrame, {
|
|
40
|
+
mode: "isolated",
|
|
41
|
+
title: "a",
|
|
42
|
+
html: HTML
|
|
43
|
+
}));
|
|
44
|
+
const inspectable = render(_jsx(SandboxedHtmlFrame, {
|
|
45
|
+
mode: "inspectable",
|
|
46
|
+
title: "b",
|
|
47
|
+
html: HTML
|
|
48
|
+
}));
|
|
49
|
+
[isolated, inspectable].forEach(({
|
|
50
|
+
container
|
|
51
|
+
}) => {
|
|
52
|
+
const sandbox = getFrame(container).getAttribute('sandbox') ?? '';
|
|
53
|
+
const hasBoth = sandbox.includes('allow-scripts') && sandbox.includes('allow-same-origin');
|
|
54
|
+
expect(hasBoth).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SandboxedHtmlFrame } from './SandboxedHtmlFrame';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
type SandboxedHtmlFrameBaseProps = {
|
|
2
|
+
/** Untrusted HTML rendered inside the iframe via `srcDoc`. */
|
|
3
|
+
html: string;
|
|
4
|
+
/** Accessible title of the iframe. */
|
|
5
|
+
title: string;
|
|
6
|
+
/** Forwarded to the iframe element so consumers can size it via `styled()`. */
|
|
7
|
+
className?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Fully isolated rendering: scripts run inside an opaque origin and cannot reach the
|
|
11
|
+
* embedding page (cookies, storage, parent DOM). Use for pure visual previews of
|
|
12
|
+
* untrusted content.
|
|
13
|
+
*/
|
|
14
|
+
export type IsolatedSandboxedHtmlFrameProps = SandboxedHtmlFrameBaseProps & {
|
|
15
|
+
mode: 'isolated';
|
|
16
|
+
onReady?: never;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Inspectable rendering: scripts are disabled entirely, but the embedding page keeps
|
|
20
|
+
* same-origin read access to the frame document. Use when the parent must measure or
|
|
21
|
+
* decorate the rendered DOM (e.g. an interaction heatmap).
|
|
22
|
+
*/
|
|
23
|
+
export type InspectableSandboxedHtmlFrameProps = SandboxedHtmlFrameBaseProps & {
|
|
24
|
+
mode: 'inspectable';
|
|
25
|
+
/**
|
|
26
|
+
* Called with the frame document once it has loaded, and again whenever the callback
|
|
27
|
+
* identity changes. May return a cleanup run before the next call and on unmount.
|
|
28
|
+
*/
|
|
29
|
+
onReady?: (document: Document) => void | (() => void);
|
|
30
|
+
};
|
|
31
|
+
/** Props of {@link SandboxedHtmlFrame}. */
|
|
32
|
+
export type SandboxedHtmlFrameProps = IsolatedSandboxedHtmlFrameProps | InspectableSandboxedHtmlFrameProps;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/index.d.ts
CHANGED
|
@@ -43,6 +43,7 @@ export { CodeEditor } from './CodeEditor';
|
|
|
43
43
|
export { EnhancedInput, SearchInput } from './EnhancedInput';
|
|
44
44
|
export { Chip, type ChipProps, type ChipVariant } from './Chip';
|
|
45
45
|
export { RichmediaPreview, useRichmediaHtml } from './RichmediaPreview';
|
|
46
|
+
export { SandboxedHtmlFrame, type SandboxedHtmlFrameProps, type IsolatedSandboxedHtmlFrameProps, type InspectableSandboxedHtmlFrameProps, } from './SandboxedHtmlFrame';
|
|
46
47
|
export { NativeRichMediaPreview, parseNativeRichMediaConfig, NATIVE_RICH_MEDIA_DISPLAY_TYPES } from './NativeRichMediaPreview';
|
|
47
48
|
export type { NativeRichMediaConfig, NativeRichMediaDisplayType, NativeRichMediaText, NativeRichMediaBorder, NativeRichMediaCover, NativeRichMediaAction, NativeRichMediaButton, NativeRichMediaReward, NativeRichMediaModalBlock, NativeRichMediaSheetBlock, NativeRichMediaCarouselBlock, NativeRichMediaStoriesBlock, NativeRichMediaBannerBlock, NativeRichMediaFullscreenBlock, NativeRichMediaVideoBlock, NativeRichMediaPipBlock, NativeRichMediaScratchcardBlock, NativeRichMediaSpinwheelBlock, NativeRichMediaPreviewProps, NativeRichMediaLocalization, } from './NativeRichMediaPreview';
|
|
48
49
|
export { RichMessageEditor, RichMessageViewer, type DynamicContent, } from './RichMessageEditor';
|
package/index.js
CHANGED
|
@@ -43,6 +43,7 @@ export { CodeEditor } from './CodeEditor';
|
|
|
43
43
|
export { EnhancedInput, SearchInput } from './EnhancedInput';
|
|
44
44
|
export { Chip } from './Chip';
|
|
45
45
|
export { RichmediaPreview, useRichmediaHtml } from './RichmediaPreview';
|
|
46
|
+
export { SandboxedHtmlFrame } from './SandboxedHtmlFrame';
|
|
46
47
|
export { NativeRichMediaPreview, parseNativeRichMediaConfig, NATIVE_RICH_MEDIA_DISPLAY_TYPES } from './NativeRichMediaPreview';
|
|
47
48
|
export { RichMessageEditor, RichMessageViewer } from './RichMessageEditor';
|
|
48
49
|
export { FrequencyCappingEdit, FrequencyCappingReadonly, FrequencyCappingInApps } from './FrequencyCapping';
|