proton-mail-android/app/src/main/java/ch/protonmail/android/views/PmWebViewClient.kt

251 lines
9.2 KiB
Kotlin

/*
* Copyright (c) 2022 Proton AG
*
* This file is part of Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.views
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.MailTo
import android.net.Uri
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.core.text.HtmlCompat
import ch.protonmail.android.R
import ch.protonmail.android.activities.composeMessage.ComposeMessageActivity
import ch.protonmail.android.core.Constants
import ch.protonmail.android.core.Constants.DUMMY_URL_PREFIX
import ch.protonmail.android.core.UserManager
import ch.protonmail.android.settings.data.AccountSettingsRepository
import ch.protonmail.android.utils.MessageUtils.addRecipientsToIntent
import ch.protonmail.android.utils.ui.dialogs.DialogUtils.Companion.showInfoDialogWithTwoButtonsAndCheckbox
import ch.protonmail.android.utils.ui.dialogs.DialogUtils.Companion.showTwoButtonInfoDialog
import kotlinx.coroutines.runBlocking
import me.proton.core.presentation.utils.showToast
import me.proton.core.util.kotlin.startsWith
import java.io.ByteArrayInputStream
import java.net.MalformedURLException
import java.net.URL
import java.util.Locale
open class PmWebViewClient(
private val userManager: UserManager,
private val accountSettingsRepository: AccountSettingsRepository,
private val activity: Activity,
private var shouldLoadRemoteContent: Boolean
) : WebViewClient() {
private var blockedImages = 0
private var isPhishingMessage = false
private val hyperlinkConfirmationWhitelistedHosts = listOf(
"protonmail.com",
"protonmail.ch",
"protonvpn.com",
"protonstatus.com",
"gdpr.eu",
"protonvpn.net",
"pm.me",
"mail.protonmail.com",
"account.protonvpn.com",
"protonirockerxow.onion"
)
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
val fixedUrl = url.replaceFirst(DUMMY_URL_PREFIX.toRegex(), "")
if (fixedUrl startsWith "mailto:") {
composeMessageWithMailToData(fixedUrl, view.context.applicationContext)
return true
}
if (fixedUrl startsWith "tel:") {
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse(fixedUrl)
if (intent.resolveActivity(activity.packageManager) != null) {
activity.startActivity(intent)
} else {
activity.showToast(R.string.no_application_found)
}
} else {
if (fixedUrl.isNotBlank()) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(fixedUrl)
if (intent.resolveActivity(activity.packageManager) != null) {
if (showHyperlinkConfirmation(fixedUrl).not()) {
activity.startActivity(intent)
}
} else {
activity.showToast(R.string.no_application_found_or_link_invalid)
}
}
}
return true
}
fun setPhishingCheck(isPhishingMessage: Boolean) {
this.isPhishingMessage = isPhishingMessage
}
private fun composeMessageWithMailToData(url: String, context: Context) {
val intent = Intent(context, ComposeMessageActivity::class.java)
val mailTo = MailTo.parse(url)
val user = userManager.currentUser
?: return
addRecipientsToIntent(
intent = intent,
extraName = ComposeMessageActivity.EXTRA_TO_RECIPIENTS,
recipientList = mailTo.to,
messageAction = Constants.MessageActionType.FROM_URL,
userAddresses = user.addresses
)
addRecipientsToIntent(
intent = intent,
extraName = ComposeMessageActivity.EXTRA_CC_RECIPIENTS,
recipientList = mailTo.cc,
messageAction = Constants.MessageActionType.FROM_URL,
userAddresses = user.addresses
)
intent.putExtra(ComposeMessageActivity.EXTRA_MAIL_TO, true)
.putExtra(ComposeMessageActivity.EXTRA_MESSAGE_TITLE, mailTo.subject)
.putExtra(ComposeMessageActivity.EXTRA_MESSAGE_BODY, mailTo.body)
activity.startActivity(intent)
}
/**
* @return true if confirmation was shown
*/
private fun showHyperlinkConfirmation(url: String): Boolean {
try {
val parsedUrl = URL(url)
// only handle http/s protocols
val protocol = parsedUrl.protocol.lowercase(Locale.getDefault())
if (protocol != "http" && protocol != "https") return false
// whitelist domains
for (host in hyperlinkConfirmationWhitelistedHosts) {
if (parsedUrl.host.endsWith(host)) return false
}
} catch (e: MalformedURLException) {
e.printStackTrace()
}
val doesRequireHyperlinkConfirmation = runBlocking {
accountSettingsRepository
.getShouldShowLinkConfirmationSetting(userManager.requireCurrentUserId())
}
return when {
isPhishingMessage -> {
showPhishingHyperlinkConfirmation(url)
true
}
doesRequireHyperlinkConfirmation -> {
showRegularHyperlinkConfirmation(url)
true
}
else -> {
false
}
}
}
private fun showRegularHyperlinkConfirmation(url: String) {
val message = HtmlCompat.fromHtml(
activity.getString(R.string.hyperlink_confirmation_dialog_text_html, ellipsesUrlIfTooLong(url)),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
showInfoDialogWithTwoButtonsAndCheckbox(
context = activity,
title = "",
message = message,
negativeBtnText = activity.getString(R.string.cancel),
positiveBtnText = activity.getString(
R.string.cont
),
checkBoxText = activity.getString(R.string.dont_ask_again),
okListener = {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
activity.startActivity(intent)
},
checkedListener = { isChecked ->
runBlocking {
accountSettingsRepository.saveShouldShowLinkConfirmationSetting(
shouldShowHyperlinkConfirmation = isChecked.not(),
userId = userManager.requireCurrentUserId()
)
}
},
cancelable = true
)
}
private fun showPhishingHyperlinkConfirmation(url: String) {
val message = HtmlCompat.fromHtml(
activity.getString(R.string.details_hyperlink_phishing_dialog_content, ellipsesUrlIfTooLong(url)),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
activity.showTwoButtonInfoDialog(
titleStringId = R.string.details_hyperlink_phishing_dialog_title,
message = message,
positiveStringId = R.string.details_hyperlink_phishing_dialog_confirm_action,
negativeStringId = R.string.details_hyperlink_phishing_dialog_cancel_action
) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
activity.startActivity(intent)
}
}
private fun ellipsesUrlIfTooLong(url: String) =
if (url.length > 100) {
url.substring(0, 60) + "..." + url.substring(url.length - 40)
} else {
url
}
protected fun amountOfRemoteResourcesBlocked(): Int =
blockedImages
fun blockRemoteResources(block: Boolean) {
blockedImages = 0
shouldLoadRemoteContent = !block
}
fun allowLoadingRemoteResources() {
blockRemoteResources(false)
}
@Deprecated("Deprecated in Java")
override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
if (shouldLoadRemoteContent) {
return super.shouldInterceptRequest(view, url)
}
val uri = Uri.parse(url)
if (uri.scheme.equals("cid", ignoreCase = true) || uri.scheme.equals("data", ignoreCase = true)) {
return super.shouldInterceptRequest(view, url)
}
if (url.lowercase(Locale.getDefault()).contains("/favicon.ico")) {
return super.shouldInterceptRequest(view, url)
}
blockedImages++
return WebResourceResponse("text/plain", "utf-8", ByteArrayInputStream(ByteArray(0)))
}
}