Skip to content

Commit 39aa159

Browse files
committed
Add algorithm pixels
1 parent 3ea4f2b commit 39aa159

File tree

7 files changed

+93
-16
lines changed

7 files changed

+93
-16
lines changed

app/src/main/java/com/duckduckgo/app/browser/webview/MaliciousSiteBlockerWebViewIntegration.kt

+21-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ import com.duckduckgo.app.browser.webview.ExemptedUrlsHolder.ExemptedUrl
2424
import com.duckduckgo.app.browser.webview.RealMaliciousSiteBlockerWebViewIntegration.IsMaliciousViewData
2525
import com.duckduckgo.app.di.AppCoroutineScope
2626
import com.duckduckgo.app.di.IsMainProcess
27+
import com.duckduckgo.app.pixels.AppPixelName
2728
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
2829
import com.duckduckgo.app.settings.db.SettingsDataStore
30+
import com.duckduckgo.app.statistics.pixels.Pixel
2931
import com.duckduckgo.common.utils.DispatcherProvider
3032
import com.duckduckgo.di.scopes.AppScope
3133
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection
@@ -97,6 +99,7 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor(
9799
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
98100
private val exemptedUrlsHolder: ExemptedUrlsHolder,
99101
@IsMainProcess private val isMainProcess: Boolean,
102+
private val pixel: Pixel,
100103
) : MaliciousSiteBlockerWebViewIntegration, PrivacyConfigCallbackPlugin {
101104

102105
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@@ -161,19 +164,31 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor(
161164
}
162165

163166
val belongsToCurrentPage = documentUri?.host == request.requestHeaders["Referer"]?.toUri()?.host
164-
if (request.isForMainFrame || (isForIframe(request) && belongsToCurrentPage)) {
165-
when (val result = checkMaliciousUrl(decodedUrl, confirmationCallback)) {
167+
val isForIframe = isForIframe(request) && belongsToCurrentPage
168+
if (request.isForMainFrame || isForIframe) {
169+
val result = checkMaliciousUrl(decodedUrl) {
170+
if (isForIframe && it is Malicious) {
171+
firePixelForMaliciousIframe(it.feed)
172+
}
173+
confirmationCallback(it)
174+
}
175+
when (result) {
166176
is ConfirmedResult -> {
167177
when (val status = result.status) {
168178
is Malicious -> {
179+
if (isForIframe) {
180+
firePixelForMaliciousIframe(status.feed)
181+
}
169182
return IsMaliciousViewData.MaliciousSite(url, status.feed, false)
170183
}
184+
171185
is Safe -> {
172186
processedUrls.add(decodedUrl)
173187
return IsMaliciousViewData.Safe
174188
}
175189
}
176190
}
191+
177192
is WaitForConfirmation -> {
178193
processedUrls.add(decodedUrl)
179194
return IsMaliciousViewData.WaitForConfirmation
@@ -231,6 +246,10 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor(
231246
}
232247
}
233248

249+
private fun firePixelForMaliciousIframe(feed: Feed) {
250+
pixel.fire(AppPixelName.MALICIOUS_SITE_DETECTED_IN_IFRAME, mapOf("category" to feed.name.lowercase()))
251+
}
252+
234253
private suspend fun checkMaliciousUrl(
235254
url: String,
236255
confirmationCallback: (maliciousStatus: MaliciousStatus) -> Unit,

app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt

+2
Original file line numberDiff line numberDiff line change
@@ -392,4 +392,6 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName {
392392
SET_AS_DEFAULT_PROMPT_CLICK("m_set-as-default_prompt_click"),
393393
SET_AS_DEFAULT_PROMPT_DISMISSED("m_set-as-default_prompt_dismissed"),
394394
SET_AS_DEFAULT_IN_MENU_CLICK("m_set-as-default_in-menu_click"),
395+
396+
MALICIOUS_SITE_DETECTED_IN_IFRAME("m_malicious-site-protection_iframe-loaded"),
395397
}

malicious-site-protection/malicious-site-protection-impl/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ dependencies {
5454

5555
implementation Google.android.material
5656

57+
implementation project(path: ':statistics-api')
58+
5759
testImplementation AndroidX.test.ext.junit
5860
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2'
5961
testImplementation Testing.junit4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.malicioussiteprotection.impl
18+
19+
import com.duckduckgo.app.statistics.pixels.Pixel
20+
21+
enum class AppPixelName(override val pixelName: String) : Pixel.PixelName {
22+
MALICIOUS_SITE_CLIENT_TIMEOUT("m_malicious-site-protection_client-timeout"),
23+
}

malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/MaliciousSiteRepository.kt

+20-10
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616

1717
package com.duckduckgo.malicioussiteprotection.impl.data
1818

19+
import com.duckduckgo.app.statistics.pixels.Pixel
1920
import com.duckduckgo.common.utils.DispatcherProvider
2021
import com.duckduckgo.di.scopes.AppScope
2122
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
2223
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed.MALWARE
2324
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed.PHISHING
25+
import com.duckduckgo.malicioussiteprotection.impl.AppPixelName.MALICIOUS_SITE_CLIENT_TIMEOUT
2426
import com.duckduckgo.malicioussiteprotection.impl.data.db.MaliciousSiteDao
2527
import com.duckduckgo.malicioussiteprotection.impl.data.db.RevisionEntity
2628
import com.duckduckgo.malicioussiteprotection.impl.data.network.FilterResponse
@@ -42,7 +44,9 @@ import com.duckduckgo.malicioussiteprotection.impl.models.Type.HASH_PREFIXES
4244
import com.squareup.anvil.annotations.ContributesBinding
4345
import dagger.SingleInstanceIn
4446
import javax.inject.Inject
47+
import kotlinx.coroutines.TimeoutCancellationException
4548
import kotlinx.coroutines.withContext
49+
import kotlinx.coroutines.withTimeout
4650

4751
interface MaliciousSiteRepository {
4852
suspend fun containsHashPrefix(hashPrefix: String): Boolean
@@ -58,6 +62,7 @@ class RealMaliciousSiteRepository @Inject constructor(
5862
private val maliciousSiteDao: MaliciousSiteDao,
5963
private val maliciousSiteService: MaliciousSiteService,
6064
private val dispatcherProvider: DispatcherProvider,
65+
private val pixels: Pixel,
6166
) : MaliciousSiteRepository {
6267

6368
override suspend fun containsHashPrefix(hashPrefix: String): Boolean {
@@ -79,18 +84,23 @@ class RealMaliciousSiteRepository @Inject constructor(
7984

8085
override suspend fun matches(hashPrefix: String): List<Match> {
8186
return try {
82-
maliciousSiteService.getMatches(hashPrefix).matches.mapNotNull {
83-
val feed = when (it.feed.uppercase()) {
84-
PHISHING.name -> PHISHING
85-
MALWARE.name -> MALWARE
86-
else -> null
87-
}
88-
if (feed != null) {
89-
Match(it.hostname, it.url, it.regex, it.hash, feed)
90-
} else {
91-
null
87+
withTimeout(1000) {
88+
maliciousSiteService.getMatches(hashPrefix).matches.mapNotNull {
89+
val feed = when (it.feed.uppercase()) {
90+
PHISHING.name -> PHISHING
91+
MALWARE.name -> MALWARE
92+
else -> null
93+
}
94+
if (feed != null) {
95+
Match(it.hostname, it.url, it.regex, it.hash, feed)
96+
} else {
97+
null
98+
}
9299
}
93100
}
101+
} catch (e: TimeoutCancellationException) {
102+
pixels.fire(MALICIOUS_SITE_CLIENT_TIMEOUT)
103+
listOf()
94104
} catch (e: Exception) {
95105
listOf()
96106
}

malicious-site-protection/malicious-site-protection-impl/src/main/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/network/MaliciousSiteProtectionRequestInterceptor.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ class MaliciousSiteProtectionRequestInterceptor @Inject constructor() : ApiInter
3737
override fun intercept(chain: Chain): Response {
3838
val request = chain.request()
3939

40-
val authRequired = chain.request().tag(Invocation::class.java)
41-
?.method()
42-
?.isAnnotationPresent(AuthRequired::class.java) == true
40+
val method = chain.request().tag(Invocation::class.java)?.method()
41+
42+
val authRequired = method?.isAnnotationPresent(AuthRequired::class.java) == true
4343

4444
return if (authRequired) {
4545
val newRequest = chain.request().newBuilder()

malicious-site-protection/malicious-site-protection-impl/src/test/kotlin/com/duckduckgo/malicioussiteprotection/impl/data/RealMaliciousSiteRepositoryTest.kt

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.duckduckgo.malicioussiteprotection.impl.data
22

3+
import com.duckduckgo.app.statistics.pixels.Pixel
34
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed.PHISHING
5+
import com.duckduckgo.malicioussiteprotection.impl.AppPixelName.MALICIOUS_SITE_CLIENT_TIMEOUT
46
import com.duckduckgo.malicioussiteprotection.impl.data.db.FilterEntity
57
import com.duckduckgo.malicioussiteprotection.impl.data.db.HashPrefixEntity
68
import com.duckduckgo.malicioussiteprotection.impl.data.db.MaliciousSiteDao
@@ -17,6 +19,7 @@ import com.duckduckgo.malicioussiteprotection.impl.models.FilterSetWithRevision.
1719
import com.duckduckgo.malicioussiteprotection.impl.models.HashPrefixesWithRevision.PhishingHashPrefixesWithRevision
1820
import com.duckduckgo.malicioussiteprotection.impl.models.Match
1921
import com.duckduckgo.malicioussiteprotection.impl.models.Type
22+
import kotlinx.coroutines.TimeoutCancellationException
2023
import kotlinx.coroutines.test.runTest
2124
import org.junit.Assert.assertEquals
2225
import org.junit.Assert.assertTrue
@@ -34,7 +37,13 @@ class RealMaliciousSiteRepositoryTest {
3437

3538
private val maliciousSiteDao: MaliciousSiteDao = mock()
3639
private val maliciousSiteService: MaliciousSiteService = mock()
37-
private val repository = RealMaliciousSiteRepository(maliciousSiteDao, maliciousSiteService, coroutineRule.testDispatcherProvider)
40+
private val mockPixel: Pixel = mock()
41+
private val repository = RealMaliciousSiteRepository(
42+
maliciousSiteDao,
43+
maliciousSiteService,
44+
coroutineRule.testDispatcherProvider,
45+
mockPixel,
46+
)
3847

3948
@Test
4049
fun loadFilters_updatesFiltersWhenNetworkRevisionIsHigher() = runTest {
@@ -136,4 +145,16 @@ class RealMaliciousSiteRepositoryTest {
136145

137146
assertEquals(matchesResponse.matches.map { Match(it.hostname, it.url, it.regex, it.hash, PHISHING) }, result)
138147
}
148+
149+
@Test
150+
fun matches_returnsEmptyListOnTimeout() = runTest {
151+
val hashPrefix = "testPrefix"
152+
153+
whenever(maliciousSiteService.getMatches(hashPrefix)).thenThrow(TimeoutCancellationException::class.java)
154+
155+
val result = repository.matches(hashPrefix)
156+
157+
assertTrue(result.isEmpty())
158+
verify(mockPixel).fire(MALICIOUS_SITE_CLIENT_TIMEOUT)
159+
}
139160
}

0 commit comments

Comments
 (0)