| // Copyright 2013 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 "components/password_manager/core/browser/password_autofill_manager.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/command_line.h" |
| #include "base/i18n/case_conversion.h" |
| #include "base/logging.h" |
| #include "base/metrics/field_trial.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/string16.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "components/autofill/core/browser/autofill_client.h" |
| #include "components/autofill/core/browser/autofill_experiments.h" |
| #include "components/autofill/core/browser/popup_item_ids.h" |
| #include "components/autofill/core/browser/suggestion.h" |
| #include "components/autofill/core/common/autofill_constants.h" |
| #include "components/autofill/core/common/autofill_data_validation.h" |
| #include "components/autofill/core/common/autofill_util.h" |
| #include "components/favicon/core/favicon_util.h" |
| #include "components/password_manager/core/browser/android_affiliation/affiliation_utils.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_metrics_recorder.h" |
| #include "components/password_manager/core/browser/password_manager_metrics_util.h" |
| #include "components/password_manager/core/browser/password_manager_util.h" |
| #include "components/password_manager/core/common/password_manager_features.h" |
| #include "components/security_state/core/security_state.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "url/gurl.h" |
| |
| #if defined(OS_ANDROID) |
| #include "base/android/build_info.h" |
| #endif |
| |
| namespace password_manager { |
| |
| namespace { |
| |
| constexpr base::char16 kPasswordReplacementChar = 0x2022; |
| |
| // Returns |username| unless it is empty. For an empty |username| returns a |
| // localised string saying this username is empty. Use this for displaying the |
| // usernames to the user. |replaced| is set to true iff |username| is empty. |
| base::string16 ReplaceEmptyUsername(const base::string16& username, |
| bool* replaced) { |
| *replaced = username.empty(); |
| if (username.empty()) |
| return l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_EMPTY_LOGIN); |
| return username; |
| } |
| |
| // Returns the prettified version of |signon_realm| to be displayed on the UI. |
| base::string16 GetHumanReadableRealm(const std::string& signon_realm) { |
| // For Android application realms, remove the hash component. Otherwise, make |
| // no changes. |
| FacetURI maybe_facet_uri(FacetURI::FromPotentiallyInvalidSpec(signon_realm)); |
| if (maybe_facet_uri.IsValidAndroidFacetURI()) |
| return base::UTF8ToUTF16("android://" + |
| maybe_facet_uri.android_package_name() + "/"); |
| GURL realm(signon_realm); |
| if (realm.is_valid()) |
| return base::UTF8ToUTF16(realm.host()); |
| return base::UTF8ToUTF16(signon_realm); |
| } |
| |
| // If |suggestion| was made for an empty username, then return the empty |
| // string, otherwise return |suggestion|. |
| base::string16 GetUsernameFromSuggestion(const base::string16& suggestion) { |
| return suggestion == |
| l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_EMPTY_LOGIN) |
| ? base::string16() |
| : suggestion; |
| } |
| |
| // If |field_suggestion| matches |field_content|, creates a Suggestion out of it |
| // and appends to |suggestions|. |
| void AppendSuggestionIfMatching( |
| const base::string16& field_suggestion, |
| const base::string16& field_contents, |
| const gfx::Image& custom_icon, |
| const std::string& signon_realm, |
| bool show_all, |
| bool is_password_field, |
| size_t password_length, |
| std::vector<autofill::Suggestion>* suggestions) { |
| base::string16 lower_suggestion = base::i18n::ToLower(field_suggestion); |
| base::string16 lower_contents = base::i18n::ToLower(field_contents); |
| if (show_all || autofill::FieldIsSuggestionSubstringStartingOnTokenBoundary( |
| lower_suggestion, lower_contents, true)) { |
| bool replaced_username; |
| autofill::Suggestion suggestion( |
| ReplaceEmptyUsername(field_suggestion, &replaced_username)); |
| suggestion.is_value_secondary = replaced_username; |
| suggestion.label = GetHumanReadableRealm(signon_realm); |
| suggestion.additional_label = |
| base::string16(password_length, kPasswordReplacementChar); |
| suggestion.frontend_id = is_password_field |
| ? autofill::POPUP_ITEM_ID_PASSWORD_ENTRY |
| : autofill::POPUP_ITEM_ID_USERNAME_ENTRY; |
| suggestion.match = |
| show_all || base::StartsWith(lower_suggestion, lower_contents, |
| base::CompareCase::SENSITIVE) |
| ? autofill::Suggestion::PREFIX_MATCH |
| : autofill::Suggestion::SUBSTRING_MATCH; |
| suggestion.custom_icon = custom_icon; |
| // The UI code will pick up an icon from the resources based on the string. |
| suggestion.icon = base::ASCIIToUTF16("globeIcon"); |
| suggestions->push_back(suggestion); |
| } |
| } |
| |
| // This function attempts to fill |suggestions| from |fill_data| based on |
| // |current_username| that is the current value of the field. Unless |show_all| |
| // is true, it only picks suggestions allowed by |
| // FieldIsSuggestionSubstringStartingOnTokenBoundary. It can pick either a |
| // substring or a prefix based on the flag. |
| void GetSuggestions(const autofill::PasswordFormFillData& fill_data, |
| const base::string16& current_username, |
| const gfx::Image& custom_icon, |
| bool show_all, |
| bool is_password_field, |
| std::vector<autofill::Suggestion>* suggestions) { |
| AppendSuggestionIfMatching( |
| fill_data.username_field.value, current_username, custom_icon, |
| fill_data.preferred_realm, show_all, is_password_field, |
| fill_data.password_field.value.size(), suggestions); |
| |
| for (const auto& login : fill_data.additional_logins) { |
| AppendSuggestionIfMatching(login.first, current_username, custom_icon, |
| login.second.realm, show_all, is_password_field, |
| login.second.password.size(), suggestions); |
| } |
| |
| // Prefix matches should precede other token matches. |
| if (!show_all && autofill::IsFeatureSubstringMatchEnabled()) { |
| std::sort(suggestions->begin(), suggestions->end(), |
| [](const autofill::Suggestion& a, const autofill::Suggestion& b) { |
| return a.match < b.match; |
| }); |
| } |
| } |
| |
| bool ShouldShowManualFallbackForPreLollipop(syncer::SyncService* sync_service) { |
| #if defined(OS_ANDROID) |
| return base::android::BuildInfo::GetInstance()->sdk_int() >= |
| base::android::SDK_VERSION_LOLLIPOP || |
| password_manager_util::IsSyncingWithNormalEncryption(sync_service); |
| #else |
| return true; |
| #endif |
| } |
| |
| } // namespace |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // PasswordAutofillManager, public: |
| |
| PasswordAutofillManager::PasswordAutofillManager( |
| PasswordManagerDriver* password_manager_driver, |
| autofill::AutofillClient* autofill_client, |
| PasswordManagerClient* password_client) |
| : password_manager_driver_(password_manager_driver), |
| autofill_client_(autofill_client), |
| password_client_(password_client), |
| weak_ptr_factory_(this) {} |
| |
| PasswordAutofillManager::~PasswordAutofillManager() { |
| if (deletion_callback_) |
| std::move(deletion_callback_).Run(); |
| } |
| |
| void PasswordAutofillManager::OnPopupShown() {} |
| |
| void PasswordAutofillManager::OnPopupHidden() {} |
| |
| void PasswordAutofillManager::OnPopupSuppressed() {} |
| |
| void PasswordAutofillManager::DidSelectSuggestion(const base::string16& value, |
| int identifier) { |
| ClearPreviewedForm(); |
| if (identifier == autofill::POPUP_ITEM_ID_ALL_SAVED_PASSWORDS_ENTRY || |
| identifier == autofill::POPUP_ITEM_ID_GENERATE_PASSWORD_ENTRY) |
| return; |
| bool success = PreviewSuggestion(GetUsernameFromSuggestion(value)); |
| DCHECK(success); |
| } |
| |
| void PasswordAutofillManager::DidAcceptSuggestion(const base::string16& value, |
| int identifier, |
| int position) { |
| using metrics_util::PasswordDropdownSelectedOption; |
| |
| if (identifier == autofill::POPUP_ITEM_ID_GENERATE_PASSWORD_ENTRY) { |
| password_client_->GeneratePassword(); |
| metrics_util::LogPasswordDropdownItemSelected( |
| PasswordDropdownSelectedOption::kGenerate); |
| } else if (identifier == autofill::POPUP_ITEM_ID_ALL_SAVED_PASSWORDS_ENTRY) { |
| password_client_->NavigateToManagePasswordsPage( |
| ManagePasswordsReferrer::kPasswordDropdown); |
| metrics_util::LogContextOfShowAllSavedPasswordsAccepted( |
| metrics_util::SHOW_ALL_SAVED_PASSWORDS_CONTEXT_PASSWORD); |
| metrics_util::LogPasswordDropdownItemSelected( |
| PasswordDropdownSelectedOption::kShowAll); |
| |
| if (password_client_ && password_client_->GetMetricsRecorder()) { |
| using UserAction = |
| password_manager::PasswordManagerMetricsRecorder::PageLevelUserAction; |
| password_client_->GetMetricsRecorder()->RecordPageLevelUserAction( |
| UserAction::kShowAllPasswordsWhileSomeAreSuggested); |
| } |
| } else { |
| metrics_util::LogPasswordDropdownItemSelected( |
| PasswordDropdownSelectedOption::kPassword); |
| bool success = FillSuggestion(GetUsernameFromSuggestion(value)); |
| DCHECK(success); |
| } |
| |
| autofill_client_->HideAutofillPopup(); |
| } |
| |
| bool PasswordAutofillManager::GetDeletionConfirmationText( |
| const base::string16& value, |
| int identifier, |
| base::string16* title, |
| base::string16* body) { |
| return false; |
| } |
| |
| bool PasswordAutofillManager::RemoveSuggestion(const base::string16& value, |
| int identifier) { |
| // Password suggestions cannot be deleted this way. |
| // See http://crbug.com/329038#c15 |
| return false; |
| } |
| |
| void PasswordAutofillManager::ClearPreviewedForm() { |
| password_manager_driver_->ClearPreviewedForm(); |
| } |
| |
| autofill::PopupType PasswordAutofillManager::GetPopupType() const { |
| return autofill::PopupType::kPasswords; |
| } |
| |
| autofill::AutofillDriver* PasswordAutofillManager::GetAutofillDriver() { |
| return password_manager_driver_->GetAutofillDriver(); |
| } |
| |
| void PasswordAutofillManager::RegisterDeletionCallback( |
| base::OnceClosure deletion_callback) { |
| deletion_callback_ = std::move(deletion_callback); |
| } |
| |
| void PasswordAutofillManager::OnAddPasswordFillData( |
| const autofill::PasswordFormFillData& fill_data) { |
| if (!autofill::IsValidPasswordFormFillData(fill_data)) |
| return; |
| |
| fill_data_ = std::make_unique<autofill::PasswordFormFillData>(fill_data); |
| RequestFavicon(fill_data.origin); |
| } |
| |
| void PasswordAutofillManager::DeleteFillData() { |
| fill_data_.reset(); |
| if (autofill_client_) |
| autofill_client_->HideAutofillPopup(); |
| } |
| |
| void PasswordAutofillManager::OnShowPasswordSuggestions( |
| base::i18n::TextDirection text_direction, |
| const base::string16& typed_username, |
| int options, |
| const gfx::RectF& bounds) { |
| std::vector<autofill::Suggestion> suggestions; |
| if (!fill_data_) { |
| // Probably the credential was deleted in the mean time. |
| return; |
| } |
| GetSuggestions(*fill_data_, typed_username, page_favicon_, |
| (options & autofill::SHOW_ALL) != 0, |
| (options & autofill::IS_PASSWORD_FIELD) != 0, &suggestions); |
| |
| if (suggestions.empty()) { |
| autofill_client_->HideAutofillPopup(); |
| return; |
| } |
| |
| if (ShouldShowManualFallbackForPreLollipop( |
| autofill_client_->GetSyncService())) { |
| autofill::Suggestion suggestion( |
| l10n_util::GetStringUTF8(IDS_PASSWORD_MANAGER_MANAGE_PASSWORDS), |
| std::string(), std::string(), |
| autofill::POPUP_ITEM_ID_ALL_SAVED_PASSWORDS_ENTRY); |
| suggestions.push_back(suggestion); |
| |
| metrics_util::LogContextOfShowAllSavedPasswordsShown( |
| metrics_util::SHOW_ALL_SAVED_PASSWORDS_CONTEXT_PASSWORD); |
| } |
| |
| metrics_util::LogPasswordDropdownShown( |
| metrics_util::PasswordDropdownState::kStandard); |
| autofill_client_->ShowAutofillPopup(bounds, text_direction, suggestions, |
| false, weak_ptr_factory_.GetWeakPtr()); |
| } |
| |
| bool PasswordAutofillManager::MaybeShowPasswordSuggestions( |
| const gfx::RectF& bounds, |
| base::i18n::TextDirection text_direction) { |
| if (!fill_data_) |
| return false; |
| OnShowPasswordSuggestions(text_direction, base::string16(), |
| autofill::SHOW_ALL | autofill::IS_PASSWORD_FIELD, |
| bounds); |
| return true; |
| } |
| |
| bool PasswordAutofillManager::MaybeShowPasswordSuggestionsWithGeneration( |
| const gfx::RectF& bounds, |
| base::i18n::TextDirection text_direction) { |
| if (!fill_data_) |
| return false; |
| std::vector<autofill::Suggestion> suggestions; |
| GetSuggestions(*fill_data_, base::string16(), page_favicon_, |
| true /* show_all */, true /* is_password_field */, |
| &suggestions); |
| |
| // Add 'Generation' option. |
| // The UI code will pick up an icon from the resources based on the string. |
| autofill::Suggestion suggestion( |
| l10n_util::GetStringUTF8(IDS_PASSWORD_MANAGER_GENERATE_PASSWORD), |
| std::string(), std::string("keyIcon"), |
| autofill::POPUP_ITEM_ID_GENERATE_PASSWORD_ENTRY); |
| suggestions.push_back(suggestion); |
| |
| // Add "Manage passwords". |
| if (ShouldShowManualFallbackForPreLollipop( |
| autofill_client_->GetSyncService())) { |
| autofill::Suggestion suggestion( |
| l10n_util::GetStringUTF8(IDS_PASSWORD_MANAGER_MANAGE_PASSWORDS), |
| std::string(), std::string(), |
| autofill::POPUP_ITEM_ID_ALL_SAVED_PASSWORDS_ENTRY); |
| suggestions.push_back(suggestion); |
| |
| metrics_util::LogContextOfShowAllSavedPasswordsShown( |
| metrics_util::SHOW_ALL_SAVED_PASSWORDS_CONTEXT_PASSWORD); |
| } |
| |
| metrics_util::LogPasswordDropdownShown( |
| metrics_util::PasswordDropdownState::kStandardGenerate); |
| autofill_client_->ShowAutofillPopup(bounds, text_direction, suggestions, |
| false, weak_ptr_factory_.GetWeakPtr()); |
| return true; |
| } |
| |
| void PasswordAutofillManager::DidNavigateMainFrame() { |
| fill_data_.reset(); |
| favicon_tracker_.TryCancelAll(); |
| page_favicon_ = gfx::Image(); |
| } |
| |
| bool PasswordAutofillManager::FillSuggestionForTest( |
| const base::string16& username) { |
| return FillSuggestion(username); |
| } |
| |
| bool PasswordAutofillManager::PreviewSuggestionForTest( |
| const base::string16& username) { |
| return PreviewSuggestion(username); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // PasswordAutofillManager, private: |
| |
| bool PasswordAutofillManager::FillSuggestion(const base::string16& username) { |
| autofill::PasswordAndRealm password_and_realm; |
| if (fill_data_ && GetPasswordAndRealmForUsername(username, *fill_data_, |
| &password_and_realm)) { |
| bool is_android_credential = |
| FacetURI::FromPotentiallyInvalidSpec(password_and_realm.realm) |
| .IsValidAndroidFacetURI(); |
| metrics_util::LogFilledCredentialIsFromAndroidApp(is_android_credential); |
| password_manager_driver_->FillSuggestion(username, |
| password_and_realm.password); |
| return true; |
| } |
| return false; |
| } |
| |
| bool PasswordAutofillManager::PreviewSuggestion( |
| const base::string16& username) { |
| autofill::PasswordAndRealm password_and_realm; |
| if (fill_data_ && GetPasswordAndRealmForUsername(username, *fill_data_, |
| &password_and_realm)) { |
| password_manager_driver_->PreviewSuggestion(username, |
| password_and_realm.password); |
| return true; |
| } |
| return false; |
| } |
| |
| bool PasswordAutofillManager::GetPasswordAndRealmForUsername( |
| const base::string16& current_username, |
| const autofill::PasswordFormFillData& fill_data, |
| autofill::PasswordAndRealm* password_and_realm) { |
| // TODO(dubroy): When password access requires some kind of authentication |
| // (e.g. Keychain access on Mac OS), use |password_manager_client_| here to |
| // fetch the actual password. See crbug.com/178358 for more context. |
| |
| // Look for any suitable matches to current field text. |
| if (fill_data.username_field.value == current_username) { |
| password_and_realm->password = fill_data.password_field.value; |
| password_and_realm->realm = fill_data.preferred_realm; |
| return true; |
| } |
| |
| // Scan additional logins for a match. |
| auto iter = fill_data.additional_logins.find(current_username); |
| if (iter != fill_data.additional_logins.end()) { |
| *password_and_realm = iter->second; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void PasswordAutofillManager::RequestFavicon(const GURL& url) { |
| if (!password_client_) |
| return; |
| favicon::GetFaviconImageForPageURL( |
| password_client_->GetFaviconService(), url, |
| favicon_base::IconType::kFavicon, |
| base::BindRepeating(&PasswordAutofillManager::OnFaviconReady, |
| weak_ptr_factory_.GetWeakPtr()), |
| &favicon_tracker_); |
| } |
| |
| void PasswordAutofillManager::OnFaviconReady( |
| const favicon_base::FaviconImageResult& result) { |
| if (!result.image.IsEmpty()) |
| page_favicon_ = result.image; |
| } |
| |
| } // namespace password_manager |