package com.ruoyi.system.utils; import com.ruoyi.common.utils.html.HTMLFilter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 协议富文本白名单清洗工具。 * * @author ruoyi */ public final class ProtocolHtmlSanitizer { private static final Pattern DANGEROUS_ELEMENT_PATTERN = Pattern.compile( "<(script|style|iframe|object|embed|link|meta)[^>]*>.*?", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); private static final Pattern DANGEROUS_SINGLE_ELEMENT_PATTERN = Pattern.compile( "<(script|style|iframe|object|embed|link|meta)[^>]*?/?>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); private static final Pattern STYLE_ATTRIBUTE_PATTERN = Pattern.compile( "\\sstyle\\s*=\\s*(\"([^\"]*)\"|'([^']*)'|([^\\s>]+))", Pattern.CASE_INSENSITIVE); private static final Pattern SAFE_COLOR_PATTERN = Pattern.compile("#[0-9a-fA-F]{3,8}"); private static final Pattern SAFE_FONT_SIZE_PATTERN = Pattern.compile( "(?:xx-small|x-small|small|medium|large|x-large|xx-large|[1-9][0-9]{0,2}px)", Pattern.CASE_INSENSITIVE); private static final Pattern SAFE_FONT_WEIGHT_PATTERN = Pattern.compile("(?:bold|normal|[1-9]00)", Pattern.CASE_INSENSITIVE); private static final Pattern SAFE_FONT_STYLE_PATTERN = Pattern.compile("(?:italic|normal)", Pattern.CASE_INSENSITIVE); private ProtocolHtmlSanitizer() { } /** * 仅保留协议编辑器支持的安全 HTML 标签和格式。 * * @param html 原始协议 HTML * @return 清洗后的协议 HTML */ public static String clean(String html) { if (html == null) { return null; } String content = DANGEROUS_ELEMENT_PATTERN.matcher(html).replaceAll(""); content = DANGEROUS_SINGLE_ELEMENT_PATTERN.matcher(content).replaceAll(""); content = sanitizeStyleAttributes(content); return createFilter().filter(content).replace("\\\"", "\""); } private static String sanitizeStyleAttributes(String html) { Matcher matcher = STYLE_ATTRIBUTE_PATTERN.matcher(html); StringBuffer result = new StringBuffer(); while (matcher.find()) { String style = matcher.group(2) != null ? matcher.group(2) : matcher.group(3) != null ? matcher.group(3) : matcher.group(4); String safeStyle = sanitizeStyle(style); String replacement = safeStyle.isEmpty() ? "" : " style=\"" + safeStyle + "\""; matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); } matcher.appendTail(result); return result.toString(); } private static String sanitizeStyle(String style) { List safeDeclarations = new ArrayList<>(); for (String declaration : style.split(";")) { String[] parts = declaration.split(":", 2); if (parts.length != 2) { continue; } String property = parts[0].trim().toLowerCase(); String value = parts[1].trim(); if ("text-align".equals(property) && Arrays.asList("left", "center", "right", "justify").contains(value.toLowerCase())) { safeDeclarations.add("text-align: " + value.toLowerCase()); } else if ("color".equals(property) && SAFE_COLOR_PATTERN.matcher(value).matches()) { safeDeclarations.add("color: " + value.toLowerCase()); } else if ("font-size".equals(property) && SAFE_FONT_SIZE_PATTERN.matcher(value).matches()) { safeDeclarations.add("font-size: " + value.toLowerCase()); } else if ("font-weight".equals(property) && SAFE_FONT_WEIGHT_PATTERN.matcher(value).matches()) { safeDeclarations.add("font-weight: " + value.toLowerCase()); } else if ("font-style".equals(property) && SAFE_FONT_STYLE_PATTERN.matcher(value).matches()) { safeDeclarations.add("font-style: " + value.toLowerCase()); } } return String.join("; ", safeDeclarations); } private static HTMLFilter createFilter() { HashMap> allowed = new HashMap<>(); List styleAttribute = Collections.singletonList("style"); for (String tag : Arrays.asList("div", "p", "span", "ul", "ol", "li", "blockquote", "pre", "h1", "h2", "h3", "h4", "h5", "h6")) { allowed.put(tag, styleAttribute); } for (String tag : Arrays.asList("strong", "b", "em", "i", "br")) { allowed.put(tag, Collections.emptyList()); } Map config = new HashMap<>(); config.put("vAllowed", allowed); config.put("vSelfClosingTags", new String[] { "br" }); config.put("vNeedClosingTags", new String[] { "div", "p", "span", "ul", "ol", "li", "blockquote", "pre", "h1", "h2", "h3", "h4", "h5", "h6", "strong", "b", "em", "i" }); config.put("vDisallowed", new String[] {}); config.put("vAllowedProtocols", new String[] {}); config.put("vProtocolAtts", new String[] {}); config.put("vRemoveBlanks", new String[] {}); config.put("vAllowedEntities", new String[] { "amp", "gt", "lt", "quot", "nbsp" }); config.put("stripComment", true); config.put("encodeQuotes", false); config.put("alwaysMakeTags", false); return new HTMLFilter(config); } }