[Linked fields] Add Linked Field as a custom field type (#1563)

* Add linked fields support

* Fix style, don't show linked field if Secure Note

* Finish basic linked fields for Login

* Use Field.LinkedId to store linked field info

* Reset Linked Custom Fields if cipherType changes

* Refactor to use ItemView class

* Use enum for LinkedId

* Detect if no linkedFieldOptions
This commit is contained in:
Thomas Rittson 2021-11-09 07:34:16 +10:00 committed by GitHub
parent 3cb8adeeff
commit 90b62d61ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 263 additions and 30 deletions

View File

@ -656,6 +656,16 @@
</Keyboard>
</Entry.Keyboard>
</controls:MonoEntry>
<StackLayout
StyleClass="box-row, box-row-input"
IsVisible="{Binding IsLinkedType}">
<Picker
x:Name="_linkedFieldOptionPicker"
ItemsSource="{Binding LinkedFieldOptions, Mode=OneTime}"
SelectedIndex="{Binding LinkedFieldOptionSelectedIndex}"
ItemDisplayBinding="{Binding Key}"
StyleClass="box-value" />
</StackLayout>
<Switch
IsToggled="{Binding BooleanValue}"
Grid.Row="0"

View File

@ -64,13 +64,6 @@ namespace Bit.App.Pages
new KeyValuePair<UriMatchType?, string>(UriMatchType.Exact, AppResources.Exact),
new KeyValuePair<UriMatchType?, string>(UriMatchType.Never, AppResources.Never)
};
private List<KeyValuePair<FieldType, string>> _fieldTypeOptions =
new List<KeyValuePair<FieldType, string>>
{
new KeyValuePair<FieldType, string>(FieldType.Text, AppResources.FieldTypeText),
new KeyValuePair<FieldType, string>(FieldType.Hidden, AppResources.FieldTypeHidden),
new KeyValuePair<FieldType, string>(FieldType.Boolean, AppResources.FieldTypeBoolean)
};
public AddEditPageViewModel()
{
@ -667,8 +660,20 @@ namespace Bit.App.Pages
public async void AddField()
{
var fieldTypeOptions = new List<KeyValuePair<FieldType, string>>
{
new KeyValuePair<FieldType, string>(FieldType.Text, AppResources.FieldTypeText),
new KeyValuePair<FieldType, string>(FieldType.Hidden, AppResources.FieldTypeHidden),
new KeyValuePair<FieldType, string>(FieldType.Boolean, AppResources.FieldTypeBoolean),
};
if (Cipher.LinkedFieldOptions != null)
{
fieldTypeOptions.Add(new KeyValuePair<FieldType, string>(FieldType.Linked, AppResources.FieldTypeLinked));
}
var typeSelection = await Page.DisplayActionSheet(AppResources.SelectTypeField, AppResources.Cancel, null,
_fieldTypeOptions.Select(f => f.Value).ToArray());
fieldTypeOptions.Select(f => f.Value).ToArray());
if (typeSelection != null && typeSelection != AppResources.Cancel)
{
var name = await _deviceActionService.DisplayPromptAync(AppResources.CustomFieldName);
@ -680,7 +685,7 @@ namespace Bit.App.Pages
{
Fields = new ExtendedObservableCollection<AddEditPageFieldViewModel>();
}
var type = _fieldTypeOptions.FirstOrDefault(f => f.Value == typeSelection).Key;
var type = fieldTypeOptions.FirstOrDefault(f => f.Value == typeSelection).Key;
Fields.Add(new AddEditPageFieldViewModel(Cipher, new FieldView
{
Type = type,
@ -746,6 +751,12 @@ namespace Bit.App.Pages
{
Cipher.Type = TypeOptions[TypeSelectedIndex].Value;
TriggerCipherChanged();
// Linked Custom Fields only apply to a specific item type
foreach (var field in Fields.Where(f => f.IsLinkedType).ToList())
{
Fields.Remove(field);
}
}
}
@ -850,23 +861,29 @@ namespace Bit.App.Pages
public class AddEditPageFieldViewModel : ExtendedViewModel
{
private II18nService _i18nService;
private FieldView _field;
private CipherView _cipher;
private bool _showHiddenValue;
private bool _booleanValue;
private int _linkedFieldOptionSelectedIndex;
private string[] _additionalFieldProperties = new string[]
{
nameof(IsBooleanType),
nameof(IsHiddenType),
nameof(IsTextType),
nameof(IsLinkedType),
};
public AddEditPageFieldViewModel(CipherView cipher, FieldView field)
{
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_cipher = cipher;
Field = field;
ToggleHiddenValueCommand = new Command(ToggleHiddenValue);
BooleanValue = IsBooleanType && field.Value == "true";
LinkedFieldOptionSelectedIndex = !Field.LinkedId.HasValue ? 0 :
LinkedFieldOptions.FindIndex(lfo => lfo.Value == Field.LinkedId.Value);
}
public FieldView Field
@ -898,12 +915,32 @@ namespace Bit.App.Pages
}
}
public int LinkedFieldOptionSelectedIndex
{
get => _linkedFieldOptionSelectedIndex;
set
{
if (SetProperty(ref _linkedFieldOptionSelectedIndex, value))
{
LinkedFieldValueChanged();
}
}
}
public List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions
{
get => _cipher.LinkedFieldOptions
.Select(kvp => new KeyValuePair<string, LinkedIdType>(_i18nService.T(kvp.Key), kvp.Value))
.ToList();
}
public Command ToggleHiddenValueCommand { get; set; }
public string ShowHiddenValueIcon => _showHiddenValue ? "" : "";
public bool IsTextType => _field.Type == FieldType.Text;
public bool IsBooleanType => _field.Type == FieldType.Boolean;
public bool IsHiddenType => _field.Type == FieldType.Hidden;
public bool IsLinkedType => _field.Type == FieldType.Linked;
public bool ShowViewHidden => IsHiddenType && (_cipher.ViewPassword || _field.NewField);
public void ToggleHiddenValue()
@ -920,5 +957,14 @@ namespace Bit.App.Pages
{
TriggerPropertyChanged(nameof(Field), _additionalFieldProperties);
}
private void LinkedFieldValueChanged()
{
if (Field != null && LinkedFieldOptionSelectedIndex > -1)
{
Field.LinkedId = LinkedFieldOptions.Find(lfo =>
lfo.Value == LinkedFieldOptions[LinkedFieldOptionSelectedIndex].Value).Value;
}
}
}
}

View File

@ -561,6 +561,12 @@
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsTextType}" />
<controls:FaLabel
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsLinkedType}" />
<controls:FaLabel
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"

View File

@ -704,6 +704,7 @@ namespace Bit.App.Pages
public class ViewPageFieldViewModel : ExtendedViewModel
{
private II18nService _i18nService;
private ViewPageViewModel _vm;
private FieldView _field;
private CipherView _cipher;
@ -711,6 +712,7 @@ namespace Bit.App.Pages
public ViewPageFieldViewModel(ViewPageViewModel vm, CipherView cipher, FieldView field)
{
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_vm = vm;
_cipher = cipher;
Field = field;
@ -741,16 +743,38 @@ namespace Bit.App.Pages
});
}
public string ValueText
{
get
{
if (IsBooleanType)
{
return _field.Value == "true" ? "" : "";
}
else if (IsLinkedType)
{
var i18nKey = _cipher.LinkedFieldI18nKey(Field.LinkedId.GetValueOrDefault());
return " " + _i18nService.T(i18nKey);
}
else
{
return _field.Value;
}
}
}
public Command ToggleHiddenValueCommand { get; set; }
public string ValueText => IsBooleanType ? (_field.Value == "true" ? "" : "") : _field.Value;
public string ShowHiddenValueIcon => _showHiddenValue ? "" : "";
public bool IsTextType => _field.Type == Core.Enums.FieldType.Text;
public bool IsBooleanType => _field.Type == Core.Enums.FieldType.Boolean;
public bool IsHiddenType => _field.Type == Core.Enums.FieldType.Hidden;
public bool IsLinkedType => _field.Type == Core.Enums.FieldType.Linked;
public bool ShowViewHidden => IsHiddenType && _cipher.ViewPassword;
public bool ShowCopyButton => _field.Type != Core.Enums.FieldType.Boolean &&
!string.IsNullOrWhiteSpace(_field.Value) && !(IsHiddenType && !_cipher.ViewPassword);
!string.IsNullOrWhiteSpace(_field.Value) &&
!(IsHiddenType && !_cipher.ViewPassword) &&
_field.Type != FieldType.Linked;
public async void ToggleHiddenValue()
{

View File

@ -1,6 +1,7 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@ -9,9 +10,10 @@
namespace Bit.App.Resources {
using System;
using System.Reflection;
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class AppResources {
@ -1779,6 +1781,12 @@ namespace Bit.App.Resources {
}
}
public static string FullName {
get {
return ResourceManager.GetString("FullName", resourceCulture);
}
}
public static string LicenseNumber {
get {
return ResourceManager.GetString("LicenseNumber", resourceCulture);
@ -2025,6 +2033,12 @@ namespace Bit.App.Resources {
}
}
public static string FieldTypeLinked {
get {
return ResourceManager.GetString("FieldTypeLinked", resourceCulture);
}
}
public static string FieldTypeText {
get {
return ResourceManager.GetString("FieldTypeText", resourceCulture);

View File

@ -1059,6 +1059,9 @@
<data name="LastName" xml:space="preserve">
<value>Last Name</value>
</data>
<data name="FullName" xml:space="preserve">
<value>Full Name</value>
</data>
<data name="LicenseNumber" xml:space="preserve">
<value>License Number</value>
</data>
@ -1183,6 +1186,9 @@
<data name="FieldTypeHidden" xml:space="preserve">
<value>Hidden</value>
</data>
<data name="FieldTypeLinked" xml:space="preserve">
<value>Linked</value>
</data>
<data name="FieldTypeText" xml:space="preserve">
<value>Text</value>
</data>

View File

@ -4,6 +4,7 @@
{
Text = 0,
Hidden = 1,
Boolean = 2
Boolean = 2,
Linked = 3,
}
}

View File

@ -0,0 +1,38 @@
namespace Bit.Core.Enums {
public enum LinkedIdType: int
{
// Login
Login_Username = 100,
Login_Password = 101,
// Card
Card_CardholderName = 300,
Card_ExpMonth = 301,
Card_ExpYear = 302,
Card_Code = 303,
Card_Brand = 304,
Card_Number = 305,
// Identity
Identity_Title = 400,
Identity_MiddleName = 401,
Identity_Address1 = 402,
Identity_Address2 = 403,
Identity_Address3 = 404,
Identity_City = 405,
Identity_State = 406,
Identity_PostalCode = 407,
Identity_Country = 408,
Identity_Company = 409,
Identity_Email = 410,
Identity_Phone = 411,
Identity_Ssn = 412,
Identity_Username = 413,
Identity_PassportNumber = 414,
Identity_LicenseNumber = 415,
Identity_FirstName = 416,
Identity_LastName = 417,
Identity_FullName = 418,
}
}

View File

@ -7,5 +7,6 @@ namespace Bit.Core.Models.Api
public FieldType Type { get; set; }
public string Name { get; set; }
public string Value { get; set; }
public LinkedIdType? LinkedId { get; set; }
}
}

View File

@ -12,10 +12,12 @@ namespace Bit.Core.Models.Data
Type = data.Type;
Name = data.Name;
Value = data.Value;
LinkedId = data.LinkedId;
}
public FieldType Type { get; set; }
public string Name { get; set; }
public string Value { get; set; }
public LinkedIdType? LinkedId { get; set; }
}
}

View File

@ -19,12 +19,14 @@ namespace Bit.Core.Models.Domain
public Field(FieldData obj, bool alreadyEncrypted = false)
{
Type = obj.Type;
LinkedId = obj.LinkedId;
BuildDomainModel(this, obj, _map, alreadyEncrypted);
}
public EncString Name { get; set; }
public EncString Value { get; set; }
public FieldType Type { get; set; }
public LinkedIdType? LinkedId { get; set; }
public Task<FieldView> DecryptAsync(string orgId)
{
@ -38,10 +40,12 @@ namespace Bit.Core.Models.Domain
{
"Name",
"Value",
"Type"
"Type",
"LinkedId"
}, new HashSet<string>
{
"Type"
"Type",
"LinkedId"
});
return f;
}

View File

@ -81,7 +81,8 @@ namespace Bit.Core.Models.Request
{
Type = f.Type,
Name = f.Name?.EncryptedString,
Value = f.Value?.EncryptedString
Value = f.Value?.EncryptedString,
LinkedId = f.LinkedId,
}).ToList();
PasswordHistory = cipher.PasswordHistory?.Select(ph => new PasswordHistoryRequest

View File

@ -1,9 +1,11 @@
using Bit.Core.Models.Domain;
using Bit.Core.Enums;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Bit.Core.Models.View
{
public class CardView : View
public class CardView : ItemView
{
private string _brand;
private string _number;
@ -40,7 +42,7 @@ namespace Bit.Core.Models.View
}
}
public string SubTitle
public override string SubTitle
{
get
{
@ -82,6 +84,19 @@ namespace Bit.Core.Models.View
}
}
public override List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions
{
get => new List<KeyValuePair<string, LinkedIdType>>()
{
new KeyValuePair<string, LinkedIdType>("CardholderName", LinkedIdType.Card_CardholderName),
new KeyValuePair<string, LinkedIdType>("ExpirationMonth", LinkedIdType.Card_ExpMonth),
new KeyValuePair<string, LinkedIdType>("ExpirationYear", LinkedIdType.Card_ExpYear),
new KeyValuePair<string, LinkedIdType>("SecurityCode", LinkedIdType.Card_Code),
new KeyValuePair<string, LinkedIdType>("Brand", LinkedIdType.Card_Brand),
new KeyValuePair<string, LinkedIdType>("Number", LinkedIdType.Card_Number),
};
}
private string FormatYear(string year)
{
return year.Length == 2 ? string.Concat("20", year) : year;

View File

@ -50,20 +50,20 @@ namespace Bit.Core.Models.View
public DateTime? DeletedDate { get; set; }
public CipherRepromptType Reprompt { get; set; }
public string SubTitle
public ItemView Item
{
get
{
switch (Type)
{
case CipherType.Login:
return Login.SubTitle;
return Login;
case CipherType.SecureNote:
return SecureNote.SubTitle;
return SecureNote;
case CipherType.Card:
return Card.SubTitle;
return Card;
case CipherType.Identity:
return Identity.SubTitle;
return Identity;
default:
break;
}
@ -71,6 +71,8 @@ namespace Bit.Core.Models.View
}
}
public List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions => Item.LinkedFieldOptions;
public string SubTitle => Item.SubTitle;
public bool Shared => OrganizationId != null;
public bool HasPasswordHistory => PasswordHistory?.Any() ?? false;
public bool HasAttachments => Attachments?.Any() ?? false;
@ -102,5 +104,11 @@ namespace Bit.Core.Models.View
}
}
public bool IsDeleted => DeletedDate.HasValue;
public string LinkedFieldI18nKey(LinkedIdType id)
{
return LinkedFieldOptions.Find(lfo => lfo.Value == id).Key;
}
}
}

View File

@ -10,6 +10,7 @@ namespace Bit.Core.Models.View
public FieldView(Field f)
{
Type = f.Type;
LinkedId = f.LinkedId;
}
public string Name { get; set; }
@ -17,5 +18,6 @@ namespace Bit.Core.Models.View
public FieldType Type { get; set; }
public string MaskedValue => Value != null ? "••••••••" : null;
public bool NewField { get; set; }
public LinkedIdType? LinkedId { get; set; }
}
}

View File

@ -1,8 +1,10 @@
using Bit.Core.Models.Domain;
using Bit.Core.Enums;
using System.Collections.Generic;
namespace Bit.Core.Models.View
{
public class IdentityView : View
public class IdentityView : ItemView
{
private string _firstName;
private string _lastName;
@ -47,7 +49,7 @@ namespace Bit.Core.Models.View
public string PassportNumber { get; set; }
public string LicenseNumber { get; set; }
public string SubTitle
public override string SubTitle
{
get
{
@ -141,5 +143,31 @@ namespace Bit.Core.Models.View
return string.Format("{0}, {1}, {2}", city, state, postalCode);
}
}
public override List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions
{
get => new List<KeyValuePair<string, LinkedIdType>>()
{
new KeyValuePair<string, LinkedIdType>("Title", LinkedIdType.Identity_Title),
new KeyValuePair<string, LinkedIdType>("MiddleName", LinkedIdType.Identity_MiddleName),
new KeyValuePair<string, LinkedIdType>("Address1", LinkedIdType.Identity_Address1),
new KeyValuePair<string, LinkedIdType>("Address2", LinkedIdType.Identity_Address2),
new KeyValuePair<string, LinkedIdType>("Address3", LinkedIdType.Identity_Address3),
new KeyValuePair<string, LinkedIdType>("CityTown", LinkedIdType.Identity_City),
new KeyValuePair<string, LinkedIdType>("StateProvince", LinkedIdType.Identity_State),
new KeyValuePair<string, LinkedIdType>("ZipPostalCode", LinkedIdType.Identity_PostalCode),
new KeyValuePair<string, LinkedIdType>("Country", LinkedIdType.Identity_Country),
new KeyValuePair<string, LinkedIdType>("Company", LinkedIdType.Identity_Company),
new KeyValuePair<string, LinkedIdType>("Email", LinkedIdType.Identity_Email),
new KeyValuePair<string, LinkedIdType>("Phone", LinkedIdType.Identity_Phone),
new KeyValuePair<string, LinkedIdType>("SSN", LinkedIdType.Identity_Ssn),
new KeyValuePair<string, LinkedIdType>("Username", LinkedIdType.Identity_Username),
new KeyValuePair<string, LinkedIdType>("PassportNumber", LinkedIdType.Identity_PassportNumber),
new KeyValuePair<string, LinkedIdType>("LicenseNumber", LinkedIdType.Identity_LicenseNumber),
new KeyValuePair<string, LinkedIdType>("FirstName", LinkedIdType.Identity_FirstName),
new KeyValuePair<string, LinkedIdType>("LastName", LinkedIdType.Identity_LastName),
new KeyValuePair<string, LinkedIdType>("FullName", LinkedIdType.Identity_FullName),
};
}
}
}

View File

@ -0,0 +1,14 @@
using System.Collections.Generic;
using Bit.Core.Enums;
namespace Bit.Core.Models.View
{
public abstract class ItemView : View
{
public ItemView() { }
public abstract string SubTitle { get; }
public abstract List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions { get; }
}
}

View File

@ -1,11 +1,12 @@
using Bit.Core.Models.Domain;
using Bit.Core.Enums;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Bit.Core.Models.View
{
public class LoginView : View
public class LoginView : ItemView
{
public LoginView() { }
@ -21,9 +22,18 @@ namespace Bit.Core.Models.View
public List<LoginUriView> Uris { get; set; }
public string Uri => HasUris ? Uris[0].Uri : null;
public string MaskedPassword => Password != null ? "••••••••" : null;
public string SubTitle => Username;
public override string SubTitle => Username;
public bool CanLaunch => HasUris && Uris.Any(u => u.CanLaunch);
public string LaunchUri => HasUris ? Uris.FirstOrDefault(u => u.CanLaunch)?.LaunchUri : null;
public bool HasUris => (Uris?.Count ?? 0) > 0;
public override List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions
{
get => new List<KeyValuePair<string, LinkedIdType>>()
{
new KeyValuePair<string, LinkedIdType>("Username", LinkedIdType.Login_Username),
new KeyValuePair<string, LinkedIdType>("Password", LinkedIdType.Login_Password),
};
}
}
}

View File

@ -1,9 +1,10 @@
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using System.Collections.Generic;
namespace Bit.Core.Models.View
{
public class SecureNoteView : View
public class SecureNoteView : ItemView
{
public SecureNoteView() { }
@ -13,6 +14,7 @@ namespace Bit.Core.Models.View
}
public SecureNoteType Type { get; set; }
public string SubTitle => null;
public override string SubTitle => null;
public override List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions => null;
}
}

View File

@ -1190,7 +1190,8 @@ namespace Bit.Core.Services
{
var field = new Field
{
Type = model.Type
Type = model.Type,
LinkedId = model.LinkedId,
};
// normalize boolean type field values
if (model.Type == FieldType.Boolean && model.Value != "true")