package cn.iocoder.yudao.module.qcreport.engine.render;
import cn.hutool.core.util.StrUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
/**
* 画布安全性校验:模板保存时把「渲染时会原样拼进 HTML 的内容」先卡一遍。
*
* 报告产物是拼字符串拼出来的({@link HtmlRenderer}),模板画布又是用户可编辑数据,
* 所以画布里凡是未经转义就进入产物的内容都是注入面。逐条列明它们,改动渲染或本类时两边一起看:
*
* - {@code resolveTag} —— 节点的 {@code tagName} 直接拼成 {@code },本类按白名单卡。
* - {@code renderAttributes} —— 只对属性值做转义,属性名是直接拼上去的,
* 一个含引号的属性名就能把后面的内容顶成新属性,本类按字符集卡。
* - {@code buildCss} —— {@code styles} 若是字符串会被原样返回(整个 {@code
*
*
* 为什么在保存时卡而不是渲染时卡:渲染链路上的 {@link HtmlRenderer} 与前端
* {@code engine/render.ts} 必须逐字一致,改它就要重算对拍产物、并让存量报告的 {@code regenerate}
* 不再逐字复现;而「入站内容是否可信」本来就是写入侧的问题,在数据进库前拦掉,渲染侧可以继续
* 保持与前端完全对称。代价是对校验上线前就已入库的脏数据没有兜底——存量版本需要另行核查。
*
* 为什么走的是白名单而不是过滤危险标签:黑名单永远漏,而报告排版用得到的标签是有限的、
* 可枚举的。正常模板不会因为这条白名单被拒(已对库里全部存量版本实测过)。
*/
public final class CanvasSafety {
/**
* 允许出现在画布里的标签。
*
* 只收「排版与文本」类标签:组件注册表能产出的({@code div/p/span/hr/img/h1~h6} 与表格一族)
* 加上 GrapesJS 基础组件与常见行内语义标签。刻意排除四类——
* 可执行({@code script})、可注入样式({@code style})、可嵌入外部内容({@code iframe/object/embed})、
* 可提交数据({@code form} 及其控件),以及 {@code svg/math} 这类自带脚本能力的命名空间标签。
*
* GrapesJS 会在组件上挂 {@code docEl: {tagName: "html"}}、{@code head: {type: "head"}} 这类
* 不会渲染的元数据,所以本类只沿 {@code components} 走(与 {@code RenderNode.children()} 一致),
* 白名单里也就不需要 {@code html}/{@code head}。
*/
private static final Set ALLOWED_TAGS = Set.of(
// 区块与容器
"div", "span", "section", "article", "header", "footer", "main", "aside", "nav",
"figure", "figcaption", "blockquote", "pre", "address", "center",
// 文本与标题
"p", "br", "hr", "h1", "h2", "h3", "h4", "h5", "h6",
"b", "i", "u", "s", "strong", "em", "small", "big", "sub", "sup", "mark",
"code", "kbd", "samp", "var", "tt", "strike", "font", "wbr", "bdi", "bdo", "ruby", "rt", "rp",
"abbr", "cite", "q", "time", "del", "ins",
// 列表
"ul", "ol", "li", "dl", "dt", "dd",
// 表格
"table", "thead", "tbody", "tfoot", "tr", "td", "th", "caption", "col", "colgroup",
// 图片与链接
"img", "a", "label");
/** 合法的属性名:字母/下划线/冒号开头,其后字母数字与 {@code - _ : .}。含引号或空格的属性名一律拒绝 */
private static final Pattern ATTRIBUTE_NAME = Pattern.compile("^[A-Za-z_:][-A-Za-z0-9_:.]*$");
/** 最多报几条:一条报错信息里堆几十条没人看得下去,改完再存一次就能看到下一批 */
private static final int MAX_PROBLEMS = 8;
/** 路径展示的最大层数,再深就折叠成 …,避免一条提示里出现一长串「第 N 个组件」 */
private static final int MAX_PATH_DEPTH = 6;
private static final String UNSAFE_LT = "<";
private CanvasSafety() {
}
/**
* 校验一份画布。
*
* @param grapes GradesJS 项目数据(模板 Schema 的 {@code grapes} 字段),可为 null
* @return 问题清单,空列表代表通过。文案已可直接拼给用户看
*/
public static List validate(Map grapes) {
List problems = new ArrayList<>();
if (grapes == null) {
return problems;
}
validateComponentTree(grapes, problems);
validateStyles(grapes.get("styles"), problems);
return problems;
}
/* ------------------------------ 组件树 ------------------------------ */
private static void validateComponentTree(Map grapes, List problems) {
for (Map frameComponent : frameComponents(grapes)) {
walk(frameComponent, "根", 0, problems);
}
}
/** 取所有页所有 frame 的根组件。渲染器只读 {@code pages[0].frames[0]},这里全查一遍:多出来的部分也不能藏脏东西 */
private static List