| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/password_manager/android/password_accessory_controller_impl.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/callback.h" |
| #include "base/containers/span.h" |
| #include "base/feature_list.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/autofill/manual_filling_controller.h" |
| #include "chrome/browser/autofill/manual_filling_utils.h" |
| #include "chrome/browser/password_manager/android/password_accessory_controller.h" |
| #include "chrome/browser/password_manager/android/password_accessory_metrics_util.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/profiles/profile.h" |
| #include "chrome/browser/ui/passwords/manage_passwords_view_utils.h" |
| #include "chrome/browser/vr/vr_tab_helper.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/autofill/core/browser/ui/accessory_sheet_data.h" |
| #include "components/autofill/core/browser/ui/accessory_sheet_enums.h" |
| #include "components/autofill/core/common/autofill_features.h" |
| #include "components/autofill/core/common/autofill_util.h" |
| #include "components/autofill/core/common/password_generation_util.h" |
| #include "components/password_manager/content/browser/content_password_manager_driver.h" |
| #include "components/password_manager/content/browser/content_password_manager_driver_factory.h" |
| #include "components/password_manager/core/browser/android_affiliation/affiliation_utils.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/common/password_manager_features.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "components/url_formatter/elide_url.h" |
| #include "content/public/browser/web_contents.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| using autofill::AccessorySheetData; |
| using autofill::FooterCommand; |
| using autofill::UserInfo; |
| using autofill::mojom::FocusedFieldType; |
| using password_manager::CredentialCache; |
| using password_manager::PasswordStore; |
| using password_manager::UiCredential; |
| using BlacklistedStatus = |
| password_manager::OriginCredentialStore::BlacklistedStatus; |
| using FillingSource = ManualFillingController::FillingSource; |
| using IsPslMatch = autofill::UserInfo::IsPslMatch; |
| |
| namespace { |
| |
| autofill::UserInfo TranslateCredentials(bool current_field_is_password, |
| const url::Origin& frame_origin, |
| const UiCredential& credential) { |
| DCHECK(!credential.origin().opaque()); |
| UserInfo user_info(credential.origin().Serialize(), |
| credential.is_public_suffix_match()); |
| |
| base::string16 username = GetDisplayUsername(credential); |
| user_info.add_field( |
| UserInfo::Field(username, username, /*is_password=*/false, |
| /*selectable=*/!credential.username().empty() && |
| !current_field_is_password)); |
| |
| user_info.add_field(UserInfo::Field( |
| credential.password(), |
| l10n_util::GetStringFUTF16( |
| IDS_PASSWORD_MANAGER_ACCESSORY_PASSWORD_DESCRIPTION, username), |
| /*is_password=*/true, /*selectable=*/current_field_is_password)); |
| |
| return user_info; |
| } |
| |
| base::string16 GetTitle(bool has_suggestions, const url::Origin& origin) { |
| const base::string16 elided_url = |
| url_formatter::FormatOriginForSecurityDisplay( |
| origin, url_formatter::SchemeDisplay::OMIT_CRYPTOGRAPHIC); |
| return l10n_util::GetStringFUTF16( |
| has_suggestions |
| ? IDS_PASSWORD_MANAGER_ACCESSORY_PASSWORD_LIST_TITLE |
| : IDS_PASSWORD_MANAGER_ACCESSORY_PASSWORD_LIST_EMPTY_MESSAGE, |
| elided_url); |
| } |
| |
| } // namespace |
| |
| PasswordAccessoryControllerImpl::~PasswordAccessoryControllerImpl() = default; |
| |
| void PasswordAccessoryControllerImpl::OnFillingTriggered( |
| const autofill::UserInfo::Field& selection) { |
| if (!AppearsInSuggestions(selection.display_text(), selection.is_obfuscated(), |
| GetFocusedFrameOrigin())) { |
| NOTREACHED() << "Tried to fill '" << selection.display_text() << "' into " |
| << GetFocusedFrameOrigin(); |
| return; // Never fill across different origins! |
| } |
| |
| password_manager::ContentPasswordManagerDriverFactory* factory = |
| password_manager::ContentPasswordManagerDriverFactory::FromWebContents( |
| web_contents_); |
| password_manager::ContentPasswordManagerDriver* driver = |
| factory->GetDriverForFrame(web_contents_->GetFocusedFrame()); |
| driver->FillIntoFocusedField(selection.is_obfuscated(), |
| selection.display_text()); |
| } |
| |
| // static |
| bool PasswordAccessoryController::AllowedForWebContents( |
| content::WebContents* web_contents) { |
| DCHECK(web_contents) << "Need valid WebContents to attach controller to!"; |
| // TODO(crbug.com/902305): Re-enable if possible. |
| return !vr::VrTabHelper::IsInVr(web_contents); |
| } |
| |
| // static |
| PasswordAccessoryController* PasswordAccessoryController::GetOrCreate( |
| content::WebContents* web_contents, |
| password_manager::CredentialCache* credential_cache) { |
| DCHECK(PasswordAccessoryController::AllowedForWebContents(web_contents)); |
| |
| 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, |
| password_manager::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)))); |
| } |
| } |
| |
| // static |
| void PasswordAccessoryControllerImpl::CreateForWebContentsForTesting( |
| content::WebContents* web_contents, |
| password_manager::CredentialCache* credential_cache, |
| base::WeakPtr<ManualFillingController> mf_controller, |
| password_manager::PasswordManagerClient* password_client) { |
| DCHECK(web_contents) << "Need valid WebContents to attach controller to!"; |
| DCHECK(!FromWebContents(web_contents)) << "Controller already attached!"; |
| DCHECK(mf_controller); |
| DCHECK(password_client); |
| |
| web_contents->SetUserData( |
| UserDataKey(), base::WrapUnique(new PasswordAccessoryControllerImpl( |
| web_contents, credential_cache, |
| std::move(mf_controller), password_client))); |
| } |
| |
| // static |
| bool PasswordAccessoryControllerImpl::ShouldAcceptFocusEvent( |
| content::WebContents* web_contents, |
| password_manager::ContentPasswordManagerDriver* driver, |
| FocusedFieldType focused_field_type) { |
| // Only react to focus events that are sent for the current focused frame. |
| // This is used to make sure that obsolette events that come in an unexpected |
| // order are not processed. Example: (Frame1, focus) -> (Frame2, focus) -> |
| // (Frame1, unfocus) would otherwise unset all the data set for Frame2, which |
| // would be wrong. |
| if (web_contents->GetFocusedFrame() && |
| driver->render_frame_host() == web_contents->GetFocusedFrame()) |
| return true; |
| |
| // The one event that is accepted even if there is no focused frame is an |
| // "unfocus" event that resulted in all frames being unfocused. This can be |
| // used to reset the state of the accessory. |
| if (!web_contents->GetFocusedFrame() && |
| focused_field_type == FocusedFieldType::kUnknown) |
| return true; |
| return false; |
| } |
| |
| void PasswordAccessoryControllerImpl::OnOptionSelected( |
| autofill::AccessoryAction selected_action) { |
| if (selected_action == autofill::AccessoryAction::MANAGE_PASSWORDS) { |
| password_manager_launcher::ShowPasswordSettings( |
| web_contents_, |
| password_manager::ManagePasswordsReferrer::kPasswordsAccessorySheet); |
| return; |
| } |
| if (selected_action == autofill::AccessoryAction::GENERATE_PASSWORD_MANUAL) { |
| OnGenerationRequested( |
| autofill::password_generation::PasswordGenerationType::kManual); |
| GetManualFillingController()->Hide(); |
| return; |
| } |
| if (selected_action == |
| autofill::AccessoryAction::GENERATE_PASSWORD_AUTOMATIC) { |
| OnGenerationRequested( |
| autofill::password_generation::PasswordGenerationType::kAutomatic); |
| GetManualFillingController()->Hide(); |
| return; |
| } |
| NOTREACHED() << "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() << "Unhandled selected action: " |
| << static_cast<int>(toggled_action); |
| } |
| |
| void PasswordAccessoryControllerImpl::RefreshSuggestionsForField( |
| FocusedFieldType focused_field_type, |
| bool is_manual_generation_available) { |
| // 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 (web_contents_->GetFocusedFrame() == nullptr) |
| return; |
| url::Origin origin = GetFocusedFrameOrigin(); |
| if (origin.opaque()) |
| return; // Don't proceed for invalid origins. |
| std::vector<UserInfo> info_to_add; |
| std::vector<FooterCommand> footer_commands_to_add; |
| |
| const bool is_password_field = |
| focused_field_type == FocusedFieldType::kFillablePasswordField; |
| |
| if (autofill::IsFillable(focused_field_type)) { |
| base::span<const UiCredential> suggestions = |
| credential_cache_->GetCredentialStore(origin).GetCredentials(); |
| info_to_add.reserve(suggestions.size()); |
| for (const auto& credential : suggestions) { |
| if (credential.is_public_suffix_match() && |
| !base::FeatureList::IsEnabled( |
| autofill::features::kAutofillKeyboardAccessory)) { |
| continue; // PSL origins have no representation in V1. Don't show them! |
| } |
| info_to_add.push_back( |
| TranslateCredentials(is_password_field, origin, credential)); |
| } |
| } |
| |
| if (is_password_field && is_manual_generation_available) { |
| base::string16 generate_password_title = l10n_util::GetStringUTF16( |
| IDS_PASSWORD_MANAGER_ACCESSORY_GENERATE_PASSWORD_BUTTON_TITLE); |
| footer_commands_to_add.push_back( |
| FooterCommand(generate_password_title, |
| autofill::AccessoryAction::GENERATE_PASSWORD_MANUAL)); |
| } |
| |
| base::string16 manage_passwords_title = l10n_util::GetStringUTF16( |
| IDS_PASSWORD_MANAGER_ACCESSORY_ALL_PASSWORDS_LINK); |
| footer_commands_to_add.push_back(FooterCommand( |
| manage_passwords_title, autofill::AccessoryAction::MANAGE_PASSWORDS)); |
| |
| bool has_suggestions = !info_to_add.empty(); |
| AccessorySheetData data = autofill::CreateAccessorySheetData( |
| autofill::AccessoryTabType::PASSWORDS, GetTitle(has_suggestions, origin), |
| std::move(info_to_add), std::move(footer_commands_to_add)); |
| |
| if (base::FeatureList::IsEnabled( |
| password_manager::features::kRecoverFromNeverSaveAndroid) && |
| base::FeatureList::IsEnabled( |
| autofill::features::kAutofillKeyboardAccessory) && |
| is_password_field && |
| password_client_->IsSavingAndFillingEnabled(origin.GetURL())) { |
| BlacklistedStatus blacklisted_status = |
| credential_cache_->GetCredentialStore(origin).GetBlacklistedStatus(); |
| if (blacklisted_status == BlacklistedStatus::kWasBlacklisted || |
| blacklisted_status == BlacklistedStatus::kIsBlacklisted) { |
| bool enabled = (blacklisted_status == BlacklistedStatus::kWasBlacklisted); |
| if (!enabled) { |
| UMA_HISTOGRAM_BOOLEAN( |
| "KeyboardAccessory.DisabledSavingAccessoryImpressions", true); |
| } |
| autofill::OptionToggle option_toggle = autofill::OptionToggle( |
| l10n_util::GetStringUTF16(IDS_PASSWORD_SAVING_STATUS_TOGGLE), enabled, |
| autofill::AccessoryAction::TOGGLE_SAVE_PASSWORDS); |
| data.set_option_toggle(option_toggle); |
| } |
| } |
| |
| GetManualFillingController()->RefreshSuggestions(std::move(data)); |
| } |
| |
| void PasswordAccessoryControllerImpl::OnGenerationRequested( |
| autofill::password_generation::PasswordGenerationType type) { |
| PasswordGenerationController* pwd_generation_controller = |
| PasswordGenerationController::GetIfExisting(web_contents_); |
| |
| DCHECK(pwd_generation_controller); |
| pwd_generation_controller->OnGenerationRequested(type); |
| } |
| |
| PasswordAccessoryControllerImpl::PasswordAccessoryControllerImpl( |
| content::WebContents* web_contents, |
| password_manager::CredentialCache* credential_cache, |
| base::WeakPtr<ManualFillingController> mf_controller, |
| password_manager::PasswordManagerClient* password_client) |
| : web_contents_(web_contents), |
| credential_cache_(credential_cache), |
| mf_controller_(std::move(mf_controller)), |
| password_client_(password_client) {} |
| |
| void PasswordAccessoryControllerImpl::ChangeCurrentOriginSavePasswordsStatus( |
| bool saving_enabled) { |
| const url::Origin origin = GetFocusedFrameOrigin(); |
| if (origin.opaque()) |
| return; |
| |
| const GURL origin_as_gurl = origin.GetURL(); |
| password_manager::PasswordStore::FormDigest form_digest( |
| autofill::PasswordForm::Scheme::kHtml, |
| password_manager::GetSignonRealm(origin_as_gurl), origin_as_gurl); |
| password_manager::PasswordStore* store = |
| password_client_->GetProfilePasswordStore(); |
| if (saving_enabled) { |
| store->Unblacklist(form_digest, base::NullCallback()); |
| } else { |
| autofill::PasswordForm form = |
| password_manager_util::MakeNormalizedBlacklistedForm( |
| std::move(form_digest)); |
| form.date_created = base::Time::Now(); |
| store->AddLogin(form); |
| } |
| password_client_->UpdateFormManagers(); |
| } |
| |
| bool PasswordAccessoryControllerImpl::AppearsInSuggestions( |
| const base::string16& suggestion, |
| bool is_password, |
| const url::Origin& origin) const { |
| if (origin.opaque()) |
| return false; // Don't proceed for invalid origins. |
| |
| const auto& credentials = |
| credential_cache_->GetCredentialStore(origin).GetCredentials(); |
| return std::any_of( |
| credentials.begin(), credentials.end(), [&](const auto& credential) { |
| return suggestion == |
| (is_password ? credential.password() : credential.username()); |
| }); |
| } |
| |
| base::WeakPtr<ManualFillingController> |
| PasswordAccessoryControllerImpl::GetManualFillingController() { |
| if (!mf_controller_) |
| mf_controller_ = ManualFillingController::GetOrCreate(web_contents_); |
| DCHECK(mf_controller_); |
| return mf_controller_; |
| } |
| |
| url::Origin PasswordAccessoryControllerImpl::GetFocusedFrameOrigin() const { |
| if (web_contents_->GetFocusedFrame() == nullptr) { |
| LOG(DFATAL) << "Tried to get retrieve origin without focused " |
| "frame."; |
| return url::Origin(); // Nonce! |
| } |
| return web_contents_->GetFocusedFrame()->GetLastCommittedOrigin(); |
| } |
| |
| WEB_CONTENTS_USER_DATA_KEY_IMPL(PasswordAccessoryControllerImpl) |