1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.codelibs.fess.helper;
17
18 import java.util.Arrays;
19
20 import jakarta.annotation.PostConstruct;
21
22 import org.apache.logging.log4j.LogManager;
23 import org.apache.logging.log4j.Logger;
24 import org.commonmark.Extension;
25 import org.commonmark.ext.gfm.tables.TablesExtension;
26 import org.commonmark.parser.Parser;
27 import org.commonmark.renderer.html.HtmlRenderer;
28 import org.owasp.html.HtmlPolicyBuilder;
29 import org.owasp.html.PolicyFactory;
30
31
32
33
34
35 public class MarkdownRenderer {
36
37 private static final Logger logger = LogManager.getLogger(MarkdownRenderer.class);
38
39 private Parser markdownParser;
40 private HtmlRenderer htmlRenderer;
41 private PolicyFactory htmlSanitizer;
42
43
44
45
46 public MarkdownRenderer() {
47
48 }
49
50
51
52
53 @PostConstruct
54 public void init() {
55
56 final Iterable<Extension> extensions = Arrays.asList(TablesExtension.create());
57
58 markdownParser = Parser.builder().extensions(extensions).build();
59
60 htmlRenderer = HtmlRenderer.builder().extensions(extensions).softbreak("<br/>").build();
61
62
63 htmlSanitizer = new HtmlPolicyBuilder()
64
65 .allowElements("h1", "h2", "h3", "h4", "h5", "h6")
66
67 .allowElements("p", "br", "hr")
68 .allowElements("strong", "em", "b", "i", "u", "s", "del")
69
70 .allowElements("ul", "ol", "li")
71
72 .allowElements("code", "pre")
73
74 .allowElements("blockquote")
75
76 .allowElements("table", "thead", "tbody", "tr", "th", "td")
77
78 .allowElements("a")
79 .allowUrlProtocols("http", "https")
80 .allowAttributes("href")
81 .onElements("a")
82 .requireRelNofollowOnLinks()
83
84 .allowElements("img")
85 .allowUrlProtocols("http", "https")
86 .allowAttributes("src", "alt", "title")
87 .onElements("img")
88
89 .allowElements("span", "div")
90
91 .allowAttributes("class")
92 .onElements("code", "pre", "span", "div")
93 .toFactory();
94
95 if (logger.isDebugEnabled()) {
96 logger.debug("MarkdownRenderer initialized with commonmark and OWASP sanitizer");
97 }
98 }
99
100
101
102
103
104
105
106 public String render(final String markdown) {
107 if (markdown == null || markdown.isEmpty()) {
108 return "";
109 }
110
111 try {
112
113 final var document = markdownParser.parse(markdown);
114
115
116 final String html = htmlRenderer.render(document);
117
118
119 final String sanitizedHtml = htmlSanitizer.sanitize(html);
120
121 if (logger.isDebugEnabled()) {
122 logger.debug("Rendered markdown. inputLength={}, outputLength={}", markdown.length(), sanitizedHtml.length());
123 }
124
125 return sanitizedHtml;
126 } catch (final Exception e) {
127 logger.warn("Failed to render markdown, returning escaped plain text. error={}", e.getMessage());
128
129 return escapeHtml(markdown);
130 }
131 }
132
133
134
135
136
137
138
139 private String escapeHtml(final String text) {
140 if (text == null) {
141 return "";
142 }
143 return text.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """).replace("'", "'");
144 }
145
146
147
148
149
150
151 public boolean isInitialized() {
152 return markdownParser != null && htmlRenderer != null && htmlSanitizer != null;
153 }
154 }