zjk
2026-07-31 8665daa7aa863f96d0d8f27c167edae1efc38022
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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);
    }
}