chromium/ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_password_mediator.mm

// Copyright 2018 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/autofill/ui_bundled/manual_fill/manual_fill_password_mediator.h"

#import <vector>

#import "base/i18n/message_formatter.h"
#import "base/metrics/user_metrics.h"
#import "base/strings/sys_string_conversions.h"
#import "components/autofill/ios/browser/autofill_util.h"
#import "components/autofill/ios/browser/form_suggestion.h"
#import "components/autofill/ios/form_util/form_activity_observer_bridge.h"
#import "components/autofill/ios/form_util/form_activity_params.h"
#import "components/password_manager/core/browser/form_fetcher_impl.h"
#import "components/password_manager/core/browser/password_manager_client.h"
#import "components/password_manager/core/browser/ui/credential_ui_entry.h"
#import "components/password_manager/core/browser/ui/saved_passwords_presenter.h"
#import "components/sync/base/data_type.h"
#import "components/sync/service/sync_service.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/form_fetcher_consumer_bridge.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_action_cell.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_constants.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_content_injector.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_credential+PasswordForm.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_credential.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_password_cell.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/password_consumer.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/password_list_navigator.h"
#import "ios/chrome/browser/favicon/model/favicon_loader.h"
#import "ios/chrome/browser/net/model/crurl.h"
#import "ios/chrome/browser/passwords/model/password_counter_delegate_bridge.h"
#import "ios/chrome/browser/passwords/model/password_tab_helper.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/list_model/list_model.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_model.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/menu/browser_action_factory.h"
#import "ios/chrome/browser/ui/settings/password/saved_passwords_presenter_observer.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/web_state_observer_bridge.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "ui/gfx/favicon_size.h"
#import "url/gurl.h"

using password_manager::CredentialUIEntry;
using password_manager::PasswordForm;

// Checks if two credential are connected. They are considered connected if they
// have the same host.
BOOL AreCredentialsAtIndicesConnected(
    NSArray<ManualFillCredential*>* credentials,
    int first_index,
    int second_index) {
  CHECK(!IsKeyboardAccessoryUpgradeEnabled());
  if (first_index < 0 || first_index >= (int)credentials.count ||
      second_index < 0 || second_index >= (int)credentials.count) {
    return NO;
  }

  return [credentials[first_index].host
      isEqualToString:credentials[second_index].host];
}

@interface ManualFillPasswordMediator () <CRWWebStateObserver,
                                          FormActivityObserver,
                                          FormFetcherConsumer,
                                          ManualFillContentInjector,
                                          PasswordCounterObserver,
                                          SavedPasswordsPresenterObserver>

// The favicon loader used in TableViewFaviconDataSource.
@property(nonatomic, assign) FaviconLoader* faviconLoader;

// A cache of the saved credentials to present.
@property(nonatomic, strong) NSArray<ManualFillCredential*>* credentials;

// A cache of the password forms used to create ManualFillCredentials and
// ManualFillCredentialItems.
@property(nonatomic, assign) std::vector<PasswordForm> passwordForms;

// YES if passwords were fetched at least once.
@property(nonatomic, assign) BOOL passwordsWereFetched;

// YES if the active field is obfuscated.
@property(nonatomic, assign) BOOL activeFieldIsObfuscated;

// The relevant active web state.
@property(nonatomic, assign) web::WebState* webState;

// Sync service.
@property(nonatomic, assign) syncer::SyncService* syncService;

@end

@implementation ManualFillPasswordMediator {
  // Bridge to observe the web state from Objective-C.
  std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge;

  // Bridge to observe form activity in `_webState`.
  std::unique_ptr<autofill::FormActivityObserverBridge>
      _formActivityObserverBridge;

  // Origin to fetch passwords for.
  GURL _URL;

  // Service which gives us a view on users' saved passwords. Only set this
  // property if there's a need to fetch all of the user's saved passwords.
  raw_ptr<password_manager::SavedPasswordsPresenter> _savedPasswordsPresenter;

  // Bridge to observe changes in saved passwords. This variable will only be
  // initialized if the `_savedPasswordsPresenter` variable is set.
  std::unique_ptr<SavedPasswordsPresenterObserverBridge>
      _savedPasswordsPresenterObserver;

  // Bridge to observe the number of passwords in the password stores.
  std::unique_ptr<PasswordCounterDelegateBridge> _passwordCounter;

  // Service that fetches passwords for the current domain. This variable is
  // lazily initialized the first time passwords associated with the current
  // origin are requested (see `fetchPasswordsForOrigin`).
  std::unique_ptr<password_manager::FormFetcher> _formFetcher;

  // Bridge to get notified when the passwords associated with the current site
  // have been fetched by the `_formFetcher`.
  std::unique_ptr<FormFetcherConsumerBridge> _formFetcherConsumer;

  // Whether or not the user has passwords saved in the password stores.
  BOOL _hasSavedPasswords;

  // Indicates whether to show the autofill button for the items.
  BOOL _showAutofillFormButton;
}

- (instancetype)initWithFaviconLoader:(FaviconLoader*)faviconLoader
                             webState:(web::WebState*)webState
                          syncService:(syncer::SyncService*)syncService
                                  URL:(const GURL&)URL
             invokedOnObfuscatedField:(BOOL)invokedOnObfuscatedField
                 profilePasswordStore:
                     (scoped_refptr<password_manager::PasswordStoreInterface>)
                         profilePasswordStore
                 accountPasswordStore:
                     (scoped_refptr<password_manager::PasswordStoreInterface>)
                         accountPasswordStore
               showAutofillFormButton:(BOOL)showAutofillFormButton {
  self = [super init];
  if (self) {
    _credentials = @[];
    _passwordForms = {};
    _faviconLoader = faviconLoader;
    _webState = webState;
    _syncService = syncService;
    _URL = URL;
    _activeFieldIsObfuscated = invokedOnObfuscatedField;
    _webStateObserverBridge =
        std::make_unique<web::WebStateObserverBridge>(self);
    _webState->AddObserver(_webStateObserverBridge.get());
    _formActivityObserverBridge =
        std::make_unique<autofill::FormActivityObserverBridge>(_webState, self);
    _showAutofillFormButton = showAutofillFormButton;

    // A valid `profilePasswordStore` is needed to observe PasswordCounter.
    if (IsKeyboardAccessoryUpgradeEnabled() && profilePasswordStore) {
      _passwordCounter = std::make_unique<PasswordCounterDelegateBridge>(
          self, profilePasswordStore.get(), accountPasswordStore.get());
    }
  }
  return self;
}

- (void)dealloc {
  [self disconnect];
}

- (void)disconnect {
  if (_webState) {
    [self webStateDestroyed:_webState];
  }
  if (_savedPasswordsPresenter) {
    _savedPasswordsPresenter->RemoveObserver(
        _savedPasswordsPresenterObserver.get());
    _savedPasswordsPresenterObserver.reset();
    _savedPasswordsPresenter = nullptr;
  }
  if (_passwordCounter) {
    _passwordCounter.reset();
  }
  if (_formFetcher) {
    _formFetcher->RemoveConsumer(_formFetcherConsumer.get());
    _formFetcherConsumer.reset();
    _formFetcher = nullptr;
  }
}

- (void)fetchPasswordsForOrigin {
  if (!_formFetcher) {
    [self setFormFetcher:[self createFormFetcher]];
  }

  _formFetcher->Fetch();
}

- (void)fetchAllPasswords {
  CHECK(_savedPasswordsPresenter);

  std::vector<CredentialUIEntry> savedCredentials =
      _savedPasswordsPresenter->GetSavedCredentials();
  self.passwordForms = [self passwordFormsFromCredentials:savedCredentials];
  self.credentials =
      [self createManualFillCredentialsFromPasswordForms:self.passwordForms];
  self.passwordsWereFetched = YES;
  [self postDataToConsumer];
}

#pragma mark - UISearchResultsUpdating

- (void)updateSearchResultsForSearchController:
    (UISearchController*)searchController {
  NSString* searchText = searchController.searchBar.text;
  if (!searchText.length) {
    NSArray<ManualFillCredentialItem*>* credentialItems =
        [self createItemsForCredentials:self.credentials
                      withPasswordForms:self.passwordForms];
    [self.consumer presentCredentials:credentialItems];
    return;
  }

  NSPredicate* predicate = [NSPredicate
      predicateWithFormat:@"host CONTAINS[cd] %@ OR username CONTAINS[cd] %@",
                          searchText, searchText];
  NSArray* filteredCredentials =
      [self.credentials filteredArrayUsingPredicate:predicate];
  NSArray<ManualFillCredentialItem*>* credentialItems =
      [self createItemsForCredentials:filteredCredentials
                    withPasswordForms:self.passwordForms];
  [self.consumer presentCredentials:credentialItems];
}

#pragma mark - Private

- (void)postDataToConsumer {
  // To avoid duplicating the metric tracking how many passwords are sent to the
  // consumer, only post credentials if at least the first fetch is done. Or
  // else there will be spam metrics with 0 passwords everytime the screen is
  // open.
  if (self.passwordsWereFetched) {
    [self postCredentialsToConsumer];
    [self postActionsToConsumer];
  }
}

// Posts the credentials to the consumer. If filtered is `YES` it only post the
// ones associated with the active web state.
- (void)postCredentialsToConsumer {
  if (!self.consumer) {
    return;
  }
  NSArray<ManualFillCredentialItem*>* credentials =
      [self createItemsForCredentials:self.credentials
                    withPasswordForms:self.passwordForms];
  [self.consumer presentCredentials:credentials];
}

// Creates a table view model with the passed credentials.
- (NSArray<ManualFillCredentialItem*>*)
    createItemsForCredentials:(NSArray<ManualFillCredential*>*)credentials
            withPasswordForms:(const std::vector<PasswordForm>&)passwordForms {
  int credentialCount = (int)credentials.count;
  NSMutableArray* items =
      [[NSMutableArray alloc] initWithCapacity:credentialCount];
  for (int i = 0; i < credentialCount; i++) {
    // Credentials from the same affiliated group are never connected when the
    // Keyboard Accessory Upgrade feature is enabled.
    BOOL isConnectedToPreviousItem =
        IsKeyboardAccessoryUpgradeEnabled()
            ? NO
            : AreCredentialsAtIndicesConnected(credentials, i, i - 1);
    BOOL isConnectedToNextItem =
        IsKeyboardAccessoryUpgradeEnabled()
            ? NO
            : AreCredentialsAtIndicesConnected(credentials, i, i + 1);

    NSArray<UIAction*>* menuActions =
        IsKeyboardAccessoryUpgradeEnabled()
            ? @[ [self createMenuEditActionForPassword:passwordForms[i]] ]
            : @[];

    NSString* cellIndexAccessibilityLabel = base::SysUTF16ToNSString(
        base::i18n::MessageFormatter::FormatWithNamedArgs(
            l10n_util::GetStringUTF16(
                IDS_IOS_MANUAL_FALLBACK_PASSWORD_CELL_INDEX),
            "count", credentialCount, "position", i + 1));

    ManualFillCredentialItem* item = [[ManualFillCredentialItem alloc]
                 initWithCredential:credentials[i]
          isConnectedToPreviousItem:isConnectedToPreviousItem
              isConnectedToNextItem:isConnectedToNextItem
                    contentInjector:self
                        menuActions:menuActions
                          cellIndex:i
        cellIndexAccessibilityLabel:cellIndexAccessibilityLabel
             showAutofillFormButton:_showAutofillFormButton
            fromAllPasswordsContext:[self isFromAllPasswordsContext]];
    [items addObject:item];
  }
  return items;
}

- (void)postActionsToConsumer {
  if (!self.consumer) {
    return;
  }
  if (self.isActionSectionEnabled) {
    NSMutableArray<ManualFillActionItem*>* actions =
        [[NSMutableArray alloc] init];
    __weak __typeof(self) weakSelf = self;

    password_manager::PasswordManagerClient* passwordManagerClient =
        _webState ? PasswordTabHelper::FromWebState(_webState)
                        ->GetPasswordManagerClient()
                  : nullptr;
    if (_syncService &&
        _syncService->GetActiveDataTypes().Has(syncer::PASSWORDS) &&
        passwordManagerClient &&
        passwordManagerClient->IsSavingAndFillingEnabled(_URL) &&
        _activeFieldIsObfuscated) {
      NSString* suggestPasswordTitleString = l10n_util::GetNSString(
          IDS_IOS_MANUAL_FALLBACK_SUGGEST_STRONG_PASSWORD_WITH_DOTS);
      ManualFillActionItem* suggestPasswordItem = [[ManualFillActionItem alloc]
          initWithTitle:suggestPasswordTitleString
                 action:^{
                   base::RecordAction(base::UserMetricsAction(
                       "ManualFallback_Password_OpenSuggestPassword"));
                   [weakSelf.navigator openPasswordSuggestion];
                 }];
      suggestPasswordItem.accessibilityIdentifier =
          manual_fill::kSuggestPasswordAccessibilityIdentifier;
      [actions addObject:suggestPasswordItem];
    }

    if (!IsKeyboardAccessoryUpgradeEnabled() ||
        (IsKeyboardAccessoryUpgradeEnabled() && _hasSavedPasswords)) {
      NSString* otherPasswordsTitleString = l10n_util::GetNSString(
          IDS_IOS_MANUAL_FALLBACK_SELECT_PASSWORD_WITH_DOTS);
      ManualFillActionItem* otherPasswordsItem = [[ManualFillActionItem alloc]
          initWithTitle:otherPasswordsTitleString
                 action:^{
                   base::RecordAction(base::UserMetricsAction(
                       "ManualFallback_Password_OpenOtherPassword"));
                   [weakSelf.navigator openAllPasswordsList];
                 }];
      otherPasswordsItem.accessibilityIdentifier =
          manual_fill::kOtherPasswordsAccessibilityIdentifier;
      [actions addObject:otherPasswordsItem];
    }

    // "Manage Passwords..." is available in both configurations.
    NSString* managePasswordsTitle =
        l10n_util::GetNSString(IDS_IOS_MANUAL_FALLBACK_MANAGE_PASSWORDS);
    ManualFillActionItem* managePasswordsItem = [[ManualFillActionItem alloc]
        initWithTitle:managePasswordsTitle
               action:^{
                 base::RecordAction(base::UserMetricsAction(
                     "ManualFallback_Password_OpenManagePassword"));
                 [weakSelf.navigator openPasswordManager];
               }];
    managePasswordsItem.accessibilityIdentifier =
        manual_fill::kManagePasswordsAccessibilityIdentifier;
    [actions addObject:managePasswordsItem];

    NSString* manageSettingsTitle =
        l10n_util::GetNSString(IDS_IOS_MANUAL_FALLBACK_MANAGE_SETTINGS);
    ManualFillActionItem* manageSettingsItem = [[ManualFillActionItem alloc]
        initWithTitle:manageSettingsTitle
               action:^{
                 base::RecordAction(base::UserMetricsAction(
                     "ManualFallback_Password_OpenManageSettings"));
                 [weakSelf.navigator openPasswordSettings];
               }];
    manageSettingsItem.accessibilityIdentifier =
        manual_fill::kManageSettingsAccessibilityIdentifier;

    [actions addObject:manageSettingsItem];

    [self.consumer presentActions:actions];
  }
}

// Fetches all Password Forms related to the given list of saved credentials.
- (std::vector<PasswordForm>)passwordFormsFromCredentials:
    (const std::vector<password_manager::CredentialUIEntry>&)credentials {
  std::vector<PasswordForm> passwordforms;
  passwordforms.reserve(credentials.size());

  for (const auto& credential : credentials) {
    std::vector<PasswordForm> correspondingPasswordForms =
        _savedPasswordsPresenter->GetCorrespondingPasswordForms(credential);
    passwordforms.insert(passwordforms.end(),
                         correspondingPasswordForms.begin(),
                         correspondingPasswordForms.end());
  }
  return passwordforms;
}

// Creates and returns a list of manual fill credentials built off of a list of
// password forms.
- (NSMutableArray<ManualFillCredential*>*)
    createManualFillCredentialsFromPasswordForms:
        (const std::vector<PasswordForm>&)passwordForms {
  NSMutableArray<ManualFillCredential*>* manualFillCredentials =
      [[NSMutableArray alloc] initWithCapacity:passwordForms.size()];
  for (const auto& passwordForm : passwordForms) {
    ManualFillCredential* manualFillCredential =
        [[ManualFillCredential alloc] initWithPasswordForm:passwordForm];
    [manualFillCredentials addObject:manualFillCredential];
  }

  return manualFillCredentials;
}

// Creates and configures a form fetcher.
- (std::unique_ptr<password_manager::FormFetcherImpl>)createFormFetcher {
  password_manager::PasswordFormDigest formDigest(
      password_manager::PasswordForm::Scheme::kHtml,
      password_manager::GetSignonRealm(_URL), _URL);

  PasswordTabHelper* tabHelper = PasswordTabHelper::FromWebState(_webState);
  if (!tabHelper) {
    return nullptr;
  }

  password_manager::PasswordManagerClient* passwordManagerClient =
      tabHelper->GetPasswordManagerClient();

  std::unique_ptr<password_manager::FormFetcherImpl> formFetcher =
      std::make_unique<password_manager::FormFetcherImpl>(
          formDigest, passwordManagerClient,
          /*should_migrate_http_passwords=*/false);
  formFetcher->set_filter_grouped_credentials(false);

  return formFetcher;
}

// Creates a UIAction to edit a password from a UIMenu.
- (UIAction*)createMenuEditActionForPassword:(PasswordForm)password {
  MenuScenarioHistogram menuScenario =
      [self isFromAllPasswordsContext]
          ? kMenuScenarioHistogramAutofillManualFallbackAllPasswordsEntry
          : kMenuScenarioHistogramAutofillManualFallbackPasswordEntry;
  ActionFactory* actionFactory =
      [[ActionFactory alloc] initWithScenario:menuScenario];

  __weak __typeof(self) weakSelf = self;
  UIAction* editAction = [actionFactory actionToEditWithBlock:^{
    [weakSelf openPasswordDetailsInEditMode:CredentialUIEntry(password)];
  }];

  return editAction;
}

// Requests the appropriate delegate to open the details of the given credential
// in edit mode.
- (void)openPasswordDetailsInEditMode:(CredentialUIEntry)credential {
  BOOL fromAllPasswordContext = [self isFromAllPasswordsContext];
  if (fromAllPasswordContext) {
    base::RecordAction(base::UserMetricsAction(
        "ManualFallback_OtherPasswords_OverflowMenu_Edit"));
    [self.delegate manualFillPasswordMediator:self
        didTriggerOpenPasswordDetailsInEditMode:credential];
  } else {
    base::RecordAction(
        base::UserMetricsAction("ManualFallback_Password_OverflowMenu_Edit"));
    [self.navigator openPasswordDetailsInEditModeForCredential:credential];
  }
}

- (BOOL)isFromAllPasswordsContext {
  return !self.isActionSectionEnabled;
}

#pragma mark - Setters

- (void)setConsumer:(id<ManualFillPasswordConsumer>)consumer {
  if (consumer == _consumer) {
    return;
  }
  _consumer = consumer;
  [self postDataToConsumer];
}

- (void)setSavedPasswordsPresenter:
    (password_manager::SavedPasswordsPresenter*)savedPasswordsPresenter {
  if (_savedPasswordsPresenter == savedPasswordsPresenter) {
    return;
  }

  _savedPasswordsPresenter = savedPasswordsPresenter;
  _savedPasswordsPresenterObserver =
      std::make_unique<SavedPasswordsPresenterObserverBridge>(
          self, _savedPasswordsPresenter);
}

// Sets `_formFetcher` and its consumer `_formFetcherConsumer`.
- (void)setFormFetcher:
    (std::unique_ptr<password_manager::FormFetcher>)formFetcher {
  _formFetcher = std::move(formFetcher);
  _formFetcherConsumer =
      std::make_unique<FormFetcherConsumerBridge>(self, _formFetcher.get());
}

#pragma mark - ManualFillContentInjector

- (BOOL)canUserInjectInPasswordField:(BOOL)passwordField
                       requiresHTTPS:(BOOL)requiresHTTPS {
  return [self.contentInjector canUserInjectInPasswordField:passwordField
                                              requiresHTTPS:requiresHTTPS];
}

- (void)userDidPickContent:(NSString*)content
             passwordField:(BOOL)passwordField
             requiresHTTPS:(BOOL)requiresHTTPS {
  [self.delegate manualFillPasswordMediatorWillInjectContent:self];
  [self.contentInjector userDidPickContent:content
                             passwordField:passwordField
                             requiresHTTPS:requiresHTTPS];
}

- (void)autofillFormWithCredential:(ManualFillCredential*)credential
                      shouldReauth:(BOOL)shouldReauth {
  [self.delegate manualFillPasswordMediatorWillInjectContent:self];
  [self.contentInjector autofillFormWithCredential:credential
                                      shouldReauth:shouldReauth];
}

- (void)autofillFormWithSuggestion:(FormSuggestion*)formSuggestion
                           atIndex:(NSInteger)index {
  [self.delegate manualFillPasswordMediatorWillInjectContent:self];
  [self.contentInjector autofillFormWithSuggestion:formSuggestion
                                           atIndex:index];
}

- (BOOL)isActiveFormAPasswordForm {
  return [self.contentInjector isActiveFormAPasswordForm];
}

#pragma mark - TableViewFaviconDataSource

- (void)faviconForPageURL:(CrURL*)URL
               completion:(void (^)(FaviconAttributes*))completion {
  DCHECK(completion);
  self.faviconLoader->FaviconForPageUrlOrHost(URL.gurl, gfx::kFaviconSize,
                                              completion);
}

#pragma mark - FormActivityObserver

- (void)webState:(web::WebState*)webState
    didRegisterFormActivity:(const autofill::FormActivityParams&)params
                    inFrame:(web::WebFrame*)frame {
  DCHECK_EQ(_webState, webState);
  if (_activeFieldIsObfuscated !=
      (params.field_type == autofill::kObfuscatedFieldType)) {
    _activeFieldIsObfuscated =
        params.field_type == autofill::kObfuscatedFieldType;
    [self postActionsToConsumer];
  }
}

#pragma mark - CRWWebStateObserver

- (void)webStateDestroyed:(web::WebState*)webState {
  DCHECK_EQ(_webState, webState);
  if (_webState) {
    if (_webStateObserverBridge) {
      _webState->RemoveObserver(_webStateObserverBridge.get());
    }
    _webState = nullptr;
  }
  _webStateObserverBridge.reset();
  _formActivityObserverBridge.reset();
}

#pragma mark - SavedPasswordsPresenterBridge

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

#pragma mark - PasswordCounterObserver

- (void)passwordCounterChanged:(size_t)totalPasswords {
  if (_hasSavedPasswords == (totalPasswords > 0)) {
    return;
  }

  _hasSavedPasswords = totalPasswords;
  [self postActionsToConsumer];
}

#pragma mark - FormFetcherConsumer

- (void)fetchDidComplete {
  // Fetch the passwords.
  const base::span<const password_manager::PasswordForm> passwordForms =
      _formFetcher->GetBestMatches();

  self.passwordForms =
      std::vector<PasswordForm>(passwordForms.begin(), passwordForms.end());
  self.credentials =
      [self createManualFillCredentialsFromPasswordForms:self.passwordForms];

  // Pass the passwords to the consumer.
  self.passwordsWereFetched = YES;
  [self postDataToConsumer];
}

@end