Skip to content

Commit 2592f74

Browse files
committed
Add algorithm pixels
1 parent f6a88b2 commit 2592f74

File tree

7 files changed

+73
-5
lines changed

7 files changed

+73
-5
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
@@ -387,4 +387,6 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName {
387387
DEDICATED_WEBVIEW_URL_EXTRACTION_FAILED("m_dedicated_webview_url_extraction_failed"),
388388

389389
BLOCKLIST_TDS_FAILURE("blocklist_experiment_tds_download_failure"),
390+
391+
MALICIOUS_SITE_DETECTED_IN_IFRAME("m_malicious-site-protection_iframe-loaded"),
390392
}

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

+7
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
@@ -41,6 +43,7 @@ import com.duckduckgo.malicioussiteprotection.impl.models.Type.FILTER_SET
4143
import com.duckduckgo.malicioussiteprotection.impl.models.Type.HASH_PREFIXES
4244
import com.squareup.anvil.annotations.ContributesBinding
4345
import dagger.SingleInstanceIn
46+
import java.util.concurrent.TimeoutException
4447
import javax.inject.Inject
4548
import kotlinx.coroutines.withContext
4649

@@ -58,6 +61,7 @@ class RealMaliciousSiteRepository @Inject constructor(
5861
private val maliciousSiteDao: MaliciousSiteDao,
5962
private val maliciousSiteService: MaliciousSiteService,
6063
private val dispatcherProvider: DispatcherProvider,
64+
private val pixels: Pixel,
6165
) : MaliciousSiteRepository {
6266

6367
override suspend fun containsHashPrefix(hashPrefix: String): Boolean {
@@ -91,6 +95,9 @@ class RealMaliciousSiteRepository @Inject constructor(
9195
null
9296
}
9397
}
98+
} catch (e: TimeoutException) {
99+
pixels.fire(MALICIOUS_SITE_CLIENT_TIMEOUT)
100+
listOf()
94101
} catch (e: Exception) {
95102
listOf()
96103
}

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

+8-3
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,14 @@ 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
43+
44+
val timeoutAvailable = method?.isAnnotationPresent(Timeout::class.java) == true
45+
if (timeoutAvailable) {
46+
method?.getAnnotation(Timeout::class.java)?.let { chain.call().timeout().timeout(it.duration, it.unit) }
47+
}
4348

4449
return if (authRequired) {
4550
val newRequest = chain.request().newBuilder()

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

+10
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import com.duckduckgo.anvil.annotations.ContributesServiceApi
2020
import com.duckduckgo.common.utils.AppUrl.Url.API
2121
import com.duckduckgo.di.scopes.AppScope
2222
import com.squareup.moshi.Json
23+
import java.util.concurrent.TimeUnit
24+
import java.util.concurrent.TimeUnit.MILLISECONDS
2325
import retrofit2.http.GET
2426
import retrofit2.http.Query
2527

@@ -48,6 +50,7 @@ interface MaliciousSiteService {
4850
@GET("$BASE_URL$FILTER_SET_PATH?$CATEGORY=$MALWARE")
4951
suspend fun getMalwareFilterSet(@Query("revision") revision: Int): FilterSetResponse
5052

53+
@Timeout(1000, MILLISECONDS)
5154
@AuthRequired
5255
@GET("$BASE_URL/matches")
5356
suspend fun getMatches(@Query("hashPrefix") hashPrefix: String): MatchesResponse
@@ -99,3 +102,10 @@ data class RevisionResponse(
99102
@Target(AnnotationTarget.FUNCTION)
100103
@Retention(AnnotationRetention.RUNTIME)
101104
annotation class AuthRequired
105+
106+
/**
107+
* This annotation is used in interceptors to be able to intercept the annotated service calls
108+
*/
109+
@Target(AnnotationTarget.FUNCTION)
110+
@Retention(AnnotationRetention.RUNTIME)
111+
annotation class Timeout(val duration: Long, val unit: TimeUnit)

0 commit comments

Comments
 (0)