blob: 7b8696e4edf7130d2b33363b1c3eae25d38b5dd3 [file] [log] [blame]
// 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 "components/search_engines/site_search_policy_handler.h"
#include "base/containers/flat_set.h"
#include "base/feature_list.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "base/values.h"
#include "components/omnibox/common/omnibox_features.h"
#include "components/policy/core/browser/policy_error_map.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/policy_constants.h"
#include "components/prefs/pref_value_map.h"
#include "components/search_engines/default_search_manager.h"
#include "components/search_engines/enterprise_site_search_manager.h"
#include "components/search_engines/search_terms_data.h"
#include "components/search_engines/template_url.h"
#include "components/search_engines/template_url_data.h"
#include "components/strings/grit/components_strings.h"
namespace policy {
namespace {
bool IsSiteSearchPolicyEnabled() {
// Check that FeatureList is available as a protection against early startup
// crashes. Some policy providers are initialized very early even before
// base::FeatureList is available, but when policies are finally applied, the
// feature stack is fully initialized. The instance check ensures that the
// final decision is delayed until all features are initialized, without any
// other downstream effect.
return base::FeatureList::GetInstance() &&
base::FeatureList::IsEnabled(omnibox::kSiteSearchSettingsPolicy);
}
// Converts a site search policy entry `policy_dict` into a dictionary to be
// saved to prefs, with fields corresponding to `TemplateURLData`.
base::Value SiteSearchDictFromPolicyValue(const base::Value::Dict& policy_dict,
bool featured) {
base::Value::Dict dict;
const std::string* name =
policy_dict.FindString(SiteSearchPolicyHandler::kName);
CHECK(name);
dict.Set(DefaultSearchManager::kShortName, *name);
const std::string* shortcut =
policy_dict.FindString(SiteSearchPolicyHandler::kShortcut);
CHECK(shortcut);
dict.Set(DefaultSearchManager::kKeyword,
featured ? "@" + *shortcut : *shortcut);
const std::string* url =
policy_dict.FindString(SiteSearchPolicyHandler::kUrl);
CHECK(url);
dict.Set(DefaultSearchManager::kURL, *url);
dict.Set(DefaultSearchManager::kFeaturedByPolicy, featured);
dict.Set(DefaultSearchManager::kCreatedByPolicy,
static_cast<int>(TemplateURLData::CreatedByPolicy::kSiteSearch));
dict.Set(DefaultSearchManager::kEnforcedByPolicy, false);
dict.Set(DefaultSearchManager::kIsActive,
static_cast<int>(TemplateURLData::ActiveStatus::kTrue));
// TODO(b/307543761): Create a new field `featured_by_policy` and setting
// according to the corresponding dictionary field.
dict.Set(DefaultSearchManager::kFaviconURL,
TemplateURL::GenerateFaviconURL(GURL(*url)).spec());
dict.Set(DefaultSearchManager::kSafeForAutoReplace, false);
double timestamp = static_cast<double>(
base::Time::Now().ToDeltaSinceWindowsEpoch().InMicroseconds());
dict.Set(DefaultSearchManager::kDateCreated, timestamp);
dict.Set(DefaultSearchManager::kLastModified, timestamp);
return base::Value(std::move(dict));
}
const std::string& GetField(const base::Value& provider,
const char* field_name) {
const std::string* value = provider.GetDict().FindString(field_name);
// This is safe because `SimpleSchemaValidatingPolicyHandler` guarantees that
// the policy value is valid according to the schema.
CHECK(value);
return *value;
}
const std::string& GetShortcut(const base::Value& provider) {
return GetField(provider, SiteSearchPolicyHandler::kShortcut);
}
const std::string& GetName(const base::Value& provider) {
return GetField(provider, SiteSearchPolicyHandler::kName);
}
const std::string& GetUrl(const base::Value& provider) {
return GetField(provider, SiteSearchPolicyHandler::kUrl);
}
bool ShortcutIsEmpty(const std::string& policy_name,
const std::string& shortcut,
PolicyErrorMap* errors) {
if (!shortcut.empty()) {
return false;
}
errors->AddError(policy_name,
IDS_POLICY_SITE_SEARCH_SETTINGS_SHORTCUT_IS_EMPTY);
return true;
}
bool NameIsEmpty(const std::string& policy_name,
const std::string& name,
PolicyErrorMap* errors) {
if (!name.empty()) {
return false;
}
errors->AddError(policy_name, IDS_POLICY_SITE_SEARCH_SETTINGS_NAME_IS_EMPTY);
return true;
}
bool UrlIsEmpty(const std::string& policy_name,
const std::string& url,
PolicyErrorMap* errors) {
if (!url.empty()) {
return false;
}
errors->AddError(policy_name, IDS_POLICY_SITE_SEARCH_SETTINGS_URL_IS_EMPTY);
return true;
}
bool ShortcutHasWhitespace(const std::string& policy_name,
const std::string& shortcut,
PolicyErrorMap* errors) {
if (shortcut.find_first_of(base::kWhitespaceASCII) == std::u16string::npos) {
return false;
}
errors->AddError(policy_name,
IDS_POLICY_SITE_SEARCH_SETTINGS_SHORTCUT_CONTAINS_SPACE,
shortcut);
return true;
}
bool ShortcutStartsWithAtSymbol(const std::string& policy_name,
const std::string& shortcut,
PolicyErrorMap* errors) {
if (shortcut[0] != '@') {
return false;
}
errors->AddError(policy_name,
IDS_POLICY_SITE_SEARCH_SETTINGS_SHORTCUT_STARTS_WITH_AT,
shortcut);
return true;
}
bool ShortcutEqualsDefaultSearchProviderKeyword(const std::string& policy_name,
const std::string& shortcut,
const PolicyMap& policies,
PolicyErrorMap* errors) {
const base::Value* provider_enabled = policies.GetValue(
key::kDefaultSearchProviderEnabled, base::Value::Type::BOOLEAN);
const base::Value* provider_keyword = policies.GetValue(
key::kDefaultSearchProviderKeyword, base::Value::Type::STRING);
// Ignore if `DefaultSearchProviderEnabled` is not set, invalid, or disabled.
// Ignore if `DefaultSearchProviderKeyword` is not set, invalid, or different
// from `shortcut`.
if (!provider_enabled || !provider_enabled->GetBool() || !provider_keyword ||
shortcut != provider_keyword->GetString()) {
return false;
}
errors->AddError(policy_name,
IDS_POLICY_SITE_SEARCH_SETTINGS_SHORTCUT_EQUALS_DSP_KEYWORD,
shortcut);
return true;
}
bool ReplacementStringIsMissingFromUrl(const std::string& policy_name,
const std::string& url,
PolicyErrorMap* errors) {
TemplateURLData data;
data.SetURL(url);
SearchTermsData search_terms_data;
if (TemplateURL(data).SupportsReplacement(search_terms_data)) {
return false;
}
errors->AddError(
policy_name,
IDS_POLICY_SITE_SEARCH_SETTINGS_URL_DOESNT_SUPPORT_REPLACEMENT, url);
return true;
}
void WarnIfNonHttpsUrl(const std::string& policy_name,
const std::string& url,
PolicyErrorMap* errors) {
GURL gurl(url);
if (!gurl.SchemeIs(url::kHttpsScheme)) {
errors->AddError(policy_name, IDS_POLICY_SITE_SEARCH_SETTINGS_URL_NOT_HTTPS,
url);
}
}
bool ShortcutAlreadySeen(
const std::string& policy_name,
const std::string& shortcut,
const base::flat_set<std::string>& shortcuts_already_seen,
PolicyErrorMap* errors,
base::flat_set<std::string>* duplicated_shortcuts) {
if (shortcuts_already_seen.find(shortcut) == shortcuts_already_seen.end()) {
return false;
}
if (duplicated_shortcuts->find(shortcut) == duplicated_shortcuts->end()) {
duplicated_shortcuts->insert(shortcut);
// Only show an error message once per shortcut.
errors->AddError(policy_name,
IDS_POLICY_SITE_SEARCH_SETTINGS_DUPLICATED_SHORTCUT,
shortcut);
}
return true;
}
} // namespace
const char SiteSearchPolicyHandler::kName[] = "name";
const char SiteSearchPolicyHandler::kShortcut[] = "shortcut";
const char SiteSearchPolicyHandler::kUrl[] = "url";
const char SiteSearchPolicyHandler::kFeatured[] = "featured";
const int SiteSearchPolicyHandler::kMaxSiteSearchProviders = 100;
const int SiteSearchPolicyHandler::kMaxFeaturedProviders = 3;
SiteSearchPolicyHandler::SiteSearchPolicyHandler(Schema schema)
: SimpleSchemaValidatingPolicyHandler(
key::kSiteSearchSettings,
EnterpriseSiteSearchManager::kSiteSearchSettingsPrefName,
schema,
policy::SchemaOnErrorStrategy::SCHEMA_ALLOW_UNKNOWN,
SimpleSchemaValidatingPolicyHandler::RECOMMENDED_PROHIBITED,
SimpleSchemaValidatingPolicyHandler::MANDATORY_ALLOWED) {}
SiteSearchPolicyHandler::~SiteSearchPolicyHandler() = default;
bool SiteSearchPolicyHandler::CheckPolicySettings(const PolicyMap& policies,
PolicyErrorMap* errors) {
ignored_shortcuts_.clear();
if (!IsSiteSearchPolicyEnabled() || !policies.Get(policy_name())) {
return true;
}
if (!SimpleSchemaValidatingPolicyHandler::CheckPolicySettings(policies,
errors)) {
return false;
}
const base::Value::List& site_search_providers =
policies.GetValue(policy_name(), base::Value::Type::LIST)->GetList();
if (site_search_providers.size() > kMaxSiteSearchProviders) {
errors->AddError(policy_name(),
IDS_POLICY_SITE_SEARCH_SETTINGS_MAX_PROVIDERS_LIMIT_ERROR,
base::NumberToString(kMaxSiteSearchProviders));
return false;
}
int num_featured = base::ranges::count_if(
site_search_providers, [](const base::Value& provider) {
return provider.GetDict()
.FindBool(SiteSearchPolicyHandler::kFeatured)
.value_or(false);
});
if (num_featured > kMaxFeaturedProviders) {
errors->AddError(
policy_name(),
IDS_POLICY_SITE_SEARCH_SETTINGS_MAX_FEATURED_PROVIDERS_LIMIT_ERROR,
base::NumberToString(kMaxFeaturedProviders));
return false;
}
base::flat_set<std::string> shortcuts_already_seen;
base::flat_set<std::string> duplicated_shortcuts;
for (const base::Value& provider : site_search_providers) {
const std::string& shortcut = GetShortcut(provider);
const std::string& url = GetUrl(provider);
// TODO(b/309457951): Add validation to ensure that at most 3 entries are
// featured_by_policy.
bool invalid_entry =
ShortcutIsEmpty(policy_name(), shortcut, errors) ||
NameIsEmpty(policy_name(), GetName(provider), errors) ||
UrlIsEmpty(policy_name(), url, errors) ||
ShortcutHasWhitespace(policy_name(), shortcut, errors) ||
ShortcutStartsWithAtSymbol(policy_name(), shortcut, errors) ||
ShortcutEqualsDefaultSearchProviderKeyword(policy_name(), shortcut,
policies, errors) ||
ShortcutAlreadySeen(policy_name(), shortcut, shortcuts_already_seen,
errors, &duplicated_shortcuts) ||
ReplacementStringIsMissingFromUrl(policy_name(), url, errors);
if (invalid_entry) {
ignored_shortcuts_.insert(shortcut);
} else {
WarnIfNonHttpsUrl(policy_name(), url, errors);
}
shortcuts_already_seen.insert(shortcut);
}
// Accept if there is at least one shortcut that should not be ignored.
if (shortcuts_already_seen.size() > ignored_shortcuts_.size()) {
return true;
}
errors->AddError(policy_name(),
IDS_POLICY_SITE_SEARCH_SETTINGS_NO_VALID_PROVIDER);
return false;
}
void SiteSearchPolicyHandler::ApplyPolicySettings(const PolicyMap& policies,
PrefValueMap* prefs) {
if (!IsSiteSearchPolicyEnabled()) {
return;
}
const base::Value* policy_value =
policies.GetValue(policy_name(), base::Value::Type::LIST);
if (!policy_value) {
// Reset site search engines if policy was reset.
EnterpriseSiteSearchManager::AddPrefValueToMap(base::Value::List(), prefs);
return;
}
CHECK(policy_value->is_list());
base::Value::List providers;
for (const base::Value& item : policy_value->GetList()) {
const std::string& shortcut = GetShortcut(item);
if (ignored_shortcuts_.find(shortcut) == ignored_shortcuts_.end()) {
const base::Value::Dict& policy_dict = item.GetDict();
providers.Append(
SiteSearchDictFromPolicyValue(policy_dict, /*featured=*/false));
if (policy_dict.FindBool(SiteSearchPolicyHandler::kFeatured)
.value_or(false)) {
providers.Append(SiteSearchDictFromPolicyValue(policy_dict,
/*featured=*/true));
}
}
}
EnterpriseSiteSearchManager::AddPrefValueToMap(std::move(providers), prefs);
}
} // namespace policy