910e62b5创建于 1月15日历史提交
// Copyright 2023 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/compose/compose_enabling.h"

#include <functional>
#include <memory>
#include <tuple>
#include <type_traits>

#include "base/check.h"
#include "base/containers/flat_set.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/rand_util.h"
#include "base/strings/string_util.h"
#include "chrome/browser/about_flags.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/compose/proto/compose_optimization_guide.pb.h"
#include "chrome/browser/flag_descriptions.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/common/pref_names.h"
#include "components/compose/buildflags.h"
#include "components/compose/core/browser/compose_features.h"
#include "components/compose/core/browser/compose_metrics.h"
#include "components/compose/core/browser/config.h"
#include "components/optimization_guide/core/model_execution/feature_keys.h"
#include "components/prefs/pref_service.h"
#include "components/variations/service/variations_service.h"
#include "components/variations/service/variations_service_utils.h"
#include "components/webui/flags/feature_entry.h"
#include "components/webui/flags/flags_storage.h"
#include "content/public/browser/context_menu_params.h"
#include "content/public/browser/render_frame_host.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "chromeos/constants/chromeos_features.h"
#endif  // BUILDFLAG(IS_CHROMEOS)

namespace {

bool AutocompleteAllowed(std::string_view autocomplete_attribute) {
  // Check autocomplete is not turned off.
  return autocomplete_attribute != std::string("off");
}

std::unique_ptr<std::string>& GetCountryCodeOverride() {
  static base::NoDestructor<std::unique_ptr<std::string>> country_code_override(
      nullptr);
  return *country_code_override;
}

std::string GetCountryCode() {
  if (GetCountryCodeOverride()) {
    return *GetCountryCodeOverride();
  }
  std::string country_code =
      base::ToLowerASCII(variations::GetCurrentCountryCode(
          g_browser_process->variations_service()));
  DLOG_IF(WARNING, country_code.empty()) << "Couldn't get country info.";
  return country_code;
}

// Given a set of countries checks if the current variations country is in the
// list. A list with a single item that is "*" will accept all countries.
// Return tuple: (current_country_code, enabled_for_country).
std::tuple<std::string, bool> IsEnabledForCountry(
    const base::flat_set<std::string>& enabled_countries) {
  std::string country_code = GetCountryCode();
  if (enabled_countries.size() == 1 && enabled_countries.contains("*")) {
    return {country_code, true};
  }
  return {country_code, enabled_countries.contains(country_code)};
}

}  // namespace

// Static members' initializers.
int ComposeEnabling::enabled_for_testing_{0};
int ComposeEnabling::skip_user_check_for_testing_{0};

ComposeEnabling::ComposeEnabling(
    Profile* profile,
    signin::IdentityManager* identity_manager,
    OptimizationGuideKeyedService* opt_guide)
    : profile_(profile),
      opt_guide_(opt_guide),
      identity_manager_(identity_manager) {
  DCHECK(profile_);
}

ComposeEnabling::~ComposeEnabling() {
  opt_guide_ = nullptr;
  identity_manager_ = nullptr;
  profile_ = nullptr;
}

// Static.
ComposeEnabling::ScopedOverride
ComposeEnabling::ScopedEnableComposeForTesting() {
  enabled_for_testing_++;
  return std::make_unique<base::ScopedClosureRunner>(base::BindOnce(
      [](int& enabled_for_testing) {
        enabled_for_testing--;
        DCHECK(enabled_for_testing >= 0);
      },
      std::ref(enabled_for_testing_)));
}

// Static.
ComposeEnabling::ScopedOverride
ComposeEnabling::ScopedSkipUserCheckForTesting() {
  skip_user_check_for_testing_++;
  return std::make_unique<base::ScopedClosureRunner>(base::BindOnce(
      [](int& skip_user_check_for_testing) {
        skip_user_check_for_testing--;
        DCHECK(skip_user_check_for_testing >= 0);
      },
      std::ref(skip_user_check_for_testing_)));
}

// Static.
ComposeEnabling::ScopedOverride ComposeEnabling::OverrideCountryForTesting(
    std::string country_code) {
  CHECK(!GetCountryCodeOverride());
  GetCountryCodeOverride() = std::make_unique<std::string>(country_code);
  return std::make_unique<base::ScopedClosureRunner>(
      base::BindOnce([]() { GetCountryCodeOverride().reset(); }));
}

compose::ComposeHintDecision ComposeEnabling::GetOptimizationGuidanceForUrl(
    const GURL& url,
    Profile* profile) {

  if (!opt_guide_) {
    DVLOG(2) << "Optimization guide not found, returns unspecified";
    return compose::ComposeHintDecision::COMPOSE_HINT_DECISION_UNSPECIFIED;
  }

  optimization_guide::OptimizationMetadata metadata;

  auto opt_guide_has_hint = opt_guide_->CanApplyOptimization(
      url, optimization_guide::proto::OptimizationType::COMPOSE, &metadata);
  if (opt_guide_has_hint !=
      optimization_guide::OptimizationGuideDecision::kTrue) {
    DVLOG(2) << "Optimization guide has no hint, returns unspecified";
    return compose::ComposeHintDecision::COMPOSE_HINT_DECISION_UNSPECIFIED;
  }

  std::optional<compose::ComposeHintMetadata> compose_metadata;
  if (metadata.any_metadata().has_value()) {
    compose_metadata =
        optimization_guide::ParsedAnyMetadata<compose::ComposeHintMetadata>(
            metadata.any_metadata().value());
  }
  if (!compose_metadata.has_value()) {
    DVLOG(2) << "Optimization guide has no metadata, returns unspecified";
    return compose::ComposeHintDecision::COMPOSE_HINT_DECISION_UNSPECIFIED;
  }

  DVLOG(2) << "Optimization guide returns enum "
           << static_cast<int>(compose_metadata->decision());
  return compose_metadata->decision();
}

// Member function public entry point.
base::expected<void, compose::ComposeShowStatus> ComposeEnabling::IsEnabled() {
  return CheckEnabling(opt_guide_, identity_manager_);
}

// Static public entry point.
bool ComposeEnabling::IsEnabledForProfile(Profile* profile) {
  OptimizationGuideKeyedService* opt_guide =
      OptimizationGuideKeyedServiceFactory::GetForProfile(profile);
  signin::IdentityManager* identity_manager =
      IdentityManagerFactory::GetForProfileIfExists(profile);
  return CheckEnabling(opt_guide, identity_manager).has_value();
}

bool ComposeEnabling::IsSettingVisible(Profile* profile) {
  OptimizationGuideKeyedService* opt_guide =
      OptimizationGuideKeyedServiceFactory::GetForProfile(profile);
  signin::IdentityManager* identity_manager =
      IdentityManagerFactory::GetForProfileIfExists(profile);
  auto enabled = CheckEnabling(opt_guide, identity_manager);
  if (!enabled.has_value() &&
      enabled.error() ==
          compose::ComposeShowStatus::kUserNotAllowedByOptimizationGuide) {
    return opt_guide->IsSettingVisible(
        optimization_guide::UserVisibleFeatureKey::kCompose);
  }
  return enabled.has_value();
}

// Private static.
base::expected<void, compose::ComposeShowStatus> ComposeEnabling::CheckEnabling(
    OptimizationGuideKeyedService* opt_guide,
    signin::IdentityManager* identity_manager) {
  if (enabled_for_testing_) {
    DVLOG(2) << "enabled for testing";
    return base::ok();
  }

  if (identity_manager == nullptr || opt_guide == nullptr) {
    DVLOG(2) << "feature not reachable, a required pointer is nullptr";
    return base::unexpected(compose::ComposeShowStatus::kGenericBlocked);
  }

  // Check if the compose feature is still eligible.
  if (!base::FeatureList::IsEnabled(compose::features::kComposeEligible)) {
    DVLOG(2) << "feature not eligible";
    return base::unexpected(compose::ComposeShowStatus::kNotComposeEligible);
  }

  // Check that the feature flag is enabled.
  if (!base::FeatureList::IsEnabled(compose::features::kEnableCompose)) {
    DVLOG(2) << "feature not enabled ";
    return base::unexpected(
        compose::ComposeShowStatus::kComposeFeatureFlagDisabled);
  }

  // Check if we're running in an enabled country. Note that an empty country
  // code will cause Compose to be disabled.
  std::string country_code;
  bool is_enabled_for_country;
  std::tie(country_code, is_enabled_for_country) =
      IsEnabledForCountry(compose::GetComposeConfig().enabled_countries);
  if (!is_enabled_for_country) {
    DVLOG(2) << "not running in an enabled country: \"" << country_code << "\"";
    return base::unexpected(
        country_code.empty()
            ? compose::ComposeShowStatus::kUndefinedCountry
            : compose::ComposeShowStatus::kComposeNotEnabledInCountry);
  }

  // Check signin status.
  CoreAccountInfo core_account_info =
      identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
  if (core_account_info.IsEmpty() ||
      identity_manager->HasAccountWithRefreshTokenInPersistentErrorState(
          core_account_info.account_id)) {
    DVLOG(2) << "user not signed in";
    return base::unexpected(compose::ComposeShowStatus::kSignedOut);
  }

  // TODO(b/314199871): Remove test bypass once this check becomes mock-able.
  if (!skip_user_check_for_testing_ &&
      (!opt_guide->ShouldFeatureBeCurrentlyEnabledForUser(
          optimization_guide::UserVisibleFeatureKey::kCompose))) {
    DVLOG(2) << "Feature not available for this user";
    return base::unexpected(
        compose::ComposeShowStatus::kUserNotAllowedByOptimizationGuide);
  }

// For ChromeOS only, check whether this device is supported.
#if BUILDFLAG(IS_CHROMEOS)
  if (chromeos::features::ShouldDisableChromeComposeOnChromeOS()) {
    DVLOG(2) << "feature disabled on ChromeOS";
    return base::unexpected(compose::ComposeShowStatus::kDisabledOnChromeOS);
  }
#endif  // BUILDFLAG(IS_CHROMEOS)

  DVLOG(2) << "enabled";
  return base::ok();
}

base::expected<void, compose::ComposeShowStatus>
ComposeEnabling::ShouldTriggerNoStatePopup(
    std::string_view autocomplete_attribute,
    bool allows_writing_suggestions,
    Profile* profile,
    PrefService* prefs,
    translate::TranslateManager* translate_manager,
    const url::Origin& top_level_frame_origin,
    const url::Origin& element_frame_origin,
    GURL url,
    bool is_msbb_enabled) {
  // Check if we're running in a country where the no state popup is enabled.
  // Note that an empty country code will block the no state popup.
  std::string country_code;
  bool is_enabled_for_country;
  std::tie(country_code, is_enabled_for_country) = IsEnabledForCountry(
      compose::GetComposeConfig().proactive_nudge_countries);
  if (!is_enabled_for_country) {
    DVLOG(2) << "not running in an enabled country: \"" << country_code << "\"";
    return base::unexpected(
        country_code.empty()
            ? compose::ComposeShowStatus::kUndefinedCountry
            : compose::ComposeShowStatus::kComposeNotEnabledInCountry);
  }

  // TODO(b/319661274): Support fenced frame checks from the Autofill popup
  // entry point.
  bool is_in_fenced_frame = false;
  if (auto page_checks =
          PageLevelChecks(translate_manager, url, top_level_frame_origin,
                          element_frame_origin, is_in_fenced_frame);
      !page_checks.has_value()) {
    return base::unexpected(page_checks.error());
  }

  // The no state popup should not show for unsupported languages even if the
  // language bypass feature is enabled.
  if (!IsPageLanguageSupported(translate_manager)) {
    DVLOG(2) << "language not supported";
    return base::unexpected(compose::ComposeShowStatus::kUnsupportedLanguage);
  }

  if (!is_msbb_enabled) {
    return base::unexpected(
        compose::ComposeShowStatus::kProactiveNudgeDisabledByMSBB);
  }

  // Check URL with Optimization guide.
  switch (GetOptimizationGuidanceForUrl(url, profile)) {
    case compose::ComposeHintDecision::COMPOSE_HINT_DECISION_COMPOSE_DISABLED:
      return base::unexpected(compose::ComposeShowStatus::kPerUrlChecksFailed);
    case compose::ComposeHintDecision::COMPOSE_HINT_DECISION_DISABLE_NUDGE:
      if (!compose::GetComposeConfig()
               .proactive_nudge_bypass_optimization_guide) {
        return base::unexpected(
            compose::ComposeShowStatus::kProactiveNudgeDisabledByServerConfig);
      }
      break;
    case compose::ComposeHintDecision::COMPOSE_HINT_DECISION_UNSPECIFIED:
      if (!base::FeatureList::IsEnabled(
              compose::features::kEnableNudgeForUnspecifiedHint)) {
        return base::unexpected(
            compose::ComposeShowStatus::kProactiveNudgeUnknownServerConfig);
      }
      break;
    case compose::ComposeHintDecision::COMPOSE_HINT_DECISION_ENABLED:
      break;
  }

  // Check autocomplete attribute if the proactive nudge would be presented.
  // TODO(b/303288183): Decide if we should keep this check or not.
  if (!AutocompleteAllowed(autocomplete_attribute)) {
    DVLOG(2) << "autocomplete=off";
    return base::unexpected(compose::ComposeShowStatus::kAutocompleteOff);
  }

  if (!allows_writing_suggestions) {
    DVLOG(2) << "writingsuggestions=false";
    return base::unexpected(
        compose::ComposeShowStatus::kWritingSuggestionsFalse);
  }

  if (!prefs->GetBoolean(prefs::kEnableProactiveNudge)) {
    return base::unexpected(
        compose::ComposeShowStatus::
            kProactiveNudgeDisabledGloballyByUserPreference);
  }

  if (prefs->GetDict(prefs::kProactiveNudgeDisabledSitesWithTime)
          .Find(element_frame_origin.Serialize())) {
    return base::unexpected(compose::ComposeShowStatus::
                                kProactiveNudgeDisabledForSiteByUserPreference);
  }

  if (!compose::GetComposeConfig().proactive_nudge_enabled) {
    return base::unexpected(
        compose::ComposeShowStatus::kProactiveNudgeFeatureDisabled);
  }

  return base::ok();
}

bool ComposeEnabling::ShouldTriggerSavedStatePopup(
    autofill::AutofillSuggestionTriggerSource trigger_source) {
  // No need to preform field and page level checks since there is already saved
  // state. Only check config and features.

  if (!compose::GetComposeConfig().saved_state_nudge_enabled) {
    return false;
  }

  if (trigger_source ==
          autofill::AutofillSuggestionTriggerSource::kComposeDialogLostFocus &&
      !base::FeatureList::IsEnabled(
          compose::features::kEnableComposeSavedStateNotification)) {
    return false;
  }

  return true;
}

bool ComposeEnabling::ShouldTriggerContextMenu(
    Profile* profile,
    translate::TranslateManager* translate_manager,
    content::RenderFrameHost* rfh,
    content::ContextMenuParams& params) {
  // Make sure the underlying field is one the feature works for.
  if (!(params.is_content_editable_for_autofill ||
        (params.form_control_type &&
         *params.form_control_type ==
             blink::mojom::FormControlType::kTextArea))) {
    compose::LogComposeContextMenuShowStatus(
        compose::ComposeShowStatus::kIncompatibleFieldType);
    DVLOG(2) << "not a supported text field";
    return false;
  }

  // Get the page URL of the outermost frame.
  GURL url = rfh->GetMainFrame()->GetLastCommittedURL();

  // Check URL with the optimization guide.
  compose::ComposeHintDecision decision =
      GetOptimizationGuidanceForUrl(url, profile);
  if (decision ==
      compose::ComposeHintDecision::COMPOSE_HINT_DECISION_COMPOSE_DISABLED) {
    compose::LogComposeContextMenuShowStatus(
        compose::ComposeShowStatus::kPerUrlChecksFailed);
    DVLOG(2) << "disabled for the main frame URL";
    return false;
  }

  auto show_status = PageLevelChecks(
      translate_manager, url, rfh->GetMainFrame()->GetLastCommittedOrigin(),
      params.frame_origin, rfh->IsNestedWithinFencedFrame());
  if (!show_status.has_value()) {
    compose::LogComposeContextMenuShowStatus(show_status.error());
    DVLOG(2) << "page level checks failed";
    return false;
  }

  if (!base::FeatureList::IsEnabled(
          compose::features::kEnableComposeLanguageBypassForContextMenu) &&
      !IsPageLanguageSupported(translate_manager)) {
    DVLOG(2) << "language not supported";
    compose::LogComposeContextMenuShowStatus(
        compose::ComposeShowStatus::kUnsupportedLanguage);
    return false;
  }

  compose::LogComposeContextMenuShowStatus(
      compose::ComposeShowStatus::kShouldShow);
  return true;
}

base::expected<void, compose::ComposeShowStatus>
ComposeEnabling::PageLevelChecks(translate::TranslateManager* translate_manager,
                                 GURL url,
                                 const url::Origin& top_level_frame_origin,
                                 const url::Origin& element_frame_origin,
                                 bool is_nested_within_fenced_frame) {
  if (auto profile_show_status = IsEnabled();
      !profile_show_status.has_value()) {
    DVLOG(2) << "not enabled";
    return profile_show_status;
  }

  if (!url.SchemeIsHTTPOrHTTPS()) {
    DVLOG(2) << "incorrect scheme";
    return base::unexpected(compose::ComposeShowStatus::kIncorrectScheme);
  }

  if (is_nested_within_fenced_frame) {
    DVLOG(2) << "field nested within fenced frame not supported";
    return base::unexpected(
        compose::ComposeShowStatus::kFormFieldNestedInFencedFrame);
  }

  // Note: This does not check frames between the current and the top level
  // frame. Because all our metadata for compose is either based on the origin
  // of the top level frame or actually part of the top level frame, this is
  // sufficient for now. TODO(b/309162238) follow up on whether this is
  // sufficient long-term.
  if (top_level_frame_origin != element_frame_origin) {
    DVLOG(2) << "cross frame origin not supported";
    return base::unexpected(
        compose::ComposeShowStatus::kFormFieldInCrossOriginFrame);
  }

  return base::ok();
}

bool ComposeEnabling::IsPageLanguageSupported(
    translate::TranslateManager* translate_manager) {
  std::string page_language =
      translate_manager
          ? translate_manager->GetLanguageState()->source_language()
          : "";

  // TODO(b/307814938): Make this finch configurable.
  // Only English is supported for MVP, we will add more languages over time.
  // We accept the empty string which might be returned if the translate system
  // has not yet deterimed the language, and "und" which means translate
  // couldn't find an answer.
  return (page_language == "en" || page_language == "und" ||
          page_language.empty());
}