blob: 724258958f500b14c3d0540efc7ff82975acca06 [file] [log] [blame]
// Copyright 2013 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.
#include "chrome/browser/ui/passwords/manage_passwords_bubble_model.h"
#include <stddef.h>
#include <algorithm>
#include <limits>
#include <memory>
#include <string>
#include <vector>
#include "base/metrics/histogram_macros.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/default_clock.h"
#include "chrome/browser/password_manager/password_store_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sync/profile_sync_service_factory.h"
#include "chrome/browser/ui/passwords/manage_passwords_view_utils.h"
#include "chrome/browser/ui/passwords/passwords_model_delegate.h"
#include "chrome/grit/generated_resources.h"
#include "components/browser_sync/profile_sync_service.h"
#include "components/password_manager/core/browser/password_bubble_experiment.h"
#include "components/password_manager/core/browser/password_form_metrics_recorder.h"
#include "components/password_manager/core/browser/password_manager_constants.h"
#include "components/password_manager/core/browser/password_store.h"
#include "components/password_manager/core/common/password_manager_pref_names.h"
#include "components/password_manager/core/common/password_manager_ui.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/l10n/l10n_util.h"
#if defined(OS_WIN)
#include "chrome/browser/ui/desktop_ios_promotion/desktop_ios_promotion_util.h"
#endif
namespace metrics_util = password_manager::metrics_util;
namespace {
void CleanStatisticsForSite(Profile* profile, const GURL& origin) {
DCHECK(profile);
password_manager::PasswordStore* password_store =
PasswordStoreFactory::GetForProfile(profile,
ServiceAccessType::IMPLICIT_ACCESS)
.get();
password_store->RemoveSiteStats(origin.GetOrigin());
}
std::vector<autofill::PasswordForm> DeepCopyForms(
const std::vector<std::unique_ptr<autofill::PasswordForm>>& forms) {
std::vector<autofill::PasswordForm> result;
result.reserve(forms.size());
std::transform(forms.begin(), forms.end(), std::back_inserter(result),
[](const std::unique_ptr<autofill::PasswordForm>& form) {
return *form;
});
return result;
}
bool IsSyncUser(Profile* profile) {
const browser_sync::ProfileSyncService* sync_service =
ProfileSyncServiceFactory::GetForProfile(profile);
return password_bubble_experiment::IsSmartLockUser(sync_service);
}
} // namespace
// Class responsible for collecting and reporting all the runtime interactions
// with the bubble.
class ManagePasswordsBubbleModel::InteractionKeeper {
public:
InteractionKeeper(
password_manager::InteractionsStats stats,
password_manager::metrics_util::UIDisplayDisposition display_disposition);
~InteractionKeeper() = default;
// Records UMA events, updates the interaction statistics and sends
// notifications to the delegate when the bubble is closed.
void ReportInteractions(const ManagePasswordsBubbleModel* model);
void set_dismissal_reason(
password_manager::metrics_util::UIDismissalReason reason) {
dismissal_reason_ = reason;
}
void set_sign_in_promo_dismissal_reason(
password_manager::metrics_util::SyncSignInUserAction reason) {
sign_in_promo_dismissal_reason_ = reason;
}
void SetClockForTesting(base::Clock* clock) { clock_ = clock; }
void set_sign_in_promo_shown_count(int count) {
sign_in_promo_shown_count = count;
}
private:
// The way the bubble appeared.
const password_manager::metrics_util::UIDisplayDisposition
display_disposition_;
// Dismissal reason for a password bubble.
password_manager::metrics_util::UIDismissalReason dismissal_reason_;
// Dismissal reason for the Chrome Sign in bubble.
password_manager::metrics_util::SyncSignInUserAction
sign_in_promo_dismissal_reason_;
// Current statistics for the save password bubble;
password_manager::InteractionsStats interaction_stats_;
// Used to retrieve the current time, in base::Time units.
base::Clock* clock_;
// Number of times the sign-in promo was shown to the user.
int sign_in_promo_shown_count;
DISALLOW_COPY_AND_ASSIGN(InteractionKeeper);
};
ManagePasswordsBubbleModel::InteractionKeeper::InteractionKeeper(
password_manager::InteractionsStats stats,
password_manager::metrics_util::UIDisplayDisposition display_disposition)
: display_disposition_(display_disposition),
dismissal_reason_(metrics_util::NO_DIRECT_INTERACTION),
sign_in_promo_dismissal_reason_(metrics_util::CHROME_SIGNIN_DISMISSED),
interaction_stats_(std::move(stats)),
clock_(base::DefaultClock::GetInstance()),
sign_in_promo_shown_count(0) {}
void ManagePasswordsBubbleModel::InteractionKeeper::ReportInteractions(
const ManagePasswordsBubbleModel* model) {
if (model->state() == password_manager::ui::PENDING_PASSWORD_STATE) {
// Update the statistics for the save password bubble.
Profile* profile = model->GetProfile();
if (profile) {
if (dismissal_reason_ == metrics_util::NO_DIRECT_INTERACTION &&
display_disposition_ ==
metrics_util::AUTOMATIC_WITH_PASSWORD_PENDING) {
if (interaction_stats_.dismissal_count <
std::numeric_limits<decltype(
interaction_stats_.dismissal_count)>::max())
interaction_stats_.dismissal_count++;
interaction_stats_.update_time = clock_->Now();
password_manager::PasswordStore* password_store =
PasswordStoreFactory::GetForProfile(
profile, ServiceAccessType::IMPLICIT_ACCESS).get();
password_store->AddSiteStats(interaction_stats_);
}
}
}
// Log UMA histograms.
if (model->state() == password_manager::ui::CHROME_SIGN_IN_PROMO_STATE) {
metrics_util::LogSyncSigninPromoUserAction(sign_in_promo_dismissal_reason_);
switch (sign_in_promo_dismissal_reason_) {
case password_manager::metrics_util::CHROME_SIGNIN_OK:
UMA_HISTOGRAM_COUNTS_100("PasswordManager.SignInPromoCountTilSignIn",
sign_in_promo_shown_count);
break;
case password_manager::metrics_util::CHROME_SIGNIN_CANCEL:
UMA_HISTOGRAM_COUNTS_100("PasswordManager.SignInPromoCountTilNoThanks",
sign_in_promo_shown_count);
break;
case password_manager::metrics_util::CHROME_SIGNIN_DISMISSED:
UMA_HISTOGRAM_COUNTS_100("PasswordManager.SignInPromoDismissalCount",
sign_in_promo_shown_count);
break;
case password_manager::metrics_util::CHROME_SIGNIN_ACTION_COUNT:
NOTREACHED();
break;
}
} else if (model->state() ==
password_manager::ui::PENDING_PASSWORD_UPDATE_STATE) {
metrics_util::LogUpdateUIDismissalReason(dismissal_reason_);
} else if (model->state() == password_manager::ui::PENDING_PASSWORD_STATE) {
metrics_util::LogSaveUIDismissalReason(dismissal_reason_);
} else {
metrics_util::LogGeneralUIDismissalReason(dismissal_reason_);
}
// Update the delegate so that it can send votes to the server.
if (model->state() == password_manager::ui::PENDING_PASSWORD_UPDATE_STATE ||
model->state() == password_manager::ui::PENDING_PASSWORD_STATE) {
// Send a notification if there was no interaction with the bubble.
bool no_interaction =
dismissal_reason_ == metrics_util::NO_DIRECT_INTERACTION;
if (no_interaction && model->delegate_) {
model->delegate_->OnNoInteraction();
}
}
// Record UKM statistics on dismissal reason.
if (model->metrics_recorder_)
model->metrics_recorder_->RecordUIDismissalReason(dismissal_reason_);
}
ManagePasswordsBubbleModel::ManagePasswordsBubbleModel(
base::WeakPtr<PasswordsModelDelegate> delegate,
DisplayReason display_reason)
: delegate_(std::move(delegate)),
interaction_reported_(false),
are_passwords_revealed_when_bubble_is_opened_(false),
metrics_recorder_(delegate_->GetPasswordFormMetricsRecorder()) {
origin_ = delegate_->GetOrigin();
state_ = delegate_->GetState();
password_manager::InteractionsStats interaction_stats;
if (state_ == password_manager::ui::PENDING_PASSWORD_STATE ||
state_ == password_manager::ui::PENDING_PASSWORD_UPDATE_STATE) {
pending_password_ = delegate_->GetPendingPassword();
local_credentials_ = DeepCopyForms(delegate_->GetCurrentForms());
if (state_ == password_manager::ui::PENDING_PASSWORD_STATE) {
interaction_stats.origin_domain = origin_.GetOrigin();
interaction_stats.username_value = pending_password_.username_value;
const password_manager::InteractionsStats* stats =
delegate_->GetCurrentInteractionStats();
if (stats) {
DCHECK_EQ(interaction_stats.username_value, stats->username_value);
DCHECK_EQ(interaction_stats.origin_domain, stats->origin_domain);
interaction_stats.dismissal_count = stats->dismissal_count;
}
}
if (delegate_->ArePasswordsRevealedWhenBubbleIsOpened()) {
are_passwords_revealed_when_bubble_is_opened_ = true;
delegate_->OnPasswordsRevealed();
}
// The condition for the password reauth:
// If the bubble opened after reauth -> no more reauth necessary, otherwise
// If a password was autofilled -> require reauth to view it, otherwise
// Require reauth iff the user opened the bubble manually and it's not the
// manual saving state. The manual saving state as well as automatic prompt
// are temporary states, therefore, it's better for the sake of convinience
// for the user not to break the UX with the reauth prompt.
password_revealing_requires_reauth_ =
!are_passwords_revealed_when_bubble_is_opened_ &&
(pending_password_.form_has_autofilled_value ||
(!delegate_->BubbleIsManualFallbackForSaving() &&
display_reason == USER_ACTION));
enable_editing_ = delegate_->GetCredentialSource() !=
password_manager::metrics_util::CredentialSourceType::
kCredentialManagementAPI;
UpdatePendingStateTitle();
} else if (state_ == password_manager::ui::CONFIRMATION_STATE) {
title_ =
l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_CONFIRM_SAVED_TITLE);
} else if (state_ == password_manager::ui::AUTO_SIGNIN_STATE) {
pending_password_ = delegate_->GetPendingPassword();
} else if (state_ == password_manager::ui::MANAGE_STATE) {
local_credentials_ = DeepCopyForms(delegate_->GetCurrentForms());
UpdateManageStateTitle();
// TODO(pbos): Remove manage_link_ + accessors when the cocoa dialog goes
// away. This temporarily uses the button label which is equivalent with
// the previous link.
manage_link_ =
l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_MANAGE_PASSWORDS_BUTTON);
}
if (state_ == password_manager::ui::CONFIRMATION_STATE) {
base::string16 link = l10n_util::GetStringUTF16(IDS_MANAGE_PASSWORDS_LINK);
size_t offset;
save_confirmation_text_ = l10n_util::GetStringFUTF16(
IDS_MANAGE_PASSWORDS_CONFIRM_GENERATED_TEXT, link, &offset);
save_confirmation_link_range_ = gfx::Range(offset, offset + link.length());
}
password_manager::metrics_util::UIDisplayDisposition display_disposition =
metrics_util::AUTOMATIC_WITH_PASSWORD_PENDING;
if (display_reason == USER_ACTION) {
switch (state_) {
case password_manager::ui::PENDING_PASSWORD_STATE:
display_disposition = metrics_util::MANUAL_WITH_PASSWORD_PENDING;
break;
case password_manager::ui::PENDING_PASSWORD_UPDATE_STATE:
display_disposition =
metrics_util::MANUAL_WITH_PASSWORD_PENDING_UPDATE;
break;
case password_manager::ui::MANAGE_STATE:
display_disposition = metrics_util::MANUAL_MANAGE_PASSWORDS;
break;
case password_manager::ui::CONFIRMATION_STATE:
display_disposition =
metrics_util::MANUAL_GENERATED_PASSWORD_CONFIRMATION;
break;
case password_manager::ui::CREDENTIAL_REQUEST_STATE:
case password_manager::ui::AUTO_SIGNIN_STATE:
case password_manager::ui::CHROME_SIGN_IN_PROMO_STATE:
case password_manager::ui::CHROME_DESKTOP_IOS_PROMO_STATE:
case password_manager::ui::INACTIVE_STATE:
NOTREACHED();
break;
}
} else {
switch (state_) {
case password_manager::ui::PENDING_PASSWORD_STATE:
display_disposition = metrics_util::AUTOMATIC_WITH_PASSWORD_PENDING;
break;
case password_manager::ui::PENDING_PASSWORD_UPDATE_STATE:
display_disposition =
metrics_util::AUTOMATIC_WITH_PASSWORD_PENDING_UPDATE;
break;
case password_manager::ui::CONFIRMATION_STATE:
display_disposition =
metrics_util::AUTOMATIC_GENERATED_PASSWORD_CONFIRMATION;
break;
case password_manager::ui::AUTO_SIGNIN_STATE:
display_disposition = metrics_util::AUTOMATIC_SIGNIN_TOAST;
break;
case password_manager::ui::MANAGE_STATE:
case password_manager::ui::CREDENTIAL_REQUEST_STATE:
case password_manager::ui::CHROME_SIGN_IN_PROMO_STATE:
case password_manager::ui::CHROME_DESKTOP_IOS_PROMO_STATE:
case password_manager::ui::INACTIVE_STATE:
NOTREACHED();
break;
}
}
if (metrics_recorder_) {
metrics_recorder_->RecordPasswordBubbleShown(
delegate_->GetCredentialSource(), display_disposition);
}
metrics_util::LogUIDisplayDisposition(display_disposition);
interaction_keeper_.reset(new InteractionKeeper(std::move(interaction_stats),
display_disposition));
delegate_->OnBubbleShown();
}
ManagePasswordsBubbleModel::~ManagePasswordsBubbleModel() {
if (!interaction_reported_)
OnBubbleClosing();
}
void ManagePasswordsBubbleModel::OnBubbleClosing() {
interaction_keeper_->ReportInteractions(this);
if (delegate_)
delegate_->OnBubbleHidden();
delegate_.reset();
interaction_reported_ = true;
}
void ManagePasswordsBubbleModel::OnNopeUpdateClicked() {
DCHECK_EQ(password_manager::ui::PENDING_PASSWORD_UPDATE_STATE, state_);
interaction_keeper_->set_dismissal_reason(metrics_util::CLICKED_CANCEL);
if (delegate_)
delegate_->OnNopeUpdateClicked();
}
void ManagePasswordsBubbleModel::OnNeverForThisSiteClicked() {
DCHECK_EQ(password_manager::ui::PENDING_PASSWORD_STATE, state_);
interaction_keeper_->set_dismissal_reason(metrics_util::CLICKED_NEVER);
if (delegate_) {
CleanStatisticsForSite(GetProfile(), origin_);
delegate_->NeverSavePassword();
}
}
void ManagePasswordsBubbleModel::OnCredentialEdited(
base::string16 new_username,
base::string16 new_password) {
DCHECK(state_ == password_manager::ui::PENDING_PASSWORD_STATE ||
state_ == password_manager::ui::PENDING_PASSWORD_UPDATE_STATE);
pending_password_.username_value = std::move(new_username);
pending_password_.password_value = std::move(new_password);
}
void ManagePasswordsBubbleModel::OnSaveClicked() {
DCHECK(state_ == password_manager::ui::PENDING_PASSWORD_STATE ||
state_ == password_manager::ui::PENDING_PASSWORD_UPDATE_STATE);
interaction_keeper_->set_dismissal_reason(metrics_util::CLICKED_SAVE);
if (delegate_) {
CleanStatisticsForSite(GetProfile(), origin_);
delegate_->SavePassword(pending_password_.username_value,
pending_password_.password_value);
}
}
void ManagePasswordsBubbleModel::OnManageClicked() {
interaction_keeper_->set_dismissal_reason(metrics_util::CLICKED_MANAGE);
if (delegate_)
delegate_->NavigateToPasswordManagerSettingsPage();
}
void ManagePasswordsBubbleModel::
OnNavigateToPasswordManagerAccountDashboardLinkClicked() {
interaction_keeper_->set_dismissal_reason(
metrics_util::CLICKED_PASSWORDS_DASHBOARD);
if (delegate_)
delegate_->NavigateToPasswordManagerAccountDashboard();
}
void ManagePasswordsBubbleModel::OnBrandLinkClicked() {
interaction_keeper_->set_dismissal_reason(metrics_util::CLICKED_BRAND_NAME);
if (delegate_)
delegate_->NavigateToSmartLockHelpPage();
}
void ManagePasswordsBubbleModel::OnAutoSignInToastTimeout() {
interaction_keeper_->set_dismissal_reason(
metrics_util::AUTO_SIGNIN_TOAST_TIMEOUT);
}
void ManagePasswordsBubbleModel::OnPasswordAction(
const autofill::PasswordForm& password_form,
PasswordAction action) {
Profile* profile = GetProfile();
if (!profile)
return;
password_manager::PasswordStore* password_store =
PasswordStoreFactory::GetForProfile(
profile, ServiceAccessType::EXPLICIT_ACCESS).get();
DCHECK(password_store);
if (action == REMOVE_PASSWORD)
password_store->RemoveLogin(password_form);
else
password_store->AddLogin(password_form);
}
void ManagePasswordsBubbleModel::OnSignInToChromeClicked(
const AccountInfo& account,
bool is_default_promo_account) {
// Enabling sync for an existing account and starting a new sign-in are
// triggered by the user interacting with the sign-in promo.
interaction_keeper_->set_sign_in_promo_dismissal_reason(
metrics_util::CHROME_SIGNIN_OK);
GetProfile()->GetPrefs()->SetBoolean(
password_manager::prefs::kWasSignInPasswordPromoClicked, true);
if (delegate_)
delegate_->EnableSync(account, is_default_promo_account);
}
void ManagePasswordsBubbleModel::OnSkipSignInClicked() {
interaction_keeper_->set_sign_in_promo_dismissal_reason(
metrics_util::CHROME_SIGNIN_CANCEL);
GetProfile()->GetPrefs()->SetBoolean(
password_manager::prefs::kWasSignInPasswordPromoClicked, true);
}
Profile* ManagePasswordsBubbleModel::GetProfile() const {
content::WebContents* web_contents = GetWebContents();
if (!web_contents)
return nullptr;
return Profile::FromBrowserContext(web_contents->GetBrowserContext());
}
content::WebContents* ManagePasswordsBubbleModel::GetWebContents() const {
return delegate_ ? delegate_->GetWebContents() : nullptr;
}
bool ManagePasswordsBubbleModel::IsCurrentStateUpdate() const {
DCHECK(state_ == password_manager::ui::PENDING_PASSWORD_UPDATE_STATE ||
state_ == password_manager::ui::PENDING_PASSWORD_STATE);
return std::any_of(local_credentials_.begin(), local_credentials_.end(),
[this](const autofill::PasswordForm& form) {
return form.username_value ==
pending_password_.username_value;
});
}
bool ManagePasswordsBubbleModel::ShouldShowFooter() const {
return (state_ == password_manager::ui::PENDING_PASSWORD_UPDATE_STATE ||
state_ == password_manager::ui::PENDING_PASSWORD_STATE) &&
IsSyncUser(GetProfile()) &&
// TODO(crbug.com/862269): Remove when "Smart Lock" is gone.
pending_password_.federation_origin.opaque();
}
const base::string16& ManagePasswordsBubbleModel::GetCurrentUsername() const {
return pending_password_.username_value;
}
bool ManagePasswordsBubbleModel::ReplaceToShowPromotionIfNeeded() {
Profile* profile = GetProfile();
if (!profile)
return false;
PrefService* prefs = profile->GetPrefs();
const browser_sync::ProfileSyncService* sync_service =
ProfileSyncServiceFactory::GetForProfile(profile);
// Signin promotion.
if (password_bubble_experiment::ShouldShowChromeSignInPasswordPromo(
prefs, sync_service)) {
interaction_keeper_->ReportInteractions(this);
title_brand_link_range_ = gfx::Range();
title_ =
l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_CONFIRM_SAVED_TITLE);
state_ = password_manager::ui::CHROME_SIGN_IN_PROMO_STATE;
int show_count = prefs->GetInteger(
password_manager::prefs::kNumberSignInPasswordPromoShown);
show_count++;
prefs->SetInteger(password_manager::prefs::kNumberSignInPasswordPromoShown,
show_count);
interaction_keeper_->set_sign_in_promo_shown_count(show_count);
return true;
}
#if defined(OS_WIN)
// Desktop to mobile promotion only enabled on windows.
if (desktop_ios_promotion::IsEligibleForIOSPromotion(
profile,
desktop_ios_promotion::PromotionEntryPoint::SAVE_PASSWORD_BUBBLE)) {
interaction_keeper_->ReportInteractions(this);
title_brand_link_range_ = gfx::Range();
title_ = desktop_ios_promotion::GetPromoTitle(
desktop_ios_promotion::PromotionEntryPoint::SAVE_PASSWORD_BUBBLE);
state_ = password_manager::ui::CHROME_DESKTOP_IOS_PROMO_STATE;
return true;
}
#endif
return false;
}
void ManagePasswordsBubbleModel::SetClockForTesting(base::Clock* clock) {
interaction_keeper_->SetClockForTesting(clock);
}
bool ManagePasswordsBubbleModel::RevealPasswords() {
bool reveal_immediately = !password_revealing_requires_reauth_ ||
(delegate_ && delegate_->AuthenticateUser());
if (reveal_immediately)
delegate_->OnPasswordsRevealed();
return reveal_immediately;
}
void ManagePasswordsBubbleModel::UpdatePendingStateTitle() {
title_brand_link_range_ = gfx::Range();
PasswordTitleType type =
state_ == password_manager::ui::PENDING_PASSWORD_UPDATE_STATE
? PasswordTitleType::UPDATE_PASSWORD
: (pending_password_.federation_origin.opaque()
? PasswordTitleType::SAVE_PASSWORD
: PasswordTitleType::SAVE_ACCOUNT);
GetSavePasswordDialogTitleTextAndLinkRange(GetWebContents()->GetVisibleURL(),
origin_, type, &title_);
}
void ManagePasswordsBubbleModel::UpdateManageStateTitle() {
GetManagePasswordsDialogTitleText(GetWebContents()->GetVisibleURL(), origin_,
!local_credentials_.empty(), &title_);
}