• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pmd / pmd / 915

29 Oct 2026 01:50PM UTC coverage: 80.076% (+0.02%) from 80.056%
915

push

github

web-flow
[core] HTMLRenderers: Properly escape rule name and externalInfoUrl (#7260)

20596 of 26718 branches covered (77.09%)

Branch coverage included in aggregate %.

29 of 31 new or added lines in 2 files covered. (93.55%)

43986 of 53933 relevant lines covered (81.56%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.55
/pmd-core/src/main/java/net/sourceforge/pmd/renderers/HTMLRenderer.java
1
/*
2
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3
 */
4

5
package net.sourceforge.pmd.renderers;
6

7
import java.io.IOException;
8
import java.io.PrintWriter;
9
import java.io.UnsupportedEncodingException;
10
import java.io.Writer;
11
import java.net.URI;
12
import java.net.URISyntaxException;
13
import java.net.URLEncoder;
14
import java.nio.charset.StandardCharsets;
15
import java.util.Iterator;
16
import java.util.List;
17
import java.util.Locale;
18
import java.util.Optional;
19
import java.util.Set;
20

21
import org.apache.commons.lang3.StringEscapeUtils;
22
import org.apache.commons.lang3.StringUtils;
23
import org.slf4j.Logger;
24
import org.slf4j.LoggerFactory;
25

26
import net.sourceforge.pmd.lang.document.FileId;
27
import net.sourceforge.pmd.lang.rule.Rule;
28
import net.sourceforge.pmd.properties.PropertyDescriptor;
29
import net.sourceforge.pmd.properties.PropertyFactory;
30
import net.sourceforge.pmd.reporting.Report;
31
import net.sourceforge.pmd.reporting.Report.ConfigurationError;
32
import net.sourceforge.pmd.reporting.RuleViolation;
33
import net.sourceforge.pmd.util.CollectionUtil;
34

35
/**
36
 * Renderer to basic HTML format.
37
 *
38
 * FIXME: this class should just work with the XMLRenderer and then apply an
39
 * XSLT transformation + stylesheet. No need to hard-code HTML markup here.
40
 */
41
public class HTMLRenderer extends AbstractIncrementingRenderer {
42
    private static final Logger LOGGER = LoggerFactory.getLogger(HTMLRenderer.class);
1 ✔
43

44
    private static final Set<String> ALLOWED_SCHEMES = CollectionUtil.setOf("http", "https");
1 ✔
45

46
    public static final String NAME = "html";
47

48
    public static final PropertyDescriptor<Optional<String>> LINE_PREFIX =
1 ✔
49
        PropertyFactory.stringProperty("linePrefix")
1 ✔
50
                       .desc("Prefix for line number anchor in the source file.")
1 ✔
51
                       .toOptional("<none>")
1 ✔
52
                       .defaultValue(Optional.empty())
1 ✔
53
                       .build();
1 ✔
54

55
    public static final PropertyDescriptor<String> LINK_PREFIX =
1 ✔
56
        PropertyFactory.stringProperty("linkPrefix").desc("Path to HTML source.").defaultValue("").build();
1 ✔
57

58
    public static final PropertyDescriptor<Boolean> HTML_EXTENSION =
1 ✔
59
        PropertyFactory.booleanProperty("htmlExtension")
1 ✔
60
                       .desc("Replace file extension with .html for the links.")
1 ✔
61
                       // default value is false - to have the old (pre 6.23.0) behavior, this needs to be set to true.
62
                       .defaultValue(false)
1 ✔
63
                       .build();
1 ✔
64

65
    private String linkPrefix;
66
    private String linePrefix;
67
    private boolean replaceHtmlExtension;
68

69
    boolean colorize = true;
1 ✔
70

71
    public HTMLRenderer() {
72
        super(NAME, "HTML format");
1 ✔
73

74
        definePropertyDescriptor(LINK_PREFIX);
1 ✔
75
        definePropertyDescriptor(LINE_PREFIX);
1 ✔
76
        definePropertyDescriptor(HTML_EXTENSION);
1 ✔
77
    }
1 ✔
78

79
    @Override
80
    public String defaultFileExtension() {
81
        return "html";
×
82
    }
83

84
    private static String escape(String s) {
85
        return StringEscapeUtils.escapeHtml4(s);
1 ✔
86
    }
87

88
    /**
89
     * Write the body of the main body of the HTML content.
90
     */
91
    public void renderBody(PrintWriter writer, Report report) throws IOException {
92
        linkPrefix = safeUrl(getProperty(LINK_PREFIX));
1 ✔
93
        linePrefix = getProperty(LINE_PREFIX).orElse(null);
1 ✔
94
        replaceHtmlExtension = getProperty(HTML_EXTENSION);
1 ✔
95

96
        writer.write("<center><h3>PMD report</h3></center>");
1 ✔
97
        writer.write("<center><h3>Problems found</h3></center>");
1 ✔
98
        writer.println("<table align=\"center\" cellspacing=\"0\" cellpadding=\"3\"><tr>");
1 ✔
99
        writer.println("<th>#</th><th>File</th><th>Line</th><th>Problem</th></tr>");
1 ✔
100
        setWriter(writer);
1 ✔
101
        renderFileReport(report);
1 ✔
102
        writer.write("</table>");
1 ✔
103
        glomProcessingErrors(writer, errors);
1 ✔
104
        if (showSuppressedViolations) {
1 ✔
105
            glomSuppressions(writer, suppressed);
1 ✔
106
        }
107
        glomConfigurationErrors(writer, configErrors);
1 ✔
108
    }
1 ✔
109

110
    @Override
111
    public void start() throws IOException {
112
        linkPrefix = safeUrl(getProperty(LINK_PREFIX));
1 ✔
113
        linePrefix = getProperty(LINE_PREFIX).orElse(null);
1 ✔
114
        replaceHtmlExtension = getProperty(HTML_EXTENSION);
1 ✔
115

116
        writer.println("<html><head><title>PMD</title></head><body>");
1 ✔
117
        writer.write("<center><h3>PMD report</h3></center>");
1 ✔
118
        writer.write("<center><h3>Problems found</h3></center>");
1 ✔
119
        writer.println("<table align=\"center\" cellspacing=\"0\" cellpadding=\"3\"><tr>");
1 ✔
120
        writer.println("<th>#</th><th>File</th><th>Line</th><th>Problem</th></tr>");
1 ✔
121
    }
1 ✔
122

123
    @Override
124
    public void renderFileViolations(Iterator<RuleViolation> violations) throws IOException {
125
        glomRuleViolations(writer, violations);
1 ✔
126
    }
1 ✔
127

128
    @Override
129
    public void end() throws IOException {
130
        writer.write("</table>");
1 ✔
131
        glomProcessingErrors(writer, errors);
1 ✔
132
        if (showSuppressedViolations) {
1 !
133
            glomSuppressions(writer, suppressed);
1 ✔
134
        }
135
        glomConfigurationErrors(writer, configErrors);
1 ✔
136
        writer.println("</body></html>");
1 ✔
137
    }
1 ✔
138

139
    private void glomRuleViolations(Writer writer, Iterator<RuleViolation> violations) throws IOException {
140
        int violationCount = 1;
1 ✔
141

142
        StringBuilder buf = new StringBuilder(500);
1 ✔
143

144
        while (violations.hasNext()) {
1 ✔
145
            RuleViolation rv = violations.next();
1 ✔
146
            buf.setLength(0);
1 ✔
147
            buf.append("<tr");
1 ✔
148
            if (colorize) {
1 ✔
149
                buf.append(" bgcolor=\"lightgrey\"");
1 ✔
150
            }
151
            colorize = !colorize;
1 ✔
152
            buf.append("> ").append(System.lineSeparator());
1 ✔
153
            buf.append("<td align=\"center\">").append(violationCount).append("</td>").append(System.lineSeparator());
1 ✔
154
            buf.append("<td width=\"*%\">")
1 ✔
155
               .append(renderFileNameEscaped(rv.getFileId(), rv.getBeginLine()))
1 ✔
156
               .append("</td>")
1 ✔
157
                .append(System.lineSeparator());
1 ✔
158
            buf.append("<td align=\"center\" width=\"5%\">").append(rv.getBeginLine()).append("</td>").append(System.lineSeparator());
1 ✔
159

160
            String d = escape(rv.getDescription());
1 ✔
161

162
            String infoUrl = safeUrl(rv.getRule().getExternalInfoUrl());
1 ✔
163
            if (StringUtils.isNotBlank(infoUrl)) {
1 ✔
164
                d = "<a href=\"" + infoUrl + "\">" + d + "</a>";
1 ✔
165
            }
166
            buf.append("<td width=\"*\">")
1 ✔
167
               .append(d)
1 ✔
168
               .append("</td>")
1 ✔
169
               .append(System.lineSeparator())
1 ✔
170
               .append("</tr>")
1 ✔
171
                .append(System.lineSeparator());
1 ✔
172
            writer.write(buf.toString());
1 ✔
173
            violationCount++;
1 ✔
174
        }
1 ✔
175
    }
1 ✔
176

177
    /**
178
     * Converts the given URL to be safe for using as the value of a href attribute.
179
     *
180
     * @param raw the URL to convert
181
     * @return the safe URL or an empty string if the URL is invalid
182
     */
183
    private static String safeUrl(String raw) {
184
        if (raw == null || raw.isEmpty()) {
1 !
185
            return "";
1 ✔
186
        }
187
        URI uri;
188
        try {
189
            uri = new URI(raw); // rejects spaces, tabs, control chars, quotes
1 ✔
190
        } catch (URISyntaxException e) {
1 ✔
191
            LOGGER.debug("Ignoring invalid URL: {}", raw, e);
1 ✔
192
            return "";
1 ✔
193
        }
1 ✔
194

195
        String scheme = uri.getScheme();
1 ✔
196
        if (scheme == null && uri.getRawAuthority() == null) {
1 !
197
            // relative reference: allow paths, but not "//host/..."
198
            return uri.toASCIIString();
1 ✔
199
        }
200

201
        if (scheme == null
1 !
202
                || !ALLOWED_SCHEMES.contains(scheme.toLowerCase(Locale.ROOT))
1 ✔
203
                || uri.getHost() == null) {   // e.g. "https:foo" (opaque)
1 !
204
            LOGGER.debug("Ignoring URL due to invalid scheme: {}", raw);
1 ✔
205
            return "";
1 ✔
206
        }
207
        return uri.toASCIIString();
1 ✔
208
    }
209

210
    private String renderFileNameEscaped(FileId fileId, int beginLine) {
211
        String actualLineSelector = linePrefix == null || beginLine < 0 ? "" : linePrefix + beginLine;
1 ✔
212
        String actualLineSelectorEncoded = null;
1 ✔
213
        try {
214
            actualLineSelectorEncoded = URLEncoder.encode(actualLineSelector, StandardCharsets.UTF_8.name());
1 ✔
NEW
215
        } catch (UnsupportedEncodingException e) {
×
NEW
216
            throw new RuntimeException(e);
×
217
        }
1 ✔
218
        return maybeWrap(escape(determineFileName(fileId)), actualLineSelectorEncoded);
1 ✔
219
    }
220

221
    private String renderRuleNameEscaped(Rule rule) {
222
        String name = escape(rule.getName());
1 ✔
223
        String infoUrl = safeUrl(rule.getExternalInfoUrl());
1 ✔
224
        if (StringUtils.isNotBlank(infoUrl)) {
1 ✔
225
            return "<a href=\"" + infoUrl + "\">" + name + "</a>";
1 ✔
226
        }
227
        return name;
1 ✔
228
    }
229

230
    private void glomProcessingErrors(PrintWriter writer, List<Report.ProcessingError> errors) throws IOException {
231

232
        if (errors.isEmpty()) {
1 ✔
233
            return;
1 ✔
234
        }
235

236
        writer.write("<hr/>");
1 ✔
237
        writer.write("<center><h3>Processing errors</h3></center>");
1 ✔
238
        writer.println("<table align=\"center\" cellspacing=\"0\" cellpadding=\"3\"><tr>");
1 ✔
239
        writer.println("<th>File</th><th>Problem</th></tr>");
1 ✔
240

241
        StringBuilder buf = new StringBuilder(500);
1 ✔
242
        boolean colorize = true;
1 ✔
243
        for (Report.ProcessingError pe : errors) {
1 ✔
244
            buf.setLength(0);
1 ✔
245
            buf.append("<tr");
1 ✔
246
            if (colorize) {
1 !
247
                buf.append(" bgcolor=\"lightgrey\"");
1 ✔
248
            }
249
            colorize = !colorize;
1 !
250
            buf.append("> ").append(System.lineSeparator());
1 ✔
251
            buf.append("<td>").append(renderFileNameEscaped(pe.getFileId(), -1)).append("</td>").append(System.lineSeparator());
1 ✔
252
            buf.append("<td><pre>").append(escape(pe.getDetail())).append("</pre></td>").append(System.lineSeparator());
1 ✔
253
            buf.append("</tr>").append(System.lineSeparator());
1 ✔
254
            writer.write(buf.toString());
1 ✔
255
        }
1 ✔
256
        writer.write("</table>");
1 ✔
257
    }
1 ✔
258

259
    private void glomSuppressions(PrintWriter writer, List<Report.SuppressedViolation> suppressed) throws IOException {
260
        if (suppressed.isEmpty()) {
1 ✔
261
            return;
1 ✔
262
        }
263

264
        writer.write("<hr/>");
1 ✔
265
        writer.write("<center><h3>Suppressed warnings</h3></center>");
1 ✔
266
        writer.println("<table align=\"center\" cellspacing=\"0\" cellpadding=\"3\"><tr>");
1 ✔
267
        writer.println("<th>File</th><th>Line</th><th>Rule</th><th>NOPMD or Annotation</th><th>Reason</th></tr>");
1 ✔
268

269
        StringBuilder buf = new StringBuilder(500);
1 ✔
270
        boolean colorize = true;
1 ✔
271
        for (Report.SuppressedViolation sv : suppressed) {
1 ✔
272
            buf.setLength(0);
1 ✔
273
            buf.append("<tr");
1 ✔
274
            if (colorize) {
1 ✔
275
                buf.append(" bgcolor=\"lightgrey\"");
1 ✔
276
            }
277
            colorize = !colorize;
1 ✔
278
            buf.append("> ").append(System.lineSeparator());
1 ✔
279
            RuleViolation rv = sv.getRuleViolation();
1 ✔
280
            String userMessage = Optional.ofNullable(sv.getUserMessage()).orElse("");
1 ✔
281
            buf.append("<td align=\"left\">").append(renderFileNameEscaped(rv.getFileId(), rv.getBeginLine())).append("</td>").append(System.lineSeparator());
1 ✔
282
            buf.append("<td align=\"center\">").append(rv.getBeginLine()).append("</td>").append(System.lineSeparator());
1 ✔
283
            buf.append("<td align=\"center\">").append(renderRuleNameEscaped(rv.getRule())).append("</td>").append(System.lineSeparator());
1 ✔
284
            buf.append("<td align=\"center\">").append(escape(sv.getSuppressor().getId())).append("</td>").append(System.lineSeparator());
1 ✔
285
            buf.append("<td align=\"center\">").append(escape(userMessage)).append("</td>").append(System.lineSeparator());
1 ✔
286
            buf.append("</tr>").append(System.lineSeparator());
1 ✔
287
            writer.write(buf.toString());
1 ✔
288
        }
1 ✔
289
        writer.write("</table>");
1 ✔
290
    }
1 ✔
291

292
    private void glomConfigurationErrors(final PrintWriter writer, final List<ConfigurationError> configErrors) throws IOException {
293
        if (configErrors.isEmpty()) {
1 ✔
294
            return;
1 ✔
295
        }
296

297
        writer.write("<hr/>");
1 ✔
298
        writer.write("<center><h3>Configuration errors</h3></center>");
1 ✔
299
        writer.println("<table align=\"center\" cellspacing=\"0\" cellpadding=\"3\"><tr>");
1 ✔
300
        writer.println("<th>Rule</th><th>Problem</th></tr>");
1 ✔
301

302
        StringBuilder buf = new StringBuilder(500);
1 ✔
303
        boolean colorize = true;
1 ✔
304
        for (Report.ConfigurationError ce : configErrors) {
1 ✔
305
            buf.setLength(0);
1 ✔
306
            buf.append("<tr");
1 ✔
307
            if (colorize) {
1 !
308
                buf.append(" bgcolor=\"lightgrey\"");
1 ✔
309
            }
310
            colorize = !colorize;
1 !
311
            buf.append("> ").append(System.lineSeparator());
1 ✔
312
            buf.append("<td>").append(renderRuleNameEscaped(ce.rule())).append("</td>").append(System.lineSeparator());
1 ✔
313
            buf.append("<td>").append(escape(ce.issue())).append("</td>").append(System.lineSeparator());
1 ✔
314
            buf.append("</tr>").append(System.lineSeparator());
1 ✔
315
            writer.write(buf.toString());
1 ✔
316
        }
1 ✔
317
        writer.write("</table>");
1 ✔
318
    }
1 ✔
319

320
    private String maybeWrap(String filenameEscaped, String line) {
321
        if (StringUtils.isBlank(linkPrefix)) {
1 ✔
322
            return filenameEscaped;
1 ✔
323
        }
324
        String newFileName = filenameEscaped.replace('\\', '/');
1 ✔
325

326
        if (replaceHtmlExtension) {
1 ✔
327
            int index = filenameEscaped.lastIndexOf('.');
1 ✔
328
            if (index >= 0) {
1 !
329
                newFileName = filenameEscaped.substring(0, index);
×
330
            }
331
        }
332

333
        return "<a href=\"" + linkPrefix + newFileName + (replaceHtmlExtension ? ".html#" : "#") + line + "\">"
1 ✔
334
            + newFileName + "</a>";
335
    }
336
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc