Fix .onion link linkification.

Fixes #11458.
fork-5.53.8
Justin Tracey 2022-04-14 17:58:05 -04:00 zatwierdzone przez Greyson Parrelli
rodzic 6563ea970f
commit 8a2f89b4f6
8 zmienionych plików z 108 dodań i 77 usunięć

Wyświetl plik

@ -106,7 +106,6 @@ import org.thoughtcrime.securesms.jobs.MmsSendJob;
import org.thoughtcrime.securesms.jobs.SmsSendJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.PartAuthority;
@ -123,6 +122,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.revealable.ViewOnceMessageView;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan;
import org.thoughtcrime.securesms.util.LinkUtil;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.PlaceholderURLSpan;
@ -1362,7 +1362,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
if (hasLinks) {
Stream.of(messageBody.getSpans(0, messageBody.length(), URLSpan.class))
.filterNot(url -> LinkPreviewUtil.isLegalUrl(url.getURL()))
.filterNot(url -> LinkUtil.isLegalUrl(url.getURL()))
.forEach(messageBody::removeSpan);
URLSpan[] urlSpans = messageBody.getSpans(0, messageBody.length(), URLSpan.class);

Wyświetl plik

@ -19,7 +19,7 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.util.LinkUtil;
import org.thoughtcrime.securesms.util.LongClickCopySpan;
public final class GroupDescriptionUtil {
@ -43,7 +43,7 @@ public final class GroupDescriptionUtil {
if (hasLinks) {
Stream.of(descriptionSpannable.getSpans(0, descriptionSpannable.length(), URLSpan.class))
.filterNot(url -> LinkPreviewUtil.isLegalUrl(url.getURL()))
.filterNot(url -> LinkUtil.isLegalUrl(url.getURL()))
.forEach(descriptionSpannable::removeSpan);
URLSpan[] urlSpans = descriptionSpannable.getSpans(0, descriptionSpannable.length(), URLSpan.class);

Wyświetl plik

@ -11,6 +11,7 @@ import androidx.core.util.Consumer;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.signal.core.util.Hex;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.InvalidMessageException;
@ -45,8 +46,8 @@ import org.thoughtcrime.securesms.stickers.StickerUrl;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.ByteUnit;
import org.signal.core.util.Hex;
import org.thoughtcrime.securesms.util.ImageCompressionUtil;
import org.thoughtcrime.securesms.util.LinkUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.OkHttpUtil;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
@ -95,7 +96,7 @@ public class LinkPreviewRepository {
CompositeRequestController compositeController = new CompositeRequestController();
if (!LinkPreviewUtil.isValidPreviewUrl(url)) {
if (!LinkUtil.isValidPreviewUrl(url)) {
Log.w(TAG, "Tried to get a link preview for a non-whitelisted domain.");
callback.onError(Error.PREVIEW_NOT_AVAILABLE);
return compositeController;
@ -164,7 +165,7 @@ public class LinkPreviewRepository {
Optional<String> imageUrl = openGraph.getImageUrl();
long date = openGraph.getDate();
if (imageUrl.isPresent() && !LinkPreviewUtil.isValidPreviewUrl(imageUrl.get())) {
if (imageUrl.isPresent() && !LinkUtil.isValidPreviewUrl(imageUrl.get())) {
Log.i(TAG, "Image URL was invalid or for a non-whitelisted domain. Skipping.");
imageUrl = Optional.empty();
}

Wyświetl plik

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.linkpreview;
import android.annotation.SuppressLint;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.URLSpan;
import android.text.util.Linkify;
@ -14,10 +13,8 @@ import androidx.core.text.util.LinkifyCompat;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.stickers.StickerUrl;
import org.thoughtcrime.securesms.util.DateUtils;
import org.signal.core.util.SetUtil;
import org.thoughtcrime.securesms.util.LinkUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.util.OptionalUtil;
@ -34,12 +31,6 @@ import okhttp3.HttpUrl;
public final class LinkPreviewUtil {
private static final String TAG = Log.tag(LinkPreviewUtil.class);
private static final Pattern DOMAIN_PATTERN = Pattern.compile("^(https?://)?([^/]+).*$");
private static final Pattern ALL_ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7F]*$");
private static final Pattern ALL_NON_ASCII_PATTERN = Pattern.compile("^[^\\x00-\\x7F]*$");
private static final Pattern ILLEGAL_CHARACTERS_PATTERN = Pattern.compile("[\u202C\u202D\u202E\u2500-\u25FF]");
private static final Pattern OPEN_GRAPH_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*og:([^\"]+)\"[^>]*/?\\s*>");
private static final Pattern ARTICLE_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*article:([^\"]+)\"[^>]*/?\\s*>");
private static final Pattern OPEN_GRAPH_CONTENT_PATTERN = Pattern.compile("content\\s*=\\s*\"([^\"]*)\"");
@ -47,8 +38,6 @@ public final class LinkPreviewUtil {
private static final Pattern FAVICON_PATTERN = Pattern.compile("<\\s*link[^>]*rel\\s*=\\s*\".*icon.*\"[^>]*>");
private static final Pattern FAVICON_HREF_PATTERN = Pattern.compile("href\\s*=\\s*\"([^\"]*)\"");
private static final Set<String> INVALID_TOP_LEVEL_DOMAINS = SetUtil.newHashSet("onion", "i2p");
public static @Nullable String getTopLevelDomain(@Nullable String urlString) {
if (!Util.isEmpty(urlString)) {
HttpUrl url = HttpUrl.parse(urlString);
@ -61,7 +50,7 @@ public final class LinkPreviewUtil {
}
/**
* @return All whitelisted URLs in the source text.
* @return All URLs allowed as previews in the source text.
*/
public static @NonNull Links findValidPreviewUrls(@NonNull String text) {
SpannableString spannable = new SpannableString(text);
@ -73,47 +62,10 @@ public final class LinkPreviewUtil {
return new Links(Stream.of(spannable.getSpans(0, spannable.length(), URLSpan.class))
.map(span -> new Link(span.getURL(), spannable.getSpanStart(span)))
.filter(link -> isValidPreviewUrl(link.getUrl()))
.filter(link -> LinkUtil.isValidPreviewUrl(link.getUrl()))
.toList());
}
/**
* @return True if the host is present in the link whitelist.
*/
public static boolean isValidPreviewUrl(@Nullable String linkUrl) {
if (linkUrl == null) return false;
if (StickerUrl.isValidShareLink(linkUrl)) return true;
HttpUrl url = HttpUrl.parse(linkUrl);
return url != null &&
!TextUtils.isEmpty(url.scheme()) &&
"https".equals(url.scheme()) &&
isLegalUrl(linkUrl);
}
public static boolean isLegalUrl(@NonNull String url) {
if (ILLEGAL_CHARACTERS_PATTERN.matcher(url).find()) {
return false;
}
Matcher matcher = DOMAIN_PATTERN.matcher(url);
if (matcher.matches()) {
String domain = matcher.group(2);
String cleanedDomain = domain.replaceAll("\\.", "");
String topLevelDomain = parseTopLevelDomain(domain);
boolean validCharacters = ALL_ASCII_PATTERN.matcher(cleanedDomain).matches() ||
ALL_NON_ASCII_PATTERN.matcher(cleanedDomain).matches();
boolean validTopLevelDomain = !INVALID_TOP_LEVEL_DOMAINS.contains(topLevelDomain);
return validCharacters && validTopLevelDomain;
} else {
return false;
}
}
public static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html) {
if (html == null) {
return new OpenGraph(Collections.emptyMap(), null, null);
@ -169,16 +121,6 @@ public final class LinkPreviewUtil {
return new OpenGraph(openGraphTags, htmlTitle, faviconUrl);
}
private static @Nullable String parseTopLevelDomain(@NonNull String domain) {
int periodIndex = domain.lastIndexOf(".");
if (periodIndex >= 0 && periodIndex < domain.length() - 1) {
return domain.substring(periodIndex + 1);
} else {
return null;
}
}
private static @NonNull String fromDoubleEncoded(@NonNull String html) {
return HtmlCompat.fromHtml(HtmlCompat.fromHtml(html, 0).toString(), 0).toString();
}

Wyświetl plik

@ -26,8 +26,8 @@ import org.thoughtcrime.securesms.components.FullScreenDialogFragment;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.conversation.colors.ColorizerView;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.LinkUtil;
import org.thoughtcrime.securesms.util.Projection;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.views.Stub;
@ -152,7 +152,7 @@ public class LongMessageFragment extends FullScreenDialogFragment {
if (hasLinks) {
Stream.of(messageBody.getSpans(0, messageBody.length(), URLSpan.class))
.filterNot(url -> LinkPreviewUtil.isLegalUrl(url.getURL()))
.filterNot(url -> LinkUtil.isLegalUrl(url.getURL()))
.forEach(messageBody::removeSpan);
}
return messageBody;

Wyświetl plik

@ -128,6 +128,7 @@ import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.LinkUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -2702,7 +2703,7 @@ public final class MessageContentProcessor {
Optional<String> description = Optional.ofNullable(preview.getDescription());
boolean hasTitle = !TextUtils.isEmpty(title.orElse(""));
boolean presentInBody = url.isPresent() && urlsInMessage.containsUrl(url.get());
boolean validDomain = url.isPresent() && LinkPreviewUtil.isValidPreviewUrl(url.get());
boolean validDomain = url.isPresent() && LinkUtil.isValidPreviewUrl(url.get());
if (hasTitle && (presentInBody || isStoryEmbed) && validDomain) {
LinkPreview linkPreview = new LinkPreview(url.get(), title.orElse(""), description.orElse(""), preview.getDate(), thumbnail);

Wyświetl plik

@ -0,0 +1,87 @@
package org.thoughtcrime.securesms.util;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.SetUtil;
import org.thoughtcrime.securesms.stickers.StickerUrl;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okhttp3.HttpUrl;
public final class LinkUtil {
private static final Pattern DOMAIN_PATTERN = Pattern.compile("^(https?://)?([^/]+).*$");
private static final Pattern ALL_ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7F]*$");
private static final Pattern ALL_NON_ASCII_PATTERN = Pattern.compile("^[^\\x00-\\x7F]*$");
private static final Pattern ILLEGAL_CHARACTERS_PATTERN = Pattern.compile("[\u202C\u202D\u202E\u2500-\u25FF]");
private static final Set<String> INVALID_TOP_LEVEL_DOMAINS = SetUtil.newHashSet("onion", "i2p");
private LinkUtil() {}
/**
* @return True if URL is valid for link previews.
*/
public static boolean isValidPreviewUrl(@Nullable String linkUrl) {
if (linkUrl == null) {
return false;
}
if (StickerUrl.isValidShareLink(linkUrl)) {
return true;
}
HttpUrl url = HttpUrl.parse(linkUrl);
return url != null &&
!TextUtils.isEmpty(url.scheme()) &&
"https".equals(url.scheme()) &&
isLegalUrl(linkUrl, false);
}
/**
* @return True if URL is valid, mostly useful for linkifying.
*/
public static boolean isLegalUrl(@NonNull String url) {
return isLegalUrl(url, true);
}
private static boolean isLegalUrl(@NonNull String url, boolean skipTopLevelDomainValidation) {
if (ILLEGAL_CHARACTERS_PATTERN.matcher(url).find()) {
return false;
}
Matcher matcher = DOMAIN_PATTERN.matcher(url);
if (matcher.matches()) {
String domain = Objects.requireNonNull(matcher.group(2));
String cleanedDomain = domain.replaceAll("\\.", "");
String topLevelDomain = parseTopLevelDomain(domain);
boolean validCharacters = ALL_ASCII_PATTERN.matcher(cleanedDomain).matches() ||
ALL_NON_ASCII_PATTERN.matcher(cleanedDomain).matches();
boolean validTopLevelDomain = skipTopLevelDomainValidation || !INVALID_TOP_LEVEL_DOMAINS.contains(topLevelDomain);
return validCharacters && validTopLevelDomain;
} else {
return false;
}
}
private static @Nullable String parseTopLevelDomain(@NonNull String domain) {
int periodIndex = domain.lastIndexOf(".");
if (periodIndex >= 0 && periodIndex < domain.length() - 1) {
return domain.substring(periodIndex + 1);
} else {
return null;
}
}
}

Wyświetl plik

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.linkpreview;
package org.thoughtcrime.securesms.util;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -10,7 +10,7 @@ import java.util.Collection;
import static junit.framework.TestCase.assertEquals;
@RunWith(Parameterized.class)
public class LinkPreviewUtilTest_isLegal {
public class LinkUtilTest_isLegal {
private final String input;
private final boolean output;
@ -24,12 +24,12 @@ public class LinkPreviewUtilTest_isLegal {
{ "https://foo.google.com/some/path.html", true },
{ "кц.рф", true },
{ "https://кц.рф/some/path", true },
{ "https://abcdefg.onion", true },
{ "https://abcdefg.i2p", true },
{ "http://кц.com", false },
{ "кц.com", false },
{ "http://asĸ.com", false },
{ "http://foo.кц.рф", false },
{ "https://abcdefg.onion", false },
{ "https://abcdefg.i2p", false },
{ "кц.рф\u202C", false },
{ "кц.рф\u202D", false },
{ "кц.рф\u202E", false },
@ -40,13 +40,13 @@ public class LinkPreviewUtilTest_isLegal {
});
}
public LinkPreviewUtilTest_isLegal(String input, boolean output) {
public LinkUtilTest_isLegal(String input, boolean output) {
this.input = input;
this.output = output;
}
@Test
public void isLegal() {
assertEquals(output, LinkPreviewUtil.isLegalUrl(input));
assertEquals(output, LinkUtil.isLegalUrl(input));
}
}