chromium/ios/chrome/browser/ui/settings/password/password_issues/password_issues_coordinator.mm

// Copyright 2019 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_issues/password_issues_coordinator.h"

#import "base/apple/foundation_util.h"
#import "base/memory/raw_ptr.h"
#import "base/memory/scoped_refptr.h"
#import "ios/chrome/browser/favicon/model/favicon_loader.h"
#import "ios/chrome/browser/favicon/model/ios_chrome_favicon_loader_factory.h"
#import "ios/chrome/browser/passwords/model/ios_chrome_password_check_manager.h"
#import "ios/chrome/browser/passwords/model/ios_chrome_password_check_manager_factory.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_checkup_utils.h"
#import "ios/chrome/browser/shared/model/browser/browser.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/open_new_tab_command.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_utils.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_coordinator.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_coordinator_delegate.h"
#import "ios/chrome/browser/ui/settings/password/password_issues/password_issue.h"
#import "ios/chrome/browser/ui/settings/password/password_issues/password_issues_consumer.h"
#import "ios/chrome/browser/ui/settings/password/password_issues/password_issues_mediator.h"
#import "ios/chrome/browser/ui/settings/password/password_issues/password_issues_presenter.h"
#import "ios/chrome/browser/ui/settings/password/password_issues/password_issues_table_view_controller.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 "ui/base/l10n/l10n_util.h"

using password_manager::WarningType;

namespace {

// Returns a DetailsContext based on the given WarningType.
DetailsContext ComputeDetailsContextFromWarningType(WarningType warning_type) {
  switch (warning_type) {
    case WarningType::kCompromisedPasswordsWarning:
      return DetailsContext::kCompromisedIssues;
    case WarningType::kReusedPasswordsWarning:
      return DetailsContext::kReusedIssues;
    case WarningType::kWeakPasswordsWarning:
      return DetailsContext::kWeakIssues;
    case WarningType::kDismissedWarningsWarning:
      return DetailsContext::kDismissedWarnings;
    case WarningType::kNoInsecurePasswordsWarning:
      return DetailsContext::kPasswordSettings;
  }
}

}  // namespace

@interface PasswordIssuesCoordinator () <PasswordDetailsCoordinatorDelegate,
                                         PasswordIssuesCoordinatorDelegate,
                                         PasswordIssuesPresenter,
                                         ReauthenticationCoordinatorDelegate>

@end

@implementation PasswordIssuesCoordinator {
  // Main view controller for this coordinator.
  PasswordIssuesTableViewController* _viewController;

  // Main mediator for this coordinator.
  PasswordIssuesMediator* _mediator;

  // Coordinator for password details.
  PasswordDetailsCoordinator* _passwordDetails;

  // Password check manager to power mediator.
  raw_ptr<IOSChromePasswordCheckManager> _manager;

  // Type of insecure credentials issues to display.
  password_manager::WarningType _warningType;

  // Coordinator for password issues displaying dismissed compromised
  // credentials.
  PasswordIssuesCoordinator* _dismissedPasswordIssuesCoordinator;

  // Flag indicating if the coordinator should dismiss its view controller after
  // the view controller of a child coordinator is removed from the stack. When
  // the issues and dismissed warnings are removed by the user, the coordinator
  // should dismiss its view controller and go back to the previous screen. If
  // there are child coordinators, this flag is used to dismiss the view
  // controller after the children are dismissed.
  BOOL _shouldDismissAfterChildCoordinatorRemoved;

  // Coordinator for blocking Password Issues until Local Authentication is
  // passed. Used for requiring authentication when opening Password Issues
  // from outside the Password Manager and when the app is
  // backgrounded/foregrounded with Password Issues opened.
  ReauthenticationCoordinator* _reauthCoordinator;

  // For recording visits after successful authentication.
  IOSPasswordManagerVisitsRecorder* _visitsRecorder;
}

@synthesize baseNavigationController = _baseNavigationController;

- (instancetype)initForWarningType:(password_manager::WarningType)warningType
          baseNavigationController:(UINavigationController*)navigationController
                           browser:(Browser*)browser {
  self = [super initWithBaseViewController:navigationController
                                   browser:browser];
  if (self) {
    _warningType = warningType;
    _baseNavigationController = navigationController;
    _dispatcher = HandlerForProtocol(self.browser->GetCommandDispatcher(),
                                     ApplicationCommands);
    _skipAuthenticationOnStart = NO;
  }
  return self;
}

- (void)start {
  [super start];

  ChromeBrowserState* browserState = self.browser->GetBrowserState();
  _mediator = [[PasswordIssuesMediator alloc]
        initForWarningType:_warningType
      passwordCheckManager:IOSChromePasswordCheckManagerFactory::
                               GetForBrowserState(browserState)
                                   .get()
             faviconLoader:IOSChromeFaviconLoaderFactory::GetForBrowserState(
                               browserState)
               syncService:SyncServiceFactory::GetForBrowserState(
                               browserState)];

  PasswordIssuesTableViewController* passwordIssuesTableViewController =
      [[PasswordIssuesTableViewController alloc]
          initWithWarningType:_warningType];
  passwordIssuesTableViewController.imageDataSource = _mediator;
  _viewController = passwordIssuesTableViewController;

  // If reauthentication module was not provided, coordinator will create its
  // own.
  if (!self.reauthModule) {
    self.reauthModule = password_manager::BuildReauthenticationModule(
        /*successfulReauthTimeAccessor=*/_mediator);
  }

  _mediator.consumer = _viewController;
  _viewController.presenter = self;

  _visitsRecorder = [[IOSPasswordManagerVisitsRecorder alloc]
      initWithPasswordManagerSurface:password_manager::PasswordManagerSurface::
                                         kPasswordIssues];

  // Only record visit if no auth is required, otherwise wait for successful
  // auth.
  if (_skipAuthenticationOnStart) {
    [_visitsRecorder maybeRecordVisitMetric];
  }

  // Disable animation when content will be blocked for reauth to prevent
  // flickering in navigation bar.
  [self.baseNavigationController pushViewController:_viewController
                                           animated:_skipAuthenticationOnStart];

  [self startReauthCoordinatorWithAuthOnStart:!_skipAuthenticationOnStart];
}

- (void)stop {
  [_mediator disconnect];
  _mediator = nil;
  _viewController = nil;

  [_passwordDetails stop];
  _passwordDetails.delegate = nil;
  _passwordDetails = nil;

  [self stopDismissedPasswordIssuesCoordinator];
  [self stopReauthenticationCoordinator];
}

#pragma mark - PasswordIssuesPresenter

- (void)dismissPasswordIssuesTableViewController {
  [self.delegate passwordIssuesCoordinatorDidRemove:self];
}

- (void)dismissAndOpenURL:(CrURL*)URL {
  OpenNewTabCommand* command =
      [OpenNewTabCommand commandWithURLFromChrome:URL.gurl];
  [self.dispatcher closeSettingsUIAndOpenURL:command];
}

- (void)presentPasswordIssueDetails:(PasswordIssue*)password {
  DCHECK(!_passwordDetails);

  [self stopReauthCoordinatorBeforeStartingChildCoordinator];

  _passwordDetails = [[PasswordDetailsCoordinator alloc]
      initWithBaseNavigationController:self.baseNavigationController
                               browser:self.browser
                            credential:password.credential
                          reauthModule:self.reauthModule
                               context:ComputeDetailsContextFromWarningType(
                                           _warningType)];
  _passwordDetails.delegate = self;
  [_passwordDetails start];
}

- (void)presentDismissedCompromisedCredentials {
  // Not an invariant due to possible race conditions. DCHECKing for debugging
  // purposes. See crbug.com/40067451.
  DCHECK(!_dismissedPasswordIssuesCoordinator);

  [self stopReauthCoordinatorBeforeStartingChildCoordinator];

  _dismissedPasswordIssuesCoordinator = [[PasswordIssuesCoordinator alloc]
            initForWarningType:password_manager::WarningType::
                                   kDismissedWarningsWarning
      baseNavigationController:self.baseNavigationController
                       browser:self.browser];
  _dismissedPasswordIssuesCoordinator.skipAuthenticationOnStart = YES;
  _dismissedPasswordIssuesCoordinator.reauthModule = self.reauthModule;
  _dismissedPasswordIssuesCoordinator.delegate = self;
  [_dismissedPasswordIssuesCoordinator start];
}

- (void)dismissAfterAllIssuesGone {
  if (self.baseNavigationController.topViewController == _viewController) {
    [self.baseNavigationController popViewControllerAnimated:NO];
  } else {
    _shouldDismissAfterChildCoordinatorRemoved = YES;
  }
}

#pragma mark - PasswordDetailsCoordinatorDelegate

- (void)passwordDetailsCoordinatorDidRemove:
    (PasswordDetailsCoordinator*)coordinator {
  DCHECK_EQ(_passwordDetails, coordinator);
  [_passwordDetails stop];
  _passwordDetails.delegate = nil;
  _passwordDetails = nil;

  [self onChildCoordinatorDidRemove];
}

#pragma mark - PasswordIssuesCoordinatorDelegate

- (void)passwordIssuesCoordinatorDidRemove:
    (PasswordIssuesCoordinator*)coordinator {
  CHECK_EQ(_dismissedPasswordIssuesCoordinator, coordinator);
  [self stopDismissedPasswordIssuesCoordinator];

  [self onChildCoordinatorDidRemove];
}

#pragma mark - PasswordManagerReauthenticationDelegate

- (void)dismissPasswordManagerAfterFailedReauthentication {
  [_delegate dismissPasswordManagerAfterFailedReauthentication];
}

#pragma mark - ReauthenticationCoordinatorDelegate

- (void)successfulReauthenticationWithCoordinator:
    (ReauthenticationCoordinator*)coordinator {
  [_visitsRecorder maybeRecordVisitMetric];
}

- (void)dismissUIAfterFailedReauthenticationWithCoordinator:
    (ReauthenticationCoordinator*)coordinator {
  CHECK_EQ(_reauthCoordinator, coordinator);

  [_delegate dismissPasswordManagerAfterFailedReauthentication];
}

- (void)willPushReauthenticationViewController {
  // No-op.
}

#pragma mark - Private

- (void)stopDismissedPasswordIssuesCoordinator {
  [_dismissedPasswordIssuesCoordinator stop];
  _dismissedPasswordIssuesCoordinator.reauthModule = nil;
  _dismissedPasswordIssuesCoordinator.delegate = nil;
  _dismissedPasswordIssuesCoordinator = nil;
}

- (void)stopReauthenticationCoordinator {
  [_reauthCoordinator stop];
  _reauthCoordinator.delegate = nil;
  _reauthCoordinator = nil;
}

// Called after the view controller of a child coordinator of `self` was removed
// from the navigation stack.
- (void)onChildCoordinatorDidRemove {
  // If the content of the view controller was gone while a child coordinator
  // was presenting content, dismiss the view controller now that the child
  // coordinator's vc was removed.
  if (_shouldDismissAfterChildCoordinatorRemoved) {
    CHECK_EQ(self.baseNavigationController.topViewController, _viewController);
    _shouldDismissAfterChildCoordinatorRemoved = NO;
    dispatch_async(dispatch_get_main_queue(), ^{
      [self.baseNavigationController popViewControllerAnimated:NO];
    });
  } else {
    // Otherwise restart scene monitoring so authentication is required when the
    // scene is backgrounded/foregrounded.
    [self restartReauthCoordinator];
  }
}

// Starts reauthCoordinator.
// - authOnStart: Pass `YES` to cover Password Issues with an empty view
// controller until successful Local Authentication when reauthCoordinator
// starts.
//
// Local authentication is required every time the current
// scene is backgrounded and foregrounded until reauthCoordinator is stopped.
- (void)startReauthCoordinatorWithAuthOnStart:(BOOL)authOnStart {
  DCHECK(!_reauthCoordinator);

  _reauthCoordinator = [[ReauthenticationCoordinator alloc]
      initWithBaseNavigationController:_baseNavigationController
                               browser:self.browser
                reauthenticationModule:_reauthModule
                           authOnStart:authOnStart];

  _reauthCoordinator.delegate = self;

  [_reauthCoordinator start];
}

// Stop reauth coordinator when a child coordinator will be started.
//
// Needed so reauth coordinator doesn't block for reauth if the scene state
// changes while the child coordinator is presenting its content. The child
// coordinator will add its own reauth coordinator to block its content for
// reauth.
- (void)stopReauthCoordinatorBeforeStartingChildCoordinator {
  // See PasswordsCoordinator
  // stopReauthCoordinatorBeforeStartingChildCoordinator.
  [_reauthCoordinator stopAndPopViewController];
  _reauthCoordinator.delegate = nil;
  _reauthCoordinator = nil;
}

// Starts reauthCoordinator after a child coordinator content was dismissed.
- (void)restartReauthCoordinator {
  // Restart reauth coordinator so it monitors scene state changes and requests
  // local authentication after the scene goes to the background.
  [self startReauthCoordinatorWithAuthOnStart:NO];
}

@end