blob: 040567ad5a51cdf960e30279b8edc983f6ff508d [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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/password_controller.h"
#include <stddef.h>
#include <algorithm>
#include <map>
#include <memory>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/mac/foundation_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string16.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "base/values.h"
#include "components/autofill/core/browser/ui/popup_item_ids.h"
#include "components/autofill/core/common/autofill_features.h"
#include "components/autofill/core/common/form_data.h"
#include "components/autofill/core/common/password_form_fill_data.h"
#include "components/autofill/core/common/password_form_generation_data.h"
#include "components/autofill/core/common/renderer_id.h"
#include "components/autofill/core/common/signatures.h"
#include "components/autofill/ios/browser/autofill_util.h"
#import "components/autofill/ios/form_util/form_activity_observer_bridge.h"
#include "components/autofill/ios/form_util/form_activity_params.h"
#include "components/autofill/ios/form_util/unique_id_data_tab_helper.h"
#include "components/infobars/core/infobar_manager.h"
#include "components/password_manager/core/browser/password_bubble_experiment.h"
#include "components/password_manager/core/browser/password_form.h"
#include "components/password_manager/core/browser/password_form_manager_for_ui.h"
#include "components/password_manager/core/browser/password_generation_frame_helper.h"
#include "components/password_manager/core/browser/password_manager.h"
#include "components/password_manager/core/browser/password_manager_client.h"
#include "components/password_manager/core/browser/password_manager_driver.h"
#include "components/password_manager/ios/account_select_fill_data.h"
#import "components/password_manager/ios/js_password_manager.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"
#include "components/strings/grit/components_strings.h"
#include "components/sync/driver/sync_service.h"
#import "components/ukm/ios/ukm_url_recorder.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#include "ios/chrome/browser/infobars/infobar_ios.h"
#include "ios/chrome/browser/infobars/infobar_manager_impl.h"
#import "ios/chrome/browser/infobars/infobar_type.h"
#import "ios/chrome/browser/main/browser.h"
#import "ios/chrome/browser/passwords/ios_chrome_save_password_infobar_delegate.h"
#import "ios/chrome/browser/passwords/notify_auto_signin_view_controller.h"
#include "ios/chrome/browser/sync/profile_sync_service_factory.h"
#import "ios/chrome/browser/ui/alert_coordinator/action_sheet_coordinator.h"
#import "ios/chrome/browser/ui/commands/application_commands.h"
#import "ios/chrome/browser/ui/commands/command_dispatcher.h"
#import "ios/chrome/browser/ui/commands/password_breach_commands.h"
#import "ios/chrome/browser/ui/infobars/coordinators/infobar_password_coordinator.h"
#import "ios/chrome/browser/ui/infobars/infobar_feature.h"
#include "ios/chrome/browser/ui/util/ui_util.h"
#include "ios/chrome/browser/web/tab_id_tab_helper.h"
#include "ios/chrome/grit/ios_strings.h"
#include "ios/web/common/url_scheme_util.h"
#include "ios/web/public/js_messaging/web_frame.h"
#include "ios/web/public/js_messaging/web_frame_util.h"
#include "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/web_state.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using autofill::FormActivityObserverBridge;
using autofill::FormData;
using autofill::PasswordFormGenerationData;
using password_manager::PasswordForm;
using autofill::FormRendererId;
using autofill::FieldRendererId;
using base::SysNSStringToUTF16;
using base::SysUTF16ToNSString;
using l10n_util::GetNSString;
using l10n_util::GetNSStringF;
using password_manager::metrics_util::LogPasswordDropdownShown;
using password_manager::metrics_util::PasswordDropdownState;
using password_manager::AccountSelectFillData;
using password_manager::FillData;
using password_manager::GetPageURLAndCheckTrustLevel;
using password_manager::PasswordFormManagerForUI;
using password_manager::PasswordGenerationFrameHelper;
using password_manager::PasswordManager;
using password_manager::PasswordManagerClient;
using password_manager::PasswordManagerDriver;
using password_manager::SerializePasswordFormFillData;
using web::WebFrame;
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;
// The action sheet coordinator, if one is currently being shown.
@property(nonatomic, strong) ActionSheetCoordinator* actionSheetCoordinator;
// Tracks current potential generated password until accepted or rejected.
@property(nonatomic, copy) NSString* generatedPotentialPassword;
// 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/435048): Animate disappearance.
- (void)hideAutosigninNotification;
@end
@implementation PasswordController {
std::unique_ptr<PasswordManager> _passwordManager;
std::unique_ptr<PasswordManagerClient> _passwordManagerClient;
std::unique_ptr<PasswordManagerDriver> _passwordManagerDriver;
// The WebState this instance is observing. Will be null after
// -webStateDestroyed: has been called.
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];
return self;
}
- (instancetype)initWithWebState:(WebState*)webState
client:(std::unique_ptr<PasswordManagerClient>)
passwordManagerClient {
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));
_passwordManager.reset(new PasswordManager(_passwordManagerClient.get()));
PasswordFormHelper* formHelper =
[[PasswordFormHelper alloc] initWithWebState:webState];
PasswordSuggestionHelper* suggestionHelper =
[[PasswordSuggestionHelper alloc] init];
_sharedPasswordController = [[SharedPasswordController alloc]
initWithWebState:_webState
manager:_passwordManager.get()
formHelper:formHelper
suggestionHelper:suggestionHelper];
_sharedPasswordController.delegate = self;
_passwordManagerDriver.reset(new IOSChromePasswordManagerDriver(
_sharedPasswordController, _passwordManager.get()));
}
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();
}
- (PasswordManagerDriver*)passwordManagerDriver {
return _passwordManagerDriver.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;
}
_passwordManagerDriver.reset();
_passwordManager.reset();
_passwordManagerClient.reset();
}
#pragma mark - FormSuggestionProvider
- (id<FormSuggestionProvider>)suggestionProvider {
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/435048): 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()];
TabIdTabHelper* tabIdHelper = TabIdTabHelper::FromWebState(_webState);
if (![_delegate displaySignInNotification:self.notifyAutoSigninViewController
fromTabId:tabIdHelper->tab_id()]) {
// 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::TimeDelta::FromSeconds(kNotifyAutoSigninDuration),
base::BindRepeating(^{
[weakSelf hideAutosigninNotification];
}));
}
- (void)showPasswordBreachForLeakType:(CredentialLeakType)leakType
URL:(const GURL&)URL {
[self.passwordBreachDispatcher showPasswordBreachForLeakType:leakType
URL:URL];
}
#pragma mark - Private methods
// The dispatcher used for ApplicationCommands.
- (id<ApplicationCommands>)applicationCommandsHandler {
DCHECK(self.browser);
DCHECK(self.browser->GetCommandDispatcher());
return HandlerForProtocol(self.browser->GetCommandDispatcher(),
ApplicationCommands);
}
// The dispatcher used for PasswordBreachCommands.
- (id<PasswordBreachCommands>)passwordBreachDispatcher {
DCHECK(self.browser);
DCHECK(self.browser->GetCommandDispatcher());
return HandlerForProtocol(self.browser->GetCommandDispatcher(),
PasswordBreachCommands);
}
- (InfoBarIOS*)findInfobarOfType:(InfobarType)infobarType manual:(BOOL)manual {
infobars::InfoBarManager* infoBarManager =
InfoBarManagerImpl::FromWebState(_webState);
size_t count = infoBarManager->infobar_count();
for (size_t i = 0; i < count; i++) {
InfoBarIOS* infobar =
static_cast<InfoBarIOS*>(infoBarManager->infobar_at(i));
if (infobar->infobar_type() == infobarType &&
infobar->skip_banner() == manual)
return infobar;
}
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;
bool isSyncUser = false;
if (self.browserState) {
syncer::SyncService* sync_service =
ProfileSyncServiceFactory::GetForBrowserState(self.browserState);
isSyncUser = password_bubble_experiment::IsSmartLockUser(sync_service);
}
infobars::InfoBarManager* infoBarManager =
InfoBarManagerImpl::FromWebState(_webState);
switch (type) {
case PasswordInfoBarType::SAVE: {
auto delegate = std::make_unique<IOSChromeSavePasswordInfoBarDelegate>(
isSyncUser, /*password_update*/ false, std::move(form));
delegate->set_handler(self.applicationCommandsHandler);
// Count only new infobar showings, not replacements.
if (![self findInfobarOfType:InfobarType::kInfobarTypePasswordSave
manual:manual]) {
base::UmaHistogramBoolean("PasswordManager.iOS.InfoBar.PasswordSave",
true);
}
std::unique_ptr<InfoBarIOS> infobar;
// If manual save, skip showing banner.
bool skip_banner = manual;
if (IsInfobarOverlayUIEnabled()) {
infobar = std::make_unique<InfoBarIOS>(
InfobarType::kInfobarTypePasswordSave, std::move(delegate),
skip_banner);
} else {
InfobarPasswordCoordinator* coordinator = [[InfobarPasswordCoordinator
alloc]
initWithInfoBarDelegate:delegate.get()
type:InfobarType::kInfobarTypePasswordSave];
infobar = std::make_unique<InfoBarIOS>(
coordinator, std::move(delegate), skip_banner);
}
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>(
isSyncUser, /*password_update*/ true, std::move(form));
delegate->set_handler(self.applicationCommandsHandler);
InfobarPasswordCoordinator* coordinator = [[InfobarPasswordCoordinator
alloc]
initWithInfoBarDelegate:delegate.get()
type:InfobarType::kInfobarTypePasswordUpdate];
// If manual save, skip showing banner.
std::unique_ptr<InfoBarIOS> infobar = std::make_unique<InfoBarIOS>(
coordinator, 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;
}
- (void)generatePasswordPopupDismissed {
[self.actionSheetCoordinator stop];
self.actionSheetCoordinator = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
self.generatedPotentialPassword = nil;
}
- (void)updateGeneratePasswordStrings:(id)sender {
NSString* title = [NSString
stringWithFormat:@"%@\n%@\n ", GetNSString(IDS_IOS_SUGGESTED_PASSWORD),
self.generatedPotentialPassword];
self.actionSheetCoordinator.attributedTitle =
[[NSMutableAttributedString alloc]
initWithString:title
attributes:@{
NSFontAttributeName :
[UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]
}];
NSString* message = GetNSString(IDS_IOS_SUGGESTED_PASSWORD_HINT);
self.actionSheetCoordinator.attributedMessage =
[[NSMutableAttributedString alloc]
initWithString:message
attributes:@{
NSFontAttributeName :
[UIFont preferredFontForTextStyle:UIFontTextStyleFootnote]
}];
// TODO(crbug.com/886583): find a way to make action sheet coordinator
// responsible for font size changes.
[self.actionSheetCoordinator updateAttributedText];
}
#pragma mark - SharedPasswordControllerDelegate
- (void)sharedPasswordController:(SharedPasswordController*)controller
showGeneratedPotentialPassword:(NSString*)generatedPotentialPassword
decisionHandler:(void (^)(BOOL accept))decisionHandler {
self.generatedPotentialPassword = generatedPotentialPassword;
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(updateGeneratePasswordStrings:)
name:UIContentSizeCategoryDidChangeNotification
object:nil];
// TODO(crbug.com/886583): add eg tests
self.actionSheetCoordinator = [[ActionSheetCoordinator alloc]
initWithBaseViewController:self.baseViewController
browser:self.browser
title:@""
message:@""
rect:self.baseViewController.view.frame
view:self.baseViewController.view];
self.actionSheetCoordinator.popoverArrowDirection = 0;
self.actionSheetCoordinator.alertStyle =
IsIPadIdiom() ? UIAlertControllerStyleAlert
: UIAlertControllerStyleActionSheet;
// Set attributed text.
[self updateGeneratePasswordStrings:self];
__weak PasswordController* weakSelf = self;
auto popupDismissed = ^{
[weakSelf generatePasswordPopupDismissed];
};
[self.actionSheetCoordinator
addItemWithTitle:GetNSString(IDS_IOS_USE_SUGGESTED_PASSWORD)
action:^{
decisionHandler(YES);
popupDismissed();
}
style:UIAlertActionStyleDefault];
[self.actionSheetCoordinator addItemWithTitle:GetNSString(IDS_CANCEL)
action:^{
decisionHandler(NO);
popupDismissed();
}
style:UIAlertActionStyleCancel];
// Set 'suggest' as preferred action, as per UX.
self.actionSheetCoordinator.alertController.preferredAction =
self.actionSheetCoordinator.alertController.actions[0];
[self.actionSheetCoordinator start];
}
- (void)sharedPasswordController:(SharedPasswordController*)controller
didAcceptSuggestion:(FormSuggestion*)suggestion {
if (suggestion.identifier ==
autofill::POPUP_ITEM_ID_ALL_SAVED_PASSWORDS_ENTRY) {
// Navigate to the settings list.
[self.delegate displaySavedPasswordList];
}
}
@end