From b9e436d56deee6ffbafbee3d9bd350def8604bfa Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 2 Jan 2018 16:28:54 +0100 Subject: [PATCH] implemented saving of data from autofill service (#9) --- .../AutofillBase/AutofillFieldMetadata.cs | 71 ++++++++++--------- .../AutofillBase/AutofillServiceBase.cs | 25 ++++++- .../services/AutofillBase/StructureParser.cs | 46 ++++++++---- .../AutofillBase/model/FilledAutofillField.cs | 29 ++++++-- .../model/FilledAutofillFieldCollection.cs | 2 +- .../Kp2aAutofill/ChooseForAutofillActivity.cs | 16 +++-- .../Kp2aAutofill/Kp2aAutofillService.cs | 36 ++++++++++ 7 files changed, 165 insertions(+), 60 deletions(-) diff --git a/src/keepass2android/services/AutofillBase/AutofillFieldMetadata.cs b/src/keepass2android/services/AutofillBase/AutofillFieldMetadata.cs index 47c4f175..28c19adc 100644 --- a/src/keepass2android/services/AutofillBase/AutofillFieldMetadata.cs +++ b/src/keepass2android/services/AutofillBase/AutofillFieldMetadata.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using Android.App.Assist; using Android.Service.Autofill; using Android.Views; @@ -65,47 +67,48 @@ namespace keepass2android.services.AutofillBase return -1; } + static readonly HashSet _creditCardHints = new HashSet(StringComparer.OrdinalIgnoreCase) + { + View.AutofillHintCreditCardExpirationDate, + View.AutofillHintCreditCardExpirationDay, + View.AutofillHintCreditCardExpirationMonth, + View.AutofillHintCreditCardExpirationYear, + View.AutofillHintCreditCardNumber, + View.AutofillHintCreditCardSecurityCode + }; + + static readonly HashSet _addressHints = new HashSet(StringComparer.OrdinalIgnoreCase) + { + View.AutofillHintPostalAddress, + View.AutofillHintPostalCode + }; + void UpdateSaveTypeFromHints() { - //TODO future add savetypes for W3cHints SaveType = 0; if (AutofillCanonicalHints == null) { return; } - foreach (var hint in AutofillCanonicalHints) - { - switch (hint) - { - case View.AutofillHintCreditCardExpirationDate: - case View.AutofillHintCreditCardExpirationDay: - case View.AutofillHintCreditCardExpirationMonth: - case View.AutofillHintCreditCardExpirationYear: - case View.AutofillHintCreditCardNumber: - case View.AutofillHintCreditCardSecurityCode: - SaveType |= SaveDataType.CreditCard; - break; - case View.AutofillHintEmailAddress: - SaveType |= SaveDataType.EmailAddress; - break; - case View.AutofillHintPhone: - case View.AutofillHintName: - SaveType |= SaveDataType.Generic; - break; - case View.AutofillHintPassword: - SaveType |= SaveDataType.Password; - SaveType &= ~SaveDataType.EmailAddress; - SaveType &= ~SaveDataType.Username; - break; - case View.AutofillHintPostalAddress: - case View.AutofillHintPostalCode: - SaveType |= SaveDataType.Address; - break; - case View.AutofillHintUsername: - SaveType |= SaveDataType.Username; - break; - } - } + if (AutofillCanonicalHints.Any(h => _creditCardHints.Contains(h))) + { + SaveType |= SaveDataType.CreditCard; + } + if (AutofillCanonicalHints.Any(h => h.Equals(View.AutofillHintEmailAddress, StringComparison.OrdinalIgnoreCase))) + SaveType |= SaveDataType.EmailAddress; + if (AutofillCanonicalHints.Any(h => _addressHints.Contains(h))) + { + SaveType |= SaveDataType.Address; + } + if (AutofillCanonicalHints.Any(h => h.Equals(View.AutofillHintUsername, StringComparison.OrdinalIgnoreCase))) + SaveType |= SaveDataType.Username; + + if (AutofillCanonicalHints.Any(h => h.Equals(View.AutofillHintPassword, StringComparison.OrdinalIgnoreCase))) + { + SaveType |= SaveDataType.Password; + SaveType &= ~SaveDataType.EmailAddress; + SaveType &= ~SaveDataType.Username; + } } } } diff --git a/src/keepass2android/services/AutofillBase/AutofillServiceBase.cs b/src/keepass2android/services/AutofillBase/AutofillServiceBase.cs index 9b4f2749..352bc50e 100644 --- a/src/keepass2android/services/AutofillBase/AutofillServiceBase.cs +++ b/src/keepass2android/services/AutofillBase/AutofillServiceBase.cs @@ -76,6 +76,8 @@ namespace keepass2android.services.AutofillBase responseBuilder.AddDataset(entryDataset); AddQueryDataset(query, isManual, autofillIds, responseBuilder, !hasEntryDataset); + responseBuilder.SetSaveInfo(new SaveInfo.Builder(parser.AutofillFields.SaveType, + parser.AutofillFields.GetAutofillIds()).Build()); callback.OnSuccess(responseBuilder.Build()); } @@ -122,10 +124,29 @@ namespace keepass2android.services.AutofillBase public override void OnSaveRequest(SaveRequest request, SaveCallback callback) { - //TODO implement save - callback.OnFailure("Saving data is currently not implemented in Keepass2Android."); + + var structure = request.FillContexts?.LastOrDefault()?.Structure; + if (structure == null) + { + return; + } + + var parser = new StructureParser(this, structure); + string query = parser.ParseForSave(); + try + { + HandleSaveRequest(parser, query); + callback.OnSuccess(); + } + catch (Exception e) + { + callback.OnFailure(e.Message); + } + } + protected abstract void HandleSaveRequest(StructureParser parser, string query); + public override void OnConnected() { diff --git a/src/keepass2android/services/AutofillBase/StructureParser.cs b/src/keepass2android/services/AutofillBase/StructureParser.cs index a6d38bec..5e914054 100644 --- a/src/keepass2android/services/AutofillBase/StructureParser.cs +++ b/src/keepass2android/services/AutofillBase/StructureParser.cs @@ -6,6 +6,8 @@ using Android.Content; using Android.Text; using Android.Util; using Android.Views; +using Android.Views.Autofill; +using keepass2android.services.AutofillBase.model; using FilledAutofillFieldCollection = keepass2android.services.AutofillBase.model.FilledAutofillFieldCollection; namespace keepass2android.services.AutofillBase @@ -61,32 +63,33 @@ namespace keepass2android.services.AutofillBase ParseLocked(forFill, isManualRequest, view, ref webDomain); } - - if (AutofillFields.Empty) + + List passwordFields = new List(); + List usernameFields = new List(); + if (AutofillFields.Empty) { - var passwordFields = _editTextsWithoutHint - .Where(IsPassword).ToList(); + passwordFields = _editTextsWithoutHint.Where(IsPassword).ToList(); if (!passwordFields.Any()) { passwordFields = _editTextsWithoutHint.Where(HasPasswordHint).ToList(); } + foreach (var passwordField in passwordFields) { - AutofillFields.Add(new AutofillFieldMetadata(passwordField, new[] { View.AutofillHintPassword })); var usernameField = _editTextsWithoutHint.TakeWhile(f => f.AutofillId != passwordField.AutofillId).LastOrDefault(); if (usernameField != null) { - AutofillFields.Add(new AutofillFieldMetadata(usernameField, new[] {View.AutofillHintUsername})); + usernameFields.Add(usernameField); } } //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) { - AutofillFields.Add(new AutofillFieldMetadata(_editTextsWithoutHint.First(), new[] { View.AutofillHintUsername })); - + usernameFields.Add(_editTextsWithoutHint.First()); } + } @@ -97,12 +100,30 @@ namespace keepass2android.services.AutofillBase { if (editText.IsFocused) { - AutofillFields.Add(new AutofillFieldMetadata(editText, new[] { IsPassword(editText) || HasPasswordHint(editText) ? View.AutofillHintPassword : View.AutofillHintUsername })); + if (IsPassword(editText) || HasPasswordHint(editText)) + passwordFields.Add(editText); + else + usernameFields.Add(editText); break; } } } + + if (forFill) + { + foreach (var pf in passwordFields) + AutofillFields.Add(new AutofillFieldMetadata(pf, new[] { View.AutofillHintPassword })); + foreach (var uf in usernameFields) + AutofillFields.Add(new AutofillFieldMetadata(uf, new[] { View.AutofillHintUsername })); + } + else + { + foreach (var pf in passwordFields) + ClientFormData.Add(new FilledAutofillField(pf, new[] { View.AutofillHintPassword })); + foreach (var uf in usernameFields) + ClientFormData.Add(new FilledAutofillField(uf, new[] { View.AutofillHintUsername })); + } @@ -182,10 +203,9 @@ namespace keepass2android.services.AutofillBase } else { - //TODO implement save - throw new NotImplementedException("TODO: Port and use AutoFill hints"); - //ClientFormData.Add(new FilledAutofillField(viewNode)); - } + FilledAutofillField filledAutofillField = new FilledAutofillField(viewNode); + ClientFormData.Add(filledAutofillField); + } } else { diff --git a/src/keepass2android/services/AutofillBase/model/FilledAutofillField.cs b/src/keepass2android/services/AutofillBase/model/FilledAutofillField.cs index 478abd50..fd5dabd7 100644 --- a/src/keepass2android/services/AutofillBase/model/FilledAutofillField.cs +++ b/src/keepass2android/services/AutofillBase/model/FilledAutofillField.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Android.App.Assist; using Android.Views.Autofill; +using KeePassLib.Utility; namespace keepass2android.services.AutofillBase.model { @@ -11,6 +12,17 @@ namespace keepass2android.services.AutofillBase.model 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; + } + /// /// returns the autofill hints for the filled field. These are always lowercased for simpler string comparison. /// @@ -33,11 +45,17 @@ namespace keepass2android.services.AutofillBase.model public FilledAutofillField() {} - - public FilledAutofillField(AssistStructure.ViewNode viewNode) + + public FilledAutofillField(AssistStructure.ViewNode viewNode) + : this(viewNode, viewNode.GetAutofillHints()) + { + + } + + public FilledAutofillField(AssistStructure.ViewNode viewNode, string[] hints) { - string[] rawHints = AutofillHintsHelper.FilterForSupportedHints(viewNode.GetAutofillHints()); + string[] rawHints = AutofillHintsHelper.FilterForSupportedHints(hints); List hintList = new List(); string nextHint = null; @@ -81,9 +99,8 @@ namespace keepass2android.services.AutofillBase.model CommonUtil.loge($"Invalid hint: {rawHints[i]}"); } } - AutofillHints = hintList.ToArray(); - - //TODO port updated FilledAutofillField for saving + AutofillHints = AutofillHintsHelper.ConvertToCanonicalHints(hintList.ToArray()).ToArray(); + AutofillValue autofillValue = viewNode.AutofillValue; if (autofillValue != null) { diff --git a/src/keepass2android/services/AutofillBase/model/FilledAutofillFieldCollection.cs b/src/keepass2android/services/AutofillBase/model/FilledAutofillFieldCollection.cs index 9a2391f5..68d4d3c2 100644 --- a/src/keepass2android/services/AutofillBase/model/FilledAutofillFieldCollection.cs +++ b/src/keepass2android/services/AutofillBase/model/FilledAutofillFieldCollection.cs @@ -44,7 +44,7 @@ namespace keepass2android.services.AutofillBase.model { if (AutofillHintsHelper.IsSupportedHint(hint)) { - HintMap.Add(hint, filledAutofillField); + HintMap.TryAdd(hint, filledAutofillField); } else { diff --git a/src/keepass2android/services/Kp2aAutofill/ChooseForAutofillActivity.cs b/src/keepass2android/services/Kp2aAutofill/ChooseForAutofillActivity.cs index 0dcd33f3..1afe8b27 100644 --- a/src/keepass2android/services/Kp2aAutofill/ChooseForAutofillActivity.cs +++ b/src/keepass2android/services/Kp2aAutofill/ChooseForAutofillActivity.cs @@ -119,6 +119,14 @@ namespace keepass2android.services.Kp2aAutofill private static readonly Dictionary keyToHint = BuildKeyToHint(); + public static string GetKp2aKeyFromHint(string canonicalHint) + { + var key = keyToHint.FirstOrDefault(p => p.Value.Equals(canonicalHint, StringComparison.OrdinalIgnoreCase)).Key; + if (string.IsNullOrWhiteSpace(key)) + return canonicalHint; + return key; + } + private static Dictionary BuildKeyToHint() { var result = new Dictionary @@ -127,15 +135,15 @@ namespace keepass2android.services.Kp2aAutofill {PwDefs.PasswordField, View.AutofillHintPassword}, {PwDefs.UrlField, W3cHints.URL}, { - Android.App.Application.Context.GetString(Resource.String.TemplateField_CreditCard_CVV), + Application.Context.GetString(Resource.String.TemplateField_CreditCard_CVV), View.AutofillHintCreditCardSecurityCode }, { - Android.App.Application.Context.GetString(Resource.String.TemplateField_CreditCard_Owner), + Application.Context.GetString(Resource.String.TemplateField_CreditCard_Owner), W3cHints.CC_NAME }, - {Android.App.Application.Context.GetString(Resource.String.TemplateField_Number), View.AutofillHintCreditCardNumber}, - {Android.App.Application.Context.GetString(Resource.String.TemplateField_IdCard_Name), View.AutofillHintName}, + {Application.Context.GetString(Resource.String.TemplateField_Number), View.AutofillHintCreditCardNumber}, + {Application.Context.GetString(Resource.String.TemplateField_IdCard_Name), View.AutofillHintName}, }; return result; } diff --git a/src/keepass2android/services/Kp2aAutofill/Kp2aAutofillService.cs b/src/keepass2android/services/Kp2aAutofill/Kp2aAutofillService.cs index 7dc24713..88cd6131 100644 --- a/src/keepass2android/services/Kp2aAutofill/Kp2aAutofillService.cs +++ b/src/keepass2android/services/Kp2aAutofill/Kp2aAutofillService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Android; using Android.App; using Android.Content; @@ -6,6 +7,10 @@ using Android.Runtime; using keepass2android.services.AutofillBase; using keepass2android.services.AutofillBase.model; using keepass2android.services.Kp2aAutofill; +using Keepass2android.Pluginsdk; +using KeePassLib; +using KeePassLib.Utility; +using Org.Json; using AutofillServiceBase = keepass2android.services.AutofillBase.AutofillServiceBase; namespace keepass2android.services @@ -34,6 +39,37 @@ namespace keepass2android.services return null; } + protected override void HandleSaveRequest(StructureParser parser, string query) + { + + + var intent = new Intent(this, typeof(FileSelectActivity)); + + Dictionary outputFields = new Dictionary(); + foreach (var p in parser.ClientFormData.HintMap) + { + CommonUtil.logd(p.Key + " = " + p.Value.ValueToString()); + outputFields.TryAdd(ChooseForAutofillActivity.GetKp2aKeyFromHint(p.Key), p.Value.ValueToString()); + + } + if (query != null) + outputFields.TryAdd(PwDefs.UrlField, query); + + JSONObject jsonOutput = new JSONObject(outputFields); + var jsonOutputStr = jsonOutput.ToString(); + intent.PutExtra(Strings.ExtraEntryOutputData, jsonOutputStr); + + JSONArray jsonProtectedFields = new JSONArray( + (System.Collections.ICollection)new string[]{}); + intent.PutExtra(Strings.ExtraProtectedFieldsList, jsonProtectedFields.ToString()); + + intent.PutExtra(AppTask.AppTaskKey, "CreateEntryThenCloseTask"); + intent.PutExtra(CreateEntryThenCloseTask.ShowUserNotificationsKey, "false"); + + StartActivity(intent); + + } + public override IAutofillIntentBuilder IntentBuilder => new Kp2aAutofillIntentBuilder(); } } \ No newline at end of file