diff --git a/backend/build.gradle b/backend/build.gradle index d54f8b3..2fa95cd 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -42,6 +42,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'net.codecrete.qrbill:qrbill-generator:3.4.0' implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'org.jsoup:jsoup:1.18.3' implementation platform('org.lwjgl:lwjgl-bom:3.3.4') implementation 'org.lwjgl:lwjgl' implementation 'org.lwjgl:lwjgl-assimp' diff --git a/backend/src/main/java/com/printcalculator/service/admin/AdminShopProductControllerService.java b/backend/src/main/java/com/printcalculator/service/admin/AdminShopProductControllerService.java index 7a3d262..f562a92 100644 --- a/backend/src/main/java/com/printcalculator/service/admin/AdminShopProductControllerService.java +++ b/backend/src/main/java/com/printcalculator/service/admin/AdminShopProductControllerService.java @@ -22,6 +22,9 @@ import com.printcalculator.service.SlicerService; import com.printcalculator.service.media.PublicMediaQueryService; import com.printcalculator.service.shop.ShopStorageService; import com.printcalculator.service.storage.ClamAVService; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.safety.Safelist; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; @@ -64,6 +67,10 @@ public class AdminShopProductControllerService { private static final Pattern DIACRITICS_PATTERN = Pattern.compile("\\p{M}+"); private static final Pattern NON_ALPHANUMERIC_PATTERN = Pattern.compile("[^a-z0-9]+"); private static final Pattern EDGE_DASH_PATTERN = Pattern.compile("(^-+|-+$)"); + private static final Safelist PRODUCT_DESCRIPTION_SAFELIST = Safelist.none() + .addTags("p", "div", "br", "strong", "b", "em", "i", "u", "ul", "ol", "li", "a") + .addAttributes("a", "href") + .addProtocols("a", "href", "http", "https", "mailto", "tel"); private final ShopProductRepository shopProductRepository; private final ShopCategoryRepository shopCategoryRepository; @@ -613,17 +620,17 @@ public class AdminShopProductControllerService { excerpts.put("fr", firstNonBlank(normalizeOptional(payload.getExcerptFr()), fallbackExcerpt)); String fallbackDescription = firstNonBlank( - normalizeOptional(payload.getDescription()), - normalizeOptional(payload.getDescriptionIt()), - normalizeOptional(payload.getDescriptionEn()), - normalizeOptional(payload.getDescriptionDe()), - normalizeOptional(payload.getDescriptionFr()) + normalizeRichTextOptional(payload.getDescription()), + normalizeRichTextOptional(payload.getDescriptionIt()), + normalizeRichTextOptional(payload.getDescriptionEn()), + normalizeRichTextOptional(payload.getDescriptionDe()), + normalizeRichTextOptional(payload.getDescriptionFr()) ); Map descriptions = new LinkedHashMap<>(); - descriptions.put("it", firstNonBlank(normalizeOptional(payload.getDescriptionIt()), fallbackDescription)); - descriptions.put("en", firstNonBlank(normalizeOptional(payload.getDescriptionEn()), fallbackDescription)); - descriptions.put("de", firstNonBlank(normalizeOptional(payload.getDescriptionDe()), fallbackDescription)); - descriptions.put("fr", firstNonBlank(normalizeOptional(payload.getDescriptionFr()), fallbackDescription)); + descriptions.put("it", firstNonBlank(normalizeRichTextOptional(payload.getDescriptionIt()), fallbackDescription)); + descriptions.put("en", firstNonBlank(normalizeRichTextOptional(payload.getDescriptionEn()), fallbackDescription)); + descriptions.put("de", firstNonBlank(normalizeRichTextOptional(payload.getDescriptionDe()), fallbackDescription)); + descriptions.put("fr", firstNonBlank(normalizeRichTextOptional(payload.getDescriptionFr()), fallbackDescription)); String fallbackSeoTitle = firstNonBlank( normalizeOptional(payload.getSeoTitle()), @@ -689,6 +696,27 @@ public class AdminShopProductControllerService { return normalized.isBlank() ? null : normalized; } + private String normalizeRichTextOptional(String value) { + String normalized = normalizeOptional(value); + if (normalized == null) { + return null; + } + + String sanitized = Jsoup.clean( + normalized, + "", + PRODUCT_DESCRIPTION_SAFELIST, + new Document.OutputSettings().prettyPrint(false) + ).trim(); + + if (sanitized.isBlank()) { + return null; + } + + String plainText = Jsoup.parse(sanitized).text(); + return plainText != null && !plainText.trim().isEmpty() ? sanitized : null; + } + private String firstNonBlank(String... values) { if (values == null) { return null;