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 Bit.iOS.Core.Controllers;
|
||||||
using SimpleInjector;
|
using SimpleInjector;
|
||||||
using XLabs.Ioc.SimpleInjectorContainer;
|
using XLabs.Ioc.SimpleInjectorContainer;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Bit.iOS.Extension
|
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;
|
NSDictionary itemData = null;
|
||||||
if(_context.ProviderType == UTType.PropertyList)
|
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 scriptJson = JsonConvert.SerializeObject(fillScript, _jsonSettings);
|
||||||
var scriptDict = new NSDictionary(Constants.AppExtensionWebViewPageFillScript, scriptJson);
|
var scriptDict = new NSDictionary(Constants.AppExtensionWebViewPageFillScript, scriptJson);
|
||||||
itemData = new NSDictionary(NSJavaScriptExtension.FinalizeArgumentKey, scriptDict);
|
itemData = new NSDictionary(NSJavaScriptExtension.FinalizeArgumentKey, scriptDict);
|
||||||
|
@ -210,7 +212,7 @@ namespace Bit.iOS.Extension
|
||||||
else if(_context.ProviderType == Constants.UTTypeAppExtensionFillBrowserAction
|
else if(_context.ProviderType == Constants.UTTypeAppExtensionFillBrowserAction
|
||||||
|| _context.ProviderType == Constants.UTTypeAppExtensionFillWebViewAction)
|
|| _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);
|
var scriptJson = JsonConvert.SerializeObject(fillScript, _jsonSettings);
|
||||||
itemData = new NSDictionary(Constants.AppExtensionWebViewPageFillScript, scriptJson);
|
itemData = new NSDictionary(Constants.AppExtensionWebViewPageFillScript, scriptJson);
|
||||||
}
|
}
|
||||||
|
|
|
@ -177,7 +177,7 @@ namespace Bit.iOS.Extension
|
||||||
else if(LoadingController != null)
|
else if(LoadingController != null)
|
||||||
{
|
{
|
||||||
LoadingController.CompleteUsernamePasswordRequest(UsernameCell.TextField.Text, PasswordCell.TextField.Text,
|
LoadingController.CompleteUsernamePasswordRequest(UsernameCell.TextField.Text, PasswordCell.TextField.Text,
|
||||||
null);
|
null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(saveTask.Result.Errors.Count() > 0)
|
else if(saveTask.Result.Errors.Count() > 0)
|
||||||
|
|
|
@ -207,7 +207,8 @@ namespace Bit.iOS.Extension
|
||||||
totp = GetTotp(item);
|
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) ||
|
else if(!string.IsNullOrWhiteSpace(item.Username) || !string.IsNullOrWhiteSpace(item.Password) ||
|
||||||
!string.IsNullOrWhiteSpace(item.Totp.Value))
|
!string.IsNullOrWhiteSpace(item.Totp.Value))
|
||||||
|
|
|
@ -2,12 +2,17 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Bit.iOS.Extension.Models
|
namespace Bit.iOS.Extension.Models
|
||||||
{
|
{
|
||||||
public class FillScript
|
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)
|
if(pageDetails == null)
|
||||||
{
|
{
|
||||||
|
@ -16,6 +21,38 @@ namespace Bit.iOS.Extension.Models
|
||||||
|
|
||||||
DocumentUUID = pageDetails.DocumentUUID;
|
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> usernames = new List<PageDetails.Field>();
|
||||||
List<PageDetails.Field> passwords = 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> { "click_on_opid", username.OpId });
|
||||||
Script.Add(new List<string> { "fill_by_opid", username.OpId, fillUsername });
|
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> { "click_on_opid", password.OpId });
|
||||||
Script.Add(new List<string> { "fill_by_opid", password.OpId, fillPassword });
|
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,
|
private PageDetails.Field FindUsernameField(PageDetails pageDetails, PageDetails.Field passwordField, bool canBeHidden,
|
||||||
bool checkForm)
|
bool checkForm)
|
||||||
{
|
{
|
||||||
return pageDetails.Fields.LastOrDefault(f =>
|
PageDetails.Field usernameField = null;
|
||||||
(!checkForm || f.Form == passwordField.Form)
|
|
||||||
&& (canBeHidden || f.Viewable)
|
foreach(var f in pageDetails.Fields)
|
||||||
&& f.ElementNumber < passwordField.ElementNumber
|
{
|
||||||
&& (f.Type == "text" || f.Type == "email" || f.Type == "tel"));
|
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")]
|
[JsonProperty(PropertyName = "script")]
|
||||||
public List<List<string>> Script { get; set; } = new List<List<string>>();
|
public List<List<string>> Script { get; set; } = new List<List<string>>();
|
||||||
[JsonProperty(PropertyName = "autosubmit")]
|
|
||||||
public Submit AutoSubmit { get; set; }
|
|
||||||
[JsonProperty(PropertyName = "documentUUID")]
|
[JsonProperty(PropertyName = "documentUUID")]
|
||||||
public object DocumentUUID { get; set; }
|
public object DocumentUUID { get; set; }
|
||||||
[JsonProperty(PropertyName = "properties")]
|
[JsonProperty(PropertyName = "properties")]
|
||||||
|
@ -116,12 +247,5 @@ namespace Bit.iOS.Extension.Models
|
||||||
public object Options { get; set; } = new { animate = false };
|
public object Options { get; set; } = new { animate = false };
|
||||||
[JsonProperty(PropertyName = "metadata")]
|
[JsonProperty(PropertyName = "metadata")]
|
||||||
public object MetaData { get; set; } = new object();
|
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 Bit.App.Models;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Bit.iOS.Extension.Models
|
namespace Bit.iOS.Extension.Models
|
||||||
{
|
{
|
||||||
|
@ -13,6 +15,22 @@ namespace Bit.iOS.Extension.Models
|
||||||
Password = login.Password?.Decrypt(login.OrganizationId);
|
Password = login.Password?.Decrypt(login.OrganizationId);
|
||||||
Uri = login.Uri?.Decrypt(login.OrganizationId);
|
Uri = login.Uri?.Decrypt(login.OrganizationId);
|
||||||
Totp = new Lazy<string>(() => login.Totp?.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; }
|
public string Id { get; set; }
|
||||||
|
@ -21,5 +39,6 @@ namespace Bit.iOS.Extension.Models
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
public string Uri { get; set; }
|
public string Uri { get; set; }
|
||||||
public Lazy<string> Totp { 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.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
|
@ -36,6 +37,9 @@ namespace Bit.iOS.Extension.Models
|
||||||
public string HtmlClass { get; set; }
|
public string HtmlClass { get; set; }
|
||||||
public string LabelRight { get; set; }
|
public string LabelRight { get; set; }
|
||||||
public string LabelLeft { 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 Type { get; set; }
|
||||||
public string Value { get; set; }
|
public string Value { get; set; }
|
||||||
public bool Disabled { get; set; }
|
public bool Disabled { get; set; }
|
||||||
|
|
Loading…
Reference in New Issue