2019-01-15 00:41:05 -08:00
|
|
|
package org.thoughtcrime.securesms.linkpreview;
|
|
|
|
|
|
|
|
import android.support.annotation.NonNull;
|
2019-05-06 12:25:53 -07:00
|
|
|
import android.support.annotation.Nullable;
|
2019-01-15 00:41:05 -08:00
|
|
|
import android.text.SpannableString;
|
|
|
|
import android.text.TextUtils;
|
|
|
|
import android.text.style.URLSpan;
|
|
|
|
import android.text.util.Linkify;
|
|
|
|
|
|
|
|
import com.annimon.stream.Stream;
|
|
|
|
|
2019-04-17 10:21:30 -04:00
|
|
|
import org.thoughtcrime.securesms.stickers.StickerUrl;
|
|
|
|
|
2019-01-15 00:41:05 -08:00
|
|
|
import java.util.Collections;
|
|
|
|
import java.util.List;
|
2019-03-18 17:37:19 -07:00
|
|
|
import java.util.regex.Matcher;
|
|
|
|
import java.util.regex.Pattern;
|
2019-01-15 00:41:05 -08:00
|
|
|
|
|
|
|
import okhttp3.HttpUrl;
|
|
|
|
|
|
|
|
public final class LinkPreviewUtil {
|
|
|
|
|
2019-03-18 17:37:19 -07:00
|
|
|
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]*$");
|
2019-04-17 10:21:30 -04:00
|
|
|
private static final Pattern STICKER_URL_PATTERN = Pattern.compile("^.*#pack_id=(.*)&pack_key=(.*)$");
|
2019-03-18 17:37:19 -07:00
|
|
|
|
2019-01-15 00:41:05 -08:00
|
|
|
/**
|
|
|
|
* @return All whitelisted URLs in the source text.
|
|
|
|
*/
|
2019-02-14 13:55:48 -08:00
|
|
|
public static @NonNull List<Link> findWhitelistedUrls(@NonNull String text) {
|
2019-01-15 00:41:05 -08:00
|
|
|
SpannableString spannable = new SpannableString(text);
|
|
|
|
boolean found = Linkify.addLinks(spannable, Linkify.WEB_URLS);
|
|
|
|
|
|
|
|
if (!found) {
|
|
|
|
return Collections.emptyList();
|
|
|
|
}
|
|
|
|
|
|
|
|
return Stream.of(spannable.getSpans(0, spannable.length(), URLSpan.class))
|
2019-02-14 13:55:48 -08:00
|
|
|
.map(span -> new Link(span.getURL(), spannable.getSpanStart(span)))
|
|
|
|
.filter(link -> isWhitelistedLinkUrl(link.getUrl()))
|
2019-01-15 00:41:05 -08:00
|
|
|
.toList();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return True if the host is present in the link whitelist.
|
|
|
|
*/
|
2019-05-06 12:25:53 -07:00
|
|
|
public static boolean isWhitelistedLinkUrl(@Nullable String linkUrl) {
|
2019-04-17 10:21:30 -04:00
|
|
|
if (linkUrl == null) return false;
|
|
|
|
if (StickerUrl.isValidShareLink(linkUrl)) return true;
|
2019-05-06 12:25:53 -07:00
|
|
|
|
2019-01-15 00:41:05 -08:00
|
|
|
HttpUrl url = HttpUrl.parse(linkUrl);
|
2019-02-20 17:00:23 -08:00
|
|
|
return url != null &&
|
|
|
|
!TextUtils.isEmpty(url.scheme()) &&
|
|
|
|
"https".equals(url.scheme()) &&
|
|
|
|
LinkPreviewDomains.LINKS.contains(url.host()) &&
|
|
|
|
isLegalUrl(linkUrl);
|
2019-01-15 00:41:05 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return True if the top-level domain is present in the media whitelist.
|
|
|
|
*/
|
2019-05-06 12:25:53 -07:00
|
|
|
public static boolean isWhitelistedMediaUrl(@Nullable String mediaUrl) {
|
|
|
|
if (mediaUrl == null) return false;
|
|
|
|
|
2019-01-15 00:41:05 -08:00
|
|
|
HttpUrl url = HttpUrl.parse(mediaUrl);
|
2019-02-20 17:00:23 -08:00
|
|
|
return url != null &&
|
|
|
|
!TextUtils.isEmpty(url.scheme()) &&
|
|
|
|
"https".equals(url.scheme()) &&
|
|
|
|
LinkPreviewDomains.IMAGES.contains(url.topPrivateDomain()) &&
|
|
|
|
isLegalUrl(mediaUrl);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static boolean isLegalUrl(@NonNull String url) {
|
2019-03-18 17:37:19 -07:00
|
|
|
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;
|
2019-02-20 17:00:23 -08:00
|
|
|
}
|
2019-01-15 00:41:05 -08:00
|
|
|
}
|
|
|
|
}
|