package com.yunhai.svchall.utils; import org.jsoup.Jsoup; import org.jsoup.safety.Safelist; import java.util.regex.Pattern; /** * 富文本白名单清洗(Cms HtmlSanitizer 原样平移:jsoup Safelist.relaxed() 基础 + * figure/figcaption/span + 受限 style + a/img 协议收紧 + preserveRelativeLinks)。 * script/iframe/on* 一律剥除;列表页纯文本字段 th:text 等价转义,utext 仅限本工具清洗过的内容。 */ public final class HtmlSanitizer { /** 允许保留的内联样式属性(排版用途),其余 style 值丢弃 */ private static final Pattern SAFE_STYLE = Pattern.compile("^(\\s*(text-align|color|background-color|font-size|font-weight" + "|line-height|width|height|max-width|float|margin|padding)\\s*:\\s*[^;()]+;?)*\\s*$", Pattern.CASE_INSENSITIVE); private static final Safelist WHITELIST = Safelist.relaxed() .addTags("figure", "figcaption", "span") .addAttributes(":all", "style") .addAttributes("img", "style", "alt", "width", "height") .addProtocols("a", "href", "http", "https", "mailto", "ftp", "#") .addProtocols("img", "src", "http", "https", "data") // 相对链接保留(Cms B-9 修复平移):上传地址相对路径入库,编辑器贴图 img src 即相对 URL .preserveRelativeLinks(true); private HtmlSanitizer() { } /** * 清洗富文本;空入参原样返回。baseUri 用占位 https 域(Cms B-9 根因修复配套, * 配合 preserveRelativeLinks(true) 回写原始相对值)。 */ public static String clean(String html) { if (html == null || html.isEmpty()) { return html; } String cleaned = Jsoup.clean(html, "https://sanitizer.base.invalid/", WHITELIST); return stripUnsafeStyle(cleaned); } /** 纯文本提取(摘要兜底/搜索片段用):去标签 + 折叠空白 */ public static String toText(String html, int maxLength) { if (html == null || html.isEmpty()) { return ""; } String text = Jsoup.parse(html).text().replaceAll("\\s+", " ").trim(); if (maxLength > 0 && text.length() > maxLength) { return text.substring(0, maxLength); } return text; } /** jsoup 不支持按值过滤属性,二次遍历按 SAFE_STYLE 白名单过滤 style(Cms A-07 修复平移) */ private static String stripUnsafeStyle(String html) { org.jsoup.nodes.Document doc = org.jsoup.Jsoup.parseBodyFragment(html); for (org.jsoup.nodes.Element el : doc.select("[style]")) { String style = el.attr("style"); if (style == null || !SAFE_STYLE.matcher(style).matches()) { el.removeAttr("style"); } } return doc.body().html(); } }