diff --git a/formcycle-adapter/formcycle-adapter-impl/pom.xml b/formcycle-adapter/formcycle-adapter-impl/pom.xml index bd1750eef4dfb9a53d088f6a581a895b61387cec..0f6b4c73e79d31ac81dc76c35a044f4985382924 100644 --- a/formcycle-adapter/formcycle-adapter-impl/pom.xml +++ b/formcycle-adapter/formcycle-adapter-impl/pom.xml @@ -38,19 +38,19 @@ <properties> <formcycle-interface.version>${project.version}</formcycle-interface.version> + <jsoup.version>1.17.2</jsoup.version> </properties> <dependencies> <!--own project--> <dependency> - <groupId>de.ozgcloud.vorgang</groupId> - <artifactId>vorgang-manager-utils</artifactId> + <groupId>de.ozgcloud.eingang</groupId> + <artifactId>formcycle-adapter-interface</artifactId> + <version>${formcycle-interface.version}</version> </dependency> <dependency> - <groupId>de.ozgcloud.vorgang</groupId> - <artifactId>vorgang-manager-utils</artifactId> - <type>test-jar</type> - <scope>test</scope> + <groupId>de.ozgcloud.eingang</groupId> + <artifactId>semantik-adapter</artifactId> </dependency> <dependency> <groupId>de.ozgcloud.eingang</groupId> @@ -58,6 +58,16 @@ <type>test-jar</type> <scope>test</scope> </dependency> + <dependency> + <groupId>de.ozgcloud.vorgang</groupId> + <artifactId>vorgang-manager-utils</artifactId> + </dependency> + <dependency> + <groupId>de.ozgcloud.vorgang</groupId> + <artifactId>vorgang-manager-utils</artifactId> + <type>test-jar</type> + <scope>test</scope> + </dependency> <!--spring--> <dependency> @@ -69,15 +79,10 @@ <artifactId>spring-boot-starter-actuator</artifactId> </dependency> - <dependency> - <groupId>de.ozgcloud.eingang</groupId> - <artifactId>formcycle-adapter-interface</artifactId> - <version>${formcycle-interface.version}</version> - </dependency> - <dependency> - <groupId>de.ozgcloud.eingang</groupId> - <artifactId>semantik-adapter</artifactId> + <groupId>org.jsoup</groupId> + <artifactId>jsoup</artifactId> + <version>${jsoup.version}</version> </dependency> </dependencies> diff --git a/formcycle-adapter/formcycle-adapter-impl/src/main/java/de/ozgcloud/eingang/formcycle/FormDataController.java b/formcycle-adapter/formcycle-adapter-impl/src/main/java/de/ozgcloud/eingang/formcycle/FormDataController.java index d860e7833dc3cb2c41fa2255247015d8a6798575..72cadbdcb3d51ee6a32ed3c77ed128ee56bdb5c8 100644 --- a/formcycle-adapter/formcycle-adapter-impl/src/main/java/de/ozgcloud/eingang/formcycle/FormDataController.java +++ b/formcycle-adapter/formcycle-adapter-impl/src/main/java/de/ozgcloud/eingang/formcycle/FormDataController.java @@ -65,6 +65,7 @@ class FormDataController { private final FormCycleFormDataMapper mapper; private final SemantikAdapter semantikAdapter; private final VorgangNummerSupplier vorgangNummerSupplier; + private final FormDataHtmlCleaner formDataHtmlCleaner; @PostMapping(consumes = "multipart/form-data", produces = HTTP_TYPE_PROTOBUF) public FormCycleConfirmationResponse receiveFormData(@RequestPart FormCycleFormData formData, @@ -72,6 +73,7 @@ class FormDataController { @RequestPart(required = false) Optional<Collection<MultipartFile>> attachments) { FormData mappedFormData = mapper.toFormData(formData); + mappedFormData = formDataHtmlCleaner.clean(mappedFormData); mappedFormData = addRepresentations(representations, mappedFormData); mappedFormData = addFiles(formData, attachments, mappedFormData); mappedFormData = addServiceKonto(formData, mappedFormData); diff --git a/formcycle-adapter/formcycle-adapter-impl/src/main/java/de/ozgcloud/eingang/formcycle/FormDataHtmlCleaner.java b/formcycle-adapter/formcycle-adapter-impl/src/main/java/de/ozgcloud/eingang/formcycle/FormDataHtmlCleaner.java new file mode 100644 index 0000000000000000000000000000000000000000..b4e236471551346681fb79bb0000ccf9f23c01fe --- /dev/null +++ b/formcycle-adapter/formcycle-adapter-impl/src/main/java/de/ozgcloud/eingang/formcycle/FormDataHtmlCleaner.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.eingang.formcycle; + +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.safety.Safelist; +import org.springframework.stereotype.Component; + +import de.ozgcloud.eingang.common.formdata.FormData; + +@Component +public class FormDataHtmlCleaner { + + public FormData clean(FormData formData) { + return formData.toBuilder().formData(cleanFormData(formData.getFormData())).build(); + } + + Map<String, Object> cleanFormData(Map<String, Object> formData) { + return formData.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> cleanValue(entry.getValue()))); + } + + @SuppressWarnings("unchecked") + Object cleanValue(Object value) { + if (value instanceof Map) { + return cleanFormData((Map<String, Object>) value); + } else if (value instanceof Collection<?> values) { + return values.stream().map(this::cleanValue).toList(); + } else if (value instanceof String valueString) { + return parseHtml(valueString); + } + return value; + } + + Object parseHtml(String html) { + var jsoupDocument = Jsoup.parse(html); + var outputSettings = new Document.OutputSettings(); + outputSettings.prettyPrint(false); + jsoupDocument.outputSettings(outputSettings); + var str = jsoupDocument.html().replace("\\\\n", "\n"); + return Jsoup.clean(str, "", Safelist.none(), outputSettings); + } + +} diff --git a/formcycle-adapter/formcycle-adapter-impl/src/test/java/de/ozgcloud/eingang/formcycle/FormDataHtmlCleanerTest.java b/formcycle-adapter/formcycle-adapter-impl/src/test/java/de/ozgcloud/eingang/formcycle/FormDataHtmlCleanerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..4f1109c25ba64e079a4bf6bf68d5828095ec1bc1 --- /dev/null +++ b/formcycle-adapter/formcycle-adapter-impl/src/test/java/de/ozgcloud/eingang/formcycle/FormDataHtmlCleanerTest.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.eingang.formcycle; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.Map; + +import org.assertj.core.data.MapEntry; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Spy; + +import de.ozgcloud.eingang.common.formdata.FormData; + +class FormDataHtmlCleanerTest { + + private static final String KEY = "key"; + private static final Object VALUE = "value"; + + @Spy + @InjectMocks + private FormDataHtmlCleaner cleaner; + + @Nested + class TestClean { + + private final static Map<String, Object> FORM_DATA_MAP = Map.of(KEY, VALUE); + private final static FormData FORM_DATA = FormData.builder().formData(FORM_DATA_MAP).build(); + + @Test + void shouldCallCleanFormData() { + cleaner.clean(FORM_DATA); + + verify(cleaner).cleanFormData(FORM_DATA_MAP); + } + + @Test + void shouldSetCleanedFormData() { + var cleanedFormData = Map.of(KEY, VALUE); + doReturn(cleanedFormData).when(cleaner).cleanFormData(anyMap()); + + var result = cleaner.clean(FORM_DATA); + + assertThat(result.getFormData()).isSameAs(cleanedFormData); + } + } + + @Nested + class TestCleanFormData { + + @Test + void shouldCallCleanValue() { + cleaner.cleanFormData(Map.of(KEY, VALUE)); + + verify(cleaner).cleanValue(VALUE); + } + + @Test + void shouldReturnCleanedMap() { + var cleanedValue = "noHtml"; + doReturn(cleanedValue).when(cleaner).cleanValue(any()); + + var result = cleaner.cleanFormData(Map.of(KEY, VALUE)); + + assertThat(result).containsOnly(MapEntry.entry(KEY, cleanedValue)); + } + } + + @Nested + class TestCleanValue { + + @Nested + class TestCleanMap { + + @Test + void shouldCallCleanFormData() { + var expectedMap = Map.of(KEY, VALUE); + + cleaner.cleanValue(expectedMap); + + verify(cleaner).cleanFormData(expectedMap); + } + + @Test + void shouldReturnValue() { + var expectedMap = Map.of(KEY, VALUE); + doReturn(expectedMap).when(cleaner).cleanFormData(anyMap()); + + var result = cleaner.cleanValue(Map.of("a", "b")); + + assertThat(result).isSameAs(expectedMap); + } + } + + @Nested + class TestCleanCollection { + + @Test + void shouldCallCleanValue() { + cleaner.cleanValue(List.of(VALUE)); + + verify(cleaner).cleanValue(VALUE); + } + + @Test + void shouldReturnValue() { + doReturn(List.of(VALUE)).when(cleaner).cleanValue(any()); + + var result = cleaner.cleanValue(List.of("a")); + + assertThat(result).isInstanceOf(List.class).asList().containsExactly(VALUE); + } + } + + @Nested + class TestCleanString { + + @Test + void shouldCallParseHtml() { + var stringValue = VALUE.toString(); + + cleaner.cleanValue(VALUE); + + verify(cleaner).parseHtml(stringValue); + } + + @Test + void shouldReturnValue() { + var cleanedValue = "noHtml"; + doReturn(cleanedValue).when(cleaner).parseHtml(anyString()); + + var result = cleaner.cleanValue(VALUE); + + assertThat(result).isEqualTo(cleanedValue); + } + } + + @Test + void shouldReturnUnmodifiedValue() { + var value = 1; + + var result = cleaner.cleanValue(value); + + assertThat(result).isEqualTo(1); + verify(cleaner, never()).parseHtml(any()); + } + } + + @Nested + class TestHtmlCleaner { + + static final String KEY_LABEL = "label"; + static final String KEY_VALUE = "value"; + + static final Map<String, Object> FORM_DATA_MAP = Map.of("tf1", Map.of( + KEY_LABEL, "<p><em>Ä</em></p>", + KEY_VALUE, "Ä - Wert"), + "tf2", Map.of( + KEY_LABEL, "<p><strong>Ö</strong></p>", + KEY_VALUE, "Ö - Wert"), + "fs1", Map.of( + KEY_LABEL, "Ü", + KEY_VALUE, Map.of( + "tf3", Map.of( + KEY_LABEL, " <p><s>Label mit</s> ß</p>", + KEY_VALUE, "ein Text mit ß und <html><body><h1>Hello</h1><body><html>")), + "tf4", Map.of( + KEY_LABEL, "<p><span style=\"background-color:#1abc9c;\">ä</span></p>", + KEY_VALUE, "Text"), + "ed1", Map.of( + KEY_LABEL, + "<ol>\n\t<li><em><strong><u>ö</u></strong></em></li>\n\t<li><span style=\"color:#e74c3c;\">ü</span></li>\n</ol>", + KEY_VALUE, "TExt\nmit\n Leerzeichen\nund\n Umbrüchen" + ))); + + static final Map<String, Object> EXPECTED_MAP = Map.of("tf1", Map.of( + KEY_LABEL, "Ä", + KEY_VALUE, "Ä - Wert"), + "tf2", Map.of( + KEY_LABEL, "Ö", + KEY_VALUE, "Ö - Wert"), + "fs1", Map.of( + KEY_LABEL, "Ü", + KEY_VALUE, Map.of( + "tf3", Map.of( + KEY_LABEL, "Label mit ß", + KEY_VALUE, "ein Text mit ß und Hello")), + "tf4", Map.of( + KEY_LABEL, "ä", + KEY_VALUE, "Text"), + "ed1", Map.of( + KEY_LABEL, + "\n\tö\n\tü\n", + KEY_VALUE, "TExt\nmit\n Leerzeichen\nund\n Umbrüchen" + ))); + + @Test + void shouldCleanHtml() { + var result = cleaner.clean(FormData.builder().formData(FORM_DATA_MAP).build()); + + assertThat(result.getFormData()).isEqualTo(EXPECTED_MAP); + } + } +} \ No newline at end of file