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.
This commit is contained in:
Andreas Kling 2026-05-24 15:23:13 +02:00 committed by Andreas Kling
parent a8a78100b3
commit 5fef343164
6 changed files with 101 additions and 12 deletions

View file

@ -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);

View file

@ -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 ?? "";

View file

@ -333,8 +333,16 @@ ErrorOr<void> 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<ByteString> 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);

View file

@ -67,6 +67,15 @@ static Array<ConfigVariableDefinition, static_cast<size_t>(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<size_t>(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<Application>)
{
// FIXME: Move this to a generic "Ladybird config directory" helper.
@ -200,7 +234,7 @@ Settings Settings::create(Badge<Application>)
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<size_t>(variable.id)] = *value;
}
}
@ -651,10 +685,29 @@ bool Settings::config_variable_as_bool(ConfigVariableID id) const
return *value;
}
Vector<String> 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<String> 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<size_t>(id)].equals(value))

View file

@ -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<JsonValue::Type> array_element_type;
};
WEBVIEW_API ReadonlySpan<ConfigVariableDefinition const> config_variable_definitions();
@ -134,6 +136,7 @@ public:
JsonValue const& config_variable(ConfigVariableID) const;
bool config_variable_as_bool(ConfigVariableID) const;
Vector<String> config_variable_as_string_array(ConfigVariableID) const;
void set_config_variable(ConfigVariableID, JsonValue);
void set_config_variable(StringView name, JsonValue const&);

View file

@ -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));