diff --git a/common/src/androidMain/kotlin/com/artemchep/keyguard/common/usecase/impl/GetSuggestionsImpl.kt b/common/src/androidMain/kotlin/com/artemchep/keyguard/common/usecase/impl/GetSuggestionsImpl.kt index 0e64bf96..9f146f74 100644 --- a/common/src/androidMain/kotlin/com/artemchep/keyguard/common/usecase/impl/GetSuggestionsImpl.kt +++ b/common/src/androidMain/kotlin/com/artemchep/keyguard/common/usecase/impl/GetSuggestionsImpl.kt @@ -17,10 +17,12 @@ import com.artemchep.keyguard.common.model.LinkInfoPlatform import com.artemchep.keyguard.common.model.contains import com.artemchep.keyguard.common.service.extract.LinkInfoExtractor import com.artemchep.keyguard.common.usecase.CipherUrlCheck +import com.artemchep.keyguard.common.usecase.GetAutofillDefaultMatchDetection import com.artemchep.keyguard.common.usecase.GetSuggestions import com.artemchep.keyguard.feature.home.vault.search.findAlike import io.ktor.http.Url import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first import org.kodein.di.DirectDI import org.kodein.di.allInstances import org.kodein.di.instance @@ -113,10 +115,12 @@ private const val scoreThreshold = 0.1f class GetSuggestionsImpl( private val androidExtractors: List>, + private val getAutofillDefaultMatchDetection: GetAutofillDefaultMatchDetection, private val cipherUrlCheck: CipherUrlCheck, ) : GetSuggestions { constructor(directDI: DirectDI) : this( androidExtractors = directDI.allInstances(), + getAutofillDefaultMatchDetection = directDI.instance(), cipherUrlCheck = directDI.instance(), ) @@ -179,6 +183,8 @@ class GetSuggestionsImpl( ) } + val defaultMatchDetection = getAutofillDefaultMatchDetection() + .first() val c = ciphers .parallelSearch { wrapper -> val cipher = getter.get(wrapper) @@ -190,8 +196,13 @@ class GetSuggestionsImpl( AutofillHint.PASSWORD in target.hints ) { run { - val scoreByWebDomainRaw = - autofillTargetWeb?.findByWebDomain(cipher, tokens, cipherUrlCheck) + val scoreByWebDomainRaw = autofillTargetWeb + ?.findByWebDomain( + secret = cipher, + tokens = tokens, + cipherUrlCheck = cipherUrlCheck, + defaultMatchDetection = defaultMatchDetection, + ) val scoreByWebDomain = scoreByWebDomainRaw ?: 0f if (scoreByWebDomain > scoreThreshold) { @@ -358,6 +369,7 @@ private suspend fun GetSuggestionsImpl.AutofillTargetWeb.findByWebDomain( secret: DSecret, tokens: Set, cipherUrlCheck: CipherUrlCheck, + defaultMatchDetection: DSecret.Uri.MatchType, ): Float = run { val webHost = webUrl.host val webUrl = webUrl.toString() @@ -365,13 +377,13 @@ private suspend fun GetSuggestionsImpl.AutofillTargetWeb.findByWebDomain( // the cipher, then we give it maximum priority. val scoreByUri = secret.uris .map { uri -> - val match = cipherUrlCheck.invoke(uri, webUrl) + val match = cipherUrlCheck.invoke(uri, webUrl, defaultMatchDetection) .attempt() .bind() .getOrElse { false } if (match) { @Suppress("MoveVariableDeclarationIntoWhen") - val matchType = uri.match ?: DSecret.Uri.MatchType.default + val matchType = uri.match ?: defaultMatchDetection when (matchType) { DSecret.Uri.MatchType.Domain -> 10f DSecret.Uri.MatchType.Host -> 20f diff --git a/common/src/commonMain/composeResources/values/strings.xml b/common/src/commonMain/composeResources/values/strings.xml index ef95b85d..d45fc21d 100644 --- a/common/src/commonMain/composeResources/values/strings.xml +++ b/common/src/commonMain/composeResources/values/strings.xml @@ -351,6 +351,8 @@ Unsecure Match app Match detection + Default + Uses a global Default match detection behavior selected in Settings → Autofill → Default URI match detection. Base domain matching is the default option. Base domain Match resources by top-level and second-level domain. Host @@ -1117,6 +1119,7 @@ 3. Confirm and restart Chrome Auto-copy one-time passwords When filling a login information, automatically copy one-time passwords + Default match detection Inline suggestions Embeds autofill suggestions directly into compatible keyboards Manual selection diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/DFilter.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/DFilter.kt index 183d0c62..3e39e37f 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/DFilter.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/DFilter.kt @@ -21,6 +21,7 @@ import com.artemchep.keyguard.common.usecase.CipherExpiringCheck import com.artemchep.keyguard.common.usecase.CipherIncompleteCheck import com.artemchep.keyguard.common.usecase.CipherUnsecureUrlCheck import com.artemchep.keyguard.common.usecase.CipherUrlDuplicateCheck +import com.artemchep.keyguard.common.usecase.GetAutofillDefaultMatchDetection import com.artemchep.keyguard.common.usecase.GetBreaches import com.artemchep.keyguard.common.usecase.GetPasskeys import com.artemchep.keyguard.common.usecase.GetTwoFa @@ -47,6 +48,7 @@ import com.artemchep.keyguard.ui.icons.KeyguardReusedPassword import com.artemchep.keyguard.ui.icons.KeyguardTwoFa import com.artemchep.keyguard.ui.icons.KeyguardUnsecureWebsites import io.ktor.http.Url +import kotlinx.coroutines.flow.first import kotlinx.datetime.Clock import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -769,6 +771,8 @@ sealed interface DFilter { directDI: DirectDI, ciphers: List, ): Set = ioEffect { + val getAutofillDefaultMatchDetection = + directDI.instance() val check: CipherBreachCheck = directDI.instance() val getBreaches: GetBreaches = directDI.instance() @@ -778,6 +782,8 @@ sealed interface DFilter { } .bind() + val defaultMatchDetection = getAutofillDefaultMatchDetection() + .first() ciphers .filter { cipher -> val shouldIgnore = shouldIgnore(cipher) @@ -785,7 +791,7 @@ sealed interface DFilter { return@filter false } - check(cipher, breaches) + check(cipher, breaches, defaultMatchDetection) .handleError { false } .bind() } @@ -1244,7 +1250,12 @@ sealed interface DFilter { directDI: DirectDI, ciphers: List, ) = kotlin.run { + val getAutofillDefaultMatchDetection = + directDI.instance() val cipherUrlDuplicateCheck = directDI.instance() + + val defaultMatchDetection = getAutofillDefaultMatchDetection() + .first() ciphers .filter { cipher -> if (shouldIgnore(cipher)) { @@ -1263,7 +1274,7 @@ sealed interface DFilter { val a = uris[i] val b = uris[j] - val duplicate = cipherUrlDuplicateCheck(a, b) + val duplicate = cipherUrlDuplicateCheck(a, b, defaultMatchDetection) .attempt() .bind() .isRight { it != null } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/DSecret.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/DSecret.kt index 88d361c7..9d4d25df 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/DSecret.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/DSecret.kt @@ -429,7 +429,8 @@ fun DSecret.Type.titleH() = when (this) { DSecret.Type.None -> Res.string.cipher_type_unknown } -fun DSecret.Uri.MatchType.titleH() = when (this) { +fun DSecret.Uri.MatchType?.titleH() = when (this) { + null -> Res.string.uri_match_detection_default_title DSecret.Uri.MatchType.Domain -> Res.string.uri_match_detection_domain_title DSecret.Uri.MatchType.Host -> Res.string.uri_match_detection_host_title DSecret.Uri.MatchType.StartsWith -> Res.string.uri_match_detection_startswith_title diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/MatchDetection.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/MatchDetection.kt new file mode 100644 index 00000000..0a159903 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/model/MatchDetection.kt @@ -0,0 +1,27 @@ +package com.artemchep.keyguard.common.model; + +enum class MatchDetection( + val matchType: DSecret.Uri.MatchType?, +) { + Default(null), + Domain(DSecret.Uri.MatchType.Domain), + Host(DSecret.Uri.MatchType.Host), + StartsWith(DSecret.Uri.MatchType.StartsWith), + Exact(DSecret.Uri.MatchType.Exact), + RegularExpression(DSecret.Uri.MatchType.RegularExpression), + Never(DSecret.Uri.MatchType.Never); + + companion object { + fun valueOf( + matchType: DSecret.Uri.MatchType?, + ): MatchDetection = when (matchType) { + null -> Default + DSecret.Uri.MatchType.Domain -> Domain + DSecret.Uri.MatchType.Host -> Host + DSecret.Uri.MatchType.StartsWith -> StartsWith + DSecret.Uri.MatchType.Exact -> Exact + DSecret.Uri.MatchType.RegularExpression -> RegularExpression + DSecret.Uri.MatchType.Never -> Never + } + } +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/settings/SettingsReadRepository.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/settings/SettingsReadRepository.kt index 13f81d84..936a2bb0 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/settings/SettingsReadRepository.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/settings/SettingsReadRepository.kt @@ -13,6 +13,8 @@ import kotlin.time.Duration * @author Artem Chepurnyi */ interface SettingsReadRepository { + fun getAutofillDefaultMatchDetection(): Flow + fun getAutofillInlineSuggestions(): Flow fun getAutofillManualSelection(): Flow diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/settings/SettingsReadWriteRepository.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/settings/SettingsReadWriteRepository.kt index 72326290..39b0d030 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/settings/SettingsReadWriteRepository.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/settings/SettingsReadWriteRepository.kt @@ -13,6 +13,10 @@ import kotlin.time.Duration * @author Artem Chepurnyi */ interface SettingsReadWriteRepository : SettingsReadRepository { + fun setAutofillDefaultMatchDetection( + matchDetection: String, + ): IO + fun setAutofillInlineSuggestions( inlineSuggestions: Boolean, ): IO diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/settings/impl/SettingsRepositoryImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/settings/impl/SettingsRepositoryImpl.kt index 4856477f..0a400811 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/settings/impl/SettingsRepositoryImpl.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/service/settings/impl/SettingsRepositoryImpl.kt @@ -36,6 +36,7 @@ class SettingsRepositoryImpl( private val json: Json, ) : SettingsReadWriteRepository { companion object { + private const val KEY_AUTOFILL_DEFAULT_MATCH_DETECTION = "autofill.default_match_detection" private const val KEY_AUTOFILL_INLINE_SUGGESTIONS = "autofill.inline_suggestions" private const val KEY_AUTOFILL_MANUAL_SELECTION = "autofill.manual_selection" private const val KEY_AUTOFILL_RESPECT_AUTOFILL_OFF = "autofill.respect_autofill_off" @@ -82,6 +83,9 @@ class SettingsRepositoryImpl( private const val NONE_DURATION = -1L } + private val autofillDefaultMatchDetectionPref = + store.getString(KEY_AUTOFILL_DEFAULT_MATCH_DETECTION, "") + private val autofillInlineSuggestionsPref = store.getBoolean(KEY_AUTOFILL_INLINE_SUGGESTIONS, true) @@ -223,6 +227,13 @@ class SettingsRepositoryImpl( json = directDI.instance(), ) + override fun setAutofillDefaultMatchDetection( + matchDetection: String, + ) = autofillDefaultMatchDetectionPref + .setAndCommit(matchDetection) + + override fun getAutofillDefaultMatchDetection() = autofillDefaultMatchDetectionPref + override fun setAutofillInlineSuggestions(inlineSuggestions: Boolean) = autofillInlineSuggestionsPref .setAndCommit(inlineSuggestions) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/CipherBreachCheck.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/CipherBreachCheck.kt index 36373fd6..7dec72b0 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/CipherBreachCheck.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/CipherBreachCheck.kt @@ -4,4 +4,8 @@ import com.artemchep.keyguard.common.io.IO import com.artemchep.keyguard.common.model.DSecret import com.artemchep.keyguard.provider.bitwarden.entity.HibpBreachGroup -interface CipherBreachCheck : (DSecret, HibpBreachGroup) -> IO +interface CipherBreachCheck : ( + DSecret, + HibpBreachGroup, + DSecret.Uri.MatchType, +) -> IO diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/CipherUrlCheck.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/CipherUrlCheck.kt index b49c2ad2..02d2dc10 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/CipherUrlCheck.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/CipherUrlCheck.kt @@ -3,4 +3,8 @@ package com.artemchep.keyguard.common.usecase import com.artemchep.keyguard.common.io.IO import com.artemchep.keyguard.common.model.DSecret -interface CipherUrlCheck : (DSecret.Uri, String) -> IO +interface CipherUrlCheck : ( + DSecret.Uri, + String, + DSecret.Uri.MatchType, +) -> IO diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/CipherUrlDuplicateCheck.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/CipherUrlDuplicateCheck.kt index ef418bfd..41940326 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/CipherUrlDuplicateCheck.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/CipherUrlDuplicateCheck.kt @@ -3,4 +3,8 @@ package com.artemchep.keyguard.common.usecase import com.artemchep.keyguard.common.io.IO import com.artemchep.keyguard.common.model.DSecret -interface CipherUrlDuplicateCheck : (DSecret.Uri, DSecret.Uri) -> IO +interface CipherUrlDuplicateCheck : ( + DSecret.Uri, + DSecret.Uri, + DSecret.Uri.MatchType, +) -> IO diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/GetAutofillDefaultMatchDetection.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/GetAutofillDefaultMatchDetection.kt new file mode 100644 index 00000000..73dcdcbb --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/GetAutofillDefaultMatchDetection.kt @@ -0,0 +1,6 @@ +package com.artemchep.keyguard.common.usecase + +import com.artemchep.keyguard.common.model.DSecret +import kotlinx.coroutines.flow.Flow + +interface GetAutofillDefaultMatchDetection : () -> Flow diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/PutAutofillDefaultMatchDetection.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/PutAutofillDefaultMatchDetection.kt new file mode 100644 index 00000000..30c9a16f --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/PutAutofillDefaultMatchDetection.kt @@ -0,0 +1,6 @@ +package com.artemchep.keyguard.common.usecase + +import com.artemchep.keyguard.common.io.IO +import com.artemchep.keyguard.common.model.DSecret + +interface PutAutofillDefaultMatchDetection : (DSecret.Uri.MatchType) -> IO diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/GetAutofillDefaultMatchDetectionImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/GetAutofillDefaultMatchDetectionImpl.kt new file mode 100644 index 00000000..92521ead --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/GetAutofillDefaultMatchDetectionImpl.kt @@ -0,0 +1,37 @@ +package com.artemchep.keyguard.common.usecase.impl + +import com.artemchep.keyguard.common.model.DSecret +import com.artemchep.keyguard.common.service.settings.SettingsReadRepository +import com.artemchep.keyguard.common.usecase.GetAutofillDefaultMatchDetection +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn +import org.kodein.di.DirectDI +import org.kodein.di.instance + +class GetAutofillDefaultMatchDetectionImpl( + settingsReadRepository: SettingsReadRepository, +) : GetAutofillDefaultMatchDetection { + private val sharedFlow = settingsReadRepository.getAutofillDefaultMatchDetection() + .map { key -> findOrDefault(key) } + .distinctUntilChanged() + .shareIn( + scope = GlobalScope, + started = SharingStarted.WhileSubscribed(5000L), + replay = 1, + ) + + constructor(directDI: DirectDI) : this( + settingsReadRepository = directDI.instance(), + ) + + override fun invoke() = sharedFlow + + private fun findOrDefault(key: String) = findOrNull(key) + ?: DSecret.Uri.MatchType.default + + private fun findOrNull(key: String) = DSecret.Uri.MatchType.entries + .firstOrNull { it.name.equals(key, ignoreCase = true) } +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/PutAutofillDefaultMatchDetectionImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/PutAutofillDefaultMatchDetectionImpl.kt new file mode 100644 index 00000000..ad7d0fbf --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/PutAutofillDefaultMatchDetectionImpl.kt @@ -0,0 +1,21 @@ +package com.artemchep.keyguard.common.usecase.impl + +import com.artemchep.keyguard.common.io.IO +import com.artemchep.keyguard.common.model.DSecret +import com.artemchep.keyguard.common.service.settings.SettingsReadWriteRepository +import com.artemchep.keyguard.common.usecase.PutAutofillDefaultMatchDetection +import org.kodein.di.DirectDI +import org.kodein.di.instance + +class PutAutofillDefaultMatchDetectionImpl( + private val settingsReadWriteRepository: SettingsReadWriteRepository, +) : PutAutofillDefaultMatchDetection { + constructor(directDI: DirectDI) : this( + settingsReadWriteRepository = directDI.instance(), + ) + + override fun invoke( + matchDetection: DSecret.Uri.MatchType, + ): IO = settingsReadWriteRepository + .setAutofillDefaultMatchDetection(matchDetection.name) +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/WatchtowerSyncerImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/WatchtowerSyncerImpl.kt index 557cc413..88527abe 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/WatchtowerSyncerImpl.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/common/usecase/impl/WatchtowerSyncerImpl.kt @@ -25,6 +25,7 @@ import com.artemchep.keyguard.common.usecase.CipherExpiringCheck import com.artemchep.keyguard.common.usecase.CipherIncompleteCheck import com.artemchep.keyguard.common.usecase.CipherUnsecureUrlCheck import com.artemchep.keyguard.common.usecase.CipherUrlDuplicateCheck +import com.artemchep.keyguard.common.usecase.GetAutofillDefaultMatchDetection import com.artemchep.keyguard.common.usecase.GetBreaches import com.artemchep.keyguard.common.usecase.GetCheckPasskeys import com.artemchep.keyguard.common.usecase.GetCheckPwnedPasswords @@ -53,6 +54,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf @@ -318,6 +320,7 @@ class WatchtowerPasswordPwned( } class WatchtowerWebsitePwned( + private val getAutofillDefaultMatchDetection: GetAutofillDefaultMatchDetection, private val cipherBreachCheck: CipherBreachCheck, private val getBreaches: GetBreaches, private val getCheckPwnedServices: GetCheckPwnedServices, @@ -326,6 +329,7 @@ class WatchtowerWebsitePwned( get() = DWatchtowerAlertType.PWNED_WEBSITE.value constructor(directDI: DirectDI) : this( + getAutofillDefaultMatchDetection = directDI.instance(), cipherBreachCheck = directDI.instance(), getBreaches = directDI.instance(), getCheckPwnedServices = directDI.instance(), @@ -333,6 +337,8 @@ class WatchtowerWebsitePwned( override fun version() = combineJoinToVersion( getDatabaseVersionFlow(), + getAutofillDefaultMatchDetection() + .map { it.name }, getCheckPwnedServices() .map { it.int.toString() @@ -349,8 +355,11 @@ class WatchtowerWebsitePwned( override suspend fun process( ciphers: List, ): List { + val defaultMatchDetection = getAutofillDefaultMatchDetection() + .first() val set = buildDuplicatesState( ciphers = ciphers, + defaultMatchDetection = defaultMatchDetection, ) return ciphers .map { cipher -> @@ -375,6 +384,7 @@ class WatchtowerWebsitePwned( private suspend fun buildDuplicatesState( ciphers: List, + defaultMatchDetection: DSecret.Uri.MatchType, ): Set = ioEffect { val breaches = getBreaches() .handleError { @@ -389,7 +399,7 @@ class WatchtowerWebsitePwned( return@filter false } - cipherBreachCheck(cipher, breaches) + cipherBreachCheck(cipher, breaches, defaultMatchDetection) .handleError { false } .bind() } @@ -766,24 +776,32 @@ class WatchtowerInactiveTfa( } class WatchtowerDuplicateUris( + private val getAutofillDefaultMatchDetection: GetAutofillDefaultMatchDetection, private val cipherUrlDuplicateCheck: CipherUrlDuplicateCheck, ) : WatchtowerClientTyped { override val type: Long get() = DWatchtowerAlertType.DUPLICATE_URIS.value constructor(directDI: DirectDI) : this( + getAutofillDefaultMatchDetection = directDI.instance(), cipherUrlDuplicateCheck = directDI.instance(), ) - override fun version() = flowOf("1") + override fun version() = combineJoinToVersion( + getAutofillDefaultMatchDetection() + .map { it.name }, + ) override suspend fun process( ciphers: List, ): List { + val defaultMatchDetection = getAutofillDefaultMatchDetection() + .first() return ciphers .map { cipher -> val value = hasAlert( cipher = cipher, + defaultMatchDetection = defaultMatchDetection, ) val threat = value != null WatchtowerClientResult( @@ -796,17 +814,19 @@ class WatchtowerDuplicateUris( private suspend fun hasAlert( cipher: DSecret, + defaultMatchDetection: DSecret.Uri.MatchType, ): String? { val shouldIgnore = shouldIgnore(cipher) if (shouldIgnore) { return null } - return match(cipher) + return match(cipher, defaultMatchDetection) } private suspend fun match( cipher: DSecret, + defaultMatchDetection: DSecret.Uri.MatchType, ) = kotlin.run { val uris = cipher.uris if (uris.isEmpty()) { @@ -822,7 +842,7 @@ class WatchtowerDuplicateUris( val a = uris[i] val b = uris[j] - val duplicate = cipherUrlDuplicateCheck(a, b) + val duplicate = cipherUrlDuplicateCheck(a, b, defaultMatchDetection) .attempt() .bind() .isRight { it != null } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/SettingListScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/SettingListScreen.kt index 6f5dc03a..6b3b9f71 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/SettingListScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/SettingListScreen.kt @@ -173,7 +173,7 @@ private val items = listOfNotNull( text = TextHolder.Res(Res.string.pref_item_autofill_text), icon = Icons.Outlined.AutoAwesome, route = AutofillSettingsRoute, - ).takeIf { CurrentPlatform.hasAutofill() }, + ).takeIf { CurrentPlatform.hasAutofill() || !isRelease }, SettingsItem( id = "security", title = TextHolder.Res(Res.string.pref_item_security_title), diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/SettingPaneContent.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/SettingPaneContent.kt index 2e874147..fad64ab1 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/SettingPaneContent.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/SettingPaneContent.kt @@ -27,6 +27,7 @@ import com.artemchep.keyguard.feature.home.settings.component.settingAboutTelegr import com.artemchep.keyguard.feature.home.settings.component.settingApkProvider import com.artemchep.keyguard.feature.home.settings.component.settingAppIconsProvider import com.artemchep.keyguard.feature.home.settings.component.settingAutofillCopyTotpProvider +import com.artemchep.keyguard.feature.home.settings.component.settingAutofillDefaultMatchDetectionProvider import com.artemchep.keyguard.feature.home.settings.component.settingAutofillInlineSuggestionsProvider import com.artemchep.keyguard.feature.home.settings.component.settingAutofillManualSelectionProvider import com.artemchep.keyguard.feature.home.settings.component.settingAutofillProvider @@ -117,6 +118,7 @@ object Setting { const val CREDENTIAL_PROVIDER = "credential_provider" const val AUTOFILL = "autofill" + const val AUTOFILL_DEFAULT_MATCH_DETECTION = "autofill_default_match_detection" const val AUTOFILL_INLINE_SUGGESTIONS = "autofill_inline_suggestions" const val AUTOFILL_MANUAL_SELECTION = "autofill_manual_selection" const val AUTOFILL_RESPECT_AUTOFILL_OFF = "autofill_respect_autofill_off" @@ -204,6 +206,7 @@ val LocalSettingItemArgs = staticCompositionLocalOf { val hub = mapOf SettingComponent>( Setting.CREDENTIAL_PROVIDER to ::settingCredentialProviderProvider, Setting.AUTOFILL to ::settingAutofillProvider, + Setting.AUTOFILL_DEFAULT_MATCH_DETECTION to ::settingAutofillDefaultMatchDetectionProvider, Setting.AUTOFILL_INLINE_SUGGESTIONS to ::settingAutofillInlineSuggestionsProvider, Setting.AUTOFILL_MANUAL_SELECTION to ::settingAutofillManualSelectionProvider, Setting.AUTOFILL_RESPECT_AUTOFILL_OFF to ::settingAutofillRespectAutofillOffProvider, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/autofill/AutofillSettingsScreen.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/autofill/AutofillSettingsScreen.kt index 92ae1fef..0f6737c0 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/autofill/AutofillSettingsScreen.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/autofill/AutofillSettingsScreen.kt @@ -29,6 +29,7 @@ fun AutofillSettingsScreen() { SettingPaneItem.Group( key = "general", list = persistentListOf( + SettingPaneItem.Item(Setting.AUTOFILL_DEFAULT_MATCH_DETECTION), SettingPaneItem.Item(Setting.AUTOFILL_INLINE_SUGGESTIONS), SettingPaneItem.Item(Setting.AUTOFILL_MANUAL_SELECTION), SettingPaneItem.Item(Setting.AUTOFILL_RESPECT_AUTOFILL_OFF), diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/component/SettingAutofillDefaultMatchDetection.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/component/SettingAutofillDefaultMatchDetection.kt new file mode 100644 index 00000000..e9780025 --- /dev/null +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/component/SettingAutofillDefaultMatchDetection.kt @@ -0,0 +1,98 @@ +package com.artemchep.keyguard.feature.home.settings.component + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Domain +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import com.artemchep.keyguard.common.io.launchIn +import com.artemchep.keyguard.common.model.DSecret +import com.artemchep.keyguard.common.model.titleH +import com.artemchep.keyguard.common.usecase.GetAutofillDefaultMatchDetection +import com.artemchep.keyguard.common.usecase.PutAutofillDefaultMatchDetection +import com.artemchep.keyguard.common.usecase.WindowCoroutineScope +import com.artemchep.keyguard.feature.localization.TextHolder +import com.artemchep.keyguard.res.Res +import com.artemchep.keyguard.res.* +import com.artemchep.keyguard.ui.FlatDropdown +import com.artemchep.keyguard.ui.FlatItemAction +import com.artemchep.keyguard.ui.FlatItemTextContent +import com.artemchep.keyguard.ui.icons.icon +import org.jetbrains.compose.resources.stringResource +import kotlinx.coroutines.flow.map +import org.jetbrains.compose.resources.StringResource +import org.kodein.di.DirectDI +import org.kodein.di.instance +import kotlin.collections.map + +fun settingAutofillDefaultMatchDetectionProvider( + directDI: DirectDI, +) = settingAutofillDefaultMatchDetectionProvider( + getAutofillDefaultMatchDetection = directDI.instance(), + putAutofillDefaultMatchDetection = directDI.instance(), + windowCoroutineScope = directDI.instance(), +) + +fun settingAutofillDefaultMatchDetectionProvider( + getAutofillDefaultMatchDetection: GetAutofillDefaultMatchDetection, + putAutofillDefaultMatchDetection: PutAutofillDefaultMatchDetection, + windowCoroutineScope: WindowCoroutineScope, +): SettingComponent = getAutofillDefaultMatchDetection().map { matchDetection -> + val text = matchDetection.titleH() + val variants = DSecret.Uri.MatchType.entries + val dropdown = variants + .map { entry -> + val actionSelected = entry == matchDetection + val actionTitleRes = entry.titleH() + FlatItemAction( + title = TextHolder.Res(actionTitleRes), + selected = actionSelected, + onClick = { + putAutofillDefaultMatchDetection(entry) + .launchIn(windowCoroutineScope) + }, + ) + } + + SettingIi( + search = SettingIi.Search( + group = "autofill", + tokens = listOf( + "autofill", + "match", + "detection", + "uri", + ), + ), + ) { + SettingAutofillDefaultMatchDetection( + text = text, + dropdown = dropdown, + ) + } +} + +@Composable +private fun SettingAutofillDefaultMatchDetection( + text: StringResource, + dropdown: List, +) { + FlatDropdown( + leading = icon(Icons.Outlined.Domain), + content = { + FlatItemTextContent( + title = { + Text( + text = stringResource(Res.string.pref_item_autofill_default_match_detection_title), + ) + }, + text = { + Text( + text = stringResource(text), + ) + }, + ) + }, + dropdown = dropdown, + ) +} diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddStateProducer.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddStateProducer.kt index 6551807e..451d7d92 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddStateProducer.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/add/AddStateProducer.kt @@ -36,6 +36,7 @@ import com.artemchep.keyguard.common.model.DSecret import com.artemchep.keyguard.common.model.KeyPair import com.artemchep.keyguard.common.model.KeyPairDecor import com.artemchep.keyguard.common.model.Loadable +import com.artemchep.keyguard.common.model.MatchDetection import com.artemchep.keyguard.common.model.ToastMessage import com.artemchep.keyguard.common.model.TotpToken import com.artemchep.keyguard.common.model.UsernameVariation2 @@ -81,6 +82,7 @@ import com.artemchep.keyguard.common.usecase.AddCipher import com.artemchep.keyguard.common.usecase.CipherUnsecureUrlCheck import com.artemchep.keyguard.common.usecase.CopyText import com.artemchep.keyguard.common.usecase.GetAccounts +import com.artemchep.keyguard.common.usecase.GetAutofillDefaultMatchDetection import com.artemchep.keyguard.common.usecase.GetCiphers import com.artemchep.keyguard.common.usecase.GetCollections import com.artemchep.keyguard.common.usecase.GetFolders @@ -198,6 +200,7 @@ fun produceAddScreenState( logRepository = instance(), clipboardService = instance(), otpMigrationService = instance(), + getAutofillDefaultMatchDetection = instance(), cipherUnsecureUrlCheck = instance(), showMessage = instance(), addCipher = instance(), @@ -228,6 +231,7 @@ fun produceAddScreenState( logRepository: LogRepository, clipboardService: ClipboardService, otpMigrationService: OtpMigrationService, + getAutofillDefaultMatchDetection: GetAutofillDefaultMatchDetection, cipherUnsecureUrlCheck: CipherUnsecureUrlCheck, showMessage: ShowMessage, addCipher: AddCipher, @@ -363,6 +367,7 @@ fun produceAddScreenState( val urisFactories = kotlin.run { val uriFactory = AddStateItemUriFactory( + getAutofillDefaultMatchDetection = getAutofillDefaultMatchDetection, cipherUnsecureUrlCheck = cipherUnsecureUrlCheck, ) listOf( @@ -948,6 +953,7 @@ class AddStateItemAttachmentFactory : Foo2Factory, DS } class AddStateItemUriFactory( + private val getAutofillDefaultMatchDetection: GetAutofillDefaultMatchDetection, private val cipherUnsecureUrlCheck: CipherUnsecureUrlCheck, ) : Foo2Factory, DSecret.Uri> { override val type: String = "uri" @@ -967,8 +973,8 @@ class AddStateItemUriFactory( } val uriMutableState = asComposeState(uriKey) - val matchTypeSink = mutablePersistedFlow("$key.match_type") { - initial?.match ?: DSecret.Uri.MatchType.default + val matchDetectionSink = mutablePersistedFlow("$key.match_detection") { + MatchDetection.valueOf(initial?.match) } val actionsAppPickerItem = FlatItemAction( @@ -981,7 +987,7 @@ class AddStateItemUriFactory( val route = registerRouteResultReceiver(AppPickerRoute) { result -> if (result is AppPickerResult.Confirm) { uriMutableState.value = result.uri - matchTypeSink.value = DSecret.Uri.MatchType.default + matchDetectionSink.value = MatchDetection.Default } } val intent = NavigationIntent.NavigateToRoute( @@ -992,16 +998,16 @@ class AddStateItemUriFactory( ) // Add a ability to change the // match type via an option. - val actionsMatchTypeItemFlow = matchTypeSink + val actionsMatchTypeItemFlow = matchDetectionSink .map { selectedMatchType -> FlatItemAction( leading = icon(Icons.Stub), title = Res.string.uri_match_detection_title.wrap(), - text = selectedMatchType.titleH().wrap(), + text = selectedMatchType.matchType.titleH().wrap(), onClick = onClick { - val items = DSecret.Uri.MatchType.entries + val items = MatchDetection.entries .map { type -> - val typeTitle = translate(type.titleH()) + val typeTitle = translate(type.matchType.titleH()) ConfirmationRoute.Args.Item.EnumItem.Item( key = type.name, title = typeTitle, @@ -1013,27 +1019,31 @@ class AddStateItemUriFactory( value = selectedMatchType.name, items = items, docs = mapOf( - DSecret.Uri.MatchType.Domain.name to ConfirmationRoute.Args.Item.EnumItem.Doc( + MatchDetection.Default.name to ConfirmationRoute.Args.Item.EnumItem.Doc( + text = translate(Res.string.uri_match_detection_default_note), + url = "https://bitwarden.com/help/uri-match-detection/#default-match-detection", + ), + MatchDetection.Domain.name to ConfirmationRoute.Args.Item.EnumItem.Doc( text = translate(Res.string.uri_match_detection_domain_note), url = "https://bitwarden.com/help/uri-match-detection/#base-domain", ), - DSecret.Uri.MatchType.Host.name to ConfirmationRoute.Args.Item.EnumItem.Doc( + MatchDetection.Host.name to ConfirmationRoute.Args.Item.EnumItem.Doc( text = translate(Res.string.uri_match_detection_host_note), url = "https://bitwarden.com/help/uri-match-detection/#host", ), - DSecret.Uri.MatchType.StartsWith.name to ConfirmationRoute.Args.Item.EnumItem.Doc( + MatchDetection.StartsWith.name to ConfirmationRoute.Args.Item.EnumItem.Doc( text = translate(Res.string.uri_match_detection_startswith_note), url = "https://bitwarden.com/help/uri-match-detection/#starts-with", ), - DSecret.Uri.MatchType.Exact.name to ConfirmationRoute.Args.Item.EnumItem.Doc( + MatchDetection.Exact.name to ConfirmationRoute.Args.Item.EnumItem.Doc( text = translate(Res.string.uri_match_detection_exact_note), url = "https://bitwarden.com/help/uri-match-detection/#regular-expression", ), - DSecret.Uri.MatchType.RegularExpression.name to ConfirmationRoute.Args.Item.EnumItem.Doc( + MatchDetection.RegularExpression.name to ConfirmationRoute.Args.Item.EnumItem.Doc( text = translate(Res.string.uri_match_detection_regex_note), url = "https://bitwarden.com/help/uri-match-detection/#regular-expression", ), - DSecret.Uri.MatchType.Never.name to ConfirmationRoute.Args.Item.EnumItem.Doc( + MatchDetection.Never.name to ConfirmationRoute.Args.Item.EnumItem.Doc( text = translate(Res.string.uri_match_detection_never_note), url = "https://bitwarden.com/help/uri-match-detection/#exact", ), @@ -1041,8 +1051,7 @@ class AddStateItemUriFactory( ), title = translate(Res.string.uri_match_detection_title), ) { newMatchTypeKey -> - matchTypeSink.value = - DSecret.Uri.MatchType.valueOf(newMatchTypeKey) + matchDetectionSink.value = MatchDetection.valueOf(newMatchTypeKey) } navigate(intent) }, @@ -1056,6 +1065,7 @@ class AddStateItemUriFactory( } } + val defaultMatchDetection = getAutofillDefaultMatchDetection() val textFlow = uriSink .map { uri -> TextFieldModel2( @@ -1066,10 +1076,12 @@ class AddStateItemUriFactory( } val stateFlow = combine( textFlow, - matchTypeSink, + matchDetectionSink, + defaultMatchDetection, actionsFlow, - ) { text, matchType, actions -> - val badge = when (matchType) { + ) { text, matchDetection, defaultMatchDetection, actions -> + val matchType = matchDetection.matchType + val badge = when (matchType ?: defaultMatchDetection) { DSecret.Uri.MatchType.Never, DSecret.Uri.MatchType.Host, DSecret.Uri.MatchType.Domain, @@ -1110,7 +1122,6 @@ class AddStateItemUriFactory( text = text.copy(vl = badge), matchType = matchType, matchTypeTitle = matchType - .takeIf { it != DSecret.Uri.MatchType.default } ?.let { val name = translate(it.titleH()) name @@ -1122,7 +1133,7 @@ class AddStateItemUriFactory( started = SharingStarted.WhileSubscribed(), initialValue = AddStateItem.Url.State( text = TextFieldModel2.empty, - matchType = DSecret.Uri.MatchType.default, + matchType = null, ), ) return AddStateItem.Url( diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultViewStateProducer.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultViewStateProducer.kt index f17313de..873d0ff2 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultViewStateProducer.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultViewStateProducer.kt @@ -2172,7 +2172,6 @@ private suspend fun RememberStateFlowScope.createUriItem( val uri = holder.uri val matchTypeTitle = holder.uri.match - .takeUnless { it == DSecret.Uri.MatchType.default } ?.titleH() ?.let { translate(it) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/view/SendViewStateProducer.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/view/SendViewStateProducer.kt index 8d6b9a0b..16cdac14 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/view/SendViewStateProducer.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/send/view/SendViewStateProducer.kt @@ -14,11 +14,8 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.unit.dp -import com.artemchep.keyguard.android.downloader.journal.room.DownloadInfoEntity2 import com.artemchep.keyguard.common.io.attempt import com.artemchep.keyguard.common.io.bind -import com.artemchep.keyguard.common.io.ioRaise -import com.artemchep.keyguard.common.io.ioUnit import com.artemchep.keyguard.common.model.BarcodeImageFormat import com.artemchep.keyguard.common.model.DAccount import com.artemchep.keyguard.common.model.DSecret @@ -28,20 +25,9 @@ import com.artemchep.keyguard.common.service.clipboard.ClipboardService import com.artemchep.keyguard.common.service.download.DownloadManager import com.artemchep.keyguard.common.service.extract.LinkInfoExtractor import com.artemchep.keyguard.common.service.twofa.TwoFaService -import com.artemchep.keyguard.common.usecase.AddCipherOpenedHistory -import com.artemchep.keyguard.common.usecase.ChangeCipherNameById -import com.artemchep.keyguard.common.usecase.ChangeCipherPasswordById -import com.artemchep.keyguard.common.usecase.CipherExpiringCheck -import com.artemchep.keyguard.common.usecase.CipherFieldSwitchToggle -import com.artemchep.keyguard.common.usecase.CipherIncompleteCheck -import com.artemchep.keyguard.common.usecase.CipherUnsecureUrlAutoFix -import com.artemchep.keyguard.common.usecase.CipherUnsecureUrlCheck -import com.artemchep.keyguard.common.usecase.CipherUrlCheck -import com.artemchep.keyguard.common.usecase.CopyCipherById import com.artemchep.keyguard.common.usecase.CopyText import com.artemchep.keyguard.common.usecase.DateFormatter import com.artemchep.keyguard.common.usecase.DownloadAttachment -import com.artemchep.keyguard.common.usecase.FavouriteCipherById import com.artemchep.keyguard.common.usecase.GetAccounts import com.artemchep.keyguard.common.usecase.GetAppIcons import com.artemchep.keyguard.common.usecase.GetCanWrite @@ -55,22 +41,13 @@ import com.artemchep.keyguard.common.usecase.GetOrganizations import com.artemchep.keyguard.common.usecase.GetPasswordStrength import com.artemchep.keyguard.common.usecase.GetSends import com.artemchep.keyguard.common.usecase.GetWebsiteIcons -import com.artemchep.keyguard.common.usecase.MoveCipherToFolderById -import com.artemchep.keyguard.common.usecase.PatchSendById -import com.artemchep.keyguard.common.usecase.RemoveAttachment -import com.artemchep.keyguard.common.usecase.RemoveCipherById -import com.artemchep.keyguard.common.usecase.RemoveSendById -import com.artemchep.keyguard.common.usecase.RestoreCipherById import com.artemchep.keyguard.common.usecase.RetryCipher import com.artemchep.keyguard.common.usecase.SendToolbox -import com.artemchep.keyguard.common.usecase.TrashCipherById import com.artemchep.keyguard.common.usecase.WindowCoroutineScope -import com.artemchep.keyguard.feature.attachments.model.AttachmentItem import com.artemchep.keyguard.feature.attachments.util.createAttachmentItem import com.artemchep.keyguard.feature.barcodetype.BarcodeTypeRoute import com.artemchep.keyguard.feature.favicon.FaviconImage import com.artemchep.keyguard.feature.favicon.FaviconUrl -import com.artemchep.keyguard.feature.filepicker.humanReadableByteCountSI import com.artemchep.keyguard.feature.home.vault.model.VaultViewItem import com.artemchep.keyguard.feature.largetype.LargeTypeRoute import com.artemchep.keyguard.feature.localization.TextHolder @@ -92,13 +69,9 @@ import com.artemchep.keyguard.ui.buildContextItems import com.artemchep.keyguard.ui.icons.ChevronIcon import com.artemchep.keyguard.ui.icons.IconBox import com.artemchep.keyguard.ui.icons.KeyguardView -import com.artemchep.keyguard.ui.selection.SelectionHandle -import com.artemchep.keyguard.ui.selection.selectionHandle import com.artemchep.keyguard.ui.text.annotate import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser import io.ktor.http.Url -import kotlinx.collections.immutable.ImmutableList -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first @@ -133,16 +106,11 @@ fun sendViewScreenState( toolbox = instance(), downloadManager = instance(), downloadAttachment = instance(), - removeAttachment = instance(), - cipherExpiringCheck = instance(), - cipherIncompleteCheck = instance(), - cipherUrlCheck = instance(), tfaService = instance(), clipboardService = instance(), getGravatarUrl = instance(), getEnvSendUrl = instance(), dateFormatter = instance(), - addCipherOpenedHistory = instance(), windowCoroutineScope = instance(), linkInfoExtractors = allInstances(), contentColor = contentColor, @@ -176,16 +144,11 @@ fun sendViewScreenState( toolbox: SendToolbox, downloadManager: DownloadManager, downloadAttachment: DownloadAttachment, - removeAttachment: RemoveAttachment, - cipherExpiringCheck: CipherExpiringCheck, - cipherIncompleteCheck: CipherIncompleteCheck, - cipherUrlCheck: CipherUrlCheck, tfaService: TwoFaService, clipboardService: ClipboardService, getGravatarUrl: GetGravatarUrl, getEnvSendUrl: GetEnvSendUrl, dateFormatter: DateFormatter, - addCipherOpenedHistory: AddCipherOpenedHistory, windowCoroutineScope: WindowCoroutineScope, linkInfoExtractors: List>, sendId: String, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipher.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipher.kt index 6be829b2..55aadc2c 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipher.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipher.kt @@ -526,18 +526,15 @@ private suspend fun BitwardenCipher.Login.Companion.of( return@mapNotNull null } - val match = uri.match - .takeUnless { it == DSecret.Uri.MatchType.default } - ?.let { - when (it) { - DSecret.Uri.MatchType.Domain -> BitwardenCipher.Login.Uri.MatchType.Domain - DSecret.Uri.MatchType.Host -> BitwardenCipher.Login.Uri.MatchType.Host - DSecret.Uri.MatchType.StartsWith -> BitwardenCipher.Login.Uri.MatchType.StartsWith - DSecret.Uri.MatchType.Exact -> BitwardenCipher.Login.Uri.MatchType.Exact - DSecret.Uri.MatchType.RegularExpression -> BitwardenCipher.Login.Uri.MatchType.RegularExpression - DSecret.Uri.MatchType.Never -> BitwardenCipher.Login.Uri.MatchType.Never - } - } + val match = when (uri.match) { + null -> null + DSecret.Uri.MatchType.Domain -> BitwardenCipher.Login.Uri.MatchType.Domain + DSecret.Uri.MatchType.Host -> BitwardenCipher.Login.Uri.MatchType.Host + DSecret.Uri.MatchType.StartsWith -> BitwardenCipher.Login.Uri.MatchType.StartsWith + DSecret.Uri.MatchType.Exact -> BitwardenCipher.Login.Uri.MatchType.Exact + DSecret.Uri.MatchType.RegularExpression -> BitwardenCipher.Login.Uri.MatchType.RegularExpression + DSecret.Uri.MatchType.Never -> BitwardenCipher.Login.Uri.MatchType.Never + } BitwardenCipher.Login.Uri( uri = uri.uri, match = match, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipherUri.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipherUri.kt index 2b3096d1..170c9a9f 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipherUri.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipherUri.kt @@ -12,11 +12,13 @@ import com.artemchep.keyguard.common.model.AddUriCipherRequest import com.artemchep.keyguard.common.model.DSecret import com.artemchep.keyguard.common.usecase.AddUriCipher import com.artemchep.keyguard.common.usecase.CipherUrlDuplicateCheck +import com.artemchep.keyguard.common.usecase.GetAutofillDefaultMatchDetection import com.artemchep.keyguard.common.usecase.isEmpty import com.artemchep.keyguard.common.util.Browsers import com.artemchep.keyguard.core.store.bitwarden.BitwardenCipher import com.artemchep.keyguard.provider.bitwarden.mapper.toDomain import com.artemchep.keyguard.provider.bitwarden.usecase.util.ModifyCipherById +import kotlinx.coroutines.flow.first import org.kodein.di.DirectDI import org.kodein.di.instance @@ -25,6 +27,7 @@ import org.kodein.di.instance */ class AddUriCipherImpl( private val modifyCipherById: ModifyCipherById, + private val getAutofillDefaultMatchDetection: GetAutofillDefaultMatchDetection, private val cipherUrlDuplicateCheck: CipherUrlDuplicateCheck, ) : AddUriCipher { companion object { @@ -33,6 +36,7 @@ class AddUriCipherImpl( constructor(directDI: DirectDI) : this( modifyCipherById = directDI.instance(), + getAutofillDefaultMatchDetection = directDI.instance(), cipherUrlDuplicateCheck = directDI.instance(), ) @@ -43,6 +47,8 @@ class AddUriCipherImpl( return@ioEffect io(false) } + val defaultMatchDetection = getAutofillDefaultMatchDetection() + .first() modifyCipherById( setOf(request.cipherId), ) { model -> @@ -71,7 +77,11 @@ class AddUriCipherImpl( val newUriDomain = newUri.toDomain() oldUrisDomain .none { oldUriDomain -> - val isDuplicate = cipherUrlDuplicateCheck(oldUriDomain, newUriDomain) + val isDuplicate = cipherUrlDuplicateCheck( + oldUriDomain, + newUriDomain, + defaultMatchDetection, + ) .attempt() .bind() .isRight { it != null } @@ -122,7 +132,6 @@ fun List.autofill( if (!androidAppUriExists) { existingUris += DSecret.Uri( uri = androidAppUri, - match = DSecret.Uri.MatchType.default, ) } } @@ -135,7 +144,6 @@ fun List.autofill( val webUri = "$webPrefix$webDomain" existingUris += DSecret.Uri( uri = webUri, - match = DSecret.Uri.MatchType.default, ) } } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/CipherBreachCheck.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/CipherBreachCheck.kt index 81fd859e..f2c0ec45 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/CipherBreachCheck.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/CipherBreachCheck.kt @@ -50,6 +50,7 @@ class CipherBreachCheckImpl( override fun invoke( cipher: DSecret, breaches: HibpBreachGroup, + defaultMatchDetection: DSecret.Uri.MatchType, ): IO = ioEffect { val login = cipher.login ?: return@ioEffect false @@ -78,7 +79,7 @@ class CipherBreachCheckImpl( ?.takeIf { it.isNotBlank() } ?: return@filter false cipher.uris.any { uri -> - cipherUrlCheck(uri, domain) + cipherUrlCheck(uri, domain, defaultMatchDetection) .bind() } } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/CipherUrlCheck.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/CipherUrlCheck.kt index 81663372..653588bc 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/CipherUrlCheck.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/CipherUrlCheck.kt @@ -31,8 +31,9 @@ class CipherUrlCheckImpl( override fun invoke( uri: DSecret.Uri, url: String, + defaultMatchDetection: DSecret.Uri.MatchType, ): IO { - return when (uri.match ?: DSecret.Uri.MatchType.default) { + return when (uri.match ?: defaultMatchDetection) { DSecret.Uri.MatchType.Domain -> { when { uri.uri.startsWith(PROTOCOL_ANDROID_APP) -> ::checkUrlMatchByHost diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/CipherUrlDuplicateCheck.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/CipherUrlDuplicateCheck.kt index cdc904b8..ba4de45e 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/CipherUrlDuplicateCheck.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/CipherUrlDuplicateCheck.kt @@ -22,9 +22,10 @@ class CipherUrlDuplicateCheckImpl( override fun invoke( a: DSecret.Uri, b: DSecret.Uri, + defaultMatchDetection: DSecret.Uri.MatchType, ): IO { - val aMatch = a.match ?: DSecret.Uri.MatchType.default - val bMatch = b.match ?: DSecret.Uri.MatchType.default + val aMatch = a.match ?: defaultMatchDetection + val bMatch = b.match ?: defaultMatchDetection // If one of the URIs are set to never match then // ignore it, it can not be a duplicate. @@ -45,7 +46,7 @@ class CipherUrlDuplicateCheckImpl( return io(result) } - return cipherUrlCheck(a, b.uri) + return cipherUrlCheck(a, b.uri, defaultMatchDetection) .effectMap { areMatching -> a.takeIf { areMatching } } diff --git a/common/src/jvmMain/kotlin/com/artemchep/keyguard/di/GlobalModuleJvm.kt b/common/src/jvmMain/kotlin/com/artemchep/keyguard/di/GlobalModuleJvm.kt index 06e3a9c3..36a63e4d 100644 --- a/common/src/jvmMain/kotlin/com/artemchep/keyguard/di/GlobalModuleJvm.kt +++ b/common/src/jvmMain/kotlin/com/artemchep/keyguard/di/GlobalModuleJvm.kt @@ -108,6 +108,7 @@ import com.artemchep.keyguard.common.usecase.GetAppVersion import com.artemchep.keyguard.common.usecase.GetAppVersionCode import com.artemchep.keyguard.common.usecase.GetAppVersionName import com.artemchep.keyguard.common.usecase.GetAutofillCopyTotp +import com.artemchep.keyguard.common.usecase.GetAutofillDefaultMatchDetection import com.artemchep.keyguard.common.usecase.GetAutofillInlineSuggestions import com.artemchep.keyguard.common.usecase.GetAutofillManualSelection import com.artemchep.keyguard.common.usecase.GetAutofillRespectAutofillOff @@ -177,6 +178,7 @@ import com.artemchep.keyguard.common.usecase.PutAllowTwoPanelLayoutInLandscape import com.artemchep.keyguard.common.usecase.PutAllowTwoPanelLayoutInPortrait import com.artemchep.keyguard.common.usecase.PutAppIcons import com.artemchep.keyguard.common.usecase.PutAutofillCopyTotp +import com.artemchep.keyguard.common.usecase.PutAutofillDefaultMatchDetection import com.artemchep.keyguard.common.usecase.PutAutofillInlineSuggestions import com.artemchep.keyguard.common.usecase.PutAutofillManualSelection import com.artemchep.keyguard.common.usecase.PutAutofillRespectAutofillOff @@ -242,6 +244,7 @@ import com.artemchep.keyguard.common.usecase.impl.GetAppVersionCodeImpl import com.artemchep.keyguard.common.usecase.impl.GetAppVersionImpl import com.artemchep.keyguard.common.usecase.impl.GetAppVersionNameImpl import com.artemchep.keyguard.common.usecase.impl.GetAutofillCopyTotpImpl +import com.artemchep.keyguard.common.usecase.impl.GetAutofillDefaultMatchDetectionImpl import com.artemchep.keyguard.common.usecase.impl.GetAutofillInlineSuggestionsImpl import com.artemchep.keyguard.common.usecase.impl.GetAutofillManualSelectionImpl import com.artemchep.keyguard.common.usecase.impl.GetAutofillRespectAutofillOffImpl @@ -311,6 +314,7 @@ import com.artemchep.keyguard.common.usecase.impl.PutAllowTwoPanelLayoutInLandsc import com.artemchep.keyguard.common.usecase.impl.PutAllowTwoPanelLayoutInPortraitImpl import com.artemchep.keyguard.common.usecase.impl.PutAppIconsImpl import com.artemchep.keyguard.common.usecase.impl.PutAutofillCopyTotpImpl +import com.artemchep.keyguard.common.usecase.impl.PutAutofillDefaultMatchDetectionImpl import com.artemchep.keyguard.common.usecase.impl.PutAutofillInlineSuggestionsImpl import com.artemchep.keyguard.common.usecase.impl.PutAutofillManualSelectionImpl import com.artemchep.keyguard.common.usecase.impl.PutAutofillRespectAutofillOffImpl @@ -550,6 +554,11 @@ fun globalModuleJvm() = DI.Module( directDI = this, ) } + bindSingleton { + GetAutofillDefaultMatchDetectionImpl( + directDI = this, + ) + } bindSingleton { GetAutofillInlineSuggestionsImpl( directDI = this, @@ -738,6 +747,11 @@ fun globalModuleJvm() = DI.Module( directDI = this, ) } + bindSingleton { + PutAutofillDefaultMatchDetectionImpl( + directDI = this, + ) + } bindSingleton { PutAutofillInlineSuggestionsImpl( directDI = this,