From 5fef343164a6301d8dcf2b01ec285abb85a92df4 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sun, 24 May 2026 15:23:13 +0200 Subject: [PATCH] LibWebView: Load content blocker lists from settings Add a content_blocking.list_paths advanced setting backed by a JSON array. This lets content blocker list paths avoid splitting one string. The about:settings editor presents array values as one path per line. It persists them as arrays. Load configured paths before command-line paths so temporary lists can still be appended for a single browser session. --- Base/res/ladybird/about-pages/settings.html | 11 +++- .../ladybird/about-pages/settings/advanced.js | 16 +++++ Libraries/LibWebView/Application.cpp | 10 ++- Libraries/LibWebView/Settings.cpp | 65 +++++++++++++++++-- Libraries/LibWebView/Settings.h | 3 + Libraries/LibWebView/WebUI/SettingsUI.cpp | 8 ++- 6 files changed, 101 insertions(+), 12 deletions(-) diff --git a/Base/res/ladybird/about-pages/settings.html b/Base/res/ladybird/about-pages/settings.html index c8042fe687..d0d1de3dbd 100644 --- a/Base/res/ladybird/about-pages/settings.html +++ b/Base/res/ladybird/about-pages/settings.html @@ -117,7 +117,8 @@ input[type="search"], input[type="text"], input[type="url"], - select { + select, + textarea { background-color: var(--input-background-color); width: 100%; @@ -319,10 +320,16 @@ } .config-control input[type="text"], - .config-control input[type="number"] { + .config-control input[type="number"], + .config-control textarea { width: 100%; } + .config-control textarea { + min-width: 280px; + resize: vertical; + } + @media (max-width: 600px) { .config-row { grid-template-columns: minmax(0, 1fr); diff --git a/Base/res/ladybird/about-pages/settings/advanced.js b/Base/res/ladybird/about-pages/settings/advanced.js index a58341c85c..8c15bbefa1 100644 --- a/Base/res/ladybird/about-pages/settings/advanced.js +++ b/Base/res/ladybird/about-pages/settings/advanced.js @@ -24,6 +24,22 @@ function createControl(variable) { return control; } + if (variable.type === "array" && variable.elementType === "string") { + const input = document.createElement("textarea"); + input.rows = 3; + input.value = Array.isArray(variable.value) ? variable.value.join("\n") : ""; + input.addEventListener("change", () => { + const value = input.value + .split(/\r?\n/) + .map(entry => entry.trim()) + .filter(entry => entry.length !== 0); + + sendConfigVariableValue(variable, value); + }); + control.append(input); + return control; + } + const input = document.createElement("input"); input.value = variable.value ?? ""; diff --git a/Libraries/LibWebView/Application.cpp b/Libraries/LibWebView/Application.cpp index 9a2a453e0c..d7e28a6b10 100644 --- a/Libraries/LibWebView/Application.cpp +++ b/Libraries/LibWebView/Application.cpp @@ -333,8 +333,16 @@ ErrorOr Application::initialize(Main::Arguments const& arguments) if (profile_process.has_value()) profile_process_type = process_type_from_name(*profile_process); + auto configured_content_blocker_list_paths = m_settings.config_variable_as_string_array(ConfigVariableID::ContentBlockerListPaths); + Vector content_blocker_list_paths_as_byte_strings; - TRY(content_blocker_list_paths_as_byte_strings.try_ensure_capacity(content_blocker_list_paths.size())); + TRY(content_blocker_list_paths_as_byte_strings.try_ensure_capacity(configured_content_blocker_list_paths.size() + content_blocker_list_paths.size())); + for (auto const& path : configured_content_blocker_list_paths) { + if (path.is_empty()) + continue; + + content_blocker_list_paths_as_byte_strings.unchecked_append(path.to_byte_string()); + } for (auto path : content_blocker_list_paths) content_blocker_list_paths_as_byte_strings.unchecked_append(path); diff --git a/Libraries/LibWebView/Settings.cpp b/Libraries/LibWebView/Settings.cpp index f7a197ebdb..455d462dfd 100644 --- a/Libraries/LibWebView/Settings.cpp +++ b/Libraries/LibWebView/Settings.cpp @@ -67,6 +67,15 @@ static Array(ConfigVariableID::Cou .title = "Show WebContent process ID in tab titles"sv, .description = "Append the active WebContent process ID to each tab title and tooltip."sv, .default_value = false, + .array_element_type = {}, + }, + { + .id = ConfigVariableID::ContentBlockerListPaths, + .name = "content_blocking.list_paths"sv, + .title = "Content blocker list paths"sv, + .description = "Load content blocker lists from these filesystem paths on startup, in order."sv, + .default_value = JsonArray {}, + .array_element_type = JsonValue::Type::String, }, } }; @@ -90,24 +99,49 @@ static ConfigVariableDefinition const& config_variable_definition(ConfigVariable return config_variable_definitions()[static_cast(id)]; } -static bool config_variable_value_matches_type(JsonValue const& default_value, JsonValue const& value) +static bool json_value_matches_type(JsonValue const& value, JsonValue::Type type) { - switch (default_value.type()) { + switch (type) { + case JsonValue::Type::Null: + return value.is_null(); case JsonValue::Type::Bool: return value.is_bool(); case JsonValue::Type::Number: return value.is_number(); case JsonValue::Type::String: return value.is_string(); - case JsonValue::Type::Null: case JsonValue::Type::Array: + return value.is_array(); case JsonValue::Type::Object: - return value.type() == default_value.type(); + return value.is_object(); } VERIFY_NOT_REACHED(); } +static bool json_array_contains_only_type(JsonArray const& array, JsonValue::Type type) +{ + bool contains_only_type = true; + + array.for_each([&](JsonValue const& value) { + if (!json_value_matches_type(value, type)) + contains_only_type = false; + }); + + return contains_only_type; +} + +static bool config_variable_value_is_valid(ConfigVariableDefinition const& variable, JsonValue const& value) +{ + if (!json_value_matches_type(value, variable.default_value.type())) + return false; + + if (variable.default_value.is_array() && variable.array_element_type.has_value()) + return json_array_contains_only_type(value.as_array(), *variable.array_element_type); + + return true; +} + Settings Settings::create(Badge) { // FIXME: Move this to a generic "Ladybird config directory" helper. @@ -200,7 +234,7 @@ Settings Settings::create(Badge) if (auto config_variables = settings_json.value().get_object(CONFIG_VARIABLES_KEY); config_variables.has_value()) { for (auto const& variable : config_variable_definitions()) { if (auto value = config_variables->get(variable.name); value.has_value()) { - if (config_variable_value_matches_type(variable.default_value, *value)) + if (config_variable_value_is_valid(variable, *value)) settings.m_config_variables[static_cast(variable.id)] = *value; } } @@ -651,10 +685,29 @@ bool Settings::config_variable_as_bool(ConfigVariableID id) const return *value; } +Vector Settings::config_variable_as_string_array(ConfigVariableID id) const +{ + auto const& variable = config_variable_definition(id); + VERIFY(variable.default_value.is_array()); + + auto const& value = config_variable(id); + VERIFY(value.is_array()); + + Vector values; + values.ensure_capacity(value.as_array().size()); + + value.as_array().for_each([&](JsonValue const& entry) { + if (entry.is_string()) + values.append(entry.as_string()); + }); + + return values; +} + void Settings::set_config_variable(ConfigVariableID id, JsonValue value) { auto const& variable = config_variable_definition(id); - if (!config_variable_value_matches_type(variable.default_value, value)) + if (!config_variable_value_is_valid(variable, value)) return; if (m_config_variables[static_cast(id)].equals(value)) diff --git a/Libraries/LibWebView/Settings.h b/Libraries/LibWebView/Settings.h index 19ca31205f..b189c624f2 100644 --- a/Libraries/LibWebView/Settings.h +++ b/Libraries/LibWebView/Settings.h @@ -45,6 +45,7 @@ enum class GlobalPrivacyControl { enum class ConfigVariableID : u8 { ShowWebContentProcessIDInTabTitle, + ContentBlockerListPaths, Count, }; @@ -54,6 +55,7 @@ struct ConfigVariableDefinition { StringView title; StringView description; JsonValue default_value; + Optional array_element_type; }; WEBVIEW_API ReadonlySpan config_variable_definitions(); @@ -134,6 +136,7 @@ public: JsonValue const& config_variable(ConfigVariableID) const; bool config_variable_as_bool(ConfigVariableID) const; + Vector config_variable_as_string_array(ConfigVariableID) const; void set_config_variable(ConfigVariableID, JsonValue); void set_config_variable(StringView name, JsonValue const&); diff --git a/Libraries/LibWebView/WebUI/SettingsUI.cpp b/Libraries/LibWebView/WebUI/SettingsUI.cpp index 0056490b98..cb239feae7 100644 --- a/Libraries/LibWebView/WebUI/SettingsUI.cpp +++ b/Libraries/LibWebView/WebUI/SettingsUI.cpp @@ -12,9 +12,9 @@ namespace WebView { -static StringView config_variable_type_to_string(JsonValue const& value) +static StringView config_variable_type_to_string(JsonValue::Type type) { - switch (value.type()) { + switch (type) { case JsonValue::Type::Null: return "null"sv; case JsonValue::Type::Bool: @@ -114,7 +114,9 @@ void SettingsUI::load_current_settings() variable_object.set("name"sv, variable.name); variable_object.set("title"sv, variable.title); variable_object.set("description"sv, variable.description); - variable_object.set("type"sv, config_variable_type_to_string(variable.default_value)); + variable_object.set("type"sv, config_variable_type_to_string(variable.default_value.type())); + if (variable.array_element_type.has_value()) + variable_object.set("elementType"sv, config_variable_type_to_string(*variable.array_element_type)); variable_object.set("defaultValue"sv, variable.default_value); variable_object.set("value"sv, WebView::Application::settings().config_variable(variable.id));