@ptengine/inline-editor 0.1.3 → 0.1.4
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/CHANGELOG.md +8 -0
- package/README.md +5 -5
- package/dist/InlineEditor.d.ts +4 -10
- package/dist/InlineEditor.js +1 -6
- package/dist/kernel/ai/aiInlineApply.d.ts +4 -18
- package/dist/kernel/ai/aiInlineApply.js +4 -15
- package/dist/kernel/ai/imageSource.d.ts +7 -17
- package/dist/kernel/ai/imageSource.js +2 -7
- package/dist/kernel/ai/insertHtmlGate.d.ts +1 -4
- package/dist/kernel/ai/insertHtmlGate.js +3 -3
- package/dist/kernel/ai/replaceHtmlGate.d.ts +4 -11
- package/dist/kernel/ai/replaceHtmlGate.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.4
|
|
4
|
+
|
|
5
|
+
**变更**
|
|
6
|
+
- AI 改动里的图片地址(`CHANGE_IMAGE`、`EDIT_HTML` 与 `INSERT_ELEMENT` 里的 `img[src]`)**不再按域名放行**,只要求 http(s) 绝对地址。
|
|
7
|
+
此前只认宿主注入的 origin 表 + 本会话读到过的地址,生图域名随部署环境变(测试区产图在 npstaticprod),漏配一个就整处改动被拒。
|
|
8
|
+
`javascript:` / `data:` / 相对地址 / 没有 `src` 仍以 `UNSAFE_IMAGE_SRC` 拒;`srcset` 照旧剥掉,`style` 里的 `url()` 照旧禁止。
|
|
9
|
+
- `InlineEditor` 的 `allowedImageOrigins` / `allowedImageUrls` 标为 `@deprecated`、不再读取;保留是为了宿主不用跟着改,传了也会被忽略。
|
|
10
|
+
|
|
3
11
|
## 0.1.3
|
|
4
12
|
|
|
5
13
|
**变更**
|
package/README.md
CHANGED
|
@@ -231,18 +231,18 @@ configureInlineEditor({
|
|
|
231
231
|
## AI 改动的八个 op
|
|
232
232
|
|
|
233
233
|
`AI_SAFE_OPS` 与 mastra 的 `EDITOR_INLINE_OPS` **逐字一致**(顺序也一样)。
|
|
234
|
-
|
|
234
|
+
其中改跳转多一道**来源闸**,依据由宿主注入:
|
|
235
235
|
|
|
236
|
-
- `allowedImageOrigins` —— `CHANGE_IMAGE` 的 `src` 允许的 origin(图片上传域等;
|
|
237
|
-
客户页自己的 origin 由包自动加上)。不传 = 只允许同源的图,**这是有意的 fail closed**:
|
|
238
|
-
模型凭记忆敲出来的域名会被每一个真实访客加载。
|
|
239
236
|
- `allowedLinkUrls` —— `SET_LINK` 允许的**站外**地址,用户在对话里逐字给过的那些
|
|
240
237
|
(**整串**相等,不是 origin 相等)。同源地址不用进这张表。
|
|
241
238
|
|
|
239
|
+
图片地址(换图、整块重写与插入里的 `img[src]`)只查协议,必须是 http(s) 绝对地址。
|
|
240
|
+
域名白名单 2026-10-20 去掉了,`allowedImageOrigins` / `allowedImageUrls` 两个 props 保留但不再生效。
|
|
241
|
+
|
|
242
242
|
`EDIT_HTML`(整块重写)与 `INSERT_ELEMENT` 是**两道不同的闸**,别复用:
|
|
243
243
|
`insertHtmlGate` 审的是「加一个小元素」,`replaceHtmlGate` 审的是「顶替一个既有元素」——
|
|
244
244
|
后者强制**单根**(多根会让其后所有 `:nth-of-type` selector 错位,且上线才暴露)、
|
|
245
|
-
放行 `img`(含图的区块否则永远重写不了,`src`
|
|
245
|
+
放行 `img`(含图的区块否则永远重写不了,`src` 只认 http(s),`srcset` 剥掉)、
|
|
246
246
|
预算 8KB。
|
|
247
247
|
|
|
248
248
|
## 几条容易踩的约束
|
package/dist/InlineEditor.d.ts
CHANGED
|
@@ -70,17 +70,11 @@ export interface InlineEditorProps {
|
|
|
70
70
|
*/
|
|
71
71
|
resolveChangeId?: (id: string) => string;
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* 不传 = 只允许换成与客户页同源的图。**这是有意的 fail closed**:模型凭记忆敲出来的
|
|
76
|
-
* 域名会被每一个真实访客加载,等于我们替客户往他页面上挂了个第三方请求。
|
|
73
|
+
* @deprecated 2026-10-20 起不再生效:AI 改动里的图片地址只查协议(http(s)),不再按域名放行
|
|
74
|
+
* (见 `kernel/ai/imageSource.ts`)。保留只是为了宿主不用跟着改,传了也会被忽略。
|
|
77
75
|
*/
|
|
78
76
|
allowedImageOrigins?: string[];
|
|
79
|
-
/**
|
|
80
|
-
* 本会话在这个页面上读到过的图片地址(宿主从发给 agent 的 `anchor-image:` / `element-image:`
|
|
81
|
-
* 行与 html 里的 `<img src>` 攒)。origin 白名单之外的第二条放行路,整串相等才算 ——
|
|
82
|
-
* 换图、整块重写、插入模块三处共用(见 `kernel/ai/imageSource.ts`)。不传 = 这一路不放行。
|
|
83
|
-
*/
|
|
77
|
+
/** @deprecated 同 `allowedImageOrigins`,传了也会被忽略。 */
|
|
84
78
|
allowedImageUrls?: string[];
|
|
85
79
|
/**
|
|
86
80
|
* `SET_LINK` 允许的**站外**地址 —— 用户在对话里逐字给过的那些(宿主从会话里攒)。
|
|
@@ -346,4 +340,4 @@ export interface AiApplyApi {
|
|
|
346
340
|
/** 当前版本 —— 宿主把 agent 给的版本**名**映射成 id 时要知道现在在哪一版。 */
|
|
347
341
|
readonly activeVersionId?: string;
|
|
348
342
|
}
|
|
349
|
-
export declare function InlineEditor({ url, device, onDeviceChange, title, versionLabel, chrome, onExit, onSave, libraryComponents, libraryLoading, onAddVersion, onRenameVersion, onPreview, onToggleAi, aiOpen, uploadImage, initialChanges, onHydrated, onAskAi, onPageContextReady, anchors, onEditorReady, onSessionReady, onAiContextChange, onAiApplyReady, onAiInsertedRemoved, onAiInsertedRestored, trackAiSelection, renderNodeIcon, versions, activeVersionId, versionChanges, onVersionChange, onVersionLeave, onVersionEnter, resolveChangeId,
|
|
343
|
+
export declare function InlineEditor({ url, device, onDeviceChange, title, versionLabel, chrome, onExit, onSave, libraryComponents, libraryLoading, onAddVersion, onRenameVersion, onPreview, onToggleAi, aiOpen, uploadImage, initialChanges, onHydrated, onAskAi, onPageContextReady, anchors, onEditorReady, onSessionReady, onAiContextChange, onAiApplyReady, onAiInsertedRemoved, onAiInsertedRestored, trackAiSelection, renderNodeIcon, versions, activeVersionId, versionChanges, onVersionChange, onVersionLeave, onVersionEnter, resolveChangeId, allowedLinkUrls, viewportWidthOverride, blockOverlay }: InlineEditorProps): import("react").JSX.Element;
|
package/dist/InlineEditor.js
CHANGED
|
@@ -51,7 +51,7 @@ function imageTargetOf(caps) {
|
|
|
51
51
|
return branch === 'background' ? 'background' : undefined;
|
|
52
52
|
}
|
|
53
53
|
const AI_ELEMENT_IMAGE_LIMIT = 4;
|
|
54
|
-
export function InlineEditor({ url, device, onDeviceChange, title, versionLabel = 'V1', chrome = 'full', onExit, onSave, libraryComponents, libraryLoading, onAddVersion, onRenameVersion, onPreview, onToggleAi, aiOpen, uploadImage, initialChanges, onHydrated, onAskAi, onPageContextReady, anchors, onEditorReady, onSessionReady, onAiContextChange, onAiApplyReady, onAiInsertedRemoved, onAiInsertedRestored, trackAiSelection, renderNodeIcon, versions, activeVersionId, versionChanges, onVersionChange, onVersionLeave, onVersionEnter, resolveChangeId,
|
|
54
|
+
export function InlineEditor({ url, device, onDeviceChange, title, versionLabel = 'V1', chrome = 'full', onExit, onSave, libraryComponents, libraryLoading, onAddVersion, onRenameVersion, onPreview, onToggleAi, aiOpen, uploadImage, initialChanges, onHydrated, onAskAi, onPageContextReady, anchors, onEditorReady, onSessionReady, onAiContextChange, onAiApplyReady, onAiInsertedRemoved, onAiInsertedRestored, trackAiSelection, renderNodeIcon, versions, activeVersionId, versionChanges, onVersionChange, onVersionLeave, onVersionEnter, resolveChangeId, allowedLinkUrls, viewportWidthOverride, blockOverlay }) {
|
|
55
55
|
const { t } = useTranslation();
|
|
56
56
|
const canvasAreaRef = useRef(null);
|
|
57
57
|
const canvasIframeRef = useRef(null);
|
|
@@ -94,7 +94,6 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
94
94
|
return '';
|
|
95
95
|
}
|
|
96
96
|
}, [url]);
|
|
97
|
-
const imageOrigins = useMemo(() => Array.from(new Set([pageOrigin, ...(allowedImageOrigins ?? [])].filter(Boolean))), [pageOrigin, allowedImageOrigins]);
|
|
98
97
|
const changeListRef = useRef(changeList);
|
|
99
98
|
changeListRef.current = changeList;
|
|
100
99
|
const versionChangesRef = useRef(versionChanges);
|
|
@@ -695,8 +694,6 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
695
694
|
addMove: changeList.addMove,
|
|
696
695
|
addAttribute: changeList.addAttribute,
|
|
697
696
|
addAction: changeList.addAction,
|
|
698
|
-
allowedImageOrigins: imageOrigins,
|
|
699
|
-
allowedImageUrls: allowedImageUrls ?? [],
|
|
700
697
|
allowedLinkUrls: allowedLinkUrls ?? [],
|
|
701
698
|
pageOrigin
|
|
702
699
|
});
|
|
@@ -744,8 +741,6 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
744
741
|
changeList.addMove,
|
|
745
742
|
changeList.addAttribute,
|
|
746
743
|
changeList.addAction,
|
|
747
|
-
imageOrigins,
|
|
748
|
-
allowedImageUrls,
|
|
749
744
|
allowedLinkUrls,
|
|
750
745
|
pageOrigin,
|
|
751
746
|
enqueue,
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
* 前五个 op 的内容是模型自己写的文字与样式,落点全在客户页内部;这两个不一样 ——
|
|
42
42
|
* 一个 `src` / `url` 就是一条**对外请求面**,写进去等于让客户页去拉一个第三方地址
|
|
43
43
|
* (换图还会被真实用户加载,改跳转会把流量送走)。所以它们的地址必须**来源可指认**:
|
|
44
|
-
*
|
|
45
|
-
*
|
|
44
|
+
* 链接与页面同源或用户逐字给过,模型凭记忆敲一个外站地址出来,这里必须拦下。图片只查协议
|
|
45
|
+
* (http(s),见 `imageSource.ts`)—— 域名白名单 2026-10-20 去掉了。
|
|
46
46
|
*
|
|
47
47
|
* ⚠️ 两个编辑器必须产出同样的 change,否则同一个 AI 建议在两边存出来的数据不一样,
|
|
48
48
|
* 而这种偏差要到发布后看线上效果才会发现。错误码也照抄(`INVALID_TEXT_CONTENT` 等),
|
|
@@ -85,7 +85,7 @@ export interface AiWireChange {
|
|
|
85
85
|
mode?: string;
|
|
86
86
|
/** MOVE_ELEMENT 的目标 —— 只允许选中元素的祖先之一。 */
|
|
87
87
|
targetSelector?: string;
|
|
88
|
-
/** CHANGE_IMAGE
|
|
88
|
+
/** CHANGE_IMAGE 的新图地址。必须是 http(s) 绝对地址(见 `imageSource.ts`)。 */
|
|
89
89
|
src?: string;
|
|
90
90
|
/** CHANGE_IMAGE 的新 alt。有值时**额外**落一条 `changeElementAttribute alt`。 */
|
|
91
91
|
alt?: string;
|
|
@@ -110,7 +110,7 @@ export type AiRejectReason = 'NOT_IN_WHITELIST' | 'INVALID_SHAPE' | 'OUT_OF_SCOP
|
|
|
110
110
|
| 'INVALID_MODE'
|
|
111
111
|
/** 移动的目标不是选中元素的祖先,或它在页面上不唯一。 */
|
|
112
112
|
| 'INVALID_MOVE_TARGET' | 'APPLY_FAILED'
|
|
113
|
-
/**
|
|
113
|
+
/** 图片地址不是 http(s) 绝对地址(`javascript:` / `data:` / 相对地址 / 空)。 */
|
|
114
114
|
| 'UNSAFE_IMAGE_SRC'
|
|
115
115
|
/**
|
|
116
116
|
* `EDIT_TEXT` 的目标是个容器(自己没有直接文本、却有子元素):换 innerHTML 会把里面的图片、按钮
|
|
@@ -305,20 +305,6 @@ export interface AiApplyDeps {
|
|
|
305
305
|
* 省略 / 抛错 / 返回 null = 判不出目标类型,这两个 op 一律拒(不硬猜)。
|
|
306
306
|
*/
|
|
307
307
|
getCapabilities?: (selector: string) => Promise<ElementCapabilities | null>;
|
|
308
|
-
/**
|
|
309
|
-
* 允许出现在 `src` 里的 origin 白名单(宿主注入:图片上传域 + 客户页自身的 origin)。
|
|
310
|
-
*
|
|
311
|
-
* 白名单而不是「只要是 http(s) 就行」:模型完全可能凭记忆敲一个域名出来,
|
|
312
|
-
* 而那个 `src` 会被**每一个真实访客**加载 —— 等于我们替客户往他的页面上挂了一个
|
|
313
|
-
* 第三方请求(可跟踪、可换内容、可失效)。空数组 = 全拒(fail closed)。
|
|
314
|
-
*/
|
|
315
|
-
allowedImageOrigins: string[];
|
|
316
|
-
/**
|
|
317
|
-
* 本会话编辑器在这个页面上**读到过**的图片地址(完整 URL,整串相等才放行)。
|
|
318
|
-
* origin 白名单之外的第二条路:Shopify 这类站的图在 `cdn.shopify.com`,不在客户页 origin 下
|
|
319
|
-
* (见 `imageSource.ts`)。换图、整块重写、插入三处共用。缺省 = 这一路不放行。
|
|
320
|
-
*/
|
|
321
|
-
allowedImageUrls?: string[];
|
|
322
308
|
/**
|
|
323
309
|
* 用户在对话里**逐字给过**的跳转地址。同源之外只认这一张表,且是**整串逐字相等**
|
|
324
310
|
* (不是 origin 相等):外站地址是把客户的流量送走,得由用户自己说出来才算数。
|
|
@@ -247,10 +247,7 @@ export async function applyAiChanges(changes, deps) {
|
|
|
247
247
|
});
|
|
248
248
|
continue;
|
|
249
249
|
}
|
|
250
|
-
const gate = sanitizeReplaceHtml(change.content
|
|
251
|
-
allowedImageOrigins: deps.allowedImageOrigins,
|
|
252
|
-
allowedImageUrls: deps.allowedImageUrls
|
|
253
|
-
});
|
|
250
|
+
const gate = sanitizeReplaceHtml(change.content);
|
|
254
251
|
if (gate.ok === false) {
|
|
255
252
|
rejected.push({
|
|
256
253
|
index,
|
|
@@ -301,10 +298,7 @@ export async function applyAiChanges(changes, deps) {
|
|
|
301
298
|
});
|
|
302
299
|
continue;
|
|
303
300
|
}
|
|
304
|
-
const gate = sanitizeInsertHtml(change.content
|
|
305
|
-
allowedImageOrigins: deps.allowedImageOrigins,
|
|
306
|
-
allowedImageUrls: deps.allowedImageUrls
|
|
307
|
-
});
|
|
301
|
+
const gate = sanitizeInsertHtml(change.content);
|
|
308
302
|
if (gate.ok === false) {
|
|
309
303
|
rejected.push({
|
|
310
304
|
index,
|
|
@@ -371,17 +365,12 @@ export async function applyAiChanges(changes, deps) {
|
|
|
371
365
|
continue;
|
|
372
366
|
}
|
|
373
367
|
if (change.abTestType === 'CHANGE_IMAGE') {
|
|
374
|
-
|
|
375
|
-
if (!src ||
|
|
376
|
-
!imageSourceAllowed(change.src, {
|
|
377
|
-
allowedImageOrigins: deps.allowedImageOrigins,
|
|
378
|
-
allowedImageUrls: deps.allowedImageUrls
|
|
379
|
-
})) {
|
|
368
|
+
if (!imageSourceAllowed(change.src)) {
|
|
380
369
|
rejected.push({
|
|
381
370
|
index,
|
|
382
371
|
selector,
|
|
383
372
|
reason: 'UNSAFE_IMAGE_SRC',
|
|
384
|
-
detail:
|
|
373
|
+
detail: 'not an http(s) url'
|
|
385
374
|
});
|
|
386
375
|
continue;
|
|
387
376
|
}
|
|
@@ -1,24 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AI
|
|
2
|
+
* AI 改动里**图片地址**的判定 —— 换图(`CHANGE_IMAGE`)、整块重写(`EDIT_HTML`)、
|
|
3
3
|
* 插入(`INSERT_ELEMENT`)三处共用这一份,免得三道闸各写一套、口径漂移。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* 只认协议:http(s) 的绝对地址放行;`javascript:` / `data:` / 相对地址 / 空一律拒。
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* 存在的理由:Shopify 这类站的图全在 `cdn.shopify.com`,不在客户页 origin 下 —— 只认 origin 的话,
|
|
12
|
-
* AI 连「把页面上已有的商品图放进新模块」「整块重写时把原图原样写回去」都会被拒。
|
|
13
|
-
* 按整串而不是按 origin 放:放行整个 `cdn.shopify.com` 等于允许任意 Shopify 店铺的任意图片。
|
|
14
|
-
* 与 `SET_LINK` 的 `allowedLinkUrls`(本会话出现过的链接)同一个思路。
|
|
7
|
+
* 2026-10-20 去掉了域名白名单(宿主注入的 origin 表 + 本会话读到过的图片地址)。生图落在哪个
|
|
8
|
+
* 域名随部署环境变(测试区 mastra 的产图在 npstaticprod,上传图在 npstaticdev),白名单要每个
|
|
9
|
+
* 环境配一遍,漏一个就整处改动被拒;落地页 AI 那边本来也不查域名。宿主那两个入参
|
|
10
|
+
* (`allowedImageOrigins` / `allowedImageUrls`)保留但不再读,见 `InlineEditor` 的 props。
|
|
15
11
|
*/
|
|
16
|
-
export interface ImageSourcePolicy {
|
|
17
|
-
/** 空数组 = 这一路全拒。 */
|
|
18
|
-
allowedImageOrigins: string[];
|
|
19
|
-
/** 本会话在这个页面上读到过的图片地址(完整 URL)。缺省 = 这一路不放行任何地址。 */
|
|
20
|
-
allowedImageUrls?: string[];
|
|
21
|
-
}
|
|
22
12
|
/** 只认 http(s) 的绝对地址;解析失败返回 null。 */
|
|
23
13
|
export declare function parseImageUrl(raw: string | null | undefined): URL | null;
|
|
24
|
-
export declare function imageSourceAllowed(raw: string | null | undefined
|
|
14
|
+
export declare function imageSourceAllowed(raw: string | null | undefined): boolean;
|
|
@@ -9,11 +9,6 @@ export function parseImageUrl(raw) {
|
|
|
9
9
|
return null;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
-
export function imageSourceAllowed(raw
|
|
13
|
-
|
|
14
|
-
if (!url)
|
|
15
|
-
return false;
|
|
16
|
-
if (policy.allowedImageOrigins.includes(url.origin))
|
|
17
|
-
return true;
|
|
18
|
-
return (policy.allowedImageUrls ?? []).some(known => parseImageUrl(known)?.href === url.href);
|
|
12
|
+
export function imageSourceAllowed(raw) {
|
|
13
|
+
return parseImageUrl(raw) !== null;
|
|
19
14
|
}
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
* 代价是模型偶尔会因为一个多余属性被打回整条。这是有意的取舍:被打回时用户看到「这条被
|
|
25
25
|
* 跳过」并可以再问一次,而清洗后的错误元素他看不出来。
|
|
26
26
|
*/
|
|
27
|
-
import { type ImageSourcePolicy } from './imageSource.js';
|
|
28
27
|
/**
|
|
29
28
|
* 一段插入 HTML 的字节上限。
|
|
30
29
|
*
|
|
@@ -66,6 +65,4 @@ export type InsertHtmlOutcome =
|
|
|
66
65
|
* 依赖 `DOMParser`(产品页与 jsdom 都有)。拿不到解析器时**一律拒绝** ——
|
|
67
66
|
* 「审不了就放过」是这类闸最糟的降级。
|
|
68
67
|
*/
|
|
69
|
-
export declare function sanitizeInsertHtml(raw: string
|
|
70
|
-
/** `img[src]` 的来源规则(见 `imageSource.ts`)。缺省 = 任何 img 都拒(fail closed)。 */
|
|
71
|
-
images?: ImageSourcePolicy): InsertHtmlOutcome;
|
|
68
|
+
export declare function sanitizeInsertHtml(raw: string): InsertHtmlOutcome;
|
|
@@ -61,7 +61,7 @@ function isSafeStyleAttr(value) {
|
|
|
61
61
|
!v.includes('expression(') &&
|
|
62
62
|
!v.includes('url('));
|
|
63
63
|
}
|
|
64
|
-
export function sanitizeInsertHtml(raw
|
|
64
|
+
export function sanitizeInsertHtml(raw) {
|
|
65
65
|
const html = (raw ?? '').trim();
|
|
66
66
|
if (!html)
|
|
67
67
|
return { ok: false, problem: { reason: 'EMPTY' } };
|
|
@@ -108,13 +108,13 @@ export function sanitizeInsertHtml(raw, images = { allowedImageOrigins: [] }) {
|
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
-
if (tag === 'img' && !imageSourceAllowed(el.getAttribute('src')
|
|
111
|
+
if (tag === 'img' && !imageSourceAllowed(el.getAttribute('src'))) {
|
|
112
112
|
const src = el.getAttribute('src');
|
|
113
113
|
return {
|
|
114
114
|
ok: false,
|
|
115
115
|
problem: {
|
|
116
116
|
reason: 'UNSAFE_IMAGE_SRC',
|
|
117
|
-
detail: src ? `
|
|
117
|
+
detail: src ? `not an http(s) url: ${src.slice(0, 60)}` : 'img without src'
|
|
118
118
|
}
|
|
119
119
|
};
|
|
120
120
|
}
|
|
@@ -11,18 +11,16 @@
|
|
|
11
11
|
* (`dialogs/htmlValidation.ts`),AI 这条只会更需要。
|
|
12
12
|
* 2. **必须能带 `img`。** 整块重写要把块里原有的图再吐一遍;不允许 `img` 等于说
|
|
13
13
|
* 「含图的区块永远重写不了」——而首屏、商品展示这些最想改的块恰恰都含图。
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* `img[src]` 与 `CHANGE_IMAGE` 同一套地址规则(`imageSource.ts`:只认 http(s)),
|
|
15
|
+
* `srcset` 整条剥掉:那是第二条 URL 通道。
|
|
16
16
|
* 3. **预算更大。** 2048B 是按「一个小新元素」定的,真实区块的 outerHTML 不是那个量级。
|
|
17
17
|
*
|
|
18
18
|
* ⚠️ 用 `<template>` 解析而不是 `<div>`:`<div>` 会把 `<td>` / `<tr>` / `<option>` 这类
|
|
19
19
|
* **需要正确宿主**的标签当成错误内容丢掉 —— 于是「审查通过」了,应用后结构却塌掉。
|
|
20
20
|
* 这一条与 `dialogs/htmlValidation.ts` 同源,理由也一样。
|
|
21
21
|
*
|
|
22
|
-
* ⚠️ `url()`
|
|
23
|
-
* 而 `style="background:url(...)"` 绕开那张表。
|
|
22
|
+
* ⚠️ `url()` 仍然禁止:`style="background:url(...)"` 是 `img` 之外的又一条对外请求通道。
|
|
24
23
|
*/
|
|
25
|
-
import { type ImageSourcePolicy } from './imageSource.js';
|
|
26
24
|
/** 一段替换 HTML 的字节上限。比插入那份宽,但模型偶尔会吐一整屏 markup,仍要封顶。 */
|
|
27
25
|
export declare const REPLACE_HTML_BUDGET_BYTES = 8192;
|
|
28
26
|
export type ReplaceHtmlReject = {
|
|
@@ -57,15 +55,10 @@ export type ReplaceHtmlOutcome =
|
|
|
57
55
|
ok: false;
|
|
58
56
|
problem: ReplaceHtmlReject;
|
|
59
57
|
};
|
|
60
|
-
/**
|
|
61
|
-
* `img[src]` 的来源规则:与 `CHANGE_IMAGE`、`INSERT_ELEMENT` 同一份(见 `imageSource.ts`)——
|
|
62
|
-
* origin 白名单(客户页自身 + 上传 / 生图域),或本页已出现过的图片地址(整串相等)。
|
|
63
|
-
*/
|
|
64
|
-
export type ReplaceHtmlOptions = ImageSourcePolicy;
|
|
65
58
|
/**
|
|
66
59
|
* 审查并规整一段将**替换掉某个既有元素**的 HTML。
|
|
67
60
|
*
|
|
68
61
|
* 依赖 `DOMParser`(产品页与 jsdom 都有)。拿不到解析器时**一律拒绝** ——
|
|
69
62
|
* 「审不了就放过」是这类闸最糟的降级。
|
|
70
63
|
*/
|
|
71
|
-
export declare function sanitizeReplaceHtml(raw: string
|
|
64
|
+
export declare function sanitizeReplaceHtml(raw: string): ReplaceHtmlOutcome;
|
|
@@ -62,7 +62,7 @@ function isSafeStyleAttr(value) {
|
|
|
62
62
|
const v = value.toLowerCase();
|
|
63
63
|
return !v.includes('javascript:') && !v.includes('expression(') && !v.includes('url(');
|
|
64
64
|
}
|
|
65
|
-
export function sanitizeReplaceHtml(raw
|
|
65
|
+
export function sanitizeReplaceHtml(raw) {
|
|
66
66
|
const html = (raw ?? '').trim();
|
|
67
67
|
if (!html)
|
|
68
68
|
return { ok: false, problem: { reason: 'EMPTY' } };
|
|
@@ -132,13 +132,13 @@ export function sanitizeReplaceHtml(raw, opts) {
|
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
|
-
if (tag === 'img' && !imageSourceAllowed(el.getAttribute('src')
|
|
135
|
+
if (tag === 'img' && !imageSourceAllowed(el.getAttribute('src'))) {
|
|
136
136
|
const src = el.getAttribute('src');
|
|
137
137
|
return {
|
|
138
138
|
ok: false,
|
|
139
139
|
problem: {
|
|
140
140
|
reason: 'UNSAFE_IMAGE_SRC',
|
|
141
|
-
detail: src ? `
|
|
141
|
+
detail: src ? `not an http(s) url: ${src.slice(0, 60)}` : 'img without src'
|
|
142
142
|
}
|
|
143
143
|
};
|
|
144
144
|
}
|