blob: 784ee86829da138b13f902dbb33c73530e66bd37 [file] [log] [blame]
// Copyright 2022 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/ui/autofill/autofill_context_menu_manager.h"
#include <string>
#include "base/containers/adapters.h"
#include "base/containers/flat_map.h"
#include "base/functional/overloaded.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/ui/autofill/chrome_autofill_client.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "components/autofill/content/browser/content_autofill_driver.h"
#include "components/autofill/core/browser/data_model/autofill_profile.h"
#include "components/autofill/core/browser/data_model/credit_card.h"
#include "components/autofill/core/browser/field_types.h"
#include "components/autofill/core/browser/personal_data_manager.h"
#include "components/autofill/core/common/autofill_features.h"
#include "ui/base/models/menu_model.h"
namespace autofill {
namespace {
// The range of command IDs reserved for autofill's custom menus.
static constexpr int kAutofillContextCustomFirst =
IDC_CONTENT_CONTEXT_AUTOFILL_CUSTOM_FIRST;
static constexpr int kAutofillContextCustomLast =
IDC_CONTENT_CONTEXT_AUTOFILL_CUSTOM_LAST;
} // namespace
// static
AutofillContextMenuManager::CommandId
AutofillContextMenuManager::ConvertToAutofillCustomCommandId(int offset) {
return AutofillContextMenuManager::CommandId(
kAutofillContextCustomFirst + SubMenuType::NUM_SUBMENU_TYPES + offset);
}
// static
bool AutofillContextMenuManager::IsAutofillCustomCommandId(
CommandId command_id) {
return command_id.value() >= kAutofillContextCustomFirst &&
command_id.value() <= kAutofillContextCustomLast;
}
AutofillContextMenuManager::AutofillContextMenuManager(
PersonalDataManager* personal_data_manager,
RenderViewContextMenuBase* delegate,
ui::SimpleMenuModel* menu_model,
Browser* browser)
: personal_data_manager_(personal_data_manager),
menu_model_(menu_model),
delegate_(delegate),
browser_(browser) {
DCHECK(delegate_);
params_ = delegate_->params();
}
AutofillContextMenuManager::~AutofillContextMenuManager() {
cached_menu_models_.clear();
command_id_to_menu_item_value_mapper_.clear();
}
base::flat_map<std::u16string, AutofillProfile*>
AutofillContextMenuManager::GetAddressProfilesWithTitles() {
std::vector<std::pair<std::u16string, AutofillProfile*>> profiles;
for (AutofillProfile* profile : personal_data_manager_->GetProfiles())
profiles.emplace_back(GetProfileDescription(*profile), profile);
return base::flat_map<std::u16string, AutofillProfile*>(std::move(profiles));
}
base::flat_map<std::u16string, CreditCard*>
AutofillContextMenuManager::GetCreditCardProfilesWithTitles() {
std::vector<std::pair<std::u16string, CreditCard*>> cards;
for (CreditCard* card : personal_data_manager_->GetCreditCards())
cards.emplace_back(card->CardIdentifierStringForAutofillDisplay(), card);
return base::flat_map<std::u16string, CreditCard*>(std::move(cards));
}
void AutofillContextMenuManager::AppendItems() {
content::RenderFrameHost* rfh = delegate_->GetRenderFrameHost();
if (!rfh)
return;
ContentAutofillDriver* driver =
ContentAutofillDriver::GetForRenderFrameHost(rfh);
// Do not show autofill context menu options for input fields that cannot be
// filled by the driver. See crbug.com/1367547.
if (!driver || !driver->CanShowAutofillUi())
return;
if (params_.field_renderer_id) {
LocalFrameToken frame_token(rfh->GetFrameToken().value());
// Formless fields have default form renderer id.
FormGlobalId form_global_id = {
frame_token, params_.form_renderer_id
? FormRendererId(*params_.form_renderer_id)
: FormRendererId()};
driver->OnContextMenuShownInField(
form_global_id,
{frame_token, FieldRendererId(*params_.field_renderer_id)});
}
if (!base::FeatureList::IsEnabled(
features::kAutofillShowManualFallbackInContextMenu)) {
return;
}
DCHECK(personal_data_manager_);
DCHECK(menu_model_);
content::WebContents* web_contents = delegate_->GetWebContents();
AutofillClient* autofill_client =
autofill::ChromeAutofillClient::FromWebContents(web_contents);
// If the autofill popup is shown and the user double clicks from within the
// bounds of the initiating field, it is assumed that the context menu would
// overlap with the autofill popup. In that case, hide the autofill popup.
if (autofill_client) {
autofill_client->HideAutofillPopup(
PopupHidingReason::kOverlappingWithAutofillContextMenu);
}
// Stores all the profile values added to the context menu along with the
// command id of the row.
std::vector<std::pair<CommandId, ContextMenuItem>>
detail_items_added_to_context_menu;
AddAddressOrCreditCardItemsToMenu(detail_items_added_to_context_menu,
GetAddressProfilesWithTitles());
AddAddressOrCreditCardItemsToMenu(detail_items_added_to_context_menu,
GetCreditCardProfilesWithTitles());
command_id_to_menu_item_value_mapper_ =
base::flat_map<CommandId, ContextMenuItem>(
std::move(detail_items_added_to_context_menu));
}
bool AutofillContextMenuManager::IsCommandIdChecked(
CommandId command_id) const {
return false;
}
bool AutofillContextMenuManager::IsCommandIdVisible(
CommandId command_id) const {
return true;
}
bool AutofillContextMenuManager::IsCommandIdEnabled(
CommandId command_id) const {
return true;
}
void AutofillContextMenuManager::ExecuteCommand(CommandId command_id) {
content::RenderFrameHost* rfh = delegate_->GetRenderFrameHost();
if (!rfh)
return;
auto it = command_id_to_menu_item_value_mapper_.find(command_id);
if (it == command_id_to_menu_item_value_mapper_.end())
return;
DCHECK(IsAutofillCustomCommandId(command_id));
// Field Renderer id should be present because the context menu is triggered
// on a input field. Otherwise, Autofill context menu models would not have
// been added to the context menu.
if (!params_.field_renderer_id)
return;
if (it->second.is_manage_item) {
DCHECK(browser_);
switch (it->second.sub_menu_type) {
case SubMenuType::SUB_MENU_TYPE_ADDRESS:
chrome::ShowAddresses(browser_);
break;
case SubMenuType::SUB_MENU_TYPE_CREDIT_CARD:
chrome::ShowPaymentMethods(browser_);
break;
case SubMenuType::SUB_MENU_TYPE_PASSWORD:
chrome::ShowPasswordManager(browser_);
break;
case SubMenuType::NUM_SUBMENU_TYPES:
[[fallthrough]];
default:
NOTREACHED();
}
return;
}
// TODO(crbug.com/1325811): When filling credit card number via the context
// masked number is filled, fix it + implement reauth mechanism.
ContentAutofillDriver* driver =
ContentAutofillDriver::GetForRenderFrameHost(rfh);
driver->browser_events().RendererShouldFillFieldWithValue(
{LocalFrameToken(rfh->GetFrameToken().value()),
FieldRendererId(params_.field_renderer_id.value())},
it->second.fill_value);
// TODO(crbug.com/1325811): Use `it->second.sub_menu_type` to record the usage
// of the context menu based on the type.
}
void AutofillContextMenuManager::AddAddressOrCreditCardItemsToMenu(
std::vector<std::pair<CommandId, ContextMenuItem>>&
detail_items_added_to_context_menu,
absl::variant<AddressProfilesWithTitles, CreditCardProfilesWithTitles>
profiles) {
bool is_address_menu =
absl::holds_alternative<AddressProfilesWithTitles>(profiles);
if (is_address_menu && absl::get<AddressProfilesWithTitles>(profiles).empty())
return;
if (!is_address_menu &&
absl::get<CreditCardProfilesWithTitles>(profiles).empty()) {
return;
}
SubMenuType sub_menu_type =
is_address_menu ? SUB_MENU_TYPE_ADDRESS : SUB_MENU_TYPE_CREDIT_CARD;
// Address field types that are supposed to be shown in the menu.
static constexpr FieldsToShow kAddressFieldTypesToShow[] = {
{ui::MenuModel::ItemType::TYPE_TITLE, NAME_FULL},
{ui::MenuModel::ItemType::TYPE_SEPARATOR, UNKNOWN_TYPE},
{ui::MenuModel::ItemType::TYPE_TITLE, ADDRESS_HOME_STREET_ADDRESS},
{ui::MenuModel::ItemType::TYPE_TITLE, ADDRESS_HOME_CITY},
{ui::MenuModel::ItemType::TYPE_TITLE, ADDRESS_HOME_ZIP},
{ui::MenuModel::ItemType::TYPE_SEPARATOR, UNKNOWN_TYPE},
{ui::MenuModel::ItemType::TYPE_TITLE, PHONE_HOME_WHOLE_NUMBER},
{ui::MenuModel::ItemType::TYPE_TITLE, EMAIL_ADDRESS}};
// Address menu of Others.
static constexpr FieldsToShow kAddressFieldTypesToShowOtherSection[] = {
{ui::MenuModel::ItemType::TYPE_TITLE, NAME_FIRST},
{ui::MenuModel::ItemType::TYPE_TITLE, NAME_LAST},
{ui::MenuModel::ItemType::TYPE_SEPARATOR, UNKNOWN_TYPE},
{ui::MenuModel::ItemType::TYPE_TITLE, ADDRESS_HOME_LINE1},
{ui::MenuModel::ItemType::TYPE_TITLE, ADDRESS_HOME_LINE2},
};
// Credit card field types that are supposed to be shown in the menu.
static constexpr FieldsToShow kCardFieldTypesToShow[] = {
{ui::MenuModel::ItemType::TYPE_TITLE, CREDIT_CARD_NAME_FULL},
{ui::MenuModel::ItemType::TYPE_TITLE, CREDIT_CARD_NUMBER},
{ui::MenuModel::ItemType::TYPE_SEPARATOR, UNKNOWN_TYPE},
{ui::MenuModel::ItemType::TYPE_TITLE, CREDIT_CARD_EXP_MONTH},
{ui::MenuModel::ItemType::TYPE_TITLE, CREDIT_CARD_EXP_2_DIGIT_YEAR}};
// Used to create menu model for storing address/card description. Would be
// attached to the top level "Fill Address Info/Fill Payment" item in the
// context menu.
ui::SimpleMenuModel* menu = CreateSimpleMenuModel();
// True if a row is added in the menu.
bool profile_added = false;
auto field_types_to_show = is_address_menu
? base::span(kAddressFieldTypesToShow)
: base::span(kCardFieldTypesToShow);
auto field_types_to_show_in_other =
is_address_menu ? base::span(kAddressFieldTypesToShowOtherSection)
: base::span<const FieldsToShow>();
absl::visit(
[&](const auto& addresses_or_cards) {
for (const auto& [profile_title, profile] : addresses_or_cards) {
if (!HaveEnoughIdsForProfile(profile, field_types_to_show,
field_types_to_show_in_other)) {
break;
}
AddAddressOrCreditCardItemToMenu(profile, profile_title,
field_types_to_show,
field_types_to_show_in_other, menu,
detail_items_added_to_context_menu);
profile_added = true;
}
},
profiles);
if (!profile_added)
return;
menu->AddSeparator(ui::NORMAL_SEPARATOR);
absl::optional<CommandId> manage_item_command_id =
GetNextAvailableAutofillCommandId();
DCHECK(manage_item_command_id);
// TODO(crbug.com/1325811): Use i18n string.
menu->AddItem(
manage_item_command_id->value(),
is_address_menu ? u"Manage addresses" : u"Manage payment methods");
detail_items_added_to_context_menu.emplace_back(
*manage_item_command_id, ContextMenuItem{u"", sub_menu_type, true});
// Add a menu option to suggest filling address/card in the context menu.
// Hovering over it opens a submenu suggesting all the address/card profiles
// stored in the profile.
// TODO(crbug.com/1325811): Use i18n string.
menu_model_->AddSubMenu(
kAutofillContextCustomFirst + sub_menu_type,
is_address_menu ? u"Fill Address Info" : u"Fill Payment", menu);
}
void AutofillContextMenuManager::AddAddressOrCreditCardItemToMenu(
absl::variant<const AutofillProfile*, const CreditCard*> profile,
const std::u16string& profile_title,
base::span<const FieldsToShow> field_types_to_show,
base::span<const FieldsToShow> other_fields_to_show,
ui::SimpleMenuModel* menu,
std::vector<std::pair<CommandId, ContextMenuItem>>&
detail_items_added_to_context_menu) {
bool is_address_menu =
absl::holds_alternative<const AutofillProfile*>(profile);
SubMenuType sub_menu_type =
is_address_menu ? SUB_MENU_TYPE_ADDRESS : SUB_MENU_TYPE_CREDIT_CARD;
// Creates a menu model for storing address/card details.
// Is attached to the address/card description menu item as a submenu.
ui::SimpleMenuModel* details_submenu = CreateSimpleMenuModel();
// Create a submenu for each address/card profile with their details.
AddProfileDataToMenu(profile, field_types_to_show, details_submenu,
detail_items_added_to_context_menu, sub_menu_type);
// Add "Other" section for addresses.
if (is_address_menu) {
details_submenu->AddSeparator(ui::NORMAL_SEPARATOR);
ui::SimpleMenuModel* other_menu_model = CreateSimpleMenuModel();
AddProfileDataToMenu(profile, other_fields_to_show, other_menu_model,
detail_items_added_to_context_menu, sub_menu_type);
absl::optional<CommandId> others_menu_id =
GetNextAvailableAutofillCommandId();
DCHECK(others_menu_id);
// TODO(crbug.com/1325811): Use i18n string.
details_submenu->AddSubMenu(others_menu_id->value(), u"Other",
other_menu_model);
}
// Add a menu item showing address/card profile description. Hovering
// over it opens a submenu with the address/card details.
absl::optional<CommandId> menu_id = GetNextAvailableAutofillCommandId();
if (menu_id)
menu->AddSubMenu(menu_id->value(), profile_title, details_submenu);
}
void AutofillContextMenuManager::AddProfileDataToMenu(
absl::variant<const AutofillProfile*, const CreditCard*>
profile_or_credit_card,
base::span<const FieldsToShow> field_types_to_show,
ui::SimpleMenuModel* menu_model,
std::vector<std::pair<CommandId, ContextMenuItem>>&
detail_items_added_to_context_menu,
SubMenuType sub_menu_type) {
std::vector<std::pair<ui::MenuModel::ItemType, std::u16string>>
items_to_add_to_menu;
items_to_add_to_menu.reserve(field_types_to_show.size());
// True if the separator needs to be shown.
bool is_separator_required = false;
// Iterate over the `field_types_to_show` from the back and check if there is
// a need for a separator. We do not want to show consecutive separators if
// the data does not exist in the profile.
for (const auto& [item_type, field_type] :
base::Reversed(field_types_to_show)) {
if (item_type == ui::MenuModel::ItemType::TYPE_SEPARATOR) {
if (is_separator_required)
items_to_add_to_menu.emplace_back(item_type, u"");
is_separator_required = false;
continue;
}
std::u16string value = absl::visit(
base::Overloaded{
[&field_type = field_type](const CreditCard* card) {
if (field_type == CREDIT_CARD_NUMBER)
return card->ObfuscatedNumberWithVisibleLastFourDigits();
return card->GetRawInfo(field_type);
},
[&field_type = field_type](const AutofillProfile* profile) {
return profile->GetRawInfo(field_type);
}},
profile_or_credit_card);
if (value.empty())
continue;
items_to_add_to_menu.emplace_back(item_type, value);
is_separator_required = true;
}
// Iterate in reverse again and add items to the menu model.
for (const auto& [item_type, value] : base::Reversed(items_to_add_to_menu)) {
if (item_type == ui::MenuModel::ItemType::TYPE_SEPARATOR) {
menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
} else {
absl::optional<CommandId> value_menu_id =
GetNextAvailableAutofillCommandId();
DCHECK(value_menu_id);
// Create a menu item with the address/credit card details and attach
// to the model.
menu_model->AddItem(value_menu_id->value(), value);
detail_items_added_to_context_menu.emplace_back(
*value_menu_id, ContextMenuItem{value, sub_menu_type});
}
}
}
bool AutofillContextMenuManager::HaveEnoughIdsForProfile(
absl::variant<const AutofillProfile*, const CreditCard*>
profile_or_credit_card,
base::span<const FieldsToShow> field_types_to_show,
base::span<const FieldsToShow> other_fields_to_show) {
// Count of items to be added to the context menu. Empty values are not
// considered.
auto non_empty_values_in_profile = [&](const auto& entry) {
ServerFieldType field_type = entry.field_type;
return field_type != UNKNOWN_TYPE &&
!absl::visit(
[field_type](const auto& alternative) {
return alternative->GetRawInfo(field_type);
},
profile_or_credit_card)
.empty();
};
int count_of_items_to_be_added =
base::ranges::count_if(field_types_to_show, non_empty_values_in_profile);
// For addresses, include the "Other" section in the count. For credit cards,
// this should be empty.
if (absl::holds_alternative<const CreditCard*>(profile_or_credit_card))
DCHECK(other_fields_to_show.empty());
count_of_items_to_be_added +=
base::ranges::count_if(other_fields_to_show, non_empty_values_in_profile);
// Check if there are enough command ids for adding all the items to the
// context menu.
// 1 is added to count for the address/credit card description.
// Another 1 is added to account for the manage addresses/payment methods
// option.
if (!IsAutofillCustomCommandId(CommandId(
kAutofillContextCustomFirst + SubMenuType::NUM_SUBMENU_TYPES +
count_of_items_added_to_menu_model_ + count_of_items_to_be_added + 1 +
1))) {
return false;
}
return true;
}
ui::SimpleMenuModel* AutofillContextMenuManager::CreateSimpleMenuModel() {
cached_menu_models_.push_back(
std::make_unique<ui::SimpleMenuModel>(delegate_));
return cached_menu_models_.back().get();
}
absl::optional<AutofillContextMenuManager::CommandId>
AutofillContextMenuManager::GetNextAvailableAutofillCommandId() {
int max_index = kAutofillContextCustomLast - kAutofillContextCustomFirst;
if (count_of_items_added_to_menu_model_ >= max_index)
return absl::nullopt;
return ConvertToAutofillCustomCommandId(
count_of_items_added_to_menu_model_++);
}
std::u16string AutofillContextMenuManager::GetProfileDescription(
const AutofillProfile& profile) {
// All user-visible fields.
static constexpr ServerFieldType kDetailsFields[] = {
NAME_FULL,
ADDRESS_HOME_LINE1,
ADDRESS_HOME_LINE2,
ADDRESS_HOME_DEPENDENT_LOCALITY,
ADDRESS_HOME_CITY,
ADDRESS_HOME_STATE,
ADDRESS_HOME_ZIP,
EMAIL_ADDRESS,
PHONE_HOME_WHOLE_NUMBER,
COMPANY_NAME,
ADDRESS_HOME_COUNTRY};
return profile.ConstructInferredLabel(
kDetailsFields, std::size(kDetailsFields),
/*num_fields_to_include=*/2, personal_data_manager_->app_locale());
}
} // namespace autofill