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

// 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_issues/password_issues_mediator.h"

#import <memory>
#import <utility>

#import "base/memory/raw_ptr.h"
#import "base/strings/sys_string_conversions.h"
#import "components/google/core/common/google_util.h"
#import "components/password_manager/core/browser/ui/insecure_credentials_manager.h"
#import "components/password_manager/core/browser/ui/saved_passwords_presenter.h"
#import "components/sync/service/sync_service.h"
#import "ios/chrome/browser/favicon/model/favicon_loader.h"
#import "ios/chrome/browser/net/model/crurl.h"
#import "ios/chrome/browser/passwords/model/ios_chrome_password_check_manager.h"
#import "ios/chrome/browser/passwords/model/password_check_observer_bridge.h"
#import "ios/chrome/browser/passwords/model/password_checkup_utils.h"
#import "ios/chrome/browser/passwords/model/password_manager_util_ios.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/ui/settings/password/password_checkup/password_checkup_constants.h"
#import "ios/chrome/browser/ui/settings/password/password_issues/password_issues_consumer.h"
#import "ios/chrome/browser/ui/settings/password/saved_passwords_presenter_observer.h"
#import "ios/chrome/common/ui/favicon/favicon_constants.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"

using password_manager::CredentialUIEntry;
using password_manager::WarningType;

namespace {

// Creates PasswordIssues from CredentialUIEntry to display them in the Password
// Issues list UI for the given `warning_type`. PasswordIssues are sorted by
// website and username.
NSArray<PasswordIssue*>* GetSortedPasswordIssues(
    WarningType warning_type,
    const std::vector<CredentialUIEntry>& insecure_credentials) {
  NSMutableArray<PasswordIssue*>* passwords = [[NSMutableArray alloc] init];

  BOOL enable_compromised_description =
      warning_type == WarningType::kCompromisedPasswordsWarning ||
      warning_type == WarningType::kDismissedWarningsWarning;

  for (auto credential : insecure_credentials) {
    [passwords addObject:[[PasswordIssue alloc] initWithCredential:credential
                                      enableCompromisedDescription:
                                          enable_compromised_description]];
  }

  NSSortDescriptor* origin = [[NSSortDescriptor alloc] initWithKey:@"website"
                                                         ascending:YES];
  NSSortDescriptor* username = [[NSSortDescriptor alloc] initWithKey:@"username"
                                                           ascending:YES];

  return [passwords sortedArrayUsingDescriptors:@[ origin, username ]];
}

// Creates a `PasswordIssueGroup` for each set of issues with the same password.
// Issues in the same group are sorted by their position in `password_issues`.
// Groups are sorted by the position of their first issue in `password_issues`.
NSArray<PasswordIssueGroup*>* GroupIssuesByPassword(
    NSArray<PasswordIssue*>* password_issues) {
  // Holds issues with the same password.
  // Used for tracking the order of each group.
  NSMutableArray<NSMutableArray<PasswordIssue*>*>* same_password_issues =
      [NSMutableArray array];
  // Used for grouping issues by passsword.
  NSMutableDictionary<NSString*, NSMutableArray*>* issue_groups =
      [NSMutableDictionary dictionary];

  for (PasswordIssue* issue in password_issues) {
    NSString* password = base::SysUTF16ToNSString(issue.credential.password);

    NSMutableArray<PasswordIssue*>* issues_in_group =
        [issue_groups objectForKey:password];
    // Add issue to existing group with same password.
    if (issues_in_group) {
      [issues_in_group addObject:issue];
    } else {
      // First issue with this password, add it to its own group.
      issues_in_group = [NSMutableArray arrayWithObject:issue];
      [same_password_issues addObject:issues_in_group];
      issue_groups[password] = issues_in_group;
    }
  }

  // Map issue groups to PasswordIssueGroups.
  NSMutableArray<PasswordIssueGroup*>* password_issue_groups =
      [NSMutableArray arrayWithCapacity:same_password_issues.count];
  [same_password_issues
      enumerateObjectsUsingBlock:^(NSMutableArray<PasswordIssue*>* issues,
                                   NSUInteger index, BOOL* stop) {
        NSString* headerText =
            l10n_util::GetNSStringF(IDS_IOS_REUSED_PASSWORD_ISSUES_GROUP_HEADER,
                                    base::NumberToString16(issues.count));
        [password_issue_groups
            addObject:[[PasswordIssueGroup alloc] initWithHeaderText:headerText
                                                      passwordIssues:issues]];
      }];

  return password_issue_groups;
}

// Maps CredentialUIEntry to PasswordIssue sorted and grouped according to their
// `warning_type`.
NSArray<PasswordIssueGroup*>* GetPasswordIssueGroups(
    WarningType warning_type,
    const std::vector<CredentialUIEntry>& insecure_credentials) {
  if (insecure_credentials.empty()) {
    return @[];
  }

  // Sort by website and username.
  NSArray<PasswordIssue*>* sorted_issues =
      GetSortedPasswordIssues(warning_type, insecure_credentials);

  // Reused issues are grouped by passwords.
  if (warning_type == WarningType::kReusedPasswordsWarning) {
    return GroupIssuesByPassword(sorted_issues);
  } else {
    // Other types are all displayed in the same group without header.
    return @[ [[PasswordIssueGroup alloc] initWithHeaderText:nil
                                              passwordIssues:sorted_issues] ];
  }
}

// Computes the number of dimissed insecure credentials warnings.
// Only Compromissed credentials warnings can be dismissed, other warning types
// always return 0.
NSInteger GetDismissedWarningsCount(
    WarningType warning_type,
    const std::vector<CredentialUIEntry>& all_insecure_credentials) {
  if (warning_type == WarningType::kCompromisedPasswordsWarning) {
    return GetPasswordCountForWarningType(
        WarningType::kDismissedWarningsWarning, all_insecure_credentials);
  }

  return 0;
}

}  // namespace

@interface PasswordIssuesMediator () <PasswordCheckObserver,
                                      SavedPasswordsPresenterObserver> {
  WarningType _warningType;

  raw_ptr<IOSChromePasswordCheckManager> _manager;

  std::unique_ptr<PasswordCheckObserverBridge> _passwordCheckObserver;

  std::unique_ptr<SavedPasswordsPresenterObserverBridge>
      _passwordsPresenterObserver;

  // Last set of insecure credentials provided to the consumer. Used to avoid
  // updating the UI when changes in the insecure credentials happen but the
  // credentials provided to the consumer don't (e.g a new compromised
  // credential was detected but the consumer is displaying weak credentials).
  // A value of nullopt means the consumer hasn't been provided with credentials
  // yet.
  std::optional<std::vector<CredentialUIEntry>> _insecureCredentials;

  // Last number of dismissed warnings passed to the consumer.
  // Used to only update the consumer when the data it displays changed.
  NSInteger _dismissedWarningsCount;

  // 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 Password Issues Screen.
  __strong NSDate* _successfulReauthTime;

  // FaviconLoader is a keyed service that uses LargeIconService to retrieve
  // favicon images.
  raw_ptr<FaviconLoader> _faviconLoader;

  // Service to know whether passwords are synced.
  raw_ptr<syncer::SyncService> _syncService;
}

@end

@implementation PasswordIssuesMediator

- (instancetype)initForWarningType:(WarningType)warningType
              passwordCheckManager:(IOSChromePasswordCheckManager*)manager
                     faviconLoader:(FaviconLoader*)faviconLoader
                       syncService:(syncer::SyncService*)syncService {
  CHECK(manager);
  CHECK(syncService);
  CHECK_NE(warningType, WarningType::kNoInsecurePasswordsWarning);
  // `faviconLoader` might be null in tests.

  self = [super init];
  if (self) {
    _warningType = warningType;
    _syncService = syncService;
    _faviconLoader = faviconLoader;
    _manager = manager;
    _passwordCheckObserver =
        std::make_unique<PasswordCheckObserverBridge>(self, manager);
    _passwordsPresenterObserver =
        std::make_unique<SavedPasswordsPresenterObserverBridge>(
            self, _manager->GetSavedPasswordsPresenter());
  }
  return self;
}

- (void)disconnect {
  _passwordCheckObserver.reset();
  _passwordsPresenterObserver.reset();

  _manager = nullptr;
  _faviconLoader = nullptr;
  _syncService = nullptr;
}

- (void)setConsumer:(id<PasswordIssuesConsumer>)consumer {
  if (_consumer == consumer) {
    return;
  }
  _consumer = consumer;

  [self setConsumerHeader];

  [self providePasswordsToConsumer];
}

#pragma mark - PasswordCheckObserver

- (void)passwordCheckStateDidChange:(PasswordCheckState)state {
  // No-op.
}

- (void)insecureCredentialsDidChange {
  [self providePasswordsToConsumer];
}

- (void)passwordCheckManagerWillShutdown {
  _passwordCheckObserver.reset();
}

#pragma mark - SavedPasswordsPresenterObserver

- (void)savedPasswordsDidChange {
  [self providePasswordsToConsumer];
}

#pragma mark - Private Methods

- (void)providePasswordsToConsumer {
  DCHECK(self.consumer);

  std::vector<CredentialUIEntry> allInsecureCredentials =
      _manager->GetInsecureCredentials();

  std::vector<CredentialUIEntry> insecureCredentialsForWarningType =
      GetPasswordsForWarningType(_warningType, allInsecureCredentials);

  NSInteger dismissedWarningsCount =
      GetDismissedWarningsCount(_warningType, allInsecureCredentials);

  if (![self
          shouldUpdateConsumerWithInsecureCredentials:
              &insecureCredentialsForWarningType
                               dismissedWarningsCount:dismissedWarningsCount]) {
    return;
  }

  _insecureCredentials = insecureCredentialsForWarningType;
  _dismissedWarningsCount = dismissedWarningsCount;

  NSArray<PasswordIssueGroup*>* passwordIssueGroups =
      GetPasswordIssueGroups(_warningType, insecureCredentialsForWarningType);

  [self.consumer setPasswordIssues:passwordIssueGroups
            dismissedWarningsCount:dismissedWarningsCount];

  [self.consumer
      setNavigationBarTitle:[self
                                navigationBarTitleForNumberOfIssues:
                                    insecureCredentialsForWarningType.size()]];
}

// Computes the navigation bar title based on `_warningType` and number of
// issues.
- (NSString*)navigationBarTitleForNumberOfIssues:(long)numberOfIssues {
  switch (_warningType) {
    case WarningType::kWeakPasswordsWarning:
      return base::SysUTF16ToNSString(l10n_util::GetPluralStringFUTF16(
          IDS_IOS_WEAK_PASSWORD_ISSUES_TITLE, numberOfIssues));

    case WarningType::kCompromisedPasswordsWarning:
      return base::SysUTF16ToNSString(l10n_util::GetPluralStringFUTF16(
          IDS_IOS_COMPROMISED_PASSWORD_ISSUES_TITLE, numberOfIssues));

    case WarningType::kDismissedWarningsWarning:
      return l10n_util::GetNSString(
          IDS_IOS_DISMISSED_WARNINGS_PASSWORD_ISSUES_TITLE);

    case WarningType::kReusedPasswordsWarning:
      return l10n_util::GetNSStringF(IDS_IOS_REUSED_PASSWORD_ISSUES_TITLE,
                                     base::NumberToString16(numberOfIssues));

    case WarningType::kNoInsecurePasswordsWarning:
      NOTREACHED();
  }
}

- (void)setConsumerHeader {
  int headerTextID;
  std::optional<GURL> headerURL;

  switch (_warningType) {
    case WarningType::kWeakPasswordsWarning:
      headerTextID = IDS_IOS_WEAK_PASSWORD_ISSUES_DESCRIPTION;
      headerURL = GURL(
          password_manager::kPasswordManagerHelpCenterCreateStrongPasswordsURL);
      break;
    case WarningType::kCompromisedPasswordsWarning:
      headerTextID = IDS_IOS_COMPROMISED_PASSWORD_ISSUES_DESCRIPTION;
      headerURL = GURL(
          password_manager::kPasswordManagerHelpCenterChangeUnsafePasswordsURL);
      break;
    case WarningType::kReusedPasswordsWarning:
      headerTextID = IDS_IOS_REUSED_PASSWORD_ISSUES_DESCRIPTION;
      headerURL = std::nullopt;
      break;
    // Dismissed Warnings Page doesn't have a header.
    case WarningType::kDismissedWarningsWarning:
    case WarningType::kNoInsecurePasswordsWarning:
      // no-op
      return;
  }

  NSString* headerText = l10n_util::GetNSString(headerTextID);
  CrURL* localizedHeaderURL =
      headerURL.has_value()
          ? [[CrURL alloc] initWithGURL:google_util::AppendGoogleLocaleParam(
                                            headerURL.value(),
                                            GetApplicationContext()
                                                ->GetApplicationLocale())]
          : nil;

  [self.consumer setHeader:headerText URL:localizedHeaderURL];
}

// Whether the consumer should be updated after a change in insecure credentials
// or the number of dismissed compromised warnings.
- (BOOL)shouldUpdateConsumerWithInsecureCredentials:
            (std::vector<CredentialUIEntry>*)insecureCredentials
                             dismissedWarningsCount:
                                 (NSInteger)dismissedWarningsCount {
  // There's no need to update the UI when no changes occurred in the insecure
  // credentials for the warning type being displayed or the number of dismissed
  // compromised warnings.
  return dismissedWarningsCount != _dismissedWarningsCount ||
         !_insecureCredentials.has_value() ||
         _insecureCredentials.value() != *insecureCredentials;
}

#pragma mark SuccessfulReauthTimeAccessor

- (void)updateSuccessfulReauthTime {
  _successfulReauthTime = [[NSDate alloc] init];
}

- (NSDate*)lastSuccessfulReauthTime {
  return _successfulReauthTime;
}

#pragma mark - TableViewFaviconDataSource

- (void)faviconForPageURL:(CrURL*)URL
               completion:(void (^)(FaviconAttributes*))completion {
  BOOL fallbackToGoogleServer =
      password_manager_util::IsSavingPasswordsToAccountWithNormalEncryption(
          _syncService);
  _faviconLoader->FaviconForPageUrl(URL.gurl, kDesiredMediumFaviconSizePt,
                                    kMinFaviconSizePt, fallbackToGoogleServer,
                                    completion);
}

@end