-
-
Notifications
You must be signed in to change notification settings - Fork 289
支持自定义html的插入,并且修改bug,实现更为安全,同时当存在多个顶层元素时也可以添加 #760
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2b2c15c
dd325f1
6b79195
7bbbf0c
eb29a9e
100d5e8
7e094de
1f4665d
fb5f25a
4fa4c7d
de56134
3813934
86a8ff9
f5fad55
38debef
b0b0032
bbfff22
96a3146
2f25898
189417f
6d7257e
19c85a4
669bed8
d12f0f1
f08b771
38a0a0d
9dd45d4
563bc06
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,6 +39,7 @@ export enum commandType { | |
getUserInput, | ||
applyStyle, | ||
wait, | ||
setCustomHtml, // 设置自定义HTML | ||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,6 +39,7 @@ export enum commandType { | |
getUserInput, | ||
applyStyle, | ||
wait, | ||
setCustomHtml, // 设置自定义html | ||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
import { ISentence } from '@/Core/controller/scene/sceneInterface'; | ||
import { IPerform } from '@/Core/Modules/perform/performInterface'; | ||
import { webgalStore } from '@/store/store'; | ||
import { stageActions } from '@/store/stageReducer'; | ||
import React from 'react'; | ||
|
||
// 工具函数:将css字符串转为对象,并返回 feature 字段 | ||
function parseCssString(css: string): { styleObj: Record<string, string>; feature?: string } { | ||
const styleObj: Record<string, string> = {}; | ||
let feature: string | undefined; | ||
css | ||
.replace(/[{}]/g, '') | ||
.split(',') | ||
.map((s) => s.trim()) | ||
.filter(Boolean) | ||
.forEach((item) => { | ||
const [key, value] = item.split(':').map((s) => s.trim()); | ||
if (key && value) { | ||
if (key === 'feature') { | ||
feature = value; | ||
} else { | ||
// 驼峰化 | ||
const camelKey = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); | ||
styleObj[camelKey] = value; | ||
} | ||
} | ||
}); | ||
return { styleObj, feature }; | ||
} | ||
|
||
// 解析HTML元素,提取标签名、属性和内容 | ||
interface ParsedElement { | ||
tagName: string; | ||
attributes: Record<string, string>; | ||
innerHTML: string; | ||
children: ParsedElement[]; | ||
selfClosing: boolean; // 标记是否为自闭合标签 | ||
} | ||
|
||
// 自动添加绝对定位 | ||
function autoAddPositionAbsolute(style: string): string { | ||
// 只要有 left/top/width/height 且没有 position,就加上 | ||
if (!/position\s*:/.test(style) && /(left\s*:|top\s*:|right\s*:|bottom\s*:|width\s*:|height\s*:)/.test(style)) { | ||
// 逗号分隔 | ||
return `position: absolute,pointer-events: none, ${style}`; | ||
} | ||
return style; | ||
} | ||
|
||
// 将单个解析后的元素转换为带样式的HTML字符串 | ||
// 支持返回 feature 字段 | ||
function convertElementToStyledHtml(element: ParsedElement): { html: string; feature?: string } { | ||
let feature: string | undefined; | ||
let html = `<${element.tagName}`; | ||
|
||
// 处理style属性 | ||
if (element.attributes.style) { | ||
element.attributes.style = autoAddPositionAbsolute(element.attributes.style); | ||
const { styleObj, feature: f } = parseCssString(element.attributes.style); | ||
if (f && !feature) feature = f; | ||
const cssString = Object.entries(styleObj) | ||
.map(([key, value]) => { | ||
const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase(); | ||
return `${cssKey}: ${value}`; | ||
}) | ||
.join('; '); | ||
html += ` style="${cssString}"`; | ||
} | ||
|
||
// 处理其他属性,但过滤掉所有on*事件处理器以防止XSS | ||
Object.entries(element.attributes).forEach(([key, value]) => { | ||
// 禁止所有on*事件处理器属性,防止XSS攻击 | ||
if (key !== 'style' && !key.startsWith('on')) { | ||
html += ` ${key}="${value}"`; | ||
} | ||
}); | ||
|
||
// 处理自闭合标签 | ||
if (element.selfClosing) { | ||
html += '/>'; | ||
} else { | ||
html += '>'; | ||
// 添加子元素 | ||
if (element.children.length > 0) { | ||
// 递归处理所有子元素 | ||
const childResults = element.children.map((child) => convertElementToStyledHtml(child)); | ||
html += childResults | ||
.map((result) => { | ||
if (result.feature && !feature) feature = result.feature; | ||
return result.html; | ||
}) | ||
.join(''); | ||
} else if (element.innerHTML) { | ||
html += element.innerHTML; | ||
} | ||
html += `</${element.tagName}>`; | ||
} | ||
|
||
return { html, feature }; | ||
} | ||
|
||
// 解析HTML字符串,提取所有元素及其样式 | ||
function parseHtmlWithStyles(htmlInput: string): ParsedElement[] { | ||
// 使用DOMParser解析HTML字符串,更加安全和可靠 | ||
const parser = new DOMParser(); | ||
const doc = parser.parseFromString(htmlInput, 'text/html'); | ||
|
||
// 递归遍历节点并转换为ParsedElement结构 | ||
function parseNode(node: Node): ParsedElement[] { | ||
const elements: ParsedElement[] = []; | ||
|
||
// 遍历所有子节点 | ||
node.childNodes.forEach((child) => { | ||
if (child.nodeType === Node.ELEMENT_NODE) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 存在严重的安全漏洞。当前实现没有对 HTML 进行充分的净化,允许执行任意 JavaScript 代码(XSS)。由于最终内容会通过 具体来说:
强烈建议使用成熟的 HTML 净化库(如 例如,在 const ALLOWED_TAGS = ['div', 'span', 'p', 'b', 'i', 'u', 'strong', 'em'];
// ...
if (child.nodeType === Node.ELEMENT_NODE) {
const element = child as HTMLElement;
const tagName = element.tagName.toLowerCase();
if (!ALLOWED_TAGS.includes(tagName)) {
// 跳过或移除不允许的标签
return;
}
// ...
} |
||
const element = child as HTMLElement; | ||
const tagName = element.tagName.toLowerCase(); | ||
const attributes: Record<string, string> = {}; | ||
|
||
// 提取所有属性 | ||
for (const attr of element.attributes) { | ||
attributes[attr.name] = attr.value; | ||
} | ||
|
||
// 判断是否为自闭合标签 | ||
const selfClosingTags = [ | ||
'img', | ||
'br', | ||
'hr', | ||
'input', | ||
'link', | ||
'meta', | ||
'area', | ||
'base', | ||
'col', | ||
'embed', | ||
'source', | ||
'track', | ||
'wbr', | ||
]; | ||
const isSelfClosing = | ||
selfClosingTags.includes(tagName) || (element.childNodes.length === 0 && !element.textContent); | ||
|
||
// 递归处理子元素(如果不是自闭合标签) | ||
const children = isSelfClosing ? [] : parseNode(element); | ||
|
||
// 获取innerHTML内容 | ||
let innerHTML = ''; | ||
if (children.length === 0 && !isSelfClosing) { | ||
innerHTML = element.innerHTML; | ||
} | ||
|
||
elements.push({ | ||
tagName, | ||
attributes, | ||
innerHTML, | ||
children, | ||
selfClosing: isSelfClosing, | ||
}); | ||
} | ||
}); | ||
|
||
return elements; | ||
} | ||
|
||
// 从body开始解析(因为DOMParser会自动包装HTML) | ||
return parseNode(doc.body); | ||
} | ||
|
||
export const setCustomHtml = (sentence: ISentence): IPerform => { | ||
const removeMatch = sentence.content.match(/^remove\((\d+)\)$/); | ||
if (removeMatch) { | ||
const idx = parseInt(removeMatch[1], 10) - 1 || 0; | ||
webgalStore.dispatch(stageActions.removeCustomHtml(idx)); | ||
return { | ||
performName: 'none', | ||
duration: 0, | ||
isHoldOn: false, | ||
stopFunction: () => {}, | ||
blockingNext: () => false, | ||
blockingAuto: () => true, | ||
stopTimeout: undefined, | ||
}; | ||
} | ||
|
||
// 直接解析HTML中的样式(不再支持<div>content</div> {css}格式) | ||
const html = sentence.content.trim(); | ||
|
||
// 解析HTML并处理内联样式 | ||
const elements = parseHtmlWithStyles(html); | ||
|
||
// 为每个元素单独处理和分发action | ||
elements.forEach((element) => { | ||
const { html: styledHtml, feature } = convertElementToStyledHtml(element); | ||
|
||
// 直接从元素的attributes中提取样式,避免再次解析HTML字符串 | ||
let style: React.CSSProperties = { position: 'absolute' }; | ||
if (element.attributes.style) { | ||
const processedStyle = autoAddPositionAbsolute(element.attributes.style); | ||
const { styleObj } = parseCssString(processedStyle); | ||
// 将styleObj转换为React.CSSProperties | ||
Object.keys(styleObj).forEach((key) => { | ||
// 使用类型断言来避免TypeScript错误 | ||
(style as any)[key] = styleObj[key]; | ||
}); | ||
} | ||
|
||
// 添加到状态管理,带 feature 字段和style对象 | ||
webgalStore.dispatch(stageActions.addCustomHtml({ html: styledHtml, _feature: feature, style })); | ||
}); | ||
Comment on lines
+192
to
+209
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
return { | ||
performName: 'none', | ||
duration: 0, | ||
isHoldOn: false, | ||
stopFunction: () => {}, | ||
blockingNext: () => false, | ||
blockingAuto: () => true, | ||
stopTimeout: undefined, | ||
}; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
在
parseCssString
函数中,您使用逗号,
作为 CSS 属性的分隔符。这不是标准的 CSS 语法,会导致在处理包含逗号的合法 CSS 值(例如font-family: "Times New Roman", serif
或box-shadow
的多重阴影)时解析错误。这使得该功能不够健壮,并限制了其可用性。建议遵循标准的 CSS 语法,使用分号
;
作为属性分隔符。这样开发者可以直接使用标准的 CSS,也使功能更加可靠。对应的,demo_zh_cn.txt
中的setCustomHtml
命令也需要更新。