start to refactor Autofill code to extract some logic into an non-Androidlibrary
This commit is contained in:
@@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PCloudBindings", "PCloudBin
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "keepass2android-app", "keepass2android\keepass2android-app.csproj", "{D4C32E0A-0193-4496-9DB4-02CC126FD9F3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kp2aAutofillParser", "Kp2aAutofillParser\Kp2aAutofillParser.csproj", "{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -283,6 +285,30 @@ Global
|
||||
{D4C32E0A-0193-4496-9DB4-02CC126FD9F3}.ReleaseNoNet|x64.ActiveCfg = Release|Any CPU
|
||||
{D4C32E0A-0193-4496-9DB4-02CC126FD9F3}.ReleaseNoNet|x64.Build.0 = Release|Any CPU
|
||||
{D4C32E0A-0193-4496-9DB4-02CC126FD9F3}.ReleaseNoNet|x64.Deploy.0 = Release|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Debug|Win32.ActiveCfg = Debug|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Debug|Win32.Build.0 = Debug|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Release|Win32.ActiveCfg = Release|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Release|Win32.Build.0 = Release|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Release|x64.Build.0 = Release|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.ReleaseNoNet|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.ReleaseNoNet|Any CPU.Build.0 = Release|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.ReleaseNoNet|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.ReleaseNoNet|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.ReleaseNoNet|Win32.ActiveCfg = Release|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.ReleaseNoNet|Win32.Build.0 = Release|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.ReleaseNoNet|x64.ActiveCfg = Release|Any CPU
|
||||
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.ReleaseNoNet|x64.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
983
src/Kp2aAutofillParser/AutofillParser.cs
Normal file
983
src/Kp2aAutofillParser/AutofillParser.cs
Normal file
@@ -0,0 +1,983 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
|
||||
namespace Kp2aAutofillParser
|
||||
{
|
||||
public class W3cHints
|
||||
{
|
||||
|
||||
// Supported W3C autofill tokens (https://html.spec.whatwg.org/multipage/forms.html#autofill)
|
||||
public const string HONORIFIC_PREFIX = "honorific-prefix";
|
||||
public const string NAME = "name";
|
||||
public const string GIVEN_NAME = "given-name";
|
||||
public const string ADDITIONAL_NAME = "additional-name";
|
||||
public const string FAMILY_NAME = "family-name";
|
||||
public const string HONORIFIC_SUFFIX = "honorific-suffix";
|
||||
public const string USERNAME = "username";
|
||||
public const string NEW_PASSWORD = "new-password";
|
||||
public const string CURRENT_PASSWORD = "current-password";
|
||||
public const string ORGANIZATION_TITLE = "organization-title";
|
||||
public const string ORGANIZATION = "organization";
|
||||
public const string STREET_ADDRESS = "street-address";
|
||||
public const string ADDRESS_LINE1 = "address-line1";
|
||||
public const string ADDRESS_LINE2 = "address-line2";
|
||||
public const string ADDRESS_LINE3 = "address-line3";
|
||||
public const string ADDRESS_LEVEL4 = "address-level4";
|
||||
public const string ADDRESS_LEVEL3 = "address-level3";
|
||||
public const string ADDRESS_LEVEL2 = "address-level2";
|
||||
public const string ADDRESS_LEVEL1 = "address-level1";
|
||||
public const string COUNTRY = "country";
|
||||
public const string COUNTRY_NAME = "country-name";
|
||||
public const string POSTAL_CODE = "postal-code";
|
||||
public const string CC_NAME = "cc-name";
|
||||
public const string CC_GIVEN_NAME = "cc-given-name";
|
||||
public const string CC_ADDITIONAL_NAME = "cc-additional-name";
|
||||
public const string CC_FAMILY_NAME = "cc-family-name";
|
||||
public const string CC_NUMBER = "cc-number";
|
||||
public const string CC_EXPIRATION = "cc-exp";
|
||||
public const string CC_EXPIRATION_MONTH = "cc-exp-month";
|
||||
public const string CC_EXPIRATION_YEAR = "cc-exp-year";
|
||||
public const string CC_CSC = "cc-csc";
|
||||
public const string CC_TYPE = "cc-type";
|
||||
public const string TRANSACTION_CURRENCY = "transaction-currency";
|
||||
public const string TRANSACTION_AMOUNT = "transaction-amount";
|
||||
public const string LANGUAGE = "language";
|
||||
public const string BDAY = "bday";
|
||||
public const string BDAY_DAY = "bday-day";
|
||||
public const string BDAY_MONTH = "bday-month";
|
||||
public const string BDAY_YEAR = "bday-year";
|
||||
public const string SEX = "sex";
|
||||
public const string URL = "url";
|
||||
public const string PHOTO = "photo";
|
||||
// Optional W3C prefixes
|
||||
public const string PREFIX_SECTION = "section-";
|
||||
public const string SHIPPING = "shipping";
|
||||
public const string BILLING = "billing";
|
||||
// W3C prefixes below...
|
||||
public const string PREFIX_HOME = "home";
|
||||
public const string PREFIX_WORK = "work";
|
||||
public const string PREFIX_FAX = "fax";
|
||||
public const string PREFIX_PAGER = "pager";
|
||||
// ... require those suffix
|
||||
public const string TEL = "tel";
|
||||
public const string TEL_COUNTRY_CODE = "tel-country-code";
|
||||
public const string TEL_NATIONAL = "tel-national";
|
||||
public const string TEL_AREA_CODE = "tel-area-code";
|
||||
public const string TEL_LOCAL = "tel-local";
|
||||
public const string TEL_LOCAL_PREFIX = "tel-local-prefix";
|
||||
public const string TEL_LOCAL_SUFFIX = "tel-local-suffix";
|
||||
public const string TEL_EXTENSION = "tel_extension";
|
||||
public const string EMAIL = "email";
|
||||
public const string IMPP = "impp";
|
||||
|
||||
private W3cHints()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static bool isW3cSectionPrefix(string hint)
|
||||
{
|
||||
return hint.ToLower().StartsWith(W3cHints.PREFIX_SECTION);
|
||||
}
|
||||
|
||||
public static bool isW3cAddressType(string hint)
|
||||
{
|
||||
switch (hint.ToLower())
|
||||
{
|
||||
case W3cHints.SHIPPING:
|
||||
case W3cHints.BILLING:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool isW3cTypePrefix(string hint)
|
||||
{
|
||||
switch (hint.ToLower())
|
||||
{
|
||||
case W3cHints.PREFIX_WORK:
|
||||
case W3cHints.PREFIX_FAX:
|
||||
case W3cHints.PREFIX_HOME:
|
||||
case W3cHints.PREFIX_PAGER:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool isW3cTypeHint(string hint)
|
||||
{
|
||||
switch (hint.ToLower())
|
||||
{
|
||||
case W3cHints.TEL:
|
||||
case W3cHints.TEL_COUNTRY_CODE:
|
||||
case W3cHints.TEL_NATIONAL:
|
||||
case W3cHints.TEL_AREA_CODE:
|
||||
case W3cHints.TEL_LOCAL:
|
||||
case W3cHints.TEL_LOCAL_PREFIX:
|
||||
case W3cHints.TEL_LOCAL_SUFFIX:
|
||||
case W3cHints.TEL_EXTENSION:
|
||||
case W3cHints.EMAIL:
|
||||
case W3cHints.IMPP:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// FilledAutofillFieldCollection is the model that holds all of the data on a client app's page,
|
||||
/// plus the dataset name associated with it.
|
||||
/// </summary>
|
||||
public class FilledAutofillFieldCollection<FieldT> where FieldT:InputField
|
||||
{
|
||||
public Dictionary<string, FilledAutofillField<FieldT>> HintMap { get; }
|
||||
public string DatasetName { get; set; }
|
||||
|
||||
public FilledAutofillFieldCollection(Dictionary<string, FilledAutofillField<FieldT>> hintMap, string datasetName = "")
|
||||
{
|
||||
//recreate hint map making sure we compare case insensitive
|
||||
HintMap = BuildHintMap();
|
||||
foreach (var p in hintMap)
|
||||
HintMap.Add(p.Key, p.Value);
|
||||
DatasetName = datasetName;
|
||||
}
|
||||
|
||||
public FilledAutofillFieldCollection() : this(BuildHintMap())
|
||||
{ }
|
||||
|
||||
private static Dictionary<string, FilledAutofillField<FieldT>> BuildHintMap()
|
||||
{
|
||||
return new Dictionary<string, FilledAutofillField<FieldT>>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a filledAutofillField to the collection, indexed by all of its hints.
|
||||
/// </summary>
|
||||
/// <returns>The add.</returns>
|
||||
/// <param name="filledAutofillField">Filled autofill field.</param>
|
||||
public void Add(FilledAutofillField<FieldT> filledAutofillField)
|
||||
{
|
||||
foreach (string hint in filledAutofillField.AutofillHints)
|
||||
{
|
||||
if (AutofillHintsHelper.IsSupportedHint(hint))
|
||||
{
|
||||
HintMap.TryAdd(hint, filledAutofillField);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Takes in a list of autofill hints (`autofillHints`), usually associated with a View or set of
|
||||
/// Views. Returns whether any of the filled fields on the page have at least 1 of these
|
||||
/// `autofillHint`s.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c>, if with hints was helpsed, <c>false</c> otherwise.</returns>
|
||||
/// <param name="autofillHints">Autofill hints.</param>
|
||||
public bool HelpsWithHints(List<string> autofillHints)
|
||||
{
|
||||
for (int i = 0; i < autofillHints.Count; i++)
|
||||
{
|
||||
var autofillHint = autofillHints[i];
|
||||
if (HintMap.ContainsKey(autofillHint) && !HintMap[autofillHint].IsNull())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
class AutofillHintsHelper
|
||||
{
|
||||
const string AutofillHint2faAppOtp = "2faAppOTPCode";
|
||||
const string AutofillHintBirthDateDay = "birthDateDay";
|
||||
const string AutofillHintBirthDateFull = "birthDateFull";
|
||||
const string AutofillHintBirthDateMonth = "birthDateMonth";
|
||||
const string AutofillHintBirthDateYear = "birthDateYear";
|
||||
const string AutofillHintCreditCardExpirationDate = "creditCardExpirationDate";
|
||||
const string AutofillHintCreditCardExpirationDay = "creditCardExpirationDay";
|
||||
const string AutofillHintCreditCardExpirationMonth = "creditCardExpirationMonth";
|
||||
const string AutofillHintCreditCardExpirationYear = "creditCardExpirationYear";
|
||||
const string AutofillHintCreditCardNumber = "creditCardNumber";
|
||||
const string AutofillHintCreditCardSecurityCode = "creditCardSecurityCode";
|
||||
const string AutofillHintEmailAddress = "emailAddress";
|
||||
const string AutofillHintEmailOtp = "emailOTPCode";
|
||||
const string AutofillHintGender = "gender";
|
||||
const string AutofillHintName = "name";
|
||||
const string AutofillHintNewPassword = "newPassword";
|
||||
const string AutofillHintNewUsername = "newUsername";
|
||||
const string AutofillHintNotApplicable = "notApplicable";
|
||||
const string AutofillHintPassword = "password";
|
||||
const string AutofillHintPersonName = "personName";
|
||||
const string AutofillHintPersonNameFAMILY = "personFamilyName";
|
||||
const string AutofillHintPersonNameGIVEN = "personGivenName";
|
||||
const string AutofillHintPersonNameMIDDLE = "personMiddleName";
|
||||
const string AutofillHintPersonNameMIDDLE_INITIAL = "personMiddleInitial";
|
||||
const string AutofillHintPersonNamePREFIX = "personNamePrefix";
|
||||
const string AutofillHintPersonNameSUFFIX = "personNameSuffix";
|
||||
const string AutofillHintPhone = "phone";
|
||||
const string AutofillHintPhoneContryCode = "phoneCountryCode";
|
||||
const string AutofillHintPostalAddressAPT_NUMBER = "aptNumber";
|
||||
const string AutofillHintPostalAddressCOUNTRY = "addressCountry";
|
||||
const string AutofillHintPostalAddressDEPENDENT_LOCALITY = "dependentLocality";
|
||||
const string AutofillHintPostalAddressEXTENDED_ADDRESS = "extendedAddress";
|
||||
const string AutofillHintPostalAddressEXTENDED_POSTAL_CODE = "extendedPostalCode";
|
||||
const string AutofillHintPostalAddressLOCALITY = "addressLocality";
|
||||
const string AutofillHintPostalAddressREGION = "addressRegion";
|
||||
const string AutofillHintPostalAddressSTREET_ADDRESS = "streetAddress";
|
||||
const string AutofillHintPostalCode = "postalCode";
|
||||
const string AutofillHintPromoCode = "promoCode";
|
||||
const string AutofillHintSMS_OTP = "smsOTPCode";
|
||||
const string AutofillHintUPI_VPA = "upiVirtualPaymentAddress";
|
||||
const string AutofillHintUsername = "username";
|
||||
const string AutofillHintWifiPassword = "wifiPassword";
|
||||
const string AutofillHintPhoneNational = "phoneNational";
|
||||
const string AutofillHintPhoneNumber = "phoneNumber";
|
||||
const string AutofillHintPhoneNumberDevice = "phoneNumberDevice";
|
||||
const string AutofillHintPostalAddress = "postalAddress";
|
||||
|
||||
private static readonly HashSet<string> _allSupportedHints = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
AutofillHintCreditCardExpirationDate,
|
||||
AutofillHintCreditCardExpirationDay,
|
||||
AutofillHintCreditCardExpirationMonth,
|
||||
AutofillHintCreditCardExpirationYear,
|
||||
AutofillHintCreditCardNumber,
|
||||
AutofillHintCreditCardSecurityCode,
|
||||
AutofillHintEmailAddress,
|
||||
AutofillHintPhone,
|
||||
AutofillHintName,
|
||||
AutofillHintPassword,
|
||||
AutofillHintPostalAddress,
|
||||
AutofillHintPostalCode,
|
||||
AutofillHintUsername,
|
||||
W3cHints.HONORIFIC_PREFIX,
|
||||
W3cHints.NAME,
|
||||
W3cHints.GIVEN_NAME,
|
||||
W3cHints.ADDITIONAL_NAME,
|
||||
W3cHints.FAMILY_NAME,
|
||||
W3cHints.HONORIFIC_SUFFIX,
|
||||
W3cHints.USERNAME,
|
||||
W3cHints.NEW_PASSWORD,
|
||||
W3cHints.CURRENT_PASSWORD,
|
||||
W3cHints.ORGANIZATION_TITLE,
|
||||
W3cHints.ORGANIZATION,
|
||||
W3cHints.STREET_ADDRESS,
|
||||
W3cHints.ADDRESS_LINE1,
|
||||
W3cHints.ADDRESS_LINE2,
|
||||
W3cHints.ADDRESS_LINE3,
|
||||
W3cHints.ADDRESS_LEVEL4,
|
||||
W3cHints.ADDRESS_LEVEL3,
|
||||
W3cHints.ADDRESS_LEVEL2,
|
||||
W3cHints.ADDRESS_LEVEL1,
|
||||
W3cHints.COUNTRY,
|
||||
W3cHints.COUNTRY_NAME,
|
||||
W3cHints.POSTAL_CODE,
|
||||
W3cHints.CC_NAME,
|
||||
W3cHints.CC_GIVEN_NAME,
|
||||
W3cHints.CC_ADDITIONAL_NAME,
|
||||
W3cHints.CC_FAMILY_NAME,
|
||||
W3cHints.CC_NUMBER,
|
||||
W3cHints.CC_EXPIRATION,
|
||||
W3cHints.CC_EXPIRATION_MONTH,
|
||||
W3cHints.CC_EXPIRATION_YEAR,
|
||||
W3cHints.CC_CSC,
|
||||
W3cHints.CC_TYPE,
|
||||
W3cHints.TRANSACTION_CURRENCY,
|
||||
W3cHints.TRANSACTION_AMOUNT,
|
||||
W3cHints.LANGUAGE,
|
||||
W3cHints.BDAY,
|
||||
W3cHints.BDAY_DAY,
|
||||
W3cHints.BDAY_MONTH,
|
||||
W3cHints.BDAY_YEAR,
|
||||
W3cHints.SEX,
|
||||
W3cHints.URL,
|
||||
W3cHints.PHOTO,
|
||||
W3cHints.TEL,
|
||||
W3cHints.TEL_COUNTRY_CODE,
|
||||
W3cHints.TEL_NATIONAL,
|
||||
W3cHints.TEL_AREA_CODE,
|
||||
W3cHints.TEL_LOCAL,
|
||||
W3cHints.TEL_LOCAL_PREFIX,
|
||||
W3cHints.TEL_LOCAL_SUFFIX,
|
||||
W3cHints.TEL_EXTENSION,
|
||||
W3cHints.EMAIL,
|
||||
W3cHints.IMPP,
|
||||
};
|
||||
|
||||
private static readonly List<HashSet<string>> partitionsOfCanonicalHints = new List<HashSet<string>>()
|
||||
{
|
||||
|
||||
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
AutofillHintEmailAddress,
|
||||
AutofillHintPhone,
|
||||
AutofillHintName,
|
||||
AutofillHintPassword,
|
||||
AutofillHintUsername,
|
||||
W3cHints.HONORIFIC_PREFIX,
|
||||
W3cHints.NAME,
|
||||
W3cHints.GIVEN_NAME,
|
||||
W3cHints.ADDITIONAL_NAME,
|
||||
W3cHints.FAMILY_NAME,
|
||||
W3cHints.HONORIFIC_SUFFIX,
|
||||
W3cHints.ORGANIZATION_TITLE,
|
||||
W3cHints.ORGANIZATION,
|
||||
W3cHints.LANGUAGE,
|
||||
W3cHints.BDAY,
|
||||
W3cHints.BDAY_DAY,
|
||||
W3cHints.BDAY_MONTH,
|
||||
W3cHints.BDAY_YEAR,
|
||||
W3cHints.SEX,
|
||||
W3cHints.URL,
|
||||
W3cHints.PHOTO,
|
||||
W3cHints.TEL,
|
||||
W3cHints.TEL_COUNTRY_CODE,
|
||||
W3cHints.TEL_NATIONAL,
|
||||
W3cHints.TEL_AREA_CODE,
|
||||
W3cHints.TEL_LOCAL,
|
||||
W3cHints.TEL_LOCAL_PREFIX,
|
||||
W3cHints.TEL_LOCAL_SUFFIX,
|
||||
W3cHints.TEL_EXTENSION,
|
||||
W3cHints.IMPP,
|
||||
},
|
||||
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
AutofillHintPostalAddress,
|
||||
AutofillHintPostalCode,
|
||||
|
||||
W3cHints.STREET_ADDRESS,
|
||||
W3cHints.ADDRESS_LINE1,
|
||||
W3cHints.ADDRESS_LINE2,
|
||||
W3cHints.ADDRESS_LINE3,
|
||||
W3cHints.ADDRESS_LEVEL4,
|
||||
W3cHints.ADDRESS_LEVEL3,
|
||||
W3cHints.ADDRESS_LEVEL2,
|
||||
W3cHints.ADDRESS_LEVEL1,
|
||||
W3cHints.COUNTRY,
|
||||
W3cHints.COUNTRY_NAME
|
||||
},
|
||||
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
AutofillHintCreditCardExpirationDate,
|
||||
AutofillHintCreditCardExpirationDay,
|
||||
AutofillHintCreditCardExpirationMonth,
|
||||
AutofillHintCreditCardExpirationYear,
|
||||
AutofillHintCreditCardNumber,
|
||||
AutofillHintCreditCardSecurityCode,
|
||||
|
||||
W3cHints.CC_NAME,
|
||||
W3cHints.CC_GIVEN_NAME,
|
||||
W3cHints.CC_ADDITIONAL_NAME,
|
||||
W3cHints.CC_FAMILY_NAME,
|
||||
W3cHints.CC_TYPE,
|
||||
W3cHints.TRANSACTION_CURRENCY,
|
||||
W3cHints.TRANSACTION_AMOUNT,
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, string> hintToCanonicalReplacement = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{W3cHints.EMAIL, AutofillHintEmailAddress},
|
||||
{W3cHints.USERNAME, AutofillHintUsername},
|
||||
{W3cHints.CURRENT_PASSWORD, AutofillHintPassword},
|
||||
{W3cHints.NEW_PASSWORD, AutofillHintPassword},
|
||||
{W3cHints.CC_EXPIRATION_MONTH, AutofillHintCreditCardExpirationMonth },
|
||||
{W3cHints.CC_EXPIRATION_YEAR, AutofillHintCreditCardExpirationYear },
|
||||
{W3cHints.CC_EXPIRATION, AutofillHintCreditCardExpirationDate },
|
||||
{W3cHints.CC_NUMBER, AutofillHintCreditCardNumber },
|
||||
{W3cHints.CC_CSC, AutofillHintCreditCardSecurityCode },
|
||||
{W3cHints.POSTAL_CODE, AutofillHintPostalCode },
|
||||
|
||||
|
||||
};
|
||||
|
||||
public static bool IsSupportedHint(string hint)
|
||||
{
|
||||
return _allSupportedHints.Contains(hint);
|
||||
}
|
||||
|
||||
|
||||
public static string[] FilterForSupportedHints(string[] hints)
|
||||
{
|
||||
if (hints == null)
|
||||
return Array.Empty<string>();
|
||||
var filteredHints = new string[hints.Length];
|
||||
int i = 0;
|
||||
foreach (var hint in hints)
|
||||
{
|
||||
if (IsSupportedHint(hint))
|
||||
{
|
||||
filteredHints[i++] = hint;
|
||||
}
|
||||
|
||||
}
|
||||
var finalFilteredHints = new string[i];
|
||||
Array.Copy(filteredHints, 0, finalFilteredHints, 0, i);
|
||||
return finalFilteredHints;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// transforms hints by replacing some W3cHints by their Android counterparts and transforming everything to lowercase
|
||||
/// </summary>
|
||||
public static List<string> ConvertToCanonicalHints(string[] supportedHints)
|
||||
{
|
||||
List<string> result = new List<string>();
|
||||
foreach (string hint in supportedHints)
|
||||
{
|
||||
string canonicalHint;
|
||||
if (!hintToCanonicalReplacement.TryGetValue(hint, out canonicalHint))
|
||||
canonicalHint = hint;
|
||||
result.Add(canonicalHint.ToLower());
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
public static int GetPartitionIndex(string hint)
|
||||
{
|
||||
for (int i = 0; i < partitionsOfCanonicalHints.Count; i++)
|
||||
{
|
||||
if (partitionsOfCanonicalHints[i].Contains(hint))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static FilledAutofillFieldCollection<FieldT> FilterForPartition<FieldT>(FilledAutofillFieldCollection<FieldT> autofillFields, int partitionIndex) where FieldT: InputField
|
||||
{
|
||||
FilledAutofillFieldCollection<FieldT> filteredCollection =
|
||||
new FilledAutofillFieldCollection<FieldT> { DatasetName = autofillFields.DatasetName };
|
||||
|
||||
if (partitionIndex == -1)
|
||||
return filteredCollection;
|
||||
|
||||
foreach (var field in autofillFields.HintMap.Values.Distinct())
|
||||
{
|
||||
foreach (var hint in field.AutofillHints)
|
||||
{
|
||||
if (GetPartitionIndex(hint) == partitionIndex)
|
||||
{
|
||||
filteredCollection.Add(field);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredCollection;
|
||||
}
|
||||
|
||||
public static FilledAutofillFieldCollection<FieldT> FilterForPartition<FieldT>(FilledAutofillFieldCollection<FieldT> filledAutofillFieldCollection, List<string> autofillFieldsFocusedAutofillCanonicalHints) where FieldT: InputField
|
||||
{
|
||||
|
||||
//only apply partition data if we have FocusedAutofillCanonicalHints. This may be empty on buggy Firefox.
|
||||
if (autofillFieldsFocusedAutofillCanonicalHints.Any())
|
||||
{
|
||||
int partitionIndex = AutofillHintsHelper.GetPartitionIndex(autofillFieldsFocusedAutofillCanonicalHints.FirstOrDefault());
|
||||
return AutofillHintsHelper.FilterForPartition(filledAutofillFieldCollection, partitionIndex);
|
||||
}
|
||||
|
||||
return filledAutofillFieldCollection;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// This enum represents the Android.Text.InputTypes values. For testability, this is duplicated here.
|
||||
/// </summary>
|
||||
public enum InputTypes
|
||||
{
|
||||
ClassDatetime = 4,
|
||||
ClassNumber = 2,
|
||||
ClassPhone = 3,
|
||||
ClassText = 1,
|
||||
DatetimeVariationDate = 16,
|
||||
DatetimeVariationNormal = 0,
|
||||
DatetimeVariationTime = 32,
|
||||
MaskClass = 15,
|
||||
MaskFlags = 16773120,
|
||||
MaskVariation = 4080,
|
||||
Null = 0,
|
||||
NumberFlagDecimal = 8192,
|
||||
NumberFlagSigned = 4096,
|
||||
NumberVariationNormal = 0,
|
||||
NumberVariationPassword = 16,
|
||||
TextFlagAutoComplete = 65536,
|
||||
TextFlagAutoCorrect = 32768,
|
||||
TextFlagCapCharacters = 4096,
|
||||
TextFlagCapSentences = 16384,
|
||||
TextFlagCapWords = 8192,
|
||||
TextFlagEnableTextConversionSuggestions = 1048576,
|
||||
TextFlagImeMultiLine = 262144,
|
||||
TextFlagMultiLine = 131072,
|
||||
TextFlagNoSuggestions = 524288,
|
||||
TextVariationEmailAddress = 32,
|
||||
TextVariationEmailSubject = 48,
|
||||
TextVariationFilter = 176,
|
||||
TextVariationLongMessage = 80,
|
||||
TextVariationNormal = 0,
|
||||
TextVariationPassword = 128,
|
||||
TextVariationPersonName = 96,
|
||||
TextVariationPhonetic = 192,
|
||||
TextVariationPostalAddress = 112,
|
||||
TextVariationShortMessage = 64,
|
||||
TextVariationUri = 16,
|
||||
TextVariationVisiblePassword = 144,
|
||||
TextVariationWebEditText = 160,
|
||||
TextVariationWebEmailAddress = 208,
|
||||
TextVariationWebPassword = 224
|
||||
}
|
||||
|
||||
public interface IKp2aDigitalAssetLinksDataSource
|
||||
{
|
||||
bool IsTrustedApp(string packageName);
|
||||
bool IsTrustedLink(string domain, string targetPackage);
|
||||
|
||||
}
|
||||
public class FilledAutofillField<FieldT> where FieldT : InputField
|
||||
{
|
||||
private string[] _autofillHints;
|
||||
public string TextValue { get; set; }
|
||||
public long? DateValue { get; set; }
|
||||
public bool? ToggleValue { get; set; }
|
||||
|
||||
public string ValueToString()
|
||||
{
|
||||
if (DateValue != null)
|
||||
{
|
||||
return TimeUtil.ConvertUnixTime((long)DateValue / 1000.0).ToLongDateString();
|
||||
}
|
||||
if (ToggleValue != null)
|
||||
return ToggleValue.ToString();
|
||||
return TextValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns the autofill hints for the filled field. These are always lowercased for simpler string comparison.
|
||||
/// </summary>
|
||||
public string[] AutofillHints
|
||||
{
|
||||
get
|
||||
{
|
||||
return _autofillHints;
|
||||
}
|
||||
set
|
||||
{
|
||||
_autofillHints = value;
|
||||
for (int i = 0; i < _autofillHints.Length; i++)
|
||||
_autofillHints[i] = _autofillHints[i].ToLower();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public FilledAutofillField()
|
||||
{ }
|
||||
|
||||
public FilledAutofillField(FieldT inputField)
|
||||
: this(inputField, inputField.AutofillHints)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public FilledAutofillField(FieldT inputField, string[] hints)
|
||||
{
|
||||
|
||||
string[] rawHints = AutofillHintsHelper.FilterForSupportedHints(hints);
|
||||
List<string> hintList = new List<string>();
|
||||
|
||||
string nextHint = null;
|
||||
for (int i = 0; i < rawHints.Length; i++)
|
||||
{
|
||||
string hint = rawHints[i];
|
||||
if (i < rawHints.Length - 1)
|
||||
{
|
||||
nextHint = rawHints[i + 1];
|
||||
}
|
||||
// First convert the compound W3C autofill hints
|
||||
if (W3cHints.isW3cSectionPrefix(hint) && i < rawHints.Length - 1)
|
||||
{
|
||||
hint = rawHints[++i];
|
||||
|
||||
if (i < rawHints.Length - 1)
|
||||
{
|
||||
nextHint = rawHints[i + 1];
|
||||
}
|
||||
}
|
||||
if (W3cHints.isW3cTypePrefix(hint) && nextHint != null && W3cHints.isW3cTypeHint(nextHint))
|
||||
{
|
||||
hint = nextHint;
|
||||
i++;
|
||||
|
||||
}
|
||||
if (W3cHints.isW3cAddressType(hint) && nextHint != null)
|
||||
{
|
||||
hint = nextHint;
|
||||
i++;
|
||||
|
||||
}
|
||||
|
||||
// Then check if the "actual" hint is supported.
|
||||
if (AutofillHintsHelper.IsSupportedHint(hint))
|
||||
{
|
||||
hintList.Add(hint);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
AutofillHints = AutofillHintsHelper.ConvertToCanonicalHints(hintList.ToArray()).ToArray();
|
||||
|
||||
|
||||
}
|
||||
|
||||
public bool IsNull()
|
||||
{
|
||||
return TextValue == null && DateValue == null && ToggleValue == null;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (this == obj) return true;
|
||||
if (obj == null || GetType() != obj.GetType()) return false;
|
||||
|
||||
FilledAutofillField<FieldT> that = (FilledAutofillField<FieldT>)obj;
|
||||
|
||||
if (!TextValue?.Equals(that.TextValue) ?? that.TextValue != null)
|
||||
return false;
|
||||
if (DateValue != null ? !DateValue.Equals(that.DateValue) : that.DateValue != null)
|
||||
return false;
|
||||
return ToggleValue != null ? ToggleValue.Equals(that.ToggleValue) : that.ToggleValue == null;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var result = TextValue != null ? TextValue.GetHashCode() : 0;
|
||||
result = 31 * result + (DateValue != null ? DateValue.GetHashCode() : 0);
|
||||
result = 31 * result + (ToggleValue != null ? ToggleValue.GetHashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for everything that is a input field which might (or might not) be autofilled.
|
||||
/// For testability, this is independent from Android classes like ViewNode
|
||||
/// </summary>
|
||||
public abstract class InputField
|
||||
{
|
||||
public string IdEntry { get; set; }
|
||||
public string Hint { get; set; }
|
||||
public string ClassName { get; set; }
|
||||
public string[] AutofillHints { get; set; }
|
||||
public bool IsFocused { get; set; }
|
||||
|
||||
public InputTypes InputType { get; set; }
|
||||
|
||||
public string HtmlInfoTag { get; set; }
|
||||
public string HtmlInfoTypeAttribute { get; set; }
|
||||
|
||||
public abstract void FillFilledAutofillValue(FilledAutofillField<FieldT> filledField);
|
||||
|
||||
|
||||
}
|
||||
|
||||
public class AutofillView<TField> where TField : InputField
|
||||
{
|
||||
public List<TField> InputFields { get; set; } = new List<TField>();
|
||||
|
||||
public string PackageId { get; set; } = null;
|
||||
public string WebDomain { get; set; } = null;
|
||||
}
|
||||
|
||||
public interface ILogger
|
||||
{
|
||||
void Log(string x);
|
||||
}
|
||||
|
||||
public class StructureParserBase<FieldT> where FieldT: InputField
|
||||
{
|
||||
private readonly ILogger _log;
|
||||
private readonly IKp2aDigitalAssetLinksDataSource _digitalAssetLinksDataSource;
|
||||
|
||||
public string PackageId { get; set; }
|
||||
|
||||
public StructureParserBase(ILogger logger, IKp2aDigitalAssetLinksDataSource digitalAssetLinksDataSource)
|
||||
{
|
||||
_log = logger;
|
||||
_digitalAssetLinksDataSource = digitalAssetLinksDataSource;
|
||||
}
|
||||
|
||||
public class AutofillTargetId
|
||||
{
|
||||
public string PackageName { get; set; }
|
||||
|
||||
public string PackageNameWithPseudoSchema
|
||||
{
|
||||
get { return AndroidAppScheme + PackageName; }
|
||||
}
|
||||
|
||||
public const string AndroidAppScheme = "androidapp://";
|
||||
|
||||
public string WebDomain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If PackageName and WebDomain are not compatible (by DAL or because PackageName is a trusted browser in which case we treat all domains as "compatible"
|
||||
/// we need to issue a warning. If we would fill credentials for the package, a malicious website could try to get credentials for the app.
|
||||
/// If we would fill credentials for the domain, a malicious app could get credentials for the domain.
|
||||
/// </summary>
|
||||
public bool IncompatiblePackageAndDomain { get; set; }
|
||||
|
||||
public string DomainOrPackage
|
||||
{
|
||||
get
|
||||
{
|
||||
return WebDomain ?? PackageNameWithPseudoSchema;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public AutofillTargetId ParseForFill(bool isManual, AutofillView<FieldT> autofillView)
|
||||
{
|
||||
return Parse(true, isManual, autofillView);
|
||||
}
|
||||
|
||||
public AutofillTargetId ParseForSave(AutofillView<FieldT> autofillView)
|
||||
{
|
||||
return Parse(false, true, autofillView);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Traverse AssistStructure and add ViewNode metadata to a flat list.
|
||||
/// </summary>
|
||||
/// <returns>The parse.</returns>
|
||||
/// <param name="forFill">If set to <c>true</c> for fill.</param>
|
||||
/// <param name="isManualRequest"></param>
|
||||
AutofillTargetId Parse(bool forFill, bool isManualRequest, AutofillView<FieldT> autofillView)
|
||||
{
|
||||
AutofillTargetId result = new AutofillTargetId();
|
||||
|
||||
|
||||
_editTextsWithoutHint.Clear();
|
||||
|
||||
_log.Log("parsing autofillStructure...");
|
||||
|
||||
//TODO remove from production
|
||||
_log.Log("will log the autofillStructure...");
|
||||
string debugInfo = JsonConvert.SerializeObject(autofillView, Formatting.Indented);
|
||||
_log.Log("will log the autofillStructure... size is " + debugInfo.Length);
|
||||
_log.Log("This is the autofillStructure: \n\n " + debugInfo);
|
||||
|
||||
foreach (var viewNode in autofillView.InputFields)
|
||||
{
|
||||
string[] viewHints = viewNode.AutofillHints;
|
||||
if (viewHints != null && viewHints.Length == 1 && viewHints.First() == "off" && viewNode.IsFocused &&
|
||||
isManualRequest)
|
||||
viewHints[0] = "on";
|
||||
/*if (viewHints != null && viewHints.Any())
|
||||
{
|
||||
CommonUtil.logd("viewHints=" + viewHints);
|
||||
CommonUtil.logd("class=" + viewNode.ClassName);
|
||||
CommonUtil.logd("tag=" + (viewNode?.HtmlInfo?.Tag ?? "(null)"));
|
||||
}*/
|
||||
|
||||
if (IsPassword(viewNode) || HasPasswordHint(viewNode) || (HasUsernameHint(viewNode)))
|
||||
{
|
||||
if (forFill)
|
||||
{
|
||||
AutofillFields.Add(new AutofillFieldMetadata(viewNode.ViewNode));
|
||||
}
|
||||
else
|
||||
{
|
||||
FilledAutofillField filledAutofillField = new FilledAutofillField(viewNode.ViewNode);
|
||||
ClientFormData.Add(filledAutofillField);
|
||||
}
|
||||
}
|
||||
else if (viewNode.ClassName == "android.widget.EditText"
|
||||
|| viewNode.ClassName == "android.widget.AutoCompleteTextView"
|
||||
|| viewNode.HtmlInfoTag == "input"
|
||||
|| ((viewHints?.Length ?? 0) > 0))
|
||||
{
|
||||
_log.Log("Found something that looks fillable " + viewNode.ClassName);
|
||||
|
||||
}
|
||||
|
||||
if (viewHints != null && viewHints.Length > 0 && viewHints.First() != "on" /*if hint is "on", treat as if there is no hint*/)
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
if (viewNode.ClassName == "android.widget.EditText"
|
||||
|| viewNode.ClassName == "android.widget.AutoCompleteTextView"
|
||||
|| viewNode.HtmlInfoTag == "input")
|
||||
{
|
||||
_editTextsWithoutHint.Add(viewNode);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
List<ViewNodeInputField> passwordFields = new List<ViewNodeInputField>();
|
||||
List<ViewNodeInputField> usernameFields = new List<ViewNodeInputField>();
|
||||
if (AutofillFields.Empty)
|
||||
{
|
||||
passwordFields = _editTextsWithoutHint.Where(IsPassword).ToList();
|
||||
if (!passwordFields.Any())
|
||||
{
|
||||
passwordFields = _editTextsWithoutHint.Where(HasPasswordHint).ToList();
|
||||
}
|
||||
|
||||
usernameFields = _editTextsWithoutHint.Where(HasUsernameHint).ToList();
|
||||
|
||||
if (usernameFields.Any() == false)
|
||||
{
|
||||
|
||||
foreach (var passwordField in passwordFields)
|
||||
{
|
||||
var usernameField = _editTextsWithoutHint
|
||||
.TakeWhile(f => f != passwordField).LastOrDefault();
|
||||
if (usernameField != null)
|
||||
{
|
||||
usernameFields.Add(usernameField);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (usernameFields.Any() == false)
|
||||
{
|
||||
//for some pages with two-step login, we don't see a password field and don't display the autofill for non-manual requests. But if the user forces autofill,
|
||||
//let's assume it is a username field:
|
||||
if (isManualRequest && !passwordFields.Any() && _editTextsWithoutHint.Count == 1)
|
||||
{
|
||||
usernameFields.Add(_editTextsWithoutHint.First());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
//force focused fields to be included in autofill fields when request was triggered manually. This allows to fill fields which are "off" or don't have a hint (in case there are hints)
|
||||
if (isManualRequest)
|
||||
{
|
||||
foreach (var editText in _editTextsWithoutHint)
|
||||
{
|
||||
if (editText.IsFocused)
|
||||
{
|
||||
if (IsPassword(editText) || HasPasswordHint(editText))
|
||||
passwordFields.Add(editText);
|
||||
else
|
||||
usernameFields.Add(editText);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (forFill)
|
||||
{
|
||||
foreach (var uf in usernameFields)
|
||||
AutofillFields.Add(new AutofillFieldMetadata(uf.ViewNode, new[] { View.AutofillHintUsername }));
|
||||
foreach (var pf in passwordFields)
|
||||
AutofillFields.Add(new AutofillFieldMetadata(pf.ViewNode, new[] { View.AutofillHintPassword }));
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var uf in usernameFields)
|
||||
ClientFormData.Add(new FilledAutofillField(uf.ViewNode, new[] { View.AutofillHintUsername }));
|
||||
foreach (var pf in passwordFields)
|
||||
ClientFormData.Add(new FilledAutofillField(pf.ViewNode, new[] { View.AutofillHintPassword }));
|
||||
}
|
||||
|
||||
|
||||
result.WebDomain = autofillView.WebDomain;
|
||||
result.PackageName = Structure.ActivityComponent.PackageName;
|
||||
if (!string.IsNullOrEmpty(autofillView.WebDomain) && !PreferenceManager.GetDefaultSharedPreferences(mContext).GetBoolean(mContext.GetString(Resource.String.NoDalVerification_key), false))
|
||||
{
|
||||
result.IncompatiblePackageAndDomain = !kp2aDigitalAssetLinksDataSource.IsTrustedLink(autofillView.WebDomain, result.PackageName);
|
||||
if (result.IncompatiblePackageAndDomain)
|
||||
{
|
||||
CommonUtil.loge($"DAL verification failed for {result.PackageName}/{result.WebDomain}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.IncompatiblePackageAndDomain = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static readonly HashSet<string> _passwordHints = new HashSet<string> { "password", "passwort", "passwordAuto", "pswd" };
|
||||
private static bool HasPasswordHint(InputField f)
|
||||
{
|
||||
return ContainsAny(f.IdEntry, _passwordHints) ||
|
||||
ContainsAny(f.Hint, _passwordHints);
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> _usernameHints = new HashSet<string> { "email", "e-mail", "username" };
|
||||
|
||||
private static bool HasUsernameHint(InputField f)
|
||||
{
|
||||
return ContainsAny(f.IdEntry, _usernameHints) ||
|
||||
ContainsAny(f.Hint, _usernameHints);
|
||||
}
|
||||
|
||||
private static bool ContainsAny(string value, IEnumerable<string> terms)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var lowerValue = value.ToLowerInvariant();
|
||||
return terms.Any(t => lowerValue.Contains(t));
|
||||
}
|
||||
|
||||
private static bool IsInputTypeClass(InputTypes inputType, InputTypes inputTypeClass)
|
||||
{
|
||||
if (!InputTypes.MaskClass.HasFlag(inputTypeClass))
|
||||
throw new Exception("invalid inputTypeClass");
|
||||
return (((int)inputType) & (int)InputTypes.MaskClass) == (int)(inputTypeClass);
|
||||
}
|
||||
private static bool IsInputTypeVariation(InputTypes inputType, InputTypes inputTypeVariation)
|
||||
{
|
||||
if (!InputTypes.MaskVariation.HasFlag(inputTypeVariation))
|
||||
throw new Exception("invalid inputTypeVariation");
|
||||
return (((int)inputType) & (int)InputTypes.MaskVariation) == (int)(inputTypeVariation);
|
||||
}
|
||||
|
||||
private static bool IsPassword(InputField f)
|
||||
{
|
||||
InputTypes inputType = f.InputType;
|
||||
|
||||
return
|
||||
(!f.IdEntry?.ToLowerInvariant().Contains("search") ?? true) &&
|
||||
(!f.Hint?.ToLowerInvariant().Contains("search") ?? true) &&
|
||||
(
|
||||
(IsInputTypeClass(inputType, InputTypes.ClassText)
|
||||
&&
|
||||
(
|
||||
IsInputTypeVariation(inputType, InputTypes.TextVariationPassword)
|
||||
|| IsInputTypeVariation(inputType, InputTypes.TextVariationVisiblePassword)
|
||||
|| IsInputTypeVariation(inputType, InputTypes.TextVariationWebPassword)
|
||||
)
|
||||
)
|
||||
|| (f.HtmlInfoTypeAttribute == "password")
|
||||
);
|
||||
}
|
||||
|
||||
AssistStructure Structure;
|
||||
private List<ViewNodeInputField> _editTextsWithoutHint = new List<ViewNodeInputField>();
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
12
src/Kp2aAutofillParser/Kp2aAutofillParser.csproj
Normal file
12
src/Kp2aAutofillParser/Kp2aAutofillParser.csproj
Normal file
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1944,6 +1944,10 @@
|
||||
<Project>{545b4a6b-8bba-4fbe-92fc-4ac060122a54}</Project>
|
||||
<Name>KeePassLib2Android</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Kp2aAutofillParser\Kp2aAutofillParser.csproj">
|
||||
<Project>{39b12571-bafe-4d3a-aee2-4d74f14dfd96}</Project>
|
||||
<Name>Kp2aAutofillParser</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Kp2aBusinessLogic\Kp2aBusinessLogic.csproj">
|
||||
<Project>{53a9cb7f-6553-4bc0-b56b-9410bb2e59aa}</Project>
|
||||
<Name>Kp2aBusinessLogic</Name>
|
||||
|
||||
@@ -7,11 +7,12 @@ using Android.Runtime;
|
||||
using Android.Service.Autofill;
|
||||
using Android.Util;
|
||||
using Android.Views;
|
||||
using Android.Views.Autofill;
|
||||
using Android.Widget;
|
||||
using Android.Widget.Inline;
|
||||
using AndroidX.AutoFill.Inline;
|
||||
using AndroidX.AutoFill.Inline.V1;
|
||||
using FilledAutofillFieldCollection = keepass2android.services.AutofillBase.model.FilledAutofillFieldCollection;
|
||||
using Kp2aAutofillParser;
|
||||
|
||||
namespace keepass2android.services.AutofillBase
|
||||
{
|
||||
@@ -93,13 +94,9 @@ namespace keepass2android.services.AutofillBase
|
||||
/// Wraps autofill data in a LoginCredential Dataset object which can then be sent back to the
|
||||
/// client View.
|
||||
/// </summary>
|
||||
/// <returns>The dataset.</returns>
|
||||
/// <param name="context">Context.</param>
|
||||
/// <param name="autofillFields">Autofill fields.</param>
|
||||
/// <param name="filledAutofillFieldCollection">Filled autofill field collection.</param>
|
||||
public static Dataset NewDataset(Context context,
|
||||
AutofillFieldMetadataCollection autofillFields,
|
||||
FilledAutofillFieldCollection filledAutofillFieldCollection,
|
||||
FilledAutofillFieldCollection<ViewNodeInputField> filledAutofillFieldCollection,
|
||||
IAutofillIntentBuilder intentBuilder,
|
||||
Android.Widget.Inline.InlinePresentationSpec inlinePresentationSpec)
|
||||
{
|
||||
@@ -108,21 +105,115 @@ namespace keepass2android.services.AutofillBase
|
||||
var datasetBuilder = new Dataset.Builder(NewRemoteViews(context.PackageName, datasetName, intentBuilder.AppIconResource));
|
||||
datasetBuilder.SetId(datasetName);
|
||||
|
||||
var setValueAtLeastOnce = filledAutofillFieldCollection.ApplyToFields(autofillFields, datasetBuilder);
|
||||
var setValueAtLeastOnce = ApplyToFields(filledAutofillFieldCollection, autofillFields, datasetBuilder);
|
||||
AddInlinePresentation(context, inlinePresentationSpec, datasetName, datasetBuilder, intentBuilder.AppIconResource, null);
|
||||
|
||||
if (setValueAtLeastOnce)
|
||||
{
|
||||
return datasetBuilder.Build();
|
||||
}
|
||||
else
|
||||
/*else
|
||||
{
|
||||
Kp2aLog.Log("Failed to set at least one value. #fields=" + autofillFields.GetAutofillIds().Length + " " + autofillFields.FocusedAutofillCanonicalHints);
|
||||
}
|
||||
}*/
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populates a Dataset.Builder with appropriate values for each AutofillId
|
||||
/// in a AutofillFieldMetadataCollection.
|
||||
///
|
||||
/// In other words, it constructs an autofill Dataset.Builder
|
||||
/// by applying saved values (from this FilledAutofillFieldCollection)
|
||||
/// to Views specified in a AutofillFieldMetadataCollection, which represents the current
|
||||
/// page the user is on.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c>, if to fields was applyed, <c>false</c> otherwise.</returns>
|
||||
/// <param name="filledAutofillFieldCollection"></param>
|
||||
/// <param name="autofillFieldMetadataCollection">Autofill field metadata collection.</param>
|
||||
/// <param name="datasetBuilder">Dataset builder.</param>
|
||||
public static bool ApplyToFields(FilledAutofillFieldCollection<ViewNodeInputField> filledAutofillFieldCollection,
|
||||
AutofillFieldMetadataCollection autofillFieldMetadataCollection, Dataset.Builder datasetBuilder)
|
||||
{
|
||||
bool setValueAtLeastOnce = false;
|
||||
|
||||
foreach (string hint in autofillFieldMetadataCollection.AllAutofillCanonicalHints)
|
||||
{
|
||||
foreach (AutofillFieldMetadata autofillFieldMetadata in autofillFieldMetadataCollection.GetFieldsForHint(hint))
|
||||
{
|
||||
FilledAutofillField<ViewNodeInputField> filledAutofillField;
|
||||
if (!filledAutofillFieldCollection.HintMap.TryGetValue(hint, out filledAutofillField) || (filledAutofillField == null))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var autofillId = autofillFieldMetadata.AutofillId;
|
||||
var autofillType = autofillFieldMetadata.AutofillType;
|
||||
switch (autofillType)
|
||||
{
|
||||
case AutofillType.List:
|
||||
var listValue = autofillFieldMetadata.GetAutofillOptionIndex(filledAutofillField.TextValue);
|
||||
if (listValue != -1)
|
||||
{
|
||||
datasetBuilder.SetValue(autofillId, AutofillValue.ForList(listValue));
|
||||
setValueAtLeastOnce = true;
|
||||
}
|
||||
break;
|
||||
case AutofillType.Date:
|
||||
var dateValue = filledAutofillField.DateValue;
|
||||
datasetBuilder.SetValue(autofillId, AutofillValue.ForDate((long)dateValue));
|
||||
setValueAtLeastOnce = true;
|
||||
break;
|
||||
case AutofillType.Text:
|
||||
var textValue = filledAutofillField.TextValue;
|
||||
if (textValue != null)
|
||||
{
|
||||
datasetBuilder.SetValue(autofillId, AutofillValue.ForText(textValue));
|
||||
setValueAtLeastOnce = true;
|
||||
}
|
||||
break;
|
||||
case AutofillType.Toggle:
|
||||
var toggleValue = filledAutofillField.ToggleValue;
|
||||
if (toggleValue != null)
|
||||
{
|
||||
datasetBuilder.SetValue(autofillId, AutofillValue.ForToggle(toggleValue.Value));
|
||||
setValueAtLeastOnce = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Log.Warn(CommonUtil.Tag, "Invalid autofill type - " + autofillType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
if (!setValueAtLeastOnce)
|
||||
{
|
||||
Kp2aLog.Log("No value set. Hint keys : " + string.Join(",", HintMap.Keys));
|
||||
foreach (string hint in autofillFieldMetadataCollection.AllAutofillCanonicalHints)
|
||||
{
|
||||
Kp2aLog.Log("No value set. Hint = " + hint);
|
||||
foreach (AutofillFieldMetadata autofillFieldMetadata in autofillFieldMetadataCollection
|
||||
.GetFieldsForHint(hint))
|
||||
{
|
||||
Kp2aLog.Log("No value set. fieldForHint = " + autofillFieldMetadata.AutofillId.ToString());
|
||||
FilledAutofillField filledAutofillField;
|
||||
if (!HintMap.TryGetValue(hint, out filledAutofillField) || (filledAutofillField == null))
|
||||
{
|
||||
Kp2aLog.Log("No value set. Hint map does not contain value, " +
|
||||
(filledAutofillField == null));
|
||||
continue;
|
||||
}
|
||||
|
||||
Kp2aLog.Log("autofill type=" + autofillFieldMetadata.AutofillType);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
return setValueAtLeastOnce;
|
||||
}
|
||||
|
||||
public static void AddInlinePresentation(Context context, InlinePresentationSpec inlinePresentationSpec,
|
||||
string datasetName, Dataset.Builder datasetBuilder, int iconId, PendingIntent pendingIntent)
|
||||
{
|
||||
|
||||
@@ -14,256 +14,5 @@ using keepass2android.services.AutofillBase.model;
|
||||
|
||||
namespace keepass2android.services.AutofillBase
|
||||
{
|
||||
class AutofillHintsHelper
|
||||
{
|
||||
private static readonly HashSet<string> _allSupportedHints = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
View.AutofillHintCreditCardExpirationDate,
|
||||
View.AutofillHintCreditCardExpirationDay,
|
||||
View.AutofillHintCreditCardExpirationMonth,
|
||||
View.AutofillHintCreditCardExpirationYear,
|
||||
View.AutofillHintCreditCardNumber,
|
||||
View.AutofillHintCreditCardSecurityCode,
|
||||
View.AutofillHintEmailAddress,
|
||||
View.AutofillHintPhone,
|
||||
View.AutofillHintName,
|
||||
View.AutofillHintPassword,
|
||||
View.AutofillHintPostalAddress,
|
||||
View.AutofillHintPostalCode,
|
||||
View.AutofillHintUsername,
|
||||
W3cHints.HONORIFIC_PREFIX,
|
||||
W3cHints.NAME,
|
||||
W3cHints.GIVEN_NAME,
|
||||
W3cHints.ADDITIONAL_NAME,
|
||||
W3cHints.FAMILY_NAME,
|
||||
W3cHints.HONORIFIC_SUFFIX,
|
||||
W3cHints.USERNAME,
|
||||
W3cHints.NEW_PASSWORD,
|
||||
W3cHints.CURRENT_PASSWORD,
|
||||
W3cHints.ORGANIZATION_TITLE,
|
||||
W3cHints.ORGANIZATION,
|
||||
W3cHints.STREET_ADDRESS,
|
||||
W3cHints.ADDRESS_LINE1,
|
||||
W3cHints.ADDRESS_LINE2,
|
||||
W3cHints.ADDRESS_LINE3,
|
||||
W3cHints.ADDRESS_LEVEL4,
|
||||
W3cHints.ADDRESS_LEVEL3,
|
||||
W3cHints.ADDRESS_LEVEL2,
|
||||
W3cHints.ADDRESS_LEVEL1,
|
||||
W3cHints.COUNTRY,
|
||||
W3cHints.COUNTRY_NAME,
|
||||
W3cHints.POSTAL_CODE,
|
||||
W3cHints.CC_NAME,
|
||||
W3cHints.CC_GIVEN_NAME,
|
||||
W3cHints.CC_ADDITIONAL_NAME,
|
||||
W3cHints.CC_FAMILY_NAME,
|
||||
W3cHints.CC_NUMBER,
|
||||
W3cHints.CC_EXPIRATION,
|
||||
W3cHints.CC_EXPIRATION_MONTH,
|
||||
W3cHints.CC_EXPIRATION_YEAR,
|
||||
W3cHints.CC_CSC,
|
||||
W3cHints.CC_TYPE,
|
||||
W3cHints.TRANSACTION_CURRENCY,
|
||||
W3cHints.TRANSACTION_AMOUNT,
|
||||
W3cHints.LANGUAGE,
|
||||
W3cHints.BDAY,
|
||||
W3cHints.BDAY_DAY,
|
||||
W3cHints.BDAY_MONTH,
|
||||
W3cHints.BDAY_YEAR,
|
||||
W3cHints.SEX,
|
||||
W3cHints.URL,
|
||||
W3cHints.PHOTO,
|
||||
W3cHints.TEL,
|
||||
W3cHints.TEL_COUNTRY_CODE,
|
||||
W3cHints.TEL_NATIONAL,
|
||||
W3cHints.TEL_AREA_CODE,
|
||||
W3cHints.TEL_LOCAL,
|
||||
W3cHints.TEL_LOCAL_PREFIX,
|
||||
W3cHints.TEL_LOCAL_SUFFIX,
|
||||
W3cHints.TEL_EXTENSION,
|
||||
W3cHints.EMAIL,
|
||||
W3cHints.IMPP,
|
||||
};
|
||||
|
||||
private static readonly List<HashSet<string>> partitionsOfCanonicalHints = new List<HashSet<string>>()
|
||||
{
|
||||
|
||||
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
View.AutofillHintEmailAddress,
|
||||
View.AutofillHintPhone,
|
||||
View.AutofillHintName,
|
||||
View.AutofillHintPassword,
|
||||
View.AutofillHintUsername,
|
||||
W3cHints.HONORIFIC_PREFIX,
|
||||
W3cHints.NAME,
|
||||
W3cHints.GIVEN_NAME,
|
||||
W3cHints.ADDITIONAL_NAME,
|
||||
W3cHints.FAMILY_NAME,
|
||||
W3cHints.HONORIFIC_SUFFIX,
|
||||
W3cHints.ORGANIZATION_TITLE,
|
||||
W3cHints.ORGANIZATION,
|
||||
W3cHints.LANGUAGE,
|
||||
W3cHints.BDAY,
|
||||
W3cHints.BDAY_DAY,
|
||||
W3cHints.BDAY_MONTH,
|
||||
W3cHints.BDAY_YEAR,
|
||||
W3cHints.SEX,
|
||||
W3cHints.URL,
|
||||
W3cHints.PHOTO,
|
||||
W3cHints.TEL,
|
||||
W3cHints.TEL_COUNTRY_CODE,
|
||||
W3cHints.TEL_NATIONAL,
|
||||
W3cHints.TEL_AREA_CODE,
|
||||
W3cHints.TEL_LOCAL,
|
||||
W3cHints.TEL_LOCAL_PREFIX,
|
||||
W3cHints.TEL_LOCAL_SUFFIX,
|
||||
W3cHints.TEL_EXTENSION,
|
||||
W3cHints.IMPP,
|
||||
},
|
||||
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
View.AutofillHintPostalAddress,
|
||||
View.AutofillHintPostalCode,
|
||||
|
||||
W3cHints.STREET_ADDRESS,
|
||||
W3cHints.ADDRESS_LINE1,
|
||||
W3cHints.ADDRESS_LINE2,
|
||||
W3cHints.ADDRESS_LINE3,
|
||||
W3cHints.ADDRESS_LEVEL4,
|
||||
W3cHints.ADDRESS_LEVEL3,
|
||||
W3cHints.ADDRESS_LEVEL2,
|
||||
W3cHints.ADDRESS_LEVEL1,
|
||||
W3cHints.COUNTRY,
|
||||
W3cHints.COUNTRY_NAME
|
||||
},
|
||||
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
View.AutofillHintCreditCardExpirationDate,
|
||||
View.AutofillHintCreditCardExpirationDay,
|
||||
View.AutofillHintCreditCardExpirationMonth,
|
||||
View.AutofillHintCreditCardExpirationYear,
|
||||
View.AutofillHintCreditCardNumber,
|
||||
View.AutofillHintCreditCardSecurityCode,
|
||||
|
||||
W3cHints.CC_NAME,
|
||||
W3cHints.CC_GIVEN_NAME,
|
||||
W3cHints.CC_ADDITIONAL_NAME,
|
||||
W3cHints.CC_FAMILY_NAME,
|
||||
W3cHints.CC_TYPE,
|
||||
W3cHints.TRANSACTION_CURRENCY,
|
||||
W3cHints.TRANSACTION_AMOUNT,
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, string> hintToCanonicalReplacement= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{W3cHints.EMAIL, View.AutofillHintEmailAddress},
|
||||
{W3cHints.USERNAME, View.AutofillHintUsername},
|
||||
{W3cHints.CURRENT_PASSWORD, View.AutofillHintPassword},
|
||||
{W3cHints.NEW_PASSWORD, View.AutofillHintPassword},
|
||||
{W3cHints.CC_EXPIRATION_MONTH, View.AutofillHintCreditCardExpirationMonth },
|
||||
{W3cHints.CC_EXPIRATION_YEAR, View.AutofillHintCreditCardExpirationYear },
|
||||
{W3cHints.CC_EXPIRATION, View.AutofillHintCreditCardExpirationDate },
|
||||
{W3cHints.CC_NUMBER, View.AutofillHintCreditCardNumber },
|
||||
{W3cHints.CC_CSC, View.AutofillHintCreditCardSecurityCode },
|
||||
{W3cHints.POSTAL_CODE, View.AutofillHintPostalCode },
|
||||
|
||||
|
||||
};
|
||||
|
||||
public static bool IsSupportedHint(string hint)
|
||||
{
|
||||
return _allSupportedHints.Contains(hint);
|
||||
}
|
||||
|
||||
|
||||
public static string[] FilterForSupportedHints(string[] hints)
|
||||
{
|
||||
var filteredHints = new string[hints.Length];
|
||||
int i = 0;
|
||||
foreach (var hint in hints)
|
||||
{
|
||||
if (IsSupportedHint(hint))
|
||||
{
|
||||
filteredHints[i++] = hint;
|
||||
}
|
||||
else
|
||||
{
|
||||
CommonUtil.logd("Invalid autofill hint: " + hint);
|
||||
}
|
||||
}
|
||||
var finalFilteredHints = new string[i];
|
||||
Array.Copy(filteredHints, 0, finalFilteredHints, 0, i);
|
||||
return finalFilteredHints;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// transforms hints by replacing some W3cHints by their Android counterparts and transforming everything to lowercase
|
||||
/// </summary>
|
||||
public static List<string> ConvertToCanonicalHints(string[] supportedHints)
|
||||
{
|
||||
List<string> result = new List<string>();
|
||||
foreach (string hint in supportedHints)
|
||||
{
|
||||
string canonicalHint;
|
||||
if (!hintToCanonicalReplacement.TryGetValue(hint, out canonicalHint))
|
||||
canonicalHint = hint;
|
||||
result.Add(canonicalHint.ToLower());
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
public static int GetPartitionIndex(string hint)
|
||||
{
|
||||
for (int i = 0; i < partitionsOfCanonicalHints.Count; i++)
|
||||
{
|
||||
if (partitionsOfCanonicalHints[i].Contains(hint))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static FilledAutofillFieldCollection FilterForPartition(FilledAutofillFieldCollection autofillFields, int partitionIndex)
|
||||
{
|
||||
FilledAutofillFieldCollection filteredCollection =
|
||||
new FilledAutofillFieldCollection {DatasetName = autofillFields.DatasetName};
|
||||
|
||||
if (partitionIndex == -1)
|
||||
return filteredCollection;
|
||||
|
||||
foreach (var field in autofillFields.HintMap.Values.Distinct())
|
||||
{
|
||||
foreach (var hint in field.AutofillHints)
|
||||
{
|
||||
if (GetPartitionIndex(hint) == partitionIndex)
|
||||
{
|
||||
filteredCollection.Add(field);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredCollection;
|
||||
}
|
||||
|
||||
public static FilledAutofillFieldCollection FilterForPartition(FilledAutofillFieldCollection filledAutofillFieldCollection, List<string> autofillFieldsFocusedAutofillCanonicalHints)
|
||||
{
|
||||
|
||||
//only apply partition data if we have FocusedAutofillCanonicalHints. This may be empty on buggy Firefox.
|
||||
if (autofillFieldsFocusedAutofillCanonicalHints.Any())
|
||||
{
|
||||
int partitionIndex = AutofillHintsHelper.GetPartitionIndex(autofillFieldsFocusedAutofillCanonicalHints.FirstOrDefault());
|
||||
return AutofillHintsHelper.FilterForPartition(filledAutofillFieldCollection, partitionIndex);
|
||||
}
|
||||
|
||||
return filledAutofillFieldCollection;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -137,7 +137,7 @@ namespace keepass2android.services.AutofillBase
|
||||
return;
|
||||
}
|
||||
|
||||
AutofillFieldMetadataCollection autofillFields = parser.AutofillFields;
|
||||
|
||||
InlineSuggestionsRequest inlineSuggestionsRequest = null;
|
||||
IList<InlinePresentationSpec> inlinePresentationSpecs = null;
|
||||
if (((int) Build.VERSION.SdkInt >= 30)
|
||||
@@ -149,7 +149,7 @@ namespace keepass2android.services.AutofillBase
|
||||
}
|
||||
|
||||
|
||||
var autofillIds = autofillFields.GetAutofillIds();
|
||||
var autofillIds = parser.AutofillFields.GetAutofillIds();
|
||||
if (autofillIds.Length != 0 && CanAutofill(query, isManual))
|
||||
{
|
||||
var responseBuilder = new FillResponse.Builder();
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
using System.Linq;
|
||||
using Android.Content;
|
||||
using Android.Preferences;
|
||||
using Kp2aAutofillParser;
|
||||
|
||||
namespace keepass2android.services.AutofillBase
|
||||
{
|
||||
|
||||
|
||||
internal class Kp2aDigitalAssetLinksDataSource
|
||||
internal class Kp2aDigitalAssetLinksDataSource : IKp2aDigitalAssetLinksDataSource
|
||||
{
|
||||
|
||||
private const string Autofilltrustedapps = "AutoFillTrustedApps";
|
||||
|
||||
@@ -11,10 +11,138 @@ using Android.Views.Autofill;
|
||||
using Android.Views.InputMethods;
|
||||
using DomainNameParser;
|
||||
using keepass2android.services.AutofillBase.model;
|
||||
using Kp2aAutofillParser;
|
||||
using Newtonsoft.Json;
|
||||
using static Android.App.Assist.AssistStructure;
|
||||
using static Java.IO.ObjectOutputStream;
|
||||
using FilledAutofillFieldCollection = keepass2android.services.AutofillBase.model.FilledAutofillFieldCollection;
|
||||
using InputTypes = Kp2aAutofillParser.InputTypes;
|
||||
|
||||
namespace keepass2android.services.AutofillBase
|
||||
{
|
||||
public class ViewNodeInputField : Kp2aAutofillParser.InputField
|
||||
{
|
||||
public ViewNodeInputField(AssistStructure.ViewNode viewNode)
|
||||
{
|
||||
ViewNode = viewNode;
|
||||
IdEntry = viewNode.IdEntry;
|
||||
Hint = viewNode.Hint;
|
||||
ClassName = viewNode.ClassName;
|
||||
AutofillHints = viewNode.GetAutofillHints();
|
||||
IsFocused = viewNode.IsFocused;
|
||||
InputType = (Kp2aAutofillParser.InputTypes) ((int)viewNode.InputType);
|
||||
HtmlInfoTag = viewNode.HtmlInfo?.Tag;
|
||||
HtmlInfoTypeAttribute = viewNode.HtmlInfo?.Attributes?.FirstOrDefault(p => p.First?.ToString() == "type")?.Second?.ToString();
|
||||
|
||||
}
|
||||
[JsonIgnore]
|
||||
public AssistStructure.ViewNode ViewNode { get; set; }
|
||||
|
||||
public override void FillFilledAutofillValue(FilledAutofillField filledField)
|
||||
{
|
||||
AutofillValue autofillValue = ViewNode.AutofillValue;
|
||||
if (autofillValue != null)
|
||||
{
|
||||
if (autofillValue.IsList)
|
||||
{
|
||||
string[] autofillOptions = ViewNode.GetAutofillOptions();
|
||||
int index = autofillValue.ListValue;
|
||||
if (autofillOptions != null && autofillOptions.Length > 0)
|
||||
{
|
||||
filledField.TextValue = autofillOptions[index];
|
||||
}
|
||||
}
|
||||
else if (autofillValue.IsDate)
|
||||
{
|
||||
filledField.DateValue = autofillValue.DateValue;
|
||||
}
|
||||
else if (autofillValue.IsText)
|
||||
{
|
||||
filledField.TextValue = autofillValue.TextValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an AssistStructure into a list of InputFields
|
||||
/// </summary>
|
||||
class AutofillViewFromAssistStructureFinder
|
||||
{
|
||||
private readonly Context _context;
|
||||
private readonly AssistStructure _structure;
|
||||
private PublicSuffixRuleCache domainSuffixParserCache;
|
||||
|
||||
public AutofillViewFromAssistStructureFinder(Context context, AssistStructure structure)
|
||||
{
|
||||
_context = context;
|
||||
_structure = structure;
|
||||
domainSuffixParserCache = new PublicSuffixRuleCache(context);
|
||||
}
|
||||
|
||||
public AutofillView<ViewNodeInputField> GetAutofillView(bool isManualRequest)
|
||||
{
|
||||
AutofillView<ViewNodeInputField> autofillView = new AutofillView<ViewNodeInputField>();
|
||||
|
||||
|
||||
int nodeCount = _structure.WindowNodeCount;
|
||||
for (int i = 0; i < nodeCount; i++)
|
||||
{
|
||||
var node = _structure.GetWindowNodeAt(i);
|
||||
|
||||
var view = node.RootViewNode;
|
||||
ParseRecursive(autofillView, view, isManualRequest);
|
||||
}
|
||||
|
||||
return autofillView;
|
||||
|
||||
}
|
||||
|
||||
|
||||
void ParseRecursive(AutofillView<ViewNodeInputField> autofillView, AssistStructure.ViewNode viewNode, bool isManualRequest)
|
||||
{
|
||||
String webDomain = viewNode.WebDomain;
|
||||
if ((autofillView.PackageId == null) && (!string.IsNullOrWhiteSpace(viewNode.IdPackage)) &&
|
||||
(viewNode.IdPackage != "android"))
|
||||
{
|
||||
autofillView.PackageId = viewNode.IdPackage;
|
||||
}
|
||||
|
||||
DomainName outDomain;
|
||||
if (DomainName.TryParse(webDomain, domainSuffixParserCache, out outDomain))
|
||||
{
|
||||
webDomain = outDomain.RawDomainName;
|
||||
}
|
||||
|
||||
if (webDomain != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(autofillView.WebDomain))
|
||||
{
|
||||
if (webDomain != autofillView.WebDomain)
|
||||
{
|
||||
throw new Java.Lang.SecurityException($"Found multiple web domains: valid= {autofillView.WebDomain}, child={webDomain}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
autofillView.WebDomain = webDomain;
|
||||
}
|
||||
}
|
||||
|
||||
autofillView.InputFields.Add(new ViewNodeInputField(viewNode));
|
||||
Kp2aLog.Log($"Now we have {autofillView.InputFields.Count} fields, just added {autofillView.InputFields.Last().IdEntry} of type {autofillView.InputFields.Last().ClassName}");
|
||||
|
||||
var childrenSize = viewNode.ChildCount;
|
||||
if (childrenSize > 0)
|
||||
{
|
||||
for (int i = 0; i < childrenSize; i++)
|
||||
{
|
||||
ParseRecursive(autofillView, viewNode.GetChildAt(i), isManualRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parser for an AssistStructure object. This is invoked when the Autofill Service receives an
|
||||
/// AssistStructure from the client Activity, representing its View hierarchy. In this sample, it
|
||||
@@ -24,10 +152,7 @@ namespace keepass2android.services.AutofillBase
|
||||
{
|
||||
public Context mContext { get; }
|
||||
public AutofillFieldMetadataCollection AutofillFields { get; set; }
|
||||
AssistStructure Structure;
|
||||
private List<AssistStructure.ViewNode> _editTextsWithoutHint = new List<AssistStructure.ViewNode>();
|
||||
private PublicSuffixRuleCache domainSuffixParserCache;
|
||||
public FilledAutofillFieldCollection ClientFormData { get; set; }
|
||||
public FilledAutofillFieldCollection ClientFormData { get; set; }
|
||||
|
||||
public string PackageId { get; set; }
|
||||
|
||||
@@ -37,7 +162,7 @@ namespace keepass2android.services.AutofillBase
|
||||
mContext = context;
|
||||
Structure = structure;
|
||||
AutofillFields = new AutofillFieldMetadataCollection();
|
||||
domainSuffixParserCache = new PublicSuffixRuleCache(context);
|
||||
|
||||
}
|
||||
|
||||
public class AutofillTargetId
|
||||
@@ -87,23 +212,73 @@ namespace keepass2android.services.AutofillBase
|
||||
{
|
||||
AutofillTargetId result = new AutofillTargetId();
|
||||
CommonUtil.logd("Parsing structure for " + Structure.ActivityComponent);
|
||||
var nodes = Structure.WindowNodeCount;
|
||||
|
||||
ClientFormData = new FilledAutofillFieldCollection();
|
||||
String webDomain = null;
|
||||
|
||||
_editTextsWithoutHint.Clear();
|
||||
|
||||
for (int i = 0; i < nodes; i++)
|
||||
{
|
||||
var node = Structure.GetWindowNodeAt(i);
|
||||
Kp2aLog.Log("parsing autofillStructure...");
|
||||
|
||||
var view = node.RootViewNode;
|
||||
ParseLocked(forFill, isManualRequest, view, ref webDomain);
|
||||
}
|
||||
AutofillView<ViewNodeInputField> autofillView = new AutofillViewFromAssistStructureFinder(mContext, Structure).GetAutofillView(isManualRequest);
|
||||
|
||||
//TODO remove from production
|
||||
Kp2aLog.Log("will log the autofillStructure...");
|
||||
string debugInfo = JsonConvert.SerializeObject(autofillView, Formatting.Indented);
|
||||
Kp2aLog.Log("will log the autofillStructure... size is " + debugInfo.Length);
|
||||
Kp2aLog.Log("This is the autofillStructure: \n\n " + debugInfo);
|
||||
|
||||
foreach (var viewNode in autofillView.InputFields)
|
||||
{
|
||||
string[] viewHints = viewNode.AutofillHints;
|
||||
if (viewHints != null && viewHints.Length == 1 && viewHints.First() == "off" && viewNode.IsFocused &&
|
||||
isManualRequest)
|
||||
viewHints[0] = "on";
|
||||
/*if (viewHints != null && viewHints.Any())
|
||||
{
|
||||
CommonUtil.logd("viewHints=" + viewHints);
|
||||
CommonUtil.logd("class=" + viewNode.ClassName);
|
||||
CommonUtil.logd("tag=" + (viewNode?.HtmlInfo?.Tag ?? "(null)"));
|
||||
}*/
|
||||
|
||||
List<AssistStructure.ViewNode> passwordFields = new List<AssistStructure.ViewNode>();
|
||||
List<AssistStructure.ViewNode> usernameFields = new List<AssistStructure.ViewNode>();
|
||||
if (IsPassword(viewNode) || HasPasswordHint(viewNode) || (HasUsernameHint(viewNode)))
|
||||
{
|
||||
if (forFill)
|
||||
{
|
||||
AutofillFields.Add(new AutofillFieldMetadata(viewNode.ViewNode));
|
||||
}
|
||||
else
|
||||
{
|
||||
FilledAutofillField filledAutofillField = new FilledAutofillField(viewNode.ViewNode);
|
||||
ClientFormData.Add(filledAutofillField);
|
||||
}
|
||||
}
|
||||
else if (viewNode.ClassName == "android.widget.EditText"
|
||||
|| viewNode.ClassName == "android.widget.AutoCompleteTextView"
|
||||
|| viewNode.HtmlInfoTag == "input"
|
||||
|| ((viewHints?.Length ?? 0) > 0))
|
||||
{
|
||||
Kp2aLog.Log("Found something that looks fillable " + viewNode.ClassName);
|
||||
|
||||
}
|
||||
|
||||
if (viewHints != null && viewHints.Length > 0 && viewHints.First() != "on" /*if hint is "on", treat as if there is no hint*/)
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
if (viewNode.ClassName == "android.widget.EditText"
|
||||
|| viewNode.ClassName == "android.widget.AutoCompleteTextView"
|
||||
|| viewNode.HtmlInfoTag == "input")
|
||||
{
|
||||
_editTextsWithoutHint.Add(viewNode);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
List<ViewNodeInputField> passwordFields = new List<ViewNodeInputField>();
|
||||
List<ViewNodeInputField> usernameFields = new List<ViewNodeInputField>();
|
||||
if (AutofillFields.Empty)
|
||||
{
|
||||
passwordFields = _editTextsWithoutHint.Where(IsPassword).ToList();
|
||||
@@ -120,7 +295,7 @@ namespace keepass2android.services.AutofillBase
|
||||
foreach (var passwordField in passwordFields)
|
||||
{
|
||||
var usernameField = _editTextsWithoutHint
|
||||
.TakeWhile(f => f.AutofillId != passwordField.AutofillId).LastOrDefault();
|
||||
.TakeWhile(f => f != passwordField).LastOrDefault();
|
||||
if (usernameField != null)
|
||||
{
|
||||
usernameFields.Add(usernameField);
|
||||
@@ -143,7 +318,7 @@ namespace keepass2android.services.AutofillBase
|
||||
//force focused fields to be included in autofill fields when request was triggered manually. This allows to fill fields which are "off" or don't have a hint (in case there are hints)
|
||||
if (isManualRequest)
|
||||
{
|
||||
foreach (AssistStructure.ViewNode editText in _editTextsWithoutHint)
|
||||
foreach (var editText in _editTextsWithoutHint)
|
||||
{
|
||||
if (editText.IsFocused)
|
||||
{
|
||||
@@ -160,25 +335,25 @@ namespace keepass2android.services.AutofillBase
|
||||
if (forFill)
|
||||
{
|
||||
foreach (var uf in usernameFields)
|
||||
AutofillFields.Add(new AutofillFieldMetadata(uf, new[] { View.AutofillHintUsername }));
|
||||
AutofillFields.Add(new AutofillFieldMetadata(uf.ViewNode, new[] { View.AutofillHintUsername }));
|
||||
foreach (var pf in passwordFields)
|
||||
AutofillFields.Add(new AutofillFieldMetadata(pf, new[] { View.AutofillHintPassword }));
|
||||
AutofillFields.Add(new AutofillFieldMetadata(pf.ViewNode, new[] { View.AutofillHintPassword }));
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var uf in usernameFields)
|
||||
ClientFormData.Add(new FilledAutofillField(uf, new[] { View.AutofillHintUsername }));
|
||||
ClientFormData.Add(new FilledAutofillField(uf.ViewNode, new[] { View.AutofillHintUsername }));
|
||||
foreach (var pf in passwordFields)
|
||||
ClientFormData.Add(new FilledAutofillField(pf, new[] { View.AutofillHintPassword }));
|
||||
ClientFormData.Add(new FilledAutofillField(pf.ViewNode, new[] { View.AutofillHintPassword }));
|
||||
}
|
||||
|
||||
|
||||
result.WebDomain = webDomain;
|
||||
result.WebDomain = autofillView.WebDomain;
|
||||
result.PackageName = Structure.ActivityComponent.PackageName;
|
||||
if (!string.IsNullOrEmpty(webDomain) && !PreferenceManager.GetDefaultSharedPreferences(mContext).GetBoolean(mContext.GetString(Resource.String.NoDalVerification_key), false))
|
||||
if (!string.IsNullOrEmpty(autofillView.WebDomain) && !PreferenceManager.GetDefaultSharedPreferences(mContext).GetBoolean(mContext.GetString(Resource.String.NoDalVerification_key), false))
|
||||
{
|
||||
result.IncompatiblePackageAndDomain = !kp2aDigitalAssetLinksDataSource.IsTrustedLink(webDomain, result.PackageName);
|
||||
result.IncompatiblePackageAndDomain = !kp2aDigitalAssetLinksDataSource.IsTrustedLink(autofillView.WebDomain, result.PackageName);
|
||||
if (result.IncompatiblePackageAndDomain)
|
||||
{
|
||||
CommonUtil.loge($"DAL verification failed for {result.PackageName}/{result.WebDomain}");
|
||||
@@ -190,17 +365,17 @@ namespace keepass2android.services.AutofillBase
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static readonly HashSet<string> _passwordHints = new HashSet<string> { "password","passwort" };
|
||||
private static bool HasPasswordHint(AssistStructure.ViewNode f)
|
||||
private static readonly HashSet<string> _passwordHints = new HashSet<string> { "password","passwort", "passwordAuto", "pswd" };
|
||||
private static bool HasPasswordHint(InputField f)
|
||||
{
|
||||
return ContainsAny(f.IdEntry, _passwordHints) ||
|
||||
ContainsAny(f.Hint, _passwordHints);
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> _usernameHints = new HashSet<string> { "email","e-mail","username" };
|
||||
private Kp2aDigitalAssetLinksDataSource kp2aDigitalAssetLinksDataSource;
|
||||
private readonly Kp2aDigitalAssetLinksDataSource kp2aDigitalAssetLinksDataSource;
|
||||
|
||||
private static bool HasUsernameHint(AssistStructure.ViewNode f)
|
||||
private static bool HasUsernameHint(InputField f)
|
||||
{
|
||||
return ContainsAny(f.IdEntry, _usernameHints) ||
|
||||
ContainsAny(f.Hint, _usernameHints);
|
||||
@@ -233,7 +408,7 @@ namespace keepass2android.services.AutofillBase
|
||||
|
||||
}
|
||||
|
||||
private static bool IsPassword(AssistStructure.ViewNode f)
|
||||
private static bool IsPassword(InputField f)
|
||||
{
|
||||
InputTypes inputType = f.InputType;
|
||||
|
||||
@@ -249,86 +424,14 @@ namespace keepass2android.services.AutofillBase
|
||||
|| IsInputTypeVariation(inputType, InputTypes.TextVariationWebPassword)
|
||||
)
|
||||
)
|
||||
|| (f.HtmlInfo?.Attributes.Any(p => p.First.ToString() == "type" && p.Second.ToString() == "password") ?? false)
|
||||
|| (f.HtmlInfoTypeAttribute == "password")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void ParseLocked(bool forFill, bool isManualRequest, AssistStructure.ViewNode viewNode, ref string validWebdomain)
|
||||
{
|
||||
String webDomain = viewNode.WebDomain;
|
||||
if ((PackageId == null) && (!string.IsNullOrWhiteSpace(viewNode.IdPackage)) &&
|
||||
(viewNode.IdPackage != "android"))
|
||||
{
|
||||
PackageId = viewNode.IdPackage;
|
||||
}
|
||||
|
||||
DomainName outDomain;
|
||||
if (DomainName.TryParse(webDomain, domainSuffixParserCache, out outDomain))
|
||||
{
|
||||
webDomain = outDomain.RawDomainName;
|
||||
}
|
||||
|
||||
if (webDomain != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(validWebdomain))
|
||||
{
|
||||
if (webDomain != validWebdomain)
|
||||
{
|
||||
throw new Java.Lang.SecurityException($"Found multiple web domains: valid= {validWebdomain}, child={webDomain}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
validWebdomain = webDomain;
|
||||
}
|
||||
}
|
||||
|
||||
string[] viewHints = viewNode.GetAutofillHints();
|
||||
if (viewHints != null && viewHints.Length == 1 && viewHints.First() == "off" && viewNode.IsFocused &&
|
||||
isManualRequest)
|
||||
viewHints[0] = "on";
|
||||
/*if (viewHints != null && viewHints.Any())
|
||||
{
|
||||
CommonUtil.logd("viewHints=" + viewHints);
|
||||
CommonUtil.logd("class=" + viewNode.ClassName);
|
||||
CommonUtil.logd("tag=" + (viewNode?.HtmlInfo?.Tag ?? "(null)"));
|
||||
}*/
|
||||
|
||||
|
||||
if (viewHints != null && viewHints.Length > 0 && viewHints.First() != "on" /*if hint is "on", treat as if there is no hint*/)
|
||||
{
|
||||
if (forFill)
|
||||
{
|
||||
AutofillFields.Add(new AutofillFieldMetadata(viewNode));
|
||||
}
|
||||
else
|
||||
{
|
||||
FilledAutofillField filledAutofillField = new FilledAutofillField(viewNode);
|
||||
ClientFormData.Add(filledAutofillField);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
if (viewNode.ClassName == "android.widget.EditText"
|
||||
|| viewNode.ClassName == "android.widget.AutoCompleteTextView"
|
||||
|| viewNode?.HtmlInfo?.Tag == "input")
|
||||
{
|
||||
_editTextsWithoutHint.Add(viewNode);
|
||||
}
|
||||
|
||||
}
|
||||
var childrenSize = viewNode.ChildCount;
|
||||
if (childrenSize > 0)
|
||||
{
|
||||
for (int i = 0; i < childrenSize; i++)
|
||||
{
|
||||
ParseLocked(forFill, isManualRequest, viewNode.GetChildAt(i), ref validWebdomain);
|
||||
}
|
||||
}
|
||||
}
|
||||
AssistStructure Structure;
|
||||
private List<ViewNodeInputField> _editTextsWithoutHint = new List<ViewNodeInputField>();
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,154 +2,9 @@
|
||||
using Android.App.Assist;
|
||||
using Android.Views.Autofill;
|
||||
using KeePassLib.Utility;
|
||||
using Kp2aAutofillParser;
|
||||
|
||||
namespace keepass2android.services.AutofillBase.model
|
||||
{
|
||||
public class FilledAutofillField
|
||||
{
|
||||
private string[] _autofillHints;
|
||||
public string TextValue { get; set; }
|
||||
public long? DateValue { get; set; }
|
||||
public bool? ToggleValue { get; set; }
|
||||
|
||||
public string ValueToString()
|
||||
{
|
||||
if (DateValue != null)
|
||||
{
|
||||
return TimeUtil.ConvertUnixTime((long)DateValue / 1000.0).ToLongDateString();
|
||||
}
|
||||
if (ToggleValue != null)
|
||||
return ToggleValue.ToString();
|
||||
return TextValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns the autofill hints for the filled field. These are always lowercased for simpler string comparison.
|
||||
/// </summary>
|
||||
public string[] AutofillHints
|
||||
{
|
||||
get
|
||||
{
|
||||
return _autofillHints;
|
||||
}
|
||||
set
|
||||
{
|
||||
_autofillHints = value;
|
||||
for (int i = 0; i < _autofillHints.Length; i++)
|
||||
_autofillHints[i] = _autofillHints[i].ToLower();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public FilledAutofillField()
|
||||
{}
|
||||
|
||||
public FilledAutofillField(AssistStructure.ViewNode viewNode)
|
||||
: this(viewNode, viewNode.GetAutofillHints())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public FilledAutofillField(AssistStructure.ViewNode viewNode, string[] hints)
|
||||
{
|
||||
|
||||
string[] rawHints = AutofillHintsHelper.FilterForSupportedHints(hints);
|
||||
List<string> hintList = new List<string>();
|
||||
|
||||
string nextHint = null;
|
||||
for (int i = 0; i < rawHints.Length; i++)
|
||||
{
|
||||
string hint = rawHints[i];
|
||||
if (i < rawHints.Length - 1)
|
||||
{
|
||||
nextHint = rawHints[i + 1];
|
||||
}
|
||||
// First convert the compound W3C autofill hints
|
||||
if (W3cHints.isW3cSectionPrefix(hint) && i < rawHints.Length - 1)
|
||||
{
|
||||
hint = rawHints[++i];
|
||||
CommonUtil.logd($"Hint is a W3C section prefix; using {hint} instead");
|
||||
if (i < rawHints.Length - 1)
|
||||
{
|
||||
nextHint = rawHints[i + 1];
|
||||
}
|
||||
}
|
||||
if (W3cHints.isW3cTypePrefix(hint) && nextHint != null && W3cHints.isW3cTypeHint(nextHint))
|
||||
{
|
||||
hint = nextHint;
|
||||
i++;
|
||||
CommonUtil.logd($"Hint is a W3C type prefix; using {hint} instead");
|
||||
}
|
||||
if (W3cHints.isW3cAddressType(hint) && nextHint != null)
|
||||
{
|
||||
hint = nextHint;
|
||||
i++;
|
||||
CommonUtil.logd($"Hint is a W3C address prefix; using {hint} instead");
|
||||
}
|
||||
|
||||
// Then check if the "actual" hint is supported.
|
||||
if (AutofillHintsHelper.IsSupportedHint(hint))
|
||||
{
|
||||
hintList.Add(hint);
|
||||
}
|
||||
else
|
||||
{
|
||||
CommonUtil.loge($"Invalid hint: {rawHints[i]}");
|
||||
}
|
||||
}
|
||||
AutofillHints = AutofillHintsHelper.ConvertToCanonicalHints(hintList.ToArray()).ToArray();
|
||||
|
||||
AutofillValue autofillValue = viewNode.AutofillValue;
|
||||
if (autofillValue != null)
|
||||
{
|
||||
if (autofillValue.IsList)
|
||||
{
|
||||
string[] autofillOptions = viewNode.GetAutofillOptions();
|
||||
int index = autofillValue.ListValue;
|
||||
if (autofillOptions != null && autofillOptions.Length > 0)
|
||||
{
|
||||
TextValue = autofillOptions[index];
|
||||
}
|
||||
}
|
||||
else if (autofillValue.IsDate)
|
||||
{
|
||||
DateValue = autofillValue.DateValue;
|
||||
}
|
||||
else if (autofillValue.IsText)
|
||||
{
|
||||
TextValue = autofillValue.TextValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsNull()
|
||||
{
|
||||
return TextValue == null && DateValue == null && ToggleValue == null;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (this == obj) return true;
|
||||
if (obj == null || GetType() != obj.GetType()) return false;
|
||||
|
||||
FilledAutofillField that = (FilledAutofillField)obj;
|
||||
|
||||
if (!TextValue?.Equals(that.TextValue) ?? that.TextValue != null)
|
||||
return false;
|
||||
if (DateValue != null ? !DateValue.Equals(that.DateValue) : that.DateValue != null)
|
||||
return false;
|
||||
return ToggleValue != null ? ToggleValue.Equals(that.ToggleValue) : that.ToggleValue == null;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var result = TextValue != null ? TextValue.GetHashCode() : 0;
|
||||
result = 31 * result + (DateValue != null ? DateValue.GetHashCode() : 0);
|
||||
result = 31 * result + (ToggleValue != null ? ToggleValue.GetHashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,164 +7,5 @@ using Android.Views.Autofill;
|
||||
|
||||
namespace keepass2android.services.AutofillBase.model
|
||||
{
|
||||
/// <summary>
|
||||
/// FilledAutofillFieldCollection is the model that holds all of the data on a client app's page,
|
||||
/// plus the dataset name associated with it.
|
||||
/// </summary>
|
||||
public class FilledAutofillFieldCollection
|
||||
{
|
||||
public Dictionary<string, FilledAutofillField> HintMap { get; }
|
||||
public string DatasetName { get; set; }
|
||||
|
||||
public FilledAutofillFieldCollection(Dictionary<string, FilledAutofillField> hintMap, string datasetName = "")
|
||||
{
|
||||
//recreate hint map making sure we compare case insensitive
|
||||
HintMap = BuildHintMap();
|
||||
foreach (var p in hintMap)
|
||||
HintMap.Add(p.Key, p.Value);
|
||||
DatasetName = datasetName;
|
||||
}
|
||||
|
||||
public FilledAutofillFieldCollection() : this(BuildHintMap())
|
||||
{}
|
||||
|
||||
private static Dictionary<string, FilledAutofillField> BuildHintMap()
|
||||
{
|
||||
return new Dictionary<string, FilledAutofillField>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a filledAutofillField to the collection, indexed by all of its hints.
|
||||
/// </summary>
|
||||
/// <returns>The add.</returns>
|
||||
/// <param name="filledAutofillField">Filled autofill field.</param>
|
||||
public void Add(FilledAutofillField filledAutofillField)
|
||||
{
|
||||
foreach (string hint in filledAutofillField.AutofillHints)
|
||||
{
|
||||
if (AutofillHintsHelper.IsSupportedHint(hint))
|
||||
{
|
||||
HintMap.TryAdd(hint, filledAutofillField);
|
||||
}
|
||||
else
|
||||
{
|
||||
CommonUtil.loge($"Invalid hint: {hint}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Populates a Dataset.Builder with appropriate values for each AutofillId
|
||||
/// in a AutofillFieldMetadataCollection.
|
||||
///
|
||||
/// In other words, it constructs an autofill Dataset.Builder
|
||||
/// by applying saved values (from this FilledAutofillFieldCollection)
|
||||
/// to Views specified in a AutofillFieldMetadataCollection, which represents the current
|
||||
/// page the user is on.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c>, if to fields was applyed, <c>false</c> otherwise.</returns>
|
||||
/// <param name="autofillFieldMetadataCollection">Autofill field metadata collection.</param>
|
||||
/// <param name="datasetBuilder">Dataset builder.</param>
|
||||
public bool ApplyToFields(AutofillFieldMetadataCollection autofillFieldMetadataCollection, Dataset.Builder datasetBuilder)
|
||||
{
|
||||
bool setValueAtLeastOnce = false;
|
||||
|
||||
foreach (string hint in autofillFieldMetadataCollection.AllAutofillCanonicalHints)
|
||||
{
|
||||
foreach (AutofillFieldMetadata autofillFieldMetadata in autofillFieldMetadataCollection.GetFieldsForHint(hint))
|
||||
{
|
||||
FilledAutofillField filledAutofillField;
|
||||
if (!HintMap.TryGetValue(hint, out filledAutofillField) || (filledAutofillField == null))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var autofillId = autofillFieldMetadata.AutofillId;
|
||||
var autofillType = autofillFieldMetadata.AutofillType;
|
||||
switch (autofillType)
|
||||
{
|
||||
case AutofillType.List:
|
||||
var listValue = autofillFieldMetadata.GetAutofillOptionIndex(filledAutofillField.TextValue);
|
||||
if (listValue != -1)
|
||||
{
|
||||
datasetBuilder.SetValue(autofillId, AutofillValue.ForList(listValue));
|
||||
setValueAtLeastOnce = true;
|
||||
}
|
||||
break;
|
||||
case AutofillType.Date:
|
||||
var dateValue = filledAutofillField.DateValue;
|
||||
datasetBuilder.SetValue(autofillId, AutofillValue.ForDate((long)dateValue));
|
||||
setValueAtLeastOnce = true;
|
||||
break;
|
||||
case AutofillType.Text:
|
||||
var textValue = filledAutofillField.TextValue;
|
||||
if (textValue != null)
|
||||
{
|
||||
datasetBuilder.SetValue(autofillId, AutofillValue.ForText(textValue));
|
||||
setValueAtLeastOnce = true;
|
||||
}
|
||||
break;
|
||||
case AutofillType.Toggle:
|
||||
var toggleValue = filledAutofillField.ToggleValue;
|
||||
if (toggleValue != null)
|
||||
{
|
||||
datasetBuilder.SetValue(autofillId, AutofillValue.ForToggle(toggleValue.Value));
|
||||
setValueAtLeastOnce = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Log.Warn(CommonUtil.Tag, "Invalid autofill type - " + autofillType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
if (!setValueAtLeastOnce)
|
||||
{
|
||||
Kp2aLog.Log("No value set. Hint keys : " + string.Join(",", HintMap.Keys));
|
||||
foreach (string hint in autofillFieldMetadataCollection.AllAutofillCanonicalHints)
|
||||
{
|
||||
Kp2aLog.Log("No value set. Hint = " + hint);
|
||||
foreach (AutofillFieldMetadata autofillFieldMetadata in autofillFieldMetadataCollection
|
||||
.GetFieldsForHint(hint))
|
||||
{
|
||||
Kp2aLog.Log("No value set. fieldForHint = " + autofillFieldMetadata.AutofillId.ToString());
|
||||
FilledAutofillField filledAutofillField;
|
||||
if (!HintMap.TryGetValue(hint, out filledAutofillField) || (filledAutofillField == null))
|
||||
{
|
||||
Kp2aLog.Log("No value set. Hint map does not contain value, " +
|
||||
(filledAutofillField == null));
|
||||
continue;
|
||||
}
|
||||
|
||||
Kp2aLog.Log("autofill type=" + autofillFieldMetadata.AutofillType);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
return setValueAtLeastOnce;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes in a list of autofill hints (`autofillHints`), usually associated with a View or set of
|
||||
/// Views. Returns whether any of the filled fields on the page have at least 1 of these
|
||||
/// `autofillHint`s.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c>, if with hints was helpsed, <c>false</c> otherwise.</returns>
|
||||
/// <param name="autofillHints">Autofill hints.</param>
|
||||
public bool HelpsWithHints(List<string> autofillHints)
|
||||
{
|
||||
for (int i = 0; i < autofillHints.Count; i++)
|
||||
{
|
||||
var autofillHint = autofillHints[i];
|
||||
if (HintMap.ContainsKey(autofillHint) && !HintMap[autofillHint].IsNull())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,126 +2,5 @@
|
||||
|
||||
namespace keepass2android.services.AutofillBase.model
|
||||
{
|
||||
public class W3cHints
|
||||
{
|
||||
|
||||
// Supported W3C autofill tokens (https://html.spec.whatwg.org/multipage/forms.html#autofill)
|
||||
public const string HONORIFIC_PREFIX = "honorific-prefix";
|
||||
public const string NAME = "name";
|
||||
public const string GIVEN_NAME = "given-name";
|
||||
public const string ADDITIONAL_NAME = "additional-name";
|
||||
public const string FAMILY_NAME = "family-name";
|
||||
public const string HONORIFIC_SUFFIX = "honorific-suffix";
|
||||
public const string USERNAME = "username";
|
||||
public const string NEW_PASSWORD = "new-password";
|
||||
public const string CURRENT_PASSWORD = "current-password";
|
||||
public const string ORGANIZATION_TITLE = "organization-title";
|
||||
public const string ORGANIZATION = "organization";
|
||||
public const string STREET_ADDRESS = "street-address";
|
||||
public const string ADDRESS_LINE1 = "address-line1";
|
||||
public const string ADDRESS_LINE2 = "address-line2";
|
||||
public const string ADDRESS_LINE3 = "address-line3";
|
||||
public const string ADDRESS_LEVEL4 = "address-level4";
|
||||
public const string ADDRESS_LEVEL3 = "address-level3";
|
||||
public const string ADDRESS_LEVEL2 = "address-level2";
|
||||
public const string ADDRESS_LEVEL1 = "address-level1";
|
||||
public const string COUNTRY = "country";
|
||||
public const string COUNTRY_NAME = "country-name";
|
||||
public const string POSTAL_CODE = "postal-code";
|
||||
public const string CC_NAME = "cc-name";
|
||||
public const string CC_GIVEN_NAME = "cc-given-name";
|
||||
public const string CC_ADDITIONAL_NAME = "cc-additional-name";
|
||||
public const string CC_FAMILY_NAME = "cc-family-name";
|
||||
public const string CC_NUMBER = "cc-number";
|
||||
public const string CC_EXPIRATION = "cc-exp";
|
||||
public const string CC_EXPIRATION_MONTH = "cc-exp-month";
|
||||
public const string CC_EXPIRATION_YEAR = "cc-exp-year";
|
||||
public const string CC_CSC = "cc-csc";
|
||||
public const string CC_TYPE = "cc-type";
|
||||
public const string TRANSACTION_CURRENCY = "transaction-currency";
|
||||
public const string TRANSACTION_AMOUNT = "transaction-amount";
|
||||
public const string LANGUAGE = "language";
|
||||
public const string BDAY = "bday";
|
||||
public const string BDAY_DAY = "bday-day";
|
||||
public const string BDAY_MONTH = "bday-month";
|
||||
public const string BDAY_YEAR = "bday-year";
|
||||
public const string SEX = "sex";
|
||||
public const string URL = "url";
|
||||
public const string PHOTO = "photo";
|
||||
// Optional W3C prefixes
|
||||
public const string PREFIX_SECTION = "section-";
|
||||
public const string SHIPPING = "shipping";
|
||||
public const string BILLING = "billing";
|
||||
// W3C prefixes below...
|
||||
public const string PREFIX_HOME = "home";
|
||||
public const string PREFIX_WORK = "work";
|
||||
public const string PREFIX_FAX = "fax";
|
||||
public const string PREFIX_PAGER = "pager";
|
||||
// ... require those suffix
|
||||
public const string TEL = "tel";
|
||||
public const string TEL_COUNTRY_CODE = "tel-country-code";
|
||||
public const string TEL_NATIONAL = "tel-national";
|
||||
public const string TEL_AREA_CODE = "tel-area-code";
|
||||
public const string TEL_LOCAL = "tel-local";
|
||||
public const string TEL_LOCAL_PREFIX = "tel-local-prefix";
|
||||
public const string TEL_LOCAL_SUFFIX = "tel-local-suffix";
|
||||
public const string TEL_EXTENSION = "tel_extension";
|
||||
public const string EMAIL = "email";
|
||||
public const string IMPP = "impp";
|
||||
|
||||
private W3cHints()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static bool isW3cSectionPrefix(string hint)
|
||||
{
|
||||
return hint.ToLower().StartsWith(W3cHints.PREFIX_SECTION);
|
||||
}
|
||||
|
||||
public static bool isW3cAddressType(string hint)
|
||||
{
|
||||
switch (hint.ToLower())
|
||||
{
|
||||
case W3cHints.SHIPPING:
|
||||
case W3cHints.BILLING:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool isW3cTypePrefix(string hint)
|
||||
{
|
||||
switch (hint.ToLower())
|
||||
{
|
||||
case W3cHints.PREFIX_WORK:
|
||||
case W3cHints.PREFIX_FAX:
|
||||
case W3cHints.PREFIX_HOME:
|
||||
case W3cHints.PREFIX_PAGER:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool isW3cTypeHint(string hint)
|
||||
{
|
||||
switch (hint.ToLower())
|
||||
{
|
||||
case W3cHints.TEL:
|
||||
case W3cHints.TEL_COUNTRY_CODE:
|
||||
case W3cHints.TEL_NATIONAL:
|
||||
case W3cHints.TEL_AREA_CODE:
|
||||
case W3cHints.TEL_LOCAL:
|
||||
case W3cHints.TEL_LOCAL_PREFIX:
|
||||
case W3cHints.TEL_LOCAL_SUFFIX:
|
||||
case W3cHints.TEL_EXTENSION:
|
||||
case W3cHints.EMAIL:
|
||||
case W3cHints.IMPP:
|
||||
return true;
|
||||
}
|
||||
Log.Warn(CommonUtil.Tag, "Invalid W3C type hint: " + hint);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user