diff --git a/briar-android/res/layout/fragment_blog_post.xml b/briar-android/res/layout/fragment_blog_post.xml index 18ec1f417cd1f72ba54f917eabb8913ea2c04631..3fdce0b1c5a634db1ee967423f13ee85ae620299 100644 --- a/briar-android/res/layout/fragment_blog_post.xml +++ b/briar-android/res/layout/fragment_blog_post.xml @@ -10,7 +10,7 @@ android:descendantFocusability="beforeDescendants" android:focusable="true" android:focusableInTouchMode="true"> - <!-- Above Focusability attributes prevent automatic scroll-down, + <!-- Above focusability attributes prevent automatic scroll-down, because body text is selectable --> <include diff --git a/briar-android/res/layout/fragment_link_dialog.xml b/briar-android/res/layout/fragment_link_dialog.xml new file mode 100644 index 0000000000000000000000000000000000000000..307b7110eebb5abe09820f5cb18edb7f30a6d5da --- /dev/null +++ b/briar-android/res/layout/fragment_link_dialog.xml @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="@dimen/margin_large"> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/link_warning_title" + android:textColor="@color/briar_primary" + android:textSize="@dimen/text_size_large" + android:textStyle="bold"/> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/margin_large" + android:text="@string/link_warning_intro" + android:textColor="@color/briar_primary" + android:textSize="@dimen/text_size_medium"/> + + <TextView + android:id="@+id/urlView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/margin_large" + android:textIsSelectable="true" + android:typeface="monospace" + tools:text="http://very.bad.site.com"/> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/margin_large" + android:text="@string/link_warning_text" + android:textColor="@color/briar_primary" + android:textSize="@dimen/text_size_medium"/> + + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <Button + android:id="@+id/cancelButton" + style="@style/BriarButtonFlat.Positive" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="0.5" + android:text="@string/cancel"/> + + <Button + android:id="@+id/openButton" + style="@style/BriarButtonFlat.Negative" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="0.5" + android:text="@string/link_warning_open_link"/> + + </LinearLayout> + +</LinearLayout> \ No newline at end of file diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index e866545097665ba4ad001540e73f0e50ce157360..626f3d8a8871164dc589d41bf566a512ad1edfec 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -308,6 +308,12 @@ <string name="feedback_settings_title">Feedback</string> <string name="send_feedback">Send feedback</string> + <!-- Link Warning --> + <string name="link_warning_title">Link Warning</string> + <string name="link_warning_intro">You are about to open the following link with an external app.</string> + <string name="link_warning_text">This can be used to identify you. Think about whether you trust the person that sent you this link and consider opening it with Orfox.</string> + <string name="link_warning_open_link">Open Link</string> + <!-- Multiple Identities --> <string name="anonymous">Anonymous</string> <string name="new_identity_title">New Identity</string> diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostViewHolder.java b/briar-android/src/org/briarproject/android/blogs/BlogPostViewHolder.java index 13a1b3f177215d6b94b9bf23f689bf3891cd4316..d870db1370f04851180dac7c4b4d9b75cab3b3a2 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogPostViewHolder.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogPostViewHolder.java @@ -8,6 +8,7 @@ import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityOptionsCompat; import android.support.v4.view.ViewCompat; import android.support.v7.widget.RecyclerView; +import android.text.Spanned; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; @@ -30,6 +31,8 @@ import static org.briarproject.android.BriarActivity.GROUP_ID; import static org.briarproject.android.blogs.BasePostPagerFragment.POST_ID; import static org.briarproject.android.util.AndroidUtils.TEASER_LENGTH; import static org.briarproject.android.util.AndroidUtils.getTeaser; +import static org.briarproject.android.util.AndroidUtils.getSpanned; +import static org.briarproject.android.util.AndroidUtils.makeLinksClickable; import static org.briarproject.api.blogs.MessageType.POST; @UiThread @@ -108,15 +111,17 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder { } // post body - CharSequence bodyText = item.getBody(); + Spanned bodyText = getSpanned(item.getBody()); if (listener == null) { + body.setText(bodyText); body.setTextIsSelectable(true); + makeLinksClickable(body); } else { body.setTextIsSelectable(false); if (item.getBody().length() > TEASER_LENGTH) - bodyText = getTeaser(ctx, item.getBody()); + bodyText = getTeaser(ctx, bodyText); + body.setText(bodyText); } - body.setText(bodyText); // reblog button reblogButton.setOnClickListener(new OnClickListener() { diff --git a/briar-android/src/org/briarproject/android/util/AndroidUtils.java b/briar-android/src/org/briarproject/android/util/AndroidUtils.java index 66593d0f8281c64d45d9ea9517bf9655840756c1..759a25e6b47b56bb36af1cb51fba0bd364f3dd89 100644 --- a/briar-android/src/org/briarproject/android/util/AndroidUtils.java +++ b/briar-android/src/org/briarproject/android/util/AndroidUtils.java @@ -6,14 +6,24 @@ import android.content.Context; import android.os.Build; import android.provider.Settings; import android.support.design.widget.TextInputLayout; +import android.support.v4.app.FragmentManager; import android.support.v4.content.ContextCompat; +import android.support.v7.app.AppCompatActivity; +import android.text.Html; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; +import android.text.Spanned; import android.text.format.DateUtils; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; import android.text.style.ForegroundColorSpan; +import android.text.style.URLSpan; +import android.view.View; +import android.widget.TextView; import org.briarproject.R; +import org.briarproject.android.widget.LinkDialogFragment; import org.briarproject.util.IoUtils; import org.briarproject.util.StringUtils; @@ -36,7 +46,7 @@ import static android.text.format.DateUtils.WEEK_IN_MILLIS; public class AndroidUtils { public static final long MIN_RESOLUTION = MINUTE_IN_MILLIS; - public static final int TEASER_LENGTH = 240; + public static final int TEASER_LENGTH = 320; // Fake Bluetooth address returned by BluetoothAdapter on API 23 and later private static final String FAKE_BLUETOOTH_ADDRESS = "02:00:00:00:00:00"; @@ -121,13 +131,13 @@ public class AndroidUtils { MIN_RESOLUTION, flags).toString(); } - public static SpannableStringBuilder getTeaser(Context ctx, String body) { + public static SpannableStringBuilder getTeaser(Context ctx, Spanned body) { if (body.length() < TEASER_LENGTH) throw new IllegalArgumentException( "String is shorter than TEASER_LENGTH"); SpannableStringBuilder builder = - new SpannableStringBuilder(body.substring(0, TEASER_LENGTH)); + new SpannableStringBuilder(body.subSequence(0, TEASER_LENGTH)); String ellipsis = ctx.getString(R.string.ellipsis); builder.append(ellipsis).append(" "); @@ -142,4 +152,31 @@ public class AndroidUtils { return builder; } + public static Spanned getSpanned(String s) { + return Html.fromHtml(s); + } + + public static void makeLinksClickable(TextView v) { + SpannableStringBuilder ssb = new SpannableStringBuilder(v.getText()); + URLSpan[] spans = ssb.getSpans(0, ssb.length(), URLSpan.class); + for (URLSpan span : spans) { + int start = ssb.getSpanStart(span); + int end = ssb.getSpanEnd(span); + final String url = span.getURL(); + ssb.removeSpan(span); + ClickableSpan cSpan = new ClickableSpan() { + @Override + public void onClick(View v2) { + LinkDialogFragment f = LinkDialogFragment.newInstance(url); + FragmentManager fm = ((AppCompatActivity) v2.getContext()) + .getSupportFragmentManager(); + f.show(fm, f.getUniqueTag()); + } + }; + ssb.setSpan(cSpan, start, end, 0); + } + v.setText(ssb); + v.setMovementMethod(ArticleMovementMethod.getInstance()); + } + } diff --git a/briar-android/src/org/briarproject/android/util/ArticleMovementMethod.java b/briar-android/src/org/briarproject/android/util/ArticleMovementMethod.java new file mode 100644 index 0000000000000000000000000000000000000000..ad148a719d9e78e6e7f822c6724bd7bec9268804 --- /dev/null +++ b/briar-android/src/org/briarproject/android/util/ArticleMovementMethod.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.briarproject.android.util; + +import android.text.Layout; +import android.text.Spannable; +import android.text.method.ArrowKeyMovementMethod; +import android.text.method.MovementMethod; +import android.text.style.ClickableSpan; +import android.view.MotionEvent; +import android.widget.TextView; + +public class ArticleMovementMethod extends ArrowKeyMovementMethod { + + private static ArticleMovementMethod sInstance; + + public static MovementMethod getInstance() { + if (sInstance == null) { + sInstance = new ArticleMovementMethod(); + } + return sInstance; + } + + @Override + public boolean onTouchEvent(TextView widget, Spannable buffer, + MotionEvent event) { + int action = event.getAction(); + + if (action == MotionEvent.ACTION_UP) { + int x = (int) event.getX(); + int y = (int) event.getY(); + + x -= widget.getTotalPaddingLeft(); + y -= widget.getTotalPaddingTop(); + + x += widget.getScrollX(); + y += widget.getScrollY(); + + Layout layout = widget.getLayout(); + int line = layout.getLineForVertical(y); + int off = layout.getOffsetForHorizontal(line, x); + + ClickableSpan[] link = + buffer.getSpans(off, off, ClickableSpan.class); + + if (link.length != 0) { + link[0].onClick(widget); + } + } + return super.onTouchEvent(widget, buffer, event); + } + +} diff --git a/briar-android/src/org/briarproject/android/widget/LinkDialogFragment.java b/briar-android/src/org/briarproject/android/widget/LinkDialogFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..ffee887f638c58e8f384c5868cbfde1e03a169f5 --- /dev/null +++ b/briar-android/src/org/briarproject/android/widget/LinkDialogFragment.java @@ -0,0 +1,88 @@ +package org.briarproject.android.widget; + + +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import org.briarproject.R; + +import java.util.List; + +public class LinkDialogFragment extends DialogFragment { + + private static final String TAG = LinkDialogFragment.class.getName(); + + private String url; + + public static LinkDialogFragment newInstance(String url) { + LinkDialogFragment f = new LinkDialogFragment(); + + Bundle args = new Bundle(); + args.putString("url", url); + f.setArguments(args); + + return f; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + url = getArguments().getString("url"); + + setStyle(STYLE_NO_TITLE, R.style.BriarDialogTheme); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View v = inflater.inflate(R.layout.fragment_link_dialog, container, + false); + + TextView urlView = (TextView) v.findViewById(R.id.urlView); + urlView.setText(url); + + // prepare normal intent or intent chooser + Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + PackageManager packageManager = getContext().getPackageManager(); + List activities = packageManager.queryIntentActivities(i, + PackageManager.MATCH_DEFAULT_ONLY); + boolean choice = activities.size() > 1; + final Intent intent = choice ? Intent.createChooser(i, + getString(R.string.link_warning_open_link)) : i; + + Button openButton = (Button) v.findViewById(R.id.openButton); + openButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + startActivity(intent); + getDialog().dismiss(); + } + }); + + Button cancelButton = (Button) v.findViewById(R.id.cancelButton); + cancelButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + getDialog().cancel(); + } + }); + + return v; + } + + public String getUniqueTag() { + return TAG; + } + +} diff --git a/briar-core/build.gradle b/briar-core/build.gradle index ae5071be7f7c59c2d4ac7cb348dda069c8aa9db0..9ffd0987f5189ed0d0a45e5cc781933b3866c93a 100644 --- a/briar-core/build.gradle +++ b/briar-core/build.gradle @@ -13,6 +13,7 @@ dependencies { compile 'org.jdom:jdom2:2.0.6' compile 'org.slf4j:slf4j-api:1.7.21' compile 'com.squareup.okhttp3:okhttp:3.3.1' + compile 'org.jsoup:jsoup:1.9.2' } dependencyVerification { @@ -25,6 +26,7 @@ dependencyVerification { 'com.squareup.okhttp3:okhttp:a47f4efa166551cd5acc04f1071d82dafbf05638c21f9ca13068bc6633e3bff6', 'com.rometools:rome-utils:2be18a1edc601c31fe49c2000bb5484dd75182309270c2a2561d71888d81587a', 'com.squareup.okio:okio:5cfea5afe6c6e441a4dbf6053a07a733b1249d1009382eb44ac2255ccedd0c15', + 'org.jsoup:jsoup:9c1885f1b182256e06f1e30b8451caed0c0dee96299d6348f968d18b54d0a46a', ] } diff --git a/briar-core/src/org/briarproject/feed/FeedManagerImpl.java b/briar-core/src/org/briarproject/feed/FeedManagerImpl.java index 6384f06c034b5b6e2eb362d06f95bbb4d410602e..f2ffa7ea9dcec3ff10a07e859dc43c0852545548 100644 --- a/briar-core/src/org/briarproject/feed/FeedManagerImpl.java +++ b/briar-core/src/org/briarproject/feed/FeedManagerImpl.java @@ -42,6 +42,7 @@ import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Date; @@ -66,6 +67,9 @@ import static org.briarproject.api.feed.FeedConstants.FETCH_DELAY_INITIAL; import static org.briarproject.api.feed.FeedConstants.FETCH_INTERVAL; import static org.briarproject.api.feed.FeedConstants.FETCH_UNIT; import static org.briarproject.api.feed.FeedConstants.KEY_FEEDS; +import static org.briarproject.util.HtmlUtils.article; +import static org.briarproject.util.HtmlUtils.clean; +import static org.briarproject.util.HtmlUtils.stripAll; class FeedManagerImpl implements FeedManager, Client, EventListener { @@ -337,13 +341,13 @@ class FeedManagerImpl implements FeedManager, Client, EventListener { SyndFeed f = getSyndFeed(getFeedInputStream(feed.getUrl())); title = StringUtils.isNullOrEmpty(f.getTitle()) ? null : f.getTitle(); - if (title != null) title = stripHTML(title); + if (title != null) title = clean(title, stripAll); description = StringUtils.isNullOrEmpty(f.getDescription()) ? null : f.getDescription(); - if (description != null) description = stripHTML(description); + if (description != null) description = clean(description, stripAll); author = StringUtils.isNullOrEmpty(f.getAuthor()) ? null : f.getAuthor(); - if (author != null) author = stripHTML(author); + if (author != null) author = clean(author, stripAll); if (f.getEntries().size() == 0) throw new FeedException("Feed has no entries"); @@ -418,23 +422,23 @@ class FeedManagerImpl implements FeedManager, Client, EventListener { // build post body StringBuilder b = new StringBuilder(); if (feed.getTitle() != null) { - // HTML in feed title was already stripped - b.append(feed.getTitle()).append("\n\n"); + b.append("<h3>").append(feed.getTitle()).append("</h3>"); } if (!StringUtils.isNullOrEmpty(entry.getTitle())) { - b.append(stripHTML(entry.getTitle())).append("\n\n"); + b.append("<h1>").append(entry.getTitle()).append("</h1>"); } for (SyndContent content : entry.getContents()) { - // extract content and do a very simple HTML tag stripping if (content.getValue() != null) - b.append(stripHTML(content.getValue())); + b.append(content.getValue()); } if (entry.getContents().size() == 0) { - if (entry.getDescription().getValue() != null) - b.append(stripHTML(entry.getDescription().getValue())); + if (entry.getDescription() != null && + entry.getDescription().getValue() != null) + b.append(entry.getDescription().getValue()); } + b.append("<p>"); if (!StringUtils.isNullOrEmpty(entry.getAuthor())) { - b.append("\n\n-- ").append(stripHTML(entry.getAuthor())); + b.append("-- ").append(entry.getAuthor()); } if (entry.getPublishedDate() != null) { b.append(" (").append(entry.getPublishedDate().toString()) @@ -443,8 +447,11 @@ class FeedManagerImpl implements FeedManager, Client, EventListener { b.append(" (").append(entry.getUpdatedDate().toString()) .append(")"); } - if (!StringUtils.isNullOrEmpty(entry.getLink())) { - b.append("\n\n").append(stripHTML(entry.getLink())); + b.append("</p>"); + String link = entry.getLink(); + if (!StringUtils.isNullOrEmpty(link)) { + b.append("<a href=\"").append(link).append("\">").append(link) + .append("</a>"); } // get other information for post @@ -476,14 +483,12 @@ class FeedManagerImpl implements FeedManager, Client, EventListener { } } - private String stripHTML(String s) { - s = s.replaceAll("<script.*?>(?s).*?</script>", ""); - return StringUtils.trim(s.replaceAll("<(?s).*?>", "")); - } - private String getPostBody(String text) { - if (text.length() <= MAX_BLOG_POST_BODY_LENGTH) return text; - else return text.substring(0, MAX_BLOG_POST_BODY_LENGTH); + text = clean(text, article); + byte[] textBytes = StringUtils.toUtf8(text); + if (textBytes.length <= MAX_BLOG_POST_BODY_LENGTH) + return text; + return StringUtils.fromUtf8(textBytes, 0, MAX_BLOG_POST_BODY_LENGTH); } /** diff --git a/briar-core/src/org/briarproject/util/HtmlUtils.java b/briar-core/src/org/briarproject/util/HtmlUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..e9c338401bac5e176ae1432a803da934b6b61630 --- /dev/null +++ b/briar-core/src/org/briarproject/util/HtmlUtils.java @@ -0,0 +1,16 @@ +package org.briarproject.util; + +import org.jsoup.Jsoup; +import org.jsoup.safety.Whitelist; + +public class HtmlUtils { + + public static Whitelist stripAll = Whitelist.none(); + public static Whitelist article = + Whitelist.basic().addTags("h1", "h2", "h3", "h4", "h5", "h6"); + + public static String clean(String s, Whitelist list) { + return Jsoup.clean(s, list); + } + +} diff --git a/briar-core/src/org/briarproject/util/PrivacyUtils.java b/briar-core/src/org/briarproject/util/PrivacyUtils.java index 247ccda3f24a69d32d55f3d465a0427ab7d74b6e..c2bf426287d6afb4d0ded848796e018edcb67f85 100644 --- a/briar-core/src/org/briarproject/util/PrivacyUtils.java +++ b/briar-core/src/org/briarproject/util/PrivacyUtils.java @@ -1,6 +1,5 @@ package org.briarproject.util; -import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress;