diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 8e374db5e616..3a04cb617242 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -583,6 +583,9 @@ class BrowserTabFragment : private val errorView get() = binding.includeErrorView + private val warningView + get() = binding.maliciousSiteWarningLayout + private val sslErrorView get() = binding.sslErrorWarningLayout @@ -1371,6 +1374,26 @@ class BrowserTabFragment : errorView.errorLayout.show() } + private fun showWarning(url: Uri) { + webViewContainer.gone() + newBrowserTab.newTabLayout.gone() + newBrowserTab.newTabContainerLayout.gone() + sslErrorView.gone() + // TODO (cbarreiro) we should probably define a new view mode + omnibar.setViewMode(Omnibar.ViewMode.Error) + webView?.onPause() + webView?.hide() + warningView.bind(/*handler, errorResponse*/) /*{ action -> + viewModel.onSSLCertificateWarningAction(action, errorResponse.url) + }*/ + warningView.show() + // warningView.leaveSiteButton.setOnClickListener { + // viewModel.closeCurrentTab() + // // TODO (cbarreiro) Fix, not working + // renderer.showNewTab() + // } + } + private fun showSSLWarning( handler: SslErrorHandler, errorResponse: SslErrorResponse, @@ -1711,6 +1734,7 @@ class BrowserTabFragment : ) is Command.WebViewError -> showError(it.errorType, it.url) + is Command.WebViewWarningMaliciousSite -> showWarning(it.url) is Command.SendResponseToJs -> contentScopeScripts.onResponse(it.data) is Command.SendResponseToDuckPlayer -> duckPlayerScripts.onResponse(it.data) is Command.WebShareRequest -> webShareRequest.launch(it.data) @@ -4006,7 +4030,7 @@ class BrowserTabFragment : viewModel.onCtaShown() } - private fun showNewTab() { + fun showNewTab() { newTabPageProvider.provideNewTabPageVersion().onEach { newTabPage -> if (newBrowserTab.newTabContainerLayout.childCount == 0) { newBrowserTab.newTabContainerLayout.addView( diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 4a0e7b4d7797..6bae7c822ca6 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -156,6 +156,7 @@ import com.duckduckgo.app.browser.commands.Command.ShowWebPageTitle import com.duckduckgo.app.browser.commands.Command.ToggleReportFeedback import com.duckduckgo.app.browser.commands.Command.WebShareRequest import com.duckduckgo.app.browser.commands.Command.WebViewError +import com.duckduckgo.app.browser.commands.Command.WebViewWarningMaliciousSite import com.duckduckgo.app.browser.commands.NavigationCommand import com.duckduckgo.app.browser.customtabs.CustomTabPixelNames import com.duckduckgo.app.browser.duckplayer.DUCK_PLAYER_FEATURE_NAME @@ -3187,6 +3188,11 @@ class BrowserTabViewModel @Inject constructor( command.postValue(WebViewError(errorType, url)) } + override fun onReceivedMaliciousSiteWarning(url: Uri) { + // TODO (cbarreiro): Fire pixel + command.postValue(WebViewWarningMaliciousSite(url)) + } + override fun recordErrorCode( error: String, url: String, diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt index f65ede0c371d..fa592020db65 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -79,7 +79,6 @@ import com.duckduckgo.history.api.NavigationHistory import com.duckduckgo.privacy.config.api.AmpLinks import com.duckduckgo.subscriptions.api.Subscriptions import com.duckduckgo.user.agent.api.ClientBrandHintProvider -import com.google.android.material.snackbar.Snackbar import java.net.URI import javax.inject.Inject import kotlinx.coroutines.* @@ -159,7 +158,7 @@ class BrowserWebViewClient @Inject constructor( try { Timber.v("shouldOverride webViewUrl: ${webView.url} URL: $url") webViewClientListener?.onShouldOverride() - if (requestInterceptor.shouldOverrideUrlLoading(webView, url, isForMainFrame)) { + if (requestInterceptor.shouldOverrideUrlLoading(webViewClientListener, url, isForMainFrame)) { return true } diff --git a/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt b/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt index f8fb1313a391..b9d5eeb258fe 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt @@ -95,6 +95,7 @@ interface WebViewClientListener { fun linkOpenedInNewTab(): Boolean fun isActiveTab(): Boolean fun onReceivedError(errorType: WebViewErrorResponse, url: String) + fun onReceivedMaliciousSiteWarning(url: Uri) fun recordErrorCode(error: String, url: String) fun recordHttpErrorCode(statusCode: Int, url: String) diff --git a/app/src/main/java/com/duckduckgo/app/browser/WebViewRequestInterceptor.kt b/app/src/main/java/com/duckduckgo/app/browser/WebViewRequestInterceptor.kt index f93ea7ec3947..0b630cb15bca 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/WebViewRequestInterceptor.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/WebViewRequestInterceptor.kt @@ -39,7 +39,6 @@ import com.duckduckgo.httpsupgrade.api.HttpsUpgrader import com.duckduckgo.privacy.config.api.Gpc import com.duckduckgo.request.filterer.api.RequestFilterer import com.duckduckgo.user.agent.api.UserAgentProvider -import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.withContext import timber.log.Timber @@ -63,7 +62,7 @@ interface RequestInterceptor { @WorkerThread fun shouldOverrideUrlLoading( - webView: WebView, + webViewClientListener: WebViewClientListener?, url: Uri, isForMainFrame: Boolean, ): Boolean @@ -108,9 +107,9 @@ class WebViewRequestInterceptor( val url: Uri? = request.url maliciousSiteBlockerWebViewIntegration.shouldIntercept(request, documentUri) { - handleSiteBlocked(webView) + handleSiteBlocked(webViewClientListener, url) }?.let { - handleSiteBlocked(webView) + handleSiteBlocked(webViewClientListener, url) return it } @@ -179,22 +178,22 @@ class WebViewRequestInterceptor( return getWebResourceResponse(request, documentUrl, null) } - override fun shouldOverrideUrlLoading(webView: WebView, url: Uri, isForMainFrame: Boolean): Boolean { + override fun shouldOverrideUrlLoading(webViewClientListener: WebViewClientListener?, url: Uri, isForMainFrame: Boolean): Boolean { if (maliciousSiteBlockerWebViewIntegration.shouldOverrideUrlLoading( url, isForMainFrame, ) { - handleSiteBlocked(webView) + handleSiteBlocked(webViewClientListener, url) } ) { - handleSiteBlocked(webView) + handleSiteBlocked(webViewClientListener, url) return true } return false } - private fun handleSiteBlocked(webView: WebView) { - Snackbar.make(webView, "Site blocked", Snackbar.LENGTH_SHORT).show() + private fun handleSiteBlocked(webViewClientListener: WebViewClientListener?, url: Uri?) { + url?.let { webViewClientListener?.onReceivedMaliciousSiteWarning(it) } } private fun getWebResourceResponse( diff --git a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt index 911f4d3e2b92..8638efba276a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt @@ -217,6 +217,10 @@ sealed class Command { val url: String, ) : Command() + data class WebViewWarningMaliciousSite( + val url: Uri, + ) : Command() + // TODO (cbarreiro) Rename to SendResponseToCSS data class SendResponseToJs(val data: JsCallbackData) : Command() data class SendResponseToDuckPlayer(val data: JsCallbackData) : Command() diff --git a/app/src/main/java/com/duckduckgo/app/browser/webview/MaliciousSiteBlockedWarningLayout.kt b/app/src/main/java/com/duckduckgo/app/browser/webview/MaliciousSiteBlockedWarningLayout.kt new file mode 100644 index 000000000000..0422ae1ef50d --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/webview/MaliciousSiteBlockedWarningLayout.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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 com.duckduckgo.app.browser.webview + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import com.duckduckgo.app.browser.databinding.ViewMaliciousSiteBlockedWarningBinding +import com.duckduckgo.common.ui.view.gone +import com.duckduckgo.common.ui.view.show +import com.duckduckgo.common.ui.viewbinding.viewBinding + +class MaliciousSiteBlockedWarningLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, +) : FrameLayout(context, attrs, defStyle) { + + sealed class Action { + data object VisitSite : Action() + data object LeaveSite : Action() + } + + private val binding: ViewMaliciousSiteBlockedWarningBinding by viewBinding() + + fun bind() { + resetViewState() + + with(binding) { + setListeners() + } + } + + private fun resetViewState() { + with(binding) { + advancedCTA.show() + advancedGroup.gone() + } + } + + private fun setListeners() { + with(binding) { + advancedCTA.setOnClickListener { + advancedCTA.gone() + advancedGroup.show() + errorLayout.post { + errorLayout.fullScroll(View.FOCUS_DOWN) + } + } + } + } +} diff --git a/app/src/main/res/drawable/malware_site_128.xml b/app/src/main/res/drawable/malware_site_128.xml new file mode 100644 index 000000000000..cb82a5e50e53 --- /dev/null +++ b/app/src/main/res/drawable/malware_site_128.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_browser_tab.xml b/app/src/main/res/layout/fragment_browser_tab.xml index 6a1ec794923c..7d0d6447093e 100644 --- a/app/src/main/res/layout/fragment_browser_tab.xml +++ b/app/src/main/res/layout/fragment_browser_tab.xml @@ -71,6 +71,12 @@ android:layout_height="match_parent" android:visibility="gone" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_malicious_site_blocked_warning.xml b/app/src/main/res/layout/view_malicious_site_blocked_warning.xml new file mode 100644 index 000000000000..d0b7741f8deb --- /dev/null +++ b/app/src/main/res/layout/view_malicious_site_blocked_warning.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5016e54ca41..3cfc402b4a51 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -720,6 +720,12 @@ The security certificate for %1$s does not match *.%2$s. It’s possible that the website is misconfigured or that an attacker has compromised your connection, or that your system clock is incorrect. The security certificate for %1$s is not trusted by your device\'s operating system. It’s possible that the website is misconfigured or that an attacker has compromised your connection, or that your system clock is incorrect. + + Warning: This site may put your personal information at risk + DuckDuckGo blocked this page because it may be distributing malware designed to compromise your device or steal your personal information. Learn more + Leave This Site + Advanced + If you believe this website is safe, you can report an error. You can still visit the website at your own risk.\n\nAccept Risk and Visit Site
Ready for a better, more private internet?]]>
Let\'s do it! diff --git a/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/MaliciousSiteRepository.kt b/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/MaliciousSiteRepository.kt index 8e0468fde422..cd20e693285a 100644 --- a/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/MaliciousSiteRepository.kt +++ b/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/MaliciousSiteRepository.kt @@ -97,7 +97,7 @@ class RealMaliciousSiteRepository @Inject constructor( override suspend fun matches(hashPrefix: String): List { return try { maliciousSiteService.getMatches(hashPrefix).matches.also { - Timber.d("\uD83D\uDFE2 Cris: Fetched $it matches for hash prefix $hashPrefix") + Timber.d("\uD83D\uDFE2 Cris: hash prefix $hashPrefix. Fetched $it matches") } } catch (e: Exception) { Timber.d("\uD83D\uDD34 Cris: Failed to fetch matches for hash prefix $hashPrefix") diff --git a/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/network/MaliciousSiteService.kt b/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/network/MaliciousSiteService.kt index 6f3fd6787c5d..9fcd77271cf1 100644 --- a/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/network/MaliciousSiteService.kt +++ b/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/network/MaliciousSiteService.kt @@ -57,12 +57,12 @@ data class HashPrefixResponse( val insert: Set, val delete: Set, val revision: Int, - val replace: Boolean, + val replace: String, ) data class FilterSetResponse( val insert: Set, val delete: Set, val revision: Int, - val replace: Boolean, + val replace: String, ) diff --git a/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/domain/RealMaliciousSiteProtection.kt b/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/domain/RealMaliciousSiteProtection.kt index 87384dc0ffca..72a9851ed888 100644 --- a/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/domain/RealMaliciousSiteProtection.kt +++ b/malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/domain/RealMaliciousSiteProtection.kt @@ -68,7 +68,13 @@ class RealMaliciousSiteProtection @Inject constructor( } } appCoroutineScope.launch(dispatchers.io()) { - confirmationCallback(matches(hashPrefix, url, hostname, hash)) + try { + val matches = matches(hashPrefix, url, hostname, hash) + confirmationCallback(matches) + } catch (e: Exception) { + Timber.e(e, "\uD83D\uDD34 Cris: shouldBlock $url") + confirmationCallback(false) + } } return IsMaliciousResult.WAIT_FOR_CONFIRMATION }