ContentSanitizer.java

package org.darkroomlibrary.utils;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Safelist;

import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Centralized length rules and server-side content sanitization.
 */
public final class ContentSanitizer {

    public static final int MESSAGE_MAX_LENGTH = 1000;
    public static final int MESSAGE_REPLY_MAX_LENGTH = 1000;
    public static final int REVIEW_MAX_LENGTH = 1000;
    public static final int REVIEW_REPLY_MAX_LENGTH = 500;
    public static final int PROCUREMENT_MESSAGE_MAX_LENGTH = 1000;
    public static final int NOTICE_TITLE_MAX_LENGTH = 100;
    public static final int NOTICE_HTML_MAX_LENGTH = 20000;
    public static final int ATTACHMENT_NAME_MAX_LENGTH = 255;
    public static final int ATTACHMENT_URL_MAX_LENGTH = 500;

    private static final Pattern STORED_FILE_URL = Pattern.compile(
            "^/[-A-Za-z0-9_./]+/file/(?:getFile|public|download)\\?fileName="
                    + "[a-fA-F0-9]{32}\\.([a-z0-9]{2,5})$");
    private static final Set<String> MESSAGE_ATTACHMENT_TYPES = Set.of(
            "pdf", "doc", "docx", "jpg", "jpeg", "png", "gif", "bmp", "webp", "html", "htm");
    private static final Safelist NOTICE_SAFELIST = Safelist.relaxed()
            .addTags("s")
            .preserveRelativeLinks(true);

    private ContentSanitizer() {
    }

    public static String plainText(String input) {
        if (input == null) {
            return null;
        }
        return Jsoup.clean(input, Safelist.none())
                .replace("\u00A0", " ")
                .replace("\r\n", "\n")
                .trim();
    }

    public static String richText(String input) {
        if (input == null) {
            return null;
        }
        Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false);
        return Jsoup.clean(
                input,
                "https://library.local/",
                NOTICE_SAFELIST,
                outputSettings
        ).trim();
    }

    public static boolean hasVisibleRichText(String html) {
        if (html == null || html.isBlank()) {
            return false;
        }
        Document document = Jsoup.parseBodyFragment(html);
        return !document.text().trim().isEmpty() || !document.select("img").isEmpty();
    }

    public static boolean exceedsLength(String value, int maxLength) {
        return value != null && value.codePointCount(0, value.length()) > maxLength;
    }

    public static boolean isSafeMessageAttachment(String url, String type) {
        if (url == null || url.isBlank() || type == null || type.isBlank()) {
            return false;
        }
        if (exceedsLength(url, ATTACHMENT_URL_MAX_LENGTH)) {
            return false;
        }
        Matcher matcher = STORED_FILE_URL.matcher(url.trim());
        if (!matcher.matches()) {
            return false;
        }
        String normalizedType = type.trim().toLowerCase(Locale.ROOT);
        return MESSAGE_ATTACHMENT_TYPES.contains(normalizedType)
                && normalizedType.equals(matcher.group(1));
    }
}