one-design-next 0.0.89 → 0.0.90
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.
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iframe 导航守卫:
|
|
3
|
+
*
|
|
4
|
+
* PreviewPanel 渲染 text/html 时使用 sandbox="allow-scripts" 的 iframe,
|
|
5
|
+
* 内部链接默认被沙箱拦截(既不响应点击,也不能逃逸到顶层)。
|
|
6
|
+
* 通过 `allowedNavigateDomains` 白名单放行特定域名:命中白名单的链接
|
|
7
|
+
* 在点击时以新窗口打开,其余链接继续拦截。
|
|
8
|
+
*
|
|
9
|
+
* 注意:开启白名单后 sandbox 必须追加 allow-popups / allow-popups-to-escape-sandbox
|
|
10
|
+
* (否则 window.open 被沙箱阻止),因此守卫脚本同时监听 click 与 auxclick
|
|
11
|
+
* (中键点击不触发 click,需单独拦截,否则可绕过白名单直接弹窗)。
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* 判断 hostname 是否命中白名单。
|
|
15
|
+
* 支持精确匹配与子域名匹配(如白名单 `example.com` 可放行 `www.example.com`),
|
|
16
|
+
* 忽略大小写;返回 true 表示“允许跳转”。
|
|
17
|
+
*/
|
|
18
|
+
export declare function isDomainAllowed(hostname: string | null | undefined, allowedDomains: readonly string[] | undefined): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* 将导航守卫脚本注入 HTML,返回新的 srcDoc 字符串。
|
|
21
|
+
*
|
|
22
|
+
* 脚本逻辑(依赖 sandbox 已放行 allow-scripts / allow-popups /
|
|
23
|
+
* allow-popups-to-escape-sandbox):
|
|
24
|
+
* - 锚点(`#xxx`)或非链接元素:不拦截,保留默认行为;
|
|
25
|
+
* - href 无法解析,或协议非 http/https(如 javascript:、data:):拦截;
|
|
26
|
+
* - hostname 命中白名单:以新窗口打开(_blank + noopener);
|
|
27
|
+
* - 其余链接:拦截(防止 iframe 被导航走 / 逃逸到顶层);
|
|
28
|
+
* - click 与 auxclick(中键)都拦截,避免中键绕过白名单直接弹窗。
|
|
29
|
+
*
|
|
30
|
+
* 白名单通过 `window.__ODN_NAV_GUARD_ALLOWED__` 注入,便于校验脚本与数据解耦;
|
|
31
|
+
* 注入前将 `<` 转义为 `\u003c`,防止白名单含 `</script>` 时提前闭合脚本标签。
|
|
32
|
+
*/
|
|
33
|
+
export declare function injectNavGuardScript(html: string, allowedDomains: readonly string[]): string;
|
|
34
|
+
/** 将 html 增强为“可白名单放行导航”的 srcDoc;未传白名单时原样返回。 */
|
|
35
|
+
export declare function withNavGuard(html: string, allowedDomains: readonly string[] | undefined): string;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/* cspell:words noopener */
|
|
2
|
+
/**
|
|
3
|
+
* iframe 导航守卫:
|
|
4
|
+
*
|
|
5
|
+
* PreviewPanel 渲染 text/html 时使用 sandbox="allow-scripts" 的 iframe,
|
|
6
|
+
* 内部链接默认被沙箱拦截(既不响应点击,也不能逃逸到顶层)。
|
|
7
|
+
* 通过 `allowedNavigateDomains` 白名单放行特定域名:命中白名单的链接
|
|
8
|
+
* 在点击时以新窗口打开,其余链接继续拦截。
|
|
9
|
+
*
|
|
10
|
+
* 注意:开启白名单后 sandbox 必须追加 allow-popups / allow-popups-to-escape-sandbox
|
|
11
|
+
* (否则 window.open 被沙箱阻止),因此守卫脚本同时监听 click 与 auxclick
|
|
12
|
+
* (中键点击不触发 click,需单独拦截,否则可绕过白名单直接弹窗)。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 判断 hostname 是否命中白名单。
|
|
17
|
+
* 支持精确匹配与子域名匹配(如白名单 `example.com` 可放行 `www.example.com`),
|
|
18
|
+
* 忽略大小写;返回 true 表示“允许跳转”。
|
|
19
|
+
*/
|
|
20
|
+
export function isDomainAllowed(hostname, allowedDomains) {
|
|
21
|
+
if (!hostname || !allowedDomains || allowedDomains.length === 0) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
var normalized = hostname.toLowerCase();
|
|
25
|
+
return allowedDomains.some(function (domain) {
|
|
26
|
+
if (!domain || typeof domain !== 'string') return false;
|
|
27
|
+
var allowed = domain.trim().toLowerCase();
|
|
28
|
+
if (!allowed) return false;
|
|
29
|
+
return normalized === allowed || normalized.endsWith(".".concat(allowed));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 将导航守卫脚本注入 HTML,返回新的 srcDoc 字符串。
|
|
35
|
+
*
|
|
36
|
+
* 脚本逻辑(依赖 sandbox 已放行 allow-scripts / allow-popups /
|
|
37
|
+
* allow-popups-to-escape-sandbox):
|
|
38
|
+
* - 锚点(`#xxx`)或非链接元素:不拦截,保留默认行为;
|
|
39
|
+
* - href 无法解析,或协议非 http/https(如 javascript:、data:):拦截;
|
|
40
|
+
* - hostname 命中白名单:以新窗口打开(_blank + noopener);
|
|
41
|
+
* - 其余链接:拦截(防止 iframe 被导航走 / 逃逸到顶层);
|
|
42
|
+
* - click 与 auxclick(中键)都拦截,避免中键绕过白名单直接弹窗。
|
|
43
|
+
*
|
|
44
|
+
* 白名单通过 `window.__ODN_NAV_GUARD_ALLOWED__` 注入,便于校验脚本与数据解耦;
|
|
45
|
+
* 注入前将 `<` 转义为 `\u003c`,防止白名单含 `</script>` 时提前闭合脚本标签。
|
|
46
|
+
*/
|
|
47
|
+
export function injectNavGuardScript(html, allowedDomains) {
|
|
48
|
+
// JSON.stringify 不转义 <,这里显式转义,避免白名单内出现 </script> 闭合脚本
|
|
49
|
+
var serializedDomains = JSON.stringify(allowedDomains).replace(/</g, "\\u003c");
|
|
50
|
+
var navGuardScript = "\n<script>\nwindow.__ODN_NAV_GUARD_ALLOWED__ = ".concat(serializedDomains, ";\n(function () {\n var allowed = window.__ODN_NAV_GUARD_ALLOWED__ || [];\n function hostnameAllowed(hostname) {\n if (!hostname || allowed.length === 0) return false;\n hostname = hostname.toLowerCase();\n return allowed.some(function (domain) {\n if (!domain) return false;\n domain = domain.toLowerCase();\n return hostname === domain || hostname.slice(-(domain.length + 1)) === '.' + domain;\n });\n }\n function isSafeHttp(url) {\n return url.protocol === 'http:' || url.protocol === 'https:';\n }\n function intercept(event) {\n var target = event.target;\n if (!target || target.nodeType !== 1) return;\n var anchor = target.closest && target.closest('a[href]');\n if (!anchor) return;\n var href = anchor.getAttribute('href') || '';\n if (href.charAt(0) === '#') return;\n if (href.indexOf('javascript:') === 0 || href.indexOf('data:') === 0) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n var url;\n try {\n url = new URL(href, window.location.href);\n } catch (e) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n if (!isSafeHttp(url)) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n if (hostnameAllowed(url.hostname)) {\n event.preventDefault();\n event.stopPropagation();\n window.open(url.href, '_blank', 'noopener');\n return;\n }\n event.preventDefault();\n event.stopPropagation();\n }\n if (document.addEventListener) {\n document.addEventListener('click', intercept, true);\n document.addEventListener('auxclick', intercept, true);\n }\n})();\n</script>");
|
|
51
|
+
var lower = html.toLowerCase();
|
|
52
|
+
if (lower.includes('</body>')) {
|
|
53
|
+
return html.replace(/<\/body>/i, "".concat(navGuardScript, "</body>"));
|
|
54
|
+
}
|
|
55
|
+
if (lower.includes('</head>')) {
|
|
56
|
+
return html.replace(/<\/head>/i, "".concat(navGuardScript, "</head>"));
|
|
57
|
+
}
|
|
58
|
+
return "".concat(html).concat(navGuardScript);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 将 html 增强为“可白名单放行导航”的 srcDoc;未传白名单时原样返回。 */
|
|
62
|
+
export function withNavGuard(html, allowedDomains) {
|
|
63
|
+
if (!allowedDomains || allowedDomains.length === 0) {
|
|
64
|
+
return html;
|
|
65
|
+
}
|
|
66
|
+
return injectNavGuardScript(html, allowedDomains);
|
|
67
|
+
}
|
|
@@ -78,8 +78,18 @@ export interface PreviewPanelProps {
|
|
|
78
78
|
* 未传则不渲染 Tooltip(向后兼容)。
|
|
79
79
|
*/
|
|
80
80
|
actionTooltips?: PreviewPanelActionTooltips;
|
|
81
|
+
/**
|
|
82
|
+
* HTML 预览(`text/html`)时允许跳转的域名白名单。
|
|
83
|
+
*
|
|
84
|
+
* iframe 默认 `sandbox="allow-scripts"`,内部链接一律被沙箱拦截;
|
|
85
|
+
* 传入本配置后,命中白名单的链接在点击时会以新窗口打开,其余链接保持拦截。
|
|
86
|
+
* 支持精确匹配与子域名匹配(如 `example.com` 可放行 `www.example.com`),
|
|
87
|
+
* 忽略大小写;协议仅放行 `http:` / `https:`,`javascript:` 等始终拦截。
|
|
88
|
+
* 未传时保持原行为不变。
|
|
89
|
+
*/
|
|
90
|
+
allowedNavigateDomains?: string[];
|
|
81
91
|
}
|
|
82
|
-
export declare function PreviewPanel({ tabs, activeTabId, onTabChange, onTabClose, onCloseAll, artifacts, onArtifactOpen, onDownload, onOpenInBrowser, onShare, fullscreen, onFullscreenChange, open, emptyTitle, emptyDescription, widthMode, actionTooltips, }: PreviewPanelProps): import("react").JSX.Element;
|
|
92
|
+
export declare function PreviewPanel({ tabs, activeTabId, onTabChange, onTabClose, onCloseAll, artifacts, onArtifactOpen, onDownload, onOpenInBrowser, onShare, fullscreen, onFullscreenChange, open, emptyTitle, emptyDescription, widthMode, actionTooltips, allowedNavigateDomains, }: PreviewPanelProps): import("react").JSX.Element;
|
|
83
93
|
export declare namespace PreviewPanel {
|
|
84
94
|
var displayName: string;
|
|
85
95
|
}
|
|
@@ -12,6 +12,7 @@ import Popover from "../popover";
|
|
|
12
12
|
import StreamText from "../stream-text";
|
|
13
13
|
import Tooltip from "../tooltip";
|
|
14
14
|
import { CsvPreviewTable } from "./CsvPreviewTable";
|
|
15
|
+
import { withNavGuard } from "./iframeNavGuard";
|
|
15
16
|
import { isCsvTab, parseCsv } from "./parseCsv";
|
|
16
17
|
import "./style";
|
|
17
18
|
|
|
@@ -91,7 +92,8 @@ export function PreviewPanel(_ref2) {
|
|
|
91
92
|
emptyDescription = _ref2$emptyDescriptio === void 0 ? '对话生成的附件会显示在这里' : _ref2$emptyDescriptio,
|
|
92
93
|
_ref2$widthMode = _ref2.widthMode,
|
|
93
94
|
widthMode = _ref2$widthMode === void 0 ? 'half' : _ref2$widthMode,
|
|
94
|
-
actionTooltips = _ref2.actionTooltips
|
|
95
|
+
actionTooltips = _ref2.actionTooltips,
|
|
96
|
+
allowedNavigateDomains = _ref2.allowedNavigateDomains;
|
|
95
97
|
var activeTab = (_ref3 = (_tabs$find = tabs.find(function (t) {
|
|
96
98
|
return t.id === activeTabId;
|
|
97
99
|
})) !== null && _tabs$find !== void 0 ? _tabs$find : tabs[0]) !== null && _ref3 !== void 0 ? _ref3 : null;
|
|
@@ -187,6 +189,10 @@ export function PreviewPanel(_ref2) {
|
|
|
187
189
|
if (!activeTab || !isCsvTab(activeTab.type, activeTab.name)) return null;
|
|
188
190
|
return parseCsv(activeTab.content);
|
|
189
191
|
}, [activeTab]);
|
|
192
|
+
var hasNavGuard = !!allowedNavigateDomains && allowedNavigateDomains.length > 0;
|
|
193
|
+
var htmlSrcDoc = useMemo(function () {
|
|
194
|
+
return (activeTab === null || activeTab === void 0 ? void 0 : activeTab.type) === 'text/html' ? withNavGuard(activeTab.content, allowedNavigateDomains) : undefined;
|
|
195
|
+
}, [activeTab, allowedNavigateDomains]);
|
|
190
196
|
var handleOpenInBrowser = function handleOpenInBrowser() {
|
|
191
197
|
if (!activeTab) return;
|
|
192
198
|
if (onOpenInBrowser) {
|
|
@@ -447,9 +453,10 @@ export function PreviewPanel(_ref2) {
|
|
|
447
453
|
"data-odn-preview-panel-scroll": true,
|
|
448
454
|
onScroll: handlePreviewScroll
|
|
449
455
|
}, activeTab.type === 'text/html' ? /*#__PURE__*/React.createElement("iframe", {
|
|
450
|
-
srcDoc:
|
|
451
|
-
sandbox:
|
|
456
|
+
srcDoc: htmlSrcDoc,
|
|
457
|
+
sandbox: hasNavGuard ? 'allow-scripts allow-popups allow-popups-to-escape-sandbox' : 'allow-scripts',
|
|
452
458
|
"data-odn-preview-panel-iframe": true,
|
|
459
|
+
"data-odn-preview-panel-iframe-nav-guard": hasNavGuard || undefined,
|
|
453
460
|
title: activeTab.name
|
|
454
461
|
}) : isMarkdownTab(activeTab) ? /*#__PURE__*/React.createElement("div", {
|
|
455
462
|
"data-odn-preview-panel-markdown": true
|