|
46 | 46 |
|
47 | 47 | import org.apache.commons.collections4.CollectionUtils; |
48 | 48 | import org.apache.commons.lang3.StringUtils; |
| 49 | +import org.jsoup.safety.Safelist; |
49 | 50 |
|
50 | 51 | import com.vaadin.icons.VaadinIcons; |
51 | 52 | import com.vaadin.server.ErrorMessage; |
|
134 | 135 | import de.symeda.sormas.api.utils.DataHelper; |
135 | 136 | import de.symeda.sormas.api.utils.DateHelper; |
136 | 137 | import de.symeda.sormas.api.utils.ExtendedReduced; |
| 138 | +import de.symeda.sormas.api.utils.HtmlHelper; |
137 | 139 | import de.symeda.sormas.api.utils.YesNoUnknown; |
138 | 140 | import de.symeda.sormas.api.utils.fieldvisibility.FieldVisibilityCheckers; |
139 | 141 | import de.symeda.sormas.api.utils.fieldvisibility.checkers.CountryFieldVisibilityChecker; |
@@ -220,8 +222,9 @@ public class CaseDataForm extends AbstractEditForm<CaseDataDto> { |
220 | 222 | public static final String DIAGNOSIS_CRITERIA_HEADING_LOC = "diagnosisCriteriaHeadingLoc"; |
221 | 223 | public static final String DIAGNOSIS_CRITERIA_SUBHEADING_LOC = "diagnosisCriteriaSubheadingLoc"; |
222 | 224 | 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); |
225 | 228 | //@formatter:off |
226 | 229 | private static final String MAIN_HTML_LAYOUT = |
227 | 230 | loc(CASE_DATA_HEADING_LOC) + |
@@ -1647,27 +1650,71 @@ private void getManualCaseDefinition() { |
1647 | 1650 | * @return sanitized url |
1648 | 1651 | */ |
1649 | 1652 | 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); |
1651 | 1671 | StringBuilder result = new StringBuilder(); |
1652 | 1672 | int last = 0; |
1653 | 1673 |
|
1654 | 1674 | 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 | + } |
1664 | 1691 | last = matcher.end(); |
1665 | 1692 | } |
1666 | | - |
1667 | | - result.append(escapeHtml(text.substring(last))); |
| 1693 | + result.append(sanitizedText.substring(last)); |
1668 | 1694 | return result.toString(); |
1669 | 1695 | } |
1670 | 1696 |
|
| 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., &lt; becomes <) |
| 1707 | + String step1 = value.replace("&", "&"); |
| 1708 | + // Now, safely convert standard HTML entities to real brackets |
| 1709 | + return step1.replace("<", "<").replace(">", ">").replace(""", "\"").replace("'", "'"); |
| 1710 | + } |
| 1711 | + |
| 1712 | + /** |
| 1713 | + * Converting special characters in a string into their safe HTML entity values |
| 1714 | + * |
| 1715 | + * @param value |
| 1716 | + * @return |
| 1717 | + */ |
1671 | 1718 | private static String escapeHtml(String value) { |
1672 | 1719 | return value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """).replace("'", "'"); |
1673 | 1720 | } |
|
0 commit comments