blob: cb15a98457165f4b8acfd337aa0ee459502fdf71 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/settings/save_passwords_collection_view_controller.h"
#import <UIKit/UIKit.h>
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "components/autofill/core/common/password_form.h"
#include "components/google/core/browser/google_util.h"
#include "components/keyed_service/core/service_access_type.h"
#include "components/password_manager/core/browser/password_list_sorter.h"
#include "components/password_manager/core/browser/password_manager_constants.h"
#include "components/password_manager/core/browser/password_manager_metrics_util.h"
#include "components/password_manager/core/browser/password_store.h"
#include "components/password_manager/core/browser/password_store_consumer.h"
#include "components/password_manager/core/browser/password_ui_utils.h"
#include "components/password_manager/core/common/password_manager_features.h"
#include "components/password_manager/core/common/password_manager_pref_names.h"
#include "components/prefs/pref_member.h"
#include "components/prefs/pref_service.h"
#include "components/strings/grit/components_strings.h"
#include "components/url_formatter/url_formatter.h"
#include "ios/chrome/browser/application_context.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#include "ios/chrome/browser/experimental_flags.h"
#include "ios/chrome/browser/passwords/ios_chrome_password_store_factory.h"
#import "ios/chrome/browser/ui/collection_view/cells/MDCCollectionViewCell+Chrome.h"
#import "ios/chrome/browser/ui/collection_view/cells/collection_view_footer_item.h"
#import "ios/chrome/browser/ui/collection_view/cells/collection_view_item.h"
#import "ios/chrome/browser/ui/collection_view/collection_view_model.h"
#import "ios/chrome/browser/ui/settings/cells/settings_switch_item.h"
#import "ios/chrome/browser/ui/settings/cells/settings_text_item.h"
#import "ios/chrome/browser/ui/settings/password_details_collection_view_controller.h"
#import "ios/chrome/browser/ui/settings/password_details_collection_view_controller_delegate.h"
#import "ios/chrome/browser/ui/settings/password_exporter.h"
#import "ios/chrome/browser/ui/settings/reauthentication_module.h"
#import "ios/chrome/browser/ui/settings/settings_utils.h"
#import "ios/chrome/browser/ui/settings/utils/pref_backed_boolean.h"
#include "ios/chrome/browser/ui/ui_util.h"
#import "ios/chrome/browser/ui/uikit_ui_util.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/third_party/material_components_ios/src/components/Palettes/src/MaterialPalettes.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
typedef NS_ENUM(NSInteger, SectionIdentifier) {
SectionIdentifierMessage = kSectionIdentifierEnumZero,
SectionIdentifierSavePasswordsSwitch,
SectionIdentifierSavedPasswords,
SectionIdentifierBlacklist,
SectionIdentifierExportPasswordsButton,
};
typedef NS_ENUM(NSInteger, ItemType) {
ItemTypeManageAccount = kItemTypeEnumZero,
ItemTypeHeader,
ItemTypeSavePasswordsSwitch,
ItemTypeSavedPassword, // This is a repeated item type.
ItemTypeBlacklisted, // This is a repeated item type.
ItemTypeExportPasswordsButton,
};
std::vector<std::unique_ptr<autofill::PasswordForm>> CopyOf(
const std::vector<std::unique_ptr<autofill::PasswordForm>>& password_list) {
std::vector<std::unique_ptr<autofill::PasswordForm>> password_list_copy;
for (const auto& form : password_list) {
password_list_copy.push_back(
std::make_unique<autofill::PasswordForm>(*form));
}
return password_list_copy;
}
} // namespace
namespace password_manager {
// A bridge C++ class passing notification about finished password store
// requests to owning Obj-C class SavePasswordsCollectionViewController.
class SavePasswordsConsumer : public PasswordStoreConsumer {
public:
explicit SavePasswordsConsumer(
SavePasswordsCollectionViewController* delegate);
~SavePasswordsConsumer() override;
void OnGetPasswordStoreResults(
std::vector<std::unique_ptr<autofill::PasswordForm>> results) override;
private:
__weak SavePasswordsCollectionViewController* delegate_ = nil;
DISALLOW_COPY_AND_ASSIGN(SavePasswordsConsumer);
};
SavePasswordsConsumer::SavePasswordsConsumer(
SavePasswordsCollectionViewController* delegate)
: delegate_(delegate) {}
SavePasswordsConsumer::~SavePasswordsConsumer() {}
void SavePasswordsConsumer::OnGetPasswordStoreResults(
std::vector<std::unique_ptr<autofill::PasswordForm>> results) {
if (!results.empty())
[delegate_ onGetPasswordStoreResults:results];
}
} // namespace password_manager
// Use the type of the items to convey the Saved/Blacklisted status.
@interface SavedFormContentItem : SettingsTextItem
@end
@implementation SavedFormContentItem
@end
@interface BlacklistedFormContentItem : SettingsTextItem
@end
@implementation BlacklistedFormContentItem
@end
@protocol PasswordExportActivityViewControllerDelegate<NSObject>
// Used to reset the export state when the activity view disappears.
- (void)resetExport;
@end
@interface PasswordExportActivityViewController : UIActivityViewController
- (PasswordExportActivityViewController*)
initWithActivityItems:(NSArray*)activityItems
delegate:
(id<PasswordExportActivityViewControllerDelegate>)delegate;
@end
@implementation PasswordExportActivityViewController {
__weak id<PasswordExportActivityViewControllerDelegate> _weakDelegate;
}
- (PasswordExportActivityViewController*)
initWithActivityItems:(NSArray*)activityItems
delegate:
(id<PasswordExportActivityViewControllerDelegate>)delegate {
self = [super initWithActivityItems:activityItems applicationActivities:nil];
if (self) {
_weakDelegate = delegate;
}
return self;
}
- (void)viewDidDisappear:(BOOL)animated {
[_weakDelegate resetExport];
[super viewDidDisappear:animated];
}
@end
@interface SavePasswordsCollectionViewController ()<
BooleanObserver,
PasswordDetailsCollectionViewControllerDelegate,
SuccessfulReauthTimeAccessor,
PasswordExporterDelegate,
PasswordExportActivityViewControllerDelegate> {
// The observable boolean that binds to the password manager setting state.
// Saved passwords are only on if the password manager is enabled.
PrefBackedBoolean* passwordManagerEnabled_;
// The item related to the switch for the password manager setting.
SettingsSwitchItem* savePasswordsItem_;
// The item related to the button for exporting passwords.
SettingsTextItem* exportPasswordsItem_;
// The interface for getting and manipulating a user's saved passwords.
scoped_refptr<password_manager::PasswordStore> passwordStore_;
// A helper object for passing data about saved passwords from a finished
// password store request to the SavePasswordsCollectionViewController.
std::unique_ptr<password_manager::SavePasswordsConsumer>
savedPasswordsConsumer_;
// A helper object for passing data about blacklisted sites from a finished
// password store request to the SavePasswordsCollectionViewController.
std::unique_ptr<password_manager::SavePasswordsConsumer>
blacklistPasswordsConsumer_;
// The list of the user's saved passwords.
std::vector<std::unique_ptr<autofill::PasswordForm>> savedForms_;
// The list of the user's blacklisted sites.
std::vector<std::unique_ptr<autofill::PasswordForm>> blacklistedForms_;
// Map containing duplicates of saved passwords.
password_manager::DuplicatesMap savedPasswordDuplicates_;
// Map containing duplicates of blacklisted passwords.
password_manager::DuplicatesMap blacklistedPasswordDuplicates_;
// The current Chrome browser state.
ios::ChromeBrowserState* browserState_;
// Object storing the time of the previous successful re-authentication.
// This is meant to be used by the |ReauthenticationModule| for keeping
// re-authentications valid for a certain time interval within the scope
// of the Save Passwords Settings.
NSDate* successfulReauthTime_;
// Module containing the reauthentication mechanism for viewing and copying
// passwords.
ReauthenticationModule* reauthenticationModule_;
// Boolean containing whether the export operation is ready. This implies that
// the exporter is idle and there is at least one saved passwords to export.
BOOL exportReady_;
// Alert informing the user that passwords are being prepared for
// export.
UIAlertController* preparingPasswordsAlert_;
}
// Kick off async request to get logins from password store.
- (void)getLoginsFromPasswordStore;
// Object handling passwords export operations.
@property(nonatomic, strong) PasswordExporter* passwordExporter;
@end
@implementation SavePasswordsCollectionViewController
// Private synthesized properties
@synthesize passwordExporter = passwordExporter_;
#pragma mark - Initialization
- (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState {
DCHECK(browserState);
UICollectionViewLayout* layout = [[MDCCollectionViewFlowLayout alloc] init];
self =
[super initWithLayout:layout style:CollectionViewControllerStyleAppBar];
if (self) {
browserState_ = browserState;
reauthenticationModule_ = [[ReauthenticationModule alloc]
initWithSuccessfulReauthTimeAccessor:self];
if (base::FeatureList::IsEnabled(
password_manager::features::kPasswordExport)) {
passwordExporter_ = [[PasswordExporter alloc]
initWithReauthenticationModule:reauthenticationModule_
delegate:self];
}
self.title = l10n_util::GetNSString(IDS_IOS_PASSWORDS);
self.collectionViewAccessibilityIdentifier =
@"SavePasswordsCollectionViewController";
self.shouldHideDoneButton = YES;
passwordStore_ = IOSChromePasswordStoreFactory::GetForBrowserState(
browserState_, ServiceAccessType::EXPLICIT_ACCESS);
DCHECK(passwordStore_);
passwordManagerEnabled_ = [[PrefBackedBoolean alloc]
initWithPrefService:browserState_->GetPrefs()
prefName:password_manager::prefs::kCredentialsEnableService];
[passwordManagerEnabled_ setObserver:self];
[self getLoginsFromPasswordStore];
[self updateEditButton];
// TODO(crbug.com/764578): -loadModel should not be called from
// initializer. Consider moving the other calls on instance methods as well.
[self loadModel];
}
return self;
}
- (void)dealloc {
[passwordManagerEnabled_ setObserver:nil];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.collectionView.prefetchingEnabled = NO;
}
#pragma mark - SettingsRootCollectionViewController
- (void)loadModel {
[super loadModel];
CollectionViewModel* model = self.collectionViewModel;
// Manage account message.
CollectionViewItem* manageAccountLinkItem = [self manageAccountLinkItem];
if (manageAccountLinkItem) {
[model addSectionWithIdentifier:SectionIdentifierMessage];
[model addItem:manageAccountLinkItem
toSectionWithIdentifier:SectionIdentifierMessage];
}
// Save passwords switch.
[model addSectionWithIdentifier:SectionIdentifierSavePasswordsSwitch];
savePasswordsItem_ = [self savePasswordsItem];
[model addItem:savePasswordsItem_
toSectionWithIdentifier:SectionIdentifierSavePasswordsSwitch];
// Saved passwords.
if (!savedForms_.empty()) {
[model addSectionWithIdentifier:SectionIdentifierSavedPasswords];
SettingsTextItem* headerItem =
[[SettingsTextItem alloc] initWithType:ItemTypeHeader];
headerItem.text =
l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORDS_SAVED_HEADING);
headerItem.textColor = [[MDCPalette greyPalette] tint500];
[model setHeader:headerItem
forSectionWithIdentifier:SectionIdentifierSavedPasswords];
for (const auto& form : savedForms_) {
[model addItem:[self savedFormItemWithForm:form.get()]
toSectionWithIdentifier:SectionIdentifierSavedPasswords];
}
}
if (!blacklistedForms_.empty()) {
[model addSectionWithIdentifier:SectionIdentifierBlacklist];
SettingsTextItem* headerItem =
[[SettingsTextItem alloc] initWithType:ItemTypeHeader];
headerItem.text =
l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORDS_EXCEPTIONS_HEADING);
headerItem.textColor = [[MDCPalette greyPalette] tint500];
[model setHeader:headerItem
forSectionWithIdentifier:SectionIdentifierBlacklist];
for (const auto& form : blacklistedForms_) {
[model addItem:[self blacklistedFormItemWithForm:form.get()]
toSectionWithIdentifier:SectionIdentifierBlacklist];
}
}
if (base::FeatureList::IsEnabled(
password_manager::features::kPasswordExport)) {
// Export passwords button.
[model addSectionWithIdentifier:SectionIdentifierExportPasswordsButton];
exportPasswordsItem_ = [self exportPasswordsItem];
[model addItem:exportPasswordsItem_
toSectionWithIdentifier:SectionIdentifierExportPasswordsButton];
[self updateExportPasswordsButton];
}
}
- (BOOL)shouldShowEditButton {
return YES;
}
- (BOOL)editButtonEnabled {
DCHECK([self shouldShowEditButton]);
return !savedForms_.empty() || !blacklistedForms_.empty();
}
#pragma mark - Items
- (CollectionViewItem*)manageAccountLinkItem {
CollectionViewFooterItem* footerItem =
[[CollectionViewFooterItem alloc] initWithType:ItemTypeManageAccount];
footerItem.text =
l10n_util::GetNSString(IDS_IOS_SAVE_PASSWORDS_MANAGE_ACCOUNT);
footerItem.linkURL = google_util::AppendGoogleLocaleParam(
GURL(password_manager::kPasswordManagerAccountDashboardURL),
GetApplicationContext()->GetApplicationLocale());
footerItem.linkDelegate = self;
return footerItem;
}
- (SettingsSwitchItem*)savePasswordsItem {
SettingsSwitchItem* savePasswordsItem =
[[SettingsSwitchItem alloc] initWithType:ItemTypeSavePasswordsSwitch];
savePasswordsItem.text = l10n_util::GetNSString(IDS_IOS_SAVE_PASSWORDS);
savePasswordsItem.on = [passwordManagerEnabled_ value];
savePasswordsItem.accessibilityIdentifier = @"savePasswordsItem_switch";
return savePasswordsItem;
}
- (SettingsTextItem*)exportPasswordsItem {
SettingsTextItem* exportPasswordsItem =
[[SettingsTextItem alloc] initWithType:ItemTypeExportPasswordsButton];
exportPasswordsItem.text = l10n_util::GetNSString(IDS_IOS_EXPORT_PASSWORDS);
exportPasswordsItem.accessibilityIdentifier = @"exportPasswordsItem_button";
exportPasswordsItem.accessibilityTraits = UIAccessibilityTraitButton;
return exportPasswordsItem;
}
- (SavedFormContentItem*)savedFormItemWithForm:(autofill::PasswordForm*)form {
SavedFormContentItem* passwordItem =
[[SavedFormContentItem alloc] initWithType:ItemTypeSavedPassword];
passwordItem.text = base::SysUTF8ToNSString(
password_manager::GetShownOriginAndLinkUrl(*form).first);
passwordItem.detailText = base::SysUTF16ToNSString(form->username_value);
passwordItem.accessibilityTraits |= UIAccessibilityTraitButton;
passwordItem.accessoryType =
MDCCollectionViewCellAccessoryDisclosureIndicator;
return passwordItem;
}
- (BlacklistedFormContentItem*)blacklistedFormItemWithForm:
(autofill::PasswordForm*)form {
BlacklistedFormContentItem* passwordItem =
[[BlacklistedFormContentItem alloc] initWithType:ItemTypeBlacklisted];
passwordItem.text = base::SysUTF8ToNSString(
password_manager::GetShownOriginAndLinkUrl(*form).first);
passwordItem.accessibilityTraits |= UIAccessibilityTraitButton;
passwordItem.accessoryType =
MDCCollectionViewCellAccessoryDisclosureIndicator;
return passwordItem;
}
#pragma mark - MDCCollectionViewEditingDelegate
- (BOOL)collectionViewAllowsEditing:(UICollectionView*)collectionView {
return YES;
}
#pragma mark - MDCCollectionViewStylingDelegate
- (MDCCollectionViewCellStyle)collectionView:(UICollectionView*)collectionView
cellStyleForSection:(NSInteger)section {
NSInteger sectionIdentifier =
[self.collectionViewModel sectionIdentifierForSection:section];
switch (sectionIdentifier) {
case SectionIdentifierMessage:
// Display the message in the default style with no "card" UI and no
// section padding.
return MDCCollectionViewCellStyleDefault;
default:
return self.styler.cellStyle;
}
}
- (BOOL)collectionView:(UICollectionView*)collectionView
shouldHideItemBackgroundAtIndexPath:(NSIndexPath*)indexPath {
NSInteger sectionIdentifier =
[self.collectionViewModel sectionIdentifierForSection:indexPath.section];
switch (sectionIdentifier) {
case SectionIdentifierMessage:
// Display the message without any background image or shadowing.
return YES;
default:
return NO;
}
}
- (CGFloat)collectionView:(UICollectionView*)collectionView
cellHeightAtIndexPath:(NSIndexPath*)indexPath {
CollectionViewItem* item =
[self.collectionViewModel itemAtIndexPath:indexPath];
switch (item.type) {
case ItemTypeManageAccount:
return [MDCCollectionViewCell
cr_preferredHeightForWidth:CGRectGetWidth(collectionView.bounds)
forItem:item];
case ItemTypeSavedPassword:
return MDCCellDefaultTwoLineHeight;
default:
return MDCCellDefaultOneLineHeight;
}
}
- (BOOL)collectionView:(UICollectionView*)collectionView
hidesInkViewAtIndexPath:(NSIndexPath*)indexPath {
NSInteger type = [self.collectionViewModel itemTypeForIndexPath:indexPath];
switch (type) {
case ItemTypeSavePasswordsSwitch:
return YES;
default:
return NO;
}
}
#pragma mark - UICollectionViewDataSource
- (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView
cellForItemAtIndexPath:(NSIndexPath*)indexPath {
UICollectionViewCell* cell =
[super collectionView:collectionView cellForItemAtIndexPath:indexPath];
if ([self.collectionViewModel itemTypeForIndexPath:indexPath] ==
ItemTypeSavePasswordsSwitch) {
SettingsSwitchCell* switchCell =
base::mac::ObjCCastStrict<SettingsSwitchCell>(cell);
[switchCell.switchView addTarget:self
action:@selector(savePasswordsSwitchChanged:)
forControlEvents:UIControlEventValueChanged];
}
return cell;
}
#pragma mark - BooleanObserver
- (void)booleanDidChange:(id<ObservableBoolean>)observableBoolean {
DCHECK_EQ(observableBoolean, passwordManagerEnabled_);
// Update the item.
savePasswordsItem_.on = [passwordManagerEnabled_ value];
// Update the cell.
[self reconfigureCellsForItems:@[ savePasswordsItem_ ]];
}
#pragma mark - Actions
- (void)savePasswordsSwitchChanged:(UISwitch*)switchView {
// Update the setting.
[passwordManagerEnabled_ setValue:switchView.on];
// Update the item.
savePasswordsItem_.on = [passwordManagerEnabled_ value];
}
#pragma mark - Private methods
- (void)getLoginsFromPasswordStore {
savedPasswordsConsumer_.reset(
new password_manager::SavePasswordsConsumer(self));
passwordStore_->GetAutofillableLogins(savedPasswordsConsumer_.get());
blacklistPasswordsConsumer_.reset(
new password_manager::SavePasswordsConsumer(self));
passwordStore_->GetBlacklistLogins(blacklistPasswordsConsumer_.get());
}
- (void)onGetPasswordStoreResults:
(const std::vector<std::unique_ptr<autofill::PasswordForm>>&)result {
for (auto it = result.begin(); it != result.end(); ++it) {
// PasswordForm is needed when user wants to delete the site/password.
auto form = std::make_unique<autofill::PasswordForm>(**it);
if (form->blacklisted_by_user)
blacklistedForms_.push_back(std::move(form));
else
savedForms_.push_back(std::move(form));
}
password_manager::SortEntriesAndHideDuplicates(
&savedForms_, &savedPasswordDuplicates_,
password_manager::PasswordEntryType::SAVED);
password_manager::SortEntriesAndHideDuplicates(
&blacklistedForms_, &blacklistedPasswordDuplicates_,
password_manager::PasswordEntryType::BLACKLISTED);
[self updateEditButton];
[self reloadData];
}
- (void)updateExportPasswordsButton {
if (!exportPasswordsItem_)
return;
if (!savedForms_.empty() &&
self.passwordExporter.exportState == ExportState::IDLE) {
exportReady_ = YES;
if (![self.editor isEditing]) {
[self setExportPasswordsButtonEnabled:YES];
}
} else {
exportReady_ = NO;
[self setExportPasswordsButtonEnabled:NO];
}
}
- (void)setExportPasswordsButtonEnabled:(BOOL)enabled {
if (enabled) {
DCHECK(exportReady_ && ![self.editor isEditing]);
exportPasswordsItem_.textColor = [[MDCPalette greyPalette] tint900];
exportPasswordsItem_.accessibilityTraits &= ~UIAccessibilityTraitNotEnabled;
} else {
exportPasswordsItem_.textColor = [[MDCPalette greyPalette] tint500];
exportPasswordsItem_.accessibilityTraits |= UIAccessibilityTraitNotEnabled;
}
[self reconfigureCellsForItems:@[ exportPasswordsItem_ ]];
}
- (void)startPasswordsExportFlow {
UIAlertController* exportConfirmation = [UIAlertController
alertControllerWithTitle:nil
message:l10n_util::GetNSString(
IDS_IOS_EXPORT_PASSWORDS_ALERT_MESSAGE)
preferredStyle:UIAlertControllerStyleActionSheet];
UIAlertAction* cancelAction =
[UIAlertAction actionWithTitle:l10n_util::GetNSString(
IDS_IOS_EXPORT_PASSWORDS_CANCEL_BUTTON)
style:UIAlertActionStyleCancel
handler:^(UIAlertAction* action) {
UMA_HISTOGRAM_ENUMERATION(
"PasswordManager.ExportPasswordsToCSVResult",
password_manager::metrics_util::
ExportPasswordsResult::USER_ABORTED,
password_manager::metrics_util::
ExportPasswordsResult::COUNT);
}];
[exportConfirmation addAction:cancelAction];
__weak SavePasswordsCollectionViewController* weakSelf = self;
UIAlertAction* exportAction = [UIAlertAction
actionWithTitle:l10n_util::GetNSString(IDS_IOS_EXPORT_PASSWORDS)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction* action) {
SavePasswordsCollectionViewController* strongSelf = weakSelf;
if (!strongSelf) {
return;
}
[strongSelf.passwordExporter
startExportFlow:CopyOf(strongSelf->savedForms_)];
}];
[exportConfirmation addAction:exportAction];
[self presentViewController:exportConfirmation animated:YES completion:nil];
}
#pragma mark UICollectionViewDelegate
- (void)openDetailedViewForForm:(const autofill::PasswordForm&)form {
PasswordDetailsCollectionViewController* controller =
[[PasswordDetailsCollectionViewController alloc]
initWithPasswordForm:form
delegate:self
reauthenticationModule:reauthenticationModule_];
controller.dispatcher = self.dispatcher;
[self.navigationController pushViewController:controller animated:YES];
}
- (void)collectionView:(UICollectionView*)collectionView
didSelectItemAtIndexPath:(NSIndexPath*)indexPath {
[super collectionView:collectionView didSelectItemAtIndexPath:indexPath];
// Actions should only take effect when not in editing mode.
if ([self.editor isEditing]) {
return;
}
CollectionViewModel* model = self.collectionViewModel;
NSInteger itemType = [model itemTypeForIndexPath:indexPath];
switch (itemType) {
case ItemTypeManageAccount:
case ItemTypeHeader:
case ItemTypeSavePasswordsSwitch:
break;
case ItemTypeSavedPassword:
DCHECK_EQ(SectionIdentifierSavedPasswords,
[model sectionIdentifierForSection:indexPath.section]);
DCHECK_LT(base::checked_cast<size_t>(indexPath.item), savedForms_.size());
[self openDetailedViewForForm:*savedForms_[indexPath.item]];
break;
case ItemTypeBlacklisted:
DCHECK_EQ(SectionIdentifierBlacklist,
[model sectionIdentifierForSection:indexPath.section]);
DCHECK_LT(base::checked_cast<size_t>(indexPath.item),
blacklistedForms_.size());
[self openDetailedViewForForm:*blacklistedForms_[indexPath.item]];
break;
case ItemTypeExportPasswordsButton:
DCHECK_EQ(SectionIdentifierExportPasswordsButton,
[model sectionIdentifierForSection:indexPath.section]);
DCHECK(base::FeatureList::IsEnabled(
password_manager::features::kPasswordExport));
if (exportReady_) {
[self startPasswordsExportFlow];
}
break;
default:
NOTREACHED();
}
}
#pragma mark MDCCollectionViewEditingDelegate
- (BOOL)collectionView:(UICollectionView*)collectionView
canEditItemAtIndexPath:(NSIndexPath*)indexPath {
// Only password cells are editable.
CollectionViewItem* item =
[self.collectionViewModel itemAtIndexPath:indexPath];
return [item isKindOfClass:[SavedFormContentItem class]] ||
[item isKindOfClass:[BlacklistedFormContentItem class]];
}
- (void)collectionViewWillBeginEditing:(UICollectionView*)collectionView {
[super collectionViewWillBeginEditing:collectionView];
[self setSavePasswordsSwitchItemEnabled:NO];
if (base::FeatureList::IsEnabled(
password_manager::features::kPasswordExport)) {
[self setExportPasswordsButtonEnabled:NO];
}
}
- (void)collectionViewWillEndEditing:(UICollectionView*)collectionView {
[super collectionViewWillEndEditing:collectionView];
[self setSavePasswordsSwitchItemEnabled:YES];
if (base::FeatureList::IsEnabled(
password_manager::features::kPasswordExport)) {
if (exportReady_) {
[self setExportPasswordsButtonEnabled:YES];
}
}
}
- (void)collectionView:(UICollectionView*)collectionView
willDeleteItemsAtIndexPaths:(NSArray*)indexPaths {
// Ensure indexPaths are sorted to maintain delete logic, and keep track of
// number of items deleted to adjust index for accessing elements in the
// forms vectors.
NSArray* sortedIndexPaths =
[indexPaths sortedArrayUsingSelector:@selector(compare:)];
int passwordsDeleted = 0;
int blacklistedDeleted = 0;
for (NSIndexPath* indexPath in sortedIndexPaths) {
// Only form items are editable.
SettingsTextItem* item = base::mac::ObjCCastStrict<SettingsTextItem>(
[self.collectionViewModel itemAtIndexPath:indexPath]);
BOOL blacklisted = [item isKindOfClass:[BlacklistedFormContentItem class]];
unsigned int formIndex = (unsigned int)indexPath.item;
// Adjust index to account for deleted items.
formIndex -= blacklisted ? blacklistedDeleted : passwordsDeleted;
auto& forms = blacklisted ? blacklistedForms_ : savedForms_;
auto& duplicates =
blacklisted ? blacklistedPasswordDuplicates_ : savedPasswordDuplicates_;
password_manager::PasswordEntryType entryType =
blacklisted ? password_manager::PasswordEntryType::BLACKLISTED
: password_manager::PasswordEntryType::SAVED;
DCHECK_LT(formIndex, forms.size());
auto formIterator = forms.begin() + formIndex;
std::unique_ptr<autofill::PasswordForm> form = std::move(*formIterator);
std::string key = password_manager::CreateSortKey(*form, entryType);
auto duplicatesRange = duplicates.equal_range(key);
for (auto iterator = duplicatesRange.first;
iterator != duplicatesRange.second; ++iterator) {
passwordStore_->RemoveLogin(*(iterator->second));
}
duplicates.erase(key);
forms.erase(formIterator);
passwordStore_->RemoveLogin(*form);
if (blacklisted) {
++blacklistedDeleted;
} else {
++passwordsDeleted;
}
}
// Must call super at the end of the child implementation.
[super collectionView:collectionView willDeleteItemsAtIndexPaths:indexPaths];
}
- (void)collectionView:(UICollectionView*)collectionView
didDeleteItemsAtIndexPaths:(NSArray*)indexPaths {
// Remove empty sections.
// TODO(crbug.com/593786): Move this logic in CollectionViewController.
NSMutableOrderedSet* sectionsToRemove = [NSMutableOrderedSet orderedSet];
// Sort and enumerate in reverse order to delete the items from the collection
// view model.
NSArray* sortedIndexPaths =
[indexPaths sortedArrayUsingSelector:@selector(compare:)];
for (NSIndexPath* indexPath in [sortedIndexPaths reverseObjectEnumerator]) {
if ([collectionView numberOfItemsInSection:indexPath.section] == 0) {
[sectionsToRemove addObject:@(indexPath.section)];
}
}
__weak SavePasswordsCollectionViewController* weakSelf = self;
[self.collectionView performBatchUpdates:^{
SavePasswordsCollectionViewController* strongSelf = weakSelf;
if (!strongSelf)
return;
for (NSNumber* sectionNumber in sectionsToRemove) {
NSInteger section = [sectionNumber integerValue];
NSInteger sectionIdentifier = [[strongSelf collectionViewModel]
sectionIdentifierForSection:section];
[[strongSelf collectionViewModel]
removeSectionWithIdentifier:sectionIdentifier];
[[strongSelf collectionView]
deleteSections:[NSIndexSet indexSetWithIndex:section]];
}
}
completion:^(BOOL finished) {
SavePasswordsCollectionViewController* strongSelf = weakSelf;
if (!strongSelf)
return;
if (![strongSelf editButtonEnabled]) {
[strongSelf.editor setEditing:NO];
}
[strongSelf updateEditButton];
if (base::FeatureList::IsEnabled(
password_manager::features::kPasswordExport)) {
[strongSelf updateExportPasswordsButton];
}
}];
}
#pragma mark PasswordDetailsCollectionViewControllerDelegate
- (void)deletePassword:(const autofill::PasswordForm&)form {
passwordStore_->RemoveLogin(form);
std::vector<std::unique_ptr<autofill::PasswordForm>>& forms =
form.blacklisted_by_user ? blacklistedForms_ : savedForms_;
auto iterator = std::find_if(
forms.begin(), forms.end(),
[&form](const std::unique_ptr<autofill::PasswordForm>& value) {
return *value == form;
});
DCHECK(iterator != forms.end());
forms.erase(iterator);
password_manager::DuplicatesMap& duplicates =
form.blacklisted_by_user ? blacklistedPasswordDuplicates_
: savedPasswordDuplicates_;
password_manager::PasswordEntryType entryType =
form.blacklisted_by_user
? password_manager::PasswordEntryType::BLACKLISTED
: password_manager::PasswordEntryType::SAVED;
std::string key = password_manager::CreateSortKey(form, entryType);
auto duplicatesRange = duplicates.equal_range(key);
for (auto iterator = duplicatesRange.first;
iterator != duplicatesRange.second; ++iterator) {
passwordStore_->RemoveLogin(*(iterator->second));
}
duplicates.erase(key);
[self updateEditButton];
[self reloadData];
[self.navigationController popViewControllerAnimated:YES];
}
#pragma mark SuccessfulReauthTimeAccessor
- (void)updateSuccessfulReauthTime {
successfulReauthTime_ = [[NSDate alloc] init];
}
- (NSDate*)lastSuccessfulReauthTime {
return successfulReauthTime_;
}
#pragma mark PasswordExporterDelegate
- (void)showSetPasscodeDialog {
UIAlertController* alertController = [UIAlertController
alertControllerWithTitle:l10n_util::GetNSString(
IDS_IOS_SETTINGS_SET_UP_SCREENLOCK_TITLE)
message:
l10n_util::GetNSString(
IDS_IOS_SETTINGS_EXPORT_PASSWORDS_SET_UP_SCREENLOCK_CONTENT)
preferredStyle:UIAlertControllerStyleAlert];
ProceduralBlockWithURL blockOpenURL = BlockToOpenURL(self, self.dispatcher);
UIAlertAction* learnAction = [UIAlertAction
actionWithTitle:l10n_util::GetNSString(
IDS_IOS_SETTINGS_SET_UP_SCREENLOCK_LEARN_HOW)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction*) {
blockOpenURL(GURL(kPasscodeArticleURL));
}];
[alertController addAction:learnAction];
UIAlertAction* okAction =
[UIAlertAction actionWithTitle:l10n_util::GetNSString(IDS_OK)
style:UIAlertActionStyleDefault
handler:nil];
[alertController addAction:okAction];
alertController.preferredAction = okAction;
[self presentViewController:alertController animated:YES completion:nil];
}
- (void)showPreparingPasswordsAlert {
preparingPasswordsAlert_ = [UIAlertController
alertControllerWithTitle:
l10n_util::GetNSString(IDS_IOS_EXPORT_PASSWORDS_PREPARING_ALERT_TITLE)
message:nil
preferredStyle:UIAlertControllerStyleAlert];
__weak SavePasswordsCollectionViewController* weakSelf = self;
UIAlertAction* cancelAction =
[UIAlertAction actionWithTitle:l10n_util::GetNSString(
IDS_IOS_EXPORT_PASSWORDS_CANCEL_BUTTON)
style:UIAlertActionStyleCancel
handler:^(UIAlertAction*) {
[weakSelf.passwordExporter cancelExport];
}];
[preparingPasswordsAlert_ addAction:cancelAction];
[self presentViewController:preparingPasswordsAlert_
animated:YES
completion:nil];
}
- (void)showExportErrorAlertWithLocalizedReason:(NSString*)localizedReason {
UIAlertController* alertController = [UIAlertController
alertControllerWithTitle:l10n_util::GetNSString(
IDS_IOS_EXPORT_PASSWORDS_FAILED_ALERT_TITLE)
message:localizedReason
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* okAction =
[UIAlertAction actionWithTitle:l10n_util::GetNSString(IDS_OK)
style:UIAlertActionStyleDefault
handler:nil];
[alertController addAction:okAction];
[self presentViewController:alertController];
}
- (void)showActivityViewWithActivityItems:(NSArray*)activityItems
completionHandler:(void (^)(NSString* activityType,
BOOL completed,
NSArray* returnedItems,
NSError* activityError))
completionHandler {
PasswordExportActivityViewController* activityViewController =
[[PasswordExportActivityViewController alloc]
initWithActivityItems:activityItems
delegate:self];
NSArray* excludedActivityTypes = @[
UIActivityTypeAddToReadingList, UIActivityTypeAirDrop,
UIActivityTypeCopyToPasteboard, UIActivityTypeOpenInIBooks,
UIActivityTypePostToFacebook, UIActivityTypePostToFlickr,
UIActivityTypePostToTencentWeibo, UIActivityTypePostToTwitter,
UIActivityTypePostToVimeo, UIActivityTypePostToWeibo, UIActivityTypePrint
];
[activityViewController setExcludedActivityTypes:excludedActivityTypes];
[activityViewController setCompletionWithItemsHandler:completionHandler];
UIView* sourceView = nil;
CGRect sourceRect = CGRectZero;
if (IsIPadIdiom() && !IsCompactWidth()) {
NSIndexPath* indexPath = [self.collectionViewModel
indexPathForItemType:ItemTypeExportPasswordsButton
sectionIdentifier:SectionIdentifierExportPasswordsButton];
UICollectionViewCell* cell =
[self.collectionView cellForItemAtIndexPath:indexPath];
sourceView = self.collectionView;
sourceRect = cell.frame;
}
activityViewController.modalPresentationStyle = UIModalPresentationPopover;
activityViewController.popoverPresentationController.sourceView = sourceView;
activityViewController.popoverPresentationController.sourceRect = sourceRect;
activityViewController.popoverPresentationController
.permittedArrowDirections =
UIPopoverArrowDirectionDown | UIPopoverArrowDirectionDown;
[self presentViewController:activityViewController];
}
#pragma mark - PasswordExportActivityViewControllerDelegate
- (void)resetExport {
[self.passwordExporter resetExportState];
}
#pragma mark Helper methods
- (void)presentViewController:(UIViewController*)viewController {
if (self.presentedViewController == preparingPasswordsAlert_ &&
!preparingPasswordsAlert_.beingDismissed) {
__weak SavePasswordsCollectionViewController* weakSelf = self;
[self dismissViewControllerAnimated:YES
completion:^{
[weakSelf presentViewController:viewController
animated:YES
completion:nil];
}];
} else {
[self presentViewController:viewController animated:YES completion:nil];
}
}
// Sets the save passwords switch item's enabled status to |enabled| and
// reconfigures the corresponding cell.
- (void)setSavePasswordsSwitchItemEnabled:(BOOL)enabled {
CollectionViewModel* model = self.collectionViewModel;
if (![model hasItemForItemType:ItemTypeSavePasswordsSwitch
sectionIdentifier:SectionIdentifierSavePasswordsSwitch]) {
return;
}
NSIndexPath* switchPath =
[model indexPathForItemType:ItemTypeSavePasswordsSwitch
sectionIdentifier:SectionIdentifierSavePasswordsSwitch];
SettingsSwitchItem* switchItem =
base::mac::ObjCCastStrict<SettingsSwitchItem>(
[model itemAtIndexPath:switchPath]);
[switchItem setEnabled:enabled];
[self reconfigureCellsForItems:@[ switchItem ]];
}
#pragma mark - Testing
- (void)setReauthenticationModuleForExporter:
(id<ReauthenticationProtocol>)reauthenticationModule {
passwordExporter_ = [[PasswordExporter alloc]
initWithReauthenticationModule:reauthenticationModule
delegate:self];
}
- (PasswordExporter*)getPasswordExporter {
return passwordExporter_;
}
@end