mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-10-27 21:10:40 +00:00
feat(editor): 支持配置画布 iframe 的 sandbox 属性
新增 iframeSandbox 配置,透传链路为 EditorProps -> StageOptions -> StageCore -> StageRender,在 createIframe 中设置 src 之前写入 sandbox 属性。默认不设置该属性,保持存量行为不变。 - stage: StageRenderConfig / StageCoreConfig 新增 iframeSandbox 类型 - editor: EditorProps / StageOptions 新增 iframeSandbox 并透传 - docs: 补充 iframeSandbox 说明与 srcdoc、同源限制的注意事项 - test: 覆盖未配置 / false / 空字符串 / 多 token 与 StageCore 透传
This commit is contained in:
parent
d017f43573
commit
7e8cb1f9db
@ -503,6 +503,32 @@ const renderFunction = async (stage) => {
|
||||
</template>
|
||||
```
|
||||
|
||||
## iframeSandbox
|
||||
|
||||
- **详情:**
|
||||
|
||||
画布 iframe 的 sandbox 属性值,仅 `renderType` 为 `iframe` 时生效。不传或传 `false` 时不设置 sandbox 属性。
|
||||
|
||||
token 用空格分隔,缺哪个 token,画布内对应能力就会静默失效。编辑器自身依赖 `allow-scripts` + `allow-same-origin`(需要读写 iframe 内的 DOM);组件内需要下载、弹窗、表单提交时,分别要放开 `allow-downloads`、`allow-popups`、`allow-forms` 等。
|
||||
|
||||
注意:
|
||||
|
||||
- 仅在编辑器初始化时读取,运行期修改不会生效。
|
||||
- runtime 与编辑器不同域时,会通过 `srcdoc` 加载,同样受 sandbox 约束;缺少 `allow-same-origin` 会导致编辑器无法访问 iframe 内的 DOM。
|
||||
- `allow-scripts` 与 `allow-same-origin` 同时存在且 iframe 同源时,iframe 内脚本可以移除自身的 sandbox 属性,因此该配置只用于限制 `allow-top-navigation`、`allow-popups` 等能力,不能作为安全隔离手段。
|
||||
|
||||
- **默认值:** `undefined`(不设置 sandbox 属性)
|
||||
|
||||
- **类型:** `string | false`
|
||||
|
||||
- **示例:**
|
||||
|
||||
```html
|
||||
<template>
|
||||
<m-editor iframe-sandbox="allow-scripts allow-same-origin allow-downloads"></m-editor>
|
||||
</template>
|
||||
```
|
||||
|
||||
## autoScrollIntoView
|
||||
|
||||
- **详情:**
|
||||
|
||||
@ -224,6 +224,7 @@ const stageOptions: StageOptions = {
|
||||
containerHighlightAddOnly: props.containerHighlightAddOnly,
|
||||
disabledDragStart: props.disabledDragStart,
|
||||
renderType: props.renderType,
|
||||
iframeSandbox: props.iframeSandbox,
|
||||
guidesOptions: props.guidesOptions,
|
||||
disabledMultiSelect: props.disabledMultiSelect,
|
||||
alwaysMultiSelect: props.alwaysMultiSelect,
|
||||
|
||||
@ -62,6 +62,16 @@ export interface EditorProps {
|
||||
runtimeUrl?: string;
|
||||
/** 是用iframe渲染还是直接渲染 */
|
||||
renderType?: RenderType;
|
||||
/**
|
||||
* 画布 iframe 的 sandbox 属性值(仅 iframe 渲染时生效)
|
||||
*
|
||||
* - 不传 / `false`:不设置 sandbox 属性(默认)
|
||||
* - 字符串:原样写入,token 空格分隔,如 `'allow-scripts allow-same-origin allow-downloads'`
|
||||
*
|
||||
* 需要注意编辑器自身依赖 `allow-scripts` + `allow-same-origin`(读写 iframe 内 DOM),
|
||||
* 其余 token(`allow-downloads`、`allow-popups`、`allow-forms` 等)按业务需要放开。
|
||||
*/
|
||||
iframeSandbox?: string | false;
|
||||
/** 选中时是否自动滚动到可视区域 */
|
||||
autoScrollIntoView?: boolean;
|
||||
/** 组件的属性配置表单的dsl */
|
||||
|
||||
@ -31,6 +31,7 @@ export const useStage = (stageOptions: StageOptions) => {
|
||||
containerHighlightAddOnly: stageOptions.containerHighlightAddOnly,
|
||||
disabledDragStart: stageOptions.disabledDragStart,
|
||||
renderType: stageOptions.renderType,
|
||||
iframeSandbox: stageOptions.iframeSandbox,
|
||||
canSelect: (el, event, stop) => {
|
||||
if (!stageOptions.canSelect) return true;
|
||||
|
||||
|
||||
@ -221,6 +221,8 @@ export interface StageOptions {
|
||||
*/
|
||||
disabledFlashTip?: boolean;
|
||||
zoom?: number;
|
||||
/** 画布 iframe 的 sandbox 属性值,不传 / false 表示不设置,见 @tmagic/stage 的 StageRenderConfig.iframeSandbox */
|
||||
iframeSandbox?: string | false;
|
||||
/** 画布双击前的钩子函数,返回 false 则阻止默认的双击行为 */
|
||||
beforeDblclick?: (event: MouseEvent) => Promise<boolean | void> | boolean | void;
|
||||
}
|
||||
|
||||
@ -71,6 +71,7 @@ export default class StageCore extends EventEmitter {
|
||||
runtimeUrl: config.runtimeUrl,
|
||||
zoom: config.zoom,
|
||||
renderType: config.renderType,
|
||||
iframeSandbox: config.iframeSandbox,
|
||||
customizedRender: async (): Promise<HTMLElement | null | void> => {
|
||||
if (this?.customizedRender) {
|
||||
return await this.customizedRender(this);
|
||||
|
||||
@ -38,13 +38,21 @@ export default class StageRender extends EventEmitter {
|
||||
private runtimeUrl?: string;
|
||||
private zoom = DEFAULT_ZOOM;
|
||||
private renderType: RenderType;
|
||||
private iframeSandbox?: string | false;
|
||||
private customizedRender?: () => Promise<HTMLElement | null | void>;
|
||||
|
||||
constructor({ runtimeUrl, zoom, customizedRender, renderType = RenderType.IFRAME }: StageRenderConfig) {
|
||||
constructor({
|
||||
runtimeUrl,
|
||||
zoom,
|
||||
customizedRender,
|
||||
renderType = RenderType.IFRAME,
|
||||
iframeSandbox,
|
||||
}: StageRenderConfig) {
|
||||
super();
|
||||
|
||||
this.renderType = renderType;
|
||||
this.runtimeUrl = runtimeUrl || '';
|
||||
this.iframeSandbox = iframeSandbox;
|
||||
this.customizedRender = customizedRender;
|
||||
this.setZoom(zoom);
|
||||
|
||||
@ -250,6 +258,11 @@ export default class StageRender extends EventEmitter {
|
||||
|
||||
private createIframe(): HTMLIFrameElement {
|
||||
this.iframe = globalThis.document.createElement('iframe');
|
||||
// 沙箱由使用方配置(空字符串也是合法取值,表示放开全部限制),必须在设置 src 之前写入,
|
||||
// 否则首次导航不受沙箱约束
|
||||
if (typeof this.iframeSandbox === 'string') {
|
||||
this.iframe.setAttribute('sandbox', this.iframeSandbox);
|
||||
}
|
||||
// 同源,直接加载
|
||||
this.iframe.src = this.runtimeUrl && isSameDomain(this.runtimeUrl) ? this.runtimeUrl : '';
|
||||
this.iframe.style.cssText = `
|
||||
|
||||
@ -104,6 +104,8 @@ export interface StageCoreConfig {
|
||||
* 用于从图层树、面包屑等外部选中组件时,帮助用户快速定位组件在画布中的位置。
|
||||
*/
|
||||
disabledFlashTip?: boolean;
|
||||
/** 画布 iframe 的 sandbox 属性值,见 StageRenderConfig.iframeSandbox */
|
||||
iframeSandbox?: string | false;
|
||||
}
|
||||
|
||||
export interface ActionManagerConfig {
|
||||
@ -153,6 +155,17 @@ export interface StageRenderConfig {
|
||||
runtimeUrl?: string;
|
||||
zoom: number | undefined;
|
||||
renderType?: RenderType;
|
||||
/**
|
||||
* iframe 渲染时给画布 iframe 设置的 sandbox 属性值
|
||||
*
|
||||
* - 不传 / `false`:不设置 sandbox 属性(默认,与未引入该配置前行为一致)
|
||||
* - 字符串:原样写入 sandbox 属性,token 用空格分隔,如
|
||||
* `'allow-scripts allow-same-origin allow-forms allow-popups allow-downloads'`
|
||||
*
|
||||
* 注意:沙箱缺哪个 token,iframe 内对应能力就会静默失效(下载、弹窗、表单提交等),
|
||||
* 编辑器自身需要 `allow-scripts` + `allow-same-origin`,否则画布不可用。
|
||||
*/
|
||||
iframeSandbox?: string | false;
|
||||
customizedRender?: () => Promise<HTMLElement | null | void>;
|
||||
}
|
||||
|
||||
|
||||
@ -100,6 +100,21 @@ describe('StageCore', () => {
|
||||
stage.destroy();
|
||||
});
|
||||
|
||||
test('iframeSandbox 透传到 iframe 的 sandbox 属性', async () => {
|
||||
const host = globalThis.document.createElement('div');
|
||||
globalThis.document.body.appendChild(host);
|
||||
const sandbox = 'allow-scripts allow-same-origin';
|
||||
const stage = new StageCore({
|
||||
renderType: RenderType.IFRAME,
|
||||
runtimeUrl: '',
|
||||
iframeSandbox: sandbox,
|
||||
disabledRule: true,
|
||||
});
|
||||
await stage.mount(host);
|
||||
expect(host.querySelector('iframe')!.getAttribute('sandbox')).toBe(sandbox);
|
||||
stage.destroy();
|
||||
});
|
||||
|
||||
test('select 选中节点并触发 flash', async () => {
|
||||
const { host, page, stage } = createStage();
|
||||
await stage.mount(host);
|
||||
|
||||
@ -133,6 +133,47 @@ describe('StageRender - NATIVE', () => {
|
||||
renderer.destroy();
|
||||
});
|
||||
|
||||
test('未配置 iframeSandbox 时不设置 sandbox 属性', async () => {
|
||||
const host = globalThis.document.createElement('div');
|
||||
globalThis.document.body.appendChild(host);
|
||||
const renderer = new StageRender({ renderType: RenderType.IFRAME, runtimeUrl: '' });
|
||||
await renderer.mount(host);
|
||||
|
||||
expect(host.querySelector('iframe')!.hasAttribute('sandbox')).toBe(false);
|
||||
renderer.destroy();
|
||||
});
|
||||
|
||||
test('iframeSandbox 为 false 时不设置 sandbox 属性', async () => {
|
||||
const host = globalThis.document.createElement('div');
|
||||
globalThis.document.body.appendChild(host);
|
||||
const renderer = new StageRender({ renderType: RenderType.IFRAME, runtimeUrl: '', iframeSandbox: false });
|
||||
await renderer.mount(host);
|
||||
|
||||
expect(host.querySelector('iframe')!.hasAttribute('sandbox')).toBe(false);
|
||||
renderer.destroy();
|
||||
});
|
||||
|
||||
test('iframeSandbox 配置的 token 原样写入 sandbox 属性', async () => {
|
||||
const host = globalThis.document.createElement('div');
|
||||
globalThis.document.body.appendChild(host);
|
||||
const sandbox = 'allow-scripts allow-same-origin allow-downloads';
|
||||
const renderer = new StageRender({ renderType: RenderType.IFRAME, runtimeUrl: '', iframeSandbox: sandbox });
|
||||
await renderer.mount(host);
|
||||
|
||||
expect(host.querySelector('iframe')!.getAttribute('sandbox')).toBe(sandbox);
|
||||
renderer.destroy();
|
||||
});
|
||||
|
||||
test('空字符串 iframeSandbox 也写入 sandbox 属性(放开全部限制)', async () => {
|
||||
const host = globalThis.document.createElement('div');
|
||||
globalThis.document.body.appendChild(host);
|
||||
const renderer = new StageRender({ renderType: RenderType.IFRAME, runtimeUrl: '', iframeSandbox: '' });
|
||||
await renderer.mount(host);
|
||||
|
||||
expect(host.querySelector('iframe')!.getAttribute('sandbox')).toBe('');
|
||||
renderer.destroy();
|
||||
});
|
||||
|
||||
test('getElementImage 找到元素时调用 snapdom', async () => {
|
||||
const page = globalThis.document.createElement('div');
|
||||
setIdToEl()(page, 'img-node');
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
ref="editor"
|
||||
:menu="menu"
|
||||
:runtime-url="runtimeUrl"
|
||||
:iframe-sandbox="iframeSandbox"
|
||||
:props-configs="propsConfigs"
|
||||
:props-values="propsValues"
|
||||
:event-method-list="eventMethodList"
|
||||
@ -83,6 +84,22 @@ uiService.set('propsPanelSize', 'default');
|
||||
const datasourceList: DatasourceTypeOption[] = [];
|
||||
const runtimeUrl = `${VITE_RUNTIME_PATH}/playground/index.html`;
|
||||
|
||||
/**
|
||||
* 画布 iframe 的 sandbox 配置(较宽松的演示值,非推荐值),演示「是否加、加哪些 token」由使用方决定:
|
||||
* - 改成 `false` 即不设置 sandbox 属性(与未配置时一致)
|
||||
* - 画布需要 allow-scripts + allow-same-origin(编辑器要读写 iframe 内的 DOM),
|
||||
* allow-downloads 用于画布内组件的下载,其余 token 保留表单提交、弹窗、新窗口打开能力
|
||||
*/
|
||||
const iframeSandbox = [
|
||||
'allow-downloads',
|
||||
'allow-forms',
|
||||
'allow-modals',
|
||||
'allow-popups',
|
||||
'allow-popups-to-escape-sandbox',
|
||||
'allow-same-origin',
|
||||
'allow-scripts',
|
||||
].join(' ');
|
||||
|
||||
const { propsValues, propsConfigs, eventMethodList, datasourceConfigs, datasourceValues, datasourceEventMethodList } =
|
||||
useEditorRes();
|
||||
const { contentMenuData } = useEditorContentMenuData();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user