// 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/passwords/model/password_controller.h"
#import <stddef.h>
#import <algorithm>
#import <map>
#import <memory>
#import <optional>
#import <string>
#import <utility>
#import <vector>
#import "base/apple/foundation_util.h"
#import "base/functional/bind.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/histogram_functions.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/time/time.h"
#import "base/timer/timer.h"
#import "base/values.h"
#import "components/autofill/core/browser/ui/suggestion_type.h"
#import "components/autofill/core/common/autofill_features.h"
#import "components/autofill/core/common/form_data.h"
#import "components/autofill/core/common/password_form_fill_data.h"
#import "components/autofill/core/common/password_form_generation_data.h"
#import "components/autofill/core/common/signatures.h"
#import "components/autofill/core/common/unique_ids.h"
#import "components/autofill/ios/browser/autofill_util.h"
#import "components/autofill/ios/form_util/form_activity_observer_bridge.h"
#import "components/autofill/ios/form_util/form_activity_params.h"
#import "components/infobars/core/infobar_manager.h"
#import "components/password_manager/core/browser/features/password_manager_features_util.h"
#import "components/password_manager/core/browser/password_bubble_experiment.h"
#import "components/password_manager/core/browser/password_form.h"
#import "components/password_manager/core/browser/password_form_manager_for_ui.h"
#import "components/password_manager/core/browser/password_generation_frame_helper.h"
#import "components/password_manager/core/browser/password_manager.h"
#import "components/password_manager/core/browser/password_manager_client.h"
#import "components/password_manager/core/browser/password_manager_metrics_util.h"
#import "components/password_manager/core/browser/password_sync_util.h"
#import "components/password_manager/core/common/password_manager_features.h"
#import "components/password_manager/core/common/password_manager_pref_names.h"
#import "components/password_manager/ios/account_select_fill_data.h"
#import "components/password_manager/ios/password_controller_driver_helper.h"
#import "components/password_manager/ios/password_form_helper.h"
#import "components/password_manager/ios/password_suggestion_helper.h"
#import "components/password_manager/ios/shared_password_controller.h"
#import "components/safe_browsing/core/browser/password_protection/password_reuse_detection_manager_client.h"
#import "components/strings/grit/components_strings.h"
#import "components/sync/service/sync_service.h"
#import "components/ukm/ios/ukm_url_recorder.h"
#import "ios/chrome/browser/autofill/model/bottom_sheet/autofill_bottom_sheet_tab_helper.h"
#import "ios/chrome/browser/autofill/model/form_input_accessory_view_handler.h"
#import "ios/chrome/browser/infobars/model/infobar_ios.h"
#import "ios/chrome/browser/infobars/model/infobar_manager_impl.h"
#import "ios/chrome/browser/infobars/model/infobar_type.h"
#import "ios/chrome/browser/passwords/model/ios_chrome_save_password_infobar_delegate.h"
#import "ios/chrome/browser/passwords/model/notify_auto_signin_view_controller.h"
#import "ios/chrome/browser/passwords/model/password_controller_delegate.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/credential_provider_promo_commands.h"
#import "ios/chrome/browser/shared/public/commands/password_breach_commands.h"
#import "ios/chrome/browser/shared/public/commands/password_protection_commands.h"
#import "ios/chrome/browser/shared/public/commands/password_suggestion_commands.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/common/url_scheme_util.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/web_state.h"
#import "services/network/public/cpp/shared_url_loader_factory.h"
#import "ui/base/device_form_factor.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "url/gurl.h"
using autofill::FieldRendererId;
using autofill::FormActivityObserverBridge;
using autofill::FormData;
using autofill::FormRendererId;
using autofill::PasswordFormGenerationData;
using base::SysNSStringToUTF16;
using base::SysUTF16ToNSString;
using base::SysUTF8ToNSString;
using l10n_util::GetNSString;
using l10n_util::GetNSStringF;
using password_manager::AccountSelectFillData;
using password_manager::FillData;
using password_manager::PasswordForm;
using password_manager::PasswordFormManagerForUI;
using password_manager::PasswordGenerationFrameHelper;
using password_manager::PasswordManager;
using password_manager::PasswordManagerClient;
using password_manager::metrics_util::PasswordDropdownState;
using safe_browsing::PasswordReuseDetectionManagerClient;
using web::WebState;
namespace {
// Types of password infobars to display.
enum class PasswordInfoBarType { SAVE, UPDATE };
// Duration for notify user auto-sign in dialog being displayed.
constexpr int kNotifyAutoSigninDuration = 3; // seconds
} // namespace
@interface PasswordController () <SharedPasswordControllerDelegate>
// View controller for auto sign-in notification, owned by this
// PasswordController.
@property(nonatomic, strong)
NotifyUserAutoSigninViewController* notifyAutoSigninViewController;
// Displays infobar for `form` with `type`. If `type` is UPDATE, the user
// is prompted to update the password. If `type` is SAVE, the user is prompted
// to save the password.
- (void)showInfoBarForForm:(std::unique_ptr<PasswordFormManagerForUI>)form
infoBarType:(PasswordInfoBarType)type
manual:(BOOL)manual;
// Removes infobar for given `type` if it exists. If it is not found the
// request is silently ignored (because that use case is expected).
- (void)removeInfoBarOfType:(PasswordInfoBarType)type manual:(BOOL)manual;
// Hides auto sign-in notification. Removes the view from superview and destroys
// the controller.
// TODO(crbug.com/40394758): Animate disappearance.
- (void)hideAutosigninNotification;
@end
@implementation PasswordController {
std::unique_ptr<PasswordManager> _passwordManager;
std::unique_ptr<PasswordManagerClient> _passwordManagerClient;
std::unique_ptr<PasswordReuseDetectionManagerClient>
_passwordReuseDetectionManagerClient;
// The WebState this instance is observing. Will be null after
// -webStateDestroyed: has been called.
raw_ptr<WebState> _webState;
// Bridge to observe WebState from Objective-C.
std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
// Timer for hiding "Signing in as ..." notification.
base::OneShotTimer _notifyAutoSigninTimer;
// User credential waiting to be displayed in autosign-in snackbar, once tab
// becomes active.
std::unique_ptr<PasswordForm> _pendingAutoSigninPasswordForm;
}
- (instancetype)initWithWebState:(WebState*)webState {
self = [self initWithWebState:webState
client:nullptr
reuseDetectionClient:nullptr];
return self;
}
- (instancetype)initWithWebState:(WebState*)webState
client:(std::unique_ptr<PasswordManagerClient>)
passwordManagerClient
reuseDetectionClient:
(std::unique_ptr<PasswordReuseDetectionManagerClient>)
passwordReuseDetectionManagerClient {
self = [super init];
if (self) {
DCHECK(webState);
_webState = webState;
_webStateObserverBridge =
std::make_unique<web::WebStateObserverBridge>(self);
_webState->AddObserver(_webStateObserverBridge.get());
if (passwordManagerClient) {
_passwordManagerClient = std::move(passwordManagerClient);
} else {
_passwordManagerClient.reset(new IOSChromePasswordManagerClient(self));
}
if (passwordReuseDetectionManagerClient) {
_passwordReuseDetectionManagerClient =
std::move(passwordReuseDetectionManagerClient);
} else {
_passwordReuseDetectionManagerClient.reset(
new IOSChromePasswordReuseDetectionManagerClient(self));
}
_passwordManager.reset(new PasswordManager(_passwordManagerClient.get()));
PasswordFormHelper* formHelper =
[[PasswordFormHelper alloc] initWithWebState:webState];
PasswordSuggestionHelper* suggestionHelper =
[[PasswordSuggestionHelper alloc] initWithWebState:_webState];
PasswordControllerDriverHelper* driverHelper =
[[PasswordControllerDriverHelper alloc] initWithWebState:_webState];
_sharedPasswordController = [[SharedPasswordController alloc]
initWithWebState:_webState
manager:_passwordManager.get()
formHelper:formHelper
suggestionHelper:suggestionHelper
driverHelper:driverHelper];
_sharedPasswordController.delegate = self;
}
return self;
}
- (void)dealloc {
if (_webState) {
_webState->RemoveObserver(_webStateObserverBridge.get());
}
}
#pragma mark - Properties
- (ukm::SourceId)ukmSourceId {
return _webState ? ukm::GetSourceIdForWebStateDocument(_webState)
: ukm::kInvalidSourceId;
}
- (PasswordManagerClient*)passwordManagerClient {
return _passwordManagerClient.get();
}
- (PasswordReuseDetectionManagerClient*)passwordReuseDetectionManagerClient {
return _passwordReuseDetectionManagerClient.get();
}
#pragma mark - CRWWebStateObserver
// If Tab was shown, and there is a pending PasswordForm, display autosign-in
// notification.
- (void)webStateWasShown:(WebState*)webState {
DCHECK_EQ(_webState, webState);
if (_pendingAutoSigninPasswordForm) {
[self showAutosigninNotification:std::move(_pendingAutoSigninPasswordForm)];
_pendingAutoSigninPasswordForm.reset();
}
}
// If Tab was hidden, hide auto sign-in notification.
- (void)webStateWasHidden:(WebState*)webState {
DCHECK_EQ(_webState, webState);
[self hideAutosigninNotification];
}
- (void)webStateDestroyed:(WebState*)webState {
DCHECK_EQ(_webState, webState);
if (_webState) {
_webState->RemoveObserver(_webStateObserverBridge.get());
_webStateObserverBridge.reset();
_webState = nullptr;
}
_passwordManager.reset();
_passwordManagerClient.reset();
_passwordReuseDetectionManagerClient.reset();
}
#pragma mark - FormSuggestionProvider
- (id<FormSuggestionProvider>)suggestionProvider {
return _sharedPasswordController;
}
#pragma mark - PasswordGenerationProvider
- (id<PasswordGenerationProvider>)generationProvider {
return _sharedPasswordController;
}
#pragma mark - IOSChromePasswordManagerClientBridge
- (WebState*)webState {
return _webState;
}
- (ChromeBrowserState*)browserState {
return _webState ? ChromeBrowserState::FromBrowserState(
_webState->GetBrowserState())
: nullptr;
}
- (PasswordManager*)passwordManager {
return _passwordManager.get();
}
- (const GURL&)lastCommittedURL {
return _webState ? _webState->GetLastCommittedURL() : GURL::EmptyGURL();
}
- (void)showSavePasswordInfoBar:
(std::unique_ptr<PasswordFormManagerForUI>)formToSave
manual:(BOOL)manual {
[self showInfoBarForForm:std::move(formToSave)
infoBarType:PasswordInfoBarType::SAVE
manual:manual];
}
- (void)showUpdatePasswordInfoBar:
(std::unique_ptr<PasswordFormManagerForUI>)formToUpdate
manual:(BOOL)manual {
[self showInfoBarForForm:std::move(formToUpdate)
infoBarType:PasswordInfoBarType::UPDATE
manual:manual];
}
- (void)removePasswordInfoBarManualFallback:(BOOL)manual {
[self removeInfoBarOfType:PasswordInfoBarType::SAVE manual:manual];
[self removeInfoBarOfType:PasswordInfoBarType::UPDATE manual:manual];
}
// Shows auto sign-in notification and schedules hiding it after 3 seconds.
// TODO(crbug.com/40394758): Animate appearance.
- (void)showAutosigninNotification:(std::unique_ptr<PasswordForm>)formSignedIn {
if (!_webState) {
return;
}
// If a notification is already being displayed, hides the old one, then shows
// the new one.
if (self.notifyAutoSigninViewController) {
_notifyAutoSigninTimer.Stop();
[self hideAutosigninNotification];
}
// Creates view controller then shows the subview.
self.notifyAutoSigninViewController =
[[NotifyUserAutoSigninViewController alloc]
initWithUsername:SysUTF16ToNSString(formSignedIn->username_value)
iconURL:formSignedIn->icon_url
URLLoaderFactory:_webState->GetBrowserState()
->GetSharedURLLoaderFactory()];
if (![_delegate displaySignInNotification:self.notifyAutoSigninViewController
fromTabId:_webState->GetStableIdentifier()]) {
// The notification was not shown. Store the password form in
// `_pendingAutoSigninPasswordForm` to show the notification later.
_pendingAutoSigninPasswordForm = std::move(formSignedIn);
self.notifyAutoSigninViewController = nil;
return;
}
// Hides notification after 3 seconds.
__weak PasswordController* weakSelf = self;
_notifyAutoSigninTimer.Start(FROM_HERE,
base::Seconds(kNotifyAutoSigninDuration),
base::BindRepeating(^{
[weakSelf hideAutosigninNotification];
}));
}
- (void)showPasswordBreachForLeakType:(CredentialLeakType)leakType
URL:(const GURL&)URL
username:(const std::u16string&)username {
[self.passwordBreachDispatcher showPasswordBreachForLeakType:leakType];
}
- (void)showPasswordProtectionWarning:(NSString*)warningText
completion:(void (^)(safe_browsing::WarningAction))
completion {
[self.passwordProtectionDispatcher showPasswordProtectionWarning:warningText
completion:completion];
}
- (void)showCredentialProviderPromo:(CredentialProviderPromoTrigger)trigger {
[self.credentialProviderPromoHandler
showCredentialProviderPromoWithTrigger:trigger];
}
#pragma mark - Private methods
// The dispatcher used for PasswordBreachCommands.
- (id<PasswordBreachCommands>)passwordBreachDispatcher {
DCHECK(self.dispatcher);
return HandlerForProtocol(self.dispatcher, PasswordBreachCommands);
}
// The dispatcher used for PasswordProtectionCommands.
- (id<PasswordProtectionCommands>)passwordProtectionDispatcher {
DCHECK(self.dispatcher);
return HandlerForProtocol(self.dispatcher, PasswordProtectionCommands);
}
// The handler used for CredentialProviderPromoCommands.
- (id<CredentialProviderPromoCommands>)credentialProviderPromoHandler {
DCHECK(self.dispatcher);
return HandlerForProtocol(self.dispatcher, CredentialProviderPromoCommands);
}
// The dispatcher used for PasswordSuggestionCommands.
- (id<PasswordSuggestionCommands>)passwordSuggestionDispatcher {
DCHECK(self.dispatcher);
return HandlerForProtocol(self.dispatcher, PasswordSuggestionCommands);
}
- (InfoBarIOS*)findInfobarOfType:(InfobarType)infobarType manual:(BOOL)manual {
infobars::InfoBarManager* infoBarManager =
InfoBarManagerImpl::FromWebState(_webState);
for (infobars::InfoBar* infobar : infoBarManager->infobars()) {
InfoBarIOS* infoBarIOS = static_cast<InfoBarIOS*>(infobar);
if (infoBarIOS->infobar_type() == infobarType &&
infoBarIOS->skip_banner() == manual) {
return infoBarIOS;
}
}
return nil;
}
- (void)removeInfoBarOfType:(PasswordInfoBarType)type manual:(BOOL)manual {
InfoBarIOS* infobar = nil;
switch (type) {
case PasswordInfoBarType::SAVE: {
infobar = [self findInfobarOfType:InfobarType::kInfobarTypePasswordSave
manual:manual];
break;
}
case PasswordInfoBarType::UPDATE: {
infobar = [self findInfobarOfType:InfobarType::kInfobarTypePasswordUpdate
manual:manual];
break;
}
}
if (infobar) {
InfoBarManagerImpl::FromWebState(_webState)->RemoveInfoBar(infobar);
}
}
- (void)showInfoBarForForm:(std::unique_ptr<PasswordFormManagerForUI>)form
infoBarType:(PasswordInfoBarType)type
manual:(BOOL)manual {
if (!_webState) {
return;
}
CHECK(self.browserState);
PrefService* prefs = self.browserState->GetPrefs();
syncer::SyncService* syncService =
SyncServiceFactory::GetForBrowserState(self.browserState);
const std::optional<std::string> accountToStorePassword =
password_manager::sync_util::GetAccountForSaving(prefs, syncService);
const password_manager::features_util::PasswordAccountStorageUserState
accountStorageUserState = password_manager::features_util::
ComputePasswordAccountStorageUserState(prefs, syncService);
infobars::InfoBarManager* infoBarManager =
InfoBarManagerImpl::FromWebState(_webState);
switch (type) {
case PasswordInfoBarType::SAVE: {
// Count only new infobar showings, not replacements.
if (![self findInfobarOfType:InfobarType::kInfobarTypePasswordSave
manual:manual]) {
base::UmaHistogramBoolean("PasswordManager.iOS.InfoBar.PasswordSave",
true);
}
auto delegate = std::make_unique<IOSChromeSavePasswordInfoBarDelegate>(
accountToStorePassword,
/*password_update=*/false, accountStorageUserState, std::move(form),
self.dispatcher);
std::unique_ptr<InfoBarIOS> infobar = std::make_unique<InfoBarIOS>(
InfobarType::kInfobarTypePasswordSave, std::move(delegate),
/*skip_banner=*/manual);
infoBarManager->AddInfoBar(std::move(infobar),
/*replace_existing=*/true);
break;
}
case PasswordInfoBarType::UPDATE: {
// Count only new infobar showings, not replacements.
if (![self findInfobarOfType:InfobarType::kInfobarTypePasswordUpdate
manual:manual]) {
base::UmaHistogramBoolean("PasswordManager.iOS.InfoBar.PasswordUpdate",
true);
}
auto delegate = std::make_unique<IOSChromeSavePasswordInfoBarDelegate>(
form->IsUpdateAffectingPasswordsStoredInTheGoogleAccount()
? accountToStorePassword
: std::nullopt,
/*password_update=*/true, accountStorageUserState, std::move(form),
self.dispatcher);
std::unique_ptr<InfoBarIOS> infobar = std::make_unique<InfoBarIOS>(
InfobarType::kInfobarTypePasswordUpdate, std::move(delegate),
/*skip_banner=*/manual);
infoBarManager->AddInfoBar(std::move(infobar),
/*replace_existing=*/true);
break;
}
}
}
- (void)hideAutosigninNotification {
[self.notifyAutoSigninViewController willMoveToParentViewController:nil];
[self.notifyAutoSigninViewController.view removeFromSuperview];
[self.notifyAutoSigninViewController removeFromParentViewController];
self.notifyAutoSigninViewController = nil;
}
#pragma mark - SharedPasswordControllerDelegate
- (void)sharedPasswordController:(SharedPasswordController*)controller
showGeneratedPotentialPassword:(NSString*)generatedPotentialPassword
proactive:(BOOL)proactive
decisionHandler:(void (^)(BOOL accept))decisionHandler {
[self.passwordSuggestionDispatcher
showPasswordSuggestion:generatedPotentialPassword
proactive:proactive
webState:_webState
decisionHandler:decisionHandler];
}
- (void)sharedPasswordController:(SharedPasswordController*)controller
didAcceptSuggestion:(FormSuggestion*)suggestion {
if (suggestion.type == autofill::SuggestionType::kAllSavedPasswordsEntry) {
// Navigate to the settings list.
[self.delegate displaySavedPasswordList];
}
}
- (void)attachListenersForBottomSheet:
(const std::vector<autofill::FieldRendererId>&)rendererIds
forFrameId:(const std::string&)frameId {
AutofillBottomSheetTabHelper* bottomSheetTabHelper =
AutofillBottomSheetTabHelper::FromWebState(_webState);
if (bottomSheetTabHelper) {
bottomSheetTabHelper->AttachPasswordListeners(rendererIds, frameId);
}
}
- (void)attachListenersForPasswordGenerationBottomSheet:
(const std::vector<autofill::FieldRendererId>&)rendererIds
forFrameId:
(const std::string&)frameId {
AutofillBottomSheetTabHelper* bottomSheetTabHelper =
AutofillBottomSheetTabHelper::FromWebState(_webState);
if (bottomSheetTabHelper) {
bottomSheetTabHelper->AttachPasswordGenerationListeners(rendererIds,
frameId);
}
}
- (void)detachListenersForBottomSheet:(const std::string&)frameId {
AutofillBottomSheetTabHelper* bottomSheetTabHelper =
AutofillBottomSheetTabHelper::FromWebState(_webState);
if (bottomSheetTabHelper) {
bottomSheetTabHelper->DetachPasswordListeners(frameId,
/*refocus = */ false);
}
}
@end