diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java deleted file mode 100644 index 396a9d4d55..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java +++ /dev/null @@ -1,239 +0,0 @@ -package org.thoughtcrime.securesms.linkpreview; - -import android.annotation.SuppressLint; -import android.os.Build; -import android.text.Html; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.URLSpan; -import android.text.util.Linkify; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import okhttp3.HttpUrl; -import org.session.libsession.utilities.Util; -import org.session.libsignal.utilities.Log; - -public final class LinkPreviewUtil { - - private static final Pattern DOMAIN_PATTERN = Pattern.compile("^(https?://)?([^/]+).*$", Pattern.CASE_INSENSITIVE); - private static final Pattern ALL_ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7F]*$", Pattern.CASE_INSENSITIVE); - private static final Pattern ALL_NON_ASCII_PATTERN = Pattern.compile("^[^\\x00-\\x7F]*$", Pattern.CASE_INSENSITIVE); - private static final Pattern OPEN_GRAPH_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*og:([^\"]+)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE); - private static final Pattern ARTICLE_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*article:([^\"]+)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE); - private static final Pattern OPEN_GRAPH_CONTENT_PATTERN = Pattern.compile("content\\s*=\\s*\"([^\"]*)\"", Pattern.CASE_INSENSITIVE); - private static final Pattern TITLE_PATTERN = Pattern.compile("<\\s*title[^>]*>(.*)<\\s*/title[^>]*>", Pattern.CASE_INSENSITIVE); - private static final Pattern FAVICON_PATTERN = Pattern.compile("<\\s*link[^>]*rel\\s*=\\s*\".*icon.*\"[^>]*>", Pattern.CASE_INSENSITIVE); - private static final Pattern FAVICON_HREF_PATTERN = Pattern.compile("href\\s*=\\s*\"([^\"]*)\"", Pattern.CASE_INSENSITIVE); - - /** - * @return All whitelisted URLs in the source text. - */ - public static @NonNull List findWhitelistedUrls(@NonNull String text) { - SpannableString spannable = new SpannableString(text); - boolean found = Linkify.addLinks(spannable, Linkify.WEB_URLS); - - if (!found) { - return Collections.emptyList(); - } - - URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class); - List links = new java.util.ArrayList<>(spans.length); - - for (URLSpan span : spans) { - Link link = new Link(span.getURL(), spannable.getSpanStart(span)); - if (isValidLinkUrl(link.getUrl())) { - links.add(link); - } - } - - return links; - } - - /** - * @return True if the host is valid. - */ - public static boolean isValidLinkUrl(@Nullable String linkUrl) { - if (linkUrl == null) return false; - - HttpUrl url = HttpUrl.parse(linkUrl); - return url != null && - !TextUtils.isEmpty(url.scheme()) && - "https".equals(url.scheme()) && - isLegalUrl(linkUrl); - } - - /** - * @return True if the top-level domain is valid. - */ - public static boolean isValidMediaUrl(@Nullable String mediaUrl) { - if (mediaUrl == null) return false; - - HttpUrl url = HttpUrl.parse(mediaUrl); - return url != null && - !TextUtils.isEmpty(url.scheme()) && - "https".equals(url.scheme()) && - isLegalUrl(mediaUrl); - } - - public static boolean isLegalUrl(@NonNull String url) { - Matcher matcher = DOMAIN_PATTERN.matcher(url); - - if (matcher.matches()) { - String domain = matcher.group(2); - String cleanedDomain = domain.replaceAll("\\.", ""); - - return ALL_ASCII_PATTERN.matcher(cleanedDomain).matches() || - ALL_NON_ASCII_PATTERN.matcher(cleanedDomain).matches(); - } else { - return false; - } - } - - public static boolean isValidMimeType(@NonNull String url) { - String[] validMimeType = {".jpg", ".png", ".gif", ".jpeg"}; - if (url.contains(".")) { - for (String mimeType : validMimeType) { - if (url.contains(mimeType)) { - return true; - } - } - return false; - } - return true; - } - - public static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html) { - return parseOpenGraphFields(html, text -> Html.fromHtml(text).toString()); - } - - static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html, @NonNull HtmlDecoder htmlDecoder) { - if (html == null) { - return new OpenGraph(Collections.emptyMap(), null, null); - } - - Map openGraphTags = new HashMap<>(); - Matcher openGraphMatcher = OPEN_GRAPH_TAG_PATTERN.matcher(html); - - while (openGraphMatcher.find()) { - String tag = openGraphMatcher.group(); - String property = openGraphMatcher.groupCount() > 0 ? openGraphMatcher.group(1) : null; - - if (property != null) { - Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag); - if (contentMatcher.find() && contentMatcher.groupCount() > 0) { - String content = htmlDecoder.fromEncoded(contentMatcher.group(1)); - openGraphTags.put(property.toLowerCase(), content); - } - } - } - - Matcher articleMatcher = ARTICLE_TAG_PATTERN.matcher(html); - - while (articleMatcher.find()) { - String tag = articleMatcher.group(); - String property = articleMatcher.groupCount() > 0 ? articleMatcher.group(1) : null; - - if (property != null) { - Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag); - if (contentMatcher.find() && contentMatcher.groupCount() > 0) { - String content = htmlDecoder.fromEncoded(contentMatcher.group(1)); - openGraphTags.put(property.toLowerCase(), content); - } - } - } - - String htmlTitle = ""; - String faviconUrl = ""; - - Matcher titleMatcher = TITLE_PATTERN.matcher(html); - if (titleMatcher.find() && titleMatcher.groupCount() > 0) { - htmlTitle = htmlDecoder.fromEncoded(titleMatcher.group(1)); - } - - Matcher faviconMatcher = FAVICON_PATTERN.matcher(html); - if (faviconMatcher.find()) { - Matcher faviconHrefMatcher = FAVICON_HREF_PATTERN.matcher(faviconMatcher.group()); - if (faviconHrefMatcher.find() && faviconHrefMatcher.groupCount() > 0) { - faviconUrl = faviconHrefMatcher.group(1); - } - } - - return new OpenGraph(openGraphTags, htmlTitle, faviconUrl); - } - - public static final class OpenGraph { - - private final Map values; - - private final @Nullable String htmlTitle; - private final @Nullable String faviconUrl; - - private static final String KEY_TITLE = "title"; - private static final String KEY_DESCRIPTION_URL = "description"; - private static final String KEY_IMAGE_URL = "image"; - private static final String KEY_PUBLISHED_TIME_1 = "published_time"; - private static final String KEY_PUBLISHED_TIME_2 = "article:published_time"; - private static final String KEY_MODIFIED_TIME_1 = "modified_time"; - private static final String KEY_MODIFIED_TIME_2 = "article:modified_time"; - - public OpenGraph(@NonNull Map values, @Nullable String htmlTitle, @Nullable String faviconUrl) { - this.values = values; - this.htmlTitle = htmlTitle; - this.faviconUrl = faviconUrl; - } - - public String getTitle() { - return Util.getFirstNonEmpty(values.get(KEY_TITLE), htmlTitle); - } - - public String getImageUrl() { - return Util.getFirstNonEmpty(values.get(KEY_IMAGE_URL), faviconUrl); - } - - private static long parseISO8601(String date) { - - if (date == null || date.isEmpty()) { return -1L; } - - SimpleDateFormat format; - format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); - - try { - return format.parse(date).getTime(); - } catch (ParseException pe) { - Log.w("OpenGraph", "Failed to parse date.", pe); - return -1L; - } - } - - @SuppressLint("ObsoleteSdkInt") - public long getDate() { - String[] candidates = new String[] { - values.get(KEY_PUBLISHED_TIME_1), - values.get(KEY_PUBLISHED_TIME_2), - values.get(KEY_MODIFIED_TIME_1), - values.get(KEY_MODIFIED_TIME_2) - }; - - for (String c : candidates) { - long t = parseISO8601(c); - if (t > 0) return t; - } - - return 0L; - } - } - - public interface HtmlDecoder { - @NonNull String fromEncoded(@NonNull String html); - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.kt new file mode 100644 index 0000000000..2cba231222 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.kt @@ -0,0 +1,209 @@ +package org.thoughtcrime.securesms.linkpreview + +import android.annotation.SuppressLint +import android.text.SpannableString +import android.text.style.URLSpan +import androidx.core.text.HtmlCompat +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import org.session.libsession.utilities.Util +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.conversation.v2.Util.addUrlSpansWithAutolink +import java.net.URI +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Locale + +object LinkPreviewUtil { + + private val DOMAIN_PATTERN = Regex("^(https?://)?([^/]+).*$", RegexOption.IGNORE_CASE) + private val ALL_ASCII_PATTERN = Regex("^[\\x00-\\x7F]*$", RegexOption.IGNORE_CASE) + private val ALL_NON_ASCII_PATTERN = Regex("^[^\\x00-\\x7F]*$", RegexOption.IGNORE_CASE) + + private val OPEN_GRAPH_TAG_PATTERN = Regex( + "<\\s*meta[^>]*property\\s*=\\s*\"\\s*og:([^\"]+)\"[^>]*/?\\s*>", + RegexOption.IGNORE_CASE + ) + private val ARTICLE_TAG_PATTERN = Regex( + "<\\s*meta[^>]*property\\s*=\\s*\"\\s*article:([^\"]+)\"[^>]*/?\\s*>", + RegexOption.IGNORE_CASE + ) + private val OPEN_GRAPH_CONTENT_PATTERN = + Regex("content\\s*=\\s*\"([^\"]*)\"", RegexOption.IGNORE_CASE) + private val TITLE_PATTERN = + Regex("<\\s*title[^>]*>(.*)<\\s*/title[^>]*>", RegexOption.IGNORE_CASE) + private val FAVICON_PATTERN = + Regex("<\\s*link[^>]*rel\\s*=\\s*\".*icon.*\"[^>]*>", RegexOption.IGNORE_CASE) + private val FAVICON_HREF_PATTERN = Regex("href\\s*=\\s*\"([^\"]*)\"", RegexOption.IGNORE_CASE) + + /** + * @return All whitelisted URLs in the source text. + */ + fun findWhitelistedUrls(text: String): List { + val spannable = SpannableString(text) + spannable.addUrlSpansWithAutolink() + + val spans = spannable.getSpans(0, spannable.length, URLSpan::class.java) + val links = ArrayList(spans.size) + + for (span in spans) { + val link = Link(span.url, spannable.getSpanStart(span)) + if (isValidLinkUrl(link.url)) { + links.add(link) + } + } + + return links + } + + /** + * @return True if the host is valid. + */ + fun isValidLinkUrl(linkUrl: String?): Boolean { + if (linkUrl.isNullOrEmpty()) return false + + val url = linkUrl.toHttpUrlOrNull() ?: return false + return url.scheme == "https" && isLegalUrl(linkUrl) + } + + /** + * @return True if the top-level domain is valid. + */ + fun isValidMediaUrl(mediaUrl: String?): Boolean { + if (mediaUrl.isNullOrEmpty()) return false + + val url = mediaUrl.toHttpUrlOrNull() ?: return false + return url.scheme == "https" && isLegalUrl(mediaUrl) + } + + fun isLegalUrl(url: String): Boolean { + val match = DOMAIN_PATTERN.matchEntire(url) ?: return false + val domain = match.groupValues.getOrNull(2) ?: return false + val cleanedDomain = domain.replace(".", "") + + return ALL_ASCII_PATTERN.matches(cleanedDomain) || ALL_NON_ASCII_PATTERN.matches( + cleanedDomain + ) + } + + fun isValidMimeType(url: String): Boolean { + val path = try { + URI(url).path.lowercase(Locale.ROOT) + } catch (e: Exception) { + return false + } + + val validExtensions = arrayOf(".jpg", ".png", ".gif", ".jpeg") + + if (!path.contains('.')) return true + + return validExtensions.any { path.endsWith(it) } + } + + fun parseOpenGraphFields(html: String?): OpenGraph { + return parseOpenGraphFields(html) { encoded -> + HtmlCompat.fromHtml(encoded, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() + } + } + + internal fun parseOpenGraphFields(html: String?, htmlDecoder: HtmlDecoder): OpenGraph { + if (html == null) { + return OpenGraph(emptyMap(), null, null) + } + + val openGraphTags = HashMap() + + fun extractTags(tagRegex: Regex) { + tagRegex.findAll(html).forEach { match -> + val fullTag = match.value + val property = match.groupValues.getOrNull(1) + if (!property.isNullOrEmpty()) { + val contentMatch = OPEN_GRAPH_CONTENT_PATTERN.find(fullTag) + val content = contentMatch?.groupValues?.getOrNull(1) + if (content != null) { + val decoded = htmlDecoder.fromEncoded(content) + // Store the lower-cased property name. + openGraphTags[property.lowercase()] = decoded + } + } + } + } + + extractTags(OPEN_GRAPH_TAG_PATTERN) + extractTags(ARTICLE_TAG_PATTERN) + + var htmlTitle = "" + var faviconUrl = "" + + TITLE_PATTERN.find(html)?.let { titleMatch -> + val title = titleMatch.groupValues.getOrNull(1) + if (title != null) { + htmlTitle = htmlDecoder.fromEncoded(title) + } + } + + FAVICON_PATTERN.find(html)?.let { faviconTagMatch -> + val hrefMatch = FAVICON_HREF_PATTERN.find(faviconTagMatch.value) + val href = hrefMatch?.groupValues?.getOrNull(1) + if (href != null) { + faviconUrl = href + } + } + + return OpenGraph(openGraphTags, htmlTitle, faviconUrl) + } + + class OpenGraph( + private val values: Map, + private val htmlTitle: String?, + private val faviconUrl: String? + ) { + + companion object { + private const val KEY_TITLE = "title" + private const val KEY_IMAGE_URL = "image" + private const val KEY_PUBLISHED_TIME_1 = "published_time" + private const val KEY_PUBLISHED_TIME_2 = "article:published_time" + private const val KEY_MODIFIED_TIME_1 = "modified_time" + private const val KEY_MODIFIED_TIME_2 = "article:modified_time" + } + + val title: String? + get() = Util.getFirstNonEmpty(values[KEY_TITLE], htmlTitle) + + val imageUrl: String? + get() = Util.getFirstNonEmpty(values[KEY_IMAGE_URL], faviconUrl) + + private fun parseISO8601(date: String?): Long { + if (date.isNullOrEmpty()) return -1L + + val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()) + return try { + format.parse(date)?.time ?: -1L + } catch (pe: ParseException) { + Log.w("OpenGraph", "Failed to parse date.", pe) + -1L + } + } + + @SuppressLint("ObsoleteSdkInt") + fun getDate(): Long { + val candidates = arrayOf( + values[KEY_PUBLISHED_TIME_1], + values[KEY_PUBLISHED_TIME_2], + values[KEY_MODIFIED_TIME_1], + values[KEY_MODIFIED_TIME_2] + ) + + for (c in candidates) { + val t = parseISO8601(c) + if (t > 0) return t + } + + return 0L + } + } + + fun interface HtmlDecoder { + fun fromEncoded(html: String): String + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/net/ContentProxySafetyInterceptor.java b/app/src/main/java/org/thoughtcrime/securesms/net/ContentProxySafetyInterceptor.java deleted file mode 100644 index 983a3374ef..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/net/ContentProxySafetyInterceptor.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.thoughtcrime.securesms.net; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; -import org.session.libsignal.utilities.Log; - -import java.io.IOException; - -import okhttp3.HttpUrl; -import okhttp3.Interceptor; -import okhttp3.Response; - -/** - * Interceptor to do extra safety checks on requests through the {@link ContentProxySelector} - * to prevent non-whitelisted requests from getting to it. In particular, this guards against - * requests redirecting to non-whitelisted domains. - * - * Note that because of the way interceptors are ordered, OkHttp will hit the proxy with the - * bad-redirected-domain before we can intercept the request, so we have to "look ahead" by - * detecting a redirected response on the first pass. - */ -public class ContentProxySafetyInterceptor implements Interceptor { - - private static final String TAG = Log.tag(ContentProxySafetyInterceptor.class); - - @Override - public @NonNull Response intercept(@NonNull Chain chain) throws IOException { - if (isWhitelisted(chain.request().url())) { - Response response = chain.proceed(chain.request()); - - if (response.isRedirect()) { - if (isWhitelisted(response.header("location")) || isWhitelisted(response.header("Location"))) { - return response; - } else { - Log.w(TAG, "Tried to redirect to a non-whitelisted domain!"); - chain.call().cancel(); - throw new IOException("Tried to redirect to a non-whitelisted domain!"); - } - } else { - return response; - } - } else { - Log.w(TAG, "Request was for a non-whitelisted domain!"); - chain.call().cancel(); - throw new IOException("Request was for a non-whitelisted domain!"); - } - } - - private static boolean isWhitelisted(@NonNull HttpUrl url) { - return isWhitelisted(url.toString()); - } - - private static boolean isWhitelisted(@Nullable String url) { - return LinkPreviewUtil.isValidLinkUrl(url) || LinkPreviewUtil.isValidMediaUrl(url); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/net/ContentProxySafetyInterceptor.kt b/app/src/main/java/org/thoughtcrime/securesms/net/ContentProxySafetyInterceptor.kt new file mode 100644 index 0000000000..5857964a32 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/net/ContentProxySafetyInterceptor.kt @@ -0,0 +1,55 @@ +package org.thoughtcrime.securesms.net + +import okhttp3.HttpUrl +import okhttp3.Interceptor +import okhttp3.Response +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil.isValidLinkUrl +import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil.isValidMediaUrl +import java.io.IOException + +/** + * Interceptor to do extra safety checks on requests through the [ContentProxySelector] + * to prevent non-whitelisted requests from getting to it. In particular, this guards against + * requests redirecting to non-whitelisted domains. + * + * Note that because of the way interceptors are ordered, OkHttp will hit the proxy with the + * bad-redirected-domain before we can intercept the request, so we have to "look ahead" by + * detecting a redirected response on the first pass. + */ +class ContentProxySafetyInterceptor : Interceptor { + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + if (isWhitelisted(chain.request().url)) { + val response = chain.proceed(chain.request()) + + if (response.isRedirect) { + if (isWhitelisted(response.header("location")) || isWhitelisted(response.header("Location"))) { + return response + } else { + Log.w(TAG, "Tried to redirect to a non-whitelisted domain!") + chain.call().cancel() + throw IOException("Tried to redirect to a non-whitelisted domain!") + } + } else { + return response + } + } else { + Log.w(TAG, "Request was for a non-whitelisted domain!") + chain.call().cancel() + throw IOException("Request was for a non-whitelisted domain!") + } + } + + companion object { + private val TAG: String = Log.tag(ContentProxySafetyInterceptor::class.java) + + private fun isWhitelisted(url: HttpUrl): Boolean { + return isWhitelisted(url.toString()) + } + + private fun isWhitelisted(url: String?): Boolean { + return isValidLinkUrl(url) || isValidMediaUrl(url) + } + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtilTest.java b/app/src/test/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtilTest.java deleted file mode 100644 index e45d5b3897..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtilTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.thoughtcrime.securesms.linkpreview; - -import org.junit.Test; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static junit.framework.TestCase.assertEquals; -import static junit.framework.TestCase.assertFalse; -import static junit.framework.TestCase.assertTrue; - -public class LinkPreviewUtilTest { - - @Test - public void isLegal_allAscii_noProtocol() { - assertTrue(LinkPreviewUtil.isLegalUrl("google.com")); - } - - @Test - public void isLegal_allAscii_noProtocol_subdomain() { - assertTrue(LinkPreviewUtil.isLegalUrl("foo.google.com")); - } - - @Test - public void isLegal_allAscii_subdomain() { - assertTrue(LinkPreviewUtil.isLegalUrl("https://foo.google.com")); - } - - @Test - public void isLegal_allAscii_subdomain_path() { - assertTrue(LinkPreviewUtil.isLegalUrl("https://foo.google.com/some/path.html")); - } - - @Test - public void isLegal_cyrillicHostAsciiTld() { - assertFalse(LinkPreviewUtil.isLegalUrl("http://кц.com")); - } - - @Test - public void isLegal_cyrillicHostAsciiTld_noProtocol() { - assertFalse(LinkPreviewUtil.isLegalUrl("кц.com")); - } - - @Test - public void isLegal_mixedHost_noProtocol() { - assertFalse(LinkPreviewUtil.isLegalUrl("http://asĸ.com")); - } - - @Test - public void isLegal_cyrillicHostAndTld_noProtocol() { - assertTrue(LinkPreviewUtil.isLegalUrl("кц.рф")); - } - - @Test - public void isLegal_cyrillicHostAndTld_asciiPath_noProtocol() { - assertTrue(LinkPreviewUtil.isLegalUrl("кц.рф/some/path")); - } - - @Test - public void isLegal_cyrillicHostAndTld_asciiPath() { - assertTrue(LinkPreviewUtil.isLegalUrl("https://кц.рф/some/path")); - } - - @Test - public void isLegal_asciiSubdomain_cyrillicHostAndTld() { - assertFalse(LinkPreviewUtil.isLegalUrl("http://foo.кц.рф")); - } - - @Test - public void isLegal_emptyUrl() { - assertFalse(LinkPreviewUtil.isLegalUrl("")); - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtilTest.kt b/app/src/test/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtilTest.kt new file mode 100644 index 0000000000..965b57b68f --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtilTest.kt @@ -0,0 +1,67 @@ +package org.thoughtcrime.securesms.linkpreview + +import junit.framework.TestCase +import org.junit.Test +import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil.isLegalUrl + +class LinkPreviewUtilTest { + @Test + fun isLegal_allAscii_noProtocol() { + TestCase.assertTrue(isLegalUrl("google.com")) + } + + @Test + fun isLegal_allAscii_noProtocol_subdomain() { + TestCase.assertTrue(isLegalUrl("foo.google.com")) + } + + @Test + fun isLegal_allAscii_subdomain() { + TestCase.assertTrue(isLegalUrl("https://foo.google.com")) + } + + @Test + fun isLegal_allAscii_subdomain_path() { + TestCase.assertTrue(isLegalUrl("https://foo.google.com/some/path.html")) + } + + @Test + fun isLegal_cyrillicHostAsciiTld() { + TestCase.assertFalse(isLegalUrl("http://кц.com")) + } + + @Test + fun isLegal_cyrillicHostAsciiTld_noProtocol() { + TestCase.assertFalse(isLegalUrl("кц.com")) + } + + @Test + fun isLegal_mixedHost_noProtocol() { + TestCase.assertFalse(isLegalUrl("http://asĸ.com")) + } + + @Test + fun isLegal_cyrillicHostAndTld_noProtocol() { + TestCase.assertTrue(isLegalUrl("кц.рф")) + } + + @Test + fun isLegal_cyrillicHostAndTld_asciiPath_noProtocol() { + TestCase.assertTrue(isLegalUrl("кц.рф/some/path")) + } + + @Test + fun isLegal_cyrillicHostAndTld_asciiPath() { + TestCase.assertTrue(isLegalUrl("https://кц.рф/some/path")) + } + + @Test + fun isLegal_asciiSubdomain_cyrillicHostAndTld() { + TestCase.assertFalse(isLegalUrl("http://foo.кц.рф")) + } + + @Test + fun isLegal_emptyUrl() { + TestCase.assertFalse(isLegalUrl("")) + } +}