Merge pull request #6825 from thundernest/file_uri_link

Don't attempt to open `file:` URIs in an email
This commit is contained in:
cketti 2023-04-18 13:21:57 +02:00 committed by GitHub
commit 3119985ffb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 169 additions and 153 deletions

View file

@ -50,6 +50,9 @@ class DependencyInjectionTest : AutoCloseKoinTest() {
withParameter<FolderIconProvider> {
ContextThemeWrapper(RuntimeEnvironment.getApplication(), R.style.Theme_K9_DayNight).theme
}
withParameters(clazz = Class.forName("com.fsck.k9.view.K9WebViewClient").kotlin) {
parametersOf(null, null)
}
}
}
}

View file

@ -144,12 +144,14 @@ class MessageContainerView(context: Context, attrs: AttributeSet?) :
menu.setHeaderTitle(linkUrl)
menu.add(
Menu.NONE,
MENU_ITEM_LINK_VIEW,
0,
context.getString(R.string.webview_contextmenu_link_view_action),
).setOnMenuItemClickListener(listener)
if (!linkUrl.startsWith("file:")) {
menu.add(
Menu.NONE,
MENU_ITEM_LINK_VIEW,
0,
context.getString(R.string.webview_contextmenu_link_view_action),
).setOnMenuItemClickListener(listener)
}
menu.add(
Menu.NONE,

View file

@ -1,142 +0,0 @@
package com.fsck.k9.view;
import java.io.InputStream;
import java.util.Collections;
import java.util.Map;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Browser;
import android.text.TextUtils;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.fsck.k9.mailstore.AttachmentResolver;
import com.fsck.k9.ui.R;
import com.fsck.k9.view.MessageWebView.OnPageFinishedListener;
import timber.log.Timber;
/**
* {@link WebViewClient} that intercepts requests for {@code cid:} URIs to load the respective body part.
*/
public class K9WebViewClient extends WebViewClient {
private static final String CID_SCHEME = "cid";
private static final WebResourceResponse RESULT_DO_NOT_INTERCEPT = null;
private static final WebResourceResponse RESULT_DUMMY_RESPONSE = new WebResourceResponse(null, null, null);
private OnPageFinishedListener onPageFinishedListener;
@Nullable
private final AttachmentResolver attachmentResolver;
public static K9WebViewClient newInstance(@Nullable AttachmentResolver attachmentResolver) {
return new K9WebViewClient(attachmentResolver);
}
private K9WebViewClient(@Nullable AttachmentResolver attachmentResolver) {
this.attachmentResolver = attachmentResolver;
}
@Override
public boolean shouldOverrideUrlLoading(WebView webView, String url) {
return shouldOverrideUrlLoading(webView, Uri.parse(url));
}
@Override
@RequiresApi(Build.VERSION_CODES.N)
public boolean shouldOverrideUrlLoading(WebView webView, WebResourceRequest request) {
return shouldOverrideUrlLoading(webView, request.getUrl());
}
private boolean shouldOverrideUrlLoading(WebView webView, Uri uri) {
if (CID_SCHEME.equals(uri.getScheme())) {
return false;
}
Context context = webView.getContext();
Intent intent = createBrowserViewIntent(uri, context);
try {
context.startActivity(intent);
} catch (ActivityNotFoundException ex) {
Toast.makeText(context, R.string.error_activity_not_found, Toast.LENGTH_LONG).show();
}
return true;
}
private Intent createBrowserViewIntent(Uri uri, Context context) {
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
intent.putExtra(Browser.EXTRA_CREATE_NEW_TAB, true);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
return intent;
}
public WebResourceResponse shouldInterceptRequest(WebView webView, WebResourceRequest request) {
Uri uri = request.getUrl();
if (!CID_SCHEME.equals(uri.getScheme())) {
return RESULT_DO_NOT_INTERCEPT;
}
if (attachmentResolver == null) {
return RESULT_DUMMY_RESPONSE;
}
String cid = uri.getSchemeSpecificPart();
if (TextUtils.isEmpty(cid)) {
return RESULT_DUMMY_RESPONSE;
}
Uri attachmentUri = attachmentResolver.getAttachmentUriForContentId(cid);
if (attachmentUri == null) {
return RESULT_DUMMY_RESPONSE;
}
Context context = webView.getContext();
ContentResolver contentResolver = context.getContentResolver();
try {
String mimeType = contentResolver.getType(attachmentUri);
InputStream inputStream = contentResolver.openInputStream(attachmentUri);
WebResourceResponse webResourceResponse = new WebResourceResponse(mimeType, null, inputStream);
addCacheControlHeader(webResourceResponse);
return webResourceResponse;
} catch (Exception e) {
Timber.e(e, "Error while intercepting URI: %s", uri);
return RESULT_DUMMY_RESPONSE;
}
}
private void addCacheControlHeader(WebResourceResponse response) {
Map<String, String> headers = Collections.singletonMap("Cache-Control", "no-store");
response.setResponseHeaders(headers);
}
public void setOnPageFinishedListener(OnPageFinishedListener onPageFinishedListener) {
this.onPageFinishedListener = onPageFinishedListener;
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if (onPageFinishedListener != null) {
onPageFinishedListener.onPageFinished();
}
}
}

View file

@ -0,0 +1,130 @@
package com.fsck.k9.view
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Browser
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.annotation.RequiresApi
import com.fsck.k9.helper.ClipboardManager
import com.fsck.k9.logging.Timber
import com.fsck.k9.mailstore.AttachmentResolver
import com.fsck.k9.ui.R
import com.fsck.k9.view.MessageWebView.OnPageFinishedListener
/**
* [WebViewClient] that intercepts requests for `cid:` URIs to load the respective body part.
*/
internal class K9WebViewClient(
private val clipboardManager: ClipboardManager,
private val attachmentResolver: AttachmentResolver?,
private val onPageFinishedListener: OnPageFinishedListener?,
) : WebViewClient() {
@Deprecated("Deprecated in parent class")
override fun shouldOverrideUrlLoading(webView: WebView, url: String): Boolean {
return shouldOverrideUrlLoading(webView, Uri.parse(url))
}
@RequiresApi(Build.VERSION_CODES.N)
override fun shouldOverrideUrlLoading(webView: WebView, request: WebResourceRequest): Boolean {
return shouldOverrideUrlLoading(webView, request.url)
}
private fun shouldOverrideUrlLoading(webView: WebView, uri: Uri): Boolean {
return when (uri.scheme) {
CID_SCHEME -> {
false
}
FILE_SCHEME -> {
copyUrlToClipboard(webView.context, uri)
true
}
else -> {
openUrl(webView.context, uri)
true
}
}
}
private fun copyUrlToClipboard(context: Context, uri: Uri) {
val label = context.getString(R.string.webview_contextmenu_link_clipboard_label)
clipboardManager.setText(label, uri.toString())
}
private fun openUrl(context: Context, uri: Uri) {
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
putExtra(Browser.EXTRA_APPLICATION_ID, context.packageName)
putExtra(Browser.EXTRA_CREATE_NEW_TAB, true)
addCategory(Intent.CATEGORY_BROWSABLE)
addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
}
try {
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Timber.d(e, "Couldn't open URL: %s", uri)
Toast.makeText(context, R.string.error_activity_not_found, Toast.LENGTH_LONG).show()
}
}
override fun shouldInterceptRequest(webView: WebView, request: WebResourceRequest): WebResourceResponse? {
val uri = request.url
return if (uri.scheme == CID_SCHEME) {
handleCidUri(uri, webView)
} else {
RESULT_DO_NOT_INTERCEPT
}
}
private fun handleCidUri(uri: Uri, webView: WebView): WebResourceResponse {
val attachmentUri = getAttachmentUriFromCidUri(uri) ?: return RESULT_DUMMY_RESPONSE
val context = webView.context
val contentResolver = context.contentResolver
@Suppress("TooGenericExceptionCaught")
return try {
val mimeType = contentResolver.getType(attachmentUri)
val inputStream = contentResolver.openInputStream(attachmentUri)
WebResourceResponse(mimeType, null, inputStream).apply {
addCacheControlHeader()
}
} catch (e: Exception) {
Timber.e(e, "Error while intercepting URI: %s", uri)
RESULT_DUMMY_RESPONSE
}
}
private fun getAttachmentUriFromCidUri(uri: Uri): Uri? {
return uri.schemeSpecificPart
?.let { cid -> attachmentResolver?.getAttachmentUriForContentId(cid) }
}
private fun WebResourceResponse.addCacheControlHeader() {
responseHeaders = mapOf("Cache-Control" to "no-store")
}
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
onPageFinishedListener?.onPageFinished()
}
companion object {
private const val CID_SCHEME = "cid"
private const val FILE_SCHEME = "file"
private val RESULT_DO_NOT_INTERCEPT: WebResourceResponse? = null
private val RESULT_DUMMY_RESPONSE = WebResourceResponse(null, null, null)
}
}

View file

@ -1,8 +1,10 @@
package com.fsck.k9.view
import com.fsck.k9.helper.ReplyToParser
import com.fsck.k9.mailstore.AttachmentResolver
import com.fsck.k9.message.ReplyActionStrategy
import com.fsck.k9.ui.helper.RelativeDateTimeFormatter
import com.fsck.k9.view.MessageWebView.OnPageFinishedListener
import org.koin.dsl.module
val viewModule = module {
@ -10,4 +12,8 @@ val viewModule = module {
factory { RelativeDateTimeFormatter(context = get(), clock = get()) }
factory { ReplyToParser() }
factory { ReplyActionStrategy(replyRoParser = get()) }
factory { (attachmentResolver: AttachmentResolver?, onPageFinishedListener: OnPageFinishedListener?) ->
K9WebViewClient(clipboardManager = get(), attachmentResolver, onPageFinishedListener)
}
factory { WebViewClientFactory() }
}

View file

@ -7,13 +7,17 @@ import android.webkit.WebSettings.LayoutAlgorithm
import android.webkit.WebSettings.RenderPriority
import android.webkit.WebView
import com.fsck.k9.mailstore.AttachmentResolver
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import timber.log.Timber
class MessageWebView : WebView {
class MessageWebView : WebView, KoinComponent {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle)
private val webViewClientFactory: WebViewClientFactory by inject()
fun blockNetworkData(shouldBlockNetworkData: Boolean) {
// Images with content: URIs will not be blocked, nor will network images that are already in the WebView cache.
try {
@ -81,10 +85,7 @@ class MessageWebView : WebView {
attachmentResolver: AttachmentResolver?,
onPageFinishedListener: OnPageFinishedListener?,
) {
val webViewClient = K9WebViewClient.newInstance(attachmentResolver)
if (onPageFinishedListener != null) {
webViewClient.setOnPageFinishedListener(onPageFinishedListener)
}
val webViewClient = webViewClientFactory.create(attachmentResolver, onPageFinishedListener)
setWebViewClient(webViewClient)
}

View file

@ -0,0 +1,16 @@
package com.fsck.k9.view
import android.webkit.WebViewClient
import com.fsck.k9.mailstore.AttachmentResolver
import com.fsck.k9.view.MessageWebView.OnPageFinishedListener
import org.koin.core.parameter.parametersOf
import org.koin.java.KoinJavaComponent.getKoin
internal class WebViewClientFactory {
fun create(
attachmentResolver: AttachmentResolver?,
onPageFinishedListener: OnPageFinishedListener?,
): WebViewClient {
return getKoin().get(K9WebViewClient::class) { parametersOf(attachmentResolver, onPageFinishedListener) }
}
}