Soft delete feature (#890)

* [Soft Delete] Added trash folder to mobile (#856)

* [Soft Delete] Added trash folder to mobile

* [Soft Delete] - Revert send to trash label

Co-authored-by: Chad Scharf <cscharf@users.noreply.github.com>

* [Soft Delete] - Fix for iOS autofill index behavior (#859)

* [Soft Delete] Added trash folder to mobile

* [Soft Delete] - Revert send to trash label

* [Soft Delete] - iOS autofill index behavior fix

Co-authored-by: Chad Scharf <cscharf@users.noreply.github.com>

Co-authored-by: Chad Scharf <cscharf@users.noreply.github.com>
This commit is contained in:
Chad Scharf 2020-05-20 13:35:20 -04:00 committed by GitHub
parent 4b9a036e5e
commit ce965ba5e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 378 additions and 40 deletions

View file

@ -489,7 +489,8 @@ namespace Bit.App.Pages
AppResources.InternetConnectionRequiredTitle);
return false;
}
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.DoYouReallyWantToDelete,
var confirmed = await _platformUtilsService.ShowDialogAsync(
AppResources.DoYouReallyWantToSoftDeleteCipher,
null, AppResources.Yes, AppResources.Cancel);
if (!confirmed)
{
@ -497,11 +498,11 @@ namespace Bit.App.Pages
}
try
{
await _deviceActionService.ShowLoadingAsync(AppResources.Deleting);
await _cipherService.DeleteWithServerAsync(Cipher.Id);
await _deviceActionService.ShowLoadingAsync(AppResources.SoftDeleting);
await _cipherService.SoftDeleteWithServerAsync(Cipher.Id);
await _deviceActionService.HideLoadingAsync();
_platformUtilsService.ShowToast("success", null, AppResources.ItemDeleted);
_messagingService.Send("deletedCipher", Cipher);
_platformUtilsService.ShowToast("success", null, AppResources.ItemSoftDeleted);
_messagingService.Send("softDeletedCipher", Cipher);
return true;
}
catch (ApiException e)

View file

@ -16,14 +16,19 @@ namespace Bit.App.Pages
private bool _hasFocused;
public CiphersPage(Func<CipherView, bool> filter, bool folder = false, bool collection = false,
bool type = false, string autofillUrl = null)
bool type = false, string autofillUrl = null, bool deleted = false)
{
InitializeComponent();
_vm = BindingContext as CiphersPageViewModel;
_vm.Page = this;
_vm.Filter = filter;
_vm.AutofillUrl = _autofillUrl = autofillUrl;
if (folder)
_vm.Deleted = deleted;
if (deleted)
{
_vm.PageTitle = AppResources.SearchTrash;
}
else if (folder)
{
_vm.PageTitle = AppResources.SearchFolder;
}

View file

@ -44,6 +44,7 @@ namespace Bit.App.Pages
public ExtendedObservableCollection<CipherView> Ciphers { get; set; }
public Func<CipherView, bool> Filter { get; set; }
public string AutofillUrl { get; set; }
public bool Deleted { get; set; }
public bool ShowNoData
{
@ -105,7 +106,8 @@ namespace Bit.App.Pages
}
try
{
ciphers = await _searchService.SearchCiphersAsync(searchText, Filter, null, cts.Token);
ciphers = await _searchService.SearchCiphersAsync(searchText,
Filter ?? (c => c.IsDeleted == Deleted), null, cts.Token);
cts.Token.ThrowIfCancellationRequested();
}
catch (OperationCanceledException)

View file

@ -28,7 +28,8 @@ namespace Bit.App.Pages
private PreviousPageInfo _previousPage;
public GroupingsPage(bool mainPage, CipherType? type = null, string folderId = null,
string collectionId = null, string pageTitle = null, PreviousPageInfo previousPage = null)
string collectionId = null, string pageTitle = null, PreviousPageInfo previousPage = null,
bool deleted = false)
{
_pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks);
InitializeComponent();
@ -47,6 +48,7 @@ namespace Bit.App.Pages
_vm.Type = type;
_vm.FolderId = folderId;
_vm.CollectionId = collectionId;
_vm.Deleted = deleted;
_previousPage = previousPage;
if (pageTitle != null)
{
@ -64,6 +66,11 @@ namespace Bit.App.Pages
ToolbarItems.Add(_lockItem);
ToolbarItems.Add(_exitItem);
}
if (deleted)
{
_absLayout.Children.Remove(_fab);
ToolbarItems.Remove(_addItem);
}
}
public ExtendedListView ListView { get; set; }
@ -132,6 +139,7 @@ namespace Bit.App.Pages
}
}
await ShowPreviousPageAsync();
AdjustToolbar();
}, _mainContent);
if (!_vm.MainPage)
@ -200,7 +208,11 @@ namespace Bit.App.Pages
return;
}
if (item.Cipher != null)
if (item.IsTrash)
{
await _vm.SelectTrashAsync();
}
else if (item.Cipher != null)
{
await _vm.SelectCipherAsync(item.Cipher);
}
@ -223,7 +235,7 @@ namespace Bit.App.Pages
if (DoOnce())
{
var page = new CiphersPage(_vm.Filter, _vm.FolderId != null, _vm.CollectionId != null,
_vm.Type != null);
_vm.Type != null, deleted: _vm.Deleted);
await Navigation.PushModalAsync(new NavigationPage(page), false);
}
}
@ -245,7 +257,7 @@ namespace Bit.App.Pages
private async void AddButton_Clicked(object sender, EventArgs e)
{
if (DoOnce())
if (!_vm.Deleted && DoOnce())
{
var page = new AddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId);
await Navigation.PushModalAsync(new NavigationPage(page));
@ -268,5 +280,11 @@ namespace Bit.App.Pages
}
_previousPage = null;
}
private void AdjustToolbar()
{
_addItem.IsEnabled = !_vm.Deleted;
_addItem.IconImageSource = _vm.Deleted ? null : "plus.png";
}
}
}

View file

@ -15,6 +15,7 @@ namespace Bit.App.Pages
public CipherType? Type { get; set; }
public string ItemCount { get; set; }
public bool FuzzyAutofill { get; set; }
public bool IsTrash { get; set; }
public string Name
{
@ -24,7 +25,11 @@ namespace Bit.App.Pages
{
return _name;
}
if (Folder != null)
if (IsTrash)
{
_name = AppResources.Trash;
}
else if (Folder != null)
{
_name = Folder.Name;
}
@ -64,7 +69,11 @@ namespace Bit.App.Pages
{
return _icon;
}
if (Folder != null)
if (IsTrash)
{
_icon = "\uf014"; // fa-trash-o
}
else if (Folder != null)
{
_icon = Folder.Id == null ? "" : "";
}

View file

@ -32,6 +32,7 @@ namespace Bit.App.Pages
private Dictionary<string, int> _folderCounts = new Dictionary<string, int>();
private Dictionary<string, int> _collectionCounts = new Dictionary<string, int>();
private Dictionary<CipherType, int> _typeCounts = new Dictionary<CipherType, int>();
private int _deletedCount = 0;
private readonly ICipherService _cipherService;
private readonly IFolderService _folderService;
@ -73,6 +74,7 @@ namespace Bit.App.Pages
public string FolderId { get; set; }
public string CollectionId { get; set; }
public Func<CipherView, bool> Filter { get; set; }
public bool Deleted { get; set; }
public bool HasCiphers { get; set; }
public bool HasFolders { get; set; }
@ -152,7 +154,7 @@ namespace Bit.App.Pages
ShowNoData = false;
Loading = true;
ShowList = false;
ShowAddCipherButton = true;
ShowAddCipherButton = !Deleted;
var groupedItems = new List<GroupingsPageListGroup>();
var page = Page as GroupingsPage;
@ -233,7 +235,8 @@ namespace Bit.App.Pages
}
if (Ciphers?.Any() ?? false)
{
var ciphersListItems = Ciphers.Select(c => new GroupingsPageListItem { Cipher = c }).ToList();
var ciphersListItems = Ciphers.Where(c => c.IsDeleted == Deleted)
.Select(c => new GroupingsPageListItem { Cipher = c }).ToList();
groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items,
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
}
@ -244,6 +247,18 @@ namespace Bit.App.Pages
groupedItems.Add(new GroupingsPageListGroup(noFolderCiphersListItems, AppResources.FolderNone,
noFolderCiphersListItems.Count, uppercaseGroupNames, false));
}
// Ensure this is last in the list (appears at the bottom)
if (MainPage && !Deleted)
{
groupedItems.Add(new GroupingsPageListGroup(new List<GroupingsPageListItem>()
{
new GroupingsPageListItem()
{
IsTrash = true,
ItemCount = _deletedCount.ToString("N0")
}
}, AppResources.Trash, _deletedCount, uppercaseGroupNames, false));
}
GroupedItems.ResetWithRange(groupedItems);
}
finally
@ -299,6 +314,12 @@ namespace Bit.App.Pages
await Page.Navigation.PushAsync(page);
}
public async Task SelectTrashAsync()
{
var page = new GroupingsPage(false, null, null, null, AppResources.Trash, null, true);
await Page.Navigation.PushAsync(page);
}
public async Task ExitAsync()
{
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.ExitConfirmation,
@ -344,6 +365,7 @@ namespace Bit.App.Pages
HasFolders = false;
HasCollections = false;
Filter = null;
_deletedCount = 0;
if (MainPage)
{
@ -356,9 +378,14 @@ namespace Bit.App.Pages
}
else
{
if (Type != null)
if (Deleted)
{
Filter = c => c.Type == Type.Value;
Filter = c => c.IsDeleted;
NoDataText = AppResources.NoItemsTrash;
}
else if (Type != null)
{
Filter = c => c.Type == Type.Value && !c.IsDeleted;
}
else if (FolderId != null)
{
@ -377,7 +404,7 @@ namespace Bit.App.Pages
{
PageTitle = AppResources.FolderNone;
}
Filter = c => c.FolderId == folderId;
Filter = c => c.FolderId == folderId && !c.IsDeleted;
}
else if (CollectionId != null)
{
@ -389,7 +416,7 @@ namespace Bit.App.Pages
PageTitle = collectionNode.Node.Name;
NestedCollections = (collectionNode.Children?.Count ?? 0) > 0 ? collectionNode.Children : null;
}
Filter = c => c.CollectionIds?.Contains(CollectionId) ?? false;
Filter = c => c.CollectionIds?.Contains(CollectionId) ?? false && !c.IsDeleted;
}
else
{
@ -402,6 +429,12 @@ namespace Bit.App.Pages
{
if (MainPage)
{
if (c.IsDeleted)
{
_deletedCount++;
continue;
}
if (c.Favorite)
{
if (FavoriteCiphers == null)

View file

@ -32,7 +32,7 @@
Order="Secondary" />
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
x:Name="_closeItem" x:Key="closeItem" />
<ToolbarItem Text="{u:I18n Edit}" Clicked="EditToolbarItem_Clicked" Order="Primary"
<ToolbarItem Clicked="EditToolbarItem_Clicked" Order="Primary"
x:Name="_editItem" x:Key="editItem" />
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary"
x:Name="_moreItem" x:Key="moreItem"
@ -670,7 +670,8 @@
AbsoluteLayout.LayoutFlags="PositionProportional"
AbsoluteLayout.LayoutBounds="1, 1, AutoSize, AutoSize"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n EditItem}">
AutomationProperties.Name="{u:I18n EditItem}"
IsVisible="{Binding CanEdit}">
<Button.Effects>
<effects:FabShadowEffect />
</Button.Effects>

View file

@ -118,10 +118,20 @@ namespace Bit.App.Pages
private async void EditToolbarItem_Clicked(object sender, System.EventArgs e)
{
if (DoOnce())
{
if (_vm.IsDeleted)
{
if (await _vm.RestoreAsync())
{
await Navigation.PopModalAsync();
}
}
else
{
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_vm.CipherId)));
}
}
}
private void EditButton_Clicked(object sender, System.EventArgs e)
{
@ -234,7 +244,17 @@ namespace Bit.App.Pages
private void AdjustToolbar()
{
if (Device.RuntimePlatform != Device.Android || _vm.Cipher == null)
if (_vm.Cipher == null)
{
return;
}
_editItem.Text = _vm.Cipher.IsDeleted ? AppResources.Restore :
AppResources.Edit;
if (_vm.Cipher.IsDeleted)
{
_absLayout.Children.Remove(_fab);
}
if (Device.RuntimePlatform != Device.Android)
{
return;
}
@ -268,6 +288,10 @@ namespace Bit.App.Pages
ToolbarItems.Insert(1, _collectionsItem);
}
}
if (_vm.Cipher.IsDeleted && !ToolbarItems.Contains(_editItem))
{
ToolbarItems.Insert(1, _editItem);
}
}
}
}

View file

@ -86,6 +86,8 @@ namespace Bit.App.Pages
nameof(PasswordUpdatedText),
nameof(PasswordHistoryText),
nameof(ShowIdentityAddress),
nameof(IsDeleted),
nameof(CanEdit),
});
}
public List<ViewPageFieldViewModel> Fields
@ -210,6 +212,8 @@ namespace Bit.App.Pages
Page.Resources["textTotp"] = Application.Current.Resources[value ? "text-danger" : "text-default"];
}
}
public bool IsDeleted => Cipher.IsDeleted;
public bool CanEdit => !Cipher.IsDeleted;
public async Task<bool> LoadAsync(Action finishedLoadingAction = null)
{
@ -282,7 +286,8 @@ namespace Bit.App.Pages
AppResources.InternetConnectionRequiredTitle);
return false;
}
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.DoYouReallyWantToDelete,
var confirmed = await _platformUtilsService.ShowDialogAsync(
Cipher.IsDeleted ? AppResources.DoYouReallyWantToPermanentlyDeleteCipher : AppResources.DoYouReallyWantToSoftDeleteCipher,
null, AppResources.Yes, AppResources.Cancel);
if (!confirmed)
{
@ -290,11 +295,58 @@ namespace Bit.App.Pages
}
try
{
await _deviceActionService.ShowLoadingAsync(AppResources.Deleting);
await _deviceActionService.ShowLoadingAsync(Cipher.IsDeleted ? AppResources.Deleting : AppResources.SoftDeleting);
if (Cipher.IsDeleted)
{
await _cipherService.DeleteWithServerAsync(Cipher.Id);
}
else
{
await _cipherService.SoftDeleteWithServerAsync(Cipher.Id);
}
await _deviceActionService.HideLoadingAsync();
_platformUtilsService.ShowToast("success", null, AppResources.ItemDeleted);
_messagingService.Send("deletedCipher", Cipher);
_platformUtilsService.ShowToast("success", null,
Cipher.IsDeleted ? AppResources.ItemDeleted : AppResources.ItemSoftDeleted);
_messagingService.Send(Cipher.IsDeleted ? "deletedCipher" : "softDeletedCipher", Cipher);
return true;
}
catch (ApiException e)
{
await _deviceActionService.HideLoadingAsync();
if (e?.Error != null)
{
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
AppResources.AnErrorHasOccurred);
}
}
return false;
}
public async Task<bool> RestoreAsync()
{
if (!IsDeleted)
{
return false;
}
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
{
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle);
return false;
}
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.DoYouReallyWantToRestoreCipher,
null, AppResources.Yes, AppResources.Cancel);
if (!confirmed)
{
return false;
}
try
{
await _deviceActionService.ShowLoadingAsync(AppResources.Restoring);
await _cipherService.RestoreWithServerAsync(Cipher.Id);
await _deviceActionService.HideLoadingAsync();
_platformUtilsService.ShowToast("success", null, AppResources.ItemRestored);
_messagingService.Send("restoredCipher", Cipher);
return true;
}
catch (ApiException e)

View file

@ -10,6 +10,7 @@
namespace Bit.App.Resources {
using System;
using System.Reflection;
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
@ -1924,6 +1925,12 @@ namespace Bit.App.Resources {
}
}
public static string NoItemsTrash {
get {
return ResourceManager.GetString("NoItemsTrash", resourceCulture);
}
}
public static string AutofillAccessibilityService {
get {
return ResourceManager.GetString("AutofillAccessibilityService", resourceCulture);
@ -2871,5 +2878,65 @@ namespace Bit.App.Resources {
return ResourceManager.GetString("AutofillTileUriNotFound", resourceCulture);
}
}
public static string SoftDeleting {
get {
return ResourceManager.GetString("SoftDeleting", resourceCulture);
}
}
public static string ItemSoftDeleted {
get {
return ResourceManager.GetString("ItemSoftDeleted", resourceCulture);
}
}
public static string Restore {
get {
return ResourceManager.GetString("Restore", resourceCulture);
}
}
public static string Restoring {
get {
return ResourceManager.GetString("Restoring", resourceCulture);
}
}
public static string ItemRestored {
get {
return ResourceManager.GetString("ItemRestored", resourceCulture);
}
}
public static string Trash {
get {
return ResourceManager.GetString("Trash", resourceCulture);
}
}
public static string SearchTrash {
get {
return ResourceManager.GetString("SearchTrash", resourceCulture);
}
}
public static string DoYouReallyWantToPermanentlyDeleteCipher {
get {
return ResourceManager.GetString("DoYouReallyWantToPermanentlyDeleteCipher", resourceCulture);
}
}
public static string DoYouReallyWantToRestoreCipher {
get {
return ResourceManager.GetString("DoYouReallyWantToRestoreCipher", resourceCulture);
}
}
public static string DoYouReallyWantToSoftDeleteCipher {
get {
return ResourceManager.GetString("DoYouReallyWantToSoftDeleteCipher", resourceCulture);
}
}
}
}

View file

@ -1131,6 +1131,9 @@
<data name="NoItemsFolder" xml:space="preserve">
<value>There are no items in this folder.</value>
</data>
<data name="NoItemsTrash" xml:space="preserve">
<value>There are no items in the trash.</value>
</data>
<data name="AutofillAccessibilityService" xml:space="preserve">
<value>Auto-fill Accessibility Service</value>
</data>
@ -1631,4 +1634,44 @@
<data name="AutofillTileUriNotFound" xml:space="preserve">
<value>No password fields detected</value>
</data>
<data name="SoftDeleting" xml:space="preserve">
<value>Sending to trash...</value>
<comment>Message shown when interacting with the server</comment>
</data>
<data name="ItemSoftDeleted" xml:space="preserve">
<value>Item has been sent to trash.</value>
<comment>Confirmation message after successfully soft-deleting a login</comment>
</data>
<data name="Restore" xml:space="preserve">
<value>Restore</value>
<comment>Restores an entity (verb).</comment>
</data>
<data name="Restoring" xml:space="preserve">
<value>Restoring...</value>
<comment>Message shown when interacting with the server</comment>
</data>
<data name="ItemRestored" xml:space="preserve">
<value>Item has been restored.</value>
<comment>Confirmation message after successfully restoring a soft-deleted item</comment>
</data>
<data name="Trash" xml:space="preserve">
<value>Trash</value>
<comment>(noun) Location of deleted items which have not yet been permanently deleted</comment>
</data>
<data name="SearchTrash" xml:space="preserve">
<value>Search trash</value>
<comment>(action prompt) Label for the search text field when viewing the trash folder</comment>
</data>
<data name="DoYouReallyWantToPermanentlyDeleteCipher" xml:space="preserve">
<value>Do you really want to permanently delete? This cannot be undone.</value>
<comment>Confirmation alert message when permanently deleteing a cipher.</comment>
</data>
<data name="DoYouReallyWantToRestoreCipher" xml:space="preserve">
<value>Do you really want to restore this item?</value>
<comment>Confirmation alert message when restoring a soft-deleted cipher.</comment>
</data>
<data name="DoYouReallyWantToSoftDeleteCipher" xml:space="preserve">
<value>Do you really want to send to the trash?</value>
<comment>Confirmation alert message when soft-deleting a cipher.</comment>
</data>
</root>

View file

@ -19,7 +19,11 @@ namespace Bit.App.Utilities
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
var eventService = ServiceContainer.Resolve<IEventService>("eventService");
var lockService = ServiceContainer.Resolve<ILockService>("lockService");
var options = new List<string> { AppResources.View, AppResources.Edit };
var options = new List<string> { AppResources.View };
if (!cipher.IsDeleted)
{
options.Add(AppResources.Edit);
}
if (cipher.Type == Core.Enums.CipherType.Login)
{
if (!string.IsNullOrWhiteSpace(cipher.Login.Username))

View file

@ -37,6 +37,8 @@ namespace Bit.Core.Abstractions
Task PutCipherCollectionsAsync(string id, CipherCollectionsRequest request);
Task<FolderResponse> PutFolderAsync(string id, FolderRequest request);
Task<CipherResponse> PutShareCipherAsync(string id, CipherShareRequest request);
Task PutDeleteCipherAsync(string id);
Task PutRestoreCipherAsync(string id);
Task RefreshIdentityTokenAsync();
Task<TResponse> SendAsync<TRequest, TResponse>(HttpMethod method, string path,
TRequest body, bool authed, bool hasResponse);

View file

@ -36,5 +36,7 @@ namespace Bit.Core.Abstractions
Task UpsertAsync(CipherData cipher);
Task UpsertAsync(List<CipherData> cipher);
Task<byte[]> DownloadAndDecryptAttachmentAsync(AttachmentView attachment, string organizationId);
Task SoftDeleteWithServerAsync(string id);
Task RestoreWithServerAsync(string id);
}
}

View file

@ -12,8 +12,8 @@ namespace Bit.Core.Abstractions
Task IndexCiphersAsync();
bool IsSearchable(string query);
Task<List<CipherView>> SearchCiphersAsync(string query, Func<CipherView, bool> filter = null,
List<CipherView> ciphers = null, CancellationToken ct = default(CancellationToken));
List<CipherView> ciphers = null, CancellationToken ct = default);
List<CipherView> SearchCiphersBasic(List<CipherView> ciphers, string query,
CancellationToken ct = default(CancellationToken));
CancellationToken ct = default, bool deleted = false);
}
}

View file

@ -26,6 +26,8 @@
Cipher_ClientCopiedHiddenField = 1112,
Cipher_ClientCopiedCardCode = 1113,
Cipher_ClientAutofilled = 1114,
Cipher_SoftDeleted = 1115,
Cipher_Restored = 1116,
Collection_Created = 1300,
Collection_Updated = 1301,

View file

@ -45,6 +45,7 @@ namespace Bit.Core.Models.Data
Fields = response.Fields?.Select(f => new FieldData(f)).ToList();
Attachments = response.Attachments?.Select(a => new AttachmentData(a)).ToList();
PasswordHistory = response.PasswordHistory?.Select(ph => new PasswordHistoryData(ph)).ToList();
DeletedDate = response.DeletedDate;
}
public string Id { get; set; }
@ -66,5 +67,6 @@ namespace Bit.Core.Models.Data
public List<AttachmentData> Attachments { get; set; }
public List<PasswordHistoryData> PasswordHistory { get; set; }
public List<string> CollectionIds { get; set; }
public DateTime? DeletedDate { get; set; }
}
}

View file

@ -51,6 +51,7 @@ namespace Bit.Core.Models.Domain
Attachments = obj.Attachments?.Select(a => new Attachment(a, alreadyEncrypted)).ToList();
Fields = obj.Fields?.Select(f => new Field(f, alreadyEncrypted)).ToList();
PasswordHistory = obj.PasswordHistory?.Select(ph => new PasswordHistory(ph, alreadyEncrypted)).ToList();
DeletedDate = obj.DeletedDate;
}
public string Id { get; set; }
@ -73,6 +74,8 @@ namespace Bit.Core.Models.Domain
public List<PasswordHistory> PasswordHistory { get; set; }
public HashSet<string> CollectionIds { get; set; }
public DateTime? DeletedDate { get; set; }
public async Task<CipherView> DecryptAsync()
{
var model = new CipherView(this);
@ -161,7 +164,8 @@ namespace Bit.Core.Models.Domain
Favorite = Favorite,
RevisionDate = RevisionDate,
Type = Type,
CollectionIds = CollectionIds.ToList()
CollectionIds = CollectionIds.ToList(),
DeletedDate = DeletedDate
};
BuildDataModel(this, c, new HashSet<string>
{

View file

@ -24,5 +24,6 @@ namespace Bit.Core.Models.Response
public List<AttachmentResponse> Attachments { get; set; }
public List<PasswordHistoryResponse> PasswordHistory { get; set; }
public List<string> CollectionIds { get; set; }
public DateTime? DeletedDate { get; set; }
}
}

View file

@ -22,6 +22,7 @@ namespace Bit.Core.Models.View
LocalData = c.LocalData;
CollectionIds = c.CollectionIds;
RevisionDate = c.RevisionDate;
DeletedDate = c.DeletedDate;
}
public string Id { get; set; }
@ -43,6 +44,7 @@ namespace Bit.Core.Models.View
public List<PasswordHistoryView> PasswordHistory { get; set; }
public HashSet<string> CollectionIds { get; set; }
public DateTime RevisionDate { get; set; }
public DateTime? DeletedDate { get; set; }
public string SubTitle
@ -96,5 +98,6 @@ namespace Bit.Core.Models.View
return Login.PasswordRevisionDate;
}
}
public bool IsDeleted => DeletedDate.HasValue;
}
}

View file

@ -245,6 +245,16 @@ namespace Bit.Core.Services
return SendAsync<object, object>(HttpMethod.Delete, string.Concat("/ciphers/", id), null, true, false);
}
public Task PutDeleteCipherAsync(string id)
{
return SendAsync<object, object>(HttpMethod.Put, string.Concat("/ciphers/", id, "/delete"), null, true, false);
}
public Task PutRestoreCipherAsync(string id)
{
return SendAsync<object, object>(HttpMethod.Put, string.Concat("/ciphers/", id, "/restore"), null, true, false);
}
#endregion
#region Attachments APIs

View file

@ -265,6 +265,10 @@ namespace Bit.Core.Services
var ciphers = await GetAllDecryptedAsync();
return ciphers.Where(cipher =>
{
if (cipher.IsDeleted)
{
return false;
}
if (folder && cipher.FolderId == groupingId)
{
return true;
@ -324,6 +328,11 @@ namespace Bit.Core.Services
foreach (var cipher in ciphers)
{
if (cipher.IsDeleted)
{
continue;
}
if (cipher.Type != CipherType.Login && (includeOtherTypes?.Any(t => t == cipher.Type) ?? false))
{
others.Add(cipher);
@ -695,6 +704,45 @@ namespace Bit.Core.Services
return null;
}
public async Task SoftDeleteWithServerAsync(string id)
{
var userId = await _userService.GetUserIdAsync();
var cipherKey = string.Format(Keys_CiphersFormat, userId);
var ciphers = await _storageService.GetAsync<Dictionary<string, CipherData>>(cipherKey);
if (ciphers == null)
{
return;
}
if (!ciphers.ContainsKey(id))
{
return;
}
await _apiService.PutDeleteCipherAsync(id);
ciphers[id].DeletedDate = DateTime.UtcNow;
await _storageService.SaveAsync(cipherKey, ciphers);
DecryptedCipherCache = null;
}
public async Task RestoreWithServerAsync(string id)
{
var userId = await _userService.GetUserIdAsync();
var cipherKey = string.Format(Keys_CiphersFormat, userId);
var ciphers = await _storageService.GetAsync<Dictionary<string, CipherData>>(cipherKey);
if (ciphers == null)
{
return;
}
if (!ciphers.ContainsKey(id))
{
return;
}
await _apiService.PutRestoreCipherAsync(id);
ciphers[id].DeletedDate = null;
await _storageService.SaveAsync(cipherKey, ciphers);
DecryptedCipherCache = null;
}
// Helpers
private async Task ShareAttachmentWithServerAsync(AttachmentView attachmentView, string cipherId,

View file

@ -35,7 +35,7 @@ namespace Bit.Core.Services
}
public async Task<List<CipherView>> SearchCiphersAsync(string query, Func<CipherView, bool> filter = null,
List<CipherView> ciphers = null, CancellationToken ct = default(CancellationToken))
List<CipherView> ciphers = null, CancellationToken ct = default)
{
var results = new List<CipherView>();
if (query != null)
@ -68,7 +68,7 @@ namespace Bit.Core.Services
}
public List<CipherView> SearchCiphersBasic(List<CipherView> ciphers, string query,
CancellationToken ct = default(CancellationToken))
CancellationToken ct = default, bool deleted = false)
{
ct.ThrowIfCancellationRequested();
query = query.Trim().ToLower();

View file

@ -63,7 +63,7 @@ namespace Bit.iOS.Core.Views
}
_allItems = combinedLogins
.Where(c => c.Type == Bit.Core.Enums.CipherType.Login)
.Where(c => c.Type == Bit.Core.Enums.CipherType.Login && !c.IsDeleted)
.Select(s => new CipherViewModel(s))
.ToList() ?? new List<CipherViewModel>();
FilterResults(searchFilter, new CancellationToken());

View file

@ -120,7 +120,7 @@ namespace Bit.iOS
}
}
}
else if (message.Command == "addedCipher" || message.Command == "editedCipher")
else if (message.Command == "addedCipher" || message.Command == "editedCipher" || message.Command == "restoredCipher")
{
if (_deviceActionService.SystemMajorVersion() >= 12)
{
@ -142,7 +142,7 @@ namespace Bit.iOS
await ASHelpers.ReplaceAllIdentities();
}
}
else if (message.Command == "deletedCipher")
else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher")
{
if (_deviceActionService.SystemMajorVersion() >= 12)
{
@ -168,6 +168,11 @@ namespace Bit.iOS
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
}
}
else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher")
&& _deviceActionService.SystemMajorVersion() >= 12)
{
await ASHelpers.ReplaceAllIdentities();
}
});
return base.FinishedLaunching(app, options);