mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
PM-11486: Parse the Accessibility Nodes for username and password fields (#3935)
This commit is contained in:
parent
f89b053d2e
commit
2068948035
14 changed files with 1376 additions and 19 deletions
|
@ -9,6 +9,8 @@ import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityComp
|
|||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityNodeInfoManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityNodeInfoManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNameManager
|
||||
|
@ -58,7 +60,16 @@ object AccessibilityModule {
|
|||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAccessibilityParser(): AccessibilityParser = AccessibilityParserImpl()
|
||||
fun providesAccessibilityNodeInfoManager(): AccessibilityNodeInfoManager =
|
||||
AccessibilityNodeInfoManagerImpl()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAccessibilityParser(
|
||||
accessibilityNodeInfoManager: AccessibilityNodeInfoManager,
|
||||
): AccessibilityParser = AccessibilityParserImpl(
|
||||
accessibilityNodeInfoManager = accessibilityNodeInfoManager,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import android.net.Uri
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
|
||||
/**
|
||||
* The default maximum recursive depth that the
|
||||
* [AccessibilityNodeInfoManager.findAccessibilityNodeInfoList] will go.
|
||||
*/
|
||||
const val DEFAULT_MAX_RECURSION_DEPTH: Int = 100
|
||||
|
||||
/**
|
||||
* A manager for finding fields that match particular characteristics.
|
||||
*/
|
||||
interface AccessibilityNodeInfoManager {
|
||||
/**
|
||||
* A helper function for retrieving the appropriate nodes based on the given [predicate].
|
||||
*
|
||||
* This function is recursive but will stop recurring if the depth it reaches is greater than
|
||||
* the [maxRecursionDepth].
|
||||
*/
|
||||
fun findAccessibilityNodeInfoList(
|
||||
rootNode: AccessibilityNodeInfo,
|
||||
maxRecursionDepth: Int = DEFAULT_MAX_RECURSION_DEPTH,
|
||||
predicate: (AccessibilityNodeInfo) -> Boolean,
|
||||
): List<AccessibilityNodeInfo>
|
||||
|
||||
/**
|
||||
* Determines which [AccessibilityNodeInfo] is a username field.
|
||||
*/
|
||||
fun findUsernameAccessibilityNodeInfo(
|
||||
uri: Uri,
|
||||
allNodes: List<AccessibilityNodeInfo>,
|
||||
passwordNodes: List<AccessibilityNodeInfo>,
|
||||
): AccessibilityNodeInfo?
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.getKnownUsernameFieldNull
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.isUsername
|
||||
|
||||
private const val MAX_NODE_COUNT: Int = 100
|
||||
|
||||
/**
|
||||
* The default implementation for the [AccessibilityNodeInfoManager].
|
||||
*/
|
||||
class AccessibilityNodeInfoManagerImpl : AccessibilityNodeInfoManager {
|
||||
override fun findAccessibilityNodeInfoList(
|
||||
rootNode: AccessibilityNodeInfo,
|
||||
maxRecursionDepth: Int,
|
||||
predicate: (AccessibilityNodeInfo) -> Boolean,
|
||||
): List<AccessibilityNodeInfo> =
|
||||
findAccessibilityNodeInfoList(
|
||||
rootNode = rootNode,
|
||||
maxRecursionDepth = maxRecursionDepth,
|
||||
currentRecursionDepth = 0,
|
||||
predicate = predicate,
|
||||
)
|
||||
|
||||
override fun findUsernameAccessibilityNodeInfo(
|
||||
uri: Uri,
|
||||
allNodes: List<AccessibilityNodeInfo>,
|
||||
passwordNodes: List<AccessibilityNodeInfo>,
|
||||
): AccessibilityNodeInfo? {
|
||||
val uriPath = uri
|
||||
.path
|
||||
?: return findMissingUsernameNodeInfo(
|
||||
allNodes = allNodes,
|
||||
passwordNodes = passwordNodes,
|
||||
)
|
||||
return uri
|
||||
.authority
|
||||
?.removePrefix(prefix = "www.")
|
||||
?.getKnownUsernameFieldNull()
|
||||
?.let { usernameField ->
|
||||
allNodes.firstOrNull { node ->
|
||||
node.isUsername(
|
||||
uriPath = uriPath,
|
||||
knownUsernameField = usernameField,
|
||||
)
|
||||
}
|
||||
}
|
||||
?: findMissingUsernameNodeInfo(allNodes = allNodes, passwordNodes = passwordNodes)
|
||||
}
|
||||
|
||||
private fun findAccessibilityNodeInfoList(
|
||||
rootNode: AccessibilityNodeInfo,
|
||||
maxRecursionDepth: Int,
|
||||
currentRecursionDepth: Int,
|
||||
predicate: (AccessibilityNodeInfo) -> Boolean,
|
||||
): List<AccessibilityNodeInfo> {
|
||||
if (predicate(rootNode)) return listOf(rootNode)
|
||||
if (currentRecursionDepth >= maxRecursionDepth) return emptyList()
|
||||
val childNodeCount = rootNode.childCount - 1
|
||||
if (childNodeCount > MAX_NODE_COUNT) log(message = "Too many child iterations.")
|
||||
return (0..childNodeCount.coerceAtMost(maximumValue = MAX_NODE_COUNT)).flatMap {
|
||||
val childNode = rootNode.getChild(it) ?: return@flatMap emptyList()
|
||||
if (childNode.hashCode() == this.hashCode()) {
|
||||
log(message = "Child node is the same as parent for some reason.")
|
||||
emptyList()
|
||||
} else {
|
||||
findAccessibilityNodeInfoList(
|
||||
rootNode = childNode,
|
||||
maxRecursionDepth = maxRecursionDepth,
|
||||
currentRecursionDepth = currentRecursionDepth + 1,
|
||||
predicate = predicate,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to find a username [AccessibilityNodeInfo] if there isn't one already. This
|
||||
* functions by finding the first known password node and taking the node directly above it.
|
||||
*/
|
||||
private fun findMissingUsernameNodeInfo(
|
||||
allNodes: List<AccessibilityNodeInfo>,
|
||||
passwordNodes: List<AccessibilityNodeInfo>,
|
||||
): AccessibilityNodeInfo? =
|
||||
passwordNodes
|
||||
.firstOrNull()
|
||||
?.let { allNodes.getOrNull(index = allNodes.indexOf(element = it) - 1) }
|
||||
|
||||
private fun log(message: String) {
|
||||
if (!BuildConfig.DEBUG) return
|
||||
Log.i("AccessibilityNodeInfoManager", message)
|
||||
}
|
||||
}
|
|
@ -6,6 +6,6 @@ import android.view.accessibility.AccessibilityNodeInfo
|
|||
* Represents the fillable fields for accessibility based autofill.
|
||||
*/
|
||||
data class FillableFields(
|
||||
val usernameFields: List<AccessibilityNodeInfo>,
|
||||
val usernameField: AccessibilityNodeInfo?,
|
||||
val passwordFields: List<AccessibilityNodeInfo>,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package com.x8bit.bitwarden.data.autofill.accessibility.model
|
||||
|
||||
/**
|
||||
* Represents the known username fields for a given [uriAuthority].
|
||||
*/
|
||||
data class KnownUsernameField(
|
||||
val uriAuthority: String,
|
||||
val accessOptions: List<AccessOptions>,
|
||||
) {
|
||||
constructor(
|
||||
uriAuthority: String,
|
||||
accessOption: AccessOptions,
|
||||
) : this(uriAuthority = uriAuthority, accessOptions = listOf(accessOption))
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the view IDs for a given uri path.
|
||||
*/
|
||||
data class AccessOptions(
|
||||
val matchValue: String,
|
||||
val matchingStrategy: MatchingStrategy = MatchingStrategy.ENDS_WITH_CASE_SENSITIVE,
|
||||
val usernameViewIds: List<String>,
|
||||
) {
|
||||
constructor(
|
||||
matchValue: String,
|
||||
matchingStrategy: MatchingStrategy = MatchingStrategy.ENDS_WITH_CASE_SENSITIVE,
|
||||
usernameViewId: String,
|
||||
) : this(
|
||||
matchValue = matchValue,
|
||||
matchingStrategy = matchingStrategy,
|
||||
usernameViewIds = listOf(usernameViewId),
|
||||
)
|
||||
|
||||
/**
|
||||
* Indicates the matching strategy needed for the particular [AccessOptions].
|
||||
*/
|
||||
enum class MatchingStrategy(
|
||||
val matches: (uriPath: String, matchValue: String) -> Boolean,
|
||||
) {
|
||||
CONTAINS_CASE_INSENSITIVE(
|
||||
matches = { uriPath, matchValue ->
|
||||
uriPath.contains(other = matchValue, ignoreCase = true)
|
||||
},
|
||||
),
|
||||
CONTAINS_CASE_SENSITIVE(
|
||||
matches = { uriPath, matchValue ->
|
||||
uriPath.contains(other = matchValue, ignoreCase = false)
|
||||
},
|
||||
),
|
||||
ENDS_WITH_CASE_INSENSITIVE(
|
||||
matches = { uriPath, matchValue ->
|
||||
uriPath.endsWith(suffix = matchValue, ignoreCase = true)
|
||||
},
|
||||
),
|
||||
ENDS_WITH_CASE_SENSITIVE(
|
||||
matches = { uriPath, matchValue ->
|
||||
uriPath.endsWith(suffix = matchValue, ignoreCase = false)
|
||||
},
|
||||
),
|
||||
STARTS_WITH_CASE_INSENSITIVE(
|
||||
matches = { uriPath, matchValue ->
|
||||
uriPath.startsWith(prefix = matchValue, ignoreCase = true)
|
||||
},
|
||||
),
|
||||
STARTS_WITH_CASE_SENSITIVE(
|
||||
matches = { uriPath, matchValue ->
|
||||
uriPath.startsWith(prefix = matchValue, ignoreCase = false)
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ interface AccessibilityParser {
|
|||
/**
|
||||
* Parses the fillable fields from [rootNode].
|
||||
*/
|
||||
fun parseForFillableFields(rootNode: AccessibilityNodeInfo): FillableFields
|
||||
fun parseForFillableFields(rootNode: AccessibilityNodeInfo, uri: Uri): FillableFields
|
||||
|
||||
/**
|
||||
* Parses the [Uri] from [rootNode] and returns a url, package name.
|
||||
|
|
|
@ -3,20 +3,35 @@ package com.x8bit.bitwarden.data.autofill.accessibility.parser
|
|||
import android.net.Uri
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityNodeInfoManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.FillableFields
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.getSupportedBrowserOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.isEditText
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.toUriOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.hasHttpProtocol
|
||||
|
||||
/**
|
||||
* The default implementation for the [AccessibilityParser].
|
||||
*/
|
||||
class AccessibilityParserImpl : AccessibilityParser {
|
||||
override fun parseForFillableFields(rootNode: AccessibilityNodeInfo): FillableFields {
|
||||
// TODO: Parse for username and password fields (PM-11486)
|
||||
class AccessibilityParserImpl(
|
||||
private val accessibilityNodeInfoManager: AccessibilityNodeInfoManager,
|
||||
) : AccessibilityParser {
|
||||
override fun parseForFillableFields(
|
||||
rootNode: AccessibilityNodeInfo,
|
||||
uri: Uri,
|
||||
): FillableFields {
|
||||
val nodes = accessibilityNodeInfoManager
|
||||
.findAccessibilityNodeInfoList(rootNode = rootNode) {
|
||||
it.isEditText || it.isPassword
|
||||
}
|
||||
val passwordNodes = nodes.filter { it.isPassword }
|
||||
return FillableFields(
|
||||
usernameFields = listOf(),
|
||||
passwordFields = listOf(),
|
||||
usernameField = accessibilityNodeInfoManager.findUsernameAccessibilityNodeInfo(
|
||||
uri = uri,
|
||||
allNodes = nodes,
|
||||
passwordNodes = passwordNodes,
|
||||
),
|
||||
passwordFields = passwordNodes,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -75,10 +75,11 @@ class BitwardenAccessibilityProcessorImpl(
|
|||
attemptFill: AccessibilityAction.AttemptFill,
|
||||
) {
|
||||
val loginView = attemptFill.cipherView.login ?: return
|
||||
val fields = accessibilityParser.parseForFillableFields(rootNode = rootNode)
|
||||
fields.usernameFields.forEach { usernameField ->
|
||||
usernameField.fillTextField(value = loginView.username)
|
||||
}
|
||||
val fields = accessibilityParser.parseForFillableFields(
|
||||
rootNode = rootNode,
|
||||
uri = attemptFill.uri,
|
||||
)
|
||||
fields.usernameField?.fillTextField(value = loginView.username)
|
||||
fields.passwordFields.forEach { passwordField ->
|
||||
passwordField.fillTextField(value = loginView.password)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package com.x8bit.bitwarden.data.autofill.accessibility.util
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import android.widget.EditText
|
||||
import androidx.core.os.bundleOf
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.KnownUsernameField
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
|
||||
private const val PACKAGE_NAME_BITWARDEN_PREFIX: String = "com.x8bit.bitwarden"
|
||||
|
@ -50,3 +52,36 @@ fun AccessibilityNodeInfo.fillTextField(value: String?) {
|
|||
bundleOf(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE to value),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the [AccessibilityNodeInfo] is an instance of an EditText.
|
||||
*/
|
||||
val AccessibilityNodeInfo.isEditText: Boolean
|
||||
get() = className
|
||||
?.let {
|
||||
try {
|
||||
Class.forName(it.toString())
|
||||
} catch (e: ClassNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
?.let { EditText::class.java.isAssignableFrom(it) }
|
||||
?: (className?.contains(other = "EditText") == true)
|
||||
|
||||
/**
|
||||
* Determines if the [AccessibilityNodeInfo] is a username field.
|
||||
*/
|
||||
fun AccessibilityNodeInfo.isUsername(
|
||||
knownUsernameField: KnownUsernameField,
|
||||
uriPath: String,
|
||||
): Boolean {
|
||||
knownUsernameField.accessOptions.map { options ->
|
||||
if (options.matchingStrategy.matches(uriPath, options.matchValue)) {
|
||||
options
|
||||
.usernameViewIds
|
||||
.firstOrNull { viewId -> viewId == this@isUsername.viewIdResourceName }
|
||||
?.let { return true }
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -0,0 +1,672 @@
|
|||
package com.x8bit.bitwarden.data.autofill.accessibility.util
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessOptions
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.KnownUsernameField
|
||||
|
||||
/**
|
||||
* Determines if the [String] receiver is a uri authority for a known username field and returns
|
||||
* that [KnownUsernameField] if it is a match.
|
||||
*/
|
||||
fun String.getKnownUsernameFieldNull(): KnownUsernameField? =
|
||||
LEGACY_KNOWN_USERNAME_FIELDS.find { it.uriAuthority == this@getKnownUsernameFieldNull }
|
||||
|
||||
/**
|
||||
* A list of known username fields and their IDs.
|
||||
*/
|
||||
private val LEGACY_KNOWN_USERNAME_FIELDS: List<KnownUsernameField> = listOf(
|
||||
// SECTION A ——— World-renowned web sites/applications
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.ae",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.ca",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.cn",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.co.jp",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.co.uk",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.com",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.com.au",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.com.br",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.com.mx",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.com.tr",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.de",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.es",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.fr",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.in",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.it",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.nl",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.pl",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.sa",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.se",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.sg",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.aws.amazon.com",
|
||||
accessOption = AccessOptions(matchValue = "signin", usernameViewId = "resolving_input"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "id.atlassian.com",
|
||||
accessOption = AccessOptions(matchValue = "login", usernameViewId = "username"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "bitly.com",
|
||||
accessOption = AccessOptions(matchValue = "/sso/url_slug", usernameViewId = "url_slug"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.befr.ebay.be",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.benl.ebay.be",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.cafr.ebay.ca",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.at",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.be",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.ca",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.ch",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.co.uk",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.com",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.com.au",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.com.hk",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.com.my",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.com.sg",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.de",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.es",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.fr",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.ie",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.in",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.it",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.nl",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.ph",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.pl",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "accounts.google.com",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(matchValue = "identifier", usernameViewId = "identifierId"),
|
||||
AccessOptions(matchValue = "ServiceLogin", usernameViewId = "Email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "paypal.com",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(matchValue = "signin", usernameViewId = "email"),
|
||||
AccessOptions(
|
||||
matchValue = "/connect/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewId = "email",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "tumblr.com",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "login",
|
||||
usernameViewId = "signup_determine_email",
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.az",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.by",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.co.il",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.com",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.com.am",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.com.ge",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.com.tr",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.ee",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.fi",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.fr",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.kg",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.kz",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.lt",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.lv",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.md",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.pl",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.ru",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.tj",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.tm",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.ua",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.uz",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
// SECTION B ——— Top 100 worldwide
|
||||
// As of July 2020, all entries that needed to be added from
|
||||
// Top 100 (SimilarWeb, 2019) and Top 50 (Alexa Internet, 2020)
|
||||
// matched section A.
|
||||
// Therefore, no entry currently.
|
||||
|
||||
// SECTION C ——— Top 20 for selected countries
|
||||
// For these selected countries, the Top 20 (SimilarWeb, 2020)
|
||||
// and the Top 20 (Alexa Internet, 2020) are covered.
|
||||
// Mobile and desktop versions supported.
|
||||
// Could not be added, however:
|
||||
// web sites/applications that don't use an "id" attribute for their login field.
|
||||
|
||||
KnownUsernameField(
|
||||
uriAuthority = "cfg.smt.docomo.ne.jp",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/auth/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewId = "Di_Uid",
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "id.smt.docomo.ne.jp",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/cgi7/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewId = "Di_Uid",
|
||||
),
|
||||
),
|
||||
// SECTION D ——— Miscellaneous
|
||||
// No entry, currently.
|
||||
|
||||
// SECTION Z ——— Special forms
|
||||
// Despite "user ID + password" fields both visible, detection rules required.
|
||||
// No entry, currently.
|
||||
|
||||
// Test/example purposes only
|
||||
// GitHub is a VERY special case (signup form, just to test the proper functioning
|
||||
// of special forms).
|
||||
KnownUsernameField(
|
||||
uriAuthority = "github.com",
|
||||
accessOption = AccessOptions(matchValue = "", usernameViewId = "user[login]-footer"),
|
||||
),
|
||||
)
|
|
@ -0,0 +1,216 @@
|
|||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import android.net.Uri
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.isUsername
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AccessibilityNodeInfoManagerTest {
|
||||
|
||||
private val accessibilityNodeInfoManager: AccessibilityNodeInfoManager =
|
||||
AccessibilityNodeInfoManagerImpl()
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(AccessibilityNodeInfo::isUsername)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(AccessibilityNodeInfo::isUsername)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `findAccessibilityNodeInfoList with the node matching the predicate should return that node`() {
|
||||
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo>()
|
||||
|
||||
val result = accessibilityNodeInfoManager.findAccessibilityNodeInfoList(
|
||||
rootNode = accessibilityNodeInfo,
|
||||
maxRecursionDepth = 100,
|
||||
predicate = { true },
|
||||
)
|
||||
|
||||
assertEquals(listOf(accessibilityNodeInfo), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `findAccessibilityNodeInfoList with a high recursion depth should return an empty list`() {
|
||||
// This node will always returns itself, so it should recur until it hits the max depth
|
||||
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
|
||||
every { childCount } returns 1
|
||||
every { getChild(any()) } returns this
|
||||
}
|
||||
|
||||
val result = accessibilityNodeInfoManager.findAccessibilityNodeInfoList(
|
||||
rootNode = accessibilityNodeInfo,
|
||||
maxRecursionDepth = 100,
|
||||
predicate = { false },
|
||||
)
|
||||
|
||||
assertEquals(emptyList<AccessibilityNodeInfo>(), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `findAccessibilityNodeInfoList where child node is null should return an empty list`() {
|
||||
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
|
||||
every { childCount } returns 1
|
||||
every { getChild(any()) } returns null
|
||||
}
|
||||
|
||||
val result = accessibilityNodeInfoManager.findAccessibilityNodeInfoList(
|
||||
rootNode = accessibilityNodeInfo,
|
||||
maxRecursionDepth = 100,
|
||||
predicate = { false },
|
||||
)
|
||||
|
||||
assertEquals(emptyList<AccessibilityNodeInfo>(), result)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `findAccessibilityNodeInfoList with child nodes should return list of matching child nodes`() {
|
||||
val childChildAccessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
|
||||
every { isPassword } returns true
|
||||
}
|
||||
val childAccessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
|
||||
every { isPassword } returns false
|
||||
every { childCount } returns 3
|
||||
every { getChild(any()) } returns childChildAccessibilityNodeInfo
|
||||
}
|
||||
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
|
||||
every { isPassword } returns false
|
||||
every { childCount } returns 3
|
||||
every { getChild(any()) } returns childAccessibilityNodeInfo
|
||||
}
|
||||
|
||||
val result = accessibilityNodeInfoManager.findAccessibilityNodeInfoList(
|
||||
rootNode = accessibilityNodeInfo,
|
||||
maxRecursionDepth = 100,
|
||||
predicate = { it.isPassword },
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
childChildAccessibilityNodeInfo,
|
||||
childChildAccessibilityNodeInfo,
|
||||
childChildAccessibilityNodeInfo,
|
||||
childChildAccessibilityNodeInfo,
|
||||
childChildAccessibilityNodeInfo,
|
||||
childChildAccessibilityNodeInfo,
|
||||
childChildAccessibilityNodeInfo,
|
||||
childChildAccessibilityNodeInfo,
|
||||
childChildAccessibilityNodeInfo,
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `findUsernameAccessibilityNodeInfo with null uri path and no password fields should return null`() {
|
||||
val uri: Uri = mockk {
|
||||
every { path } returns null
|
||||
}
|
||||
|
||||
val result = accessibilityNodeInfoManager.findUsernameAccessibilityNodeInfo(
|
||||
uri = uri,
|
||||
allNodes = emptyList(),
|
||||
passwordNodes = emptyList(),
|
||||
)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `findUsernameAccessibilityNodeInfo with null uri path and a password field should return field above it`() {
|
||||
val uri: Uri = mockk {
|
||||
every { path } returns null
|
||||
}
|
||||
val usernameField = mockk<AccessibilityNodeInfo>()
|
||||
val passwordField = mockk<AccessibilityNodeInfo>()
|
||||
|
||||
val result = accessibilityNodeInfoManager.findUsernameAccessibilityNodeInfo(
|
||||
uri = uri,
|
||||
allNodes = listOf(usernameField, passwordField),
|
||||
passwordNodes = listOf(passwordField),
|
||||
)
|
||||
|
||||
assertEquals(usernameField, result)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `findUsernameAccessibilityNodeInfo with null uri authority and no possible username field should return null`() {
|
||||
val uri: Uri = mockk {
|
||||
every { path } returns "amazon/qa"
|
||||
every { authority } returns null
|
||||
}
|
||||
val passwordField = mockk<AccessibilityNodeInfo>()
|
||||
|
||||
val result = accessibilityNodeInfoManager.findUsernameAccessibilityNodeInfo(
|
||||
uri = uri,
|
||||
allNodes = listOf(passwordField),
|
||||
passwordNodes = listOf(passwordField),
|
||||
)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `findUsernameAccessibilityNodeInfo with known username field but no matches should return null`() {
|
||||
val uriPath = "/ap/signin"
|
||||
val uri: Uri = mockk {
|
||||
every { path } returns uriPath
|
||||
every { authority } returns "www.amazon.com"
|
||||
}
|
||||
val usernameField = mockk<AccessibilityNodeInfo> {
|
||||
every { isUsername(knownUsernameField = any(), uriPath = uriPath) } returns false
|
||||
}
|
||||
val passwordField = mockk<AccessibilityNodeInfo> {
|
||||
every { isUsername(knownUsernameField = any(), uriPath = uriPath) } returns false
|
||||
}
|
||||
|
||||
val result = accessibilityNodeInfoManager.findUsernameAccessibilityNodeInfo(
|
||||
uri = uri,
|
||||
allNodes = listOf(passwordField, usernameField),
|
||||
passwordNodes = listOf(passwordField),
|
||||
)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `findUsernameAccessibilityNodeInfo with known username field should return correct field`() {
|
||||
val uriPath = "/ap/signin"
|
||||
val uri: Uri = mockk {
|
||||
every { path } returns uriPath
|
||||
every { authority } returns "amazon.com"
|
||||
}
|
||||
val usernameField = mockk<AccessibilityNodeInfo> {
|
||||
every { isUsername(knownUsernameField = any(), uriPath = uriPath) } returns true
|
||||
}
|
||||
val passwordField = mockk<AccessibilityNodeInfo> {
|
||||
every { isUsername(knownUsernameField = any(), uriPath = uriPath) } returns false
|
||||
}
|
||||
|
||||
val result = accessibilityNodeInfoManager.findUsernameAccessibilityNodeInfo(
|
||||
uri = uri,
|
||||
allNodes = listOf(passwordField, usernameField),
|
||||
passwordNodes = listOf(passwordField),
|
||||
)
|
||||
|
||||
assertEquals(usernameField, result)
|
||||
}
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
package com.x8bit.bitwarden.data.autofill.accessibility.parser
|
||||
|
||||
import android.net.Uri
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityNodeInfoManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.Browser
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.FillableFields
|
||||
import io.mockk.every
|
||||
|
@ -12,17 +14,112 @@ import org.junit.jupiter.api.Test
|
|||
|
||||
class AccessibilityParserTest {
|
||||
|
||||
private val accessibilityParser: AccessibilityParser = AccessibilityParserImpl()
|
||||
private val accessibilityNodeInfoManager: AccessibilityNodeInfoManager = mockk()
|
||||
|
||||
private val accessibilityParser: AccessibilityParser = AccessibilityParserImpl(
|
||||
accessibilityNodeInfoManager = accessibilityNodeInfoManager,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `parseForFillableFields should return empty data`() {
|
||||
fun `parseForFillableFields with no accessibility nodes should return empty data`() {
|
||||
val uri = mockk<Uri>()
|
||||
val rootNode = mockk<AccessibilityNodeInfo>()
|
||||
every {
|
||||
accessibilityNodeInfoManager.findAccessibilityNodeInfoList(
|
||||
rootNode = rootNode,
|
||||
predicate = any(),
|
||||
)
|
||||
} returns emptyList()
|
||||
every {
|
||||
accessibilityNodeInfoManager.findUsernameAccessibilityNodeInfo(
|
||||
uri = uri,
|
||||
allNodes = emptyList(),
|
||||
passwordNodes = emptyList(),
|
||||
)
|
||||
} returns null
|
||||
val expectedResult = FillableFields(
|
||||
usernameFields = emptyList(),
|
||||
usernameField = null,
|
||||
passwordFields = emptyList(),
|
||||
)
|
||||
|
||||
val result = accessibilityParser.parseForFillableFields(rootNode = rootNode)
|
||||
val result = accessibilityParser.parseForFillableFields(
|
||||
rootNode = rootNode,
|
||||
uri = uri,
|
||||
)
|
||||
|
||||
assertEquals(expectedResult, result)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `parseForFillableFields with password fields should return no username field and all password fields`() {
|
||||
val uri = mockk<Uri>()
|
||||
val rootNode = mockk<AccessibilityNodeInfo>()
|
||||
val passwordField = mockk<AccessibilityNodeInfo> {
|
||||
every { isPassword } returns true
|
||||
}
|
||||
val allFields = listOf(passwordField, passwordField)
|
||||
every {
|
||||
accessibilityNodeInfoManager.findAccessibilityNodeInfoList(
|
||||
rootNode = rootNode,
|
||||
predicate = any(),
|
||||
)
|
||||
} returns allFields
|
||||
every {
|
||||
accessibilityNodeInfoManager.findUsernameAccessibilityNodeInfo(
|
||||
uri = uri,
|
||||
allNodes = allFields,
|
||||
passwordNodes = allFields,
|
||||
)
|
||||
} returns null
|
||||
val expectedResult = FillableFields(
|
||||
usernameField = null,
|
||||
passwordFields = allFields,
|
||||
)
|
||||
|
||||
val result = accessibilityParser.parseForFillableFields(
|
||||
rootNode = rootNode,
|
||||
uri = uri,
|
||||
)
|
||||
|
||||
assertEquals(expectedResult, result)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `parseForFillableFields with password fields and username field should return username field and all password fields`() {
|
||||
val uri = mockk<Uri>()
|
||||
val rootNode = mockk<AccessibilityNodeInfo>()
|
||||
val usernameField = mockk<AccessibilityNodeInfo> {
|
||||
every { isPassword } returns false
|
||||
}
|
||||
val passwordField = mockk<AccessibilityNodeInfo> {
|
||||
every { isPassword } returns true
|
||||
}
|
||||
val allFields = listOf(usernameField, passwordField, passwordField)
|
||||
val passwordFields = listOf(passwordField, passwordField)
|
||||
every {
|
||||
accessibilityNodeInfoManager.findAccessibilityNodeInfoList(
|
||||
rootNode = rootNode,
|
||||
predicate = any(),
|
||||
)
|
||||
} returns allFields
|
||||
every {
|
||||
accessibilityNodeInfoManager.findUsernameAccessibilityNodeInfo(
|
||||
uri = uri,
|
||||
allNodes = allFields,
|
||||
passwordNodes = passwordFields,
|
||||
)
|
||||
} returns usernameField
|
||||
val expectedResult = FillableFields(
|
||||
usernameField = usernameField,
|
||||
passwordFields = passwordFields,
|
||||
)
|
||||
|
||||
val result = accessibilityParser.parseForFillableFields(
|
||||
rootNode = rootNode,
|
||||
uri = uri,
|
||||
)
|
||||
|
||||
assertEquals(expectedResult, result)
|
||||
}
|
||||
|
|
|
@ -279,7 +279,7 @@ class BitwardenAccessibilityProcessorTest {
|
|||
every { fillTextField(testPassword) } just runs
|
||||
}
|
||||
val fillableFields = FillableFields(
|
||||
usernameFields = listOf(mockUsernameField),
|
||||
usernameField = mockUsernameField,
|
||||
passwordFields = listOf(mockPasswordField),
|
||||
)
|
||||
val uri = mockk<Uri>()
|
||||
|
@ -293,7 +293,7 @@ class BitwardenAccessibilityProcessorTest {
|
|||
every { accessibilityAutofillManager.accessibilityAction } returns attemptFill
|
||||
every { accessibilityAutofillManager.accessibilityAction = null } just runs
|
||||
every {
|
||||
accessibilityParser.parseForFillableFields(rootNode = rootNode)
|
||||
accessibilityParser.parseForFillableFields(rootNode = rootNode, uri = uri)
|
||||
} returns fillableFields
|
||||
|
||||
bitwardenAccessibilityProcessor.processAccessibilityEvent(
|
||||
|
@ -307,7 +307,7 @@ class BitwardenAccessibilityProcessorTest {
|
|||
accessibilityAutofillManager.accessibilityAction
|
||||
accessibilityAutofillManager.accessibilityAction = null
|
||||
cipherView.login
|
||||
accessibilityParser.parseForFillableFields(rootNode = rootNode)
|
||||
accessibilityParser.parseForFillableFields(rootNode = rootNode, uri = uri)
|
||||
mockUsernameField.fillTextField(testUsername)
|
||||
mockPasswordField.fillTextField(testPassword)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package com.x8bit.bitwarden.data.autofill.accessibility.util
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessOptions
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.KnownUsernameField
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
|
@ -9,6 +11,100 @@ import org.junit.jupiter.api.Test
|
|||
|
||||
class AccessibilityNodeInfoExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `isUsername without uri match should return false`() {
|
||||
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo>()
|
||||
|
||||
val result = accessibilityNodeInfo.isUsername(
|
||||
knownUsernameField = MOCK_KNOWN_USERNAME_FIELD,
|
||||
uriPath = "",
|
||||
)
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isUsername with uri match and no matching viewId should return false`() {
|
||||
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
|
||||
every { viewIdResourceName } returns ""
|
||||
}
|
||||
|
||||
val result = accessibilityNodeInfo.isUsername(
|
||||
knownUsernameField = MOCK_KNOWN_USERNAME_FIELD,
|
||||
uriPath = MOCK_MATCH_VALUE,
|
||||
)
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isUsername with uri match and matching viewId should return true`() {
|
||||
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
|
||||
every { viewIdResourceName } returns MOCK_USERNAME_VIEW_ID
|
||||
}
|
||||
|
||||
val result = accessibilityNodeInfo.isUsername(
|
||||
knownUsernameField = MOCK_KNOWN_USERNAME_FIELD,
|
||||
uriPath = MOCK_MATCH_VALUE,
|
||||
)
|
||||
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isEditText when className is null should return false`() {
|
||||
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
|
||||
every { className } returns null
|
||||
}
|
||||
|
||||
assertFalse(accessibilityNodeInfo.isEditText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isEditText when className does not contain 'EditText' should return false`() {
|
||||
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
|
||||
every { className } returns "TextView"
|
||||
}
|
||||
|
||||
assertFalse(accessibilityNodeInfo.isEditText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isEditText when className is an EditText should return true`() {
|
||||
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
|
||||
every { className } returns "android.widget.EditText"
|
||||
}
|
||||
|
||||
assertTrue(accessibilityNodeInfo.isEditText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isEditText when className is assignable to 'EditText' should return true`() {
|
||||
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
|
||||
every { className } returns "android.widget.AutoCompleteTextView"
|
||||
}
|
||||
|
||||
assertTrue(accessibilityNodeInfo.isEditText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isEditText when className does contains 'EditText' should return true`() {
|
||||
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
|
||||
every { className } returns "com.EditText"
|
||||
}
|
||||
|
||||
assertTrue(accessibilityNodeInfo.isEditText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isEditText when className is exactly 'EditText' should return true`() {
|
||||
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
|
||||
every { className } returns "EditText"
|
||||
}
|
||||
|
||||
assertTrue(accessibilityNodeInfo.isEditText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shouldSkipPackage when packageName is null should return true`() {
|
||||
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
|
||||
|
@ -73,3 +169,14 @@ class AccessibilityNodeInfoExtensionsTest {
|
|||
assertFalse(accessibilityNodeInfo.shouldSkipPackage)
|
||||
}
|
||||
}
|
||||
|
||||
private const val MOCK_MATCH_VALUE: String = "/ap/signin"
|
||||
private const val MOCK_USERNAME_VIEW_ID: String = "ap_email"
|
||||
private val MOCK_KNOWN_USERNAME_FIELD: KnownUsernameField = KnownUsernameField(
|
||||
uriAuthority = "amazon.com",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = MOCK_MATCH_VALUE,
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", MOCK_USERNAME_VIEW_ID),
|
||||
),
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue