| // 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/passwords_table_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/common/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_ui_utils.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/passwords/save_passwords_consumer.h" |
| #import "ios/chrome/browser/ui/settings/cells/settings_cells_constants.h" |
| #import "ios/chrome/browser/ui/settings/cells/settings_search_item.h" |
| #import "ios/chrome/browser/ui/settings/cells/settings_switch_item.h" |
| #import "ios/chrome/browser/ui/settings/password_details_table_view_controller.h" |
| #import "ios/chrome/browser/ui/settings/password_details_table_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" |
| #import "ios/chrome/browser/ui/table_view/cells/table_view_cells_constants.h" |
| #import "ios/chrome/browser/ui/table_view/cells/table_view_detail_text_item.h" |
| #import "ios/chrome/browser/ui/table_view/cells/table_view_link_header_footer_item.h" |
| #import "ios/chrome/browser/ui/table_view/cells/table_view_text_header_footer_item.h" |
| #import "ios/chrome/browser/ui/table_view/cells/table_view_text_item.h" |
| #import "ios/chrome/browser/ui/table_view/table_view_navigation_controller_constants.h" |
| #include "ios/chrome/browser/ui/util/ui_util.h" |
| #import "ios/chrome/browser/ui/util/uikit_ui_util.h" |
| #import "ios/chrome/common/ui_util/constraints_ui_util.h" |
| #include "ios/chrome/grit/ios_strings.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 |
| |
| NSString* const kPasswordsTableViewId = @"PasswordsTableViewId"; |
| NSString* const kPasswordsExportConfirmViewId = @"PasswordsExportConfirmViewId"; |
| |
| namespace { |
| |
| typedef NS_ENUM(NSInteger, SectionIdentifier) { |
| SectionIdentifierSearchPasswordsBox = kSectionIdentifierEnumZero, |
| SectionIdentifierSavePasswordsSwitch, |
| SectionIdentifierSavedPasswords, |
| SectionIdentifierBlacklist, |
| SectionIdentifierExportPasswordsButton, |
| }; |
| |
| typedef NS_ENUM(NSInteger, ItemType) { |
| ItemTypeSearchBox = kItemTypeEnumZero, |
| ItemTypeLinkHeader, |
| 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 |
| |
| @interface PasswordFormContentItem : TableViewDetailTextItem |
| @property(nonatomic) autofill::PasswordForm* form; |
| @end |
| @implementation PasswordFormContentItem |
| @end |
| |
| // Use the type of the items to convey the Saved/Blacklisted status. |
| @interface SavedFormContentItem : PasswordFormContentItem |
| @end |
| @implementation SavedFormContentItem |
| @end |
| @interface BlacklistedFormContentItem : PasswordFormContentItem |
| @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 PasswordsTableViewController () < |
| BooleanObserver, |
| PasswordDetailsTableViewControllerDelegate, |
| PasswordExporterDelegate, |
| PasswordExportActivityViewControllerDelegate, |
| SavePasswordsConsumerDelegate, |
| UISearchBarDelegate, |
| SuccessfulReauthTimeAccessor> { |
| // 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. |
| TableViewTextItem* 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 PasswordsTableViewController. |
| std::unique_ptr<ios::SavePasswordsConsumer> savedPasswordsConsumer_; |
| // A helper object for passing data about blacklisted sites from a finished |
| // password store request to the PasswordsTableViewController. |
| std::unique_ptr<ios::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; |
| |
| // Current passwords search term. |
| @property(nonatomic, copy) NSString* searchTerm; |
| |
| @end |
| |
| @implementation PasswordsTableViewController |
| |
| #pragma mark - Initialization |
| |
| - (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState { |
| DCHECK(browserState); |
| self = |
| [super initWithTableViewStyle:UITableViewStyleGrouped |
| appBarStyle:ChromeTableViewControllerStyleWithAppBar]; |
| if (self) { |
| browserState_ = browserState; |
| reauthenticationModule_ = [[ReauthenticationModule alloc] |
| initWithSuccessfulReauthTimeAccessor:self]; |
| _passwordExporter = [[PasswordExporter alloc] |
| initWithReauthenticationModule:reauthenticationModule_ |
| delegate:self]; |
| self.title = l10n_util::GetNSString(IDS_IOS_PASSWORDS); |
| self.shouldHideDoneButton = YES; |
| self.searchTerm = @""; |
| 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]; |
| [self updateExportPasswordsButton]; |
| } |
| return self; |
| } |
| |
| #pragma mark - UIViewController |
| |
| - (void)viewDidLoad { |
| [super viewDidLoad]; |
| self.tableView.allowsMultipleSelectionDuringEditing = YES; |
| self.tableView.accessibilityIdentifier = kPasswordsTableViewId; |
| |
| [self loadModel]; |
| } |
| |
| - (void)setEditing:(BOOL)editing animated:(BOOL)animated { |
| [super setEditing:editing animated:animated]; |
| if (editing) { |
| [self setSavePasswordsSwitchItemEnabled:NO]; |
| [self setExportPasswordsButtonEnabled:NO]; |
| [self setSearchPasswordsItemEnabled:NO]; |
| } else { |
| [self setSavePasswordsSwitchItemEnabled:YES]; |
| if (exportReady_) { |
| [self setExportPasswordsButtonEnabled:YES]; |
| } |
| [self setSearchPasswordsItemEnabled:YES]; |
| } |
| } |
| |
| #pragma mark - SettingsRootTableViewController |
| |
| - (void)loadModel { |
| [super loadModel]; |
| TableViewModel* model = self.tableViewModel; |
| |
| // Search bar. |
| if (!savedForms_.empty() || !blacklistedForms_.empty()) { |
| SettingsSearchItem* searchItem = [self searchItem]; |
| [model addSectionWithIdentifier:SectionIdentifierSearchPasswordsBox]; |
| [model setHeader:searchItem |
| forSectionWithIdentifier:SectionIdentifierSearchPasswordsBox]; |
| } |
| |
| // Save passwords switch and manage account message. |
| [model addSectionWithIdentifier:SectionIdentifierSavePasswordsSwitch]; |
| savePasswordsItem_ = [self savePasswordsItem]; |
| [model addItem:savePasswordsItem_ |
| toSectionWithIdentifier:SectionIdentifierSavePasswordsSwitch]; |
| TableViewLinkHeaderFooterItem* manageAccountLinkItem = |
| [self manageAccountLinkItem]; |
| [model setHeader:manageAccountLinkItem |
| forSectionWithIdentifier:SectionIdentifierSavePasswordsSwitch]; |
| |
| // Saved passwords. |
| if (!savedForms_.empty()) { |
| [model addSectionWithIdentifier:SectionIdentifierSavedPasswords]; |
| TableViewTextHeaderFooterItem* headerItem = |
| [[TableViewTextHeaderFooterItem alloc] initWithType:ItemTypeHeader]; |
| headerItem.text = |
| l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORDS_SAVED_HEADING); |
| [model setHeader:headerItem |
| forSectionWithIdentifier:SectionIdentifierSavedPasswords]; |
| } |
| |
| // Blacklisted passwords. |
| if (!blacklistedForms_.empty()) { |
| [model addSectionWithIdentifier:SectionIdentifierBlacklist]; |
| TableViewTextHeaderFooterItem* headerItem = |
| [[TableViewTextHeaderFooterItem alloc] initWithType:ItemTypeHeader]; |
| headerItem.text = |
| l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORDS_EXCEPTIONS_HEADING); |
| [model setHeader:headerItem |
| forSectionWithIdentifier:SectionIdentifierBlacklist]; |
| } |
| |
| // Export passwords button. |
| [model addSectionWithIdentifier:SectionIdentifierExportPasswordsButton]; |
| exportPasswordsItem_ = [self exportPasswordsItem]; |
| [model addItem:exportPasswordsItem_ |
| toSectionWithIdentifier:SectionIdentifierExportPasswordsButton]; |
| |
| [self filterItems:self.searchTerm]; |
| } |
| |
| - (void)deleteItems:(NSArray<NSIndexPath*>*)indexPaths { |
| // Do not call super as this also deletes the section if it is empty. |
| [self deleteItemAtIndexPaths:indexPaths]; |
| } |
| |
| - (void)reloadData { |
| [super reloadData]; |
| [self updateExportPasswordsButton]; |
| } |
| |
| - (BOOL)shouldShowEditButton { |
| return YES; |
| } |
| |
| - (BOOL)editButtonEnabled { |
| DCHECK([self shouldShowEditButton]); |
| return !savedForms_.empty() || !blacklistedForms_.empty(); |
| } |
| |
| #pragma mark - Items |
| |
| - (SettingsSearchItem*)searchItem { |
| SettingsSearchItem* item = |
| [[SettingsSearchItem alloc] initWithType:ItemTypeSearchBox]; |
| item.placeholder = |
| l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORD_PLACEHOLDER_SEARCH); |
| return item; |
| } |
| |
| - (TableViewLinkHeaderFooterItem*)manageAccountLinkItem { |
| TableViewLinkHeaderFooterItem* footerItem = |
| [[TableViewLinkHeaderFooterItem alloc] initWithType:ItemTypeLinkHeader]; |
| footerItem.text = |
| l10n_util::GetNSString(IDS_IOS_SAVE_PASSWORDS_MANAGE_ACCOUNT); |
| footerItem.linkURL = google_util::AppendGoogleLocaleParam( |
| GURL(password_manager::kPasswordManagerAccountDashboardURL), |
| GetApplicationContext()->GetApplicationLocale()); |
| 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; |
| } |
| |
| - (TableViewTextItem*)exportPasswordsItem { |
| TableViewTextItem* exportPasswordsItem = |
| [[TableViewTextItem alloc] initWithType:ItemTypeExportPasswordsButton]; |
| exportPasswordsItem.text = l10n_util::GetNSString(IDS_IOS_EXPORT_PASSWORDS); |
| exportPasswordsItem.textColor = UIColorFromRGB(kTableViewTextLabelColorBlue); |
| exportPasswordsItem.accessibilityIdentifier = @"exportPasswordsItem_button"; |
| exportPasswordsItem.accessibilityTraits = UIAccessibilityTraitButton; |
| return exportPasswordsItem; |
| } |
| |
| - (SavedFormContentItem*)savedFormItemWithText:(NSString*)text |
| andDetailText:(NSString*)detailText |
| forForm:(autofill::PasswordForm*)form { |
| SavedFormContentItem* passwordItem = |
| [[SavedFormContentItem alloc] initWithType:ItemTypeSavedPassword]; |
| passwordItem.text = text; |
| passwordItem.form = form; |
| passwordItem.detailText = detailText; |
| passwordItem.accessibilityTraits |= UIAccessibilityTraitButton; |
| passwordItem.accessoryType = UITableViewCellAccessoryDisclosureIndicator; |
| return passwordItem; |
| } |
| |
| - (BlacklistedFormContentItem*) |
| blacklistedFormItemWithText:(NSString*)text |
| forForm:(autofill::PasswordForm*)form { |
| BlacklistedFormContentItem* passwordItem = |
| [[BlacklistedFormContentItem alloc] initWithType:ItemTypeBlacklisted]; |
| passwordItem.text = text; |
| passwordItem.form = form; |
| passwordItem.accessibilityTraits |= UIAccessibilityTraitButton; |
| passwordItem.accessoryType = UITableViewCellAccessoryDisclosureIndicator; |
| return passwordItem; |
| } |
| |
| #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 - SavePasswordsConsumerDelegate |
| |
| - (void)onGetPasswordStoreResults: |
| (std::vector<std::unique_ptr<autofill::PasswordForm>>&)result { |
| if (result.empty()) { |
| return; |
| } |
| 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::SortEntriesAndHideDuplicates( |
| &blacklistedForms_, &blacklistedPasswordDuplicates_); |
| |
| [self updateEditButton]; |
| [self reloadData]; |
| } |
| |
| #pragma mark - UISearchBarDelegate |
| |
| // Shows cancel button when editing starts. |
| - (void)searchBarTextDidBeginEditing:(UISearchBar*)searchBar { |
| [searchBar setShowsCancelButton:(BOOL)YES animated:(BOOL)YES]; |
| } |
| |
| // Hides cancel button and release focus on search bar. |
| - (void)searchBarCancelButtonClicked:(UISearchBar*)searchBar { |
| [searchBar setShowsCancelButton:(BOOL)NO animated:(BOOL)YES]; |
| [searchBar resignFirstResponder]; |
| searchBar.text = @""; |
| [self searchForTerm:@""]; |
| } |
| |
| // Searches for |searchText|. |
| - (void)searchBar:(UISearchBar*)searchBar textDidChange:(NSString*)searchText { |
| [self searchForTerm:searchText]; |
| } |
| |
| #pragma mark - Private methods |
| |
| - (void)searchForTerm:(NSString*)searchTerm { |
| self.searchTerm = searchTerm; |
| [self filterItems:searchTerm]; |
| |
| TableViewModel* model = self.tableViewModel; |
| NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet]; |
| if ([model hasSectionForSectionIdentifier:SectionIdentifierSavedPasswords]) { |
| NSInteger passwordSection = |
| [model sectionForSectionIdentifier:SectionIdentifierSavedPasswords]; |
| [indexSet addIndex:passwordSection]; |
| } |
| if ([model hasSectionForSectionIdentifier:SectionIdentifierBlacklist]) { |
| NSInteger blacklistedSection = |
| [model sectionForSectionIdentifier:SectionIdentifierBlacklist]; |
| [indexSet addIndex:blacklistedSection]; |
| } |
| if (indexSet.count > 0) { |
| [UIView setAnimationsEnabled:NO]; |
| [self.tableView reloadSections:indexSet |
| withRowAnimation:UITableViewRowAnimationAutomatic]; |
| [UIView setAnimationsEnabled:YES]; |
| } |
| } |
| |
| // Builds the filtered list of passwords/blacklisted based on given |
| // |searchTerm|. |
| - (void)filterItems:(NSString*)searchTerm { |
| TableViewModel* model = self.tableViewModel; |
| |
| if (!savedForms_.empty()) { |
| [model deleteAllItemsFromSectionWithIdentifier: |
| SectionIdentifierSavedPasswords]; |
| for (const auto& form : savedForms_) { |
| NSString* text = base::SysUTF8ToNSString( |
| password_manager::GetShownOriginAndLinkUrl(*form).first); |
| NSString* detailText = base::SysUTF16ToNSString(form->username_value); |
| bool hidden = |
| searchTerm.length > 0 && |
| ![text localizedCaseInsensitiveContainsString:searchTerm] && |
| ![detailText localizedCaseInsensitiveContainsString:searchTerm]; |
| if (hidden) |
| continue; |
| [model addItem:[self savedFormItemWithText:text |
| andDetailText:detailText |
| forForm:form.get()] |
| toSectionWithIdentifier:SectionIdentifierSavedPasswords]; |
| } |
| } |
| |
| if (!blacklistedForms_.empty()) { |
| [model deleteAllItemsFromSectionWithIdentifier:SectionIdentifierBlacklist]; |
| for (const auto& form : blacklistedForms_) { |
| NSString* text = base::SysUTF8ToNSString( |
| password_manager::GetShownOriginAndLinkUrl(*form).first); |
| bool hidden = searchTerm.length > 0 && |
| ![text localizedCaseInsensitiveContainsString:searchTerm]; |
| if (hidden) |
| continue; |
| [model addItem:[self blacklistedFormItemWithText:text forForm:form.get()] |
| toSectionWithIdentifier:SectionIdentifierBlacklist]; |
| } |
| } |
| } |
| |
| // Starts requests for saved and blacklisted passwords to the store. |
| - (void)getLoginsFromPasswordStore { |
| savedPasswordsConsumer_.reset(new ios::SavePasswordsConsumer(self)); |
| passwordStore_->GetAutofillableLogins(savedPasswordsConsumer_.get()); |
| blacklistPasswordsConsumer_.reset(new ios::SavePasswordsConsumer(self)); |
| passwordStore_->GetBlacklistLogins(blacklistPasswordsConsumer_.get()); |
| } |
| |
| - (void)updateExportPasswordsButton { |
| if (!exportPasswordsItem_) |
| return; |
| if (!savedForms_.empty() && |
| self.passwordExporter.exportState == ExportState::IDLE) { |
| exportReady_ = YES; |
| if (!self.editing) { |
| [self setExportPasswordsButtonEnabled:YES]; |
| } |
| } else { |
| exportReady_ = NO; |
| [self setExportPasswordsButtonEnabled:NO]; |
| } |
| } |
| |
| - (void)setExportPasswordsButtonEnabled:(BOOL)enabled { |
| if (enabled) { |
| DCHECK(exportReady_ && !self.editing); |
| exportPasswordsItem_.textColor = |
| UIColorFromRGB(kTableViewTextLabelColorBlue); |
| exportPasswordsItem_.accessibilityTraits &= ~UIAccessibilityTraitNotEnabled; |
| } else { |
| exportPasswordsItem_.textColor = |
| UIColorFromRGB(kTableViewTextLabelColorLightGrey); |
| 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]; |
| exportConfirmation.view.accessibilityIdentifier = |
| kPasswordsExportConfirmViewId; |
| |
| 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 PasswordsTableViewController* weakSelf = self; |
| UIAlertAction* exportAction = [UIAlertAction |
| actionWithTitle:l10n_util::GetNSString(IDS_IOS_EXPORT_PASSWORDS) |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| PasswordsTableViewController* strongSelf = weakSelf; |
| if (!strongSelf) { |
| return; |
| } |
| [strongSelf.passwordExporter |
| startExportFlow:CopyOf(strongSelf->savedForms_)]; |
| }]; |
| |
| [exportConfirmation addAction:exportAction]; |
| |
| [self presentViewController:exportConfirmation animated:YES completion:nil]; |
| } |
| |
| // Removes the given section if it exists and if isEmpty is true. |
| - (void)clearSectionWithIdentifier:(NSInteger)sectionIdentifier |
| ifEmpty:(bool)isEmpty { |
| TableViewModel* model = self.tableViewModel; |
| if (isEmpty && [model hasSectionForSectionIdentifier:sectionIdentifier]) { |
| NSInteger section = [model sectionForSectionIdentifier:sectionIdentifier]; |
| [model removeSectionWithIdentifier:sectionIdentifier]; |
| [[self tableView] deleteSections:[NSIndexSet indexSetWithIndex:section] |
| withRowAnimation:UITableViewRowAnimationAutomatic]; |
| } |
| } |
| |
| - (void)openDetailedViewForForm:(const autofill::PasswordForm&)form { |
| PasswordDetailsTableViewController* controller = |
| [[PasswordDetailsTableViewController alloc] |
| initWithPasswordForm:form |
| delegate:self |
| reauthenticationModule:reauthenticationModule_]; |
| controller.dispatcher = self.dispatcher; |
| [self.navigationController pushViewController:controller animated:YES]; |
| } |
| |
| - (void)deleteItemAtIndexPaths:(NSArray<NSIndexPath*>*)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:)]; |
| auto passwordIterator = savedForms_.begin(); |
| auto passwordEndIterator = savedForms_.end(); |
| auto blacklistedIterator = blacklistedForms_.begin(); |
| auto blacklistedEndIterator = blacklistedForms_.end(); |
| for (NSIndexPath* indexPath in sortedIndexPaths) { |
| // Only form items are editable. |
| PasswordFormContentItem* item = |
| base::mac::ObjCCastStrict<PasswordFormContentItem>( |
| [self.tableViewModel itemAtIndexPath:indexPath]); |
| BOOL blacklisted = [item isKindOfClass:[BlacklistedFormContentItem class]]; |
| auto& forms = blacklisted ? blacklistedForms_ : savedForms_; |
| auto& duplicates = |
| blacklisted ? blacklistedPasswordDuplicates_ : savedPasswordDuplicates_; |
| |
| const autofill::PasswordForm& deletedForm = *item.form; |
| auto begin = blacklisted ? blacklistedIterator : passwordIterator; |
| auto end = blacklisted ? blacklistedEndIterator : passwordEndIterator; |
| |
| auto formIterator = std::find_if( |
| begin, end, |
| [&deletedForm](const std::unique_ptr<autofill::PasswordForm>& value) { |
| return *value == deletedForm; |
| }); |
| DCHECK(formIterator != end); |
| |
| std::unique_ptr<autofill::PasswordForm> form = std::move(*formIterator); |
| std::string key = password_manager::CreateSortKey(*form); |
| auto duplicatesRange = duplicates.equal_range(key); |
| for (auto iterator = duplicatesRange.first; |
| iterator != duplicatesRange.second; ++iterator) { |
| passwordStore_->RemoveLogin(*(iterator->second)); |
| } |
| duplicates.erase(key); |
| |
| formIterator = forms.erase(formIterator); |
| passwordStore_->RemoveLogin(*form); |
| |
| // Keep track of where we are in the current list. |
| if (blacklisted) { |
| blacklistedIterator = formIterator; |
| } else { |
| passwordIterator = formIterator; |
| } |
| } |
| |
| // Remove empty sections. |
| __weak PasswordsTableViewController* weakSelf = self; |
| [self.tableView |
| performBatchUpdates:^{ |
| PasswordsTableViewController* strongSelf = weakSelf; |
| if (!strongSelf) |
| return; |
| |
| [strongSelf removeFromModelItemAtIndexPaths:indexPaths]; |
| [strongSelf.tableView |
| deleteRowsAtIndexPaths:indexPaths |
| withRowAnimation:UITableViewRowAnimationAutomatic]; |
| |
| // Delete in reverse order of section indexes (bottom up of section |
| // displayed), so that indexes in model matches those in the view. if |
| // we don't we'll cause a crash. |
| [strongSelf |
| clearSectionWithIdentifier:SectionIdentifierBlacklist |
| ifEmpty:strongSelf->blacklistedForms_.empty()]; |
| [strongSelf clearSectionWithIdentifier:SectionIdentifierSavedPasswords |
| ifEmpty:strongSelf->savedForms_.empty()]; |
| [strongSelf |
| clearSectionWithIdentifier:SectionIdentifierSearchPasswordsBox |
| ifEmpty:strongSelf->savedForms_.empty() && |
| strongSelf->blacklistedForms_.empty()]; |
| } |
| completion:^(BOOL finished) { |
| PasswordsTableViewController* strongSelf = weakSelf; |
| if (!strongSelf) |
| return; |
| // If both lists are empty, exit editing mode. |
| if (strongSelf->savedForms_.empty() && |
| strongSelf->blacklistedForms_.empty()) |
| [strongSelf setEditing:NO animated:YES]; |
| [strongSelf updateEditButton]; |
| [strongSelf updateExportPasswordsButton]; |
| }]; |
| } |
| |
| #pragma mark UITableViewDelegate |
| |
| - (void)tableView:(UITableView*)tableView |
| didSelectRowAtIndexPath:(NSIndexPath*)indexPath { |
| [super tableView:tableView didSelectRowAtIndexPath:indexPath]; |
| |
| // Actions should only take effect when not in editing mode. |
| if (self.editing) { |
| return; |
| } |
| |
| TableViewModel* model = self.tableViewModel; |
| NSInteger itemType = [model itemTypeForIndexPath:indexPath]; |
| switch (itemType) { |
| case ItemTypeLinkHeader: |
| case ItemTypeHeader: |
| case ItemTypeSavePasswordsSwitch: |
| case ItemTypeSearchBox: |
| break; |
| case ItemTypeSavedPassword: { |
| DCHECK_EQ(SectionIdentifierSavedPasswords, |
| [model sectionIdentifierForSection:indexPath.section]); |
| SavedFormContentItem* saveFormItem = |
| base::mac::ObjCCastStrict<SavedFormContentItem>( |
| [model itemAtIndexPath:indexPath]); |
| [self openDetailedViewForForm:*saveFormItem.form]; |
| break; |
| } |
| case ItemTypeBlacklisted: { |
| DCHECK_EQ(SectionIdentifierBlacklist, |
| [model sectionIdentifierForSection:indexPath.section]); |
| BlacklistedFormContentItem* blacklistedItem = |
| base::mac::ObjCCastStrict<BlacklistedFormContentItem>( |
| [model itemAtIndexPath:indexPath]); |
| [self openDetailedViewForForm:*blacklistedItem.form]; |
| break; |
| } |
| case ItemTypeExportPasswordsButton: |
| DCHECK_EQ(SectionIdentifierExportPasswordsButton, |
| [model sectionIdentifierForSection:indexPath.section]); |
| if (exportReady_) { |
| [self startPasswordsExportFlow]; |
| } |
| break; |
| default: |
| NOTREACHED(); |
| } |
| [tableView deselectRowAtIndexPath:indexPath animated:YES]; |
| } |
| |
| - (UIView*)tableView:(UITableView*)tableView |
| viewForHeaderInSection:(NSInteger)section { |
| UIView* view = [super tableView:tableView viewForHeaderInSection:section]; |
| switch ([self.tableViewModel sectionIdentifierForSection:section]) { |
| case SectionIdentifierSearchPasswordsBox: { |
| SettingsSearchView* searchView = |
| base::mac::ObjCCastStrict<SettingsSearchView>(view); |
| searchView.searchBar.delegate = self; |
| break; |
| } |
| case SectionIdentifierSavePasswordsSwitch: { |
| TableViewLinkHeaderFooterView* linkView = |
| base::mac::ObjCCastStrict<TableViewLinkHeaderFooterView>(view); |
| linkView.delegate = self; |
| break; |
| } |
| default: |
| break; |
| } |
| return view; |
| } |
| |
| // TODO(crbug.com/894791): Remove this after migrating to UISearchController. |
| - (CGFloat)tableView:(UITableView*)tableView |
| heightForFooterInSection:(NSInteger)section { |
| // Currently password search box is a header of an empty section. This code |
| // removes the footer with estimate height within the section to avoid a big |
| // gap between search box and next section. |
| if ([self.tableViewModel sectionIdentifierForSection:section] == |
| SectionIdentifierSearchPasswordsBox) { |
| return 0; |
| } |
| return UITableViewAutomaticDimension; |
| } |
| |
| #pragma mark - UITableViewDataSource |
| |
| - (BOOL)tableView:(UITableView*)tableView |
| canEditRowAtIndexPath:(NSIndexPath*)indexPath { |
| // Only password cells are editable. |
| TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath]; |
| return [item isKindOfClass:[SavedFormContentItem class]] || |
| [item isKindOfClass:[BlacklistedFormContentItem class]]; |
| } |
| |
| - (void)tableView:(UITableView*)tableView |
| commitEditingStyle:(UITableViewCellEditingStyle)editingStyle |
| forRowAtIndexPath:(NSIndexPath*)indexPath { |
| if (editingStyle != UITableViewCellEditingStyleDelete) |
| return; |
| [self deleteItemAtIndexPaths:@[ indexPath ]]; |
| } |
| |
| - (UITableViewCell*)tableView:(UITableView*)tableView |
| cellForRowAtIndexPath:(NSIndexPath*)indexPath { |
| UITableViewCell* cell = [super tableView:tableView |
| cellForRowAtIndexPath:indexPath]; |
| switch ([self.tableViewModel itemTypeForIndexPath:indexPath]) { |
| case ItemTypeSavePasswordsSwitch: { |
| SettingsSwitchCell* switchCell = |
| base::mac::ObjCCastStrict<SettingsSwitchCell>(cell); |
| [switchCell.switchView addTarget:self |
| action:@selector(savePasswordsSwitchChanged:) |
| forControlEvents:UIControlEventValueChanged]; |
| break; |
| } |
| } |
| return cell; |
| } |
| |
| #pragma mark PasswordDetailsTableViewControllerDelegate |
| |
| - (void)passwordDetailsTableViewController: |
| (PasswordDetailsTableViewController*)controller |
| 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_; |
| std::string key = password_manager::CreateSortKey(form); |
| 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 PasswordsTableViewController* 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.tableViewModel |
| indexPathForItemType:ItemTypeExportPasswordsButton |
| sectionIdentifier:SectionIdentifierExportPasswordsButton]; |
| UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:indexPath]; |
| sourceView = self.tableView; |
| 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 PasswordsTableViewController* 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 { |
| [savePasswordsItem_ setEnabled:enabled]; |
| [self reconfigureCellsForItems:@[ savePasswordsItem_ ]]; |
| } |
| |
| // Sets the search passwords item's enabled status to |enabled| and |
| // reconfigures the corresponding cell. |
| - (void)setSearchPasswordsItemEnabled:(BOOL)enabled { |
| TableViewModel* model = self.tableViewModel; |
| |
| if (![model |
| hasSectionForSectionIdentifier:SectionIdentifierSearchPasswordsBox]) { |
| return; |
| } |
| SettingsSearchItem* searchItem = |
| base::mac::ObjCCastStrict<SettingsSearchItem>([model |
| headerForSectionWithIdentifier:SectionIdentifierSearchPasswordsBox]); |
| [searchItem setEnabled:enabled]; |
| |
| NSUInteger section = |
| [model sectionForSectionIdentifier:SectionIdentifierSearchPasswordsBox]; |
| SettingsSearchView* view = base::mac::ObjCCastStrict<SettingsSearchView>( |
| [self.tableView headerViewForSection:section]); |
| |
| // |view| may be nil if the row is not currently on screen. If that's the case |
| // and we are disabling we force the keyboard down (since the view can't do |
| // it for us). |
| if (view) { |
| [searchItem configureHeaderFooterView:view withStyler:self.styler]; |
| } else if (!enabled) { |
| [self.view endEditing:YES]; |
| } |
| } |
| |
| #pragma mark - Testing |
| |
| - (void)setReauthenticationModuleForExporter: |
| (id<ReauthenticationProtocol>)reauthenticationModule { |
| _passwordExporter = [[PasswordExporter alloc] |
| initWithReauthenticationModule:reauthenticationModule |
| delegate:self]; |
| } |
| |
| - (PasswordExporter*)getPasswordExporter { |
| return _passwordExporter; |
| } |
| |
| @end |