mirror of
https://github.com/bitwarden/mobile.git
synced 2024-11-23 11:45:38 +01:00
autofill custom fields for iOS extension
This commit is contained in:
parent
4598c3d852
commit
163ad248af
@ -20,6 +20,7 @@ using Bit.App.Resources;
|
||||
using Bit.iOS.Core.Controllers;
|
||||
using SimpleInjector;
|
||||
using XLabs.Ioc.SimpleInjectorContainer;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.iOS.Extension
|
||||
{
|
||||
@ -191,12 +192,13 @@ namespace Bit.iOS.Extension
|
||||
}
|
||||
}
|
||||
|
||||
public void CompleteUsernamePasswordRequest(string username, string password, string totp)
|
||||
public void CompleteUsernamePasswordRequest(string username, string password,
|
||||
List<Tuple<string, string>> fields, string totp)
|
||||
{
|
||||
NSDictionary itemData = null;
|
||||
if(_context.ProviderType == UTType.PropertyList)
|
||||
{
|
||||
var fillScript = new FillScript(_context.Details, username, password);
|
||||
var fillScript = new FillScript(_context.Details, username, password, fields);
|
||||
var scriptJson = JsonConvert.SerializeObject(fillScript, _jsonSettings);
|
||||
var scriptDict = new NSDictionary(Constants.AppExtensionWebViewPageFillScript, scriptJson);
|
||||
itemData = new NSDictionary(NSJavaScriptExtension.FinalizeArgumentKey, scriptDict);
|
||||
@ -210,7 +212,7 @@ namespace Bit.iOS.Extension
|
||||
else if(_context.ProviderType == Constants.UTTypeAppExtensionFillBrowserAction
|
||||
|| _context.ProviderType == Constants.UTTypeAppExtensionFillWebViewAction)
|
||||
{
|
||||
var fillScript = new FillScript(_context.Details, username, password);
|
||||
var fillScript = new FillScript(_context.Details, username, password, fields);
|
||||
var scriptJson = JsonConvert.SerializeObject(fillScript, _jsonSettings);
|
||||
itemData = new NSDictionary(Constants.AppExtensionWebViewPageFillScript, scriptJson);
|
||||
}
|
||||
|
@ -177,7 +177,7 @@ namespace Bit.iOS.Extension
|
||||
else if(LoadingController != null)
|
||||
{
|
||||
LoadingController.CompleteUsernamePasswordRequest(UsernameCell.TextField.Text, PasswordCell.TextField.Text,
|
||||
null);
|
||||
null, null);
|
||||
}
|
||||
}
|
||||
else if(saveTask.Result.Errors.Count() > 0)
|
||||
|
@ -207,7 +207,8 @@ namespace Bit.iOS.Extension
|
||||
totp = GetTotp(item);
|
||||
}
|
||||
|
||||
_controller.LoadingController.CompleteUsernamePasswordRequest(item.Username, item.Password, totp);
|
||||
_controller.LoadingController.CompleteUsernamePasswordRequest(item.Username, item.Password,
|
||||
item.Fields.Value, totp);
|
||||
}
|
||||
else if(!string.IsNullOrWhiteSpace(item.Username) || !string.IsNullOrWhiteSpace(item.Password) ||
|
||||
!string.IsNullOrWhiteSpace(item.Totp.Value))
|
||||
|
@ -2,12 +2,17 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Bit.iOS.Extension.Models
|
||||
{
|
||||
public class FillScript
|
||||
{
|
||||
public FillScript(PageDetails pageDetails, string fillUsername, string fillPassword)
|
||||
private static string[] _usernameFieldNames = new[]{ "username", "user name", "email",
|
||||
"email address", "e-mail", "e-mail address", "userid", "user id" };
|
||||
|
||||
public FillScript(PageDetails pageDetails, string fillUsername, string fillPassword,
|
||||
List<Tuple<string, string>> fillFields)
|
||||
{
|
||||
if(pageDetails == null)
|
||||
{
|
||||
@ -16,6 +21,38 @@ namespace Bit.iOS.Extension.Models
|
||||
|
||||
DocumentUUID = pageDetails.DocumentUUID;
|
||||
|
||||
var filledOpIds = new HashSet<string>();
|
||||
|
||||
if(fillFields?.Any() ?? false)
|
||||
{
|
||||
var fieldNames = fillFields.Select(f => f.Item1?.ToLower()).ToArray();
|
||||
foreach(var field in pageDetails.Fields.Where(f => f.Viewable))
|
||||
{
|
||||
if(filledOpIds.Contains(field.OpId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var matchingIndex = FindMatchingFieldIndex(field, fieldNames);
|
||||
if(matchingIndex > -1)
|
||||
{
|
||||
filledOpIds.Add(field.OpId);
|
||||
Script.Add(new List<string> { "click_on_opid", field.OpId });
|
||||
Script.Add(new List<string> { "fill_by_opid", field.OpId, fillFields[matchingIndex].Item2 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(fillPassword))
|
||||
{
|
||||
// No password for this login. Maybe they just wanted to auto-fill some custom fields?
|
||||
if(filledOpIds.Any())
|
||||
{
|
||||
Script.Add(new List<string> { "focus_by_opid", filledOpIds.Last() });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
List<PageDetails.Field> usernames = new List<PageDetails.Field>();
|
||||
List<PageDetails.Field> passwords = new List<PageDetails.Field>();
|
||||
|
||||
@ -76,38 +113,132 @@ namespace Bit.iOS.Extension.Models
|
||||
}
|
||||
}
|
||||
|
||||
foreach(var username in usernames)
|
||||
if(!passwordFields.Any())
|
||||
{
|
||||
// No password fields on this page. Let's try to just fuzzy fill the username.
|
||||
var usernameFieldNamesList = _usernameFieldNames.ToList();
|
||||
foreach(var f in pageDetails.Fields)
|
||||
{
|
||||
if((f.Type == "text" || f.Type == "email" || f.Type == "tel") &&
|
||||
FieldIsFuzzyMatch(f, usernameFieldNamesList))
|
||||
{
|
||||
usernames.Add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach(var username in usernames.Where(u => !filledOpIds.Contains(u.OpId)))
|
||||
{
|
||||
filledOpIds.Add(username.OpId);
|
||||
Script.Add(new List<string> { "click_on_opid", username.OpId });
|
||||
Script.Add(new List<string> { "fill_by_opid", username.OpId, fillUsername });
|
||||
}
|
||||
|
||||
foreach(var password in passwords)
|
||||
foreach(var password in passwords.Where(p => !filledOpIds.Contains(p.OpId)))
|
||||
{
|
||||
filledOpIds.Add(password.OpId);
|
||||
Script.Add(new List<string> { "click_on_opid", password.OpId });
|
||||
Script.Add(new List<string> { "fill_by_opid", password.OpId, fillPassword });
|
||||
}
|
||||
|
||||
if(passwords.Any())
|
||||
if(filledOpIds.Any())
|
||||
{
|
||||
AutoSubmit = new Submit { FocusOpId = passwords.First().OpId };
|
||||
Script.Add(new List<string> { "focus_by_opid", filledOpIds.Last() });
|
||||
}
|
||||
}
|
||||
|
||||
private PageDetails.Field FindUsernameField(PageDetails pageDetails, PageDetails.Field passwordField, bool canBeHidden,
|
||||
bool checkForm)
|
||||
{
|
||||
return pageDetails.Fields.LastOrDefault(f =>
|
||||
(!checkForm || f.Form == passwordField.Form)
|
||||
&& (canBeHidden || f.Viewable)
|
||||
&& f.ElementNumber < passwordField.ElementNumber
|
||||
&& (f.Type == "text" || f.Type == "email" || f.Type == "tel"));
|
||||
PageDetails.Field usernameField = null;
|
||||
|
||||
foreach(var f in pageDetails.Fields)
|
||||
{
|
||||
if(f.ElementNumber >= passwordField.ElementNumber)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if((!checkForm || f.Form == passwordField.Form)
|
||||
&& (canBeHidden || f.Viewable)
|
||||
&& f.ElementNumber < passwordField.ElementNumber
|
||||
&& (f.Type == "text" || f.Type == "email" || f.Type == "tel"))
|
||||
{
|
||||
usernameField = f;
|
||||
|
||||
if(FindMatchingFieldIndex(f, _usernameFieldNames) > -1)
|
||||
{
|
||||
// We found an exact match. No need to keep looking.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return usernameField;
|
||||
}
|
||||
|
||||
private int FindMatchingFieldIndex(PageDetails.Field field, string[] names)
|
||||
{
|
||||
var matchingIndex = -1;
|
||||
if(!string.IsNullOrWhiteSpace(field.HtmlId))
|
||||
{
|
||||
matchingIndex = Array.IndexOf(names, field.HtmlId.ToLower());
|
||||
}
|
||||
if(matchingIndex < 0 && !string.IsNullOrWhiteSpace(field.HtmlName))
|
||||
{
|
||||
matchingIndex = Array.IndexOf(names, field.HtmlName.ToLower());
|
||||
}
|
||||
if(matchingIndex < 0 && !string.IsNullOrWhiteSpace(field.LabelTag))
|
||||
{
|
||||
matchingIndex = Array.IndexOf(names, CleanLabel(field.LabelTag));
|
||||
}
|
||||
if(matchingIndex < 0 && !string.IsNullOrWhiteSpace(field.Placeholder))
|
||||
{
|
||||
matchingIndex = Array.IndexOf(names, field.Placeholder.ToLower());
|
||||
}
|
||||
|
||||
return matchingIndex;
|
||||
}
|
||||
|
||||
private bool FieldIsFuzzyMatch(PageDetails.Field field, List<string> names)
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(field.HtmlId) && FuzzyMatch(names, field.HtmlId.ToLower()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if(!string.IsNullOrWhiteSpace(field.HtmlName) && FuzzyMatch(names, field.HtmlName.ToLower()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if(!string.IsNullOrWhiteSpace(field.LabelTag) && FuzzyMatch(names, CleanLabel(field.LabelTag)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if(!string.IsNullOrWhiteSpace(field.Placeholder) && FuzzyMatch(names, field.Placeholder.ToLower()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool FuzzyMatch(List<string> options, string value)
|
||||
{
|
||||
if((!options?.Any() ?? true) || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return options.Any(o => value.Contains(o));
|
||||
}
|
||||
|
||||
private string CleanLabel(string label)
|
||||
{
|
||||
return Regex.Replace(label, @"(?:\r\n|\r|\n)", string.Empty).Trim().ToLower();
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "script")]
|
||||
public List<List<string>> Script { get; set; } = new List<List<string>>();
|
||||
[JsonProperty(PropertyName = "autosubmit")]
|
||||
public Submit AutoSubmit { get; set; }
|
||||
[JsonProperty(PropertyName = "documentUUID")]
|
||||
public object DocumentUUID { get; set; }
|
||||
[JsonProperty(PropertyName = "properties")]
|
||||
@ -116,12 +247,5 @@ namespace Bit.iOS.Extension.Models
|
||||
public object Options { get; set; } = new { animate = false };
|
||||
[JsonProperty(PropertyName = "metadata")]
|
||||
public object MetaData { get; set; } = new object();
|
||||
|
||||
public class Submit
|
||||
{
|
||||
[JsonProperty(PropertyName = "focusOpid")]
|
||||
public string FocusOpId { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
using Bit.App.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.iOS.Extension.Models
|
||||
{
|
||||
@ -13,6 +15,22 @@ namespace Bit.iOS.Extension.Models
|
||||
Password = login.Password?.Decrypt(login.OrganizationId);
|
||||
Uri = login.Uri?.Decrypt(login.OrganizationId);
|
||||
Totp = new Lazy<string>(() => login.Totp?.Decrypt(login.OrganizationId));
|
||||
Fields = new Lazy<List<Tuple<string, string>>>(() =>
|
||||
{
|
||||
if(login.Fields?.Any() ?? true)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var fields = new List<Tuple<string, string>>();
|
||||
foreach(var field in login.Fields)
|
||||
{
|
||||
fields.Add(new Tuple<string, string>(
|
||||
field.Name?.Decrypt(login.OrganizationId),
|
||||
field.Value?.Decrypt(login.OrganizationId)));
|
||||
}
|
||||
return fields;
|
||||
});
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
@ -21,5 +39,6 @@ namespace Bit.iOS.Extension.Models
|
||||
public string Password { get; set; }
|
||||
public string Uri { get; set; }
|
||||
public Lazy<string> Totp { get; set; }
|
||||
public Lazy<List<Tuple<string, string>>> Fields { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@ -36,6 +37,9 @@ namespace Bit.iOS.Extension.Models
|
||||
public string HtmlClass { get; set; }
|
||||
public string LabelRight { get; set; }
|
||||
public string LabelLeft { get; set; }
|
||||
[JsonProperty("label-tag")]
|
||||
public string LabelTag { get; set; }
|
||||
public string Placeholder { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Value { get; set; }
|
||||
public bool Disabled { get; set; }
|
||||
|
Loading…
Reference in New Issue
Block a user