diff --git a/src/main/java/de/gultsch/common/Linkify.java b/src/main/java/de/gultsch/common/Linkify.java index 6f556c81d4202aa678ef45ac654d3b165895c4a7..da86f29df159c7ff5828f02a9ea5b90603789456 100644 --- a/src/main/java/de/gultsch/common/Linkify.java +++ b/src/main/java/de/gultsch/common/Linkify.java @@ -45,6 +45,8 @@ import eu.siacs.conversations.entities.ListItem; import eu.siacs.conversations.utils.StylingHelper; import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xmpp.Jid; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -59,6 +61,9 @@ public class Linkify { if (scheme == null) { return false; } + if (!isValidUri(match)) { + return false; + } return switch (scheme) { case "tel" -> Patterns.URI_TEL.matcher(match).matches(); case "http", "https" -> Patterns.URI_HTTP.matcher(match).matches(); @@ -77,6 +82,15 @@ public class Linkify { }; } + private static boolean isValidUri(final String match) { + try { + new URI(match); + return true; + } catch (final URISyntaxException e) { + return false; + } + } + public static void addLinks(final Spannable body) { android.text.util.Linkify.addLinks(body, Patterns.URI_GENERIC, null, MATCH_FILTER, null); for (final URLSpan span : body.getSpans(0, body.length(), URLSpan.class)) { diff --git a/src/test/java/de/gultsch/common/LinkifyTest.java b/src/test/java/de/gultsch/common/LinkifyTest.java index f4e439c27a8fb627f723c48521d633fec1943b91..8cf386740814c1695461a88ba4c4b510734a3a62 100644 --- a/src/test/java/de/gultsch/common/LinkifyTest.java +++ b/src/test/java/de/gultsch/common/LinkifyTest.java @@ -17,11 +17,40 @@ import org.robolectric.annotation.ConscryptMode; @Config(sdk = Build.VERSION_CODES.TIRAMISU, application = Conversations.class) @ConscryptMode(ConscryptMode.Mode.OFF) public class LinkifyTest { - @Test public void addLinksDoesNotLinkifyInvalidUris() { final var text = new SpannableStringBuilder("https://example.com?q=%s"); Linkify.addLinks(text); Assert.assertEquals(0, text.getSpans(0, text.length(), URLSpan.class).length); } + + @Test + public void malformedEscapeIsRejected() { + final var links = Linkify.getLinks("https://example.com/?x=%"); + + Assert.assertTrue(links.isEmpty()); + } + + @Test + public void validEscapesStillProduceLinks() { + final var links = Linkify.getLinks("https://example.com/?x=%20"); + + Assert.assertEquals(1, links.size()); + Assert.assertEquals("https://example.com/?x=%20", links.get(0).getRaw()); + } + + @Test + public void ipv6HostsStillProduceLinks() { + final var links = Linkify.getLinks("http://[::1]/foo"); + + Assert.assertEquals(1, links.size()); + Assert.assertEquals("http://[::1]/foo", links.get(0).getRaw()); + } + + @Test + public void bracketsInPathAreRejected() { + final var links = Linkify.getLinks("http://example.com/foo[bar]"); + + Assert.assertTrue(links.isEmpty()); + } }