mirror of
https://github.com/bitwarden/mobile.git
synced 2024-11-26 12:16:07 +01:00
port over models
This commit is contained in:
parent
775bee3546
commit
645576c949
107
src/iOS.Autofill/Utilities/AutofillHelpers.cs
Normal file
107
src/iOS.Autofill/Utilities/AutofillHelpers.cs
Normal file
@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Bit.App.Resources;
|
||||
using Bit.iOS.Core.Utilities;
|
||||
using Bit.iOS.Core.Views;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
|
||||
namespace Bit.iOS.Autofill.Utilities
|
||||
{
|
||||
public static class AutofillHelpers
|
||||
{
|
||||
/*
|
||||
public static void TableRowSelected(UITableView tableView, NSIndexPath indexPath,
|
||||
ExtensionTableSource tableSource, CredentialProviderViewController cpViewController,
|
||||
UITableViewController controller, ISettings settings, string loginAddSegue)
|
||||
{
|
||||
tableView.DeselectRow(indexPath, true);
|
||||
tableView.EndEditing(true);
|
||||
|
||||
if(tableSource.Items == null || tableSource.Items.Count() == 0)
|
||||
{
|
||||
controller.PerformSegue(loginAddSegue, tableSource);
|
||||
return;
|
||||
}
|
||||
|
||||
var item = tableSource.Items.ElementAt(indexPath.Row);
|
||||
if(item == null)
|
||||
{
|
||||
cpViewController.CompleteRequest(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(item.Username) && !string.IsNullOrWhiteSpace(item.Password))
|
||||
{
|
||||
string totp = null;
|
||||
if(!settings.GetValueOrDefault(App.Constants.SettingDisableTotpCopy, false))
|
||||
{
|
||||
totp = tableSource.GetTotp(item);
|
||||
}
|
||||
|
||||
cpViewController.CompleteRequest(item.Username, item.Password, totp);
|
||||
}
|
||||
else if(!string.IsNullOrWhiteSpace(item.Username) || !string.IsNullOrWhiteSpace(item.Password) ||
|
||||
!string.IsNullOrWhiteSpace(item.Totp.Value))
|
||||
{
|
||||
var sheet = Dialogs.CreateActionSheet(item.Name, controller);
|
||||
if(!string.IsNullOrWhiteSpace(item.Username))
|
||||
{
|
||||
sheet.AddAction(UIAlertAction.Create(AppResources.CopyUsername, UIAlertActionStyle.Default, a =>
|
||||
{
|
||||
UIPasteboard clipboard = UIPasteboard.General;
|
||||
clipboard.String = item.Username;
|
||||
var alert = Dialogs.CreateMessageAlert(AppResources.CopyUsername);
|
||||
controller.PresentViewController(alert, true, () =>
|
||||
{
|
||||
controller.DismissViewController(true, null);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(item.Password))
|
||||
{
|
||||
sheet.AddAction(UIAlertAction.Create(AppResources.CopyPassword, UIAlertActionStyle.Default, a =>
|
||||
{
|
||||
UIPasteboard clipboard = UIPasteboard.General;
|
||||
clipboard.String = item.Password;
|
||||
var alert = Dialogs.CreateMessageAlert(AppResources.CopiedPassword);
|
||||
controller.PresentViewController(alert, true, () =>
|
||||
{
|
||||
controller.DismissViewController(true, null);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(item.Totp.Value))
|
||||
{
|
||||
sheet.AddAction(UIAlertAction.Create(AppResources.CopyTotp, UIAlertActionStyle.Default, a =>
|
||||
{
|
||||
var totp = tableSource.GetTotp(item);
|
||||
if(string.IsNullOrWhiteSpace(totp))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UIPasteboard clipboard = UIPasteboard.General;
|
||||
clipboard.String = totp;
|
||||
var alert = Dialogs.CreateMessageAlert(AppResources.CopiedTotp);
|
||||
controller.PresentViewController(alert, true, () =>
|
||||
{
|
||||
controller.DismissViewController(true, null);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
sheet.AddAction(UIAlertAction.Create(AppResources.Cancel, UIAlertActionStyle.Cancel, null));
|
||||
controller.PresentViewController(sheet, true, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
var alert = Dialogs.CreateAlert(null, AppResources.NoUsernamePasswordConfigured, AppResources.Ok);
|
||||
controller.PresentViewController(alert, true, null);
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
@ -66,6 +66,7 @@
|
||||
<ItemGroup>
|
||||
<Compile Include="Main.cs" />
|
||||
<Compile Include="AppDelegate.cs" />
|
||||
<Compile Include="Utilities\AutofillHelpers.cs" />
|
||||
<None Include="Info.plist" />
|
||||
<None Include="Entitlements.plist" />
|
||||
<Compile Include="Models\Context.cs" />
|
||||
@ -85,6 +86,10 @@
|
||||
<Reference Include="Xamarin.iOS" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\App\App.csproj">
|
||||
<Project>{ee44c6a1-2a85-45fe-8d9b-bf1d5f88809c}</Project>
|
||||
<Name>App</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\iOS.Core\iOS.Core.csproj">
|
||||
<Project>{e71f3053-056c-4381-9638-048ed73bdff6}</Project>
|
||||
<Name>iOS.Core</Name>
|
||||
|
17
src/iOS.Extension/Models/Context.cs
Normal file
17
src/iOS.Extension/Models/Context.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using Foundation;
|
||||
using Bit.iOS.Core.Models;
|
||||
|
||||
namespace Bit.iOS.Extension.Models
|
||||
{
|
||||
public class Context : AppExtensionContext
|
||||
{
|
||||
public NSExtensionContext ExtContext { get; set; }
|
||||
public string ProviderType { get; set; }
|
||||
public string LoginTitle { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string OldPassword { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public PageDetails Details { get; set; }
|
||||
}
|
||||
}
|
276
src/iOS.Extension/Models/FillScript.cs
Normal file
276
src/iOS.Extension/Models/FillScript.cs
Normal file
@ -0,0 +1,276 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Bit.iOS.Extension.Models
|
||||
{
|
||||
public class FillScript
|
||||
{
|
||||
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)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DocumentUUID = pageDetails.DocumentUUID;
|
||||
|
||||
var filledFields = new Dictionary<string, PageDetails.Field>();
|
||||
|
||||
if(fillFields?.Any() ?? false)
|
||||
{
|
||||
var fieldNames = fillFields.Select(f => f.Item1?.ToLower()).ToArray();
|
||||
foreach(var field in pageDetails.Fields.Where(f => f.Viewable))
|
||||
{
|
||||
if(filledFields.ContainsKey(field.OpId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var matchingIndex = FindMatchingFieldIndex(field, fieldNames);
|
||||
if(matchingIndex > -1)
|
||||
{
|
||||
filledFields.Add(field.OpId, field);
|
||||
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?
|
||||
SetFillScriptForFocus(filledFields);
|
||||
return;
|
||||
}
|
||||
|
||||
List<PageDetails.Field> usernames = new List<PageDetails.Field>();
|
||||
List<PageDetails.Field> passwords = new List<PageDetails.Field>();
|
||||
|
||||
var passwordFields = pageDetails.Fields.Where(f => f.Type == "password" && f.Viewable).ToArray();
|
||||
if(!passwordFields.Any())
|
||||
{
|
||||
// not able to find any viewable password fields. maybe there are some "hidden" ones?
|
||||
passwordFields = pageDetails.Fields.Where(f => f.Type == "password").ToArray();
|
||||
}
|
||||
|
||||
foreach(var form in pageDetails.Forms)
|
||||
{
|
||||
var passwordFieldsForForm = passwordFields.Where(f => f.Form == form.Key).ToArray();
|
||||
passwords.AddRange(passwordFieldsForForm);
|
||||
|
||||
if(string.IsNullOrWhiteSpace(fillUsername))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach(var pf in passwordFieldsForForm)
|
||||
{
|
||||
var username = FindUsernameField(pageDetails, pf, false, true);
|
||||
if(username == null)
|
||||
{
|
||||
// not able to find any viewable username fields. maybe there are some "hidden" ones?
|
||||
username = FindUsernameField(pageDetails, pf, true, true);
|
||||
}
|
||||
|
||||
if(username != null)
|
||||
{
|
||||
usernames.Add(username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(passwordFields.Any() && !passwords.Any())
|
||||
{
|
||||
// The page does not have any forms with password fields. Use the first password field on the page and the
|
||||
// input field just before it as the username.
|
||||
|
||||
var pf = passwordFields.First();
|
||||
passwords.Add(pf);
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(fillUsername) && pf.ElementNumber > 0)
|
||||
{
|
||||
var username = FindUsernameField(pageDetails, pf, false, false);
|
||||
if(username == null)
|
||||
{
|
||||
// not able to find any viewable username fields. maybe there are some "hidden" ones?
|
||||
username = FindUsernameField(pageDetails, pf, true, false);
|
||||
}
|
||||
|
||||
if(username != null)
|
||||
{
|
||||
usernames.Add(username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.Viewable && (f.Type == "text" || f.Type == "email" || f.Type == "tel") &&
|
||||
FieldIsFuzzyMatch(f, usernameFieldNamesList))
|
||||
{
|
||||
usernames.Add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach(var username in usernames.Where(u => !filledFields.ContainsKey(u.OpId)))
|
||||
{
|
||||
filledFields.Add(username.OpId, username);
|
||||
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.Where(p => !filledFields.ContainsKey(p.OpId)))
|
||||
{
|
||||
filledFields.Add(password.OpId, password);
|
||||
Script.Add(new List<string> { "click_on_opid", password.OpId });
|
||||
Script.Add(new List<string> { "fill_by_opid", password.OpId, fillPassword });
|
||||
}
|
||||
|
||||
SetFillScriptForFocus(filledFields);
|
||||
}
|
||||
|
||||
private PageDetails.Field FindUsernameField(PageDetails pageDetails, PageDetails.Field passwordField, bool canBeHidden,
|
||||
bool checkForm)
|
||||
{
|
||||
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 void SetFillScriptForFocus(IDictionary<string, PageDetails.Field> filledFields)
|
||||
{
|
||||
if(!filledFields.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PageDetails.Field lastField = null, lastPasswordField = null;
|
||||
foreach(var field in filledFields)
|
||||
{
|
||||
if(field.Value.Viewable)
|
||||
{
|
||||
lastField = field.Value;
|
||||
if(field.Value.Type == "password")
|
||||
{
|
||||
lastPasswordField = field.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prioritize password field over others.
|
||||
if(lastPasswordField != null)
|
||||
{
|
||||
Script.Add(new List<string> { "focus_by_opid", lastPasswordField.OpId });
|
||||
}
|
||||
else if(lastField != null)
|
||||
{
|
||||
Script.Add(new List<string> { "focus_by_opid", lastField.OpId });
|
||||
}
|
||||
}
|
||||
|
||||
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 = "documentUUID")]
|
||||
public object DocumentUUID { get; set; }
|
||||
[JsonProperty(PropertyName = "properties")]
|
||||
public object Properties { get; set; } = new object();
|
||||
[JsonProperty(PropertyName = "options")]
|
||||
public object Options { get; set; } = new { animate = false };
|
||||
[JsonProperty(PropertyName = "metadata")]
|
||||
public object MetaData { get; set; } = new object();
|
||||
}
|
||||
}
|
52
src/iOS.Extension/Models/PageDetails.cs
Normal file
52
src/iOS.Extension/Models/PageDetails.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.iOS.Extension.Models
|
||||
{
|
||||
public class PageDetails
|
||||
{
|
||||
public string DocumentUUID { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string DocumentUrl { get; set; }
|
||||
public string TabUrl { get; set; }
|
||||
public Dictionary<string, Form> Forms { get; set; }
|
||||
public List<Field> Fields { get; set; }
|
||||
public long CollectedTimestamp { get; set; }
|
||||
public bool HasPasswordField => Fields.Any(f => f.Type == "password");
|
||||
|
||||
public class Form
|
||||
{
|
||||
public string OpId { get; set; }
|
||||
public string HtmlName { get; set; }
|
||||
public string HtmlId { get; set; }
|
||||
public string HtmlAction { get; set; }
|
||||
public string HtmlMethod { get; set; }
|
||||
}
|
||||
|
||||
public class Field
|
||||
{
|
||||
public string OpId { get; set; }
|
||||
public int ElementNumber { get; set; }
|
||||
public bool Visible { get; set; }
|
||||
public bool Viewable { get; set; }
|
||||
public string HtmlId { get; set; }
|
||||
public string HtmlName { get; set; }
|
||||
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; }
|
||||
public bool Readonly { get; set; }
|
||||
public string OnePasswordFieldType { get; set; }
|
||||
public string Form { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -68,6 +68,9 @@
|
||||
<Compile Include="AppDelegate.cs" />
|
||||
<None Include="Info.plist" />
|
||||
<None Include="Entitlements.plist" />
|
||||
<Compile Include="Models\Context.cs" />
|
||||
<Compile Include="Models\FillScript.cs" />
|
||||
<Compile Include="Models\PageDetails.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="ActionViewController.designer.cs">
|
||||
<DependentUpon>ActionViewController.cs</DependentUpon>
|
||||
@ -84,6 +87,10 @@
|
||||
<Reference Include="Xamarin.iOS" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\App\App.csproj">
|
||||
<Project>{ee44c6a1-2a85-45fe-8d9b-bf1d5f88809c}</Project>
|
||||
<Name>App</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\iOS.Core\iOS.Core.csproj">
|
||||
<Project>{e71f3053-056c-4381-9638-048ed73bdff6}</Project>
|
||||
<Name>iOS.Core</Name>
|
||||
@ -108,8 +115,6 @@
|
||||
<BundleResource Include="Resources\logo_white%402x.png" />
|
||||
<BundleResource Include="Resources\logo_white%403x.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Models\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.AppExtension.CSharp.targets" />
|
||||
</Project>
|
Loading…
Reference in New Issue
Block a user