Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading