// Copyright 2022 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_settings/password_settings_mediator.h"
#import "base/i18n/message_formatter.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/strings/sys_string_conversions.h"
#import "components/password_manager/core/browser/features/password_manager_features_util.h"
#import "components/password_manager/core/browser/password_manager_metrics_util.h"
#import "components/password_manager/core/browser/ui/credential_ui_entry.h"
#import "components/password_manager/core/common/password_manager_features.h"
#import "components/password_manager/core/common/password_manager_pref_names.h"
#import "components/prefs/pref_service.h"
#import "components/signin/public/identity_manager/objc/identity_manager_observer_bridge.h"
#import "components/sync/base/data_type.h"
#import "components/sync/base/passphrase_enums.h"
#import "components/sync/base/user_selectable_type.h"
#import "components/sync/service/sync_service_utils.h"
#import "components/sync/service/sync_user_settings.h"
#import "ios/chrome/browser/shared/model/prefs/pref_backed_boolean.h"
#import "ios/chrome/browser/shared/model/utils/observable_boolean.h"
#import "ios/chrome/browser/sync/model/sync_observer_bridge.h"
#import "ios/chrome/browser/ui/settings/password/password_exporter.h"
#import "ios/chrome/browser/ui/settings/password/saved_passwords_presenter_observer.h"
#import "ios/chrome/browser/ui/settings/utils/password_auto_fill_status_manager.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_protocol.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::prefs::kCredentialsEnableService;
namespace {
// The user action for when the bulk move passwords to account section button is
// clicked.
constexpr const char* kBulkMovePasswordsToAccountButtonClickedUserAction =
"Mobile.PasswordsSettings.BulkSavePasswordsToAccountButtonClicked";
// Returns true if the credential passed is not stored in the account store.
bool IsCredentialNotInAccountStore(const CredentialUIEntry& credential) {
return !credential.stored_in.contains(
password_manager::PasswordForm::Store::kAccountStore);
}
} // namespace
@interface PasswordSettingsMediator () <BooleanObserver,
IdentityManagerObserverBridgeDelegate,
PasswordAutoFillStatusObserver,
PasswordExporterDelegate,
SavedPasswordsPresenterObserver,
SyncObserverModelBridge> {
// A helper object for passing data about saved passwords from a finished
// password store request to the PasswordManagerViewController.
std::unique_ptr<SavedPasswordsPresenterObserverBridge>
_passwordsPresenterObserver;
// Service which gives us a view on users' saved passwords.
raw_ptr<password_manager::SavedPasswordsPresenter> _savedPasswordsPresenter;
// Allows reading and writing user preferences.
raw_ptr<PrefService> _prefService;
// The observable boolean that binds to the password manager setting state.
// Saved passwords are only on if the password manager is enabled.
PrefBackedBoolean* _passwordManagerEnabled;
// Provides status of Chrome as iOS AutoFill credential provider (i.e.,
// whether or not Chrome passwords can currently be used in other apps).
PasswordAutoFillStatusManager* _passwordAutoFillStatusManager;
// IdentityManager observer.
std::unique_ptr<signin::IdentityManagerObserverBridge>
_identityManagerObserver;
// Service providing information about sync status.
raw_ptr<syncer::SyncService> _syncService;
// Sync observer.
std::unique_ptr<SyncObserverBridge> _syncObserver;
}
// Helper object which maintains state about the "Export Passwords..." flow, and
// handles the actual serialization of the passwords.
@property(nonatomic, strong) PasswordExporter* passwordExporter;
@property(nonatomic, strong) id<BulkMoveLocalPasswordsToAccountHandler>
bulkMovePasswordsToAccountHandler;
// Delegate capable of showing alerts needed in the password export flow.
@property(nonatomic, weak) id<PasswordExportHandler> exportHandler;
// Whether or not there are any passwords saved.
@property(nonatomic, readwrite) BOOL hasSavedPasswords;
// Whether or not the password exporter is ready to be activated.
@property(nonatomic, readwrite) BOOL exporterIsReady;
@end
@implementation PasswordSettingsMediator
- (instancetype)
initWithReauthenticationModule:(id<ReauthenticationProtocol>)reauthModule
savedPasswordsPresenter:
(password_manager::SavedPasswordsPresenter*)passwordPresenter
bulkMovePasswordsToAccountHandler:
(id<BulkMoveLocalPasswordsToAccountHandler>)
bulkMovePasswordsToAccountHandler
exportHandler:(id<PasswordExportHandler>)exportHandler
prefService:(PrefService*)prefService
identityManager:(signin::IdentityManager*)identityManager
syncService:(syncer::SyncService*)syncService {
self = [super init];
if (self) {
_passwordExporter =
[[PasswordExporter alloc] initWithReauthenticationModule:reauthModule
delegate:self];
_savedPasswordsPresenter = passwordPresenter;
_passwordsPresenterObserver =
std::make_unique<SavedPasswordsPresenterObserverBridge>(
self, _savedPasswordsPresenter);
_savedPasswordsPresenter->Init();
_bulkMovePasswordsToAccountHandler = bulkMovePasswordsToAccountHandler;
_exportHandler = exportHandler;
_prefService = prefService;
_passwordManagerEnabled = [[PrefBackedBoolean alloc]
initWithPrefService:_prefService
prefName:kCredentialsEnableService];
_passwordManagerEnabled.observer = self;
_passwordAutoFillStatusManager =
[PasswordAutoFillStatusManager sharedManager];
[_passwordAutoFillStatusManager addObserver:self];
_identityManagerObserver =
std::make_unique<signin::IdentityManagerObserverBridge>(identityManager,
self);
_syncService = syncService;
_syncObserver = std::make_unique<SyncObserverBridge>(self, syncService);
}
return self;
}
- (void)setConsumer:(id<PasswordSettingsConsumer>)consumer {
_consumer = consumer;
// Now that the consumer is set, ensure that the consumer starts out with the
// correct initial value for `canExportPasswords` or else the export button
// will not behave correctly on load.
self.exporterIsReady = self.passwordExporter.exportState == ExportState::IDLE;
[self savedPasswordsDidChange];
[self.consumer setSavePasswordsEnabled:_passwordManagerEnabled.value];
[self.consumer setSignedInAccount:base::SysUTF8ToNSString(
_syncService->GetAccountInfo().email)];
// TODO(crbug.com/40131118): In addition to setting this value here, we should
// observe for changes (i.e., if policy changes while the screen is open) and
// push that to the consumer.
[self.consumer setManagedByPolicy:_prefService->IsManagedPreference(
kCredentialsEnableService)];
[self passwordAutoFillStatusDidChange];
[self.consumer setOnDeviceEncryptionState:[self onDeviceEncryptionState]];
[self updateShowBulkMovePasswordsToAccount];
}
- (void)userDidStartBulkMoveLocalPasswordsToAccountFlow {
int localPasswordsCount = [self computeLocalPasswordsCount];
_syncService->TriggerLocalDataMigration(
syncer::DataTypeSet{syncer::DataType::PASSWORDS});
// TODO(crbug.com/40281800): Remove this histogram enumeration when using
// `MoveCredentialsToAccount`.
base::UmaHistogramEnumeration(
"PasswordManager.AccountStorage.MoveToAccountStoreFlowAccepted2",
password_manager::metrics_util::MoveToAccountStoreTrigger::
kExplicitlyTriggeredForMultiplePasswordsInSettings);
base::UmaHistogramCounts100(
"IOS.PasswordManager.BulkSavePasswordsInAccountCount",
localPasswordsCount);
[self showMovedToAccountSnackbarWithPasswordCount:localPasswordsCount];
}
- (void)userDidStartExportFlow {
// Use GetSavedCredentials, rather than GetSavedPasswords, because the latter
// can return duplicate passwords that shouldn't be included in the export.
// However, this method also returns blocked sites ("Never save for
// example.com"), so those must be filtered before passing to the exporter.
std::vector<CredentialUIEntry> passwords =
_savedPasswordsPresenter->GetSavedCredentials();
std::erase_if(passwords, [](const auto& credential) {
return credential.blocked_by_user;
});
[self.passwordExporter startExportFlow:passwords];
}
- (void)userDidCompleteExportFlow {
[self.passwordExporter resetExportState];
}
- (void)exportFlowCanceled {
[self.passwordExporter cancelExport];
}
- (void)disconnect {
DCHECK(_savedPasswordsPresenter);
DCHECK(_passwordsPresenterObserver);
_savedPasswordsPresenter->RemoveObserver(_passwordsPresenterObserver.get());
_passwordsPresenterObserver.reset();
[[PasswordAutoFillStatusManager sharedManager] removeObserver:self];
[_passwordManagerEnabled stop];
_identityManagerObserver.reset();
_syncObserver.reset();
}
#pragma mark - PasswordExporterDelegate
- (void)showActivityViewWithActivityItems:(NSArray*)activityItems
completionHandler:
(void (^)(NSString*, BOOL, NSArray*, NSError*))
completionHandler {
[self.exportHandler showActivityViewWithActivityItems:activityItems
completionHandler:completionHandler];
}
- (void)showExportErrorAlertWithLocalizedReason:(NSString*)errorReason {
[self.exportHandler showExportErrorAlertWithLocalizedReason:errorReason];
}
- (void)showPreparingPasswordsAlert {
[self.exportHandler showPreparingPasswordsAlert];
}
- (void)showSetPasscodeForPasswordExportDialog {
[self.exportHandler showSetPasscodeForPasswordExportDialog];
}
- (void)updateExportPasswordsButton {
// This is invoked by the exporter when its state changes, so we have to
// re-read that state before pushing to the consumer.
self.exporterIsReady = self.passwordExporter.exportState == ExportState::IDLE;
[self pushExportStateToConsumerAndUpdate];
}
#pragma mark - PasswordSettingsDelegate
- (void)bulkMovePasswordsToAccountButtonClicked {
base::RecordAction(base::UserMetricsAction(
kBulkMovePasswordsToAccountButtonClickedUserAction));
// Create the confirmation dialog title.
NSString* alertTitle = l10n_util::GetPluralNSStringF(
IDS_IOS_PASSWORD_SETTINGS_BULK_UPLOAD_PASSWORDS_ALERT_TITLE,
[self computeLocalPasswordsCount]);
// Create the confirmation dialog description.
NSMutableArray<NSString*>* distinctDomains =
[self computeDistinctDomainsFromLocalPasswords];
std::u16string pattern = l10n_util::GetStringUTF16(
IDS_IOS_PASSWORD_SETTINGS_BULK_UPLOAD_PASSWORDS_ALERT_DESCRIPTION);
std::u16string result = base::i18n::MessageFormatter::FormatWithNamedArgs(
pattern, "COUNT", (int)[distinctDomains count], "DOMAIN_ONE",
[distinctDomains count] >= 1
? base::SysNSStringToUTF16(distinctDomains[0])
: base::SysNSStringToUTF16(@""),
"DOMAIN_TWO",
[distinctDomains count] >= 2
? base::SysNSStringToUTF16(distinctDomains[1])
: base::SysNSStringToUTF16(@""),
"OTHER_DOMAINS_COUNT", (int)([distinctDomains count] - 2), "EMAIL",
_syncService->GetAccountInfo().email);
NSString* alertDescription = base::SysUTF16ToNSString(result);
// Create and show the confirmation dialog.
[self.bulkMovePasswordsToAccountHandler
showConfirmationDialogWithAlertTitle:alertTitle
alertDescription:alertDescription];
}
- (void)savedPasswordSwitchDidChange:(BOOL)enabled {
_passwordManagerEnabled.value = enabled;
}
#pragma mark - SavedPasswordsPresenterObserver
- (void)savedPasswordsDidChange {
self.hasSavedPasswords =
!_savedPasswordsPresenter->GetSavedPasswords().empty();
[self pushExportStateToConsumerAndUpdate];
[self updateShowBulkMovePasswordsToAccount];
}
#pragma mark - BooleanObserver
- (void)booleanDidChange:(id<ObservableBoolean>)observableBoolean {
DCHECK(observableBoolean == _passwordManagerEnabled);
[self.consumer setSavePasswordsEnabled:observableBoolean.value];
}
#pragma mark - PasswordAutoFillStatusObserver
- (void)passwordAutoFillStatusDidChange {
if (_passwordAutoFillStatusManager.ready) {
[self.consumer setPasswordsInOtherAppsEnabled:_passwordAutoFillStatusManager
.autoFillEnabled];
}
}
#pragma mark - IdentityManagerObserverBridgeDelegate
- (void)onPrimaryAccountChanged:
(const signin::PrimaryAccountChangeEvent&)event {
[self.consumer setOnDeviceEncryptionState:[self onDeviceEncryptionState]];
}
#pragma mark - SyncObserverModelBridge
- (void)onSyncStateChanged {
[self.consumer setOnDeviceEncryptionState:[self onDeviceEncryptionState]];
[self.consumer setSignedInAccount:base::SysUTF8ToNSString(
_syncService->GetAccountInfo().email)];
[self updateShowBulkMovePasswordsToAccount];
}
#pragma mark - Private
// Returns the on-device encryption state according to the sync service.
- (PasswordSettingsOnDeviceEncryptionState)onDeviceEncryptionState {
if (ShouldOfferTrustedVaultOptIn(_syncService)) {
return PasswordSettingsOnDeviceEncryptionStateOfferOptIn;
}
if (_syncService->GetUserSettings()->GetPassphraseType() ==
syncer::PassphraseType::kTrustedVaultPassphrase) {
return PasswordSettingsOnDeviceEncryptionStateOptedIn;
}
return PasswordSettingsOnDeviceEncryptionStateNotShown;
}
// Pushes the current state of the exporter to the consumer and updates its
// export passwords button.
- (void)pushExportStateToConsumerAndUpdate {
[self.consumer
setCanExportPasswords:self.hasSavedPasswords && self.exporterIsReady];
[self.consumer updateExportPasswordsButton];
}
// Computes the amount of local passwords and passes that on to the consumer.
- (void)updateShowBulkMovePasswordsToAccount {
[self.consumer setLocalPasswordsCount:[self computeLocalPasswordsCount]
withUserEligibility:password_manager::features_util::
IsOptedInForAccountStorage(
_prefService, _syncService)];
}
// Returns the amount of local passwords.
- (int)computeLocalPasswordsCount {
std::vector<password_manager::AffiliatedGroup> affiliatedGroups =
_savedPasswordsPresenter->GetAffiliatedGroups();
// Count passwords that don't appear in the account store.
int passwordsCount = 0;
for (password_manager::AffiliatedGroup group : affiliatedGroups) {
passwordsCount += base::ranges::count_if(group.GetCredentials().begin(),
group.GetCredentials().end(),
IsCredentialNotInAccountStore);
}
return passwordsCount;
}
// Returns the list of distinct domains present in the local passwords. If they
// are in different affiliated groups, they are presumed to be distinct.
- (NSMutableArray<NSString*>*)computeDistinctDomainsFromLocalPasswords {
std::vector<password_manager::AffiliatedGroup> affiliatedGroups =
_savedPasswordsPresenter->GetAffiliatedGroups();
// Add distinct domains for which there exists a password that doesn't appear
// in the account store.
NSMutableArray<NSString*>* distinctDomains = [NSMutableArray array];
for (const password_manager::AffiliatedGroup& group : affiliatedGroups) {
auto credential = base::ranges::find_if(group.GetCredentials().begin(),
group.GetCredentials().end(),
IsCredentialNotInAccountStore);
// If a credential exists in this group that is in the profile store, append
// the group's display name to the distinct domains.
if (credential != group.GetCredentials().end()) {
[distinctDomains
addObject:[NSString
stringWithUTF8String:group.GetDisplayName().c_str()]];
}
}
return distinctDomains;
}
// Shows the snackbar indicating to the user that their local passwords have
// been saved to their account.
- (void)showMovedToAccountSnackbarWithPasswordCount:(int)count {
[self.bulkMovePasswordsToAccountHandler
showMovedToAccountSnackbarWithPasswordCount:count
userEmail:_syncService->GetAccountInfo()
.email];
}
@end