feat(back-end): rich text
All checks were successful
Build and Deploy / test-backend (push) Successful in 53s
Build and Deploy / test-frontend (push) Successful in 1m3s
Build and Deploy / build-and-push (push) Successful in 57s
Build and Deploy / deploy (push) Successful in 8s

This commit is contained in:
2026-03-10 18:51:15 +01:00
parent 71890e4cc2
commit d150c19f9f
2 changed files with 38 additions and 9 deletions

View File

@@ -42,6 +42,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'net.codecrete.qrbill:qrbill-generator:3.4.0' implementation 'net.codecrete.qrbill:qrbill-generator:3.4.0'
implementation 'org.springframework.boot:spring-boot-starter-mail' 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 platform('org.lwjgl:lwjgl-bom:3.3.4')
implementation 'org.lwjgl:lwjgl' implementation 'org.lwjgl:lwjgl'
implementation 'org.lwjgl:lwjgl-assimp' implementation 'org.lwjgl:lwjgl-assimp'

View File

@@ -22,6 +22,9 @@ import com.printcalculator.service.SlicerService;
import com.printcalculator.service.media.PublicMediaQueryService; import com.printcalculator.service.media.PublicMediaQueryService;
import com.printcalculator.service.shop.ShopStorageService; import com.printcalculator.service.shop.ShopStorageService;
import com.printcalculator.service.storage.ClamAVService; 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.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; 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 DIACRITICS_PATTERN = Pattern.compile("\\p{M}+");
private static final Pattern NON_ALPHANUMERIC_PATTERN = Pattern.compile("[^a-z0-9]+"); private static final Pattern NON_ALPHANUMERIC_PATTERN = Pattern.compile("[^a-z0-9]+");
private static final Pattern EDGE_DASH_PATTERN = Pattern.compile("(^-+|-+$)"); 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 ShopProductRepository shopProductRepository;
private final ShopCategoryRepository shopCategoryRepository; private final ShopCategoryRepository shopCategoryRepository;
@@ -613,17 +620,17 @@ public class AdminShopProductControllerService {
excerpts.put("fr", firstNonBlank(normalizeOptional(payload.getExcerptFr()), fallbackExcerpt)); excerpts.put("fr", firstNonBlank(normalizeOptional(payload.getExcerptFr()), fallbackExcerpt));
String fallbackDescription = firstNonBlank( String fallbackDescription = firstNonBlank(
normalizeOptional(payload.getDescription()), normalizeRichTextOptional(payload.getDescription()),
normalizeOptional(payload.getDescriptionIt()), normalizeRichTextOptional(payload.getDescriptionIt()),
normalizeOptional(payload.getDescriptionEn()), normalizeRichTextOptional(payload.getDescriptionEn()),
normalizeOptional(payload.getDescriptionDe()), normalizeRichTextOptional(payload.getDescriptionDe()),
normalizeOptional(payload.getDescriptionFr()) normalizeRichTextOptional(payload.getDescriptionFr())
); );
Map<String, String> descriptions = new LinkedHashMap<>(); Map<String, String> descriptions = new LinkedHashMap<>();
descriptions.put("it", firstNonBlank(normalizeOptional(payload.getDescriptionIt()), fallbackDescription)); descriptions.put("it", firstNonBlank(normalizeRichTextOptional(payload.getDescriptionIt()), fallbackDescription));
descriptions.put("en", firstNonBlank(normalizeOptional(payload.getDescriptionEn()), fallbackDescription)); descriptions.put("en", firstNonBlank(normalizeRichTextOptional(payload.getDescriptionEn()), fallbackDescription));
descriptions.put("de", firstNonBlank(normalizeOptional(payload.getDescriptionDe()), fallbackDescription)); descriptions.put("de", firstNonBlank(normalizeRichTextOptional(payload.getDescriptionDe()), fallbackDescription));
descriptions.put("fr", firstNonBlank(normalizeOptional(payload.getDescriptionFr()), fallbackDescription)); descriptions.put("fr", firstNonBlank(normalizeRichTextOptional(payload.getDescriptionFr()), fallbackDescription));
String fallbackSeoTitle = firstNonBlank( String fallbackSeoTitle = firstNonBlank(
normalizeOptional(payload.getSeoTitle()), normalizeOptional(payload.getSeoTitle()),
@@ -689,6 +696,27 @@ public class AdminShopProductControllerService {
return normalized.isBlank() ? null : normalized; 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) { private String firstNonBlank(String... values) {
if (values == null) { if (values == null) {
return null; return null;