Skip to content

Add support for scam protection #5853

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

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ 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.malicioussiteprotection.api.MaliciousSiteProtection.Feed.SCAM
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 @@ -3341,6 +3342,7 @@ class BrowserTabViewModel @Inject constructor(
val maliciousSiteStatus = when (feed) {
MALWARE -> MaliciousSiteStatus.MALWARE
PHISHING -> MaliciousSiteStatus.PHISHING
SCAM -> MaliciousSiteStatus.SCAM
}

buildSiteFactory(
Expand All @@ -3351,7 +3353,7 @@ class BrowserTabViewModel @Inject constructor(

if (!exempted) {
if (currentBrowserViewState().maliciousSiteBlocked && previousSite?.url == url.toString()) {
Timber.tag("Cris").d("maliciousSiteBlocked already shown for $url, previousSite: ${previousSite.url}")
Timber.d("maliciousSiteBlocked already shown for $url, previousSite: ${previousSite.url}")
} else {
val params = mapOf(CATEGORY_KEY to feed.name.lowercase(), CLIENT_SIDE_HIT_KEY to clientSideHit.toString())
pixel.fire(AppPixelName.MALICIOUS_SITE_PROTECTION_ERROR_SHOWN, params)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.duckduckgo.app.browser.webview

import android.content.Context
import android.text.SpannableStringBuilder
import android.text.TextPaint
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.text.style.URLSpan
Expand All @@ -40,6 +41,7 @@ import com.duckduckgo.common.utils.extensions.html
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.api.MaliciousSiteProtection.Feed.SCAM

class MaliciousSiteBlockedWarningLayout @JvmOverloads constructor(
context: Context,
Expand Down Expand Up @@ -79,12 +81,9 @@ class MaliciousSiteBlockedWarningLayout @JvmOverloads constructor(
) {
with(binding) {
val errorResource = when (feed) {
MALWARE -> {
R.string.maliciousSiteMalwareHeadline
}
PHISHING -> {
R.string.maliciousSitePhishingHeadline
}
MALWARE -> R.string.maliciousSiteMalwareHeadline
PHISHING -> R.string.maliciousSitePhishingHeadline
SCAM -> R.string.maliciousSiteScamHeadline
}
errorHeadline.setSpannable(errorResource) { actionHandler(LearnMore) }
expandedHeadline.setSpannable(R.string.maliciousSiteExpandedHeadline) { actionHandler(ReportError) }
Expand All @@ -100,6 +99,11 @@ class MaliciousSiteBlockedWarningLayout @JvmOverloads constructor(
override fun onClick(widget: View) {
actionHandler()
}

override fun updateDrawState(ds: TextPaint) {
ds.color = currentTextColor
ds.isUnderlineText = true
}
}
val htmlContent = context.getString(errorResource).html(context)
val spannableString = SpannableStringBuilder(htmlContent)
Expand Down
24 changes: 15 additions & 9 deletions app/src/main/res/layout/view_malicious_site_blocked_warning.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="150dp"
android:paddingBottom="50dp"
android:paddingHorizontal="@dimen/keyline_5">
android:paddingBottom="50dp">

<ImageView
android:id="@+id/alertImage"
Expand All @@ -35,7 +34,8 @@
android:src="@drawable/malware_site_128"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"
android:layout_marginHorizontal="@dimen/keyline_5"/>

<com.duckduckgo.common.ui.view.text.DaxTextView
android:id="@+id/errorTitle"
Expand All @@ -48,7 +48,8 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/alertImage"
app:typography="h2" />
app:typography="h2"
android:layout_marginHorizontal="@dimen/keyline_5"/>

<com.duckduckgo.common.ui.view.text.DaxTextView
android:id="@+id/errorHeadline"
Expand All @@ -60,7 +61,8 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/errorTitle"
app:typography="body1"
android:text="@string/maliciousSiteMalwareHeadline" />
android:text="@string/maliciousSiteMalwareHeadline"
android:layout_marginHorizontal="@dimen/keyline_5"/>


<com.duckduckgo.common.ui.view.button.DaxButtonPrimary
Expand All @@ -72,7 +74,8 @@
app:daxButtonSize="large"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/errorHeadline"/>
app:layout_constraintTop_toBottomOf="@+id/errorHeadline"
android:layout_marginHorizontal="@dimen/keyline_5"/>

<com.duckduckgo.common.ui.view.button.DaxButtonGhost
android:id="@+id/advancedCTA"
Expand All @@ -82,7 +85,8 @@
app:daxButtonSize="large"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/leaveSiteCTA" />
app:layout_constraintTop_toBottomOf="@+id/leaveSiteCTA"
android:layout_marginHorizontal="@dimen/keyline_5"/>

<androidx.constraintlayout.widget.Group
android:id="@+id/advancedGroup"
Expand Down Expand Up @@ -111,7 +115,8 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/advancedDivider"
app:typography="body2"
android:text="@string/maliciousSiteExpandedHeadline" />
android:text="@string/maliciousSiteExpandedHeadline"
android:layout_marginHorizontal="@dimen/keyline_5"/>

<com.duckduckgo.common.ui.view.text.DaxTextView
android:id="@+id/expandedCTA"
Expand All @@ -123,7 +128,8 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/expandedHeadline"
app:typography="body2"
android:text="@string/maliciousSiteExpandedCTA" />
android:text="@string/maliciousSiteExpandedCTA"
android:layout_marginHorizontal="@dimen/keyline_5"/>

</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
2 changes: 2 additions & 0 deletions app/src/main/res/values/donottranslate.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,6 @@
<!-- Skip Onboarding (not user-facing) -->
<string name="skipOnboarding">Skip Onboarding</string>"

<!-- Malicious Site Protection -->
<string name="maliciousSiteScamHeadline"><![CDATA[DuckDuckGo blocked this page because it may be trying to deceive or manipulate you into transferring money, buying counterfeit goods, or installing malware under false pretenses.\n<a href="">Learn more</a>]]></string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ interface Site {
}

enum class MaliciousSiteStatus {
PHISHING, MALWARE
PHISHING, MALWARE, SCAM
}

fun Site.orderedTrackerBlockedEntities(): List<Entity> = trackingEvents
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ interface MaliciousSiteProtection {
enum class Feed {
PHISHING,
MALWARE,
SCAM,
;

companion object {
fun fromString(name: String): Feed? {
return try {
valueOf(name)
} catch (e: IllegalArgumentException) {
null
}
}
}
}

sealed class IsMaliciousResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,8 @@ interface MaliciousSiteProtectionFeature {

@Toggle.DefaultValue(true)
fun canUpdateDatasets(): Toggle

@Toggle.InternalAlwaysEnabled
@Toggle.DefaultValue(false)
fun scamProtection(): Toggle
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import android.content.Context
import androidx.lifecycle.LifecycleOwner
import androidx.work.BackoffPolicy
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
Expand All @@ -28,6 +29,10 @@ import com.duckduckgo.anvil.annotations.ContributesWorker
import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver
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.api.MaliciousSiteProtection.Feed.SCAM
import com.duckduckgo.malicioussiteprotection.impl.data.MaliciousSiteRepository
import com.duckduckgo.privacy.config.api.PrivacyConfigCallbackPlugin
import com.squareup.anvil.annotations.ContributesMultibinding
Expand All @@ -36,6 +41,8 @@ import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlinx.coroutines.withContext

private const val TYPE = "type"

@ContributesWorker(AppScope::class)
class MaliciousSiteProtectionFiltersUpdateWorker(
context: Context,
Expand All @@ -55,7 +62,20 @@ class MaliciousSiteProtectionFiltersUpdateWorker(
if (maliciousSiteProtectionFeature.isFeatureEnabled().not() || maliciousSiteProtectionFeature.canUpdateDatasets().not()) {
return@withContext Result.success()
}
return@withContext if (maliciousSiteRepository.loadFilters().isSuccess) {

val feeds = inputData.getStringArray(TYPE)
?.mapNotNull { Feed.fromString(it) }
?.let { feeds ->
if (!maliciousSiteProtectionFeature.scamProtectionEnabled()) {
return@let feeds.filterNot { it == SCAM }
}
feeds
}
?.toTypedArray() ?: return@withContext Result.failure()

if (feeds.isEmpty()) return@withContext Result.success()

return@withContext if (maliciousSiteRepository.loadFilters(*feeds).isSuccess) {
Result.success()
} else {
Result.retry()
Expand All @@ -81,32 +101,50 @@ class MaliciousSiteProtectionFiltersUpdateWorkerScheduler @Inject constructor(

override fun onCreate(owner: LifecycleOwner) {
enqueuePeriodicWork()
enqueuePeriodicScamWork()
}

override fun onPrivacyConfigDownloaded() {
enqueuePeriodicWork()
enqueuePeriodicScamWork()
}

private fun enqueuePeriodicWork() {
if (maliciousSiteProtectionFeature.isFeatureEnabled().not() || maliciousSiteProtectionFeature.canUpdateDatasets().not()) {
workManager.cancelUniqueWork(MALICIOUS_SITE_PROTECTION_FILTERS_UPDATE_WORKER_TAG)
return
}
val workerRequest = PeriodicWorkRequestBuilder<MaliciousSiteProtectionFiltersUpdateWorker>(
enqueueWorker(PHISHING, MALWARE, tag = MALICIOUS_SITE_PROTECTION_FILTERS_UPDATE_WORKER_TAG)
}

private fun enqueuePeriodicScamWork() {
with(maliciousSiteProtectionFeature) {
if (isFeatureEnabled().not() || canUpdateDatasets().not() || scamProtectionEnabled().not()) {
workManager.cancelUniqueWork(MALICIOUS_SITE_PROTECTION_FILTERS_UPDATE_WORKER_SCAM_TAG)
return
}
}
enqueueWorker(SCAM, tag = MALICIOUS_SITE_PROTECTION_FILTERS_UPDATE_WORKER_SCAM_TAG)
}

private fun enqueueWorker(vararg feeds: Feed, tag: String) {
PeriodicWorkRequestBuilder<MaliciousSiteProtectionFiltersUpdateWorker>(
maliciousSiteProtectionFeature.getFilterSetUpdateFrequency(),
TimeUnit.MINUTES,
)
.addTag(MALICIOUS_SITE_PROTECTION_FILTERS_UPDATE_WORKER_TAG)
).addTag(tag)
.setInputData(Data.Builder().putStringArray(TYPE, feeds.map { it.name }.toTypedArray()).build())
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
.build()
workManager.enqueueUniquePeriodicWork(
MALICIOUS_SITE_PROTECTION_FILTERS_UPDATE_WORKER_TAG,
ExistingPeriodicWorkPolicy.UPDATE,
workerRequest,
)
.build().let {
workManager.enqueueUniquePeriodicWork(
tag,
ExistingPeriodicWorkPolicy.UPDATE,
it,
)
}
}

companion object {
private const val MALICIOUS_SITE_PROTECTION_FILTERS_UPDATE_WORKER_TAG = "MALICIOUS_SITE_PROTECTION_FILTERS_UPDATE_WORKER_TAG"
private const val MALICIOUS_SITE_PROTECTION_FILTERS_UPDATE_WORKER_SCAM_TAG = "MALICIOUS_SITE_PROTECTION_FILTERS_UPDATE_WORKER_SCAM_TAG"
}
}
Loading
Loading