blob: cf8fc27df60aad14300834b918f0c74e3779d328 [file] [log] [blame]
// Copyright 2016 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/ui/webui/settings/search_engines_handler.h"
#include <string>
#include <utility>
#include "base/check_deref.h"
#include "base/functional/bind.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search_engine_choice/search_engine_choice_service_factory.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/search_engines/ui_thread_search_terms_data.h"
#include "chrome/browser/ui/search_engines/template_url_table_model.h"
#include "chrome/browser/ui/webui/search_engine_choice/icon_utils.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "components/prefs/pref_service.h"
#include "components/search_engines/search_engine_choice/search_engine_choice_service.h"
#include "components/search_engines/search_engine_choice/search_engine_choice_utils.h"
#include "components/search_engines/search_engines_pref_names.h"
#include "components/search_engines/template_url.h"
#include "components/search_engines/template_url_service.h"
#include "content/public/browser/web_ui.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/management_policy.h"
#include "extensions/common/extension.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ash/public/cpp/new_window_delegate.h"
#endif
namespace {
// The following strings need to match with the IDs of the text input elements
// at settings/search_engines_page/search_engine_edit_dialog.html.
const char kSearchEngineField[] = "searchEngine";
const char kKeywordField[] = "keyword";
const char kQueryUrlField[] = "queryUrl";
// Dummy number used for indicating that a new search engine is added.
const int kNewSearchEngineIndex = -1;
} // namespace
namespace settings {
SearchEnginesHandler::SearchEnginesHandler(Profile* profile)
: profile_(profile), list_controller_(profile) {
pref_change_registrar_.Init(profile_->GetPrefs());
}
SearchEnginesHandler::~SearchEnginesHandler() {
// TODO(tommycli): Refactor KeywordEditorController to be compatible with
// ScopedObserver so this is no longer necessary.
list_controller_.table_model()->SetObserver(nullptr);
}
void SearchEnginesHandler::RegisterMessages() {
web_ui()->RegisterMessageCallback(
"getSearchEnginesList",
base::BindRepeating(&SearchEnginesHandler::HandleGetSearchEnginesList,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"setDefaultSearchEngine",
base::BindRepeating(&SearchEnginesHandler::HandleSetDefaultSearchEngine,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"setIsActiveSearchEngine",
base::BindRepeating(&SearchEnginesHandler::HandleSetIsActiveSearchEngine,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"removeSearchEngine",
base::BindRepeating(&SearchEnginesHandler::HandleRemoveSearchEngine,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"validateSearchEngineInput",
base::BindRepeating(
&SearchEnginesHandler::HandleValidateSearchEngineInput,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"searchEngineEditStarted",
base::BindRepeating(&SearchEnginesHandler::HandleSearchEngineEditStarted,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"searchEngineEditCancelled",
base::BindRepeating(
&SearchEnginesHandler::HandleSearchEngineEditCancelled,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"searchEngineEditCompleted",
base::BindRepeating(
&SearchEnginesHandler::HandleSearchEngineEditCompleted,
base::Unretained(this)));
#if BUILDFLAG(IS_CHROMEOS_ASH)
web_ui()->RegisterMessageCallback(
"openBrowserSearchSettings",
base::BindRepeating(
&SearchEnginesHandler::HandleOpenBrowserSearchSettings,
base::Unretained(this)));
#endif
}
void SearchEnginesHandler::OnJavascriptAllowed() {
list_controller_.table_model()->SetObserver(this);
}
void SearchEnginesHandler::OnJavascriptDisallowed() {
list_controller_.table_model()->SetObserver(nullptr);
pref_change_registrar_.RemoveAll();
}
base::Value::Dict SearchEnginesHandler::GetSearchEnginesList() {
// Find the default engine.
const TemplateURL* default_engine =
list_controller_.GetDefaultSearchProvider();
std::optional<size_t> default_index =
list_controller_.table_model()->IndexOfTemplateURL(default_engine);
// Build the first list (default search engines).
base::Value::List defaults;
size_t last_default_engine_index =
list_controller_.table_model()->last_search_engine_index();
for (size_t i = 0; i < last_default_engine_index; ++i) {
// Third argument is false, as the engine is not from an extension.
defaults.Append(CreateDictionaryForEngine(i, i == default_index));
}
// Build the second list (active search engines).
base::Value::List actives;
size_t last_active_engine_index =
list_controller_.table_model()->last_active_engine_index();
CHECK_LE(last_default_engine_index, last_active_engine_index);
for (size_t i = last_default_engine_index; i < last_active_engine_index;
++i) {
// Third argument is false, as the engine is not from an extension.
actives.Append(CreateDictionaryForEngine(i, i == default_index));
}
// Build the third list (other search engines).
base::Value::List others;
size_t last_other_engine_index =
list_controller_.table_model()->last_other_engine_index();
// Sanity check for https://crbug.com/781703.
CHECK_LE(last_active_engine_index, last_other_engine_index);
for (size_t i = last_active_engine_index; i < last_other_engine_index; ++i) {
others.Append(CreateDictionaryForEngine(i, i == default_index));
}
// Build the third list (omnibox extensions).
base::Value::List extensions;
size_t engine_count = list_controller_.table_model()->RowCount();
// Sanity check for https://crbug.com/781703.
CHECK_LE(last_other_engine_index, engine_count);
for (size_t i = last_other_engine_index; i < engine_count; ++i) {
extensions.Append(CreateDictionaryForEngine(i, i == default_index));
}
base::Value::Dict search_engines_info;
search_engines_info.Set("defaults", std::move(defaults));
search_engines_info.Set("actives", std::move(actives));
search_engines_info.Set("others", std::move(others));
search_engines_info.Set("extensions", std::move(extensions));
return search_engines_info;
}
void SearchEnginesHandler::OnModelChanged() {
AllowJavascript();
FireWebUIListener("search-engines-changed", GetSearchEnginesList());
}
void SearchEnginesHandler::OnItemsChanged(size_t start, size_t length) {
OnModelChanged();
}
void SearchEnginesHandler::OnItemsAdded(size_t start, size_t length) {
OnModelChanged();
}
void SearchEnginesHandler::OnItemsRemoved(size_t start, size_t length) {
OnModelChanged();
}
base::Value::Dict SearchEnginesHandler::CreateDictionaryForEngine(
size_t index,
bool is_default) {
TemplateURLTableModel* table_model = list_controller_.table_model();
const TemplateURL* template_url = list_controller_.GetTemplateURL(index);
// Sanity check for https://crbug.com/781703.
CHECK_LT(index, table_model->RowCount());
CHECK(template_url);
// The items which are to be written into |dict| are also described in
// chrome/browser/resources/settings/search_engines_page/
// in @typedef for SearchEngine. Please update it whenever you add or remove
// any keys here.
base::Value::Dict dict;
dict.Set("id", static_cast<int>(template_url->id()));
dict.Set("name", template_url->short_name());
dict.Set("displayName",
table_model->GetText(index,
IDS_SEARCH_ENGINES_EDITOR_DESCRIPTION_COLUMN));
dict.Set("keyword", table_model->GetText(
index, IDS_SEARCH_ENGINES_EDITOR_KEYWORD_COLUMN));
Profile* profile = Profile::FromWebUI(web_ui());
dict.Set("url",
template_url->url_ref().DisplayURL(UIThreadSearchTermsData()));
dict.Set("urlLocked", ((template_url->prepopulate_id() > 0) ||
(template_url->starter_pack_id() > 0)));
GURL icon_url = template_url->favicon_url();
if (icon_url.is_valid())
dict.Set("iconURL", icon_url.spec());
const bool is_search_engine_choice_settings_ui =
search_engines::IsChoiceScreenFlagEnabled(
search_engines::ChoicePromo::kAny);
// The icons that are used for search engines in the EEA region are bundled
// with Chrome. We use the favicon service for countries outside the EEA
// region to guarantee having icons for all search engines.
search_engines::SearchEngineChoiceService* search_engine_choice_service =
search_engines::SearchEngineChoiceServiceFactory::GetForProfile(profile);
const bool is_eea_region = search_engines::IsEeaChoiceCountry(
search_engine_choice_service->GetCountryId());
if (is_search_engine_choice_settings_ui && is_eea_region &&
template_url->prepopulate_id() != 0) {
std::string_view icon_path =
GetSearchEngineGeneratedIconPath(template_url->keyword());
if (!icon_path.empty()) {
// The search engine icon path are 24px, but displayed at 16px, or 32px on
// HiDPI screens. Use the 2x version (48px) for a large enough icon.
// Note that this icon path is used in `site-favicon` which does not
// support `image-set`.
dict.Set("iconPath", base::StrCat({icon_path, "@2x"}));
}
}
dict.Set("modelIndex", base::checked_cast<int>(index));
dict.Set("canBeRemoved", list_controller_.CanRemove(template_url));
dict.Set("canBeDefault", list_controller_.CanMakeDefault(template_url));
dict.Set("default", is_default);
dict.Set("canBeEdited", list_controller_.CanEdit(template_url));
dict.Set("canBeActivated", list_controller_.CanActivate(template_url));
dict.Set("canBeDeactivated", list_controller_.CanDeactivate(template_url));
dict.Set("shouldConfirmDeletion",
list_controller_.ShouldConfirmDeletion(template_url));
dict.Set("isManaged", list_controller_.IsManaged(template_url));
TemplateURL::Type type = template_url->type();
dict.Set("isOmniboxExtension", type == TemplateURL::OMNIBOX_API_EXTENSION);
dict.Set("isPrepopulated", template_url->prepopulate_id() > 0);
dict.Set("isStarterPack", template_url->starter_pack_id() > 0);
if (type == TemplateURL::NORMAL_CONTROLLED_BY_EXTENSION ||
type == TemplateURL::OMNIBOX_API_EXTENSION) {
const extensions::Extension* extension =
extensions::ExtensionRegistry::Get(profile)->GetExtensionById(
template_url->GetExtensionId(),
extensions::ExtensionRegistry::EVERYTHING);
if (extension) {
base::Value::Dict ext_info =
extensions::util::GetExtensionInfo(extension);
ext_info.Set("canBeDisabled",
!extensions::ExtensionSystem::Get(profile)
->management_policy()
->MustRemainEnabled(extension, nullptr));
dict.Set("extension", std::move(ext_info));
}
}
return dict;
}
void SearchEnginesHandler::HandleGetSearchEnginesList(
const base::Value::List& args) {
CHECK_EQ(1U, args.size());
const base::Value& callback_id = args[0];
AllowJavascript();
ResolveJavascriptCallback(callback_id, GetSearchEnginesList());
}
void SearchEnginesHandler::HandleSetDefaultSearchEngine(
const base::Value::List& args) {
CHECK_EQ(2U, args.size());
int index = args[0].GetInt();
if (index < 0 || static_cast<size_t>(index) >=
list_controller_.table_model()->RowCount()) {
return;
}
search_engines::ChoiceMadeLocation choice_made_location =
static_cast<search_engines::ChoiceMadeLocation>(args[1].GetInt());
CHECK(choice_made_location ==
search_engines::ChoiceMadeLocation::kSearchSettings ||
choice_made_location ==
search_engines::ChoiceMadeLocation::kSearchEngineSettings);
list_controller_.MakeDefaultTemplateURL(index, choice_made_location);
base::RecordAction(base::UserMetricsAction("Options_SearchEngineSetDefault"));
}
void SearchEnginesHandler::HandleSetIsActiveSearchEngine(
const base::Value::List& args) {
CHECK_EQ(2U, args.size());
const int index = args[0].GetInt();
const bool is_active = args[1].GetBool();
if (index < 0 || static_cast<size_t>(index) >=
list_controller_.table_model()->RowCount()) {
return;
}
list_controller_.SetIsActiveTemplateURL(index, is_active);
}
void SearchEnginesHandler::HandleRemoveSearchEngine(
const base::Value::List& args) {
CHECK_EQ(1U, args.size());
int index = args[0].GetInt();
if (index < 0 || static_cast<size_t>(index) >=
list_controller_.table_model()->RowCount()) {
return;
}
if (list_controller_.CanRemove(list_controller_.GetTemplateURL(index))) {
list_controller_.RemoveTemplateURL(index);
base::RecordAction(base::UserMetricsAction("Options_SearchEngineRemoved"));
}
}
void SearchEnginesHandler::HandleSearchEngineEditStarted(
const base::Value::List& args) {
CHECK_EQ(1U, args.size());
int index = args[0].GetInt();
TemplateURL* engine = nullptr;
if (index >= 0 &&
static_cast<size_t>(index) < list_controller_.table_model()->RowCount()) {
engine = list_controller_.GetTemplateURL(index);
} else if (index != kNewSearchEngineIndex) {
return;
}
edit_controller_ = std::make_unique<EditSearchEngineController>(
engine, this, Profile::FromWebUI(web_ui()));
}
void SearchEnginesHandler::OnEditedKeyword(TemplateURL* template_url,
const std::u16string& title,
const std::u16string& keyword,
const std::string& url) {
DCHECK(!url.empty());
if (template_url)
list_controller_.ModifyTemplateURL(template_url, title, keyword, url);
else
list_controller_.AddTemplateURL(title, keyword, url);
edit_controller_.reset();
}
void SearchEnginesHandler::HandleValidateSearchEngineInput(
const base::Value::List& args) {
CHECK_EQ(3U, args.size());
const base::Value& callback_id = args[0];
const std::string& field_name = args[1].GetString();
const std::string& field_value = args[2].GetString();
ResolveJavascriptCallback(
callback_id, base::Value(CheckFieldValidity(field_name, field_value)));
}
bool SearchEnginesHandler::CheckFieldValidity(const std::string& field_name,
const std::string& field_value) {
if (!edit_controller_.get())
return false;
bool is_valid = false;
if (field_name.compare(kSearchEngineField) == 0)
is_valid = edit_controller_->IsTitleValid(base::UTF8ToUTF16(field_value));
else if (field_name.compare(kKeywordField) == 0)
is_valid = edit_controller_->IsKeywordValid(base::UTF8ToUTF16(field_value));
else if (field_name.compare(kQueryUrlField) == 0)
is_valid = edit_controller_->IsURLValid(field_value);
else
NOTREACHED();
return is_valid;
}
void SearchEnginesHandler::HandleSearchEngineEditCancelled(
const base::Value::List& args) {
if (!edit_controller_.get())
return;
edit_controller_->CleanUpCancelledAdd();
edit_controller_.reset();
}
void SearchEnginesHandler::HandleSearchEngineEditCompleted(
const base::Value::List& args) {
if (!edit_controller_.get())
return;
CHECK_EQ(3U, args.size());
const std::string& search_engine = args[0].GetString();
const std::string& keyword = args[1].GetString();
const std::string& query_url = args[2].GetString();
// Recheck validity. It's possible to get here with invalid input if e.g. the
// user calls the right JS functions directly from the web inspector.
if (CheckFieldValidity(kSearchEngineField, search_engine) &&
CheckFieldValidity(kKeywordField, keyword) &&
CheckFieldValidity(kQueryUrlField, query_url)) {
edit_controller_->AcceptAddOrEdit(base::UTF8ToUTF16(search_engine),
base::UTF8ToUTF16(keyword), query_url);
}
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
void SearchEnginesHandler::HandleOpenBrowserSearchSettings(
const base::Value::List& args) {
ash::NewWindowDelegate::GetPrimary()->OpenUrl(
GURL(chrome::kChromeUISettingsURL).Resolve(chrome::kSearchSubPage),
ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction,
ash::NewWindowDelegate::Disposition::kSwitchToTab);
}
#endif
} // namespace settings