251 lines
9.2 KiB
Kotlin
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)))
|
|
}
|
|
}
|