LibWebView: Support custom search engines

This allows the user to store custom search engines via about:settings.
Custom engines will be displayed below the builtin engines in the drop-
down to select the default engine.

A couple of edge cases here:

1. We currently reject a custom engine if one with the same name already
   exists. In the future, we should allow editing custom engines.

2. If a custom engine which was the default engine is removed, we will
   disable search rather than falling back to any other engine.
This commit is contained in:
Timothy Flynn 2025-04-04 18:12:32 -04:00 committed by Andreas Kling
commit 2810071a9c
Notes: github-actions[bot] 2025-04-06 11:46:03 +00:00
7 changed files with 317 additions and 28 deletions

View file

@ -4,13 +4,12 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Find.h>
#include <LibURL/URL.h>
#include <LibWebView/SearchEngine.h>
namespace WebView {
static auto builtin_search_engines = to_array<SearchEngine>({
static auto s_builtin_search_engines = to_array<SearchEngine>({
{ "Bing"_string, "https://www.bing.com/search?q=%s"_string },
{ "Brave"_string, "https://search.brave.com/search?q=%s"_string },
{ "DuckDuckGo"_string, "https://duckduckgo.com/?q=%s"_string },
@ -23,16 +22,9 @@ static auto builtin_search_engines = to_array<SearchEngine>({
{ "Yandex"_string, "https://yandex.com/search/?text=%s"_string },
});
ReadonlySpan<SearchEngine> search_engines()
ReadonlySpan<SearchEngine> builtin_search_engines()
{
return builtin_search_engines;
}
Optional<SearchEngine> find_search_engine_by_name(StringView name)
{
return find_value(builtin_search_engines, [&](auto const& engine) {
return engine.name == name;
});
return s_builtin_search_engines;
}
String SearchEngine::format_search_query_for_display(StringView query) const

View file

@ -20,7 +20,6 @@ struct SearchEngine {
String query_url;
};
ReadonlySpan<SearchEngine> search_engines();
Optional<SearchEngine> find_search_engine_by_name(StringView name);
ReadonlySpan<SearchEngine> builtin_search_engines();
}

View file

@ -5,6 +5,7 @@
*/
#include <AK/ByteString.h>
#include <AK/Find.h>
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <AK/JsonValue.h>
@ -26,7 +27,9 @@ static constexpr auto languages_key = "languages"sv;
static auto default_language = "en"_string;
static constexpr auto search_engine_key = "searchEngine"sv;
static constexpr auto search_engine_custom_key = "custom"sv;
static constexpr auto search_engine_name_key = "name"sv;
static constexpr auto search_engine_url_key = "url"sv;
static constexpr auto autocomplete_engine_key = "autocompleteEngine"sv;
static constexpr auto autocomplete_engine_name_key = "name"sv;
@ -89,8 +92,18 @@ Settings Settings::create(Badge<Application>)
settings.m_languages = parse_json_languages(*languages);
if (auto search_engine = settings_json.value().get_object(search_engine_key); search_engine.has_value()) {
if (auto custom_engines = search_engine->get_array(search_engine_custom_key); custom_engines.has_value()) {
custom_engines->for_each([&](JsonValue const& engine) {
auto custom_engine = parse_custom_search_engine(engine);
if (!custom_engine.has_value() || settings.find_search_engine_by_name(custom_engine->name).has_value())
return;
settings.m_custom_search_engines.append(custom_engine.release_value());
});
}
if (auto search_engine_name = search_engine->get_string(search_engine_name_key); search_engine_name.has_value())
settings.m_search_engine = find_search_engine_by_name(*search_engine_name);
settings.m_search_engine = settings.find_search_engine_by_name(*search_engine_name);
}
if (auto autocomplete_engine = settings_json.value().get_object(autocomplete_engine_key); autocomplete_engine.has_value()) {
@ -144,12 +157,25 @@ JsonValue Settings::serialize_json() const
settings.set(languages_key, move(languages));
if (m_search_engine.has_value()) {
JsonArray custom_search_engines;
custom_search_engines.ensure_capacity(m_custom_search_engines.size());
for (auto const& engine : m_custom_search_engines) {
JsonObject search_engine;
search_engine.set(search_engine_name_key, engine.name);
search_engine.set(search_engine_url_key, engine.query_url);
custom_search_engines.must_append(move(search_engine));
}
JsonObject search_engine;
if (!custom_search_engines.is_empty())
search_engine.set(search_engine_custom_key, move(custom_search_engines));
if (m_search_engine.has_value())
search_engine.set(search_engine_name_key, m_search_engine->name);
if (!search_engine.is_empty())
settings.set(search_engine_key, move(search_engine));
}
if (m_autocomplete_engine.has_value()) {
JsonObject autocomplete_engine;
@ -184,6 +210,7 @@ void Settings::restore_defaults()
m_new_tab_page_url = URL::about_newtab();
m_languages = { default_language };
m_search_engine.clear();
m_custom_search_engines.clear();
m_autocomplete_engine.clear();
m_autoplay = SiteSetting {};
m_do_not_track = DoNotTrack::No;
@ -250,6 +277,62 @@ void Settings::set_search_engine(Optional<StringView> search_engine_name)
observer.search_engine_changed();
}
Optional<SearchEngine> Settings::parse_custom_search_engine(JsonValue const& search_engine)
{
if (!search_engine.is_object())
return {};
auto name = search_engine.as_object().get_string(search_engine_name_key);
auto url = search_engine.as_object().get_string(search_engine_url_key);
if (!name.has_value() || !url.has_value())
return {};
auto parsed_url = URL::Parser::basic_parse(*url);
if (!parsed_url.has_value())
return {};
return SearchEngine { .name = name.release_value(), .query_url = url.release_value() };
}
void Settings::add_custom_search_engine(SearchEngine search_engine)
{
if (find_search_engine_by_name(search_engine.name).has_value())
return;
m_custom_search_engines.append(move(search_engine));
persist_settings();
}
void Settings::remove_custom_search_engine(SearchEngine const& search_engine)
{
auto reset_default_search_engine = m_search_engine.has_value() && m_search_engine->name == search_engine.name;
if (reset_default_search_engine)
m_search_engine.clear();
m_custom_search_engines.remove_all_matching([&](auto const& engine) {
return engine.name == search_engine.name;
});
persist_settings();
if (reset_default_search_engine) {
for (auto& observer : m_observers)
observer.search_engine_changed();
}
}
Optional<SearchEngine> Settings::find_search_engine_by_name(StringView name)
{
auto comparator = [&](auto const& engine) { return engine.name == name; };
if (auto result = find_value(builtin_search_engines(), comparator); result.has_value())
return result.copy();
if (auto result = find_value(m_custom_search_engines, comparator); result.has_value())
return result.copy();
return {};
}
void Settings::set_autocomplete_engine(Optional<StringView> autocomplete_engine_name)
{
if (autocomplete_engine_name.has_value())

View file

@ -60,6 +60,10 @@ public:
Optional<SearchEngine> const& search_engine() const { return m_search_engine; }
void set_search_engine(Optional<StringView> search_engine_name);
static Optional<SearchEngine> parse_custom_search_engine(JsonValue const&);
void add_custom_search_engine(SearchEngine);
void remove_custom_search_engine(SearchEngine const&);
Optional<AutocompleteEngine> const& autocomplete_engine() const { return m_autocomplete_engine; }
void set_autocomplete_engine(Optional<StringView> autocomplete_engine_name);
@ -80,11 +84,14 @@ private:
void persist_settings();
Optional<SearchEngine> find_search_engine_by_name(StringView name);
ByteString m_settings_path;
URL::URL m_new_tab_page_url;
Vector<String> m_languages;
Optional<SearchEngine> m_search_engine;
Vector<SearchEngine> m_custom_search_engines;
Optional<AutocompleteEngine> m_autocomplete_engine;
SiteSetting m_autoplay;
DoNotTrack m_do_not_track { DoNotTrack::No };

View file

@ -34,6 +34,12 @@ void SettingsUI::register_interfaces()
register_interface("setSearchEngine"sv, [this](auto const& data) {
set_search_engine(data);
});
register_interface("addCustomSearchEngine"sv, [this](auto const& data) {
add_custom_search_engine(data);
});
register_interface("removeCustomSearchEngine"sv, [this](auto const& data) {
remove_custom_search_engine(data);
});
register_interface("setAutocompleteEngine"sv, [this](auto const& data) {
set_autocomplete_engine(data);
});
@ -94,7 +100,7 @@ void SettingsUI::set_languages(JsonValue const& languages)
void SettingsUI::load_available_engines()
{
JsonArray search_engines;
for (auto const& engine : WebView::search_engines())
for (auto const& engine : WebView::builtin_search_engines())
search_engines.must_append(engine.name);
JsonArray autocomplete_engines;
@ -116,6 +122,22 @@ void SettingsUI::set_search_engine(JsonValue const& search_engine)
WebView::Application::settings().set_search_engine(search_engine.as_string());
}
void SettingsUI::add_custom_search_engine(JsonValue const& search_engine)
{
if (auto custom_engine = Settings::parse_custom_search_engine(search_engine); custom_engine.has_value())
WebView::Application::settings().add_custom_search_engine(custom_engine.release_value());
load_current_settings();
}
void SettingsUI::remove_custom_search_engine(JsonValue const& search_engine)
{
if (auto custom_engine = Settings::parse_custom_search_engine(search_engine); custom_engine.has_value())
WebView::Application::settings().remove_custom_search_engine(*custom_engine);
load_current_settings();
}
void SettingsUI::set_autocomplete_engine(JsonValue const& autocomplete_engine)
{
if (autocomplete_engine.is_null())

View file

@ -24,6 +24,8 @@ private:
void load_available_engines();
void set_search_engine(JsonValue const&);
void add_custom_search_engine(JsonValue const&);
void remove_custom_search_engine(JsonValue const&);
void set_autocomplete_engine(JsonValue const&);
void load_forcibly_enabled_site_settings();