| // 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/password_accessory_controller.h" |
| |
| #include <vector> |
| |
| #include "base/callback.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/android/preferences/preferences_launcher.h" |
| #include "chrome/browser/favicon/favicon_service_factory.h" |
| #include "chrome/browser/password_manager/password_accessory_metrics_util.h" |
| #include "chrome/browser/password_manager/password_generation_dialog_view_interface.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/content/browser/content_autofill_driver.h" |
| #include "components/autofill/content/browser/content_autofill_driver_factory.h" |
| #include "components/autofill/core/common/password_form.h" |
| #include "components/favicon/core/favicon_service.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/password_manager_driver.h" |
| #include "components/password_manager/core/common/password_manager_features.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "content/public/browser/web_contents.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/ui_base_features.h" |
| |
| using autofill::PasswordForm; |
| using Item = PasswordAccessoryViewInterface::AccessoryItem; |
| |
| struct PasswordAccessoryController::GenerationElementData { |
| GenerationElementData(autofill::PasswordForm form, |
| autofill::FormSignature form_signature, |
| autofill::FieldSignature field_signature, |
| uint32_t max_password_length) |
| : form(std::move(form)), |
| form_signature(form_signature), |
| field_signature(field_signature), |
| max_password_length(max_password_length) {} |
| |
| // Form for which password generation is triggered. |
| autofill::PasswordForm form; |
| |
| // Signature of the form for which password generation is triggered. |
| autofill::FormSignature form_signature; |
| |
| // Signature of the field for which password generation is triggered. |
| autofill::FieldSignature field_signature; |
| |
| // Maximum length of the generated password. |
| uint32_t max_password_length; |
| }; |
| |
| struct PasswordAccessoryController::SuggestionElementData { |
| SuggestionElementData(base::string16 password, |
| base::string16 username, |
| Item::Type username_type) |
| : password(password), username(username), username_type(username_type) {} |
| |
| // Password string to be used for this credential. |
| base::string16 password; |
| |
| // Username string to be used for this credential. |
| base::string16 username; |
| |
| // Decides whether the username is interactive (i.e. empty ones are not). |
| Item::Type username_type; |
| }; |
| |
| struct PasswordAccessoryController::FaviconRequestData { |
| // List of requests waiting for favicons to be available. |
| std::vector<base::OnceCallback<void(const gfx::Image&)>> pending_requests; |
| |
| // Cached image for this origin. |IsEmpty()| unless a favicon was found. |
| gfx::Image cached_icon; |
| }; |
| |
| PasswordAccessoryController::PasswordAccessoryController( |
| content::WebContents* web_contents) |
| : web_contents_(web_contents), |
| view_(PasswordAccessoryViewInterface::Create(this)), |
| create_dialog_factory_( |
| base::BindRepeating(&PasswordGenerationDialogViewInterface::Create)), |
| favicon_service_(FaviconServiceFactory::GetForProfile( |
| Profile::FromBrowserContext(web_contents->GetBrowserContext()), |
| ServiceAccessType::EXPLICIT_ACCESS)), |
| weak_factory_(this) {} |
| |
| // Additional creation functions in unit tests only: |
| PasswordAccessoryController::PasswordAccessoryController( |
| content::WebContents* web_contents, |
| std::unique_ptr<PasswordAccessoryViewInterface> view, |
| CreateDialogFactory create_dialog_factory, |
| favicon::FaviconService* favicon_service) |
| : web_contents_(web_contents), |
| view_(std::move(view)), |
| create_dialog_factory_(create_dialog_factory), |
| favicon_service_(favicon_service), |
| weak_factory_(this) {} |
| |
| PasswordAccessoryController::~PasswordAccessoryController() = default; |
| |
| // static |
| bool PasswordAccessoryController::AllowedForWebContents( |
| content::WebContents* web_contents) { |
| DCHECK(web_contents) << "Need valid WebContents to attach controller to!"; |
| if (vr::VrTabHelper::IsInVr(web_contents)) { |
| return false; // TODO(crbug.com/865749): Reenable if works for VR keyboard. |
| } |
| // Either #passwords-keyboards-accessory or #experimental-ui must be enabled. |
| return base::FeatureList::IsEnabled( |
| password_manager::features::kPasswordsKeyboardAccessory) || |
| base::FeatureList::IsEnabled(features::kExperimentalUi); |
| } |
| |
| // static |
| void PasswordAccessoryController::CreateForWebContentsForTesting( |
| content::WebContents* web_contents, |
| std::unique_ptr<PasswordAccessoryViewInterface> view, |
| CreateDialogFactory create_dialog_factory, |
| favicon::FaviconService* favicon_service) { |
| DCHECK(web_contents) << "Need valid WebContents to attach controller to!"; |
| DCHECK(!FromWebContents(web_contents)) << "Controller already attached!"; |
| web_contents->SetUserData( |
| UserDataKey(), base::WrapUnique(new PasswordAccessoryController( |
| web_contents, std::move(view), create_dialog_factory, |
| favicon_service))); |
| } |
| |
| void PasswordAccessoryController::SavePasswordsForOrigin( |
| const std::map<base::string16, const PasswordForm*>& best_matches, |
| const url::Origin& origin) { |
| std::vector<SuggestionElementData>* suggestions = |
| &origin_suggestions_[origin]; |
| suggestions->clear(); |
| for (const auto& pair : best_matches) { |
| const PasswordForm* form = pair.second; |
| suggestions->emplace_back(form->password_value, GetDisplayUsername(*form), |
| form->username_value.empty() |
| ? Item::Type::NON_INTERACTIVE_SUGGESTION |
| : Item::Type::SUGGESTION); |
| } |
| } |
| |
| void PasswordAccessoryController::OnAutomaticGenerationStatusChanged( |
| bool available, |
| const base::Optional< |
| autofill::password_generation::PasswordGenerationUIData>& ui_data, |
| const base::WeakPtr<password_manager::PasswordManagerDriver>& driver) { |
| target_frame_driver_ = driver; |
| if (available) { |
| DCHECK(ui_data.has_value()); |
| generation_element_data_ = std::make_unique<GenerationElementData>( |
| ui_data.value().password_form, |
| autofill::CalculateFormSignature( |
| ui_data.value().password_form.form_data), |
| autofill::CalculateFieldSignatureByNameAndType( |
| ui_data.value().generation_element, "password"), |
| ui_data.value().max_length); |
| } else { |
| generation_element_data_.reset(); |
| } |
| DCHECK(view_); |
| view_->OnAutomaticGenerationStatusChanged(available); |
| } |
| |
| void PasswordAccessoryController::OnFilledIntoFocusedField( |
| autofill::FillingStatus status) { |
| if (status != autofill::FillingStatus::SUCCESS) |
| return; // TODO(crbug/853766): Record success rate. |
| view_->OpenKeyboard(); // Bring up the keyboard for the still focused field. |
| } |
| |
| void PasswordAccessoryController::RefreshSuggestionsForField( |
| const url::Origin& origin, |
| bool is_fillable, |
| bool is_password_field) { |
| if (is_fillable) { |
| current_origin_ = origin; |
| view_->OnItemsAvailable(CreateViewItems(origin, origin_suggestions_[origin], |
| is_password_field)); |
| view_->OpenKeyboard(); // Should happen automatically. |
| } else { |
| // For unfillable fields, reset the origin and send the empty state message. |
| current_origin_ = url::Origin(); |
| view_->OnItemsAvailable(CreateViewItems( |
| origin, std::vector<SuggestionElementData>(), is_password_field)); |
| view_->CloseAccessorySheet(); |
| } |
| } |
| |
| void PasswordAccessoryController::DidNavigateMainFrame() { |
| if (current_origin_.IsSameOriginWith( |
| web_contents_->GetMainFrame()->GetLastCommittedOrigin())) |
| return; // Clean requests only if the navigation was across origins. |
| favicon_tracker_.TryCancelAll(); // If there is a request pending, cancel it. |
| current_origin_ = url::Origin(); |
| icons_request_data_.clear(); |
| origin_suggestions_.clear(); |
| } |
| |
| void PasswordAccessoryController::GetFavicon( |
| base::OnceCallback<void(const gfx::Image&)> icon_callback) { |
| url::Origin origin = current_origin_; // Copy origin in case it changes. |
| // Check whether this request can be immediately answered with a cached icon. |
| // It is empty if there wasn't at least one request that found an icon yet. |
| FaviconRequestData* icon_request = &icons_request_data_[origin]; |
| if (!icon_request->cached_icon.IsEmpty()) { |
| std::move(icon_callback).Run(icon_request->cached_icon); |
| return; |
| } |
| if (!favicon_service_) { // This might happen in tests. |
| std::move(icon_callback).Run(gfx::Image()); |
| return; |
| } |
| |
| // The cache is empty. Queue the callback. |
| icon_request->pending_requests.emplace_back(std::move(icon_callback)); |
| if (icon_request->pending_requests.size() > 1) |
| return; // The favicon for this origin was already requested. |
| |
| favicon_service_->GetFaviconImageForPageURL( |
| origin.GetURL(), |
| base::BindRepeating( // FaviconService doesn't support BindOnce yet. |
| &PasswordAccessoryController::OnImageFetched, |
| weak_factory_.GetWeakPtr(), origin), |
| &favicon_tracker_); |
| } |
| |
| void PasswordAccessoryController::OnFillingTriggered( |
| bool is_password, |
| const base::string16& textToFill) { |
| password_manager::ContentPasswordManagerDriverFactory* factory = |
| password_manager::ContentPasswordManagerDriverFactory::FromWebContents( |
| web_contents_); |
| DCHECK(factory); |
| // TODO(fhorschig): Consider allowing filling on non-main frames. |
| password_manager::ContentPasswordManagerDriver* driver = |
| factory->GetDriverForFrame(web_contents_->GetMainFrame()); |
| if (!driver) { |
| return; |
| } // |driver| can be NULL if the tab is being closed. |
| driver->FillIntoFocusedField( |
| is_password, textToFill, |
| base::BindOnce(&PasswordAccessoryController::OnFilledIntoFocusedField, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void PasswordAccessoryController::OnOptionSelected( |
| const base::string16& selectedOption) const { |
| if (selectedOption == |
| l10n_util::GetStringUTF16( |
| IDS_PASSWORD_MANAGER_ACCESSORY_ALL_PASSWORDS_LINK)) { |
| UMA_HISTOGRAM_ENUMERATION("KeyboardAccessory.AccessoryActionSelected", |
| metrics::AccessoryAction::MANAGE_PASSWORDS, |
| metrics::AccessoryAction::COUNT); |
| chrome::android::PreferencesLauncher::ShowPasswordSettings(); |
| } |
| } |
| |
| void PasswordAccessoryController::OnGenerationRequested() { |
| if (!target_frame_driver_) |
| return; |
| // TODO(crbug.com/835234): Take the modal dialog logic out of the accessory |
| // controller. |
| dialog_view_ = create_dialog_factory_.Run(this); |
| uint32_t spec_priority = 0; |
| base::string16 password = |
| target_frame_driver_->GetPasswordGenerationManager()->GeneratePassword( |
| web_contents_->GetLastCommittedURL().GetOrigin(), |
| generation_element_data_->form_signature, |
| generation_element_data_->field_signature, |
| generation_element_data_->max_password_length, &spec_priority); |
| if (target_frame_driver_ && target_frame_driver_->GetPasswordManager()) { |
| target_frame_driver_->GetPasswordManager() |
| ->ReportSpecPriorityForGeneratedPassword(generation_element_data_->form, |
| spec_priority); |
| } |
| dialog_view_->Show(password); |
| } |
| |
| void PasswordAccessoryController::GeneratedPasswordAccepted( |
| const base::string16& password) { |
| if (!target_frame_driver_) |
| return; |
| target_frame_driver_->GeneratedPasswordAccepted(password); |
| dialog_view_.reset(); |
| } |
| |
| void PasswordAccessoryController::GeneratedPasswordRejected() { |
| dialog_view_.reset(); |
| } |
| |
| gfx::NativeView PasswordAccessoryController::container_view() const { |
| return web_contents_->GetNativeView(); |
| } |
| |
| gfx::NativeWindow PasswordAccessoryController::native_window() const { |
| return web_contents_->GetTopLevelNativeWindow(); |
| } |
| |
| // static |
| std::vector<Item> PasswordAccessoryController::CreateViewItems( |
| const url::Origin& origin, |
| const std::vector<SuggestionElementData>& suggestions, |
| bool is_password_field) { |
| std::vector<Item> items; |
| base::string16 passwords_title_str; |
| |
| // Create the title element |
| passwords_title_str = l10n_util::GetStringFUTF16( |
| suggestions.empty() |
| ? IDS_PASSWORD_MANAGER_ACCESSORY_PASSWORD_LIST_EMPTY_MESSAGE |
| : IDS_PASSWORD_MANAGER_ACCESSORY_PASSWORD_LIST_TITLE, |
| base::ASCIIToUTF16(origin.host())); |
| items.emplace_back(passwords_title_str, passwords_title_str, |
| /*is_password=*/false, Item::Type::LABEL); |
| |
| // Create a username and a password element for every suggestions |
| for (const SuggestionElementData& suggestion : suggestions) { |
| items.emplace_back(suggestion.username, suggestion.username, |
| /*is_password=*/false, suggestion.username_type); |
| items.emplace_back(suggestion.password, |
| l10n_util::GetStringFUTF16( |
| IDS_PASSWORD_MANAGER_ACCESSORY_PASSWORD_DESCRIPTION, |
| suggestion.username), |
| /*is_password=*/true, |
| is_password_field |
| ? Item::Type::SUGGESTION |
| : Item::Type::NON_INTERACTIVE_SUGGESTION); |
| } |
| |
| // Create a horizontal divider line before the options. |
| items.emplace_back(base::string16(), base::string16(), false, |
| Item::Type::DIVIDER); |
| |
| // Create the link to all passwords. |
| base::string16 manage_passwords_title = l10n_util::GetStringUTF16( |
| IDS_PASSWORD_MANAGER_ACCESSORY_ALL_PASSWORDS_LINK); |
| items.emplace_back(manage_passwords_title, manage_passwords_title, false, |
| Item::Type::OPTION); |
| return items; |
| } |
| |
| void PasswordAccessoryController::OnImageFetched( |
| url::Origin origin, |
| const favicon_base::FaviconImageResult& image_result) { |
| FaviconRequestData* icon_request = &icons_request_data_[origin]; |
| icon_request->cached_icon = image_result.image; |
| // Only trigger all the callbacks if they still affect a displayed origin. |
| if (origin == current_origin_) { |
| for (auto& callback : icon_request->pending_requests) { |
| std::move(callback).Run(icon_request->cached_icon); |
| } |
| } |
| icon_request->pending_requests.clear(); |
| } |