// Copyright 2020 The Chromium Authors
// 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/password/password_details/password_details_coordinator.h"
#import <utility>
#import <vector>
#import "base/apple/foundation_util.h"
#import "base/memory/scoped_refptr.h"
#import "base/strings/sys_string_conversions.h"
#import "components/password_manager/core/browser/password_manager_client.h"
#import "components/password_manager/core/browser/ui/affiliated_group.h"
#import "components/password_manager/core/browser/ui/credential_ui_entry.h"
#import "components/prefs/pref_service.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/passwords/model/metrics/ios_password_manager_metrics.h"
#import "ios/chrome/browser/passwords/model/metrics/ios_password_manager_visits_recorder.h"
#import "ios/chrome/browser/passwords/model/password_tab_helper.h"
#import "ios/chrome/browser/shared/coordinator/alert/action_sheet_coordinator.h"
#import "ios/chrome/browser/shared/coordinator/alert/alert_coordinator.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_list.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_factory.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/snackbar_commands.h"
#import "ios/chrome/browser/ui/settings/password/password_details/credential_details.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_consumer.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_coordinator_delegate.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_handler.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_mediator.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_mediator_delegate.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_table_view_controller.h"
#import "ios/chrome/browser/ui/settings/password/password_sharing/password_sharing_coordinator.h"
#import "ios/chrome/browser/ui/settings/password/password_sharing/password_sharing_coordinator_delegate.h"
#import "ios/chrome/browser/ui/settings/password/password_sharing/password_sharing_first_run_coordinator.h"
#import "ios/chrome/browser/ui/settings/password/password_sharing/password_sharing_first_run_coordinator_delegate.h"
#import "ios/chrome/browser/ui/settings/password/password_sharing/password_sharing_metrics.h"
#import "ios/chrome/browser/ui/settings/password/reauthentication/reauthentication_coordinator.h"
#import "ios/chrome/browser/ui/settings/utils/password_utils.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_protocol.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/web_state.h"
#import "ui/base/l10n/l10n_util.h"
@interface PasswordDetailsCoordinator () <
PasswordDetailsHandler,
PasswordDetailsMediatorDelegate,
ReauthenticationCoordinatorDelegate,
PasswordSharingCoordinatorDelegate,
PasswordSharingFirstRunCoordinatorDelegate>
// Main view controller for this coordinator.
@property(nonatomic, strong) PasswordDetailsTableViewController* viewController;
// Main mediator for this coordinator.
@property(nonatomic, strong) PasswordDetailsMediator* mediator;
// Module containing the reauthentication mechanism for viewing and copying
// passwords.
// Has to be strong for password bottom sheet feature or else it becomes nil.
@property(nonatomic, strong) id<ReauthenticationProtocol>
reauthenticationModule;
// Modal alert for interactions with password.
@property(nonatomic, strong) AlertCoordinator* alertCoordinator;
// The action sheet coordinator, if one is currently being shown.
@property(nonatomic, strong) ActionSheetCoordinator* actionSheetCoordinator;
// Coordinator for the password sharing flow.
@property(nonatomic, strong)
PasswordSharingCoordinator* passwordSharingCoordinator;
// Coordinator for the password sharing first run flow.
@property(nonatomic, strong)
PasswordSharingFirstRunCoordinator* passwordSharingFirstRunCoordinator;
// Coordinator for blocking password details until Local Authentication is
// successful.
@property(nonatomic, strong) ReauthenticationCoordinator* reauthCoordinator;
@end
@implementation PasswordDetailsCoordinator {
password_manager::AffiliatedGroup _affiliatedGroup;
password_manager::CredentialUIEntry _credential;
// The context in which the password details are accessed.
DetailsContext _context;
// For recording visits after successful authentication.
IOSPasswordManagerVisitsRecorder* _visitsRecorder;
}
@synthesize baseNavigationController = _baseNavigationController;
- (instancetype)
initWithBaseNavigationController:
(UINavigationController*)navigationController
browser:(Browser*)browser
credential:
(const password_manager::CredentialUIEntry&)
credential
reauthModule:(id<ReauthenticationProtocol>)reauthModule
context:(DetailsContext)context {
self = [super initWithBaseViewController:navigationController
browser:browser];
if (self) {
DCHECK(navigationController);
_baseNavigationController = navigationController;
_credential = credential;
_reauthenticationModule = reauthModule;
_context = context;
}
return self;
}
- (instancetype)
initWithBaseNavigationController:
(UINavigationController*)navigationController
browser:(Browser*)browser
affiliatedGroup:(const password_manager::AffiliatedGroup&)
affiliatedGroup
reauthModule:(id<ReauthenticationProtocol>)reauthModule
context:(DetailsContext)context {
self = [super initWithBaseViewController:navigationController
browser:browser];
if (self) {
DCHECK(navigationController);
_baseNavigationController = navigationController;
_affiliatedGroup = affiliatedGroup;
_reauthenticationModule = reauthModule;
_context = context;
}
return self;
}
- (void)start {
self.viewController = [[PasswordDetailsTableViewController alloc] init];
std::vector<password_manager::CredentialUIEntry> credentials;
NSString* displayName;
if (_affiliatedGroup.GetCredentials().size() > 0) {
displayName = [NSString
stringWithUTF8String:_affiliatedGroup.GetDisplayName().c_str()];
for (const auto& credentialGroup : _affiliatedGroup.GetCredentials()) {
credentials.push_back(credentialGroup);
}
} else {
credentials.push_back(_credential);
}
ChromeBrowserState* browserState = self.browser->GetBrowserState();
self.mediator =
[[PasswordDetailsMediator alloc] initWithPasswords:credentials
displayName:displayName
browserState:browserState
context:_context
delegate:self];
self.mediator.consumer = self.viewController;
self.viewController.handler = self;
self.viewController.delegate = self.mediator;
self.viewController.applicationCommandsHandler = HandlerForProtocol(
self.browser->GetCommandDispatcher(), ApplicationCommands);
self.viewController.snackbarCommandsHandler = HandlerForProtocol(
self.browser->GetCommandDispatcher(), SnackbarCommands);
self.viewController.reauthModule = self.reauthenticationModule;
if (self.openInEditMode) {
[self.viewController editButtonPressed];
}
if (self.showCancelButton) {
[self.viewController setupLeftCancelButton];
}
BOOL requireAuth = [self shouldRequireAuthOnStart];
// Disable animation when content will be blocked for reauth to prevent
// flickering in navigation bar.
[self.baseNavigationController pushViewController:self.viewController
animated:!requireAuth];
_visitsRecorder = [[IOSPasswordManagerVisitsRecorder alloc]
initWithPasswordManagerSurface:password_manager::PasswordManagerSurface::
kPasswordDetails];
// Wait for authentication to pass before logging a page visit.
if (!requireAuth) {
[_visitsRecorder maybeRecordVisitMetric];
}
[self startReauthCoordinator];
}
- (void)stop {
[_reauthCoordinator stop];
_reauthCoordinator.delegate = nil;
_reauthCoordinator = nil;
[self dismissActionSheetCoordinator];
[self.mediator disconnect];
self.mediator = nil;
self.viewController = nil;
[self dismissAlertCoordinator];
[self stopPasswordSharingCoordinator];
[self stopPasswordSharingFirstRunCoordinatorWithCompletion:nil];
}
#pragma mark - PasswordDetailsHandler
- (void)passwordDetailsTableViewControllerWasDismissed {
[self.delegate passwordDetailsCoordinatorDidRemove:self];
}
- (void)dismissPasswordDetailsTableViewController {
[self.delegate passwordDetailsCancelButtonWasTapped];
[self.delegate passwordDetailsCoordinatorDidRemove:self];
}
- (void)showPasswordEditDialogWithOrigin:(NSString*)origin {
NSString* message = l10n_util::GetNSStringF(IDS_IOS_EDIT_PASSWORD_DESCRIPTION,
base::SysNSStringToUTF16(origin));
self.actionSheetCoordinator = [[ActionSheetCoordinator alloc]
initWithBaseViewController:self.viewController
browser:self.browser
title:nil
message:message
barButtonItem:self.viewController.navigationItem
.rightBarButtonItem];
__weak __typeof(self) weakSelf = self;
[self.actionSheetCoordinator
addItemWithTitle:l10n_util::GetNSString(IDS_IOS_CONFIRM_PASSWORD_EDIT)
action:^{
[weakSelf.viewController passwordEditingConfirmed];
[weakSelf dismissActionSheetCoordinator];
}
style:UIAlertActionStyleDefault];
[self.actionSheetCoordinator
addItemWithTitle:l10n_util::GetNSString(IDS_IOS_CANCEL_PASSWORD_EDIT)
action:^{
[weakSelf dismissActionSheetCoordinator];
}
style:UIAlertActionStyleCancel];
[self.actionSheetCoordinator start];
}
- (void)showCredentialDeleteDialogWithCredentialDetails:
(CredentialDetails*)credential
anchorView:(UIView*)anchorView {
NSString* title;
NSString* message;
// Blocked websites have empty `password` and no title or message.
if ([credential.password length]) {
std::tie(title, message) =
password_manager::GetPasswordAlertTitleAndMessageForOrigins(
credential.origins);
}
NSString* buttonText = l10n_util::GetNSString(IDS_IOS_DELETE_ACTION_TITLE);
self.actionSheetCoordinator =
anchorView
? [[ActionSheetCoordinator alloc]
initWithBaseViewController:self.viewController
browser:self.browser
title:title
message:message
rect:anchorView.bounds
view:anchorView]
: [[ActionSheetCoordinator alloc]
initWithBaseViewController:self.viewController
browser:self.browser
title:title
message:message
barButtonItem:self.viewController.deleteButton];
__weak __typeof(self.mediator) weakMediator = self.mediator;
__weak __typeof(self) weakSelf = self;
[self.actionSheetCoordinator
addItemWithTitle:buttonText
action:^{
[weakMediator removeCredential:credential];
[weakSelf dismissActionSheetCoordinator];
}
style:UIAlertActionStyleDestructive];
[self.actionSheetCoordinator
addItemWithTitle:l10n_util::GetNSString(IDS_IOS_CANCEL_PASSWORD_DELETION)
action:^{
[weakSelf dismissActionSheetCoordinator];
}
style:UIAlertActionStyleCancel];
[self.actionSheetCoordinator start];
}
- (void)moveCredentialToAccountStore:(CredentialDetails*)credential
anchorView:(UIView*)anchorView
movedCompletion:(void (^)())movedCompletion {
if (![self.mediator hasPasswordConflictInAccount:credential]) {
[self.mediator moveCredentialToAccountStore:credential];
movedCompletion();
return;
}
NSString* actionSheetTitle =
l10n_util::GetNSString(IDS_IOS_PASSWORD_MOVE_CONFLICT_ACTION_SHEET_TITLE);
NSString* actionSheetMessage = l10n_util::GetNSString(
IDS_IOS_PASSWORD_MOVE_CONFLICT_ACTION_SHEET_MESSAGE);
self.actionSheetCoordinator = [[ActionSheetCoordinator alloc]
initWithBaseViewController:self.viewController
browser:self.browser
title:actionSheetTitle
message:actionSheetMessage
rect:anchorView.bounds
view:anchorView];
__weak __typeof(self) weakSelf = self;
[self.actionSheetCoordinator
addItemWithTitle:l10n_util::GetNSString(IDS_IOS_KEEP_RECENT_PASSWORD)
action:^{
[weakSelf.mediator
moveCredentialToAccountStoreWithConflict:credential];
movedCompletion();
[weakSelf dismissActionSheetCoordinator];
}
style:UIAlertActionStyleDefault];
[self.actionSheetCoordinator
addItemWithTitle:l10n_util::GetNSString(IDS_IOS_CANCEL_PASSWORD_MOVE)
action:^{
[weakSelf dismissActionSheetCoordinator];
}
style:UIAlertActionStyleCancel];
[self.actionSheetCoordinator start];
}
- (void)showPasswordDetailsInEditModeWithoutAuthentication {
[self.viewController showEditViewWithoutAuthentication];
}
- (void)onAllPasswordsDeleted {
DCHECK_EQ(self.baseNavigationController.topViewController,
self.viewController);
// For credential details opened outside of the settings context.
if (_context == DetailsContext::kOutsideSettings) {
[self dismissPasswordDetailsTableViewController];
} else {
// For credential details opened from the Password Manager in the settings.
[self.baseNavigationController popViewControllerAnimated:YES];
}
}
- (void)onShareButtonPressed {
LogPasswordSharingInteraction(
PasswordSharingInteraction::kPasswordDetailsShareButtonClicked);
if (self.browser->GetBrowserState()->GetPrefs()->GetBoolean(
prefs::kPasswordSharingFlowHasBeenEntered)) {
[self startPasswordSharingCoordinator];
} else {
[self.viewController showShareButton];
[self.passwordSharingFirstRunCoordinator stop];
self.passwordSharingFirstRunCoordinator =
[[PasswordSharingFirstRunCoordinator alloc]
initWithBaseViewController:self.viewController
browser:self.browser];
self.passwordSharingFirstRunCoordinator.delegate = self;
[self.passwordSharingFirstRunCoordinator start];
}
}
#pragma mark - PasswordDetailsMediatorDelegate
- (void)showDismissWarningDialogWithCredentialDetails:
(CredentialDetails*)credential {
NSString* title =
l10n_util::GetNSString(IDS_IOS_DISMISS_WARNING_DIALOG_TITLE);
NSString* message =
l10n_util::GetNSString(IDS_IOS_DISMISS_WARNING_DIALOG_MESSAGE);
self.alertCoordinator =
[[AlertCoordinator alloc] initWithBaseViewController:self.viewController
browser:self.browser
title:title
message:message];
NSString* cancelButtonText = l10n_util::GetNSString(IDS_CANCEL);
__weak PasswordDetailsCoordinator* weakSelf = self;
[self.alertCoordinator addItemWithTitle:cancelButtonText
action:^{
[weakSelf dismissAlertCoordinator];
}
style:UIAlertActionStyleDefault];
NSString* dismissButtonText =
l10n_util::GetNSString(IDS_IOS_DISMISS_WARNING_DIALOG_DISMISS_BUTTON);
__weak __typeof(self.mediator) weakMediator = self.mediator;
[self.alertCoordinator
addItemWithTitle:dismissButtonText
action:^{
[weakMediator
didConfirmWarningDismissalForPassword:credential];
[weakSelf dismissAlertCoordinator];
}
style:UIAlertActionStyleDefault
preferred:YES
enabled:YES];
[self.alertCoordinator start];
}
- (void)updateFormManagers {
ChromeBrowserState* browserState = self.browser->GetBrowserState();
BrowserList* browserList =
BrowserListFactory::GetForBrowserState(browserState);
for (Browser* browser :
browserList->BrowsersOfType(BrowserList::BrowserType::kAll)) {
[self updateFormManagersForBrowser:browser];
}
}
#pragma mark - ReauthenticationCoordinatorDelegate
- (void)successfulReauthenticationWithCoordinator:
(ReauthenticationCoordinator*)coordinator {
[_visitsRecorder maybeRecordVisitMetric];
}
- (void)dismissUIAfterFailedReauthenticationWithCoordinator:
(ReauthenticationCoordinator*)coordinator {
CHECK_EQ(_reauthCoordinator, coordinator);
[_delegate dismissPasswordManagerAfterFailedReauthentication];
}
- (void)willPushReauthenticationViewController {
// Dismiss modal ui before reauth view controller is pushed in front of
// password details.
[self dismissAlertCoordinator];
[self dismissActionSheetCoordinator];
[self stopPasswordSharingCoordinator];
[self stopPasswordSharingFirstRunCoordinatorWithCompletion:nil];
}
#pragma mark - PasswordSharingCoordinatorDelegate
- (void)passwordSharingCoordinatorDidRemove:
(PasswordSharingCoordinator*)coordinator {
if (self.passwordSharingCoordinator == coordinator) {
[self stopPasswordSharingCoordinator];
}
}
- (void)shareFlowEntered {
[self.viewController showShareButton];
}
#pragma mark - PasswordSharingFirstRunCoordinatorDelegate
- (void)passwordSharingFirstRunCoordinatorDidAccept:
(PasswordSharingFirstRunCoordinator*)coordinator {
self.browser->GetBrowserState()->GetPrefs()->SetBoolean(
prefs::kPasswordSharingFlowHasBeenEntered, true);
if (self.passwordSharingFirstRunCoordinator == coordinator) {
[self stopPasswordSharingFirstRunCoordinatorWithCompletion:^{
[self startPasswordSharingCoordinator];
}];
}
}
- (void)passwordSharingFirstRunCoordinatorWasDismissed:
(PasswordSharingFirstRunCoordinator*)coordinator {
if (self.passwordSharingFirstRunCoordinator == coordinator) {
[self stopPasswordSharingFirstRunCoordinatorWithCompletion:nil];
}
}
#pragma mark - Private
- (void)dismissActionSheetCoordinator {
[self.actionSheetCoordinator stop];
self.actionSheetCoordinator = nil;
}
- (void)dismissAlertCoordinator {
[self.alertCoordinator stop];
self.alertCoordinator = nil;
}
// Starts reauthCoordinator. If Password Details was opened from outside the
// Password Manager, Local Authentication is required. Once started
// reauthCoordinator observes scene state changes and requires authentication
// when the scene is backgrounded and then foregrounded while Password Details
// is opened.
- (void)startReauthCoordinator {
_reauthCoordinator = [[ReauthenticationCoordinator alloc]
initWithBaseNavigationController:_baseNavigationController
browser:self.browser
reauthenticationModule:_reauthenticationModule
authOnStart:[self shouldRequireAuthOnStart]];
_reauthCoordinator.delegate = self;
[_reauthCoordinator start];
}
// Starts the main coordinator for the password sharing flow.
- (void)startPasswordSharingCoordinator {
[self.passwordSharingCoordinator stop];
self.passwordSharingCoordinator = [[PasswordSharingCoordinator alloc]
initWithBaseViewController:self.viewController
browser:self.browser
credentials:self.mediator.credentials
savedPasswordsPresenter:self.mediator.savedPasswordsPresenter];
self.passwordSharingCoordinator.delegate = self;
[self.passwordSharingCoordinator start];
}
// Stops the first run coordinator for the password sharing flow and calls
// `completion` on its vc dismissal.
- (void)stopPasswordSharingFirstRunCoordinatorWithCompletion:
(ProceduralBlock)completion {
[self.passwordSharingFirstRunCoordinator stopWithCompletion:completion];
self.passwordSharingFirstRunCoordinator.delegate = nil;
self.passwordSharingFirstRunCoordinator = nil;
}
// Stops the main coordinator for the password sharing flow.
- (void)stopPasswordSharingCoordinator {
[self.passwordSharingCoordinator stop];
self.passwordSharingCoordinator.delegate = nil;
self.passwordSharingCoordinator = nil;
}
// Whether Local Authentication should be required before displaying the
// contents of Password Details.
- (BOOL)shouldRequireAuthOnStart {
// Authentication required only if opening Password Details from outside the
// Password Manager.
switch (_context) {
case DetailsContext::kWeakIssues:
case DetailsContext::kReusedIssues:
case DetailsContext::kPasswordSettings:
case DetailsContext::kCompromisedIssues:
case DetailsContext::kDismissedWarnings:
return NO;
case DetailsContext::kOutsideSettings:
return YES;
}
}
// Refreshes the password suggestions list for a specific `browser`.
- (void)updateFormManagersForBrowser:(Browser*)browser {
web::WebState* webState = browser->GetWebStateList()->GetActiveWebState();
if (!webState) {
return;
}
password_manager::PasswordManagerClient* passwordManagerClient =
PasswordTabHelper::FromWebState(webState)->GetPasswordManagerClient();
passwordManagerClient->UpdateFormManagers();
}
@end