| // 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 <optional> |
| #include <string> |
| |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/notimplemented.h" |
| #include "base/notreached.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/values.h" |
| #include "chrome/app/chrome_command_ids.h" |
| #include "chrome/app/vector_icons/vector_icons.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/feedback/show_feedback_page.h" |
| #include "chrome/browser/password_manager/chrome_password_manager_client.h" |
| #include "chrome/browser/plus_addresses/plus_address_service_factory.h" |
| #include "chrome/browser/ui/autofill/address_bubbles_controller.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/passwords/ui_utils.h" |
| #include "chrome/browser/user_education/user_education_service.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/autofill/content/browser/content_autofill_client.h" |
| #include "components/autofill/content/browser/content_autofill_driver.h" |
| #include "components/autofill/core/browser/address_data_manager.h" |
| #include "components/autofill/core/browser/autofill_driver.h" |
| #include "components/autofill/core/browser/autofill_feedback_data.h" |
| #include "components/autofill/core/browser/autofill_prediction_improvements_delegate.h" |
| #include "components/autofill/core/browser/browser_autofill_manager.h" |
| #include "components/autofill/core/browser/data_model/autofill_profile.h" |
| #include "components/autofill/core/browser/field_type_utils.h" |
| #include "components/autofill/core/browser/filling_product.h" |
| #include "components/autofill/core/browser/form_types.h" |
| #include "components/autofill/core/browser/metrics/address_save_metrics.h" |
| #include "components/autofill/core/browser/metrics/fallback_autocomplete_unrecognized_metrics.h" |
| #include "components/autofill/core/browser/metrics/manual_fallback_metrics.h" |
| #include "components/autofill/core/browser/payments_data_manager.h" |
| #include "components/autofill/core/browser/personal_data_manager.h" |
| #include "components/autofill/core/common/aliases.h" |
| #include "components/autofill/core/common/autofill_features.h" |
| #include "components/autofill/core/common/unique_ids.h" |
| #include "components/autofill_prediction_improvements/core/browser/autofill_prediction_improvements_features.h" |
| #include "components/feature_engagement/public/feature_constants.h" |
| #include "components/password_manager/content/browser/content_password_manager_driver.h" |
| #include "components/password_manager/core/browser/features/password_features.h" |
| #include "components/password_manager/core/browser/password_autofill_manager.h" |
| #include "components/password_manager/core/browser/password_manager_client.h" |
| #include "components/password_manager/core/browser/password_manager_util.h" |
| #include "components/password_manager/core/browser/password_manual_fallback_flow.h" |
| #include "components/password_manager/core/browser/password_manual_fallback_metrics_recorder.h" |
| #include "components/password_manager/core/common/password_manager_pref_names.h" |
| #include "components/plus_addresses/features.h" |
| #include "components/plus_addresses/plus_address_service.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "components/variations/service/variations_service.h" |
| #include "components/vector_icons/vector_icons.h" |
| #include "content/public/browser/global_routing_id.h" |
| #include "content/public/browser/web_contents.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/models/image_model.h" |
| #include "ui/base/models/menu_model.h" |
| |
| #if BUILDFLAG(GOOGLE_CHROME_BRANDING) |
| #include "components/plus_addresses/resources/vector_icons.h" |
| #endif |
| |
| namespace autofill { |
| |
| using FillingProductSet = DenseSet<FillingProduct>; |
| |
| namespace { |
| |
| using ::password_manager::ContentPasswordManagerDriver; |
| |
| constexpr char kFeedbackPlaceholder[] = |
| "What steps did you just take?\n" |
| "(1)\n" |
| "(2)\n" |
| "(3)\n" |
| "\n" |
| "What was the expected result?\n" |
| "\n" |
| "What happened instead? (Please include the screenshot below)"; |
| |
| // Constant determining the icon size in the context menu. |
| constexpr int kContextMenuIconSize = 16; |
| |
| #if BUILDFLAG(GOOGLE_CHROME_BRANDING) |
| const gfx::VectorIcon& kPlusAddressLogoIcon = |
| plus_addresses::kPlusAddressLogoSmallIcon; |
| #else |
| const gfx::VectorIcon& kPlusAddressLogoIcon = vector_icons::kEmailIcon; |
| #endif |
| |
| bool ShouldShowAutofillContextMenu(const content::ContextMenuParams& params) { |
| if (!params.form_control_type) { |
| return false; |
| } |
| // Return true (only) on text fields. |
| // |
| // Note that this switch is over `blink::mojom::FormControlType`, not |
| // `autofill::FormControlType`. Therefore, it does not handle |
| // `autofill::FormControlType::kContentEditable`, which is covered by the |
| // above if-condition `!params.form_control_type`. |
| // |
| // TODO(crbug.com/40285492): Unify with functions from form_autofill_util.cc. |
| switch (*params.form_control_type) { |
| case blink::mojom::FormControlType::kInputEmail: |
| case blink::mojom::FormControlType::kInputMonth: |
| case blink::mojom::FormControlType::kInputNumber: |
| case blink::mojom::FormControlType::kInputPassword: |
| case blink::mojom::FormControlType::kInputSearch: |
| case blink::mojom::FormControlType::kInputTelephone: |
| case blink::mojom::FormControlType::kInputText: |
| case blink::mojom::FormControlType::kInputUrl: |
| case blink::mojom::FormControlType::kTextArea: |
| return true; |
| case blink::mojom::FormControlType::kButtonButton: |
| case blink::mojom::FormControlType::kButtonSubmit: |
| case blink::mojom::FormControlType::kButtonReset: |
| case blink::mojom::FormControlType::kButtonPopover: |
| case blink::mojom::FormControlType::kButtonSelectList: |
| case blink::mojom::FormControlType::kFieldset: |
| case blink::mojom::FormControlType::kInputButton: |
| case blink::mojom::FormControlType::kInputCheckbox: |
| case blink::mojom::FormControlType::kInputColor: |
| case blink::mojom::FormControlType::kInputDate: |
| case blink::mojom::FormControlType::kInputDatetimeLocal: |
| case blink::mojom::FormControlType::kInputFile: |
| case blink::mojom::FormControlType::kInputHidden: |
| case blink::mojom::FormControlType::kInputImage: |
| case blink::mojom::FormControlType::kInputRadio: |
| case blink::mojom::FormControlType::kInputRange: |
| case blink::mojom::FormControlType::kInputReset: |
| case blink::mojom::FormControlType::kInputSubmit: |
| case blink::mojom::FormControlType::kInputTime: |
| case blink::mojom::FormControlType::kInputWeek: |
| case blink::mojom::FormControlType::kOutput: |
| case blink::mojom::FormControlType::kSelectOne: |
| case blink::mojom::FormControlType::kSelectMultiple: |
| case blink::mojom::FormControlType::kSelectList: |
| return false; |
| } |
| NOTREACHED(); |
| } |
| |
| // Returns true if the given id is one generated for autofill context menu. |
| bool IsAutofillCustomCommandId( |
| AutofillContextMenuManager::CommandId command_id) { |
| static constexpr auto kAutofillCommands = base::MakeFixedFlatSet<int>({ |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_ADDRESS, |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PAYMENTS, |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PLUS_ADDRESS, |
| IDC_CONTENT_CONTEXT_AUTOFILL_FEEDBACK, |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS, |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_SELECT_PASSWORD, |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_IMPORT_PASSWORDS, |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_SUGGEST_PASSWORD, |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_NO_SAVED_PASSWORDS, |
| IDC_CONTENT_CONTEXT_AUTOFILL_PREDICTION_IMPROVEMENTS, |
| }); |
| return kAutofillCommands.contains(command_id.value()); |
| } |
| |
| bool IsLikelyDogfoodClient() { |
| auto* variations_service = g_browser_process->variations_service(); |
| if (!variations_service) { |
| return false; |
| } |
| return variations_service->IsLikelyDogfoodClient(); |
| } |
| |
| // Returns true if the field is a username or password field. |
| bool IsPasswordFormField(ContentPasswordManagerDriver& password_manager_driver, |
| const content::ContextMenuParams& params) { |
| const autofill::FieldRendererId current_field_renderer_id( |
| params.field_renderer_id); |
| return password_manager_driver.GetPasswordManager() |
| ->GetPasswordFormCache() |
| ->HasPasswordForm(&password_manager_driver, current_field_renderer_id); |
| } |
| |
| // Returns true if the user has autofillable passwords saved. |
| bool UserHasPasswordsSaved( |
| ContentPasswordManagerDriver& password_manager_driver) { |
| password_manager::PasswordManagerClient* client = |
| password_manager_driver.GetPasswordManager()->GetClient(); |
| return client->GetPrefs()->GetBoolean( |
| password_manager::prefs:: |
| kAutofillableCredentialsProfileStoreLoginDatabase) || |
| client->GetPrefs()->GetBoolean( |
| password_manager::prefs:: |
| kAutofillableCredentialsAccountStoreLoginDatabase); |
| } |
| |
| base::Value::Dict LoadTriggerFormAndFieldLogs( |
| AutofillManager& manager, |
| const LocalFrameToken& frame_token, |
| const content::ContextMenuParams& params) { |
| if (!ShouldShowAutofillContextMenu(params)) { |
| return base::Value::Dict(); |
| } |
| |
| FormGlobalId form_global_id = {frame_token, |
| FormRendererId(params.form_renderer_id)}; |
| |
| base::Value::Dict trigger_form_logs; |
| if (FormStructure* form = manager.FindCachedFormById(form_global_id)) { |
| trigger_form_logs.Set("triggerFormSignature", form->FormSignatureAsStr()); |
| |
| if (params.form_control_type) { |
| FieldGlobalId field_global_id = { |
| frame_token, FieldRendererId(params.field_renderer_id)}; |
| auto field = |
| base::ranges::find_if(*form, [&field_global_id](const auto& field) { |
| return field->global_id() == field_global_id; |
| }); |
| if (field != form->end()) { |
| trigger_form_logs.Set("triggerFieldSignature", |
| (*field)->FieldSignatureAsStr()); |
| } |
| } |
| } |
| return trigger_form_logs; |
| } |
| |
| } // namespace |
| |
| AutofillContextMenuManager::AutofillContextMenuManager( |
| PersonalDataManager* personal_data_manager, |
| RenderViewContextMenuBase* delegate, |
| ui::SimpleMenuModel* menu_model) |
| : personal_data_manager_(personal_data_manager), |
| menu_model_(menu_model), |
| delegate_(delegate), |
| passwords_submenu_model_(delegate) { |
| DCHECK(delegate_); |
| params_ = delegate_->params(); |
| } |
| |
| AutofillContextMenuManager::~AutofillContextMenuManager() = default; |
| |
| void AutofillContextMenuManager::AppendItems() { |
| MaybeAddAutofillFeedbackItem(); |
| MaybeAddAutofillManualFallbackItems(); |
| } |
| |
| bool AutofillContextMenuManager::IsCommandIdSupported(int command_id) { |
| return IsAutofillCustomCommandId(CommandId(command_id)); |
| } |
| |
| bool AutofillContextMenuManager::IsCommandIdEnabled(int command_id) { |
| return command_id != |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_NO_SAVED_PASSWORDS; |
| } |
| |
| void AutofillContextMenuManager::ExecuteCommand(int command_id) { |
| content::RenderFrameHost* rfh = delegate_->GetRenderFrameHost(); |
| if (!rfh) { |
| return; |
| } |
| ContentAutofillDriver* autofill_driver = |
| ContentAutofillDriver::GetForRenderFrameHost(rfh); |
| if (!autofill_driver) { |
| return; |
| } |
| CHECK(IsAutofillCustomCommandId(CommandId(command_id))); |
| |
| if (command_id == IDC_CONTENT_CONTEXT_AUTOFILL_PREDICTION_IMPROVEMENTS) { |
| ExecutePredictionImprovementsCommand(autofill_driver->GetFrameToken(), |
| *autofill_driver); |
| return; |
| } |
| |
| if (command_id == IDC_CONTENT_CONTEXT_AUTOFILL_FEEDBACK) { |
| ExecuteAutofillFeedbackCommand(autofill_driver->GetFrameToken(), |
| autofill_driver->GetAutofillManager()); |
| return; |
| } |
| |
| if (command_id == IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_ADDRESS) { |
| ExecuteFallbackForAddressesCommand(*autofill_driver); |
| return; |
| } |
| |
| if (command_id == IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PAYMENTS) { |
| ExecuteFallbackForPaymentsCommand(*autofill_driver); |
| return; |
| } |
| |
| if (command_id == IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PLUS_ADDRESS) { |
| ExecuteFallbackForPlusAddressesCommand(*autofill_driver); |
| return; |
| } |
| |
| if (command_id == |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_SELECT_PASSWORD) { |
| ExecuteFallbackForSelectPasswordCommand(*autofill_driver); |
| return; |
| } |
| |
| content::WebContents* web_contents = |
| content::WebContents::FromRenderFrameHost(rfh); |
| if (command_id == |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_IMPORT_PASSWORDS) { |
| // This function also records metrics. |
| NavigateToManagePasswordsPage( |
| chrome::FindBrowserWithTab(web_contents), |
| password_manager::ManagePasswordsReferrer::kPasswordContextMenu); |
| return; |
| } |
| |
| if (command_id == |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_SUGGEST_PASSWORD) { |
| // This function also records metrics. |
| password_manager_util::UserTriggeredManualGenerationFromContextMenu( |
| ChromePasswordManagerClient::FromWebContents(web_contents), |
| autofill::ContentAutofillClient::FromWebContents(web_contents)); |
| return; |
| } |
| } |
| |
| void AutofillContextMenuManager::MaybeAddAutofillFeedbackItem() { |
| content::RenderFrameHost* rfh = delegate_->GetRenderFrameHost(); |
| if (!rfh) { |
| return; |
| } |
| |
| ContentAutofillDriver* autofill_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 (!autofill_driver || !autofill_driver->CanShowAutofillUi()) { |
| return; |
| } |
| |
| // Includes the option of submitting feedback on Autofill. |
| if (static_cast<BrowserAutofillManager&>( |
| autofill_driver->GetAutofillManager()) |
| .IsAutofillEnabled() && |
| IsLikelyDogfoodClient()) { |
| menu_model_->AddItemWithStringIdAndIcon( |
| IDC_CONTENT_CONTEXT_AUTOFILL_FEEDBACK, |
| IDS_CONTENT_CONTEXT_AUTOFILL_FEEDBACK, |
| ui::ImageModel::FromVectorIcon(vector_icons::kDogfoodIcon)); |
| |
| menu_model_->AddSeparator(ui::NORMAL_SEPARATOR); |
| } |
| } |
| |
| void AutofillContextMenuManager::MaybeAddAutofillManualFallbackItems() { |
| if (!ShouldShowAutofillContextMenu(params_)) { |
| // Autofill entries are only available in input or text area fields |
| return; |
| } |
| |
| content::RenderFrameHost* rfh = delegate_->GetRenderFrameHost(); |
| if (!rfh) { |
| return; |
| } |
| |
| ContentAutofillDriver* autofill_driver = |
| ContentAutofillDriver::GetForRenderFrameHost(rfh); |
| ContentPasswordManagerDriver* password_manager_driver = |
| ContentPasswordManagerDriver::GetForRenderFrameHost(rfh); |
| |
| bool add_plus_address_fallback = false; |
| bool add_address_fallback = false; |
| bool add_payments_fallback = false; |
| bool add_passwords_fallback = false; |
| bool add_prediction_improvements = false; |
| |
| // Do not show autofill context menu options for input fields that cannot be |
| // filled by the driver. See crbug.com/1367547. |
| if (autofill_driver && autofill_driver->CanShowAutofillUi()) { |
| auto* web_contents = content::WebContents::FromRenderFrameHost( |
| autofill_driver->render_frame_host()); |
| add_prediction_improvements = ShouldAddPredictionImprovementsItem( |
| autofill_driver->GetAutofillClient() |
| .GetAutofillPredictionImprovementsDelegate(), |
| web_contents->GetPrimaryMainFrame()->GetLastCommittedURL()); |
| add_plus_address_fallback = |
| ShouldAddPlusAddressManualFallbackItem(*autofill_driver); |
| add_address_fallback = ShouldAddAddressManualFallbackItem(*autofill_driver); |
| add_payments_fallback = |
| personal_data_manager_->payments_data_manager() |
| .IsAutofillPaymentMethodsEnabled() && |
| !personal_data_manager_->payments_data_manager() |
| .GetCreditCardsToSuggest() |
| .empty() && |
| base::FeatureList::IsEnabled( |
| features::kAutofillForUnclassifiedFieldsAvailable); |
| } |
| |
| // Do not show password manager context menu options for input fields that |
| // cannot be filled by the driver. See crbug.com/1367547. |
| if (password_manager_driver && password_manager_driver->CanShowAutofillUi()) { |
| add_passwords_fallback = |
| ShouldAddPasswordsManualFallbackItem(*password_manager_driver); |
| } |
| |
| if (!add_plus_address_fallback && !add_address_fallback && |
| !add_payments_fallback && !add_passwords_fallback && |
| !add_prediction_improvements) { |
| return; |
| } |
| menu_model_->AddTitle( |
| l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_AUTOFILL_FALLBACK_TITLE)); |
| |
| if (add_prediction_improvements) { |
| menu_model_->AddItemWithStringIdAndIcon( |
| IDC_CONTENT_CONTEXT_AUTOFILL_PREDICTION_IMPROVEMENTS, |
| IDS_CONTENT_CONTEXT_AUTOFILL_PREDICTION_IMPROVEMENTS, |
| ui::ImageModel::FromVectorIcon( |
| vector_icons::kLocationOnChromeRefreshIcon, ui::kColorIcon, |
| kContextMenuIconSize)); |
| } |
| |
| if (add_address_fallback) { |
| menu_model_->AddItemWithStringIdAndIcon( |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_ADDRESS, |
| IDS_CONTENT_CONTEXT_AUTOFILL_FALLBACK_ADDRESS, |
| ui::ImageModel::FromVectorIcon( |
| vector_icons::kLocationOnChromeRefreshIcon, ui::kColorIcon, |
| kContextMenuIconSize)); |
| menu_model_->SetIsNewFeatureAt( |
| menu_model_->GetItemCount() - 1, |
| UserEducationService::MaybeShowNewBadge( |
| delegate_->GetBrowserContext(), |
| features::kAutofillForUnclassifiedFieldsAvailable)); |
| |
| LogAddressManualFallbackContextMenuEntryShown(CHECK_DEREF(autofill_driver)); |
| } |
| if (add_payments_fallback) { |
| menu_model_->AddItemWithStringIdAndIcon( |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PAYMENTS, |
| IDS_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PAYMENTS, |
| ui::ImageModel::FromVectorIcon(kCreditCardChromeRefreshIcon, |
| ui::kColorIcon, kContextMenuIconSize)); |
| menu_model_->SetIsNewFeatureAt( |
| menu_model_->GetItemCount() - 1, |
| UserEducationService::MaybeShowNewBadge( |
| delegate_->GetBrowserContext(), |
| features::kAutofillForUnclassifiedFieldsAvailable)); |
| |
| LogPaymentsManualFallbackContextMenuEntryShown( |
| CHECK_DEREF(autofill_driver)); |
| } |
| if (add_passwords_fallback) { |
| AddPasswordsManualFallbackItems(*password_manager_driver); |
| |
| const bool select_passwords_option_shown = |
| UserHasPasswordsSaved(*password_manager_driver); |
| if (select_passwords_option_shown) { |
| LogSelectPasswordManualFallbackContextMenuEntryShown( |
| CHECK_DEREF(password_manager_driver)); |
| } |
| } |
| if (add_plus_address_fallback) { |
| menu_model_->AddItemWithStringIdAndIcon( |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PLUS_ADDRESS, |
| IDS_PLUS_ADDRESS_FALLBACK_LABEL_CONTEXT_MENU, |
| ui::ImageModel::FromVectorIcon(kPlusAddressLogoIcon, ui::kColorIcon, |
| kContextMenuIconSize)); |
| menu_model_->SetIsNewFeatureAt( |
| menu_model_->GetItemCount() - 1, |
| UserEducationService::MaybeShowNewBadge( |
| delegate_->GetBrowserContext(), |
| plus_addresses::features::kPlusAddressFallbackFromContextMenu)); |
| // TODO(crbug.com/327566698): Log metrics for plus address fallbacks, too. |
| } |
| menu_model_->AddSeparator(ui::NORMAL_SEPARATOR); |
| } |
| |
| bool AutofillContextMenuManager::ShouldAddPlusAddressManualFallbackItem( |
| ContentAutofillDriver& autofill_driver) { |
| if (params_.form_control_type && |
| params_.form_control_type.value() == |
| blink::mojom::FormControlType::kInputPassword) { |
| return false; |
| } |
| |
| auto* web_contents = content::WebContents::FromRenderFrameHost( |
| autofill_driver.render_frame_host()); |
| const plus_addresses::PlusAddressService* plus_address_service = |
| PlusAddressServiceFactory::GetForBrowserContext( |
| web_contents->GetBrowserContext()); |
| AutofillClient& client = autofill_driver.GetAutofillManager().client(); |
| return plus_address_service && |
| plus_address_service->ShouldShowManualFallback( |
| client.GetLastCommittedPrimaryMainFrameOrigin(), |
| client.IsOffTheRecord()) && |
| base::FeatureList::IsEnabled( |
| plus_addresses::features::kPlusAddressFallbackFromContextMenu); |
| } |
| |
| bool AutofillContextMenuManager::ShouldAddPredictionImprovementsItem( |
| AutofillPredictionImprovementsDelegate* delegate, |
| const GURL& url) { |
| return delegate && delegate->ShouldProvidePredictionImprovements(url); |
| } |
| |
| bool AutofillContextMenuManager::ShouldAddAddressManualFallbackItem( |
| ContentAutofillDriver& autofill_driver) { |
| if (!personal_data_manager_->address_data_manager() |
| .IsAutofillProfileEnabled()) { |
| return false; |
| } |
| |
| // If the field is of address type and there is information in the profile to |
| // fill it, we always show the fallback option. |
| // TODO(crbug.com/40285811): Remove the following code block once feature is |
| // cleaned up. At that point, we can only check whether a profile exists or if |
| // the user is not in incognito mode. Whether the field can be filled will be |
| // irrelevant. |
| AutofillField* field = GetAutofillField(autofill_driver); |
| if (field && FieldTypeGroupToFormType(field->Type().group()) == |
| FormType::kAddressForm) { |
| // Show the context menu entry for address fields, which can be filled |
| // with at least one of the user's profiles. |
| CHECK(personal_data_manager_); |
| if (std::ranges::any_of( |
| personal_data_manager_->address_data_manager().GetProfiles(), |
| [field](const AutofillProfile* profile) { |
| return profile->HasInfo(field->Type().GetStorableType()); |
| })) { |
| return true; |
| } |
| } |
| |
| // Also add the manual fallback option if: |
| // 1. The user has a profile stored, or |
| // 2. The user does not have a profile stored and is not in incognito mode. |
| // This is done so that users can be prompted to create an address profile. |
| const bool has_profile = |
| !personal_data_manager_->address_data_manager().GetProfiles().empty(); |
| const bool is_incognito = |
| autofill_driver.GetAutofillManager().client().IsOffTheRecord(); |
| return (has_profile || !is_incognito) && |
| base::FeatureList::IsEnabled( |
| features::kAutofillForUnclassifiedFieldsAvailable); |
| } |
| |
| bool AutofillContextMenuManager::ShouldAddPasswordsManualFallbackItem( |
| ContentPasswordManagerDriver& password_manager_driver) { |
| return password_manager_driver.GetPasswordManager() |
| ->GetClient() |
| ->IsFillingEnabled( |
| password_manager_driver.GetLastCommittedURL()) && |
| base::FeatureList::IsEnabled( |
| password_manager::features::kPasswordManualFallbackAvailable); |
| } |
| |
| void AutofillContextMenuManager::AddPasswordsManualFallbackItems( |
| ContentPasswordManagerDriver& password_manager_driver) { |
| // If the password generation feature is enabled for this user, the context |
| // menu entry is displayed only if the field is also a password. The password |
| // generation button would be a no-op on non-password fields. |
| const bool password_generation_enabled_for_current_field = |
| password_manager_util::ManualPasswordGenerationEnabled( |
| &password_manager_driver) && |
| (params_.form_control_type == |
| blink::mojom::FormControlType::kInputPassword || |
| params_.is_password_type_by_heuristics); |
| const bool user_has_passwords_saved = |
| UserHasPasswordsSaved(password_manager_driver); |
| const bool add_select_password_submenu_option = |
| password_generation_enabled_for_current_field && user_has_passwords_saved; |
| const bool add_import_passwords_submenu_option = !user_has_passwords_saved; |
| const bool add_submenu = |
| add_select_password_submenu_option || add_import_passwords_submenu_option; |
| const ui::ImageModel password_manager_icon = ui::ImageModel::FromVectorIcon( |
| vector_icons::kPasswordManagerIcon, ui::kColorIcon, kContextMenuIconSize); |
| |
| if (add_select_password_submenu_option) { |
| passwords_submenu_model_.AddItemWithStringId( |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_SELECT_PASSWORD, |
| IDS_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_SELECT_PASSWORD); |
| } else if (add_import_passwords_submenu_option) { |
| // This entry is disabled (i.e. it is greyed out and doesn't do anything |
| // upon clicking). The logic which disables it is in |
| // `AutofillContextMenuManager::IsCommandIdEnabled()`. |
| passwords_submenu_model_.AddItemWithStringId( |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_NO_SAVED_PASSWORDS, |
| IDS_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_NO_SAVED_PASSWORDS); |
| passwords_submenu_model_.AddItemWithStringId( |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_IMPORT_PASSWORDS, |
| IDS_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_IMPORT_PASSWORDS); |
| } |
| |
| if (password_generation_enabled_for_current_field) { |
| CHECK(add_submenu); |
| passwords_submenu_model_.AddItemWithStringId( |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_SUGGEST_PASSWORD, |
| IDS_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_SUGGEST_PASSWORD); |
| } |
| |
| if (add_submenu) { |
| menu_model_->AddSubMenuWithStringIdAndIcon( |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS, |
| IDS_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS, |
| &passwords_submenu_model_, password_manager_icon); |
| } else { |
| menu_model_->AddItemWithStringIdAndIcon( |
| IDC_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS_SELECT_PASSWORD, |
| IDS_CONTENT_CONTEXT_AUTOFILL_FALLBACK_PASSWORDS, password_manager_icon); |
| } |
| |
| // Note that the code above adds exactly one entry to `menu_model_` (any other |
| // entries are added to the submenu) and the goal is to display the "NEW" |
| // badge for this entry. |
| menu_model_->SetIsNewFeatureAt( |
| menu_model_->GetItemCount() - 1, |
| UserEducationService::MaybeShowNewBadge( |
| delegate_->GetBrowserContext(), |
| password_manager::features::kPasswordManualFallbackAvailable)); |
| } |
| |
| void AutofillContextMenuManager::LogAddressManualFallbackContextMenuEntryShown( |
| ContentAutofillDriver& autofill_driver) { |
| AutofillField* field = GetAutofillField(autofill_driver); |
| const bool address_option_shown_for_field_not_classified_as_address = |
| !IsAddressType(field ? field->Type().GetStorableType() : UNKNOWN_TYPE); |
| |
| if (address_option_shown_for_field_not_classified_as_address) { |
| static_cast<BrowserAutofillManager&>(autofill_driver.GetAutofillManager()) |
| .GetManualFallbackEventLogger() |
| .ContextMenuEntryShown(FillingProduct::kAddress); |
| } else { |
| // Only use AutocompleteUnrecognizedFallbackEventLogger if the address |
| // option was shown on a field that WAS classified as an address. |
| static_cast<BrowserAutofillManager&>(autofill_driver.GetAutofillManager()) |
| .GetAutocompleteUnrecognizedFallbackEventLogger() |
| .ContextMenuEntryShown( |
| /*address_field_has_ac_unrecognized=*/field |
| ->ShouldSuppressSuggestionsAndFillingByDefault()); |
| } |
| } |
| |
| void AutofillContextMenuManager::LogPaymentsManualFallbackContextMenuEntryShown( |
| ContentAutofillDriver& autofill_driver) { |
| AutofillField* field = GetAutofillField(autofill_driver); |
| const bool payments_option_shown_for_field_not_classified_as_payments = |
| !field || !FieldTypeGroupSet({FieldTypeGroup::kCreditCard, |
| FieldTypeGroup::kStandaloneCvcField}) |
| .contains(field->Type().group()); |
| |
| if (payments_option_shown_for_field_not_classified_as_payments) { |
| static_cast<BrowserAutofillManager&>(autofill_driver.GetAutofillManager()) |
| .GetManualFallbackEventLogger() |
| .ContextMenuEntryShown(FillingProduct::kCreditCard); |
| } |
| } |
| |
| void AutofillContextMenuManager:: |
| LogSelectPasswordManualFallbackContextMenuEntryShown( |
| ContentPasswordManagerDriver& password_manager_driver) { |
| password_manager_driver.GetPasswordAutofillManager() |
| ->GetPasswordManualFallbackMetricsRecorder() |
| .ContextMenuEntryShown( |
| /*classified_as_target_filling_password=*/ |
| IsPasswordFormField(password_manager_driver, params_)); |
| } |
| |
| void AutofillContextMenuManager:: |
| LogAddressManualFallbackContextMenuEntryAccepted( |
| AutofillDriver& autofill_driver) { |
| BrowserAutofillManager& manager = static_cast<BrowserAutofillManager&>( |
| autofill_driver.GetAutofillManager()); |
| AutofillField* field = GetAutofillField(autofill_driver); |
| |
| const bool is_address_field = |
| field && IsAddressType(field->Type().GetStorableType()); |
| if (is_address_field) { |
| // Address manual fallback was triggered from a classified address |
| // field. |
| manager.GetAutocompleteUnrecognizedFallbackEventLogger() |
| .ContextMenuEntryAccepted( |
| /*address_field_has_ac_unrecognized=*/field |
| ->ShouldSuppressSuggestionsAndFillingByDefault()); |
| } else { |
| manager.GetManualFallbackEventLogger().ContextMenuEntryAccepted( |
| FillingProduct::kAddress); |
| } |
| } |
| |
| void AutofillContextMenuManager:: |
| LogPaymentsManualFallbackContextMenuEntryAccepted( |
| AutofillDriver& autofill_driver) { |
| BrowserAutofillManager& manager = static_cast<BrowserAutofillManager&>( |
| autofill_driver.GetAutofillManager()); |
| AutofillField* field = GetAutofillField(autofill_driver); |
| |
| if (!field || !FieldTypeGroupSet{FieldTypeGroup::kCreditCard, |
| FieldTypeGroup::kStandaloneCvcField} |
| .contains(field->Type().group())) { |
| // Only log payments manual fallback when triggered from a field that is |
| // not classified as payments. |
| manager.GetManualFallbackEventLogger().ContextMenuEntryAccepted( |
| FillingProduct::kCreditCard); |
| } |
| } |
| |
| void AutofillContextMenuManager:: |
| LogSelectPasswordManualFallbackContextMenuEntryAccepted() { |
| content::RenderFrameHost* rfh = delegate_->GetRenderFrameHost(); |
| ContentPasswordManagerDriver* password_manager_driver = |
| rfh ? ContentPasswordManagerDriver::GetForRenderFrameHost(rfh) : nullptr; |
| |
| if (password_manager_driver) { |
| password_manager_driver->GetPasswordAutofillManager() |
| ->GetPasswordManualFallbackMetricsRecorder() |
| .ContextMenuEntryAccepted(/*classified_as_target_filling_password=*/ |
| IsPasswordFormField(*password_manager_driver, |
| params_)); |
| } |
| } |
| |
| void AutofillContextMenuManager::ExecutePredictionImprovementsCommand( |
| const LocalFrameToken& frame_token, |
| ContentAutofillDriver& autofill_driver) { |
| autofill_driver.browser_events().RendererShouldTriggerSuggestions( |
| FieldGlobalId(frame_token, FieldRendererId(params_.field_renderer_id)), |
| AutofillSuggestionTriggerSource::kPredictionImprovements); |
| } |
| |
| void AutofillContextMenuManager::ExecuteAutofillFeedbackCommand( |
| const LocalFrameToken& frame_token, |
| AutofillManager& manager) { |
| // The cast is safe since the context menu is only available on Desktop. |
| auto& client = static_cast<ContentAutofillClient&>(manager.client()); |
| Browser* browser = chrome::FindBrowserWithTab(&client.GetWebContents()); |
| chrome::ShowFeedbackPage( |
| browser, feedback::kFeedbackSourceAutofillContextMenu, |
| /*description_template=*/std::string(), |
| /*description_placeholder_text=*/kFeedbackPlaceholder, |
| /*category_tag=*/"dogfood_autofill_feedback", |
| /*extra_diagnostics=*/std::string(), |
| /*autofill_metadata=*/ |
| data_logs::FetchAutofillFeedbackData( |
| &manager, |
| LoadTriggerFormAndFieldLogs(manager, frame_token, params_))); |
| } |
| |
| void AutofillContextMenuManager::ExecuteFallbackForPlusAddressesCommand( |
| AutofillDriver& autofill_driver) { |
| autofill_driver.RendererShouldTriggerSuggestions( |
| /*field_id=*/{autofill_driver.GetFrameToken(), |
| FieldRendererId(params_.field_renderer_id)}, |
| AutofillSuggestionTriggerSource::kManualFallbackPlusAddresses); |
| |
| // TODO(crbug.com/327566698): Add metrics for plus addresses. |
| UserEducationService::MaybeNotifyPromoFeatureUsed( |
| delegate_->GetBrowserContext(), |
| plus_addresses::features::kPlusAddressFallbackFromContextMenu); |
| } |
| |
| void AutofillContextMenuManager::ExecuteFallbackForPaymentsCommand( |
| AutofillDriver& autofill_driver) { |
| autofill_driver.RendererShouldTriggerSuggestions( |
| /*field_id=*/{autofill_driver.GetFrameToken(), |
| FieldRendererId(params_.field_renderer_id)}, |
| AutofillSuggestionTriggerSource::kManualFallbackPayments); |
| |
| LogPaymentsManualFallbackContextMenuEntryAccepted(autofill_driver); |
| UserEducationService::MaybeNotifyPromoFeatureUsed( |
| delegate_->GetBrowserContext(), |
| features::kAutofillForUnclassifiedFieldsAvailable); |
| } |
| |
| void AutofillContextMenuManager::ExecuteFallbackForSelectPasswordCommand( |
| AutofillDriver& autofill_driver) { |
| autofill_driver.RendererShouldTriggerSuggestions( |
| /*field_id=*/{autofill_driver.GetFrameToken(), |
| FieldRendererId(params_.field_renderer_id)}, |
| AutofillSuggestionTriggerSource::kManualFallbackPasswords); |
| |
| LogSelectPasswordManualFallbackContextMenuEntryAccepted(); |
| UserEducationService::MaybeNotifyPromoFeatureUsed( |
| delegate_->GetBrowserContext(), |
| password_manager::features::kPasswordManualFallbackAvailable); |
| } |
| |
| void AutofillContextMenuManager::ExecuteFallbackForAddressesCommand( |
| ContentAutofillDriver& autofill_driver) { |
| AutofillField* field = GetAutofillField(autofill_driver); |
| if (!field && !base::FeatureList::IsEnabled( |
| features::kAutofillForUnclassifiedFieldsAvailable)) { |
| // The field should generally exist, since the fallback option is only shown |
| // when the field can be retrieved. But if the website removed the field |
| // before the entry was select, it might not be available anymore. |
| // |
| // Note that, when `features::kAutofillForUnclassifiedFieldsAvailable` is |
| // enabled Autofill is always available, regardless of whether |
| // `AutofillField` exists or not. |
| return; |
| } |
| |
| if (personal_data_manager_->address_data_manager().GetProfiles().empty() && |
| base::FeatureList::IsEnabled( |
| features::kAutofillForUnclassifiedFieldsAvailable)) { |
| content::RenderFrameHost* rfh = autofill_driver.render_frame_host(); |
| auto* web_contents = content::WebContents::FromRenderFrameHost(rfh); |
| AddressBubblesController::SetUpAndShowAddNewAddressBubble( |
| web_contents, |
| base::BindOnce( |
| [](AddressDataManager* adm, |
| content::GlobalRenderFrameHostId frame_id, |
| uint64_t field_renderer_id, |
| AutofillClient::AddressPromptUserDecision decision, |
| base::optional_ref<const AutofillProfile> profile) { |
| bool new_address_saved = |
| decision == |
| AutofillClient::AddressPromptUserDecision::kEditAccepted; |
| if (new_address_saved && profile.has_value()) { |
| adm->AddChangeCallback(base::BindOnce( |
| [](content::GlobalRenderFrameHostId frame_id, |
| uint64_t field_renderer_id) { |
| content::RenderFrameHost* rfh = |
| content::RenderFrameHost::FromID(frame_id); |
| if (!rfh) { |
| return; |
| } |
| AutofillDriver* driver = |
| ContentAutofillDriver::GetForRenderFrameHost(rfh); |
| if (!driver) { |
| return; |
| } |
| |
| driver->RendererShouldTriggerSuggestions( |
| /*field_id=*/{driver->GetFrameToken(), |
| FieldRendererId(field_renderer_id)}, |
| AutofillSuggestionTriggerSource:: |
| kManualFallbackAddress); |
| }, |
| frame_id, field_renderer_id)); |
| adm->AddProfile(*profile); |
| } |
| |
| LogAddNewAddressPromptOutcome( |
| new_address_saved |
| ? autofill_metrics::AutofillAddNewAddressPromptOutcome:: |
| kSaved |
| : autofill_metrics::AutofillAddNewAddressPromptOutcome:: |
| kCanceled); |
| |
| if (new_address_saved) { |
| autofill_metrics::LogManuallyAddedAddress( |
| autofill_metrics::AutofillManuallyAddedAddressSurface:: |
| kContextMenuPrompt); |
| } |
| }, |
| // `PersonalDataManager`, as a keyed service, will always outlive |
| // the bubble, which is bound to a tab. |
| &personal_data_manager_->address_data_manager(), rfh->GetGlobalId(), |
| params_.field_renderer_id)); |
| } else { |
| autofill_driver.browser_events().RendererShouldTriggerSuggestions( |
| /*field_id=*/{autofill_driver.GetFrameToken(), |
| FieldRendererId(params_.field_renderer_id)}, |
| AutofillSuggestionTriggerSource::kManualFallbackAddress); |
| } |
| |
| LogAddressManualFallbackContextMenuEntryAccepted(autofill_driver); |
| UserEducationService::MaybeNotifyPromoFeatureUsed( |
| delegate_->GetBrowserContext(), |
| features::kAutofillForUnclassifiedFieldsAvailable); |
| } |
| |
| AutofillField* AutofillContextMenuManager::GetAutofillField( |
| AutofillDriver& autofill_driver) const { |
| CHECK(ShouldShowAutofillContextMenu(params_)); |
| const LocalFrameToken frame_token = autofill_driver.GetFrameToken(); |
| FormStructure* form = autofill_driver.GetAutofillManager().FindCachedFormById( |
| {frame_token, FormRendererId(params_.form_renderer_id)}); |
| return form ? form->GetFieldById( |
| {frame_token, FieldRendererId(params_.field_renderer_id)}) |
| : nullptr; |
| } |
| |
| } // namespace autofill |