From 845d7ab20d1298067dbc316ea20e52d72b2ccd33 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 22 Apr 2026 12:33:38 +0200 Subject: [PATCH 1/2] Rename .java to .kt Signed-off-by: alperozturk96 --- .../notes/shared/util/{NoteUtilTest.java => NoteUtilTest.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/test/java/it/niedermann/owncloud/notes/shared/util/{NoteUtilTest.java => NoteUtilTest.kt} (100%) diff --git a/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.java b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.kt similarity index 100% rename from app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.java rename to app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.kt From 329e788b45049619ed5e463084ee41a8174cd6bb Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 22 Apr 2026 12:33:38 +0200 Subject: [PATCH 2/2] improve note preview Signed-off-by: alperozturk96 --- .../owncloud/notes/shared/util/NoteUtil.java | 45 ++- .../notes/shared/util/NoteUtilTest.kt | 292 ++++++++++++++---- 2 files changed, 265 insertions(+), 72 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java index 2c148649d..8df4fac58 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java @@ -18,6 +18,8 @@ import static it.niedermann.android.markdown.MarkdownUtil.removeMarkdown; import static it.niedermann.android.markdown.MarkdownUtil.replaceCheckboxesWithEmojis; +import java.util.regex.Pattern; + /** * Provides basic functionality for Note operations. */ @@ -69,18 +71,49 @@ private static String truncateString(@NonNull String str, @SuppressWarnings("Sam */ @NonNull public static String generateNoteExcerpt(@NonNull String content, @Nullable String title) { - content = removeMarkdown(replaceCheckboxesWithEmojis(content.trim())); - if (TextUtils.isEmpty(content)) { + final var trimmedContent = content.trim(); + + if (isHtml(trimmedContent)) { + return sanitizeHtml(trimmedContent); + } + + final var emojiReplacedWithCheckBoxesContent = replaceCheckboxesWithEmojis(trimmedContent); + var result = removeMarkdown(emojiReplacedWithCheckBoxesContent); + if (TextUtils.isEmpty(result)) { return ""; } + if (!TextUtils.isEmpty(title)) { - assert title != null; final String trimmedTitle = removeMarkdown(replaceCheckboxesWithEmojis(title.trim())); - if (content.startsWith(trimmedTitle)) { - content = content.substring(trimmedTitle.length()); + if (result.startsWith(trimmedTitle)) { + result = result.substring(trimmedTitle.length()); } } - return truncateString(content.trim(), 200).replace("\n", EXCERPT_LINE_SEPARATOR); + + return truncateString(result.trim(), 200).replace("\n", EXCERPT_LINE_SEPARATOR); + } + + private static final Pattern HTML_PATTERN = Pattern.compile( + "(?is)<(?:!DOCTYPE|/?(?:[a-z][a-z0-9]*))[^>]*>" + ); + + private static boolean isHtml(String content) { + if (content == null || content.isEmpty()) { + return false; + } + + return HTML_PATTERN.matcher(content).find(); + } + + private static String sanitizeHtml(String html) { + // Remove script tags and their content + String sanitized = html.replaceAll("(?is)]*>.*?", ""); + + // Remove event handlers (onclick, onerror, onload, etc.) + sanitized = sanitized.replaceAll("(?i)\\s+on\\w+\\s*=\\s*['\"][^'\"]*['\"]", ""); + sanitized = sanitized.replaceAll("(?i)\\s+on\\w+\\s*=\\s*[^\\s>]+", ""); + + return sanitized.trim(); } @NonNull diff --git a/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.kt b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.kt index 24d2a3307..bc6b87180 100644 --- a/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.kt +++ b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.kt @@ -1,107 +1,267 @@ /* * Nextcloud Notes - Android Client * - * SPDX-FileCopyrightText: 2015-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2015-2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: GPL-3.0-or-later */ -package it.niedermann.owncloud.notes.shared.util; +package it.niedermann.owncloud.notes.shared.util -import android.os.Build; - -import androidx.core.text.HtmlCompat; - -import junit.framework.TestCase; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import it.niedermann.android.markdown.MarkdownUtil; +import android.os.Build +import androidx.core.text.HtmlCompat +import it.niedermann.android.markdown.MarkdownUtil +import junit.framework.TestCase +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config /** * Tests the NoteUtil. */ -@RunWith(RobolectricTestRunner.class) -public class NoteUtilTest extends TestCase { +@RunWith(RobolectricTestRunner::class) +class NoteUtilTest : TestCase() { @Test - public void testIsEmptyLine() { - assertTrue(NoteUtil.isEmptyLine(" ")); - assertTrue(NoteUtil.isEmptyLine("\n")); - assertTrue(NoteUtil.isEmptyLine("\n ")); - assertTrue(NoteUtil.isEmptyLine(" \n")); - assertTrue(NoteUtil.isEmptyLine(" \n ")); - assertFalse(NoteUtil.isEmptyLine("a \n ")); + fun testIsEmptyLine() { + assertTrue(NoteUtil.isEmptyLine(" ")) + assertTrue(NoteUtil.isEmptyLine("\n")) + assertTrue(NoteUtil.isEmptyLine("\n ")) + assertTrue(NoteUtil.isEmptyLine(" \n")) + assertTrue(NoteUtil.isEmptyLine(" \n ")) + assertFalse(NoteUtil.isEmptyLine("a \n ")) } @Test - public void testGetLineWithoutMarkdown() { - assertEquals("Test", NoteUtil.getLineWithoutMarkdown("Test", 0)); - assertEquals("Test", NoteUtil.getLineWithoutMarkdown("\nTest", 0)); - assertEquals("Foo", NoteUtil.getLineWithoutMarkdown("Foo\nBar", 0)); - assertEquals("Bar", NoteUtil.getLineWithoutMarkdown("Foo\nBar", 1)); - assertEquals("Foo", NoteUtil.getLineWithoutMarkdown("* Foo\n* Bar", 0)); - assertEquals("Bar", NoteUtil.getLineWithoutMarkdown("- Foo\nBar", 1)); - assertEquals("Foo", NoteUtil.getLineWithoutMarkdown("# Foo", 0)); + fun testGetLineWithoutMarkdown() { + assertEquals("Test", NoteUtil.getLineWithoutMarkdown("Test", 0)) + assertEquals("Test", NoteUtil.getLineWithoutMarkdown("\nTest", 0)) + assertEquals("Foo", NoteUtil.getLineWithoutMarkdown("Foo\nBar", 0)) + assertEquals("Bar", NoteUtil.getLineWithoutMarkdown("Foo\nBar", 1)) + assertEquals("Foo", NoteUtil.getLineWithoutMarkdown("* Foo\n* Bar", 0)) + assertEquals("Bar", NoteUtil.getLineWithoutMarkdown("- Foo\nBar", 1)) + assertEquals("Foo", NoteUtil.getLineWithoutMarkdown("# Foo", 0)) } @Test - public void testGenerateNoteTitle() { - assertEquals("Test", NoteUtil.generateNoteTitle("Test")); - assertEquals("Test", NoteUtil.generateNoteTitle("Test\n")); - assertEquals("Test", NoteUtil.generateNoteTitle("Test\nFoo")); - assertEquals("Test", NoteUtil.generateNoteTitle("\nTest")); - assertEquals("Test", NoteUtil.generateNoteTitle("\n\nTest")); + fun testGenerateNoteTitle() { + assertEquals("Test", NoteUtil.generateNoteTitle("Test")) + assertEquals("Test", NoteUtil.generateNoteTitle("Test\n")) + assertEquals("Test", NoteUtil.generateNoteTitle("Test\nFoo")) + assertEquals("Test", NoteUtil.generateNoteTitle("\nTest")) + assertEquals("Test", NoteUtil.generateNoteTitle("\n\nTest")) // https://github.com/nextcloud/notes-android/issues/1104 - assertEquals("2021-03-24 - Example title", MarkdownUtil.removeMarkdown("2021-03-24 - Example title")); + assertEquals("2021-03-24 - Example title", MarkdownUtil.removeMarkdown("2021-03-24 - Example title")) } @Test - public void testGenerateNoteExcerpt() { + fun testGenerateNoteExcerpt() { + testBasicExcerpts() + testTitleMatchingExcerpts() + testEmptyLineHandling() + testMarkdownInContent() + testMarkdownInTitle() + testMarkdownInBoth() + testHtmlSanitization() + testEdgeCases() + testRealisticUserNotes() + } + + private fun testBasicExcerpts() { // title is different from content → return max. 200 characters starting with the first line which is not empty - assertEquals("Test", NoteUtil.generateNoteExcerpt("Test", "Title")); - assertEquals("Test Foo", NoteUtil.generateNoteExcerpt("Test\nFoo", "Title")); - assertEquals("Test Foo Bar", NoteUtil.generateNoteExcerpt("Test\nFoo\nBar", "Title")); - assertEquals("", NoteUtil.generateNoteExcerpt("", "Title")); + assertEquals("Test", NoteUtil.generateNoteExcerpt("Test", "Title")) + assertEquals("Test Foo", NoteUtil.generateNoteExcerpt("Test\nFoo", "Title")) + assertEquals("Test Foo Bar", NoteUtil.generateNoteExcerpt("Test\nFoo\nBar", "Title")) + assertEquals("", NoteUtil.generateNoteExcerpt("", "Title")) + } + private fun testTitleMatchingExcerpts() { // content actually starts with title → return max. 200 characters starting with the first character after the title - assertEquals("", NoteUtil.generateNoteExcerpt("Title", "Title")); - assertEquals("Foo", NoteUtil.generateNoteExcerpt("Title\nFoo", "Title")); - assertEquals("Title Bar", NoteUtil.generateNoteExcerpt("Title\nTitle\nBar", "Title")); - assertEquals("", NoteUtil.generateNoteExcerpt("", "Title")); + assertEquals("", NoteUtil.generateNoteExcerpt("Title", "Title")) + assertEquals("Foo", NoteUtil.generateNoteExcerpt("Title\nFoo", "Title")) + assertEquals("Title Bar", NoteUtil.generateNoteExcerpt("Title\nTitle\nBar", "Title")) + assertEquals("", NoteUtil.generateNoteExcerpt("", "Title")) + } + private fun testEmptyLineHandling() { // some empty lines between the actual contents → Should be ignored - assertEquals("", NoteUtil.generateNoteExcerpt("\nTitle", "Title")); - assertEquals("Foo", NoteUtil.generateNoteExcerpt("\n\n\n\nTitle\nFoo", "Title")); - assertEquals("Title Bar", NoteUtil.generateNoteExcerpt("\nTitle\n\n\nTitle\nBar", "\n\nTitle")); - assertEquals("", NoteUtil.generateNoteExcerpt("\n\n\n", "\nTitle")); + assertEquals("", NoteUtil.generateNoteExcerpt("\nTitle", "Title")) + assertEquals("Foo", NoteUtil.generateNoteExcerpt("\n\n\n\nTitle\nFoo", "Title")) + assertEquals("Title Bar", NoteUtil.generateNoteExcerpt("\nTitle\n\n\nTitle\nBar", "\n\nTitle")) + assertEquals("", NoteUtil.generateNoteExcerpt("\n\n\n", "\nTitle")) + } - // content has markdown while titles markdown is already stripped - assertEquals("", NoteUtil.generateNoteExcerpt("# Title", "Title")); - assertEquals("Foo", NoteUtil.generateNoteExcerpt("Title\n- Foo", "Title")); + private fun testMarkdownInContent() { + // content has markdown while titles, markdown is already stripped + assertEquals("", NoteUtil.generateNoteExcerpt("# Title", "Title")) + assertEquals("Foo", NoteUtil.generateNoteExcerpt("Title\n- Foo", "Title")) + } + private fun testMarkdownInTitle() { // title has markdown while contents markdown is stripped - assertEquals("", NoteUtil.generateNoteExcerpt("Title", "# Title")); - assertEquals("Foo", NoteUtil.generateNoteExcerpt("Title\nFoo", "- Title")); - assertEquals("Title Bar", NoteUtil.generateNoteExcerpt("Title\nTitle\nBar", "- Title")); + assertEquals("", NoteUtil.generateNoteExcerpt("Title", "# Title")) + assertEquals("Foo", NoteUtil.generateNoteExcerpt("Title\nFoo", "- Title")) + assertEquals("Title Bar", NoteUtil.generateNoteExcerpt("Title\nTitle\nBar", "- Title")) + } + private fun testMarkdownInBoth() { // content and title have markdown - assertEquals("", NoteUtil.generateNoteExcerpt("# Title", "# Title")); - assertEquals("Foo", NoteUtil.generateNoteExcerpt("# Title\n- Foo", "- Title")); - assertEquals("Title Bar", NoteUtil.generateNoteExcerpt("- Title\nTitle\nBar", "- Title")); + assertEquals("", NoteUtil.generateNoteExcerpt("# Title", "# Title")) + assertEquals("Foo", NoteUtil.generateNoteExcerpt("# Title\n- Foo", "- Title")) + assertEquals("Title Bar", NoteUtil.generateNoteExcerpt("- Title\nTitle\nBar", "- Title")) + } + + private fun testHtmlSanitization() { + val html = """ + click + + + """.trimIndent() + + val expectedHtml = "click\n" + assertEquals(expectedHtml, NoteUtil.generateNoteExcerpt(html, "Title")) + assertEquals(expectedHtml, NoteUtil.generateNoteExcerpt(html, null)) + + val scriptHtml = "title" + val scriptExcerpt = NoteUtil.generateNoteExcerpt(scriptHtml, "Test") + assertFalse("Excerpts should never contain executable scripts", scriptExcerpt.contains("