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)[^>]*>.*?</\\1\\s*>",
|
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<String> 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<String, List<String>> allowed = new HashMap<>();
|
List<String> 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<String, Object> 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);
|
}
|
}
|