Skip to content

Commit e26b6f3

Browse files
Merge pull request #13937 from SORMAS-Foundation/bugfix-13935-case-classification-info-format-issue
Displaying the case definition text with the actual format.
2 parents f9bdb09 + d24227b commit e26b6f3

2 files changed

Lines changed: 85 additions & 15 deletions

File tree

sormas-api/src/main/java/de/symeda/sormas/api/utils/HtmlHelper.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package de.symeda.sormas.api.utils;
1919

2020
import org.jsoup.Jsoup;
21+
import org.jsoup.nodes.Document;
2122
import org.jsoup.safety.Safelist;
2223

2324
// This class provides general XSS-Prevention methods using Jsoup.clean
@@ -55,8 +56,30 @@ public static String cleanI18nString(String string) {
5556
return (string == null) ? "" : Jsoup.clean(string, Safelist.basic());
5657
}
5758

59+
/**
60+
* to whitelist html tags in {@code htmlText} to prevent HTML injection.
61+
*
62+
* @param htmlText
63+
* @param whitelist
64+
* @return
65+
*/
66+
public static String cleanHtmlRelaxed(String htmlText, Safelist whitelist) {
67+
Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false);
68+
return Jsoup.clean(htmlText, "", whitelist, outputSettings);
69+
}
70+
5871
public static String cleanHtmlRelaxed(String string) {
59-
return (string == null) ? "" : Jsoup.clean(string, Safelist.relaxed());
72+
return (string == null)
73+
? ""
74+
: Jsoup.clean(
75+
string,
76+
Safelist.relaxed()
77+
.addTags("u", "font")
78+
.addAttributes("font", "size", "color")
79+
.addAttributes("span", "style")
80+
.addAttributes("p", "style")
81+
.addAttributes("div", "style")
82+
.addAttributes("font", "style"));
6083
}
6184

6285
/**

sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseDataForm.java

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646

4747
import org.apache.commons.collections4.CollectionUtils;
4848
import org.apache.commons.lang3.StringUtils;
49+
import org.jsoup.safety.Safelist;
4950

5051
import com.vaadin.icons.VaadinIcons;
5152
import com.vaadin.server.ErrorMessage;
@@ -134,6 +135,7 @@
134135
import de.symeda.sormas.api.utils.DataHelper;
135136
import de.symeda.sormas.api.utils.DateHelper;
136137
import de.symeda.sormas.api.utils.ExtendedReduced;
138+
import de.symeda.sormas.api.utils.HtmlHelper;
137139
import de.symeda.sormas.api.utils.YesNoUnknown;
138140
import de.symeda.sormas.api.utils.fieldvisibility.FieldVisibilityCheckers;
139141
import de.symeda.sormas.api.utils.fieldvisibility.checkers.CountryFieldVisibilityChecker;
@@ -220,8 +222,9 @@ public class CaseDataForm extends AbstractEditForm<CaseDataDto> {
220222
public static final String DIAGNOSIS_CRITERIA_HEADING_LOC = "diagnosisCriteriaHeadingLoc";
221223
public static final String DIAGNOSIS_CRITERIA_SUBHEADING_LOC = "diagnosisCriteriaSubheadingLoc";
222224
public static final String DIAGNOSIS_CRITERIA_LAB_TEST_PANEL_LOC = "diagnosisCriteriaLoc";
223-
private static final Pattern URL_PATTERN = Pattern.compile("((https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|])");
224-
225+
private static final Pattern RICH_TEXT_OR_URL_PATTERN = Pattern.compile(
226+
"(<\\/?[a-zA-Z0-9]+(?:\\s+[a-zA-Z0-9\\-]+(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^'\\\">\\s]+))?)*\\s*\\/?>)|(https?://[^<\\s]+)",
227+
Pattern.CASE_INSENSITIVE);
225228
//@formatter:off
226229
private static final String MAIN_HTML_LAYOUT =
227230
loc(CASE_DATA_HEADING_LOC) +
@@ -1647,27 +1650,71 @@ private void getManualCaseDefinition() {
16471650
* @return sanitized url
16481651
*/
16491652
private String sanitizeAndLinkify(String text) {
1650-
Matcher matcher = URL_PATTERN.matcher(text);
1653+
if (text == null || text.isEmpty()) {
1654+
return "";
1655+
}
1656+
String htmlText = unescapeHtml(text);
1657+
// Leveraging existing codebase tool to strip ALL unapproved tags,
1658+
Safelist customizedSafelist = Safelist.relaxed()
1659+
.addTags("u", "font")
1660+
.addAttributes("font", "size", "color")
1661+
.addAttributes("span", "style")
1662+
.addAttributes("p", "style")
1663+
.addAttributes("div", "style")
1664+
.addAttributes("font", "style")
1665+
.addAttributes("a", "href", "target", "rel", "style")
1666+
.addEnforcedAttribute("a", "target", "_blank")
1667+
.addEnforcedAttribute("a", "rel", "noopener noreferrer");
1668+
1669+
String sanitizedText = HtmlHelper.cleanHtmlRelaxed(htmlText, customizedSafelist);
1670+
Matcher matcher = RICH_TEXT_OR_URL_PATTERN.matcher(sanitizedText);
16511671
StringBuilder result = new StringBuilder();
16521672
int last = 0;
16531673

16541674
while (matcher.find()) {
1655-
result.append(escapeHtml(text.substring(last, matcher.start())));
1656-
1657-
String escapedUrl = escapeHtml(matcher.group(1));
1658-
result.append("<a href=\"")
1659-
.append(escapedUrl)
1660-
.append("\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color: `#197de1`; text-decoration: underline;\">")
1661-
.append(escapedUrl)
1662-
.append("</a>");
1663-
1675+
result.append(sanitizedText, last, matcher.start());
1676+
1677+
String htmlTag = matcher.group(1);
1678+
String url = matcher.group(2);
1679+
if (htmlTag != null) {
1680+
// This is a rich text tag verified clean by Jsoup. Pass it through safely.
1681+
result.append(htmlTag);
1682+
} else if (url != null) {
1683+
// It's a plain-text URL. Wrap it in your custom blue link styling.
1684+
String escapedUrl = escapeHtml(url);
1685+
result.append("<a href=\"")
1686+
.append(escapedUrl)
1687+
.append("\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color: #197de1; text-decoration: underline;\">")
1688+
.append(escapedUrl)
1689+
.append("</a>");
1690+
}
16641691
last = matcher.end();
16651692
}
1666-
1667-
result.append(escapeHtml(text.substring(last)));
1693+
result.append(sanitizedText.substring(last));
16681694
return result.toString();
16691695
}
16701696

1697+
/**
1698+
* Replacing any escape sequence with the character that it represents.
1699+
*
1700+
* @param value
1701+
* @return String
1702+
*/
1703+
private String unescapeHtml(String value) {
1704+
if (value == null)
1705+
return "";
1706+
// First, convert any double-escaped amps (e.g., &amp;lt; becomes &lt;)
1707+
String step1 = value.replace("&amp;", "&");
1708+
// Now, safely convert standard HTML entities to real brackets
1709+
return step1.replace("&lt;", "<").replace("&gt;", ">").replace("&quot;", "\"").replace("&#39;", "'");
1710+
}
1711+
1712+
/**
1713+
* Converting special characters in a string into their safe HTML entity values
1714+
*
1715+
* @param value
1716+
* @return
1717+
*/
16711718
private static String escapeHtml(String value) {
16721719
return value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;").replace("'", "&#39;");
16731720
}

0 commit comments

Comments
 (0)