// 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.
#include "chrome/browser/keyboard_accessory/android/password_accessory_controller_impl.h"
#include <optional>
#include <string>
#include <utility>
#include "base/base64.h"
#include "base/containers/flat_map.h"
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/ranges/algorithm.h"
#include "base/strings/utf_string_conversions.h"
#include "base/trace_event/trace_event.h"
#include "chrome/browser/android/resource_mapper.h"
#include "chrome/browser/keyboard_accessory/android/accessory_sheet_data.h"
#include "chrome/browser/keyboard_accessory/android/accessory_sheet_enums.h"
#include "chrome/browser/keyboard_accessory/android/manual_filling_controller.h"
#include "chrome/browser/keyboard_accessory/android/manual_filling_utils.h"
#include "chrome/browser/keyboard_accessory/android/password_accessory_controller.h"
#include "chrome/browser/password_manager/android/all_passwords_bottom_sheet_controller.h"
#include "chrome/browser/password_manager/android/password_generation_controller.h"
#include "chrome/browser/password_manager/android/password_manager_launcher_android.h"
#include "chrome/browser/password_manager/chrome_password_manager_client.h"
#include "chrome/browser/password_manager/chrome_webauthn_credentials_delegate.h"
#include "chrome/browser/plus_addresses/plus_address_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ssl/chrome_security_state_tab_helper.h"
#include "chrome/browser/ui/android/plus_addresses/all_plus_addresses_bottom_sheet_controller.h"
#include "chrome/browser/ui/android/plus_addresses/plus_addresses_helper.h"
#include "chrome/browser/ui/passwords/ui_utils.h"
#include "chrome/browser/webauthn/android/webauthn_request_delegate_android.h"
#include "chrome/grit/generated_resources.h"
#include "components/autofill/content/browser/content_autofill_client.h"
#include "components/autofill/core/common/autofill_util.h"
#include "components/autofill/core/common/mojom/autofill_types.mojom-shared.h"
#include "components/autofill/core/common/password_generation_util.h"
#include "components/device_reauth/device_authenticator.h"
#include "components/password_manager/content/browser/content_password_manager_driver.h"
#include "components/password_manager/core/browser/credential_cache.h"
#include "components/password_manager/core/browser/origin_credential_store.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/core/browser/password_manager_util.h"
#include "components/password_manager/core/browser/webauthn_credentials_delegate.h"
#include "components/password_manager/core/common/password_manager_features.h"
#include "components/plus_addresses/features.h"
#include "components/plus_addresses/plus_address_service.h"
#include "components/plus_addresses/plus_address_types.h"
#include "components/resources/android/theme_resources.h"
#include "components/url_formatter/elide_url.h"
#include "components/webauthn/android/webauthn_cred_man_delegate.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/l10n/l10n_util.h"
using autofill::AccessorySheetData;
using autofill::AccessorySheetField;
using autofill::FooterCommand;
using autofill::PasskeySection;
using autofill::PlusAddressInfo;
using autofill::UserInfo;
using autofill::mojom::FocusedFieldType;
using password_manager::CredentialCache;
using password_manager::UiCredential;
using plus_addresses::PlusProfile;
using webauthn::WebAuthnCredManDelegate;
using BlocklistedStatus =
password_manager::OriginCredentialStore::BlocklistedStatus;
using FillingSource = ManualFillingController::FillingSource;
using IsExactMatch = autofill::UserInfo::IsExactMatch;
using ShouldShowAction = ManualFillingController::ShouldShowAction;
namespace {
autofill::UserInfo TranslateCredentials(const UiCredential& credential,
const url::Origin& frame_origin,
bool current_field_is_password,
int username_icon_id) {
DCHECK(!credential.origin().opaque());
UserInfo user_info(
credential.origin().Serialize(),
IsExactMatch(credential.match_type() ==
password_manager_util::GetLoginMatchType::kExact));
std::u16string username = GetDisplayUsername(credential);
user_info.add_field(AccessorySheetField::Builder()
.SetDisplayText(username)
.SetSelectable(!credential.username().empty())
.SetIconId(username_icon_id)
.Build());
user_info.add_field(
AccessorySheetField::Builder()
.SetDisplayText(credential.password())
.SetA11yDescription(l10n_util::GetStringFUTF16(
IDS_PASSWORD_MANAGER_ACCESSORY_PASSWORD_DESCRIPTION, username))
.SetIsObfuscated(true)
.SetSelectable(current_field_is_password)
.Build());
return user_info;
}
std::u16string GetPasswordTitle(bool has_credentials,
bool has_standalone_plus_addresses,
const url::Origin& origin) {
const std::u16string elided_url =
url_formatter::FormatOriginForSecurityDisplay(
origin, url_formatter::SchemeDisplay::OMIT_CRYPTOGRAPHIC);
if (!has_credentials) {
return l10n_util::GetStringFUTF16(
IDS_PASSWORD_MANAGER_ACCESSORY_PASSWORD_LIST_EMPTY_MESSAGE, elided_url);
}
return has_standalone_plus_addresses
? l10n_util::GetStringFUTF16(
IDS_PASSWORD_MANAGER_ACCESSORY_PASSWORD_LIST_TITLE,
elided_url)
: std::u16string();
}
std::u16string GetPlusAddressTitle(bool has_standalone_plus_addresses,
const url::Origin& origin) {
const std::u16string elided_url =
url_formatter::FormatOriginForSecurityDisplay(
origin, url_formatter::SchemeDisplay::OMIT_CRYPTOGRAPHIC);
return has_standalone_plus_addresses
? l10n_util::GetStringFUTF16(
IDS_PLUS_ADDRESS_FALLBACK_MANUAL_FILLING_SHEET_TITLE,
elided_url)
: std::u16string();
}
password_manager::PasswordManagerDriver* GetPasswordManagerDriver(
content::WebContents* web_contents) {
if (content::RenderFrameHost* rfh = web_contents->GetFocusedFrame()) {
return password_manager::ContentPasswordManagerDriver::
GetForRenderFrameHost(rfh);
}
return nullptr; // No driver without focused frame!
}
ShouldShowAction ShouldShowCredManReentryAction(
FocusedFieldType focused_field_type,
bool has_pending_credman_flow) {
if (!has_pending_credman_flow) {
return ShouldShowAction(false);
}
switch (focused_field_type) {
case FocusedFieldType::kFillablePasswordField:
case FocusedFieldType::kFillableUsernameField:
case FocusedFieldType::kFillableWebauthnTaggedField:
return ShouldShowAction(true);
case FocusedFieldType::kFillableNonSearchField:
case FocusedFieldType::kFillableSearchField:
case FocusedFieldType::kFillableTextArea:
case FocusedFieldType::kUnfillableElement:
case FocusedFieldType::kUnknown:
return ShouldShowAction(false);
}
NOTREACHED() << "Showing undefined for " << focused_field_type;
}
} // namespace
PasswordAccessoryControllerImpl::~PasswordAccessoryControllerImpl() {
if (authenticator_) {
authenticator_->Cancel();
}
if (plus_profiles_provider_) {
plus_profiles_provider_->RemoveObserver(this);
}
}
void PasswordAccessoryControllerImpl::RegisterFillingSourceObserver(
FillingSourceObserver observer) {
source_observer_ = std::move(observer);
}
std::optional<AccessorySheetData>
PasswordAccessoryControllerImpl::GetSheetData() const {
// Prevent crashing by returning a nullopt if no field was focused yet or if
// the frame was (possibly temporarily) unfocused. This signals to the caller
// that no sheet is available right now.
if (GetWebContents().GetFocusedFrame() == nullptr) {
return std::nullopt;
}
if (!last_focused_field_info_) {
return std::nullopt;
}
url::Origin origin = GetFocusedFrameOrigin();
// If the focused origin doesn't match the last known origin, it is not safe
// to provide any suggestions (because e.g. information about field type isn't
// reliable).
if (!last_focused_field_info_->origin.IsSameOriginWith(origin)) {
return std::nullopt;
}
std::vector<PasskeySection> passkeys_to_add;
std::vector<UserInfo> info_to_add;
std::vector<autofill::PlusAddressInfo> plus_address_info_to_add;
base::span<const PlusProfile> plus_profiles =
plus_profiles_provider_
? plus_profiles_provider_->GetAffiliatedPlusProfiles()
: base::span<const PlusProfile, 0>();
base::flat_map<std::string, bool>::container_type items(plus_profiles.size());
for (const PlusProfile& profile : plus_profiles) {
items.push_back({*profile.plus_address, false});
}
base::flat_map<std::string, bool> plus_addresses_used_as_usernames(
std::move(items));
const bool is_password_field = last_focused_field_info_->focused_field_type ==
FocusedFieldType::kFillablePasswordField;
if (autofill::IsFillable(last_focused_field_info_->focused_field_type)) {
base::span<const UiCredential> suggestions =
credential_cache_->GetCredentialStore(origin).GetCredentials();
info_to_add.reserve(suggestions.size());
for (const auto& credential : suggestions) {
const std::string username_utf8 =
base::UTF16ToUTF8(credential.username());
int username_icon_id = 0;
if (auto it = plus_addresses_used_as_usernames.find(username_utf8);
it != plus_addresses_used_as_usernames.end()) {
it->second = true;
username_icon_id =
ResourceMapper::MapToJavaDrawableId(IDR_AUTOFILL_PLUS_ADDRESS);
}
info_to_add.emplace_back(TranslateCredentials(
credential, origin, is_password_field, username_icon_id));
}
}
for (const PlusProfile& profile : plus_profiles) {
if (!plus_addresses_used_as_usernames[*profile.plus_address]) {
plus_address_info_to_add.emplace_back(
autofill::PlusAddressInfo(profile.facet.canonical_spec(),
base::UTF8ToUTF16(*profile.plus_address)));
}
}
if (password_manager::PasswordManagerDriver* driver =
driver_supplier_.Run((&GetWebContents()))) {
if (password_manager::WebAuthnCredentialsDelegate* credentials_delegate =
password_client_->GetWebAuthnCredentialsDelegateForDriver(driver)) {
if (auto passkeys = credentials_delegate->GetPasskeys()) {
passkeys_to_add.reserve(passkeys->size());
for (const password_manager::PasskeyCredential& passkey :
passkeys.value()) {
passkeys_to_add.emplace_back(passkey.display_name(),
passkey.credential_id());
}
}
}
}
bool has_suggestions = !info_to_add.empty() || !passkeys_to_add.empty();
AccessorySheetData data = autofill::CreateAccessorySheetData(
autofill::AccessoryTabType::PASSWORDS,
GetPasswordTitle(has_suggestions, !plus_address_info_to_add.empty(),
origin),
GetPlusAddressTitle(!plus_address_info_to_add.empty(), origin),
std::move(info_to_add), CreateManagePasswordsFooter());
base::ranges::for_each(std::move(passkeys_to_add),
[&data](PasskeySection section) {
data.add_passkey_section(std::move(section));
});
base::ranges::for_each(
std::move(plus_address_info_to_add),
[&data](autofill::PlusAddressInfo plus_address_info) {
data.add_plus_address_info(std::move(plus_address_info));
});
if (ShouldShowRecoveryToggle(origin)) {
BlocklistedStatus blocklisted_status =
credential_cache_->GetCredentialStore(origin).GetBlocklistedStatus();
if (blocklisted_status == BlocklistedStatus::kWasBlocklisted ||
blocklisted_status == BlocklistedStatus::kIsBlocklisted) {
autofill::OptionToggle option_toggle = autofill::OptionToggle(
l10n_util::GetStringUTF16(IDS_PASSWORD_SAVING_STATUS_TOGGLE),
/*enabled=*/blocklisted_status == BlocklistedStatus::kWasBlocklisted,
autofill::AccessoryAction::TOGGLE_SAVE_PASSWORDS);
data.set_option_toggle(option_toggle);
}
}
return data;
}
void PasswordAccessoryControllerImpl::OnFillingTriggered(
autofill::FieldGlobalId focused_field_id,
const AccessorySheetField& selection) {
authenticator_ = password_client_->GetDeviceAuthenticator();
if (!ShouldTriggerBiometricReauth(selection)) {
authenticator_.reset();
FillSelection(selection);
return;
}
// |this| cancels the authentication when it is destroyed if one is ongoing,
// which resets the callback, so it's safe to use base::Unretained(this) here.
authenticator_->AuthenticateWithMessage(
u"", base::BindOnce(&PasswordAccessoryControllerImpl::OnReauthCompleted,
base::Unretained(this), selection));
}
void PasswordAccessoryControllerImpl::OnPasskeySelected(
const std::vector<uint8_t>& passkey_id) {
if (password_manager::PasswordManagerDriver* driver =
driver_supplier_.Run((&GetWebContents()))) {
if (password_manager::WebAuthnCredentialsDelegate* credentials_delegate =
password_client_->GetWebAuthnCredentialsDelegateForDriver(driver)) {
credentials_delegate->SelectPasskey(base::Base64Encode(passkey_id),
base::DoNothing());
}
}
}
// static
PasswordAccessoryController* PasswordAccessoryController::GetOrCreate(
content::WebContents* web_contents,
CredentialCache* credential_cache) {
PasswordAccessoryControllerImpl::CreateForWebContents(web_contents,
credential_cache);
return PasswordAccessoryControllerImpl::FromWebContents(web_contents);
}
// static
PasswordAccessoryController* PasswordAccessoryController::GetIfExisting(
content::WebContents* web_contents) {
return PasswordAccessoryControllerImpl::FromWebContents(web_contents);
}
// static
void PasswordAccessoryControllerImpl::CreateForWebContents(
content::WebContents* web_contents,
CredentialCache* credential_cache) {
DCHECK(web_contents) << "Need valid WebContents to attach controller to!";
DCHECK(credential_cache);
if (!FromWebContents(web_contents)) {
web_contents->SetUserData(
UserDataKey(),
base::WrapUnique(new PasswordAccessoryControllerImpl(
web_contents, credential_cache, nullptr,
ChromePasswordManagerClient::FromWebContents(web_contents),
base::BindRepeating(GetPasswordManagerDriver),
base::BindRepeating(&local_password_migration::ShowWarning))));
}
}
// static
void PasswordAccessoryControllerImpl::CreateForWebContentsForTesting(
content::WebContents* web_contents,
CredentialCache* credential_cache,
base::WeakPtr<ManualFillingController> manual_filling_controller,
password_manager::PasswordManagerClient* password_client,
PasswordDriverSupplierForFocusedFrame driver_supplier,
ShowMigrationWarningCallback show_migration_warning_callback) {
DCHECK(web_contents) << "Need valid WebContents to attach controller to!";
DCHECK(!FromWebContents(web_contents)) << "Controller already attached!";
DCHECK(manual_filling_controller);
DCHECK(password_client);
web_contents->SetUserData(
UserDataKey(),
base::WrapUnique(new PasswordAccessoryControllerImpl(
web_contents, credential_cache, std::move(manual_filling_controller),
password_client, std::move(driver_supplier),
std::move(show_migration_warning_callback))));
}
void PasswordAccessoryControllerImpl::OnOptionSelected(
autofill::AccessoryAction selected_action) {
switch (selected_action) {
case autofill::AccessoryAction::USE_OTHER_PASSWORD:
ShowAllPasswords();
return;
case autofill::AccessoryAction::MANAGE_PASSWORDS:
password_manager_launcher::ShowPasswordSettings(
&GetWebContents(),
password_manager::ManagePasswordsReferrer::kPasswordsAccessorySheet,
/*manage_passkeys=*/false);
return;
case autofill::AccessoryAction::GENERATE_PASSWORD_MANUAL:
OnGenerationRequested(
autofill::password_generation::PasswordGenerationType::kManual);
GetManualFillingController()->Hide();
return;
case autofill::AccessoryAction::GENERATE_PASSWORD_AUTOMATIC:
OnGenerationRequested(
autofill::password_generation::PasswordGenerationType::kAutomatic);
GetManualFillingController()->Hide();
return;
case autofill::AccessoryAction::CREDMAN_CONDITIONAL_UI_REENTRY:
if (password_manager::PasswordManagerDriver* driver =
driver_supplier_.Run(&GetWebContents())) {
WebAuthnCredManDelegate* delegate =
password_client_->GetWebAuthnCredManDelegateForDriver(driver);
if (!delegate) {
return;
}
switch (WebAuthnCredManDelegate::CredManMode()) {
case WebAuthnCredManDelegate::CredManEnabledMode::kAllCredMan:
delegate->TriggerCredManUi(
WebAuthnCredManDelegate::RequestPasswords(true));
return;
case WebAuthnCredManDelegate::CredManEnabledMode::kNonGpmPasskeys:
delegate->TriggerCredManUi(
WebAuthnCredManDelegate::RequestPasswords(false));
return;
default:
NOTREACHED_IN_MIGRATION()
<< "WebAuthnCredManDelegate should not be used if "
"CredManMode is kNotEnabled!";
}
}
return;
case autofill::AccessoryAction::CROSS_DEVICE_PASSKEY:
if (password_manager::PasswordManagerDriver* driver =
driver_supplier_.Run(&GetWebContents())) {
if (password_manager::
WebAuthnCredentialsDelegate* credentials_delegate =
password_client_->GetWebAuthnCredentialsDelegateForDriver(
driver)) {
CHECK(credentials_delegate->IsAndroidHybridAvailable());
credentials_delegate->ShowAndroidHybridSignIn();
}
}
return;
case autofill::AccessoryAction::CREATE_PLUS_ADDRESS_FROM_PASSWORD_SHEET:
if (auto* client = autofill::ContentAutofillClient::FromWebContents(
&GetWebContents())) {
client->OfferPlusAddressCreation(
client->GetLastCommittedPrimaryMainFrameOrigin(),
base::BindOnce(
&PasswordAccessoryControllerImpl::OnPlusAddressCreated,
weak_ptr_factory_.GetWeakPtr()));
GetManualFillingController()->Hide();
}
return;
case autofill::AccessoryAction::SELECT_PLUS_ADDRESS_FROM_PASSWORD_SHEET:
all_plus_addresses_bottom_sheet_controller_ = std::make_unique<
plus_addresses::AllPlusAddressesBottomSheetController>(
&GetWebContents());
all_plus_addresses_bottom_sheet_controller_->Show(base::BindOnce(
&PasswordAccessoryControllerImpl::OnPlusAddressSelected,
weak_ptr_factory_.GetWeakPtr()));
GetManualFillingController()->Hide();
return;
case autofill::AccessoryAction::MANAGE_PLUS_ADDRESS_FROM_PASSWORD_SHEET:
plus_addresses::ShowManagePlusAddressesPage(GetWebContents());
return;
default:
NOTREACHED_IN_MIGRATION()
<< "Unhandled selected action: " << static_cast<int>(selected_action);
}
}
void PasswordAccessoryControllerImpl::OnToggleChanged(
autofill::AccessoryAction toggled_action,
bool enabled) {
if (toggled_action == autofill::AccessoryAction::TOGGLE_SAVE_PASSWORDS) {
ChangeCurrentOriginSavePasswordsStatus(enabled);
return;
}
NOTREACHED_IN_MIGRATION()
<< "Unhandled selected action: " << static_cast<int>(toggled_action);
}
void PasswordAccessoryControllerImpl::RegisterPlusProfilesProvider(
base::WeakPtr<AffiliatedPlusProfilesProvider> provider) {
plus_profiles_provider_ = provider;
if (plus_profiles_provider_) {
plus_profiles_provider_->AddObserver(this);
}
}
void PasswordAccessoryControllerImpl::RefreshSuggestionsForField(
FocusedFieldType focused_field_type) {
// Discard all frame data. This ensures that the data is never used for an
// incorrect frame.
last_focused_field_info_ = std::nullopt;
all_passwords_helper_.SetLastFocusedFieldType(focused_field_type);
// Prevent crashing by not acting at all if frame became unfocused at any
// point. The next time a focus event happens, this will be called again and
// ensure we show correct data.
if (GetWebContents().GetFocusedFrame() == nullptr) {
return;
}
url::Origin origin = GetFocusedFrameOrigin();
if (origin.opaque()) {
return; // Don't proceed for invalid origins.
}
password_manager::PasswordManagerDriver* driver =
driver_supplier_.Run(&GetWebContents());
if (!driver) {
return;
}
TRACE_EVENT0("passwords",
"PasswordAccessoryControllerImpl::RefreshSuggestionsForField");
const bool is_manual_generation_available =
password_manager_util::ManualPasswordGenerationEnabled(driver) &&
password_client_->GetPasswordManager()->HaveFormManagersReceivedData(
driver);
last_focused_field_info_.emplace(origin, focused_field_type,
is_manual_generation_available);
RefreshSuggestions();
}
void PasswordAccessoryControllerImpl::OnGenerationRequested(
autofill::password_generation::PasswordGenerationType type) {
PasswordGenerationController* pwd_generation_controller =
PasswordGenerationController::GetIfExisting(&GetWebContents());
DCHECK(pwd_generation_controller);
pwd_generation_controller->OnGenerationRequested(type);
}
void PasswordAccessoryControllerImpl::UpdateCredManReentryUi(
FocusedFieldType focused_field_type) {
if (WebAuthnCredManDelegate::CredManMode() ==
WebAuthnCredManDelegate::CredManEnabledMode::kNotEnabled) {
return; // No updates required.
}
if (password_manager::PasswordManagerDriver* driver =
driver_supplier_.Run(&GetWebContents())) {
if (WebAuthnCredManDelegate* delegate =
password_client_->GetWebAuthnCredManDelegateForDriver(driver)) {
GetManualFillingController()->OnAccessoryActionAvailabilityChanged(
ShouldShowCredManReentryAction(
focused_field_type,
delegate->HasPasskeys() == WebAuthnCredManDelegate::kHasPasskeys),
autofill::AccessoryAction::CREDMAN_CONDITIONAL_UI_REENTRY);
}
}
}
base::WeakPtr<PasswordAccessoryController>
PasswordAccessoryControllerImpl::AsWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
PasswordAccessoryControllerImpl::LastFocusedFieldInfo::LastFocusedFieldInfo(
url::Origin focused_origin,
FocusedFieldType focused_field,
bool manual_generation_available)
: origin(focused_origin),
focused_field_type(focused_field),
is_manual_generation_available(manual_generation_available) {}
PasswordAccessoryControllerImpl::PasswordAccessoryControllerImpl(
content::WebContents* web_contents,
CredentialCache* credential_cache,
base::WeakPtr<ManualFillingController> manual_filling_controller,
password_manager::PasswordManagerClient* password_client,
PasswordDriverSupplierForFocusedFrame driver_supplier,
ShowMigrationWarningCallback show_migration_warning_callback)
: content::WebContentsObserver(web_contents),
content::WebContentsUserData<PasswordAccessoryControllerImpl>(
*web_contents),
credential_cache_(credential_cache),
manual_filling_controller_(std::move(manual_filling_controller)),
password_client_(password_client),
driver_supplier_(std::move(driver_supplier)),
show_migration_warning_callback_(
std::move(show_migration_warning_callback)),
plus_address_service_(PlusAddressServiceFactory::GetForBrowserContext(
GetWebContents().GetBrowserContext())) {}
std::vector<FooterCommand>
PasswordAccessoryControllerImpl::CreateManagePasswordsFooter() const {
std::vector<FooterCommand> footer_commands_to_add;
bool has_passkeys = false;
if (password_manager::PasswordManagerDriver* driver =
driver_supplier_.Run((&GetWebContents()))) {
if (webauthn::WebAuthnCredManDelegate::CredManMode() !=
webauthn::WebAuthnCredManDelegate::kNotEnabled) {
if (auto* delegate =
password_client_->GetWebAuthnCredManDelegateForDriver(driver)) {
has_passkeys |= delegate->HasPasskeys();
if (delegate->HasPasskeys()) {
footer_commands_to_add.emplace_back(
l10n_util::GetStringUTF16(
IDS_PASSWORD_MANAGER_ACCESSORY_SELECT_PASSKEY),
autofill::AccessoryAction::CREDMAN_CONDITIONAL_UI_REENTRY);
}
}
}
}
if (all_passwords_helper_.available_credentials().has_value() &&
IsSecureSite() &&
GetFocusedFrameOrigin().GetURL().SchemeIsCryptographic() &&
all_passwords_helper_.available_credentials().value() > 0) {
footer_commands_to_add.emplace_back(
l10n_util::GetStringUTF16(
IDS_PASSWORD_MANAGER_ACCESSORY_SELECT_PASSWORD),
autofill::AccessoryAction::USE_OTHER_PASSWORD);
}
const bool is_password_field = last_focused_field_info_->focused_field_type ==
FocusedFieldType::kFillablePasswordField;
if (is_password_field &&
last_focused_field_info_->is_manual_generation_available) {
std::u16string generate_password_title = l10n_util::GetStringUTF16(
IDS_PASSWORD_MANAGER_ACCESSORY_GENERATE_PASSWORD_BUTTON_TITLE);
footer_commands_to_add.emplace_back(
generate_password_title,
autofill::AccessoryAction::GENERATE_PASSWORD_MANUAL);
}
if (password_manager::PasswordManagerDriver* driver =
driver_supplier_.Run((&GetWebContents()))) {
if (password_manager::WebAuthnCredentialsDelegate* credentials_delegate =
password_client_->GetWebAuthnCredentialsDelegateForDriver(driver)) {
has_passkeys |= credentials_delegate->GetPasskeys() &&
!credentials_delegate->GetPasskeys()->empty();
if (credentials_delegate->IsAndroidHybridAvailable()) {
std::u16string passkey_other_device_title = l10n_util::GetStringUTF16(
IDS_PASSWORD_MANAGER_ACCESSORY_USE_DEVICE_PASSKEY);
footer_commands_to_add.emplace_back(
passkey_other_device_title,
autofill::AccessoryAction::CROSS_DEVICE_PASSKEY);
}
}
}
auto manage_passwords_message_id =
has_passkeys
? IDS_PASSWORD_MANAGER_ACCESSORY_ALL_PASSWORDS_AND_PASSKEYS_LINK
: IDS_PASSWORD_MANAGER_ACCESSORY_ALL_PASSWORDS_LINK;
std::u16string manage_passwords_title =
l10n_util::GetStringUTF16(manage_passwords_message_id);
footer_commands_to_add.emplace_back(
manage_passwords_title, autofill::AccessoryAction::MANAGE_PASSWORDS);
if (base::FeatureList::IsEnabled(
plus_addresses::features::kPlusAddressAndroidManualFallbackEnabled)) {
// Both `ContentAutofillClient and this controller are instances of the
// `WebContentsUserData`. There's no well-defined destruction order between
// two different `WebContentsUserData` objects. That's why
// `ContentAutofillClient` cannot be stored in a `raw_ptr` member variable
// like `PlusAddressService`.
auto* autofill_client =
autofill::ContentAutofillClient::FromWebContents(&GetWebContents());
if (autofill_client && plus_address_service_) {
// Offer plus address creation if it's supported for the current user
// session and if the user doesn't have any plus addresses created for the
// current domain.
if (plus_address_service_->IsPlusAddressCreationEnabled(
autofill_client->GetLastCommittedPrimaryMainFrameOrigin(),
autofill_client->IsOffTheRecord()) &&
plus_profiles_provider_ &&
plus_profiles_provider_->GetAffiliatedPlusProfiles().empty()) {
footer_commands_to_add.emplace_back(
l10n_util::GetStringUTF16(
IDS_PLUS_ADDRESS_CREATE_NEW_PLUS_ADDRESSES_LINK_ANDROID),
autofill::AccessoryAction::CREATE_PLUS_ADDRESS_FROM_PASSWORD_SHEET);
}
// Offer the user to select the plus address manually if plus address
// filling is supported for the last committed origin and the user has at
// least 1 plus address.
if (plus_address_service_->IsPlusAddressFillingEnabled(
autofill_client->GetLastCommittedPrimaryMainFrameOrigin()) &&
!plus_address_service_->GetPlusProfiles().empty()) {
footer_commands_to_add.emplace_back(
l10n_util::GetStringUTF16(
IDS_PLUS_ADDRESS_SELECT_PLUS_ADDRESS_LINK_ANDROID),
autofill::AccessoryAction::SELECT_PLUS_ADDRESS_FROM_PASSWORD_SHEET);
}
// Show "Manage plus addresses" action only if the user has at least 1
// affiliated plus addresses already saved for the current domain.
if (plus_profiles_provider_ &&
!plus_profiles_provider_->GetAffiliatedPlusProfiles().empty()) {
footer_commands_to_add.emplace_back(FooterCommand(
l10n_util::GetStringUTF16(
IDS_PLUS_ADDRESS_MANAGE_PLUS_ADDRESSES_LINK_ANDROID),
autofill::AccessoryAction::
MANAGE_PLUS_ADDRESS_FROM_PASSWORD_SHEET));
}
}
}
return footer_commands_to_add;
}
void PasswordAccessoryControllerImpl::WebContentsDestroyed() {
// Remove itself to avoid that pointers to other `WebContentsUserData` objects
// become invalid.
GetWebContents().RemoveUserData(UserDataKey());
// Do not add code - `this` is now destroyed.
}
void PasswordAccessoryControllerImpl::ChangeCurrentOriginSavePasswordsStatus(
bool saving_enabled) {
const url::Origin origin = GetFocusedFrameOrigin();
if (origin.opaque()) {
return;
}
const GURL origin_as_gurl = origin.GetURL();
password_manager::PasswordFormDigest form_digest(
password_manager::PasswordForm::Scheme::kHtml,
password_manager::GetSignonRealm(origin_as_gurl), origin_as_gurl);
password_manager::PasswordStoreInterface* store;
if (password_client_->GetPasswordFeatureManager()
->IsOptedInForAccountStorage() &&
password_client_->GetPasswordFeatureManager()
->GetDefaultPasswordStore() ==
password_manager::PasswordForm::Store::kAccountStore) {
store = password_client_->GetAccountPasswordStore();
} else {
store = password_client_->GetProfilePasswordStore();
}
if (saving_enabled) {
store->Unblocklist(form_digest);
} else {
password_manager::PasswordForm form =
password_manager_util::MakeNormalizedBlocklistedForm(
std::move(form_digest));
form.date_created = base::Time::Now();
store->AddLogin(form);
}
password_client_->UpdateFormManagers();
}
bool PasswordAccessoryControllerImpl::AppearsInSuggestions(
const std::u16string& suggestion,
bool is_password,
const url::Origin& origin) const {
if (origin.opaque()) {
return false; // Don't proceed for invalid origins.
}
// If the `suggestion` to fill is a valid plus address, it can be filled.
if (plus_address_service_ &&
plus_address_service_->IsPlusAddress(base::UTF16ToUTF8(suggestion))) {
return true;
}
return base::ranges::any_of(
credential_cache_->GetCredentialStore(origin).GetCredentials(),
[&](const auto& cred) {
return suggestion == (is_password ? cred.password() : cred.username());
});
}
bool PasswordAccessoryControllerImpl::ShouldShowRecoveryToggle(
const url::Origin& origin) const {
return password_client_->IsSavingAndFillingEnabled(origin.GetURL());
}
base::WeakPtr<ManualFillingController>
PasswordAccessoryControllerImpl::GetManualFillingController() {
if (!manual_filling_controller_) {
manual_filling_controller_ =
ManualFillingController::GetOrCreate(&GetWebContents());
}
DCHECK(manual_filling_controller_);
return manual_filling_controller_;
}
url::Origin PasswordAccessoryControllerImpl::GetFocusedFrameOrigin() const {
if (GetWebContents().GetFocusedFrame() == nullptr) {
LOG(DFATAL) << "Tried to get retrieve origin without focused "
"frame.";
return url::Origin(); // Nonce!
}
return GetWebContents().GetFocusedFrame()->GetLastCommittedOrigin();
}
void PasswordAccessoryControllerImpl::ShowAllPasswords() {
// If the controller is initialized that means that the UI is showing.
if (all_passords_bottom_sheet_controller_ || !last_focused_field_info_) {
return;
}
// AllPasswordsBottomSheetController assumes that the focused frame has a live
// RenderFrame so that it can use the password manager driver.
// TODO(crbug.com/40815830): Investigate if focused frame really needs
// to return RenderFrameHosts with non-live RenderFrames.
if (!GetWebContents().GetFocusedFrame()->IsRenderFrameLive()) {
return;
}
// We can use |base::Unretained| safely because at the time of calling
// |AllPasswordsSheetDismissed| we are sure that this controller is alive as
// it owns |AllPasswordsBottomSheetController| from which the method is
// called.
// TODO(crbug.com/40139552): Update the controller with the last focused
// field.
all_passords_bottom_sheet_controller_ =
std::make_unique<AllPasswordsBottomSheetController>(
&GetWebContents(), password_client_->GetProfilePasswordStore(),
password_client_->GetAccountPasswordStore(),
base::BindOnce(
&PasswordAccessoryControllerImpl::AllPasswordsSheetDismissed,
base::Unretained(this)),
last_focused_field_info_->focused_field_type);
all_passords_bottom_sheet_controller_->Show();
}
bool PasswordAccessoryControllerImpl::ShouldTriggerBiometricReauth(
const AccessorySheetField& selection) const {
if (!selection.is_obfuscated()) {
return false;
}
return password_client_->IsReauthBeforeFillingRequired(authenticator_.get());
}
void PasswordAccessoryControllerImpl::OnReauthCompleted(
AccessorySheetField selection,
bool auth_succeeded) {
authenticator_.reset();
if (!auth_succeeded) {
return;
}
FillSelection(selection);
}
void PasswordAccessoryControllerImpl::FillSelection(
const AccessorySheetField& selection) {
if (!AppearsInSuggestions(selection.display_text(), selection.is_obfuscated(),
GetFocusedFrameOrigin())) {
NOTREACHED_IN_MIGRATION() << "Tried to fill '" << selection.display_text()
<< "' into " << GetFocusedFrameOrigin();
return; // Never fill across different origins!
}
password_manager::PasswordManagerDriver* driver =
driver_supplier_.Run(&GetWebContents());
if (!driver) {
return;
}
driver->FillIntoFocusedField(selection.is_obfuscated(),
selection.display_text());
if (base::FeatureList::IsEnabled(
password_manager::features::
kUnifiedPasswordManagerLocalPasswordsMigrationWarning)) {
show_migration_warning_callback_.Run(
GetWebContents().GetTopLevelNativeWindow(),
Profile::FromBrowserContext(GetWebContents().GetBrowserContext()),
password_manager::metrics_util::PasswordMigrationWarningTriggers::
kKeyboardAcessorySheet);
}
}
void PasswordAccessoryControllerImpl::AllPasswordsSheetDismissed() {
all_passords_bottom_sheet_controller_.reset();
}
void PasswordAccessoryControllerImpl::OnPlusAddressCreated(
const std::string& plus_address) {
password_manager::PasswordManagerDriver* driver =
driver_supplier_.Run(&GetWebContents());
if (!driver) {
return;
}
driver->FillIntoFocusedField(/*is_password=*/false,
base::UTF8ToUTF16(plus_address));
}
void PasswordAccessoryControllerImpl::OnPlusAddressSelected(
base::optional_ref<const std::string> plus_address) {
all_plus_addresses_bottom_sheet_controller_.reset();
if (!plus_address) {
return;
}
if (password_manager::PasswordManagerDriver* driver =
driver_supplier_.Run(&GetWebContents())) {
driver->FillIntoFocusedField(/*is_password=*/false,
base::UTF8ToUTF16(plus_address.value()));
}
}
void PasswordAccessoryControllerImpl::RefreshSuggestions() {
if (!last_focused_field_info_) {
return;
}
bool sheet_provides_value =
last_focused_field_info_->is_manual_generation_available;
all_passwords_helper_.ClearUpdateCallback();
if (!all_passwords_helper_.available_credentials().has_value()) {
all_passwords_helper_.SetUpdateCallback(base::BindOnce(
&PasswordAccessoryControllerImpl::RefreshSuggestionsForField,
base::Unretained(this), last_focused_field_info_->focused_field_type));
} else {
sheet_provides_value |=
all_passwords_helper_.available_credentials().value() > 0;
}
if (ShouldShowRecoveryToggle(last_focused_field_info_->origin)) {
if (credential_cache_->GetCredentialStore(last_focused_field_info_->origin)
.GetBlocklistedStatus() == BlocklistedStatus::kIsBlocklisted) {
UMA_HISTOGRAM_BOOLEAN(
"KeyboardAccessory.DisabledSavingAccessoryImpressions", true);
}
sheet_provides_value = true;
}
// The all passwords sheet could cover this but if it's still loading, use
// this data as the next closest proxy to minimize delayed updates UI.
sheet_provides_value |=
!credential_cache_->GetCredentialStore(last_focused_field_info_->origin)
.GetCredentials()
.empty();
if (plus_profiles_provider_) {
sheet_provides_value |=
!plus_profiles_provider_->GetAffiliatedPlusProfiles().empty();
}
CHECK(source_observer_);
// The "Manage Passwords" entry point doesn't justify showing this fallback
// sheet for non-password fields.
source_observer_.Run(
this,
IsFillingSourceAvailable(
autofill::IsFillable(last_focused_field_info_->focused_field_type) &&
sheet_provides_value));
}
void PasswordAccessoryControllerImpl::OnAffiliatedPlusProfilesFetched() {
RefreshSuggestions();
}
bool PasswordAccessoryControllerImpl::IsSecureSite() const {
if (security_level_for_testing_) {
return security_level_for_testing_ == security_state::SECURE;
}
ChromeSecurityStateTabHelper::CreateForWebContents(&GetWebContents());
SecurityStateTabHelper* helper =
SecurityStateTabHelper::FromWebContents(&GetWebContents());
return helper && helper->GetSecurityLevel() == security_state::SECURE;
}
content::WebContents& PasswordAccessoryControllerImpl::GetWebContents() const {
// While a const_cast is not ideal. The Autofill API uses const in various
// spots and the content public API doesn't have const accessors. So the const
// cast is the lesser of two evils.
return const_cast<content::WebContents&>(
content::WebContentsUserData<
PasswordAccessoryControllerImpl>::GetWebContents());
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(PasswordAccessoryControllerImpl);