diff options
| author | test@dangofactory.cos <you@example.com> | 2026-08-20 16:57:18 +0000 |
|---|---|---|
| committer | test@dangofactory.cos <you@example.com> | 2026-08-20 16:57:18 +0000 |
| commit | 50ce5d5e6cd0d720ff437d35eace545816848428 (patch) | |
| tree | b59af206a36265cf081945e434900b32b0c43edd /app/html-cleaner/src/main/java | |
| download | monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.tar.gz monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.tar.bz2 monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.zip | |
Inital
Diffstat (limited to 'app/html-cleaner/src/main/java')
5 files changed, 199 insertions, 0 deletions
diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt new file mode 100644 index 0000000..0f0e5cd --- /dev/null +++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt @@ -0,0 +1,78 @@ +package app.k9mail.html.cleaner + +import org.jsoup.nodes.Document +import org.jsoup.safety.Cleaner +import org.jsoup.safety.Safelist + +internal class BodyCleaner { + private val cleaner: Cleaner + private val allowedBodyAttributes = setOf( + "id", "class", "dir", "lang", "style", + "alink", "background", "bgcolor", "link", "text", "vlink" + ) + + init { + val allowList = Safelist.relaxed() + .addTags("font", "hr", "ins", "del", "center", "map", "area", "title", "tt", "kbd", "samp", "var") + .addAttributes("font", "color", "face", "size") + .addAttributes("a", "name") + .addAttributes("div", "align") + .addAttributes( + "table", + "align", + "background", + "bgcolor", + "border", + "cellpadding", + "cellspacing", + "width" + ) + .addAttributes("tr", "align", "background", "bgcolor", "valign") + .addAttributes( + "th", + "align", "background", "bgcolor", "colspan", "headers", "height", "nowrap", "rowspan", "scope", + "sorted", "valign", "width" + ) + .addAttributes( + "td", + "align", "background", "bgcolor", "colspan", "headers", "height", "nowrap", "rowspan", "scope", + "valign", "width" + ) + .addAttributes("map", "name") + .addAttributes("area", "shape", "coords", "href", "alt") + .addProtocols("area", "href", "http", "https") + .addAttributes("img", "usemap") + .addAttributes(":all", "class", "style", "id", "dir") + .addProtocols("img", "src", "http", "https", "cid", "data") + // Allow all URI schemes in links + .removeProtocols("a", "href", "ftp", "http", "https", "mailto") + + cleaner = Cleaner(allowList) + } + + fun clean(dirtyDocument: Document): Document { + val cleanedDocument = cleaner.clean(dirtyDocument) + copyDocumentType(dirtyDocument, cleanedDocument) + copyBodyAttributes(dirtyDocument, cleanedDocument) + return cleanedDocument + } + + private fun copyDocumentType(dirtyDocument: Document, cleanedDocument: Document) { + dirtyDocument.documentType()?.let { documentType -> + cleanedDocument.insertChildren(0, documentType) + } + } + + private fun copyBodyAttributes(dirtyDocument: Document, cleanedDocument: Document) { + val cleanedBody = cleanedDocument.body() + for (attribute in dirtyDocument.body().attributes()) { + if (attribute.key !in allowedBodyAttributes) continue + + if (attribute.hasDeclaredValue()) { + cleanedBody.attr(attribute.key, attribute.value) + } else { + cleanedBody.attr(attribute.key, true) + } + } + } +} diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HeadCleaner.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HeadCleaner.kt new file mode 100644 index 0000000..1485291 --- /dev/null +++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HeadCleaner.kt @@ -0,0 +1,75 @@ +package app.k9mail.html.cleaner + +import org.jsoup.nodes.DataNode +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import org.jsoup.nodes.Node +import org.jsoup.nodes.TextNode +import org.jsoup.parser.Tag +import org.jsoup.select.NodeTraversor +import org.jsoup.select.NodeVisitor + +private val ALLOWED_TAGS = listOf("style", "meta", "base") + +internal class HeadCleaner { + fun clean(dirtyDocument: Document, cleanedDocument: Document) { + copySafeNodes(dirtyDocument.head(), cleanedDocument.head()) + } + + private fun copySafeNodes(source: Element, destination: Element) { + val cleaningVisitor = CleaningVisitor(source, destination) + NodeTraversor.traverse(cleaningVisitor, source) + } +} + +internal class CleaningVisitor( + private val root: Element, + private var destination: Element +) : NodeVisitor { + private var elementToSkip: Element? = null + + override fun head(source: Node, depth: Int) { + if (elementToSkip != null) return + + if (source is Element) { + if (isSafeTag(source)) { + val sourceTag = source.tagName() + val destinationAttributes = source.attributes().clone() + val destinationChild = Element(Tag.valueOf(sourceTag), source.baseUri(), destinationAttributes) + destination.appendChild(destinationChild) + destination = destinationChild + } else if (source !== root) { + elementToSkip = source + } + } else if (source is TextNode) { + val destinationText = TextNode(source.wholeText) + destination.appendChild(destinationText) + } else if (source is DataNode && isSafeTag(source.parent())) { + val destinationData = DataNode(source.wholeData) + destination.appendChild(destinationData) + } + } + + override fun tail(source: Node, depth: Int) { + if (source === elementToSkip) { + elementToSkip = null + } else if (source is Element && isSafeTag(source)) { + destination = destination.parent() ?: error("Missing parent") + } + } + + private fun isSafeTag(node: Node?): Boolean { + if (node == null || isMetaRefresh(node)) return false + + val tag = node.nodeName().lowercase() + return tag in ALLOWED_TAGS + } + + private fun isMetaRefresh(node: Node): Boolean { + val tag = node.nodeName().lowercase() + if (tag != "meta") return false + + val attributeValue = node.attributes().getIgnoreCase("http-equiv").trim().lowercase() + return attributeValue == "refresh" + } +} diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlHeadProvider.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlHeadProvider.kt new file mode 100644 index 0000000..cf234cc --- /dev/null +++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlHeadProvider.kt @@ -0,0 +1,5 @@ +package app.k9mail.html.cleaner + +interface HtmlHeadProvider { + val headHtml: String +} diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlProcessor.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlProcessor.kt new file mode 100644 index 0000000..2da379b --- /dev/null +++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlProcessor.kt @@ -0,0 +1,25 @@ +package app.k9mail.html.cleaner + +import org.jsoup.nodes.Document + +class HtmlProcessor(private val htmlHeadProvider: HtmlHeadProvider) { + private val htmlSanitizer = HtmlSanitizer() + + fun processForDisplay(html: String): String { + return htmlSanitizer.sanitize(html) + .addCustomHeadContents() + .toCompactString() + } + + private fun Document.addCustomHeadContents() = apply { + head().append(htmlHeadProvider.headHtml) + } + + private fun Document.toCompactString(): String { + outputSettings() + .prettyPrint(false) + .indentAmount(0) + + return html() + } +} diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlSanitizer.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlSanitizer.kt new file mode 100644 index 0000000..d11bb30 --- /dev/null +++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlSanitizer.kt @@ -0,0 +1,16 @@ +package app.k9mail.html.cleaner + +import org.jsoup.Jsoup +import org.jsoup.nodes.Document + +internal class HtmlSanitizer { + private val headCleaner = HeadCleaner() + private val bodyCleaner = BodyCleaner() + + fun sanitize(html: String): Document { + val dirtyDocument = Jsoup.parse(html) + val cleanedDocument = bodyCleaner.clean(dirtyDocument) + headCleaner.clean(dirtyDocument, cleanedDocument) + return cleanedDocument + } +} |
