Skip to content

Commit

Permalink
Add feed to isMalicious return type and update error page with feed i…
Browse files Browse the repository at this point in the history
…nfo (#5564)

Task/Issue URL:
https://app.asana.com/0/1205008441501016/1209270355529418/f

### Description
Add feed to isMalicious return type and update error page with feed info

### Steps to test this PR

_Feature 1_
- [x] Load
https://privacy-test-pages.site/security/badware/phishing.html
- [x] Check the error message is "This website may be impersonating a
legitimate site in order to trick you into providing personal
information, such as passwords or credit card numbers."

_Feature 2_
- [x] Load https://privacy-test-pages.site/security/badware/malware.html
- [x] Check the error message is "DuckDuckGo blocked this page because
it may be distributing malware designed to compromise your device or
steal your personal information."

### UI changes
| Before  | After |
| ------ | ----- |
!(Upload before screenshot)|(Upload after screenshot)|
  • Loading branch information
CrisBarreiro authored Feb 4, 2025
1 parent a779a24 commit 397271f
Show file tree
Hide file tree
Showing 27 changed files with 480 additions and 204 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ import com.duckduckgo.feature.toggles.api.Toggle.State
import com.duckduckgo.history.api.HistoryEntry.VisitedPage
import com.duckduckgo.history.api.NavigationHistory
import com.duckduckgo.js.messaging.api.JsCallbackData
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed.MALWARE
import com.duckduckgo.newtabpage.impl.pixels.NewTabPixels
import com.duckduckgo.privacy.config.api.AmpLinkInfo
import com.duckduckgo.privacy.config.api.AmpLinks
Expand Down Expand Up @@ -677,7 +678,6 @@ class BrowserTabViewModelTest {
toggleReports = mockToggleReports,
brokenSitePrompt = mockBrokenSitePrompt,
tabStatsBucketing = mockTabStatsBucketing,
maliciousSiteBlockerWebViewIntegration = mock(),
defaultBrowserPromptsExperiment = mockDefaultBrowserPromptsExperiment,
swipingTabsFeature = swipingTabsFeatureProvider,
)
Expand Down Expand Up @@ -5085,15 +5085,15 @@ class BrowserTabViewModelTest {
@Test
fun whenMaliciousSiteActionLeaveSiteAndCustomTabThenClose() {
val url = "http://example.com".toUri()
testee.onMaliciousSiteUserAction(LeaveSite, url, true)
testee.onMaliciousSiteUserAction(LeaveSite, url, MALWARE, true)
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
assertTrue(commandCaptor.allValues.any { it is Command.CloseCustomTab })
}

@Test
fun whenMaliciousSiteActionLeaveSiteAndCustomTabFalseThenHideSSLError() {
val url = "http://example.com".toUri()
testee.onMaliciousSiteUserAction(LeaveSite, url, false)
testee.onMaliciousSiteUserAction(LeaveSite, url, MALWARE, false)
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
assertTrue(commandCaptor.allValues.any { it is Command.EscapeMaliciousSite })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ import com.duckduckgo.app.browser.trafficquality.AndroidFeaturesHeaderPlugin
import com.duckduckgo.app.browser.trafficquality.CustomHeaderAllowedChecker
import com.duckduckgo.app.browser.trafficquality.remote.AndroidFeaturesHeaderProvider
import com.duckduckgo.app.browser.uriloaded.UriLoadedManager
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.global.model.Site
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
import com.duckduckgo.app.statistics.pixels.Pixel
Expand Down Expand Up @@ -164,7 +163,6 @@ class BrowserWebViewClientTest {
mockFeaturesHeaderProvider,
mock(),
)
private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = mock()
private val mockDuckChat: DuckChat = mock()

@UiThreadTest
Expand Down Expand Up @@ -211,7 +209,6 @@ class BrowserWebViewClientTest {
whenever(currentTimeProvider.elapsedRealtime()).thenReturn(0)
whenever(webViewVersionProvider.getMajorVersion()).thenReturn("1")
whenever(deviceInfo.appVersion).thenReturn("1")
whenever(mockMaliciousSiteProtection.shouldOverrideUrlLoading(any(), any(), any())).thenReturn(false)
}

@UiThreadTest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import androidx.test.annotation.UiThreadTest
import com.duckduckgo.adclick.api.AdClickManager
import com.duckduckgo.app.browser.useragent.provideUserAgentOverridePluginPoint
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.fakes.FakeMaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.fakes.FeatureToggleFake
import com.duckduckgo.app.fakes.UserAgentFake
import com.duckduckgo.app.fakes.UserAllowListRepositoryFake
Expand Down Expand Up @@ -98,7 +99,7 @@ class WebViewRequestInterceptorTest {
fakeToggle,
fakeUserAllowListRepository,
)
private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = mock()
private val mockMaliciousSiteBlockerWebViewIntegration: MaliciousSiteBlockerWebViewIntegration = FakeMaliciousSiteBlockerWebViewIntegration()

private var webView: WebView = mock()

Expand All @@ -119,7 +120,7 @@ class WebViewRequestInterceptorTest {
cloakedCnameDetector = mockCloakedCnameDetector,
requestFilterer = mockRequestFilterer,
duckPlayer = mockDuckPlayer,
maliciousSiteBlockerWebViewIntegration = mockMaliciousSiteProtection,
maliciousSiteBlockerWebViewIntegration = mockMaliciousSiteBlockerWebViewIntegration,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import com.duckduckgo.app.browser.*
import com.duckduckgo.app.browser.certificates.rootstore.TrustedCertificateStore
import com.duckduckgo.app.browser.cookies.ThirdPartyCookieManager
import com.duckduckgo.app.browser.httpauth.WebViewHttpAuthStore
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.cookies.api.CookieManagerProvider
import kotlinx.coroutines.test.TestScope
Expand All @@ -51,7 +50,6 @@ class UrlExtractingWebViewClientTest {
private val thirdPartyCookieManager: ThirdPartyCookieManager = mock()
private val urlExtractor: DOMUrlExtractor = mock()
private val mockWebView: WebView = mock()
private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = mock()

@UiThreadTest
@Before
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2025 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.fakes

import android.net.Uri
import android.webkit.WebResourceRequest
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.browser.webview.RealMaliciousSiteBlockerWebViewIntegration.IsMaliciousViewData
import com.duckduckgo.app.browser.webview.RealMaliciousSiteBlockerWebViewIntegration.IsMaliciousViewData.Safe
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.MaliciousStatus

class FakeMaliciousSiteBlockerWebViewIntegration : MaliciousSiteBlockerWebViewIntegration {
override suspend fun shouldIntercept(
request: WebResourceRequest,
documentUri: Uri?,
confirmationCallback: (maliciousStatus: MaliciousStatus) -> Unit,
): IsMaliciousViewData {
return Safe
}

override fun shouldOverrideUrlLoading(
url: Uri,
isForMainFrame: Boolean,
confirmationCallback: (maliciousStatus: MaliciousStatus) -> Unit,
): IsMaliciousViewData {
return Safe
}

override fun onPageLoadStarted() {
// no-op
}

override fun onSiteExempted(
url: Uri,
feed: Feed,
) {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.duckduckgo.adclick.api.AdClickManager
import com.duckduckgo.app.browser.WebViewRequestInterceptor
import com.duckduckgo.app.browser.useragent.provideUserAgentOverridePluginPoint
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.fakes.FakeMaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.fakes.FeatureToggleFake
import com.duckduckgo.app.fakes.UserAgentFake
import com.duckduckgo.app.fakes.UserAllowListRepositoryFake
Expand Down Expand Up @@ -120,7 +121,7 @@ class DomainsReferenceTest(private val testCase: TestCase) {
)
private val mockGpc: Gpc = mock()
private val mockAdClickManager: AdClickManager = mock()
private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = mock()
private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = FakeMaliciousSiteBlockerWebViewIntegration()

companion object {
private val moshi = Moshi.Builder().add(ActionJsonAdapter()).build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.duckduckgo.adclick.api.AdClickManager
import com.duckduckgo.app.browser.WebViewRequestInterceptor
import com.duckduckgo.app.browser.useragent.provideUserAgentOverridePluginPoint
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.fakes.FakeMaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.fakes.FeatureToggleFake
import com.duckduckgo.app.fakes.UserAgentFake
import com.duckduckgo.app.fakes.UserAllowListRepositoryFake
Expand Down Expand Up @@ -116,7 +117,7 @@ class SurrogatesReferenceTest(private val testCase: TestCase) {
private val mockGpc: Gpc = mock()
private val mockAdClickManager: AdClickManager = mock()
private val mockCloakedCnameDetector: CloakedCnameDetector = mock()
private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = mock()
private val mockMaliciousSiteProtection: MaliciousSiteBlockerWebViewIntegration = FakeMaliciousSiteBlockerWebViewIntegration()

companion object {
private val moshi = Moshi.Builder().add(ActionJsonAdapter()).build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ import com.duckduckgo.js.messaging.api.JsCallbackData
import com.duckduckgo.js.messaging.api.JsMessageCallback
import com.duckduckgo.js.messaging.api.JsMessaging
import com.duckduckgo.js.messaging.api.SubscriptionEventData
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
import com.duckduckgo.mobile.android.app.tracking.ui.AppTrackingProtectionScreens.AppTrackerOnboardingActivityWithEmptyParamsParams
import com.duckduckgo.navigation.api.GlobalActivityStarter
import com.duckduckgo.navigation.api.GlobalActivityStarter.DeeplinkActivityParams
Expand Down Expand Up @@ -1385,7 +1386,7 @@ class BrowserTabFragment :
errorView.errorLayout.show()
}

private fun showMaliciousWarning(url: Uri) {
private fun showMaliciousWarning(url: Uri, feed: Feed) {
webViewContainer.gone()
newBrowserTab.newTabLayout.gone()
newBrowserTab.newTabContainerLayout.gone()
Expand All @@ -1396,8 +1397,8 @@ class BrowserTabFragment :
webView?.onPause()
webView?.hide()
webView?.stopLoading()
maliciousWarningView.bind { action ->
viewModel.onMaliciousSiteUserAction(action, url, isActiveCustomTab())
maliciousWarningView.bind(feed) { action ->
viewModel.onMaliciousSiteUserAction(action, url, feed, isActiveCustomTab())
}
maliciousWarningView.show()
binding.focusDummy.requestFocus()
Expand Down Expand Up @@ -1427,8 +1428,9 @@ class BrowserTabFragment :
(activity as? CustomTabActivity)?.finishAndRemoveTask()
}

private fun onBypassMaliciousWarning(url: Uri) {
private fun onBypassMaliciousWarning(url: Uri, feed: Feed) {
showBrowser()
webViewClient.addExemptedMaliciousSite(url, feed)
webView?.loadUrl(url.toString())
}

Expand Down Expand Up @@ -1801,11 +1803,12 @@ class BrowserTabFragment :
)

is Command.WebViewError -> showError(it.errorType, it.url)
is Command.ShowWarningMaliciousSite -> showMaliciousWarning(it.url)
is Command.ShowWarningMaliciousSite -> showMaliciousWarning(it.url, it.feed)
is Command.HideWarningMaliciousSite -> hideMaliciousWarning()
is Command.EscapeMaliciousSite -> onEscapeMaliciousSite()
is Command.CloseCustomTab -> closeCustomTab()
is Command.BypassMaliciousSiteWarning -> onBypassMaliciousWarning(it.url)
is Command.BypassMaliciousSiteWarning -> onBypassMaliciousWarning(it.url, it.feed)
is Command.BypassMaliciousSiteWarning -> onBypassMaliciousWarning(it.url, it.feed)
is OpenBrokenSiteLearnMore -> openBrokenSiteLearnMore(it.url)
is ReportBrokenSiteError -> openBrokenSiteReportError(it.url)
is Command.SendResponseToJs -> contentScopeScripts.onResponse(it.data)
Expand Down
42 changes: 24 additions & 18 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,6 @@ import com.duckduckgo.app.browser.webview.MaliciousSiteBlockedWarningLayout.Acti
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockedWarningLayout.Action.LeaveSite
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockedWarningLayout.Action.ReportError
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockedWarningLayout.Action.VisitSite
import com.duckduckgo.app.browser.webview.MaliciousSiteBlockerWebViewIntegration
import com.duckduckgo.app.browser.webview.SslWarningLayout.Action
import com.duckduckgo.app.cta.ui.BrokenSitePromptDialogCta
import com.duckduckgo.app.cta.ui.Cta
Expand All @@ -231,6 +230,7 @@ import com.duckduckgo.app.fire.fireproofwebsite.ui.AutomaticFireproofSetting.ASK
import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchOptionHandler
import com.duckduckgo.app.global.events.db.UserEventKey
import com.duckduckgo.app.global.events.db.UserEventsStore
import com.duckduckgo.app.global.model.MaliciousSiteStatus
import com.duckduckgo.app.global.model.PrivacyShield
import com.duckduckgo.app.global.model.Site
import com.duckduckgo.app.global.model.SiteFactory
Expand Down Expand Up @@ -299,6 +299,9 @@ import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED
import com.duckduckgo.history.api.NavigationHistory
import com.duckduckgo.js.messaging.api.JsCallbackData
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed.MALWARE
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed.PHISHING
import com.duckduckgo.newtabpage.impl.pixels.NewTabPixels
import com.duckduckgo.privacy.config.api.AmpLinkInfo
import com.duckduckgo.privacy.config.api.AmpLinks
Expand Down Expand Up @@ -457,7 +460,6 @@ class BrowserTabViewModel @Inject constructor(
private val toggleReports: ToggleReports,
private val brokenSitePrompt: BrokenSitePrompt,
private val tabStatsBucketing: TabStatsBucketing,
private val maliciousSiteBlockerWebViewIntegration: MaliciousSiteBlockerWebViewIntegration,
private val defaultBrowserPromptsExperiment: DefaultBrowserPromptsExperiment,
private val swipingTabsFeature: SwipingTabsFeatureProvider,
) : WebViewClientListener,
Expand Down Expand Up @@ -1875,6 +1877,7 @@ class BrowserTabViewModel @Inject constructor(
fun onMaliciousSiteUserAction(
action: MaliciousSiteBlockedWarningLayout.Action,
url: Uri,
feed: Feed,
activeCustomTab: Boolean,
) {
when (action) {
Expand All @@ -1889,12 +1892,11 @@ class BrowserTabViewModel @Inject constructor(
}

VisitSite -> {
command.postValue(BypassMaliciousSiteWarning(url))
command.postValue(BypassMaliciousSiteWarning(url, feed))
browserViewState.value = currentBrowserViewState().copy(
browserShowing = true,
showPrivacyShield = HighlightableButton.Visible(enabled = true),
)
addExemptedMaliciousUrlToMemory(url)
}
LearnMore -> command.postValue(OpenBrokenSiteLearnMore(MALICIOUS_SITE_LEARN_MORE_URL))
ReportError -> command.postValue(ReportBrokenSiteError("$MALICIOUS_SITE_REPORT_ERROR_URL$url"))
Expand Down Expand Up @@ -3195,17 +3197,25 @@ class BrowserTabViewModel @Inject constructor(
command.postValue(WebViewError(errorType, url))
}

override fun onReceivedMaliciousSiteWarning(url: Uri) {
override fun onReceivedMaliciousSiteWarning(url: Uri, feed: Feed, exempted: Boolean) {
// TODO (cbarreiro): Fire pixel
loadingViewState.postValue(currentLoadingViewState().copy(isLoading = false, progress = 100, url = url.toString()))
browserViewState.postValue(
currentBrowserViewState().copy(
browserShowing = false,
showPrivacyShield = HighlightableButton.Visible(enabled = false),
maliciousSiteDetected = true,
),
)
command.postValue(ShowWarningMaliciousSite(url))
site?.maliciousSiteStatus = when (feed) {
MALWARE -> MaliciousSiteStatus.MALWARE
PHISHING -> MaliciousSiteStatus.PHISHING
}
if (!exempted) {
loadingViewState.postValue(
currentLoadingViewState().copy(isLoading = false, progress = 100, url = url.toString()),
)
browserViewState.postValue(
currentBrowserViewState().copy(
browserShowing = false,
showPrivacyShield = HighlightableButton.Visible(enabled = false),
maliciousSiteDetected = true,
),
)
command.postValue(ShowWarningMaliciousSite(url, feed))
}
}

override fun recordErrorCode(
Expand Down Expand Up @@ -3788,10 +3798,6 @@ class BrowserTabViewModel @Inject constructor(
command.value = SetOnboardingDialogBackground(getBackgroundResource(lightModeEnabled))
}

fun addExemptedMaliciousUrlToMemory(url: Uri) {
maliciousSiteBlockerWebViewIntegration.onSiteExempted(url)
}

private fun getBackgroundResource(lightModeEnabled: Boolean): Int =
if (lightModeEnabled) {
R.drawable.onboarding_background_bitmap_light
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED
import com.duckduckgo.duckplayer.api.DuckPlayer.OpenDuckPlayerInNewTab.On
import com.duckduckgo.duckplayer.impl.DUCK_PLAYER_OPEN_IN_YOUTUBE_PATH
import com.duckduckgo.history.api.NavigationHistory
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
import com.duckduckgo.privacy.config.api.AmpLinks
import com.duckduckgo.subscriptions.api.Subscriptions
import com.duckduckgo.user.agent.api.ClientBrandHintProvider
Expand Down Expand Up @@ -710,6 +711,10 @@ class BrowserWebViewClient @Inject constructor(
else -> "ERROR_OTHER"
}
}

fun addExemptedMaliciousSite(url: Uri, feed: Feed) {
requestInterceptor.addExemptedMaliciousSite(url, feed)
}
}

enum class WebViewPixelName(override val pixelName: String) : Pixel.PixelName {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.duckduckgo.app.browser.model.BasicAuthenticationRequest
import com.duckduckgo.app.global.model.Site
import com.duckduckgo.app.surrogates.SurrogateResponse
import com.duckduckgo.app.trackerdetection.model.TrackingEvent
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
import com.duckduckgo.site.permissions.api.SitePermissionsManager.SitePermissions

interface WebViewClientListener {
Expand Down Expand Up @@ -95,7 +96,7 @@ interface WebViewClientListener {
fun linkOpenedInNewTab(): Boolean
fun isActiveTab(): Boolean
fun onReceivedError(errorType: WebViewErrorResponse, url: String)
fun onReceivedMaliciousSiteWarning(url: Uri)
fun onReceivedMaliciousSiteWarning(url: Uri, feed: Feed, exempted: Boolean)
fun recordErrorCode(error: String, url: String)
fun recordHttpErrorCode(statusCode: Int, url: String)

Expand Down
Loading

0 comments on commit 397271f

Please sign in to comment.