Merge pull request #2254 from PhilippC/PhilippC-autofill-testing-and-improvements
Autofill testing and improvements
This commit is contained in:
		
							
								
								
									
										165
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										165
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,109 +3,111 @@ name: Build keepass2android app | ||||
| on: [push, pull_request] | ||||
|  | ||||
| jobs: | ||||
|   macos: | ||||
|   # macos: | ||||
|   # Disabled. Does not work, maybe due to nuget version, see https://github.com/PhilippC/keepass2android/actions/runs/4297640426/jobs/7490853348 | ||||
|   # should work again when the Project solution is converted to sdk style .csproj files. | ||||
|  | ||||
|     runs-on: macos-12 | ||||
|   #   runs-on: macos-12 | ||||
|  | ||||
|     steps: | ||||
|     - uses: actions/checkout@v3 | ||||
|   #   steps: | ||||
|   #   - uses: actions/checkout@v3 | ||||
|  | ||||
|     - name: Fetch submodules | ||||
|       run: git submodule init && git submodule update | ||||
|   #   - name: Fetch submodules | ||||
|   #     run: git submodule init && git submodule update | ||||
|  | ||||
|     - name: Setup Gradle | ||||
|       uses: gradle/gradle-build-action@v2 | ||||
|   #   - name: Setup Gradle | ||||
|   #     uses: gradle/gradle-build-action@v2 | ||||
|  | ||||
|     - name: Cache NuGet packages | ||||
|       uses: actions/cache@v3 | ||||
|       with: | ||||
|         path: ~/.nuget/packages | ||||
|         key: ${{ runner.os }}-nuget-${{ hashFiles('src/**/*.csproj', 'src/**/packages.config') }} | ||||
|         restore-keys: | | ||||
|           ${{ runner.os }}-nuget- | ||||
|   #   - name: Cache NuGet packages | ||||
|   #     uses: actions/cache@v3 | ||||
|   #     with: | ||||
|   #       path: ~/.nuget/packages | ||||
|   #       key: ${{ runner.os }}-nuget-${{ hashFiles('src/**/*.csproj', 'src/**/packages.config') }} | ||||
|   #       restore-keys: | | ||||
|   #         ${{ runner.os }}-nuget- | ||||
|  | ||||
|     # As per https://github.com/actions/runner-images/blob/main/images/macos/macos-12-Readme.md#visual-studio-for-mac | ||||
|     - name: Switch to Visual Studio 2019 | ||||
|       if: ${{ false }} # Not needed. We stay with the default 'Visual Studio 2022' of macos-12 runner. | ||||
|       run: | | ||||
|         mv "/Applications/Visual Studio.app" "/Applications/Visual Studio 2022.app" | ||||
|         mv "/Applications/Visual Studio 2019.app" "/Applications/Visual Studio.app" | ||||
|   #   # As per https://github.com/actions/runner-images/blob/main/images/macos/macos-12-Readme.md#visual-studio-for-mac | ||||
|   #   - name: Switch to Visual Studio 2019 | ||||
|   #     if: ${{ false }} # Not needed. We stay with the default 'Visual Studio 2022' of macos-12 runner. | ||||
|   #     run: | | ||||
|   #       mv "/Applications/Visual Studio.app" "/Applications/Visual Studio 2022.app" | ||||
|   #       mv "/Applications/Visual Studio 2019.app" "/Applications/Visual Studio.app" | ||||
|  | ||||
|     # As of 2022-12-02, keepass2android doesn't build with Xamarin >= 12.1 because there is some issue with SamsungPass. Removing SamsungPass would make the build succeed. | ||||
|     - name: Set default Xamarin SDK versions | ||||
|       run: | | ||||
|         # If using the github runner 'macos-12' | ||||
|         #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.3 | ||||
|         #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.0 | ||||
|         #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.1 # Build fails in this case, as of 2022-12-02 : Xamarin/Android/Xamarin.Android.D8.targets(79,5): error : java.lang.ArrayIndexOutOfBoundsException :  Index 4 out of bounds for length 4 | ||||
|         #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.2 # Build fails in this case, as of 2022-12-02 : Xamarin/Android/Xamarin.Android.D8.targets(79,5): error : java.lang.ArrayIndexOutOfBoundsException :  Index 4 out of bounds for length 4 | ||||
|         #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.3 # Build fails in this case, as of 2022-12-02 | ||||
|         $VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=13.1 | ||||
|   #   # As of 2022-12-02, keepass2android doesn't build with Xamarin >= 12.1 because there is some issue with SamsungPass. Removing SamsungPass would make the build succeed. | ||||
|   #   - name: Set default Xamarin SDK versions | ||||
|   #     run: | | ||||
|   #       # If using the github runner 'macos-12' | ||||
|   #       #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.3 | ||||
|   #       #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.0 | ||||
|   #       #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.1 # Build fails in this case, as of 2022-12-02 : Xamarin/Android/Xamarin.Android.D8.targets(79,5): error : java.lang.ArrayIndexOutOfBoundsException :  Index 4 out of bounds for length 4 | ||||
|   #       #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.2 # Build fails in this case, as of 2022-12-02 : Xamarin/Android/Xamarin.Android.D8.targets(79,5): error : java.lang.ArrayIndexOutOfBoundsException :  Index 4 out of bounds for length 4 | ||||
|   #       #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.3 # Build fails in this case, as of 2022-12-02 | ||||
|   #       $VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=13.1 | ||||
|  | ||||
|         # If using the github runner 'macos-11' | ||||
|         #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.0 | ||||
|         #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.0 | ||||
|   #       # If using the github runner 'macos-11' | ||||
|   #       #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.0 | ||||
|   #       #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.0 | ||||
|  | ||||
|         # If using the github runner 'macos-10.15' | ||||
|         # $VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.2 | ||||
|   #       # If using the github runner 'macos-10.15' | ||||
|   #       # $VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.2 | ||||
|  | ||||
|     - name: Switch to JDK-11 | ||||
|       uses: actions/setup-java@v3 | ||||
|       with: | ||||
|         java-version: '11' | ||||
|         distribution: 'temurin' | ||||
|   #   - name: Switch to JDK-11 | ||||
|   #     uses: actions/setup-java@v3 | ||||
|   #     with: | ||||
|   #       java-version: '11' | ||||
|   #       distribution: 'temurin' | ||||
|  | ||||
|     - name: Display java version | ||||
|       run: java -version | ||||
|   #   - name: Display java version | ||||
|   #     run: java -version | ||||
|  | ||||
|     # Some components of Keepass2Android currently target android API 26 which are not available on the runner | ||||
|     - name: Download android-26 API | ||||
|       run: $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --install "platforms;android-26" | ||||
|   #   # Some components of Keepass2Android currently target android API 26 which are not available on the runner | ||||
|   #   - name: Download android-26 API | ||||
|   #     run: $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --install "platforms;android-26" | ||||
|  | ||||
|     - name: Build native dependencies | ||||
|       run: make native | ||||
|   #   - name: Build native dependencies | ||||
|   #     run: make native | ||||
|  | ||||
|     - name: Build java dependencies | ||||
|       run: make java | ||||
|   #   - name: Build java dependencies | ||||
|   #     run: make java | ||||
|  | ||||
|     - name: Install NuGet dependencies (net) | ||||
|       run: make nuget Flavor=Net | ||||
|   #   - name: Install NuGet dependencies (net) | ||||
|   #     run: make nuget Flavor=Net | ||||
|  | ||||
|     - name: Build keepass2android (net) | ||||
|       run: | | ||||
|         make msbuild Flavor=Net | ||||
|   #   - name: Build keepass2android (net) | ||||
|   #     run: | | ||||
|   #       make msbuild Flavor=Net | ||||
|  | ||||
|     - name: Build APK (net) | ||||
|       run: | | ||||
|         make apk Flavor=Net | ||||
|   #   - name: Build APK (net) | ||||
|   #     run: | | ||||
|   #       make apk Flavor=Net | ||||
|  | ||||
|     - name: Archive production artifacts (net) | ||||
|       uses: actions/upload-artifact@v3 | ||||
|       with: | ||||
|         name: signed APK ('net' built on ${{ github.job }}) | ||||
|         path: | | ||||
|           src/keepass2android/bin/*/*-Signed.apk | ||||
|   #   - name: Archive production artifacts (net) | ||||
|   #     uses: actions/upload-artifact@v3 | ||||
|   #     with: | ||||
|   #       name: signed APK ('net' built on ${{ github.job }}) | ||||
|   #       path: | | ||||
|   #         src/keepass2android/bin/*/*-Signed.apk | ||||
|  | ||||
|     - name: Install NuGet dependencies (nonet) | ||||
|       run: make nuget Flavor=NoNet | ||||
|   #   - name: Install NuGet dependencies (nonet) | ||||
|   #     run: make nuget Flavor=NoNet | ||||
|  | ||||
|     - name: Build keepass2android (nonet) | ||||
|       run: | | ||||
|         make msbuild Flavor=NoNet | ||||
|   #   - name: Build keepass2android (nonet) | ||||
|   #     run: | | ||||
|   #       make msbuild Flavor=NoNet | ||||
|  | ||||
|     - name: Build APK (nonet) | ||||
|       run: | | ||||
|         make apk Flavor=NoNet | ||||
|   #   - name: Build APK (nonet) | ||||
|   #     run: | | ||||
|   #       make apk Flavor=NoNet | ||||
|  | ||||
|     - name: Archive production artifacts (nonet) | ||||
|       uses: actions/upload-artifact@v3 | ||||
|       with: | ||||
|         name: signed APK ('nonet' built on ${{ github.job }}) | ||||
|         path: | | ||||
|           src/keepass2android/bin/*/*-Signed.apk | ||||
|   #   - name: Archive production artifacts (nonet) | ||||
|   #     uses: actions/upload-artifact@v3 | ||||
|   #     with: | ||||
|   #       name: signed APK ('nonet' built on ${{ github.job }}) | ||||
|   #       path: | | ||||
|   #         src/keepass2android/bin/*/*-Signed.apk | ||||
|  | ||||
|     - name: Perform "make distclean" | ||||
|       run: make distclean | ||||
|   #   - name: Perform "make distclean" | ||||
|   #     run: make distclean | ||||
|  | ||||
|   # linux: | ||||
|   # disabled.  | ||||
| @@ -330,6 +332,9 @@ jobs: | ||||
|     - name: Build keepass2android (nonet) | ||||
|       run: | | ||||
|         make msbuild Flavor=NoNet | ||||
|     - name: Test Autofill | ||||
|       working-directory: ./src/Kp2aAutofillParserTest | ||||
|       run: dotnet test | ||||
|  | ||||
|     - name: Build APK (nonet) | ||||
|       run: | | ||||
|   | ||||
| @@ -25,6 +25,10 @@ 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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kp2aAutofillParser", "Kp2aAutofillParser\Kp2aAutofillParser.csproj", "{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}" | ||||
| EndProject | ||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kp2aAutofillParserTest", "Kp2aAutofillParserTest\Kp2aAutofillParserTest.csproj", "{3D1560FF-86BB-4CB4-8367-80BA13B81C38}" | ||||
| EndProject | ||||
| Global | ||||
| 	GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
| 		Debug|Any CPU = Debug|Any CPU | ||||
| @@ -283,6 +287,54 @@ 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 | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|Win32.ActiveCfg = Debug|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|Win32.Build.0 = Debug|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|x64.ActiveCfg = Debug|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|x64.Build.0 = Debug|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|Any CPU.Build.0 = Release|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|Mixed Platforms.Build.0 = Release|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|Win32.ActiveCfg = Release|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|Win32.Build.0 = Release|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|x64.ActiveCfg = Release|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|x64.Build.0 = Release|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|Any CPU.ActiveCfg = Release|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|Any CPU.Build.0 = Release|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|Mixed Platforms.ActiveCfg = Release|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|Mixed Platforms.Build.0 = Release|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|Win32.ActiveCfg = Release|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|Win32.Build.0 = Release|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|x64.ActiveCfg = Release|Any CPU | ||||
| 		{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|x64.Build.0 = Release|Any CPU | ||||
| 	EndGlobalSection | ||||
| 	GlobalSection(SolutionProperties) = preSolution | ||||
| 		HideSolutionNode = FALSE | ||||
|   | ||||
							
								
								
									
										963
									
								
								src/Kp2aAutofillParser/AutofillParser.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										963
									
								
								src/Kp2aAutofillParser/AutofillParser.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,963 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Diagnostics; | ||||
| using System.Linq; | ||||
| using Newtonsoft.Json; | ||||
| using Formatting = System.Xml.Formatting; | ||||
|  | ||||
| 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; | ||||
|         } | ||||
|     } | ||||
|     public class AutofillHintsHelper | ||||
|     { | ||||
|         public const string AutofillHint2faAppOtp = "2faAppOTPCode"; | ||||
|         public const string AutofillHintBirthDateDay = "birthDateDay"; | ||||
|         public const string AutofillHintBirthDateFull = "birthDateFull"; | ||||
|         public const string AutofillHintBirthDateMonth = "birthDateMonth"; | ||||
|         public const string AutofillHintBirthDateYear = "birthDateYear"; | ||||
|         public const string AutofillHintCreditCardExpirationDate = "creditCardExpirationDate"; | ||||
|         public const string AutofillHintCreditCardExpirationDay = "creditCardExpirationDay"; | ||||
|         public const string AutofillHintCreditCardExpirationMonth = "creditCardExpirationMonth"; | ||||
|         public const string AutofillHintCreditCardExpirationYear = "creditCardExpirationYear"; | ||||
|         public const string AutofillHintCreditCardNumber = "creditCardNumber"; | ||||
|         public const string AutofillHintCreditCardSecurityCode = "creditCardSecurityCode"; | ||||
|         public const string AutofillHintEmailAddress = "emailAddress"; | ||||
|         public const string AutofillHintEmailOtp = "emailOTPCode"; | ||||
|         public const string AutofillHintGender = "gender"; | ||||
|         public const string AutofillHintName = "name"; | ||||
|         public const string AutofillHintNewPassword = "newPassword"; | ||||
|         public const string AutofillHintNewUsername = "newUsername"; | ||||
|         public const string AutofillHintNotApplicable = "notApplicable"; | ||||
|         public const string AutofillHintPassword = "password"; | ||||
|         public const string AutofillHintPersonName = "personName"; | ||||
|         public const string AutofillHintPersonNameFAMILY = "personFamilyName"; | ||||
|         public const string AutofillHintPersonNameGIVEN = "personGivenName"; | ||||
|         public const string AutofillHintPersonNameMIDDLE = "personMiddleName"; | ||||
|         public const string AutofillHintPersonNameMIDDLE_INITIAL = "personMiddleInitial"; | ||||
|         public const string AutofillHintPersonNamePREFIX = "personNamePrefix"; | ||||
|         public const string AutofillHintPersonNameSUFFIX = "personNameSuffix"; | ||||
|         public const string AutofillHintPhone = "phone"; | ||||
|         public const string AutofillHintPhoneContryCode = "phoneCountryCode"; | ||||
|         public const string AutofillHintPostalAddressAPT_NUMBER = "aptNumber"; | ||||
|         public const string AutofillHintPostalAddressCOUNTRY = "addressCountry"; | ||||
|         public const string AutofillHintPostalAddressDEPENDENT_LOCALITY = "dependentLocality"; | ||||
|         public const string AutofillHintPostalAddressEXTENDED_ADDRESS = "extendedAddress"; | ||||
|         public const string AutofillHintPostalAddressEXTENDED_POSTAL_CODE = "extendedPostalCode"; | ||||
|         public const string AutofillHintPostalAddressLOCALITY = "addressLocality"; | ||||
|         public const string AutofillHintPostalAddressREGION = "addressRegion"; | ||||
|         public const string AutofillHintPostalAddressSTREET_ADDRESS = "streetAddress"; | ||||
|         public const string AutofillHintPostalCode = "postalCode"; | ||||
|         public const string AutofillHintPromoCode = "promoCode"; | ||||
|         public const string AutofillHintSMS_OTP = "smsOTPCode"; | ||||
|         public const string AutofillHintUPI_VPA = "upiVirtualPaymentAddress"; | ||||
|         public const string AutofillHintUsername = "username"; | ||||
|         public const string AutofillHintWifiPassword = "wifiPassword"; | ||||
|         public const string AutofillHintPhoneNational = "phoneNational"; | ||||
|         public const string AutofillHintPhoneNumber = "phoneNumber"; | ||||
|         public const string AutofillHintPhoneNumberDevice = "phoneNumberDevice"; | ||||
|         public 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); | ||||
|         bool IsEnabled(); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     class TimeUtil | ||||
|     { | ||||
|         private static DateTime? m_dtUnixRoot = null; | ||||
|         public static DateTime ConvertUnixTime(double dtUnix) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 if (!m_dtUnixRoot.HasValue) | ||||
|                     m_dtUnixRoot = (new DateTime(1970, 1, 1, 0, 0, 0, 0, | ||||
|                         DateTimeKind.Utc)).ToLocalTime(); | ||||
|  | ||||
|                 return m_dtUnixRoot.Value.AddSeconds(dtUnix); | ||||
|             } | ||||
|             catch (Exception) { Debug.Assert(false); } | ||||
|  | ||||
|             return DateTime.UtcNow; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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 (or could be) an 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; } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Serializable structure defining the contents of the current view (from an autofill perspective) | ||||
|     /// </summary> | ||||
|     /// <typeparam name="TField"></typeparam> | ||||
|     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; | ||||
|  | ||||
|         private readonly List<string> _autofillHintsForLogin = new List<string> | ||||
|             { | ||||
|                 AutofillHintsHelper.AutofillHintPassword, | ||||
|                 AutofillHintsHelper.AutofillHintUsername, | ||||
|                 AutofillHintsHelper.AutofillHintEmailAddress | ||||
|             }; | ||||
|  | ||||
|         public string PackageId { get; set; } | ||||
|  | ||||
|         public Dictionary<FieldT, string[]> FieldsMappedToHints = new Dictionary<FieldT, string[]>(); | ||||
|  | ||||
|         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> | ||||
|         protected virtual AutofillTargetId Parse(bool forFill, bool isManualRequest, AutofillView<FieldT> autofillView) | ||||
|         { | ||||
|             AutofillTargetId result = new AutofillTargetId() | ||||
|             { | ||||
|                 PackageName = autofillView.PackageId, | ||||
|                 WebDomain = autofillView.WebDomain | ||||
|             }; | ||||
|              | ||||
|             _log.Log("parsing autofillStructure..."); | ||||
|  | ||||
|             if (LogAutofillView) | ||||
|             { | ||||
|                 string debugInfo = JsonConvert.SerializeObject(autofillView, Newtonsoft.Json.Formatting.Indented); | ||||
|                 _log.Log("This is the autofillStructure: \n\n " + debugInfo); | ||||
|             } | ||||
|  | ||||
|  | ||||
|             //go through each input field and determine username/password fields. | ||||
|             //Depending on the target this can require more or less heuristics. | ||||
|             // * if there is a valid & supported autofill hint, we assume that all fields which should be filled do have an appropriate Autofill hint | ||||
|             // * if there is no such autofill hint, we use IsPassword to  | ||||
|  | ||||
|             HashSet<string> autofillHintsOfAllFields = autofillView.InputFields.Where(f => f.AutofillHints != null) | ||||
|                 .SelectMany(f => f.AutofillHints).ToHashSet(); | ||||
|             bool hasLoginAutofillHints = autofillHintsOfAllFields.Intersect(_autofillHintsForLogin).Any(); | ||||
|  | ||||
|             if (hasLoginAutofillHints) | ||||
|             { | ||||
|                 foreach (var viewNode in autofillView.InputFields) | ||||
|                 { | ||||
|                     string[] viewHints = viewNode.AutofillHints; | ||||
|                     if (viewHints == null) | ||||
|                         continue; | ||||
|                     if (viewHints.Intersect(_autofillHintsForLogin).Any()) | ||||
|                     { | ||||
|                         FieldsMappedToHints.Add(viewNode, viewHints); | ||||
|                     } | ||||
|  | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 //determine password fields, first by type, then by hint: | ||||
|                 List<FieldT> passwordFields = autofillView.InputFields.Where(f => IsEditText(f) && IsPassword(f)).ToList(); | ||||
|                 if (!passwordFields.Any()) | ||||
|                 { | ||||
|                     passwordFields = autofillView.InputFields.Where(f => IsEditText(f) && HasPasswordHint(f)).ToList(); | ||||
|                 } | ||||
|  | ||||
|                 //determine username fields. Try by hint, if that fails use the one before the password | ||||
|                 List<FieldT> usernameFields = autofillView.InputFields.Where(f => IsEditText(f) && HasUsernameHint(f)).ToList(); | ||||
|                 if (!usernameFields.Any()) | ||||
|                 { | ||||
|                     foreach (var passwordField in passwordFields) | ||||
|                     { | ||||
|                         var lastInputBeforePassword = autofillView.InputFields | ||||
|                             .TakeWhile(f => IsEditText(f) && f != passwordField && !passwordFields.Contains(f)).LastOrDefault(); | ||||
|                         if (lastInputBeforePassword != null) | ||||
|                             usernameFields.Add(lastInputBeforePassword); | ||||
|                     } | ||||
|                      | ||||
|                 } | ||||
|  | ||||
|                 //for "heuristic determination" we demand that one of the filled fields is focused: | ||||
|                 if (passwordFields.Concat(usernameFields).Any(f => f.IsFocused)) | ||||
|                 { | ||||
|                     foreach (var uf in usernameFields) | ||||
|                         FieldsMappedToHints.Add(uf, new string[] { AutofillHintsHelper.AutofillHintUsername }); | ||||
|                     foreach (var pf in passwordFields) | ||||
|                         FieldsMappedToHints.Add(pf, new string[] { AutofillHintsHelper.AutofillHintPassword }); | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|  | ||||
|             if (!string.IsNullOrEmpty(autofillView.WebDomain) && _digitalAssetLinksDataSource.IsEnabled()) | ||||
|             { | ||||
|                 result.IncompatiblePackageAndDomain = !_digitalAssetLinksDataSource.IsTrustedLink(autofillView.WebDomain, result.PackageName); | ||||
|                 if (result.IncompatiblePackageAndDomain) | ||||
|                 { | ||||
|                     _log.Log($"DAL verification failed for {result.PackageName}/{result.WebDomain}"); | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 result.IncompatiblePackageAndDomain = false; | ||||
|             } | ||||
|             return result; | ||||
|         } | ||||
|  | ||||
|         public bool LogAutofillView { get; set; } | ||||
|  | ||||
|         private bool IsEditText(FieldT f) | ||||
|         { | ||||
|             return (f.ClassName == "android.widget.EditText" | ||||
|                     || f.ClassName == "android.widget.AutoCompleteTextView" | ||||
|                     || f.HtmlInfoTag == "input"); | ||||
|         } | ||||
|  | ||||
|         private static readonly HashSet<string> _passwordHints = new HashSet<string> { "password", "passwort" | ||||
|             /*, "passwordAuto", "pswd"*/ }; | ||||
|         private static bool HasPasswordHint(InputField f) | ||||
|         { | ||||
|             return IsAny(f.IdEntry, _passwordHints) || | ||||
|                    IsAny(f.Hint, _passwordHints); | ||||
|         } | ||||
|  | ||||
|         private static readonly HashSet<string> _usernameHints = new HashSet<string> { "email", "e-mail", "username" }; | ||||
|  | ||||
|         private static bool HasUsernameHint(InputField f) | ||||
|         { | ||||
|             return IsAny(f.IdEntry, _usernameHints) || | ||||
|                 IsAny(f.Hint, _usernameHints); | ||||
|         } | ||||
|  | ||||
|         private static bool IsAny(string value, IEnumerable<string> terms) | ||||
|         { | ||||
|             if (string.IsNullOrWhiteSpace(value)) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
|             var lowerValue = value.ToLowerInvariant(); | ||||
|             return terms.Any(t => lowerValue == 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.AutofillHints != null && f.AutofillHints.First() == "passwordAuto") | ||||
|                     || (f.HtmlInfoTypeAttribute == "password") | ||||
|                 ); | ||||
|         } | ||||
|  | ||||
|          | ||||
|          | ||||
|  | ||||
|  | ||||
|  | ||||
|     } | ||||
| } | ||||
							
								
								
									
										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> | ||||
							
								
								
									
										114
									
								
								src/Kp2aAutofillParserTest/AutofillTest.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/Kp2aAutofillParserTest/AutofillTest.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| using Kp2aAutofillParser; | ||||
| using Newtonsoft.Json; | ||||
| using System.IO; | ||||
| using System.Reflection; | ||||
| using Xunit.Abstractions; | ||||
|  | ||||
| namespace Kp2aAutofillParserTest | ||||
| { | ||||
|     public class AutofillTest | ||||
|     { | ||||
|         private readonly ITestOutputHelper _testOutputHelper; | ||||
|  | ||||
|         public AutofillTest(ITestOutputHelper testOutputHelper) | ||||
|         { | ||||
|             _testOutputHelper = testOutputHelper; | ||||
|         } | ||||
|  | ||||
|         class TestInputField: InputField | ||||
|         { | ||||
|             public string[] ExpectedAssignedHints { get; set; } | ||||
|         } | ||||
|  | ||||
|         [Fact] | ||||
|         public void TestNotFocusedPasswordAutoIsNotFilled() | ||||
|         { | ||||
|             var resourceName = "Kp2aAutofillParserTest.com-servicenet-mobile-no-focus.json"; | ||||
|             RunTestFromAutofillInput(resourceName, "com.servicenet.mobile"); | ||||
|         } | ||||
|  | ||||
|         [Fact] | ||||
|         public void TestFocusedPasswordAutoIsFilled() | ||||
|         { | ||||
|             var resourceName = "Kp2aAutofillParserTest.com-servicenet-mobile-focused.json"; | ||||
|             RunTestFromAutofillInput(resourceName, "com.servicenet.mobile" ); | ||||
|         } | ||||
|  | ||||
|         [Fact] | ||||
|         public void TestMulitpleUnfocusedLoginsIsFilled() | ||||
|         { | ||||
|             var resourceName = "Kp2aAutofillParserTest.firefox-amazon-it.json"; | ||||
|             RunTestFromAutofillInput(resourceName, "org.mozilla.firefox", "www.amazon.it"); | ||||
|         } | ||||
|  | ||||
|         private void RunTestFromAutofillInput(string resourceName, string expectedPackageName = null, string expectedWebDomain = null) | ||||
|         { | ||||
|             var assembly = Assembly.GetExecutingAssembly(); | ||||
|              | ||||
|  | ||||
|             string input; | ||||
|             using (Stream stream = assembly.GetManifestResourceStream(resourceName)) | ||||
|             using (StreamReader reader = new StreamReader(stream)) | ||||
|             { | ||||
|                 input = reader.ReadToEnd(); | ||||
|             } | ||||
|  | ||||
|             AutofillView<TestInputField>? autofillView = | ||||
|                 JsonConvert.DeserializeObject<AutofillView<TestInputField>>(input); | ||||
|  | ||||
|             StructureParserBase<TestInputField> parser = | ||||
|                 new StructureParserBase<TestInputField>(new TestLogger(), new TestDalSourceTrustAll()); | ||||
|  | ||||
|             var result = parser.ParseForFill(false, autofillView); | ||||
|             if (expectedPackageName != null) | ||||
|                 Assert.Equal(expectedPackageName, result.PackageName); | ||||
|             if (expectedWebDomain != null) | ||||
|                 Assert.Equal(expectedWebDomain, result.WebDomain); | ||||
|             foreach (var field in autofillView.InputFields) | ||||
|             { | ||||
|                 string[] expectedHints = field.ExpectedAssignedHints; | ||||
|                 if (expectedHints == null) | ||||
|                     expectedHints = new string[0]; | ||||
|                 string[] actualHints; | ||||
|                 parser.FieldsMappedToHints.TryGetValue(field, out actualHints); | ||||
|                 if (actualHints == null) | ||||
|                     actualHints = new string[0]; | ||||
|                 if (actualHints.Any() || expectedHints.Any()) | ||||
|                 { | ||||
|                     _testOutputHelper.WriteLine($"field = {field.IdEntry} {field.Hint} {string.Join(",", field.AutofillHints ?? new string[]{})}"); | ||||
|                     _testOutputHelper.WriteLine("actual Hints = " + string.Join(", ", actualHints)); | ||||
|                     _testOutputHelper.WriteLine("expected Hints = " + string.Join(", ", expectedHints)); | ||||
|                 } | ||||
|                  | ||||
|                 Assert.Equal(expectedHints.Length, actualHints.Length); | ||||
|                 Assert.Equal(expectedHints.OrderBy(x => x), actualHints.OrderBy(x => x)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public class TestDalSourceTrustAll : IKp2aDigitalAssetLinksDataSource | ||||
|     { | ||||
|         public bool IsTrustedApp(string packageName) | ||||
|         { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         public bool IsTrustedLink(string domain, string targetPackage) | ||||
|         { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         public bool IsEnabled() | ||||
|         { | ||||
|             return true; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public class TestLogger : ILogger | ||||
|     { | ||||
|         public void Log(string x) | ||||
|         { | ||||
|             Console.WriteLine(x); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										46
									
								
								src/Kp2aAutofillParserTest/Kp2aAutofillParserTest.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/Kp2aAutofillParserTest/Kp2aAutofillParserTest.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <TargetFramework>net6.0</TargetFramework> | ||||
|     <ImplicitUsings>enable</ImplicitUsings> | ||||
|     <Nullable>enable</Nullable> | ||||
|  | ||||
|     <IsPackable>false</IsPackable> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <None Remove="com-servicenet-mobile-focused.json" /> | ||||
|     <None Remove="com-servicenet-mobile-no-focus.json" /> | ||||
|     <None Remove="firefox-amazon-it.json" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" /> | ||||
|     <PackageReference Include="xunit" Version="2.4.2" /> | ||||
|     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> | ||||
|       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|       <PrivateAssets>all</PrivateAssets> | ||||
|     </PackageReference> | ||||
|     <PackageReference Include="coverlet.collector" Version="3.1.2"> | ||||
|       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|       <PrivateAssets>all</PrivateAssets> | ||||
|     </PackageReference> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\Kp2aAutofillParser\Kp2aAutofillParser.csproj" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <EmbeddedResource Include="firefox-amazon-it.json"> | ||||
|       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||||
|     </EmbeddedResource> | ||||
|     <EmbeddedResource Include="com-servicenet-mobile-focused.json"> | ||||
|       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||||
|     </EmbeddedResource> | ||||
|     <EmbeddedResource Include="com-servicenet-mobile-no-focus.json"> | ||||
|       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||||
|     </EmbeddedResource> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
							
								
								
									
										1
									
								
								src/Kp2aAutofillParserTest/Usings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/Kp2aAutofillParserTest/Usings.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| global using Xunit; | ||||
							
								
								
									
										121
									
								
								src/Kp2aAutofillParserTest/com-servicenet-mobile-focused.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								src/Kp2aAutofillParserTest/com-servicenet-mobile-focused.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| { | ||||
|   "InputFields": [ | ||||
|     { | ||||
|       "IdEntry": null, | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.FrameLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": true, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "action_bar_root", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.LinearLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "action_mode_bar_stub", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.View", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "content", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.ViewGroup", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "username_text_input_layout", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.LinearLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "username", | ||||
|       "Hint": "Username", | ||||
|       "ClassName": "android.widget.EditText", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": true, | ||||
|       "InputType": 97, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null, | ||||
|       "ExpectedAssignedHints": [ "username" ] | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "password_text_input_layout", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.LinearLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "password", | ||||
|       "Hint": "Password", | ||||
|       "ClassName": "android.widget.EditText", | ||||
|       "AutofillHints": [ | ||||
|         "passwordAuto" | ||||
|       ], | ||||
|       "IsFocused": false, | ||||
|       "InputType": 129, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null, | ||||
|  | ||||
|       "ExpectedAssignedHints": [ "password" ] | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "login_button", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.Button", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "progressBar", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.ProgressBar", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "forgot_password", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.TextView", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     } | ||||
|   ], | ||||
|   "PackageId": "com.servicenet.mobile", | ||||
|   "WebDomain": null | ||||
| } | ||||
							
								
								
									
										119
									
								
								src/Kp2aAutofillParserTest/com-servicenet-mobile-no-focus.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								src/Kp2aAutofillParserTest/com-servicenet-mobile-no-focus.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| { | ||||
|   "InputFields": [ | ||||
|     { | ||||
|       "IdEntry": null, | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.FrameLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": true, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "action_bar_root", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.LinearLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "action_mode_bar_stub", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.View", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "content", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.ViewGroup", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "username_text_input_layout", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.LinearLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "username", | ||||
|       "Hint": "Username", | ||||
|       "ClassName": "android.widget.EditText", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 97, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "password_text_input_layout", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.LinearLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "password", | ||||
|       "Hint": "Password", | ||||
|       "ClassName": "android.widget.EditText", | ||||
|       "AutofillHints": [ | ||||
|         "passwordAuto" | ||||
|       ], | ||||
|       "IsFocused": false, | ||||
|       "InputType": 129, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null, | ||||
|  | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "login_button", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.Button", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "progressBar", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.ProgressBar", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "forgot_password", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.TextView", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     } | ||||
|   ], | ||||
|   "PackageId": "com.servicenet.mobile", | ||||
|   "WebDomain": null | ||||
| } | ||||
							
								
								
									
										469
									
								
								src/Kp2aAutofillParserTest/firefox-amazon-it.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										469
									
								
								src/Kp2aAutofillParserTest/firefox-amazon-it.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,469 @@ | ||||
| { | ||||
|   "InputFields": [ | ||||
|     { | ||||
|       "IdEntry": null, | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.FrameLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "action_bar_root", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.LinearLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "action_mode_bar_stub", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.View", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "rootContainer", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.LinearLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "navigationToolbarStub", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.View", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "container", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.FrameLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "container", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.FrameLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "gestureLayout", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.FrameLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "browserWindow", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.ViewGroup", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "browserLayout", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.ViewGroup", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "swipeRefresh", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.ViewGroup", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "engineView", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.FrameLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": null, | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.ViewGroup", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": true, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": "", | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": null, | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.ViewGroup", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": "form", | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": null, | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.EditText", | ||||
|       "AutofillHints": [ | ||||
|         "password" | ||||
|       ], | ||||
|       "IsFocused": false, | ||||
|       "InputType": 225, | ||||
|       "HtmlInfoTag": "input", | ||||
|       "HtmlInfoTypeAttribute": "password", | ||||
|       "ExpectedAssignedHints": [ "password" ] | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": null, | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.EditText", | ||||
|       "AutofillHints": [ | ||||
|         "emailAddress" | ||||
|       ], | ||||
|       "IsFocused": false, | ||||
|       "InputType": 33, | ||||
|       "HtmlInfoTag": "input", | ||||
|       "HtmlInfoTypeAttribute": "email", | ||||
|       "ExpectedAssignedHints": [ "emailAddress" ] | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": null, | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.EditText", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": "input", | ||||
|       "HtmlInfoTypeAttribute": "checkbox" | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": null, | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.EditText", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": "input", | ||||
|       "HtmlInfoTypeAttribute": "submit" | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": null, | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.ViewGroup", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": "form", | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": null, | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.EditText", | ||||
|       "AutofillHints": [ | ||||
|         "password" | ||||
|       ], | ||||
|       "IsFocused": false, | ||||
|       "InputType": 225, | ||||
|       "HtmlInfoTag": "input", | ||||
|       "HtmlInfoTypeAttribute": "password", | ||||
|       "ExpectedAssignedHints": [ "password" ] | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": null, | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.EditText", | ||||
|       "AutofillHints": [ | ||||
|         "emailAddress" | ||||
|       ], | ||||
|       "IsFocused": false, | ||||
|       "InputType": 33, | ||||
|       "HtmlInfoTag": "input", | ||||
|       "HtmlInfoTypeAttribute": "email", | ||||
|  | ||||
|       "ExpectedAssignedHints": [ "emailAddress" ] | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": null, | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.EditText", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": "input", | ||||
|       "HtmlInfoTypeAttribute": "submit" | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "stubFindInPage", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.View", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "viewDynamicDownloadDialog", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.ViewGroup", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "crash_reporter_view", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.ViewGroup", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "toolbar", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.ViewGroup", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "mozac_browser_toolbar_navigation_actions", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.LinearLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "mozac_browser_toolbar_origin_view", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.LinearLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "mozac_browser_toolbar_title_view", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.TextView", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "mozac_browser_toolbar_url_view", | ||||
|       "Hint": "Suche oder Adresse", | ||||
|       "ClassName": "android.widget.TextView", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "mozac_browser_toolbar_page_actions", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.LinearLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "mozac_browser_toolbar_browser_actions", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.LinearLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "counter_root", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.FrameLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "counter_text", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.TextView", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "mozac_browser_toolbar_menu", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.FrameLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "mozac_browser_toolbar_progress", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.ProgressBar", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "mozac_browser_toolbar_container", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.ViewGroup", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "mozac_browser_toolbar_edit_actions_start", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.LinearLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "mozac_browser_toolbar_edit_url_view", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.EditText", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 17, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "mozac_browser_toolbar_edit_actions_end", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.LinearLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "readerViewControlsBar", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.ViewGroup", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "addressSelectBar", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.ViewGroup", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "creditCardSelectBar", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.ViewGroup", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "loginSelectBar", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.view.ViewGroup", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     }, | ||||
|     { | ||||
|       "IdEntry": "tabPreview", | ||||
|       "Hint": null, | ||||
|       "ClassName": "android.widget.FrameLayout", | ||||
|       "AutofillHints": null, | ||||
|       "IsFocused": false, | ||||
|       "InputType": 0, | ||||
|       "HtmlInfoTag": null, | ||||
|       "HtmlInfoTypeAttribute": null | ||||
|     } | ||||
|   ], | ||||
|   "PackageId": "org.mozilla.firefox", | ||||
|   "WebDomain": "www.amazon.it" | ||||
| } | ||||
| @@ -1,7 +1,7 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"  | ||||
| 			android:versionCode="189"  | ||||
| 			android:versionName="1.09e-r1"  | ||||
| 			android:versionCode="190"  | ||||
| 			android:versionName="1.09e-r2"  | ||||
| 			package="keepass2android.keepass2android"  | ||||
| 			xmlns:tools="http://schemas.android.com/tools" | ||||
| 			android:installLocation="auto"> | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"  | ||||
| 			android:versionCode="189"  | ||||
| 			android:versionName="1.09e-r1"  | ||||
| 			android:versionCode="190"  | ||||
| 			android:versionName="1.09e-r2"  | ||||
| 			package="keepass2android.keepass2android_nonet" | ||||
| 			xmlns:tools="http://schemas.android.com/tools" | ||||
| 			android:installLocation="auto"> | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -5,6 +5,7 @@ using Android.App.Assist; | ||||
| using Android.Service.Autofill; | ||||
| using Android.Views; | ||||
| using Android.Views.Autofill; | ||||
| using Kp2aAutofillParser; | ||||
|  | ||||
| namespace keepass2android.services.AutofillBase | ||||
| { | ||||
| @@ -40,7 +41,6 @@ namespace keepass2android.services.AutofillBase | ||||
| 		    var supportedHints = AutofillHintsHelper.FilterForSupportedHints(autofillHints); | ||||
| 		    var canonicalHints = AutofillHintsHelper.ConvertToCanonicalHints(supportedHints); | ||||
|             SetHints(canonicalHints.ToArray()); | ||||
|  | ||||
|         } | ||||
|  | ||||
| 		void SetHints(string[] value) | ||||
|   | ||||
| @@ -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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -20,6 +20,7 @@ using AndroidX.AutoFill.Inline; | ||||
| using AndroidX.AutoFill.Inline.V1; | ||||
| using Java.Util.Concurrent.Atomic; | ||||
| using keepass2android.services.AutofillBase.model; | ||||
| using Kp2aAutofillParser; | ||||
|  | ||||
| namespace keepass2android.services.AutofillBase | ||||
| { | ||||
| @@ -137,7 +138,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 +150,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(); | ||||
| @@ -255,7 +256,7 @@ namespace keepass2android.services.AutofillBase | ||||
|                 if (warning == DisplayWarning.None) | ||||
|                 { | ||||
|            | ||||
|                     FilledAutofillFieldCollection partitionData = | ||||
|                     FilledAutofillFieldCollection<ViewNodeInputField> partitionData = | ||||
|                         AutofillHintsHelper.FilterForPartition(filledAutofillFieldCollection, parser.AutofillFields.FocusedAutofillCanonicalHints); | ||||
|  | ||||
|                     Kp2aLog.Log("AF: Add dataset"); | ||||
| @@ -299,7 +300,7 @@ namespace keepass2android.services.AutofillBase | ||||
|  | ||||
|         } | ||||
|  | ||||
|         protected abstract List<FilledAutofillFieldCollection> GetSuggestedEntries(string query); | ||||
|         protected abstract List<FilledAutofillFieldCollection<ViewNodeInputField>> GetSuggestedEntries(string query); | ||||
|  | ||||
|         public enum DisplayWarning | ||||
|         { | ||||
|   | ||||
| @@ -12,6 +12,7 @@ using Java.Util; | ||||
| using keepass2android.services.AutofillBase.model; | ||||
| using System.Linq; | ||||
| using Android.Content.PM; | ||||
| using Kp2aAutofillParser; | ||||
| #if !NoNet | ||||
| using Com.Dropbox.Core.V2.Teamlog; | ||||
| #endif | ||||
| @@ -173,7 +174,7 @@ namespace keepass2android.services.AutofillBase | ||||
|             ReplyIntent = null; | ||||
|         } | ||||
|  | ||||
|         protected void OnSuccess(FilledAutofillFieldCollection clientFormDataMap, bool isManual) | ||||
|         protected void OnSuccess(FilledAutofillFieldCollection<ViewNodeInputField> clientFormDataMap, bool isManual) | ||||
|         { | ||||
|             var intent = Intent; | ||||
|             AssistStructure structure = (AssistStructure)intent.GetParcelableExtra(AutofillManager.ExtraAssistStructure); | ||||
| @@ -229,7 +230,7 @@ namespace keepass2android.services.AutofillBase | ||||
|         /// <summary> | ||||
|         /// Creates the FilledAutofillFieldCollection from the intent returned from the query activity | ||||
|         /// </summary> | ||||
|         protected abstract FilledAutofillFieldCollection GetDataset(); | ||||
|         protected abstract FilledAutofillFieldCollection<ViewNodeInputField> GetDataset(); | ||||
|  | ||||
|         public abstract IAutofillIntentBuilder IntentBuilder { get; } | ||||
|  | ||||
|   | ||||
| @@ -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"; | ||||
| @@ -37,6 +39,11 @@ namespace keepass2android.services.AutofillBase | ||||
|             return trustedLinks.Contains(BuildLink(domain, targetPackage)); | ||||
|         } | ||||
|  | ||||
|         public bool IsEnabled() | ||||
|         { | ||||
|             return !PreferenceManager.GetDefaultSharedPreferences(_ctx).GetBoolean(_ctx.GetString(Resource.String.NoDalVerification_key), false); | ||||
|         } | ||||
|  | ||||
|         public void RememberAsTrustedApp(string packageName) | ||||
|         { | ||||
|             var prefs = PreferenceManager.GetDefaultSharedPreferences(_ctx); | ||||
|   | ||||
| @@ -1,334 +1,199 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
|  | ||||
| using Android.App.Assist; | ||||
| using Android.Content; | ||||
| using Android.Preferences; | ||||
| using Android.Text; | ||||
| using Android.Util; | ||||
| using Android.Views; | ||||
| using Android.Views.Autofill; | ||||
| using Android.Views.InputMethods; | ||||
| using DomainNameParser; | ||||
| using keepass2android.services.AutofillBase.model; | ||||
| using FilledAutofillFieldCollection = keepass2android.services.AutofillBase.model.FilledAutofillFieldCollection; | ||||
| using Kp2aAutofillParser; | ||||
| using Newtonsoft.Json; | ||||
|  | ||||
| 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 void FillFilledAutofillValue(FilledAutofillField<ViewNodeInputField> 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)); | ||||
|            | ||||
|             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 | ||||
| 	/// parses the hierarchy and collects autofill metadata from {@link ViewNode}s along the way. | ||||
| 	/// </summary> | ||||
| 	public sealed class StructureParser | ||||
| 	public sealed class StructureParser: StructureParserBase<ViewNodeInputField> | ||||
| 	{ | ||||
| 	    public Context mContext { get; } | ||||
|         private readonly AssistStructure _structure; | ||||
|         public Context _context { 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<ViewNodeInputField> ClientFormData { get; set; } | ||||
|  | ||||
|         public string PackageId { get; set; } | ||||
|  | ||||
| 		public StructureParser(Context context, AssistStructure structure) | ||||
|         : base(new Kp2aLogger(), new Kp2aDigitalAssetLinksDataSource(context)) | ||||
|         { | ||||
|             kp2aDigitalAssetLinksDataSource = new Kp2aDigitalAssetLinksDataSource(context); | ||||
| 		    mContext = context; | ||||
| 		    Structure = structure; | ||||
| 			AutofillFields = new AutofillFieldMetadataCollection(); | ||||
| 		    domainSuffixParserCache = new PublicSuffixRuleCache(context); | ||||
| 		} | ||||
| 		    _context = context; | ||||
|             _structure = structure; | ||||
|             AutofillFields = new AutofillFieldMetadataCollection(); | ||||
|              | ||||
|         public class AutofillTargetId | ||||
|         { | ||||
| 			public string PackageName { get; set; } | ||||
|  | ||||
|             public string PackageNameWithPseudoSchema | ||||
|             { | ||||
|                 get { return KeePass.AndroidAppScheme + PackageName; } | ||||
|             } | ||||
|  | ||||
|             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) | ||||
| 		{ | ||||
| 			return Parse(true, isManual); | ||||
| 		} | ||||
|  | ||||
| 		public AutofillTargetId ParseForSave() | ||||
| 		{ | ||||
| 			return Parse(false, true); | ||||
| 		} | ||||
|  | ||||
| 		/// <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) | ||||
|         protected override AutofillTargetId Parse(bool forFill, bool isManualRequest, AutofillView<ViewNodeInputField> autofillView) | ||||
|         { | ||||
|             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); | ||||
|  | ||||
| 				var view = node.RootViewNode; | ||||
| 				ParseLocked(forFill, isManualRequest, view, ref webDomain); | ||||
| 			} | ||||
|  | ||||
|  | ||||
|  | ||||
| 		    List<AssistStructure.ViewNode> passwordFields = new List<AssistStructure.ViewNode>(); | ||||
| 		    List<AssistStructure.ViewNode> usernameFields = new List<AssistStructure.ViewNode>(); | ||||
|             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.AutofillId != passwordField.AutofillId).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()); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|             var result = base.Parse(forFill, isManualRequest, autofillView); | ||||
|  | ||||
|             if (forFill) | ||||
|             { | ||||
|                 foreach (var p in FieldsMappedToHints) | ||||
|                     AutofillFields.Add(new AutofillFieldMetadata(p.Key.ViewNode, p.Value)); | ||||
|             } | ||||
| 		     | ||||
|             //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) | ||||
| 		        { | ||||
| 		            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, new[] { View.AutofillHintUsername })); | ||||
| 		        foreach (var pf in passwordFields) | ||||
| 		            AutofillFields.Add(new AutofillFieldMetadata(pf, new[] { View.AutofillHintPassword })); | ||||
|  | ||||
|             } | ||||
|             else | ||||
| 		    { | ||||
| 		        foreach (var uf in usernameFields) | ||||
| 		            ClientFormData.Add(new FilledAutofillField(uf, new[] { View.AutofillHintUsername })); | ||||
| 		        foreach (var pf in passwordFields) | ||||
| 		            ClientFormData.Add(new FilledAutofillField(pf, new[] { View.AutofillHintPassword })); | ||||
|             } | ||||
|  | ||||
|  | ||||
|             result.WebDomain = webDomain; | ||||
|             result.PackageName = Structure.ActivityComponent.PackageName; | ||||
|             if (!string.IsNullOrEmpty(webDomain) && !PreferenceManager.GetDefaultSharedPreferences(mContext).GetBoolean(mContext.GetString(Resource.String.NoDalVerification_key), false)) | ||||
| 		    { | ||||
|                 result.IncompatiblePackageAndDomain = !kp2aDigitalAssetLinksDataSource.IsTrustedLink(webDomain, result.PackageName); | ||||
| 		        if (result.IncompatiblePackageAndDomain) | ||||
| 		        {    | ||||
| 					CommonUtil.loge($"DAL verification failed for {result.PackageName}/{result.WebDomain}"); | ||||
|                 } | ||||
| 		    } | ||||
|             else | ||||
|             { | ||||
|                 result.IncompatiblePackageAndDomain = false; | ||||
|                 foreach (var p in FieldsMappedToHints) | ||||
|                     ClientFormData.Add(new FilledAutofillField<ViewNodeInputField>(p.Key, p.Value)); | ||||
|             } | ||||
|              | ||||
|  | ||||
|             return result; | ||||
| 		} | ||||
|         private static readonly HashSet<string> _passwordHints = new HashSet<string> { "password","passwort" }; | ||||
|         private static bool HasPasswordHint(AssistStructure.ViewNode 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 static bool HasUsernameHint(AssistStructure.ViewNode f) | ||||
|         public AutofillTargetId ParseForSave() | ||||
|         { | ||||
|             return ContainsAny(f.IdEntry, _usernameHints) || | ||||
|                 ContainsAny(f.Hint, _usernameHints); | ||||
|             var autofillView = new AutofillViewFromAssistStructureFinder(_context, _structure).GetAutofillView(true); | ||||
|             return Parse(false, true, autofillView); | ||||
|         } | ||||
|  | ||||
|         private static bool ContainsAny(string value, IEnumerable<string> terms) | ||||
|         public StructureParserBase<ViewNodeInputField>.AutofillTargetId ParseForFill(bool isManual) | ||||
|         { | ||||
|             if (string.IsNullOrWhiteSpace(value)) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
|             var lowerValue = value.ToLowerInvariant(); | ||||
|             return terms.Any(t => lowerValue.Contains(t)); | ||||
|             var autofillView = new AutofillViewFromAssistStructureFinder(_context, _structure).GetAutofillView(isManual); | ||||
|             return Parse(true, isManual, autofillView); | ||||
|         } | ||||
|          | ||||
|         private static bool IsInputTypeClass(InputTypes inputType, InputTypes inputTypeClass) | ||||
| 	    { | ||||
|             if (!InputTypes.MaskClass.HasFlag(inputTypeClass)) | ||||
|                 throw new Exception("invalid inputTypeClas"); | ||||
| 	        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"); | ||||
| 	        bool result = (((int)inputType) & (int)InputTypes.MaskVariation) == (int)(inputTypeVariation); | ||||
| 	        if (result) | ||||
| 	            Kp2aLog.Log("found " + ((int)inputTypeVariation).ToString("X") + " in " + ((int)inputType).ToString("X")); | ||||
|             return result; | ||||
|  | ||||
| 	    } | ||||
|     } | ||||
|  | ||||
|         private static bool IsPassword(AssistStructure.ViewNode 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.HtmlInfo?.Attributes.Any(p => p.First.ToString() == "type" && p.Second.ToString() == "password") ?? false) | ||||
| 	            ); | ||||
| 	    } | ||||
|  | ||||
| 	     | ||||
|  | ||||
|         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); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	} | ||||
|     public class Kp2aLogger : ILogger | ||||
|     { | ||||
|         public void Log(string x) | ||||
|         { | ||||
|             Kp2aLog.Log(x); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -241,6 +241,7 @@ namespace keepass2android | ||||
|                        .SetWhen(Java.Lang.JavaSystem.CurrentTimeMillis()) | ||||
|                        .SetTicker(entryName + ": " + desc) | ||||
|                        .SetVisibility((int)Android.App.NotificationVisibility.Secret) | ||||
|                        .SetAutoCancel(true) | ||||
|                        .SetContentIntent(pending); | ||||
|                 if (entryIcon != null) | ||||
|                     builder.SetLargeIcon(entryIcon); | ||||
| @@ -951,7 +952,9 @@ namespace keepass2android | ||||
|                 { | ||||
|                     CopyToClipboardService.CopyValueToClipboardWithTimeout(context, username, false); | ||||
|                 } | ||||
|                 context.SendBroadcast(new Intent(Intent.ActionCloseSystemDialogs)); //close notification drawer | ||||
|  | ||||
|                 CloseNotificationDrawer(context); | ||||
|                  | ||||
|             } | ||||
|             else if (action.Equals(Intents.CopyPassword)) | ||||
|             { | ||||
| @@ -960,7 +963,7 @@ namespace keepass2android | ||||
|                 { | ||||
|                     CopyToClipboardService.CopyValueToClipboardWithTimeout(context, password, true); | ||||
|                 } | ||||
|                 context.SendBroadcast(new Intent(Intent.ActionCloseSystemDialogs)); //close notification drawer | ||||
|                 CloseNotificationDrawer(context); | ||||
|             } | ||||
|             else if (action.Equals(Intents.CopyTotp)) | ||||
|             { | ||||
| @@ -969,7 +972,7 @@ namespace keepass2android | ||||
|                 { | ||||
|                     CopyToClipboardService.CopyValueToClipboardWithTimeout(context, totp, true); | ||||
|                 } | ||||
|                 context.SendBroadcast(new Intent(Intent.ActionCloseSystemDialogs)); //close notification drawer | ||||
|                 CloseNotificationDrawer(context); | ||||
|             } | ||||
|             else if (action.Equals(Intents.CheckKeyboard)) | ||||
|             { | ||||
| @@ -977,6 +980,11 @@ namespace keepass2android | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private static void CloseNotificationDrawer(Context context) | ||||
|         { | ||||
|             if ((int)Build.VERSION.SdkInt < 31) //sending this intent is no longer allowed since Android 31 | ||||
|                 context.SendBroadcast(new Intent(Intent.ActionCloseSystemDialogs)); //close notification drawer | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -15,6 +15,7 @@ using keepass2android.services.AutofillBase.model; | ||||
| using Keepass2android.Pluginsdk; | ||||
| using KeePassLib; | ||||
| using KeePassLib.Utility; | ||||
| using Kp2aAutofillParser; | ||||
|  | ||||
| namespace keepass2android.services.Kp2aAutofill | ||||
| { | ||||
| @@ -41,7 +42,7 @@ namespace keepass2android.services.Kp2aAutofill | ||||
|  | ||||
|         protected override Result ExpectedActivityResult => KeePass.ExitCloseAfterTaskComplete; | ||||
|  | ||||
|         protected override FilledAutofillFieldCollection GetDataset() | ||||
|         protected override FilledAutofillFieldCollection<ViewNodeInputField> GetDataset() | ||||
|         { | ||||
|             if (App.Kp2a.CurrentDb==null || (App.Kp2a.QuickLocked)) | ||||
|                 return null; | ||||
| @@ -50,18 +51,18 @@ namespace keepass2android.services.Kp2aAutofill | ||||
|             return GetFilledAutofillFieldCollectionFromEntry(entryOutput, this); | ||||
|         } | ||||
|  | ||||
|         public static FilledAutofillFieldCollection GetFilledAutofillFieldCollectionFromEntry(PwEntryOutput pwEntryOutput, Context context) | ||||
|         public static FilledAutofillFieldCollection<ViewNodeInputField> GetFilledAutofillFieldCollectionFromEntry(PwEntryOutput pwEntryOutput, Context context) | ||||
|         { | ||||
|             if (pwEntryOutput == null) | ||||
|                 return null; | ||||
|             FilledAutofillFieldCollection fieldCollection = new FilledAutofillFieldCollection(); | ||||
|             FilledAutofillFieldCollection<ViewNodeInputField> fieldCollection = new FilledAutofillFieldCollection<ViewNodeInputField>(); | ||||
|             var pwEntry = pwEntryOutput.Entry; | ||||
|  | ||||
|             foreach (string key in pwEntryOutput.OutputStrings.GetKeys()) | ||||
|             { | ||||
|                  | ||||
|                 FilledAutofillField field = | ||||
|                     new FilledAutofillField | ||||
|                 FilledAutofillField<ViewNodeInputField> field = | ||||
|                     new FilledAutofillField<ViewNodeInputField> | ||||
|                     { | ||||
|                         AutofillHints = GetCanonicalHintsFromKp2aField(key).ToArray(), | ||||
|                         TextValue = pwEntryOutput.OutputStrings.ReadSafe(key) | ||||
| @@ -72,8 +73,8 @@ namespace keepass2android.services.Kp2aAutofill | ||||
|             if (IsCreditCard(pwEntry, context) && pwEntry.Expires) | ||||
|             { | ||||
|                 DateTime expTime = pwEntry.ExpiryTime; | ||||
|                 FilledAutofillField field = | ||||
|                     new FilledAutofillField | ||||
|                 FilledAutofillField<ViewNodeInputField> field = | ||||
|                     new FilledAutofillField<ViewNodeInputField> | ||||
|                     { | ||||
|                         AutofillHints = new[] {View.AutofillHintCreditCardExpirationDate}, | ||||
|                         DateValue = (long) (1000 * TimeUtil.SerializeUnix(expTime)) | ||||
| @@ -81,7 +82,7 @@ namespace keepass2android.services.Kp2aAutofill | ||||
|                 fieldCollection.Add(field); | ||||
|  | ||||
|                 field = | ||||
|                     new FilledAutofillField | ||||
|                     new FilledAutofillField<ViewNodeInputField> | ||||
|                     { | ||||
|                         AutofillHints = new[] {View.AutofillHintCreditCardExpirationDay}, | ||||
|                         TextValue = expTime.Day.ToString() | ||||
| @@ -89,7 +90,7 @@ namespace keepass2android.services.Kp2aAutofill | ||||
|                 fieldCollection.Add(field); | ||||
|  | ||||
|                 field = | ||||
|                     new FilledAutofillField | ||||
|                     new FilledAutofillField<ViewNodeInputField> | ||||
|                     { | ||||
|                         AutofillHints = new[] {View.AutofillHintCreditCardExpirationMonth}, | ||||
|                         TextValue = expTime.Month.ToString() | ||||
| @@ -97,7 +98,7 @@ namespace keepass2android.services.Kp2aAutofill | ||||
|                 fieldCollection.Add(field); | ||||
|  | ||||
|                 field = | ||||
|                     new FilledAutofillField | ||||
|                     new FilledAutofillField<ViewNodeInputField> | ||||
|                     { | ||||
|                         AutofillHints = new[] {View.AutofillHintCreditCardExpirationYear}, | ||||
|                         TextValue = expTime.Year.ToString() | ||||
|   | ||||
| @@ -12,6 +12,7 @@ using Keepass2android.Pluginsdk; | ||||
| using KeePassLib; | ||||
| using KeePassLib.Collections; | ||||
| using KeePassLib.Utility; | ||||
| using Kp2aAutofillParser; | ||||
| using Org.Json; | ||||
| using AutofillServiceBase = keepass2android.services.AutofillBase.AutofillServiceBase; | ||||
|  | ||||
| @@ -33,10 +34,10 @@ namespace keepass2android.services | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         protected override List<FilledAutofillFieldCollection> GetSuggestedEntries(string query) | ||||
|         protected override List<FilledAutofillFieldCollection<ViewNodeInputField>> GetSuggestedEntries(string query) | ||||
|         { | ||||
|             if (!App.Kp2a.DatabaseIsUnlocked) | ||||
|                 return new List<FilledAutofillFieldCollection>(); | ||||
|                 return new List<FilledAutofillFieldCollection<ViewNodeInputField>>(); | ||||
|             var foundEntries = (ShareUrlResults.GetSearchResultsForUrl(query)?.Entries ?? new PwObjectList<PwEntry>()) | ||||
|                 .Select(e => new PwEntryOutput(e, App.Kp2a.FindDatabaseForElement(e))) | ||||
|                 .ToList(); | ||||
|   | ||||
| @@ -0,0 +1,956 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Diagnostics; | ||||
| using System.Linq; | ||||
| using Newtonsoft.Json; | ||||
| using Formatting = System.Xml.Formatting; | ||||
|  | ||||
| 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; | ||||
|         } | ||||
|     } | ||||
|     public class AutofillHintsHelper | ||||
|     { | ||||
|         public const string AutofillHint2faAppOtp = "2faAppOTPCode"; | ||||
|         public const string AutofillHintBirthDateDay = "birthDateDay"; | ||||
|         public const string AutofillHintBirthDateFull = "birthDateFull"; | ||||
|         public const string AutofillHintBirthDateMonth = "birthDateMonth"; | ||||
|         public const string AutofillHintBirthDateYear = "birthDateYear"; | ||||
|         public const string AutofillHintCreditCardExpirationDate = "creditCardExpirationDate"; | ||||
|         public const string AutofillHintCreditCardExpirationDay = "creditCardExpirationDay"; | ||||
|         public const string AutofillHintCreditCardExpirationMonth = "creditCardExpirationMonth"; | ||||
|         public const string AutofillHintCreditCardExpirationYear = "creditCardExpirationYear"; | ||||
|         public const string AutofillHintCreditCardNumber = "creditCardNumber"; | ||||
|         public const string AutofillHintCreditCardSecurityCode = "creditCardSecurityCode"; | ||||
|         public const string AutofillHintEmailAddress = "emailAddress"; | ||||
|         public const string AutofillHintEmailOtp = "emailOTPCode"; | ||||
|         public const string AutofillHintGender = "gender"; | ||||
|         public const string AutofillHintName = "name"; | ||||
|         public const string AutofillHintNewPassword = "newPassword"; | ||||
|         public const string AutofillHintNewUsername = "newUsername"; | ||||
|         public const string AutofillHintNotApplicable = "notApplicable"; | ||||
|         public const string AutofillHintPassword = "password"; | ||||
|         public const string AutofillHintPersonName = "personName"; | ||||
|         public const string AutofillHintPersonNameFAMILY = "personFamilyName"; | ||||
|         public const string AutofillHintPersonNameGIVEN = "personGivenName"; | ||||
|         public const string AutofillHintPersonNameMIDDLE = "personMiddleName"; | ||||
|         public const string AutofillHintPersonNameMIDDLE_INITIAL = "personMiddleInitial"; | ||||
|         public const string AutofillHintPersonNamePREFIX = "personNamePrefix"; | ||||
|         public const string AutofillHintPersonNameSUFFIX = "personNameSuffix"; | ||||
|         public const string AutofillHintPhone = "phone"; | ||||
|         public const string AutofillHintPhoneContryCode = "phoneCountryCode"; | ||||
|         public const string AutofillHintPostalAddressAPT_NUMBER = "aptNumber"; | ||||
|         public const string AutofillHintPostalAddressCOUNTRY = "addressCountry"; | ||||
|         public const string AutofillHintPostalAddressDEPENDENT_LOCALITY = "dependentLocality"; | ||||
|         public const string AutofillHintPostalAddressEXTENDED_ADDRESS = "extendedAddress"; | ||||
|         public const string AutofillHintPostalAddressEXTENDED_POSTAL_CODE = "extendedPostalCode"; | ||||
|         public const string AutofillHintPostalAddressLOCALITY = "addressLocality"; | ||||
|         public const string AutofillHintPostalAddressREGION = "addressRegion"; | ||||
|         public const string AutofillHintPostalAddressSTREET_ADDRESS = "streetAddress"; | ||||
|         public const string AutofillHintPostalCode = "postalCode"; | ||||
|         public const string AutofillHintPromoCode = "promoCode"; | ||||
|         public const string AutofillHintSMS_OTP = "smsOTPCode"; | ||||
|         public const string AutofillHintUPI_VPA = "upiVirtualPaymentAddress"; | ||||
|         public const string AutofillHintUsername = "username"; | ||||
|         public const string AutofillHintWifiPassword = "wifiPassword"; | ||||
|         public const string AutofillHintPhoneNational = "phoneNational"; | ||||
|         public const string AutofillHintPhoneNumber = "phoneNumber"; | ||||
|         public const string AutofillHintPhoneNumberDevice = "phoneNumberDevice"; | ||||
|         public 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); | ||||
|         bool IsEnabled(); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     class TimeUtil | ||||
|     { | ||||
|         private static DateTime? m_dtUnixRoot = null; | ||||
|         public static DateTime ConvertUnixTime(double dtUnix) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 if (!m_dtUnixRoot.HasValue) | ||||
|                     m_dtUnixRoot = (new DateTime(1970, 1, 1, 0, 0, 0, 0, | ||||
|                         DateTimeKind.Utc)).ToLocalTime(); | ||||
|  | ||||
|                 return m_dtUnixRoot.Value.AddSeconds(dtUnix); | ||||
|             } | ||||
|             catch (Exception) { Debug.Assert(false); } | ||||
|  | ||||
|             return DateTime.UtcNow; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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 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; | ||||
|  | ||||
|         private readonly List<string> _autofillHintsForLogin = new List<string> | ||||
|             { | ||||
|                 AutofillHintsHelper.AutofillHintPassword, | ||||
|                 AutofillHintsHelper.AutofillHintUsername, | ||||
|                 AutofillHintsHelper.AutofillHintEmailAddress | ||||
|             }; | ||||
|  | ||||
|         public string PackageId { get; set; } | ||||
|  | ||||
|         public Dictionary<FieldT, string[]> FieldsMappedToHints = new Dictionary<FieldT, string[]>(); | ||||
|  | ||||
|         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> | ||||
|         protected virtual AutofillTargetId Parse(bool forFill, bool isManualRequest, AutofillView<FieldT> autofillView) | ||||
|         { | ||||
|             AutofillTargetId result = new AutofillTargetId() | ||||
|             { | ||||
|                 PackageName = autofillView.PackageId, | ||||
|                 WebDomain = autofillView.WebDomain | ||||
|             }; | ||||
|              | ||||
|             _log.Log("parsing autofillStructure..."); | ||||
|              | ||||
|             //TODO remove from production | ||||
|             _log.Log("will log the autofillStructure..."); | ||||
|             string debugInfo = JsonConvert.SerializeObject(autofillView, Newtonsoft.Json.Formatting.Indented); | ||||
|             _log.Log("will log the autofillStructure... size is " + debugInfo.Length); | ||||
|             _log.Log("This is the autofillStructure: \n\n " + debugInfo); | ||||
|  | ||||
|             //go through each input field and determine username/password fields. | ||||
|             //Depending on the target this can require more or less heuristics. | ||||
|             // * if there is a valid & supported autofill hint, we assume that all fields which should be filled do have an appropriate Autofill hint | ||||
|             // * if there is no such autofill hint, we use IsPassword to  | ||||
|  | ||||
|             HashSet<string> autofillHintsOfAllFields = autofillView.InputFields.Where(f => f.AutofillHints != null) | ||||
|                 .SelectMany(f => f.AutofillHints).ToHashSet(); | ||||
|             bool hasLoginAutofillHints = autofillHintsOfAllFields.Intersect(_autofillHintsForLogin).Any(); | ||||
|  | ||||
|             if (hasLoginAutofillHints) | ||||
|             { | ||||
|                 foreach (var viewNode in autofillView.InputFields) | ||||
|                 { | ||||
|                     string[] viewHints = viewNode.AutofillHints; | ||||
|                     if (viewHints == null) | ||||
|                         continue; | ||||
|                     if (viewHints.Intersect(_autofillHintsForLogin).Any()) | ||||
|                     { | ||||
|                         FieldsMappedToHints.Add(viewNode, viewHints); | ||||
|                     } | ||||
|  | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 //determine password fields, first by type, then by hint: | ||||
|                 List<FieldT> passwordFields = autofillView.InputFields.Where(f => IsEditText(f) && IsPassword(f)).ToList(); | ||||
|                 if (!passwordFields.Any()) | ||||
|                 { | ||||
|                     passwordFields = autofillView.InputFields.Where(f => IsEditText(f) && HasPasswordHint(f)).ToList(); | ||||
|                 } | ||||
|  | ||||
|                 //determine username fields. Try by hint, if that fails use the one before the password | ||||
|                 List<FieldT> usernameFields = autofillView.InputFields.Where(f => IsEditText(f) && HasUsernameHint(f)).ToList(); | ||||
|                 if (!usernameFields.Any()) | ||||
|                 { | ||||
|                     foreach (var passwordField in passwordFields) | ||||
|                     { | ||||
|                         var lastInputBeforePassword = autofillView.InputFields | ||||
|                             .TakeWhile(f => IsEditText(f) && f != passwordField && !passwordFields.Contains(f)).LastOrDefault(); | ||||
|                         if (lastInputBeforePassword != null) | ||||
|                             usernameFields.Add(lastInputBeforePassword); | ||||
|                     } | ||||
|                      | ||||
|                 } | ||||
|  | ||||
|                 //for "heuristic determination" we demand that one of the filled fields is focused: | ||||
|                 if (passwordFields.Concat(usernameFields).Any(f => f.IsFocused)) | ||||
|                 { | ||||
|                     foreach (var uf in usernameFields) | ||||
|                         FieldsMappedToHints.Add(uf, new string[] { AutofillHintsHelper.AutofillHintUsername }); | ||||
|                     foreach (var pf in passwordFields) | ||||
|                         FieldsMappedToHints.Add(pf, new string[] { AutofillHintsHelper.AutofillHintPassword }); | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|  | ||||
|             if (!string.IsNullOrEmpty(autofillView.WebDomain) && _digitalAssetLinksDataSource.IsEnabled()) | ||||
|             { | ||||
|                 result.IncompatiblePackageAndDomain = !_digitalAssetLinksDataSource.IsTrustedLink(autofillView.WebDomain, result.PackageName); | ||||
|                 if (result.IncompatiblePackageAndDomain) | ||||
|                 { | ||||
|                     _log.Log($"DAL verification failed for {result.PackageName}/{result.WebDomain}"); | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 result.IncompatiblePackageAndDomain = false; | ||||
|             } | ||||
|             return result; | ||||
|         } | ||||
|  | ||||
|         private bool IsEditText(FieldT f) | ||||
|         { | ||||
|             return (f.ClassName == "android.widget.EditText" | ||||
|                     || f.ClassName == "android.widget.AutoCompleteTextView" | ||||
|                     || f.HtmlInfoTag == "input"); | ||||
|         } | ||||
|  | ||||
|         private static readonly HashSet<string> _passwordHints = new HashSet<string> { "password", "passwort" | ||||
|             /*, "passwordAuto", "pswd"*/ }; | ||||
|         private static bool HasPasswordHint(InputField f) | ||||
|         { | ||||
|             return IsAny(f.IdEntry, _passwordHints) || | ||||
|                    IsAny(f.Hint, _passwordHints); | ||||
|         } | ||||
|  | ||||
|         private static readonly HashSet<string> _usernameHints = new HashSet<string> { "email", "e-mail", "username" }; | ||||
|  | ||||
|         private static bool HasUsernameHint(InputField f) | ||||
|         { | ||||
|             return IsAny(f.IdEntry, _usernameHints) || | ||||
|                 IsAny(f.Hint, _usernameHints); | ||||
|         } | ||||
|  | ||||
|         private static bool IsAny(string value, IEnumerable<string> terms) | ||||
|         { | ||||
|             if (string.IsNullOrWhiteSpace(value)) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
|             var lowerValue = value.ToLowerInvariant(); | ||||
|             return terms.Any(t => lowerValue == 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.AutofillHints != null && f.AutofillHints.First() == "passwordAuto") | ||||
|                     || (f.HtmlInfoTypeAttribute == "password") | ||||
|                 ); | ||||
|         } | ||||
|  | ||||
|          | ||||
|          | ||||
|  | ||||
|  | ||||
|  | ||||
|     } | ||||
| } | ||||
| @@ -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> | ||||
		Reference in New Issue
	
	Block a user
	 PhilippC
					PhilippC