Skip to content

Commit

Permalink
Add algorithm pixels
Browse files Browse the repository at this point in the history
  • Loading branch information
CrisBarreiro committed Feb 6, 2025
1 parent 3ea4f2b commit 40e6e1a
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import com.duckduckgo.app.browser.webview.ExemptedUrlsHolder.ExemptedUrl
import com.duckduckgo.app.browser.webview.RealMaliciousSiteBlockerWebViewIntegration.IsMaliciousViewData
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.di.IsMainProcess
import com.duckduckgo.app.pixels.AppPixelName
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
import com.duckduckgo.app.settings.db.SettingsDataStore
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection
Expand Down Expand Up @@ -97,6 +99,7 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor(
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
private val exemptedUrlsHolder: ExemptedUrlsHolder,
@IsMainProcess private val isMainProcess: Boolean,
private val pixel: Pixel,
) : MaliciousSiteBlockerWebViewIntegration, PrivacyConfigCallbackPlugin {

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
Expand Down Expand Up @@ -161,19 +164,31 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor(
}

val belongsToCurrentPage = documentUri?.host == request.requestHeaders["Referer"]?.toUri()?.host
if (request.isForMainFrame || (isForIframe(request) && belongsToCurrentPage)) {
when (val result = checkMaliciousUrl(decodedUrl, confirmationCallback)) {
val isForIframe = isForIframe(request) && belongsToCurrentPage
if (request.isForMainFrame || isForIframe) {
val result = checkMaliciousUrl(decodedUrl) {
if (isForIframe && it is Malicious) {
firePixelForMaliciousIframe(it.feed)
}
confirmationCallback(it)
}
when (result) {
is ConfirmedResult -> {
when (val status = result.status) {
is Malicious -> {
if (isForIframe) {
firePixelForMaliciousIframe(status.feed)
}
return IsMaliciousViewData.MaliciousSite(url, status.feed, false)
}

is Safe -> {
processedUrls.add(decodedUrl)
return IsMaliciousViewData.Safe
}
}
}

is WaitForConfirmation -> {
processedUrls.add(decodedUrl)
return IsMaliciousViewData.WaitForConfirmation
Expand Down Expand Up @@ -231,6 +246,10 @@ class RealMaliciousSiteBlockerWebViewIntegration @Inject constructor(
}
}

private fun firePixelForMaliciousIframe(feed: Feed) {
pixel.fire(AppPixelName.MALICIOUS_SITE_DETECTED_IN_IFRAME, mapOf("category" to feed.name.lowercase()))
}

private suspend fun checkMaliciousUrl(
url: String,
confirmationCallback: (maliciousStatus: MaliciousStatus) -> Unit,
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt
Original file line number Diff line number Diff line change
Expand Up @@ -392,4 +392,6 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName {
SET_AS_DEFAULT_PROMPT_CLICK("m_set-as-default_prompt_click"),
SET_AS_DEFAULT_PROMPT_DISMISSED("m_set-as-default_prompt_dismissed"),
SET_AS_DEFAULT_IN_MENU_CLICK("m_set-as-default_in-menu_click"),

MALICIOUS_SITE_DETECTED_IN_IFRAME("m_malicious-site-protection_iframe-loaded"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ dependencies {

implementation Google.android.material

implementation project(path: ':statistics-api')

testImplementation AndroidX.test.ext.junit
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2'
testImplementation Testing.junit4
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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.malicioussiteprotection.impl

import com.duckduckgo.app.statistics.pixels.Pixel

enum class AppPixelName(override val pixelName: String) : Pixel.PixelName {
MALICIOUS_SITE_CLIENT_TIMEOUT("m_malicious-site-protection_client-timeout"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@

package com.duckduckgo.malicioussiteprotection.impl.data

import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
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.malicioussiteprotection.impl.AppPixelName.MALICIOUS_SITE_CLIENT_TIMEOUT
import com.duckduckgo.malicioussiteprotection.impl.data.db.MaliciousSiteDao
import com.duckduckgo.malicioussiteprotection.impl.data.db.RevisionEntity
import com.duckduckgo.malicioussiteprotection.impl.data.network.FilterResponse
Expand All @@ -42,7 +44,9 @@ import com.duckduckgo.malicioussiteprotection.impl.models.Type.HASH_PREFIXES
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import javax.inject.Inject
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout

interface MaliciousSiteRepository {
suspend fun containsHashPrefix(hashPrefix: String): Boolean
Expand All @@ -58,6 +62,7 @@ class RealMaliciousSiteRepository @Inject constructor(
private val maliciousSiteDao: MaliciousSiteDao,
private val maliciousSiteService: MaliciousSiteService,
private val dispatcherProvider: DispatcherProvider,
private val pixels: Pixel,
) : MaliciousSiteRepository {

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

override suspend fun matches(hashPrefix: String): List<Match> {
return try {
maliciousSiteService.getMatches(hashPrefix).matches.mapNotNull {
val feed = when (it.feed.uppercase()) {
PHISHING.name -> PHISHING
MALWARE.name -> MALWARE
else -> null
}
if (feed != null) {
Match(it.hostname, it.url, it.regex, it.hash, feed)
} else {
null
withTimeout(1000) {
maliciousSiteService.getMatches(hashPrefix).matches.mapNotNull {
val feed = when (it.feed.uppercase()) {
PHISHING.name -> PHISHING
MALWARE.name -> MALWARE
else -> null
}
if (feed != null) {
Match(it.hostname, it.url, it.regex, it.hash, feed)
} else {
null
}
}
}
} catch (e: TimeoutCancellationException) {
pixels.fire(MALICIOUS_SITE_CLIENT_TIMEOUT)
listOf()
} catch (e: Exception) {
listOf()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.duckduckgo.malicioussiteprotection.impl.data

import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed.PHISHING
import com.duckduckgo.malicioussiteprotection.impl.AppPixelName.MALICIOUS_SITE_CLIENT_TIMEOUT
import com.duckduckgo.malicioussiteprotection.impl.data.db.FilterEntity
import com.duckduckgo.malicioussiteprotection.impl.data.db.HashPrefixEntity
import com.duckduckgo.malicioussiteprotection.impl.data.db.MaliciousSiteDao
Expand All @@ -17,6 +19,7 @@ import com.duckduckgo.malicioussiteprotection.impl.models.FilterSetWithRevision.
import com.duckduckgo.malicioussiteprotection.impl.models.HashPrefixesWithRevision.PhishingHashPrefixesWithRevision
import com.duckduckgo.malicioussiteprotection.impl.models.Match
import com.duckduckgo.malicioussiteprotection.impl.models.Type
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
Expand All @@ -34,7 +37,13 @@ class RealMaliciousSiteRepositoryTest {

private val maliciousSiteDao: MaliciousSiteDao = mock()
private val maliciousSiteService: MaliciousSiteService = mock()
private val repository = RealMaliciousSiteRepository(maliciousSiteDao, maliciousSiteService, coroutineRule.testDispatcherProvider)
private val mockPixel: Pixel = mock()
private val repository = RealMaliciousSiteRepository(
maliciousSiteDao,
maliciousSiteService,
coroutineRule.testDispatcherProvider,
mockPixel,
)

@Test
fun loadFilters_updatesFiltersWhenNetworkRevisionIsHigher() = runTest {
Expand Down Expand Up @@ -136,4 +145,16 @@ class RealMaliciousSiteRepositoryTest {

assertEquals(matchesResponse.matches.map { Match(it.hostname, it.url, it.regex, it.hash, PHISHING) }, result)
}

@Test
fun matches_returnsEmptyListOnTimeout() = runTest {
val hashPrefix = "testPrefix"

whenever(maliciousSiteService.getMatches(hashPrefix)).thenThrow(TimeoutCancellationException::class.java)

val result = repository.matches(hashPrefix)

assertTrue(result.isEmpty())
verify(mockPixel).fire(MALICIOUS_SITE_CLIENT_TIMEOUT)
}
}

0 comments on commit 40e6e1a

Please sign in to comment.