blob: 3f7b31fd1a970cd2700cc05736026c759e979f6a [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/password_manager/core/browser/ui/saved_passwords_presenter.h"
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include "base/barrier_callback.h"
#include "base/barrier_closure.h"
#include "base/check.h"
#include "base/containers/cxx20_erase.h"
#include "base/containers/fixed_flat_set.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/notreached.h"
#include "base/observer_list.h"
#include "base/ranges/algorithm.h"
#include "base/task/sequenced_task_runner.h"
#include "components/password_manager/core/browser/form_parsing/form_parser.h"
#include "components/password_manager/core/browser/import/csv_password.h"
#include "components/password_manager/core/browser/password_form.h"
#include "components/password_manager/core/browser/password_list_sorter.h"
#include "components/password_manager/core/browser/password_manager_metrics_util.h"
#include "components/password_manager/core/browser/password_manager_util.h"
#include "components/password_manager/core/browser/ui/credential_ui_entry.h"
#include "components/password_manager/core/browser/ui/password_undo_helper.h"
#include "components/password_manager/core/browser/ui/passwords_grouper.h"
#include "components/password_manager/core/common/password_manager_features.h"
#include "components/sync/base/features.h"
#include "url/gurl.h"
namespace {
using password_manager::metrics_util::IsPasswordChanged;
using password_manager::metrics_util::IsPasswordNoteChanged;
using password_manager::metrics_util::IsUsernameChanged;
using password_manager::metrics_util::PasswordNoteAction;
using PasswordNote = password_manager::PasswordNote;
using Store = password_manager::PasswordForm::Store;
using EditResult = password_manager::SavedPasswordsPresenter::EditResult;
bool IsUsernameAlreadyUsed(
password_manager::SavedPasswordsPresenter::DuplicatePasswordsMap
key_to_forms,
const std::vector<password_manager::PasswordForm>& forms_to_check,
const std::u16string& new_username) {
// In case the username changed, make sure that there exists no other
// credential with the same signon_realm and username in the same store.
auto has_conflicting_username = [&forms_to_check,
&new_username](const auto& pair) {
const password_manager::PasswordForm form = pair.second;
return new_username == form.username_value &&
base::ranges::any_of(forms_to_check, [&form](const auto& old_form) {
return form.signon_realm == old_form.signon_realm &&
form.IsUsingAccountStore() ==
old_form.IsUsingAccountStore();
});
};
return base::ranges::any_of(key_to_forms, has_conflicting_username);
}
password_manager::PasswordForm GenerateFormFromCredential(
password_manager::CredentialUIEntry credential,
password_manager::PasswordForm::Type type) {
password_manager::PasswordForm form;
form.url = credential.GetURL();
form.signon_realm = credential.GetFirstSignonRealm();
form.username_value = credential.username;
form.password_value = credential.password;
form.type = type;
form.date_created = base::Time::Now();
form.date_password_modified = form.date_created;
if (!credential.note.empty())
form.SetNoteWithEmptyUniqueDisplayName(credential.note);
DCHECK(!credential.stored_in.empty());
form.in_store = *credential.stored_in.begin();
return form;
}
password_manager::PasswordStoreChangeList GetChangesForAddedForms(
const std::vector<password_manager::PasswordForm>& forms) {
password_manager::PasswordStoreChangeList changes;
for (const auto& form : forms) {
changes.emplace_back(password_manager::PasswordStoreChange::ADD, form);
}
return changes;
}
} // namespace
namespace password_manager {
SavedPasswordsPresenter::SavedPasswordsPresenter(
AffiliationService* affiliation_service,
scoped_refptr<PasswordStoreInterface> profile_store,
scoped_refptr<PasswordStoreInterface> account_store)
: profile_store_(std::move(profile_store)),
account_store_(std::move(account_store)),
undo_helper_(std::make_unique<PasswordUndoHelper>(profile_store_.get(),
account_store_.get())),
passwords_grouper_(
std::make_unique<PasswordsGrouper>(affiliation_service)) {
DCHECK(profile_store_);
}
SavedPasswordsPresenter::~SavedPasswordsPresenter() {
RemoveObservers();
}
void SavedPasswordsPresenter::Init() {
// Clear old cache.
sort_key_to_password_forms_.clear();
passwords_grouper_->ClearCache();
profile_store_->AddObserver(this);
if (account_store_)
account_store_->AddObserver(this);
pending_store_updates_++;
profile_store_->GetAllLoginsWithAffiliationAndBrandingInformation(
weak_ptr_factory_.GetWeakPtr());
if (account_store_) {
pending_store_updates_++;
account_store_->GetAllLoginsWithAffiliationAndBrandingInformation(
weak_ptr_factory_.GetWeakPtr());
}
}
bool SavedPasswordsPresenter::IsWaitingForPasswordStore() const {
return pending_store_updates_ != 0;
}
void SavedPasswordsPresenter::RemoveObservers() {
if (account_store_)
account_store_->RemoveObserver(this);
profile_store_->RemoveObserver(this);
}
bool SavedPasswordsPresenter::RemoveCredential(
const CredentialUIEntry& credential) {
std::vector<PasswordForm> forms_to_delete =
GetCorrespondingPasswordForms(credential);
undo_helper_->StartGroupingActions();
for (const auto& current_form : forms_to_delete) {
// Make sure |credential| and |current_form| share the same store.
if (credential.stored_in.contains(current_form.in_store)) {
// |current_form| is unchanged result obtained from
// 'OnGetPasswordStoreResultsFrom'. So it can be present only in one
// store at a time.
GetStoreFor(current_form).RemoveLogin(current_form);
undo_helper_->PasswordRemoved(current_form);
}
}
undo_helper_->EndGroupingActions();
return !forms_to_delete.empty();
}
void SavedPasswordsPresenter::UndoLastRemoval() {
undo_helper_->Undo();
}
SavedPasswordsPresenter::AddResult
SavedPasswordsPresenter::GetExpectedAddResult(
const CredentialUIEntry& credential) const {
if (!password_manager_util::IsValidPasswordURL(credential.GetURL()))
return AddResult::kInvalid;
if (credential.password.empty())
return AddResult::kInvalid;
auto have_equal_username_and_realm =
[&credential](const PasswordForm& entry) {
return credential.GetFirstSignonRealm() == entry.signon_realm &&
credential.username == entry.username_value;
};
auto have_equal_username_and_realm_in_profile_store =
[&have_equal_username_and_realm](
const DuplicatePasswordsMap::value_type& pair) {
return have_equal_username_and_realm(pair.second) &&
pair.second.IsUsingProfileStore();
};
auto have_equal_username_and_realm_in_account_store =
[&have_equal_username_and_realm](
const DuplicatePasswordsMap::value_type& pair) {
return have_equal_username_and_realm(pair.second) &&
pair.second.IsUsingAccountStore();
};
bool existing_credential_profile =
base::ranges::any_of(sort_key_to_password_forms_,
have_equal_username_and_realm_in_profile_store);
bool existing_credential_account =
base::ranges::any_of(sort_key_to_password_forms_,
have_equal_username_and_realm_in_account_store);
if (!existing_credential_profile && !existing_credential_account)
return AddResult::kSuccess;
auto have_exact_match = [&credential, &have_equal_username_and_realm](
const DuplicatePasswordsMap::value_type& pair) {
return have_equal_username_and_realm(pair.second) &&
credential.password == pair.second.password_value;
};
if (base::ranges::any_of(sort_key_to_password_forms_, have_exact_match))
return AddResult::kExactMatch;
if (!existing_credential_profile)
return AddResult::kConflictInAccountStore;
if (!existing_credential_account)
return AddResult::kConflictInProfileStore;
return AddResult::kConflictInProfileAndAccountStore;
}
bool SavedPasswordsPresenter::AddCredential(
const CredentialUIEntry& credential,
password_manager::PasswordForm::Type type) {
if (GetExpectedAddResult(credential) != AddResult::kSuccess)
return false;
UnblocklistBothStores(credential);
PasswordForm form = GenerateFormFromCredential(credential, type);
GetStoreFor(form).AddLogin(form);
return true;
}
void SavedPasswordsPresenter::UnblocklistBothStores(
const CredentialUIEntry& credential) {
// Try to unblocklist in both stores anyway because if credentials don't
// exist, the unblocklist operation is no-op.
auto form_digest =
PasswordFormDigest(PasswordForm::Scheme::kHtml,
credential.GetFirstSignonRealm(), credential.GetURL());
profile_store_->Unblocklist(form_digest);
if (account_store_)
account_store_->Unblocklist(form_digest);
}
void SavedPasswordsPresenter::AddCredentials(
const std::vector<CredentialUIEntry>& credentials,
password_manager::PasswordForm::Type type,
AddCredentialsCallback completion) {
if (credentials.empty()) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, std::move(completion));
return;
}
std::vector<PasswordForm> password_forms;
base::ranges::transform(credentials, std::back_inserter(password_forms),
[&](const CredentialUIEntry& credential) {
return GenerateFormFromCredential(credential, type);
});
CHECK(base::ranges::all_of(password_forms, [&](const PasswordForm& form) {
return password_forms[0].in_store == form.in_store;
}));
GetStoreFor(password_forms[0])
.AddLogins(password_forms, std::move(completion));
}
void SavedPasswordsPresenter::UpdatePasswordForms(
const std::vector<PasswordForm>& password_forms,
base::OnceClosure completion) {
if (password_forms.empty()) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, std::move(completion));
return;
}
CHECK(base::ranges::all_of(password_forms, [&](const PasswordForm& form) {
return password_forms[0].in_store == form.in_store;
}));
GetStoreFor(password_forms[0])
.UpdateLogins(password_forms, std::move(completion));
}
SavedPasswordsPresenter::EditResult
SavedPasswordsPresenter::EditSavedCredentials(
const CredentialUIEntry& original_credential,
const CredentialUIEntry& updated_credential) {
std::vector<PasswordForm> forms_to_change =
GetCorrespondingPasswordForms(original_credential);
if (forms_to_change.empty())
return EditResult::kNotFound;
IsUsernameChanged username_changed(updated_credential.username !=
original_credential.username);
IsPasswordChanged password_changed(updated_credential.password !=
original_credential.password);
IsPasswordNoteChanged note_changed(
forms_to_change[0].GetNoteWithEmptyUniqueDisplayName() !=
updated_credential.note);
bool issues_changed =
updated_credential.password_issues != forms_to_change[0].password_issues;
// Password can't be empty.
if (updated_credential.password.empty())
return EditResult::kEmptyPassword;
// Username can't be changed to the existing one.
if (username_changed &&
IsUsernameAlreadyUsed(sort_key_to_password_forms_, forms_to_change,
updated_credential.username)) {
return EditResult::kAlreadyExisits;
}
// Nothing changed.
if (!username_changed && !password_changed && !note_changed &&
!issues_changed) {
return EditResult::kNothingChanged;
}
base::RepeatingClosure completion_barrier_closure = base::DoNothing();
// Only change in username or password is interesting for OnEdited listeners.
if (username_changed || password_changed) {
completion_barrier_closure = base::BarrierClosure(
forms_to_change.size(),
base::BindOnce(&SavedPasswordsPresenter::NotifyEdited,
weak_ptr_factory_.GetWeakPtr(), updated_credential));
}
for (const auto& old_form : forms_to_change) {
PasswordStoreInterface& store = GetStoreFor(old_form);
PasswordForm new_form = old_form;
if (issues_changed) {
new_form.password_issues = updated_credential.password_issues;
}
if (password_changed) {
new_form.password_value = updated_credential.password;
new_form.date_password_modified = base::Time::Now();
new_form.password_issues.clear();
}
if (base::FeatureList::IsEnabled(syncer::kPasswordNotesWithBackup) &&
note_changed) {
new_form.SetNoteWithEmptyUniqueDisplayName(updated_credential.note);
}
// An updated username implies a change in the primary key, thus we need
// to make sure to call the right API.
if (username_changed) {
new_form.username_value = updated_credential.username;
// Phished and leaked issues are no longer relevant on username change.
// Weak and reused issues are still relevant.
new_form.password_issues.erase(InsecureType::kPhished);
new_form.password_issues.erase(InsecureType::kLeaked);
// Changing username requires deleting old form and adding new one. So
// the different API should be called.
store.UpdateLoginWithPrimaryKey(new_form, old_form,
completion_barrier_closure);
} else {
store.UpdateLogin(new_form, completion_barrier_closure);
}
}
return EditResult::kSuccess;
}
void SavedPasswordsPresenter::MoveCredentialsToAccount(
const std::vector<CredentialUIEntry>& credentials,
metrics_util::MoveToAccountStoreTrigger trigger) {
for (const auto& credential : credentials) {
std::vector<PasswordForm> move_form_candidates =
GetCorrespondingPasswordForms(credential);
// signon_realms of PasswordForms which are saved in account.
auto account_credentials_signon_realms = base::MakeFlatSet<std::string>(
move_form_candidates, {}, [](const auto& form) {
return form.IsUsingAccountStore() ? form.signon_realm : "";
});
for (const auto& form : move_form_candidates) {
if (form.IsUsingAccountStore()) {
continue;
}
CHECK(form.IsUsingProfileStore());
// Don't call AddLogin() if the credential already exists in the account
// store, 1) to avoid unnecessary sync cycles, 2) to avoid potential
// last_used_date update.
if (!account_credentials_signon_realms.contains(form.signon_realm)) {
account_store_->AddLogin(form);
}
profile_store_->RemoveLogin(form);
}
}
base::UmaHistogramEnumeration(
"PasswordManager.AccountStorage.MoveToAccountStoreFlowAccepted2",
trigger);
}
std::vector<CredentialUIEntry> SavedPasswordsPresenter::GetSavedCredentials()
const {
if (base::FeatureList::IsEnabled(features::kPasswordsGrouping)) {
return passwords_grouper_->GetAllCredentials();
}
std::vector<CredentialUIEntry> credentials;
auto it = sort_key_to_password_forms_.begin();
while (it != sort_key_to_password_forms_.end()) {
auto current_key = it->first;
// Aggregate all passwords for the current key.
std::vector<PasswordForm> current_passwords_group;
while (it != sort_key_to_password_forms_.end() &&
it->first == current_key) {
current_passwords_group.push_back(it->second);
++it;
}
credentials.emplace_back(current_passwords_group);
}
return credentials;
}
std::vector<AffiliatedGroup> SavedPasswordsPresenter::GetAffiliatedGroups() {
return passwords_grouper_->GetAffiliatedGroupsWithGroupingInfo();
}
std::vector<CredentialUIEntry> SavedPasswordsPresenter::GetSavedPasswords()
const {
auto credentials = GetSavedCredentials();
base::EraseIf(credentials, [](const auto& credential) {
return credential.blocked_by_user || !credential.federation_origin.opaque();
});
return credentials;
}
std::vector<CredentialUIEntry> SavedPasswordsPresenter::GetBlockedSites() {
DCHECK(base::FeatureList::IsEnabled(features::kPasswordsGrouping));
return passwords_grouper_->GetBlockedSites();
}
std::vector<PasswordForm>
SavedPasswordsPresenter::GetCorrespondingPasswordForms(
const CredentialUIEntry& credential) const {
std::vector<PasswordForm> forms;
if (base::FeatureList::IsEnabled(
password_manager::features::kPasswordsGrouping)) {
forms = passwords_grouper_->GetPasswordFormsFor(credential);
} else {
const auto range =
sort_key_to_password_forms_.equal_range(CreateSortKey(credential));
base::ranges::transform(range.first, range.second,
std::back_inserter(forms),
[](const auto& pair) { return pair.second; });
}
return forms;
}
void SavedPasswordsPresenter::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void SavedPasswordsPresenter::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void SavedPasswordsPresenter::NotifyEdited(
const CredentialUIEntry& credential) {
for (auto& observer : observers_)
observer.OnEdited(credential);
}
void SavedPasswordsPresenter::NotifySavedPasswordsChanged(
const PasswordStoreChangeList& changes) {
// Notify observers when there are no pending password store updates.
if (pending_store_updates_ > 0) {
return;
}
for (auto& observer : observers_)
observer.OnSavedPasswordsChanged(changes);
}
void SavedPasswordsPresenter::OnLoginsChanged(
PasswordStoreInterface* store,
const PasswordStoreChangeList& changes) {
std::vector<PasswordForm> forms_to_add;
std::vector<PasswordForm> forms_to_remove;
for (const PasswordStoreChange& change : changes) {
switch (change.type()) {
case PasswordStoreChange::ADD:
forms_to_add.push_back(change.form());
break;
case PasswordStoreChange::UPDATE:
forms_to_remove.push_back(change.form());
forms_to_add.push_back(change.form());
break;
case PasswordStoreChange::REMOVE:
forms_to_remove.push_back(change.form());
break;
}
}
RemoveForms(forms_to_remove);
// TODO(crbug.com/1381203): Inject branding info for these credentials.
AddForms(forms_to_add,
base::BindOnce(&SavedPasswordsPresenter::NotifySavedPasswordsChanged,
weak_ptr_factory_.GetWeakPtr(), changes));
}
void SavedPasswordsPresenter::OnLoginsRetained(
PasswordStoreInterface* store,
const std::vector<PasswordForm>& retained_passwords) {
bool is_using_account_store = store == account_store_.get();
// Remove cached credentials for the current store.
base::EraseIf(sort_key_to_password_forms_,
[is_using_account_store](
const DuplicatePasswordsMap::value_type& key_to_form) {
return key_to_form.second.IsUsingAccountStore() ==
is_using_account_store;
});
// TODO(crbug.com/1381203): Inject branding info for these credentials.
AddForms(retained_passwords,
base::BindOnce(&SavedPasswordsPresenter::NotifySavedPasswordsChanged,
weak_ptr_factory_.GetWeakPtr(),
PasswordStoreChangeList()));
}
void SavedPasswordsPresenter::OnGetPasswordStoreResults(
std::vector<std::unique_ptr<PasswordForm>> results) {
// This class overrides OnGetPasswordStoreResultsFrom() (the version of this
// method that also receives the originating store), so the store-less version
// never gets called.
NOTREACHED_NORETURN();
}
void SavedPasswordsPresenter::OnGetPasswordStoreResultsFrom(
PasswordStoreInterface* store,
std::vector<std::unique_ptr<PasswordForm>> results) {
pending_store_updates_--;
DCHECK_GE(pending_store_updates_, 0);
std::vector<PasswordForm> forms;
for (auto& form : results) {
forms.push_back(std::move(*form));
}
AddForms(forms,
base::BindOnce(&SavedPasswordsPresenter::NotifySavedPasswordsChanged,
weak_ptr_factory_.GetWeakPtr(),
GetChangesForAddedForms(forms)));
}
PasswordStoreInterface& SavedPasswordsPresenter::GetStoreFor(
const PasswordForm& form) {
DCHECK_NE(form.IsUsingAccountStore(), form.IsUsingProfileStore());
return form.IsUsingAccountStore() ? *account_store_ : *profile_store_;
}
void SavedPasswordsPresenter::RemoveForms(
const std::vector<PasswordForm>& forms) {
for (const auto& form : forms) {
// ArePasswordFormUniqueKeysEqual doesn't take password into account, this
// is why |in_store| has to be checked as it's possible to have two
// PasswordForms with the same unique keys but different passwords if and
// only if they are from different stores.
base::EraseIf(
sort_key_to_password_forms_,
[&form](const DuplicatePasswordsMap::value_type& key_to_form) {
return ArePasswordFormUniqueKeysEqual(key_to_form.second, form) &&
key_to_form.second.in_store == form.in_store;
});
}
}
void SavedPasswordsPresenter::AddForms(const std::vector<PasswordForm>& forms,
base::OnceClosure completion) {
for (const auto& form : forms) {
// TODO(crbug.com/1359392): Consider replacing |sort_key_to_password_forms_|
// when grouping is launched.
sort_key_to_password_forms_.insert(
std::make_pair(CreateSortKey(form, IgnoreStore(true)), form));
}
if (!base::FeatureList::IsEnabled(
password_manager::features::kPasswordsGrouping)) {
std::move(completion).Run();
return;
}
// Group passwords once we received forms from all password stores.
if (pending_store_updates_ > 0) {
return;
}
// TODO(crbug.com/1354196): Pass only added forms to |passwords_grouper_|.
std::vector<PasswordForm> all_forms;
all_forms.reserve(sort_key_to_password_forms_.size());
for (auto const& [key, form] : sort_key_to_password_forms_) {
all_forms.push_back(form);
}
// Notify observers after grouping is complete.
passwords_grouper_->GroupPasswords(
std::move(all_forms),
metrics_util::TimeCallback(std::move(completion),
"PasswordManager.PasswordsGrouping.Time"));
}
} // namespace password_manager