chromium/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate.cc

// 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/touch_to_fill/password_manager/touch_to_fill_controller_autofill_delegate.h"

#include "base/base64.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "base/types/pass_key.h"
#include "chrome/browser/password_manager/android/access_loss/password_access_loss_warning_bridge_impl.h"
#include "chrome/browser/password_manager/android/local_passwords_migration_warning_util.h"
#include "chrome/browser/password_manager/chrome_password_manager_client.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.h"
#include "components/autofill/core/common/unique_ids.h"
#include "components/device_reauth/device_authenticator.h"
#include "components/password_manager/core/browser/origin_credential_store.h"
#include "components/password_manager/core/browser/passkey_credential.h"
#include "components/password_manager/core/browser/password_credential_filler.h"
#include "components/password_manager/core/browser/password_form.h"
#include "components/password_manager/core/browser/password_manager_metrics_util.h"
#include "content/public/browser/web_contents.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "ui/gfx/native_widget_types.h"
#include "url/gurl.h"

namespace {

using ToShowVirtualKeyboard =
    password_manager::PasswordManagerDriver::ToShowVirtualKeyboard;
using password_manager::UiCredential;

// Returns whether there is at least one credential with a non-empty username.
bool ContainsNonEmptyUsername(
    const base::span<const UiCredential>& credentials) {
  return base::ranges::any_of(credentials, [](const UiCredential& credential) {
    return !credential.username().empty();
  });
}

}  // namespace

// No-op constructor for tests.
TouchToFillControllerAutofillDelegate::TouchToFillControllerAutofillDelegate(
    base::PassKey<class TouchToFillControllerAutofillTest>,
    password_manager::PasswordManagerClient* password_client,
    content::WebContents* web_contents,
    std::unique_ptr<device_reauth::DeviceAuthenticator> authenticator,
    base::WeakPtr<password_manager::WebAuthnCredentialsDelegate>
        webauthn_delegate,
    std::unique_ptr<password_manager::PasswordCredentialFiller> filler,
    const password_manager::PasswordForm* form_to_fill,
    autofill::FieldRendererId focused_field_renderer_id,
    ShowHybridOption should_show_hybrid_option,
    ShowPasswordMigrationWarningCallback show_password_migration_warning,
    std::unique_ptr<PasswordAccessLossWarningBridge> data_loss_warning_bridge)
    : password_client_(password_client),
      web_contents_(web_contents),
      authenticator_(std::move(authenticator)),
      webauthn_delegate_(webauthn_delegate),
      filler_(std::move(filler)),
      form_to_fill_(form_to_fill),
      focused_field_renderer_id_(focused_field_renderer_id),
      should_show_hybrid_option_(should_show_hybrid_option),
      show_password_migration_warning_(
          std::move(show_password_migration_warning)),
      access_loss_warning_bridge_(std::move(data_loss_warning_bridge)) {}

TouchToFillControllerAutofillDelegate::TouchToFillControllerAutofillDelegate(
    ChromePasswordManagerClient* password_client,
    std::unique_ptr<device_reauth::DeviceAuthenticator> authenticator,
    base::WeakPtr<password_manager::WebAuthnCredentialsDelegate>
        webauthn_delegate,
    std::unique_ptr<password_manager::PasswordCredentialFiller> filler,
    const password_manager::PasswordForm* form_to_fill,
    autofill::FieldRendererId focused_field_renderer_id,
    ShowHybridOption should_show_hybrid_option)
    : password_client_(password_client),
      // |TouchToFillControllerTest| doesn't provide an instance of
      // |ChromePasswordManagerClient|, so the test-only constructor should
      // be used there.
      web_contents_(static_cast<ChromePasswordManagerClient*>(password_client_)
                        ->web_contents()),
      authenticator_(std::move(authenticator)),
      webauthn_delegate_(webauthn_delegate),
      filler_(std::move(filler)),
      form_to_fill_(form_to_fill),
      focused_field_renderer_id_(focused_field_renderer_id),
      should_show_hybrid_option_(should_show_hybrid_option),
      show_password_migration_warning_(
          base::BindRepeating(&local_password_migration::ShowWarning)),
      access_loss_warning_bridge_(
          std::make_unique<PasswordAccessLossWarningBridgeImpl>()),
      source_id_(password_client->web_contents()
                     ->GetPrimaryMainFrame()
                     ->GetPageUkmSourceId()) {}

TouchToFillControllerAutofillDelegate::
    ~TouchToFillControllerAutofillDelegate() {
  if (authenticator_) {
    // This is a noop if no auth triggered by Touch To Fill is in progress.
    authenticator_->Cancel();
  }
}

void TouchToFillControllerAutofillDelegate::OnShow(
    base::span<const password_manager::UiCredential> credentials,
    base::span<password_manager::PasskeyCredential> passkey_credentials) {
  CHECK(filler_);

  filler_->UpdateTriggerSubmission(ShouldTriggerSubmission() &&
                                   ContainsNonEmptyUsername(credentials));

  base::UmaHistogramEnumeration(
      "PasswordManager.TouchToFill.SubmissionReadiness",
      filler_->GetSubmissionReadinessState());
  ukm::builders::TouchToFill_SubmissionReadiness(source_id_)
      .SetSubmissionReadiness(
          static_cast<int64_t>(filler_->GetSubmissionReadinessState()))
      .Record(ukm::UkmRecorder::Get());
}

void TouchToFillControllerAutofillDelegate::OnCredentialSelected(
    const UiCredential& credential,
    base::OnceClosure action_complete) {
  if (!filler_) {
    return;
  }

  action_complete_ = std::move(action_complete);
  ukm::builders::TouchToFill_Shown(source_id_)
      .SetUserAction(static_cast<int64_t>(UserAction::kSelectedCredential))
      .Record(ukm::UkmRecorder::Get());
  if (!password_client_->IsReauthBeforeFillingRequired(authenticator_.get())) {
    FillCredential(credential);
    return;
  }
  // `this` notifies the authenticator when it is destructed, resulting in
  // the callback being reset by the authenticator. Therefore, it is safe
  // to use base::Unretained.
  authenticator_->AuthenticateWithMessage(
      u"",
      base::BindOnce(&TouchToFillControllerAutofillDelegate::OnReauthCompleted,
                     base::Unretained(this), credential));
}

void TouchToFillControllerAutofillDelegate::OnPasskeyCredentialSelected(
    const password_manager::PasskeyCredential& credential,
    base::OnceClosure action_complete) {
  if (!webauthn_delegate_) {
    return;
  }

  webauthn_delegate_->SelectPasskey(
      base::Base64Encode(credential.credential_id()), base::DoNothing());

  CleanUpFillerAndReportOutcome(TouchToFillOutcome::kPasskeyCredentialSelected,
                                /*show_virtual_keyboard=*/false);
  std::move(action_complete).Run();
}

void TouchToFillControllerAutofillDelegate::OnManagePasswordsSelected(
    bool passkeys_shown,
    base::OnceClosure action_complete) {
  if (!filler_) {
    return;
  }

  CleanUpFillerAndReportOutcome(TouchToFillOutcome::kManagePasswordsSelected,
                                /*show_virtual_keyboard=*/false);

  if (passkeys_shown) {
    // On Android there is no passkey management available in Chrome settings
    // password management. This will attempt to launch the GMS password
    // manager where passkeys can be seen.
    password_client_->NavigateToManagePasskeysPage(
        password_manager::ManagePasswordsReferrer::kTouchToFill);
  } else {
    password_client_->NavigateToManagePasswordsPage(
        password_manager::ManagePasswordsReferrer::kTouchToFill);
  }

  ukm::builders::TouchToFill_Shown(source_id_)
      .SetUserAction(static_cast<int64_t>(UserAction::kSelectedManagePasswords))
      .Record(ukm::UkmRecorder::Get());
  std::move(action_complete).Run();
}

void TouchToFillControllerAutofillDelegate::OnHybridSignInSelected(
    base::OnceClosure action_complete) {
  if (!webauthn_delegate_) {
    return;
  }

  webauthn_delegate_->ShowAndroidHybridSignIn();

  CleanUpFillerAndReportOutcome(TouchToFillOutcome::kHybridSignInSelected,
                                /*show_virtual_keyboard=*/false);

  std::move(action_complete).Run();
}

void TouchToFillControllerAutofillDelegate::OnDismiss(
    base::OnceClosure action_complete) {
  if (!filler_) {
    return;
  }

  CleanUpFillerAndReportOutcome(TouchToFillOutcome::kSheetDismissed,
                                /*show_virtual_keyboard=*/true);
  ukm::builders::TouchToFill_Shown(source_id_)
      .SetUserAction(static_cast<int64_t>(UserAction::kDismissed))
      .Record(ukm::UkmRecorder::Get());
  std::move(action_complete).Run();
}

void TouchToFillControllerAutofillDelegate::OnCredManDismissed(
    base::OnceClosure action_completed) {
  if (!filler_) {
    return;
  }
  filler_->Dismiss(ToShowVirtualKeyboard(false));
  std::move(action_completed).Run();
}

GURL TouchToFillControllerAutofillDelegate::GetFrameUrl() {
  CHECK(filler_);
  return filler_->GetFrameUrl();
}

bool TouchToFillControllerAutofillDelegate::ShouldShowTouchToFill() {
  if (!base::FeatureList::IsEnabled(
          password_manager::features::kPasswordSuggestionBottomSheetV2)) {
    // For password suggesion bottom sheet version 1 all the conditions for
    // showing TTF are checked in the renderer (see
    // `PasswordAutofillAgent::TryToShowKeyboardReplacingSurface`). That's why
    // no additional checks are needed here.
    return true;
  }

  if (!form_to_fill_) {
    return false;
  }

  // Always show TTF for a current password field.
  if (focused_field_renderer_id_ ==
      form_to_fill_->password_element_renderer_id) {
    return true;
  }

  // Do not show TTF if it's not a current password and not a username field.
  if (focused_field_renderer_id_ !=
      form_to_fill_->username_element_renderer_id) {
    return false;
  }

  // Show TTF if the form has a current password field or if it's a single
  // username form.
  if (form_to_fill_->HasPasswordElement() ||
      form_to_fill_->IsSingleUsername()) {
    return true;
  }

  return false;
}

bool TouchToFillControllerAutofillDelegate::ShouldTriggerSubmission() {
  return filler_->ShouldTriggerSubmission();
}

bool TouchToFillControllerAutofillDelegate::ShouldShowHybridOption() {
  return should_show_hybrid_option_.value();
}

bool TouchToFillControllerAutofillDelegate::
    ShouldShowNoPasskeysSheetIfRequired() {
  return false;
}

gfx::NativeView TouchToFillControllerAutofillDelegate::GetNativeView() {
  return web_contents_->GetNativeView();
}

void TouchToFillControllerAutofillDelegate::OnReauthCompleted(
    UiCredential credential,
    bool auth_successful) {
  CHECK(action_complete_);
  if (!filler_) {
    return;
  }

  if (!auth_successful) {
    CleanUpFillerAndReportOutcome(TouchToFillOutcome::kReauthenticationFailed,
                                  /*show_virtual_keyboard=*/true);
    std::move(action_complete_).Run();
    return;
  }

  FillCredential(credential);
}

void TouchToFillControllerAutofillDelegate::FillCredential(
    const UiCredential& credential) {
  CHECK(action_complete_);
  CHECK(filler_);

  // Do not trigger autosubmission if the password migration warning is being
  // shown because it interrupts the nomal workflow.
  Profile* profile =
      Profile::FromBrowserContext(web_contents_->GetBrowserContext());
  PrefService* prefs = profile->GetPrefs();
  filler_->UpdateTriggerSubmission(
      ShouldTriggerSubmission() &&
      !local_password_migration::ShouldShowWarning(profile) &&
      !access_loss_warning_bridge_->ShouldShowAccessLossNoticeSheet(prefs));
  filler_->FillUsernameAndPassword(credential.username(),
                                   credential.password());
  if (access_loss_warning_bridge_->ShouldShowAccessLossNoticeSheet(prefs)) {
    access_loss_warning_bridge_->MaybeShowAccessLossNoticeSheet(
        prefs, web_contents_->GetTopLevelNativeWindow(), profile);
  } else {
    // TODO: crbug.com/340437382 - Deprecate the migration warning sheet.
    ShowPasswordMigrationWarningIfNeeded();
  }

  if (ShouldTriggerSubmission()) {
    password_client_->StartSubmissionTrackingAfterTouchToFill(
        credential.username());
  }

  CleanUpFillerAndReportOutcome(TouchToFillOutcome::kCredentialFilled,
                                /*show_virtual_keyboard=*/false);
  std::move(action_complete_).Run();
}

void TouchToFillControllerAutofillDelegate::CleanUpFillerAndReportOutcome(
    TouchToFillOutcome outcome,
    bool show_virtual_keyboard) {
  // User action is complete which indicates that the user has been informed
  // about any shared unnotified password. Report that to the client to mark
  // them as notified.
  // If the render frame host has been destroyed already, the url will be empty
  // and the credentials cache in the `password_client_` has been cleared. In
  // this case it's not possible to mark credentials as notitied. If the user
  // has properly interact with the touch to fill UI, the client would have been
  // notified properly.
  GURL url = GetFrameUrl();
  if (!url.is_empty()) {
    password_client_->MarkSharedCredentialsAsNotified(url);
  }
  filler_->Dismiss(ToShowVirtualKeyboard(show_virtual_keyboard));
  filler_.reset();
  base::UmaHistogramEnumeration("PasswordManager.TouchToFill.Outcome", outcome);
}

void TouchToFillControllerAutofillDelegate::
    ShowPasswordMigrationWarningIfNeeded() {
  if (!local_password_migration::ShouldShowWarning(
          Profile::FromBrowserContext(web_contents_->GetBrowserContext()))) {
    return;
  }
  show_password_migration_warning_.Run(
      web_contents_->GetTopLevelNativeWindow(),
      Profile::FromBrowserContext(web_contents_->GetBrowserContext()),
      password_manager::metrics_util::PasswordMigrationWarningTriggers::
          kTouchToFill);
}