feat: Default match detection #708
This commit is contained in:
parent
1927b066d8
commit
bdd23cb988
@ -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<LinkInfoExtractor<LinkInfoPlatform.Android, LinkInfoAndroid>>,
|
||||
private val getAutofillDefaultMatchDetection: GetAutofillDefaultMatchDetection,
|
||||
private val cipherUrlCheck: CipherUrlCheck,
|
||||
) : GetSuggestions<Any?> {
|
||||
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<String>,
|
||||
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
|
||||
|
@ -351,6 +351,8 @@
|
||||
<string name="uri_unsecure">Unsecure</string>
|
||||
<string name="uri_match_app_title">Match app</string>
|
||||
<string name="uri_match_detection_title">Match detection</string>
|
||||
<string name="uri_match_detection_default_title">Default</string>
|
||||
<string name="uri_match_detection_default_note">Uses a global Default match detection behavior selected in Settings → Autofill → Default URI match detection. Base domain matching is the default option.</string>
|
||||
<string name="uri_match_detection_domain_title">Base domain</string>
|
||||
<string name="uri_match_detection_domain_note">Match resources by top-level and second-level domain.</string>
|
||||
<string name="uri_match_detection_host_title">Host</string>
|
||||
@ -1117,6 +1119,7 @@
|
||||
3. Confirm and restart Chrome</string>
|
||||
<string name="pref_item_autofill_auto_copy_otp_title">Auto-copy one-time passwords</string>
|
||||
<string name="pref_item_autofill_auto_copy_otp_text">When filling a login information, automatically copy one-time passwords</string>
|
||||
<string name="pref_item_autofill_default_match_detection_title">Default match detection</string>
|
||||
<string name="pref_item_autofill_inline_suggestions_title">Inline suggestions</string>
|
||||
<string name="pref_item_autofill_inline_suggestions_text">Embeds autofill suggestions directly into compatible keyboards</string>
|
||||
<string name="pref_item_autofill_manual_selection_title">Manual selection</string>
|
||||
|
@ -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<DSecret>,
|
||||
): Set<String> = ioEffect {
|
||||
val getAutofillDefaultMatchDetection =
|
||||
directDI.instance<GetAutofillDefaultMatchDetection>()
|
||||
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<DSecret>,
|
||||
) = kotlin.run {
|
||||
val getAutofillDefaultMatchDetection =
|
||||
directDI.instance<GetAutofillDefaultMatchDetection>()
|
||||
val cipherUrlDuplicateCheck = directDI.instance<CipherUrlDuplicateCheck>()
|
||||
|
||||
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 }
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,8 @@ import kotlin.time.Duration
|
||||
* @author Artem Chepurnyi
|
||||
*/
|
||||
interface SettingsReadRepository {
|
||||
fun getAutofillDefaultMatchDetection(): Flow<String>
|
||||
|
||||
fun getAutofillInlineSuggestions(): Flow<Boolean>
|
||||
|
||||
fun getAutofillManualSelection(): Flow<Boolean>
|
||||
|
@ -13,6 +13,10 @@ import kotlin.time.Duration
|
||||
* @author Artem Chepurnyi
|
||||
*/
|
||||
interface SettingsReadWriteRepository : SettingsReadRepository {
|
||||
fun setAutofillDefaultMatchDetection(
|
||||
matchDetection: String,
|
||||
): IO<Unit>
|
||||
|
||||
fun setAutofillInlineSuggestions(
|
||||
inlineSuggestions: Boolean,
|
||||
): IO<Unit>
|
||||
|
@ -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)
|
||||
|
@ -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<Boolean>
|
||||
interface CipherBreachCheck : (
|
||||
DSecret,
|
||||
HibpBreachGroup,
|
||||
DSecret.Uri.MatchType,
|
||||
) -> IO<Boolean>
|
||||
|
@ -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<Boolean>
|
||||
interface CipherUrlCheck : (
|
||||
DSecret.Uri,
|
||||
String,
|
||||
DSecret.Uri.MatchType,
|
||||
) -> IO<Boolean>
|
||||
|
@ -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<DSecret.Uri?>
|
||||
interface CipherUrlDuplicateCheck : (
|
||||
DSecret.Uri,
|
||||
DSecret.Uri,
|
||||
DSecret.Uri.MatchType,
|
||||
) -> IO<DSecret.Uri?>
|
||||
|
@ -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<DSecret.Uri.MatchType>
|
@ -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<Unit>
|
@ -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) }
|
||||
}
|
@ -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<Unit> = settingsReadWriteRepository
|
||||
.setAutofillDefaultMatchDetection(matchDetection.name)
|
||||
}
|
@ -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<DSecret>,
|
||||
): List<WatchtowerClientResult> {
|
||||
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<DSecret>,
|
||||
defaultMatchDetection: DSecret.Uri.MatchType,
|
||||
): Set<String> = 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<DSecret>,
|
||||
): List<WatchtowerClientResult> {
|
||||
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 }
|
||||
|
@ -173,7 +173,7 @@ private val items = listOfNotNull<SettingsItem2>(
|
||||
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),
|
||||
|
@ -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<Any?> {
|
||||
val hub = mapOf<String, (DirectDI) -> 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,
|
||||
|
@ -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),
|
||||
|
@ -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<FlatItemAction>,
|
||||
) {
|
||||
FlatDropdown(
|
||||
leading = icon<RowScope>(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,
|
||||
)
|
||||
}
|
@ -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<AddStateItem.Attachment<*>, DS
|
||||
}
|
||||
|
||||
class AddStateItemUriFactory(
|
||||
private val getAutofillDefaultMatchDetection: GetAutofillDefaultMatchDetection,
|
||||
private val cipherUnsecureUrlCheck: CipherUnsecureUrlCheck,
|
||||
) : Foo2Factory<AddStateItem.Url<*>, DSecret.Uri> {
|
||||
override val type: String = "uri"
|
||||
@ -967,8 +973,8 @@ class AddStateItemUriFactory(
|
||||
}
|
||||
val uriMutableState = asComposeState<String>(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<CreateRequest>(
|
||||
|
@ -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)
|
||||
|
@ -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<LinkInfoExtractor<LinkInfo, LinkInfo>>,
|
||||
sendId: String,
|
||||
|
@ -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,
|
||||
|
@ -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<DSecret.Uri>.autofill(
|
||||
if (!androidAppUriExists) {
|
||||
existingUris += DSecret.Uri(
|
||||
uri = androidAppUri,
|
||||
match = DSecret.Uri.MatchType.default,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -135,7 +144,6 @@ fun List<DSecret.Uri>.autofill(
|
||||
val webUri = "$webPrefix$webDomain"
|
||||
existingUris += DSecret.Uri(
|
||||
uri = webUri,
|
||||
match = DSecret.Uri.MatchType.default,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ class CipherBreachCheckImpl(
|
||||
override fun invoke(
|
||||
cipher: DSecret,
|
||||
breaches: HibpBreachGroup,
|
||||
defaultMatchDetection: DSecret.Uri.MatchType,
|
||||
): IO<Boolean> = 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()
|
||||
}
|
||||
}
|
||||
|
@ -31,8 +31,9 @@ class CipherUrlCheckImpl(
|
||||
override fun invoke(
|
||||
uri: DSecret.Uri,
|
||||
url: String,
|
||||
defaultMatchDetection: DSecret.Uri.MatchType,
|
||||
): IO<Boolean> {
|
||||
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
|
||||
|
@ -22,9 +22,10 @@ class CipherUrlDuplicateCheckImpl(
|
||||
override fun invoke(
|
||||
a: DSecret.Uri,
|
||||
b: DSecret.Uri,
|
||||
defaultMatchDetection: DSecret.Uri.MatchType,
|
||||
): IO<DSecret.Uri?> {
|
||||
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 }
|
||||
}
|
||||
|
@ -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<GetAutofillDefaultMatchDetection> {
|
||||
GetAutofillDefaultMatchDetectionImpl(
|
||||
directDI = this,
|
||||
)
|
||||
}
|
||||
bindSingleton<GetAutofillInlineSuggestions> {
|
||||
GetAutofillInlineSuggestionsImpl(
|
||||
directDI = this,
|
||||
@ -738,6 +747,11 @@ fun globalModuleJvm() = DI.Module(
|
||||
directDI = this,
|
||||
)
|
||||
}
|
||||
bindSingleton<PutAutofillDefaultMatchDetection> {
|
||||
PutAutofillDefaultMatchDetectionImpl(
|
||||
directDI = this,
|
||||
)
|
||||
}
|
||||
bindSingleton<PutAutofillInlineSuggestions> {
|
||||
PutAutofillInlineSuggestionsImpl(
|
||||
directDI = this,
|
||||
|
Loading…
x
Reference in New Issue
Block a user