chromium/chrome/browser/android/search_permissions/search_permissions_service.cc

// Copyright 2017 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/android/search_permissions/search_permissions_service.h"

#include <memory>

#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/permissions/permission_decision_auto_blocker_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/search_engines/ui_thread_search_terms_data.h"
#include "chrome/common/pref_names.h"
#include "components/content_settings/core/browser/content_settings_utils.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/permissions/features.h"
#include "components/permissions/permission_decision_auto_blocker.h"
#include "components/permissions/permission_uma_util.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/search_engines/template_url.h"
#include "components/search_engines/template_url_service.h"
#include "url/gurl.h"
#include "url/url_constants.h"

namespace {

const char kDSENameKey[] = "dse_name";
const char kDSEOriginKey[] = "dse_origin";
const char kDSEGeolocationSettingKey[] = "geolocation_setting_to_restore";
const char kDSENotificationsSettingKey[] = "notifications_setting_to_restore";

// Default implementation of SearchEngineDelegate that is used for production
// code.
class SearchEngineDelegateImpl
    : public SearchPermissionsService::SearchEngineDelegate {
 public:
  explicit SearchEngineDelegateImpl(Profile* profile)
      : profile_(profile),
        template_url_service_(
            TemplateURLServiceFactory::GetForProfile(profile_)) {}

  std::u16string GetDSEName() override {
    if (template_url_service_) {
      const TemplateURL* template_url =
          template_url_service_->GetDefaultSearchProvider();
      if (template_url)
        return template_url->short_name();
    }

    return std::u16string();
  }

  url::Origin GetDSEOrigin() override {
    if (template_url_service_) {
      const TemplateURL* template_url =
          template_url_service_->GetDefaultSearchProvider();
      if (template_url) {
        GURL search_url = template_url->GenerateSearchURL(
            template_url_service_->search_terms_data());
        return url::Origin::Create(search_url);
      }
    }

    return url::Origin();
  }

 private:
  raw_ptr<Profile> profile_;

  // Will be null in unittests.
  raw_ptr<TemplateURLService> template_url_service_;
};

}  // namespace

struct SearchPermissionsService::PrefValue {
  std::u16string dse_name;
  std::string dse_origin;
  ContentSetting geolocation_setting_to_restore;
  ContentSetting notifications_setting_to_restore;
};

// static
SearchPermissionsService*
SearchPermissionsService::Factory::GetForBrowserContext(
    content::BrowserContext* context) {
  return static_cast<SearchPermissionsService*>(
      GetInstance()->GetServiceForBrowserContext(context, true));
}

// static
SearchPermissionsService::Factory*
SearchPermissionsService::Factory::GetInstance() {
  return base::Singleton<SearchPermissionsService::Factory>::get();
}

SearchPermissionsService::Factory::Factory()
    : ProfileKeyedServiceFactory(
          "SearchPermissionsService",
          ProfileSelections::Builder()
              .WithRegular(ProfileSelection::kOriginalOnly)
              // TODO(crbug.com/40257657): Check if this service is needed in
              // Guest mode.
              .WithGuest(ProfileSelection::kOriginalOnly)
              .Build()) {
  DependsOn(HostContentSettingsMapFactory::GetInstance());
  DependsOn(TemplateURLServiceFactory::GetInstance());
}

SearchPermissionsService::Factory::~Factory() {}

bool SearchPermissionsService::Factory::ServiceIsCreatedWithBrowserContext()
    const {
  return true;
}

KeyedService* SearchPermissionsService::Factory::BuildServiceInstanceFor(
    content::BrowserContext* context) const {
  return new SearchPermissionsService(Profile::FromBrowserContext(context));
}

void SearchPermissionsService::Factory::RegisterProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterDictionaryPref(prefs::kDSEGeolocationSettingDeprecated);
  registry->RegisterDictionaryPref(prefs::kDSEPermissionsSettings);
  registry->RegisterBooleanPref(prefs::kDSEWasDisabledByPolicy, false);
}

SearchPermissionsService::SearchPermissionsService(Profile* profile)
    : profile_(profile),
      pref_service_(profile_->GetPrefs()),
      host_content_settings_map_(
          HostContentSettingsMapFactory::GetForProfile(profile_)) {
  // This class should never be constructed in incognito.
  DCHECK(!profile_->IsOffTheRecord());

  delegate_ = std::make_unique<SearchEngineDelegateImpl>(profile_);

  InitializeSettingsIfNeeded();
}

bool SearchPermissionsService::IsDseOrigin(const url::Origin& origin) {
  return origin.scheme() == url::kHttpsScheme &&
         origin.IsSameOriginWith(delegate_->GetDSEOrigin());
}

void SearchPermissionsService::Shutdown() {
  delegate_.reset();
}

SearchPermissionsService::~SearchPermissionsService() {}

ContentSetting SearchPermissionsService::RestoreOldSettingAndReturnPrevious(
    const GURL& dse_origin,
    ContentSettingsType type,
    ContentSetting setting_to_restore,
    bool preserve_block_setting) {
  // Read the current value of the old DSE. This is the DSE setting that we want
  // to try to apply to the new DSE origin.
  ContentSetting dse_setting = GetContentSetting(dse_origin, type);

  // The user's setting should never be ASK while an origin is the DSE. There
  // should be no way for the user to reset the DSE content setting to ASK.
  if (dse_setting == CONTENT_SETTING_ASK) {
    // The style guide suggests not to handle cases which are invalid code paths
    // however in this case there are security risks to state being invalid so
    // we ensure the dse_setting is reverted to BLOCK.
    dse_setting = CONTENT_SETTING_BLOCK;
  }

  // If `preserve_block_setting` is set we don't restore a "BLOCK" setting.
  if (dse_setting == CONTENT_SETTING_BLOCK && preserve_block_setting)
    setting_to_restore = CONTENT_SETTING_BLOCK;

  // Restore the setting for the old origin. If the user has changed the setting
  // since the origin became the DSE, we reset the setting so the user will be
  // prompted.
  if (setting_to_restore != dse_setting)
    setting_to_restore = CONTENT_SETTING_ASK;
  SetContentSetting(dse_origin, type, setting_to_restore);

  return dse_setting;
}

void SearchPermissionsService::InitializeSettingsIfNeeded() {
  GURL dse_origin = delegate_->GetDSEOrigin().GetURL();

  // `dse_origin` can be invalid in tests or if the DSE is disabled by policy.
  // If that's the case or if `RevertDSEAutomaticPermissions` is enabled, we
  // restore the old settings and erase the pref.
  const bool disabled_by_policy = !dse_origin.is_valid();
  if (pref_service_->HasPrefPath(prefs::kDSEPermissionsSettings)) {
    if (disabled_by_policy)
      pref_service_->SetBoolean(prefs::kDSEWasDisabledByPolicy, true);

    PrefValue pref = GetDSEPref();
    GURL old_dse_origin(pref.dse_origin);

    ContentSetting effective_setting = RestoreOldSettingAndReturnPrevious(
        old_dse_origin, ContentSettingsType::GEOLOCATION,
        pref.geolocation_setting_to_restore, !disabled_by_policy);
    if (!disabled_by_policy) {
      RecordAutoDSEPermissionReverted(ContentSettingsType::GEOLOCATION,
                                      pref.geolocation_setting_to_restore,
                                      effective_setting, dse_origin);
    }

    if (pref.notifications_setting_to_restore != CONTENT_SETTING_DEFAULT) {
      effective_setting = RestoreOldSettingAndReturnPrevious(
          old_dse_origin, ContentSettingsType::NOTIFICATIONS,
          pref.notifications_setting_to_restore, !disabled_by_policy);
      if (!disabled_by_policy) {
        RecordAutoDSEPermissionReverted(ContentSettingsType::NOTIFICATIONS,
                                        pref.notifications_setting_to_restore,
                                        effective_setting, dse_origin);
      }
    }
    pref_service_->ClearPref(prefs::kDSEPermissionsSettings);
  }

  RecordEffectiveDSEOriginPermissions();
}

SearchPermissionsService::PrefValue SearchPermissionsService::GetDSEPref() {
  const base::Value::Dict& dict =
      pref_service_->GetDict(prefs::kDSEPermissionsSettings);

  PrefValue pref;
  const std::string* dse_name = dict.FindString(kDSENameKey);
  const std::string* dse_origin = dict.FindString(kDSEOriginKey);
  std::optional<int> geolocation_setting_to_restore =
      dict.FindInt(kDSEGeolocationSettingKey);
  std::optional<int> notifications_setting_to_restore =
      dict.FindInt(kDSENotificationsSettingKey);

  if (dse_name && dse_origin && geolocation_setting_to_restore &&
      notifications_setting_to_restore) {
    pref.dse_name = base::UTF8ToUTF16(*dse_name);
    pref.dse_origin = *dse_origin;
    pref.geolocation_setting_to_restore =
        IntToContentSetting(*geolocation_setting_to_restore);
    pref.notifications_setting_to_restore =
        IntToContentSetting(*notifications_setting_to_restore);
  }

  return pref;
}

ContentSetting SearchPermissionsService::GetContentSetting(
    const GURL& origin,
    ContentSettingsType type) {
  return host_content_settings_map_->GetUserModifiableContentSetting(
      origin, origin, type);
}

void SearchPermissionsService::SetContentSetting(const GURL& origin,
                                                 ContentSettingsType type,
                                                 ContentSetting setting) {
  // Clear a setting before setting it. This is needed because in general
  // notifications settings can't be changed from ALLOW<->BLOCK on Android O+.
  // We need to change the setting from ALLOW->BLOCK in one case, where the
  // previous DSE had permission blocked but the new DSE we're changing to has
  // permission allowed. Thus this works around that restriction.
  // WARNING: This is a special case and in general notification settings should
  // never be changed between ALLOW<->BLOCK on Android. Do not copy this code.
  // Check with the notifications team if you need to do something like this.
  host_content_settings_map_->SetContentSettingDefaultScope(
      origin, origin, type, CONTENT_SETTING_DEFAULT);

  // If we're restoring an ASK setting, it really implies that we should delete
  // the user-defined setting to fall back to the default.
  if (setting == CONTENT_SETTING_ASK)
    return;  // We deleted the setting above already.

  host_content_settings_map_->SetContentSettingDefaultScope(origin, origin,
                                                            type, setting);
}

void SearchPermissionsService::SetSearchEngineDelegateForTest(
    std::unique_ptr<SearchEngineDelegate> delegate) {
  delegate_ = std::move(delegate);
}

void SearchPermissionsService::RecordAutoDSEPermissionReverted(
    ContentSettingsType permission_type,
    ContentSetting backed_up_setting,
    ContentSetting effective_setting,
    const GURL& origin) {
  ContentSetting end_state_setting = GetContentSetting(origin, permission_type);
  permissions::PermissionUmaUtil::RecordAutoDSEPermissionReverted(
      permission_type, backed_up_setting, effective_setting, end_state_setting);
}

void SearchPermissionsService::RecordEffectiveDSEOriginPermissions() {
  GURL dse_origin = delegate_->GetDSEOrigin().GetURL();
  if (!dse_origin.is_valid())
    return;

  permissions::PermissionUmaUtil::RecordDSEEffectiveSetting(
      ContentSettingsType::NOTIFICATIONS,
      GetContentSetting(dse_origin, ContentSettingsType::NOTIFICATIONS));

  permissions::PermissionUmaUtil::RecordDSEEffectiveSetting(
      ContentSettingsType::GEOLOCATION,
      GetContentSetting(dse_origin, ContentSettingsType::GEOLOCATION));
}

void SearchPermissionsService::SetDSEPrefForTesting(
    ContentSetting geolocation_setting_to_restore,
    ContentSetting notifications_setting_to_restore) {
  base::Value::Dict dict;
  dict.Set(kDSENameKey, delegate_->GetDSEName());
  dict.Set(kDSEOriginKey, delegate_->GetDSEOrigin().GetURL().spec());
  dict.Set(kDSEGeolocationSettingKey, geolocation_setting_to_restore);
  dict.Set(kDSENotificationsSettingKey, notifications_setting_to_restore);
  pref_service_->SetDict(prefs::kDSEPermissionsSettings, std::move(dict));
}