Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions autofill/autofill-impl/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dependencies {
implementation project(path: ':settings-api') // temporary until we release new settings
implementation project(':library-loader-api')
implementation project(':saved-sites-api')
implementation project(':cookies-api')

anvil project(path: ':anvil-compiler')
implementation project(path: ':anvil-annotations')
Expand Down
4 changes: 4 additions & 0 deletions autofill/autofill-impl/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
android:name=".importing.gpm.webflow.ImportGooglePasswordsWebFlowActivity"
android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|navigation|keyboard"
android:exported="false" />
<activity
android:name=".importing.takeout.webflow.ImportGoogleBookmarksWebFlowActivity"
android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|navigation|keyboard"
android:exported="false" />
<activity
android:name=".ui.credential.management.AutofillManagementActivity"
android:configChanges="orientation|screenSize"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,40 @@
package com.duckduckgo.autofill.impl.importing.gpm.webflow

import com.duckduckgo.autofill.impl.importing.gpm.feature.AutofillImportPasswordConfigStore
import com.duckduckgo.autofill.impl.importing.takeout.store.BookmarkImportConfigStore
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.FragmentScope
import com.squareup.anvil.annotations.ContributesBinding
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import javax.inject.Inject

interface PasswordImporterScriptLoader {
suspend fun getScript(): String
interface GoogleImporterScriptLoader {
suspend fun getScriptForPasswordImport(): String

suspend fun getScriptForBookmarkImport(): String
}

@ContributesBinding(FragmentScope::class)
class PasswordImporterCssScriptLoader @Inject constructor(
class GoogleImporterScriptLoaderImpl @Inject constructor(
private val dispatchers: DispatcherProvider,
private val configStore: AutofillImportPasswordConfigStore,
) : PasswordImporterScriptLoader {
private val passwordConfigStore: AutofillImportPasswordConfigStore,
private val bookmarkConfigStore: BookmarkImportConfigStore,
) : GoogleImporterScriptLoader {
private lateinit var contentScopeJS: String

override suspend fun getScript(): String =
override suspend fun getScriptForPasswordImport(): String =
withContext(dispatchers.io()) {
getContentScopeJS()
.replace(CONTENT_SCOPE_PLACEHOLDER, getContentScopeScriptJson(loadSettingsJsonPassword()))
.replace(USER_UNPROTECTED_DOMAINS_PLACEHOLDER, getUnprotectedDomainsJson())
.replace(USER_PREFERENCES_PLACEHOLDER, getUserPreferencesJson())
}

override suspend fun getScriptForBookmarkImport(): String =
withContext(dispatchers.io()) {
getContentScopeJS()
.replace(CONTENT_SCOPE_PLACEHOLDER, getContentScopeJson(loadSettingsJson()))
.replace(CONTENT_SCOPE_PLACEHOLDER, getContentScopeScriptJson(loadSettingsJsonBookmark()))
.replace(USER_UNPROTECTED_DOMAINS_PLACEHOLDER, getUnprotectedDomainsJson())
.replace(USER_PREFERENCES_PLACEHOLDER, getUserPreferencesJson())
}
Expand All @@ -47,7 +59,7 @@ class PasswordImporterCssScriptLoader @Inject constructor(
* This enables the password import hints feature in C-S-S.
* These settings are for enabling it; the check for whether it should be enabled or not is done elsewhere.
*/
private fun getContentScopeJson(settingsJson: String): String =
private fun getContentScopeScriptJson(settingsJson: String): String =
"""{
"features":{
"autofillImport" : {
Expand All @@ -61,7 +73,9 @@ class PasswordImporterCssScriptLoader @Inject constructor(

""".trimMargin()

private suspend fun loadSettingsJson(): String = configStore.getConfig().javascriptConfigGooglePasswords
private suspend fun loadSettingsJsonPassword(): String = passwordConfigStore.getConfig().javascriptConfigGooglePasswords

private suspend fun loadSettingsJsonBookmark(): String = bookmarkConfigStore.getConfig().javascriptConfigGoogleTakeout

private fun getUserPreferencesJson(): String =
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.webkit.WebViewCompat
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.autofill.api.AutofillCapabilityChecker
import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin
import com.duckduckgo.autofill.api.BrowserAutofill
import com.duckduckgo.autofill.api.CredentialAutofillDialogFactory
Expand Down Expand Up @@ -72,13 +71,13 @@ import com.duckduckgo.common.utils.FragmentViewModelFactory
import com.duckduckgo.common.utils.plugins.PluginPoint
import com.duckduckgo.di.scopes.FragmentScope
import com.duckduckgo.user.agent.api.UserAgentProvider
import javax.inject.Inject
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import logcat.LogPriority.WARN
import logcat.logcat
import javax.inject.Inject

@InjectWith(FragmentScope::class)
class ImportGooglePasswordsWebFlowFragment :
Expand All @@ -89,7 +88,6 @@ class ImportGooglePasswordsWebFlowFragment :
NoOpEmailProtectionUserPromptListener,
NoOpAutofillEventListener,
GooglePasswordBlobConsumer.Callback {

@Inject
lateinit var userAgentProvider: UserAgentProvider

Expand All @@ -99,9 +97,6 @@ class ImportGooglePasswordsWebFlowFragment :
@Inject
lateinit var viewModelFactory: FragmentViewModelFactory

@Inject
lateinit var autofillCapabilityChecker: AutofillCapabilityChecker

@Inject
lateinit var credentialAutofillDialogFactory: CredentialAutofillDialogFactory

Expand All @@ -115,7 +110,7 @@ class ImportGooglePasswordsWebFlowFragment :
lateinit var passwordBlobConsumer: GooglePasswordBlobConsumer

@Inject
lateinit var passwordImporterScriptLoader: PasswordImporterScriptLoader
lateinit var googleImporterScriptLoader: GoogleImporterScriptLoader

@Inject
lateinit var browserAutofillConfigurator: InternalBrowserAutofillConfigurator
Expand Down Expand Up @@ -179,8 +174,7 @@ class ImportGooglePasswordsWebFlowFragment :
// no-op
}
}
}
.launchIn(lifecycleScope)
}.launchIn(lifecycleScope)
}

private fun observeCommands() {
Expand All @@ -201,8 +195,7 @@ class ImportGooglePasswordsWebFlowFragment :
showCredentialChooserDialog(command.originalUrl, command.credentials, command.triggerType)
}
}
}
.launchIn(lifecycleScope)
}.launchIn(lifecycleScope)
}

private suspend fun injectReauthenticationCredentials(
Expand All @@ -217,11 +210,12 @@ class ImportGooglePasswordsWebFlowFragment :
return@withContext
}

val credentials = LoginCredentials(
domain = url,
username = username,
password = password,
)
val credentials =
LoginCredentials(
domain = url,
username = username,
password = password,
)

logcat { "Injecting re-authentication credentials" }
browserAutofill.injectCredentials(credentials)
Expand All @@ -234,16 +228,18 @@ class ImportGooglePasswordsWebFlowFragment :
}

private fun exitFlowAsSuccess() {
val resultBundle = Bundle().also {
it.putParcelable(RESULT_KEY_DETAILS, ImportGooglePasswordResult.Success)
}
val resultBundle =
Bundle().also {
it.putParcelable(RESULT_KEY_DETAILS, ImportGooglePasswordResult.Success)
}
setFragmentResult(RESULT_KEY, resultBundle)
}

private fun exitFlowAsImpossibleToImport(reason: UserCannotImportReason) {
val resultBundle = Bundle().also {
it.putParcelable(RESULT_KEY_DETAILS, ImportGooglePasswordResult.Error(reason))
}
val resultBundle =
Bundle().also {
it.putParcelable(RESULT_KEY_DETAILS, ImportGooglePasswordResult.Error(reason))
}
setFragmentResult(RESULT_KEY, resultBundle)
}

Expand Down Expand Up @@ -343,7 +339,7 @@ class ImportGooglePasswordsWebFlowFragment :
@SuppressLint("RequiresFeature", "AddDocumentStartJavaScriptUsage")
private suspend fun configurePasswordImportJavascript(webView: WebView) {
if (importPasswordConfig.getConfig().canInjectJavascript) {
val script = passwordImporterScriptLoader.getScript()
val script = googleImporterScriptLoader.getScriptForPasswordImport()
WebViewCompat.addDocumentStartJavaScript(webView, script, setOf("*"))
}
}
Expand Down Expand Up @@ -371,12 +367,13 @@ class ImportGooglePasswordsWebFlowFragment :
return@withContext
}

val dialog = credentialAutofillDialogFactory.autofillSelectCredentialsDialog(
url,
credentials,
triggerType,
CUSTOM_FLOW_TAB_ID,
)
val dialog =
credentialAutofillDialogFactory.autofillSelectCredentialsDialog(
url,
credentials,
triggerType,
CUSTOM_FLOW_TAB_ID,
)
dialog.show(childFragmentManager, SELECT_CREDENTIALS_FRAGMENT_TAG)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* 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.autofill.impl.importing.takeout.processor

import android.net.Uri
import com.duckduckgo.autofill.impl.importing.takeout.processor.BookmarkImportProcessor.*
import com.duckduckgo.autofill.impl.importing.takeout.processor.BookmarkImportProcessor.ImportResult.Error
import com.duckduckgo.autofill.impl.importing.takeout.processor.BookmarkImportProcessor.ImportResult.Error.ParseError
import com.duckduckgo.autofill.impl.importing.takeout.zip.TakeoutBookmarkExtractor
import com.duckduckgo.autofill.impl.importing.takeout.zip.TakeoutBookmarkExtractor.ExtractionResult
import com.duckduckgo.autofill.impl.importing.takeout.zip.TakeoutZipDownloader
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.savedsites.api.service.ImportSavedSitesResult
import com.duckduckgo.savedsites.api.service.SavedSitesImporter.ImportFolder
import com.squareup.anvil.annotations.ContributesBinding
import kotlinx.coroutines.withContext
import logcat.LogPriority.WARN
import logcat.asLog
import logcat.logcat
import java.io.File
import javax.inject.Inject

interface BookmarkImportProcessor {
sealed class ImportResult {
data class Success(
val importedCount: Int,
) : ImportResult()

sealed class Error : ImportResult() {
data object DownloadError : Error()

data object ParseError : Error()

data object ImportError : Error()
}
}

suspend fun downloadAndImportFromTakeoutZipUrl(
url: String,
userAgent: String,
folderName: String,
): ImportResult
}

@ContributesBinding(AppScope::class)
class TakeoutBookmarkImportProcessor @Inject constructor(
private val dispatchers: DispatcherProvider,
private val takeoutZipDownloader: TakeoutZipDownloader,
private val bookmarkExtractor: TakeoutBookmarkExtractor,
private val takeoutBookmarkImporter: TakeoutBookmarkImporter,
) : BookmarkImportProcessor {
override suspend fun downloadAndImportFromTakeoutZipUrl(
url: String,
userAgent: String,
folderName: String,
): ImportResult =
withContext(dispatchers.io()) {
runCatching {
val zipUri = takeoutZipDownloader.downloadZip(url, userAgent)
processBookmarkZip(zipUri, folderName)
}.getOrElse { e ->
logcat(WARN) { "Bookmark zip download failed: ${e.asLog()}" }
Error.DownloadError
}
}

private suspend fun processBookmarkZip(
zipUri: Uri,
folderName: String,
): ImportResult =
runCatching {
val extractionResult = bookmarkExtractor.extractBookmarksFromFile(zipUri)
handleExtractionResult(extractionResult, folderName)
}.getOrElse { e ->
logcat(WARN) { "Error processing bookmark zip: ${e.asLog()}" }
ParseError
}.also {
cleanupZipFile(zipUri)
}

private suspend fun handleExtractionResult(
extractionResult: ExtractionResult,
folderName: String,
): ImportResult =
when (extractionResult) {
is ExtractionResult.Success -> {
val importResult =
takeoutBookmarkImporter.importBookmarks(
extractionResult.tempFileUri,
ImportFolder.Folder(folderName),
)
handleImportResult(importResult)
}

is ExtractionResult.Error -> {
logcat(WARN) { "Error extracting bookmarks from zip" }
ParseError
}
}

private fun handleImportResult(importResult: ImportSavedSitesResult): ImportResult =
when (importResult) {
is ImportSavedSitesResult.Success -> {
val importedCount = importResult.savedSites.size
logcat { "Successfully imported $importedCount bookmarks" }
ImportResult.Success(importedCount)
}

is ImportSavedSitesResult.Error -> {
logcat(WARN) { "Error importing bookmarks: ${importResult.exception.message}" }
Error.ImportError
}
}

private fun cleanupZipFile(zipUri: Uri) {
runCatching {
val zipFile = File(zipUri.path ?: return)
if (zipFile.exists() && zipFile.delete()) {
logcat { "Cleaned up downloaded zip file: ${zipFile.absolutePath}" }
}
}.onFailure { logcat(WARN) { "Failed to cleanup downloaded zip file: ${it.asLog()}" } }
}
}
Loading
Loading