Skip to content

Commit

Permalink
Add localStorage exceptions to duckduckgo.com
Browse files Browse the repository at this point in the history
  • Loading branch information
aitorvs committed Dec 3, 2024
1 parent 04e0326 commit 38af31a
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ package com.duckduckgo.app.browser

import android.annotation.SuppressLint
import android.content.Context
import android.webkit.ValueCallback
import android.webkit.WebStorage
import android.webkit.WebStorage.Origin
import android.webkit.WebView
import androidx.test.platform.app.InstrumentationRegistry
import com.duckduckgo.app.browser.httpauth.WebViewHttpAuthStore
Expand All @@ -29,9 +31,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@Suppress("RemoveExplicitTypeArguments")
@SuppressLint("NoHardcodedCoroutineDispatcher")
Expand All @@ -44,6 +51,15 @@ class WebViewDataManagerTest {
private val mockWebViewHttpAuthStore: WebViewHttpAuthStore = mock()
private val testee = WebViewDataManager(context, WebViewSessionInMemoryStorage(), mockCookieManager, mockFileDeleter, mockWebViewHttpAuthStore)

@Before
fun setup() {
doAnswer { invocation ->
val callback = invocation.arguments[0] as ValueCallback<Map<String, Origin>>
callback.onReceiveValue(emptyMap()) // Simulate callback invocation
null
}.whenever(mockStorage).getOrigins(any())
}

@Test
fun whenDataClearedThenWebViewHistoryCleared() = runTest {
withContext(Dispatchers.Main) {
Expand Down Expand Up @@ -76,7 +92,8 @@ class WebViewDataManagerTest {
withContext(Dispatchers.Main) {
val webView = TestWebView(context)
testee.clearData(webView, mockStorage)
verify(mockStorage).deleteAllData()
// we call deleteOrigin() instead and we should make sure we don't call deleteAllData()
verify(mockStorage, never()).deleteAllData()
}
}

Expand Down
53 changes: 32 additions & 21 deletions app/src/main/java/com/duckduckgo/app/browser/WebDataManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ package com.duckduckgo.app.browser

import android.content.Context
import android.webkit.WebStorage
import android.webkit.WebStorage.Origin
import android.webkit.WebView
import com.duckduckgo.app.browser.httpauth.WebViewHttpAuthStore
import com.duckduckgo.app.browser.session.WebViewSessionStorage
import com.duckduckgo.app.global.file.FileDeleter
import com.duckduckgo.cookies.api.DuckDuckGoCookieManager
import java.io.File
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

interface WebDataManager {
suspend fun clearData(
Expand Down Expand Up @@ -53,7 +56,7 @@ class WebViewDataManager @Inject constructor(
clearFormData(webView)
clearAuthentication(webView)
clearExternalCookies()
clearWebViewDirectories(exclusions = WEBVIEW_FILES_EXCLUDED_FROM_DELETION)
clearWebViewDirectories()
}

private fun clearWebViewCache(webView: WebView) {
Expand All @@ -64,26 +67,45 @@ class WebViewDataManager @Inject constructor(
webView.clearHistory()
}

private fun clearWebStorage(webStorage: WebStorage) {
webStorage.deleteAllData()
private suspend fun clearWebStorage(webStorage: WebStorage) {
suspendCoroutine { continuation ->
webStorage.getOrigins { origins ->
kotlin.runCatching {
for (origin in origins) {
val originString = (origin as Origin).origin

// Check if this is the domain to exclude
if (!originString.endsWith(".duckduckgo.com")) {
// Delete all other origins
webStorage.deleteOrigin(originString)
}
}
continuation.resume(Unit)
}.onFailure {
continuation.resume(Unit)
}
}
}
}

private fun clearFormData(webView: WebView) {
webView.clearFormData()
}

/**
* Deletes web view directory content. The Cookies file is kept as we clear cookies separately to avoid a crash and maintain ddg cookies.
* Cookies may appear in files:
* app_webview/Cookies
* app_webview/Default/Cookies
* Deletes web view directory content except the following directories
* app_webview/Cookies
* app_webview/Default/Cookies
* app_webview/Default/Local Storage
*
* the excluded directories above are to avoid clearing unnecessary cookies and because localStorage is cleared using clearWebStorage
*/
private suspend fun clearWebViewDirectories(exclusions: List<String>) {
private suspend fun clearWebViewDirectories() {
val dataDir = context.applicationInfo.dataDir
fileDeleter.deleteContents(File(dataDir, WEBVIEW_DATA_DIRECTORY_NAME), exclusions)
fileDeleter.deleteContents(File(dataDir, "app_webview"), listOf("Default", "Cookies"))

// We don't delete the Default dir as Cookies may be inside however we do clear any other content
fileDeleter.deleteContents(File(dataDir, WEBVIEW_DEFAULT_DIRECTORY_NAME), exclusions)
fileDeleter.deleteContents(File(dataDir, "app_webview/Default"), listOf("Cookies", "Local Storage"))
}

private suspend fun clearAuthentication(webView: WebView) {
Expand All @@ -98,15 +120,4 @@ class WebViewDataManager @Inject constructor(
override fun clearWebViewSessions() {
webViewSessionStorage.deleteAllSessions()
}

companion object {
private const val WEBVIEW_DATA_DIRECTORY_NAME = "app_webview"
private const val WEBVIEW_DEFAULT_DIRECTORY_NAME = "app_webview/Default"
private const val DATABASES_DIRECTORY_NAME = "databases"

private val WEBVIEW_FILES_EXCLUDED_FROM_DELETION = listOf(
"Default",
"Cookies",
)
}
}

0 comments on commit 38af31a

Please sign in to comment.