LibWeb+LibWebView+Services: Add a flag to enable experimental interfaces

This adds the --expose-experimental-interfaces command line flag to
enable experimental IDL interfaces. Any IDL interface with Experimental
in its exposed attributes will be disabled by default.

The problem is that by stubbing out or partially implementing interfaces
in LibWeb, we actually make some sites behave worse. For example, the
OffscreenCanvas interface being exposed makes sites believe we fully
support it, even though we don't. If the interface was not exposed,
these sites may fall back to ordinary canvas objects. Similarly, to
use YouTube, we currently have to patch out MSE interfaces.

This flag will allow developers to iteratively work on features,
without breaking such sites. We enable experimental interfaces during
tests.
This commit is contained in:
Timothy Flynn
2026-02-17 13:47:21 -05:00
committed by Shannon Booth
parent b357d3c3c8
commit 8ad1c72ed3
Notes: github-actions[bot] 2026-02-17 21:19:21 +00:00
10 changed files with 89 additions and 11 deletions

View File

@@ -242,4 +242,16 @@ void UniversalGlobalScopeMixin::notify_about_rejected_promises(Badge<EventLoop>)
}));
}
static bool s_experimental_interfaces_exposed = false;
void UniversalGlobalScopeMixin::set_experimental_interfaces_exposed(bool exposed)
{
s_experimental_interfaces_exposed = exposed;
}
bool UniversalGlobalScopeMixin::expose_experimental_interfaces()
{
return s_experimental_interfaces_exposed;
}
}

View File

@@ -50,6 +50,9 @@ public:
ImportMap const& import_map() const { return m_import_map; }
void set_import_map(ImportMap const& import_map) { m_import_map = import_map; }
static WEB_API void set_experimental_interfaces_exposed(bool);
static WEB_API bool expose_experimental_interfaces();
protected:
void visit_edges(GC::Cell::Visitor&);

View File

@@ -128,6 +128,7 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
bool disable_content_filter = false;
Optional<StringView> resource_substitution_map_path;
bool enable_autoplay = false;
bool expose_experimental_interfaces = false;
bool expose_internals_object = false;
bool force_cpu_painting = false;
bool force_fontconfig = false;
@@ -179,6 +180,7 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
args_parser.add_option(disable_http_disk_cache, "Disable HTTP disk cache", "disable-http-disk-cache");
args_parser.add_option(disable_content_filter, "Disable content filter", "disable-content-filter");
args_parser.add_option(enable_autoplay, "Enable multimedia autoplay", "enable-autoplay");
args_parser.add_option(expose_experimental_interfaces, "Expose experimental IDL interfaces", "expose-experimental-interfaces");
args_parser.add_option(expose_internals_object, "Expose internals object", "expose-internals-object");
args_parser.add_option(force_cpu_painting, "Force CPU painting", "force-cpu-painting");
args_parser.add_option(force_fontconfig, "Force using fontconfig for font loading", "force-fontconfig");
@@ -289,6 +291,7 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
.disable_site_isolation = disable_site_isolation ? DisableSiteIsolation::Yes : DisableSiteIsolation::No,
.enable_idl_tracing = enable_idl_tracing ? EnableIDLTracing::Yes : EnableIDLTracing::No,
.enable_http_memory_cache = disable_http_memory_cache ? EnableMemoryHTTPCache::No : EnableMemoryHTTPCache::Yes,
.expose_experimental_interfaces = expose_experimental_interfaces ? ExposeExperimentalInterfaces::Yes : ExposeExperimentalInterfaces::No,
.expose_internals_object = expose_internals_object ? ExposeInternalsObject::Yes : ExposeInternalsObject::No,
.force_cpu_painting = force_cpu_painting ? ForceCPUPainting::Yes : ForceCPUPainting::No,
.force_fontconfig = force_fontconfig ? ForceFontconfig::Yes : ForceFontconfig::No,
@@ -300,8 +303,9 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
create_platform_options(m_browser_options, m_request_server_options, m_web_content_options);
// Test mode implies internals object is exposed and the Skia CPU backend is used
// Test mode implies experimental interfaces and internals object are exposed and the Skia CPU backend is used.
if (m_web_content_options.is_test_mode == IsTestMode::Yes) {
m_web_content_options.expose_experimental_interfaces = ExposeExperimentalInterfaces::Yes;
m_web_content_options.expose_internals_object = ExposeInternalsObject::Yes;
m_web_content_options.force_cpu_painting = ForceCPUPainting::Yes;
}

View File

@@ -114,6 +114,8 @@ static ErrorOr<NonnullRefPtr<WebView::WebContentClient>> launch_web_content_proc
arguments.append("--enable-idl-tracing"sv);
if (web_content_options.enable_http_memory_cache == WebView::EnableMemoryHTTPCache::Yes)
arguments.append("--enable-http-memory-cache"sv);
if (web_content_options.expose_experimental_interfaces == WebView::ExposeExperimentalInterfaces::Yes)
arguments.append("--expose-experimental-interfaces"sv);
if (web_content_options.expose_internals_object == WebView::ExposeInternalsObject::Yes)
arguments.append("--expose-internals-object"sv);
if (web_content_options.force_cpu_painting == WebView::ForceCPUPainting::Yes)
@@ -182,6 +184,8 @@ ErrorOr<NonnullRefPtr<Web::HTML::WebWorkerClient>> launch_web_worker_process(Web
Vector<ByteString> arguments;
if (web_content_options.expose_experimental_interfaces == WebView::ExposeExperimentalInterfaces::Yes)
arguments.append("--expose-experimental-interfaces"sv);
if (web_content_options.enable_http_memory_cache == WebView::EnableMemoryHTTPCache::Yes)
arguments.append("--enable-http-memory-cache"sv);

View File

@@ -131,6 +131,11 @@ enum class DisableSiteIsolation {
Yes,
};
enum class ExposeExperimentalInterfaces {
No,
Yes,
};
enum class ExposeInternalsObject {
No,
Yes,
@@ -166,6 +171,7 @@ struct WebContentOptions {
DisableSiteIsolation disable_site_isolation { DisableSiteIsolation::No };
EnableIDLTracing enable_idl_tracing { EnableIDLTracing::No };
EnableMemoryHTTPCache enable_http_memory_cache { EnableMemoryHTTPCache::No };
ExposeExperimentalInterfaces expose_experimental_interfaces { ExposeExperimentalInterfaces::No };
ExposeInternalsObject expose_internals_object { ExposeInternalsObject::No };
ForceCPUPainting force_cpu_painting { ForceCPUPainting::No };
ForceFontconfig force_fontconfig { ForceFontconfig::No };

View File

@@ -184,10 +184,9 @@ void Intrinsics::create_web_namespace<@namespace_class@>(JS::Realm& realm)
};
generator.append(R"~~~(
static bool is_secure_context_interface(InterfaceName name)
static constexpr bool is_secure_context_interface(InterfaceName name)
{
switch (name) {
)~~~");
switch (name) {)~~~");
for (auto const& interface : interface_sets.intrinsics) {
if (!interface.extended_attributes.contains("SecureContext"))
continue;
@@ -202,15 +201,34 @@ static bool is_secure_context_interface(InterfaceName name)
return false;
}
}
)~~~");
generator.append(R"~~~(
static constexpr bool is_experimental_interface(InterfaceName name)
{
switch (name) {)~~~");
for (auto const& interface : interface_sets.intrinsics) {
if (!interface.extended_attributes.contains("Experimental"))
continue;
generator.set("experimental_interface_name", interface.name);
generator.append(R"~~~(
case InterfaceName::@experimental_interface_name@:)~~~");
}
generator.append(R"~~~(
return true;
default:
return false;
}
}
)~~~");
auto generate_global_exposed = [&generator](StringView global_name, Vector<IDL::Interface&> const& interface_set) {
generator.set("global_name", global_name);
generator.append(R"~~~(
static bool is_@global_name@_exposed(InterfaceName name)
static constexpr bool is_@global_name@_exposed(InterfaceName name)
{
switch (name) {
)~~~");
switch (name) {)~~~");
for (auto const& interface : interface_set) {
auto gen = generator.fork();
gen.set("interface_name", interface.name);
@@ -262,6 +280,10 @@ bool is_exposed(InterfaceName name, JS::Realm& realm)
if (is_secure_context_interface(name) && HTML::is_non_secure_context(principal_host_defined_environment_settings_object(realm)))
return false;
// AD-HOC: Do not expose experimental interfaces unless instructed to do so.
if (!HTML::UniversalGlobalScopeMixin::expose_experimental_interfaces() && is_experimental_interface(name))
return false;
// FIXME: 3. If realms settings objects cross-origin isolated capability is false, and construct is
// conditionally exposed on [CrossOriginIsolated], then return false.
@@ -378,6 +400,7 @@ static ErrorOr<void> generate_exposed_interface_implementation(StringView class_
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/@global_object_name@ExposedInterfaces.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/UniversalGlobalScope.h>
)~~~");
for (auto& interface : exposed_interfaces) {
auto gen = generator.fork();
@@ -408,7 +431,9 @@ namespace Web::Bindings {
void add_@global_object_snake_name@_exposed_interfaces(JS::Object& global)
{
static constexpr u8 attr = JS::Attribute::Writable | JS::Attribute::Configurable;
[[maybe_unused]] bool is_secure_context = HTML::is_secure_context(HTML::relevant_principal_settings_object(global));
[[maybe_unused]] bool expose_experimental_interfaces = HTML::UniversalGlobalScopeMixin::expose_experimental_interfaces();
)~~~");
auto add_interface = [class_name](SourceGenerator& gen, IDL::Interface const& interface) {
@@ -420,11 +445,16 @@ void add_@global_object_snake_name@_exposed_interfaces(JS::Object& global)
gen.set("interface_name", interface.namespaced_name);
gen.set("prototype_class", interface.prototype_class);
if (interface.extended_attributes.contains("SecureContext")) {
if (interface.extended_attributes.contains("SecureContext"sv)) {
gen.append(R"~~~(
if (is_secure_context) {)~~~");
}
if (interface.extended_attributes.contains("Experimental"sv)) {
gen.append(R"~~~(
if (expose_experimental_interfaces) {)~~~");
}
gen.append(R"~~~(
global.define_intrinsic_accessor("@interface_name@"_utf16_fly_string, attr, [](auto& realm) -> JS::Value { return &ensure_web_constructor<@prototype_class@>(realm, "@interface_name@"_fly_string); });)~~~");
@@ -450,7 +480,12 @@ void add_@global_object_snake_name@_exposed_interfaces(JS::Object& global)
global.define_intrinsic_accessor("@legacy_interface_name@"_utf16_fly_string, attr, [](auto& realm) -> JS::Value { return &ensure_web_constructor<@prototype_class@>(realm, "@legacy_interface_name@"_fly_string); });)~~~");
}
if (interface.extended_attributes.contains("SecureContext")) {
if (interface.extended_attributes.contains("Experimental"sv)) {
gen.append(R"~~~(
})~~~");
}
if (interface.extended_attributes.contains("SecureContext"sv)) {
gen.append(R"~~~(
})~~~");
}

View File

@@ -86,6 +86,7 @@ WPT_ARGS=(
"--install-webdriver"
"--webdriver-arg=--force-cpu-painting"
"--webdriver-arg=--default-time-zone=UTC"
"--webdriver-arg=--expose-experimental-interfaces"
"--no-pause-after-test"
"--install-fonts"
"${EXTRA_WPT_ARGS[@]}"

View File

@@ -22,6 +22,7 @@
#include <LibUnicode/TimeZone.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/Fetch/Fetching/Fetching.h>
#include <LibWeb/HTML/UniversalGlobalScope.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Internals/Internals.h>
#include <LibWeb/Loader/ContentFilter.h>
@@ -90,6 +91,7 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
int request_server_socket { -1 };
int image_decoder_socket { -1 };
bool enable_test_mode = false;
bool expose_experimental_interfaces = false;
bool expose_internals_object = false;
bool wait_for_debugger = false;
bool log_all_js_exceptions = false;
@@ -111,6 +113,7 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
args_parser.add_option(request_server_socket, "File descriptor of the socket for the RequestServer connection", "request-server-socket", 'r', "request_server_socket");
args_parser.add_option(image_decoder_socket, "File descriptor of the socket for the ImageDecoder connection", "image-decoder-socket", 'i', "image_decoder_socket");
args_parser.add_option(enable_test_mode, "Enable test mode", "test-mode");
args_parser.add_option(expose_experimental_interfaces, "Expose experimental IDL interfaces", "expose-experimental-interfaces");
args_parser.add_option(expose_internals_object, "Expose internals object", "expose-internals-object");
args_parser.add_option(certificates, "Path to a certificate file", "certificate", 'C', "certificate");
args_parser.add_option(wait_for_debugger, "Wait for debugger", "wait-for-debugger");
@@ -185,6 +188,7 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
Web::HTML::Window::set_enable_test_mode(enable_test_mode);
Web::HTML::Window::set_internals_object_exposed(expose_internals_object);
Web::HTML::UniversalGlobalScopeMixin::set_experimental_interfaces_exposed(expose_experimental_interfaces);
Web::Platform::FontPlugin::install(*new WebView::FontPlugin(enable_test_mode, &font_provider));

View File

@@ -33,7 +33,7 @@ static ErrorOr<Core::Process> launch_process(StringView application, ReadonlySpa
return result;
}
static Vector<ByteString> create_arguments(ByteString const& socket_path, bool headless, bool force_cpu_painting, Optional<StringView> debug_process, Optional<StringView> default_time_zone)
static Vector<ByteString> create_arguments(ByteString const& socket_path, bool headless, bool expose_experimental_interfaces, bool force_cpu_painting, Optional<StringView> debug_process, Optional<StringView> default_time_zone)
{
Vector<ByteString> arguments {
"--webdriver-content-path"sv,
@@ -53,6 +53,8 @@ static Vector<ByteString> create_arguments(ByteString const& socket_path, bool h
arguments.append("--force-new-process"sv);
arguments.append("--enable-autoplay"sv);
arguments.append("--disable-scrollbar-painting"sv);
if (expose_experimental_interfaces)
arguments.append("--expose-experimental-interfaces"sv);
if (force_cpu_painting)
arguments.append("--force-cpu-painting"sv);
@@ -75,6 +77,7 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
auto listen_address = "0.0.0.0"sv;
int port = 8000;
bool expose_experimental_interfaces = false;
bool force_cpu_painting = false;
bool headless = false;
Optional<StringView> debug_process;
@@ -84,6 +87,7 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
args_parser.add_option(listen_address, "IP address to listen on", "listen-address", 'l', "listen_address");
args_parser.add_option(port, "Port to listen on", "port", 'p', "port");
args_parser.add_option(certificates, "Path to a certificate file", "certificate", 'C', "certificate");
args_parser.add_option(expose_experimental_interfaces, "Expose experimental IDL interfaces", "expose-experimental-interfaces");
args_parser.add_option(force_cpu_painting, "Launch browser with GPU painting disabled", "force-cpu-painting");
args_parser.add_option(debug_process, "Wait for a debugger to attach to the given process name (WebContent, RequestServer, etc.)", "debug-process", 0, "process-name");
args_parser.add_option(headless, "Launch browser without a graphical interface", "headless");
@@ -128,7 +132,7 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
}
auto launch_browser_callback = [&](ByteString const& socket_path, bool headless) {
auto arguments = create_arguments(socket_path, headless, force_cpu_painting, debug_process, default_time_zone);
auto arguments = create_arguments(socket_path, headless, expose_experimental_interfaces, force_cpu_painting, debug_process, default_time_zone);
return launch_process("Ladybird"sv, arguments.span());
};

View File

@@ -14,6 +14,7 @@
#include <LibMain/Main.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/Fetch/Fetching/Fetching.h>
#include <LibWeb/HTML/UniversalGlobalScope.h>
#include <LibWeb/Loader/GeneratedPagesLoader.h>
#include <LibWeb/Loader/ResourceLoader.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
@@ -50,6 +51,7 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
StringView serenity_resource_root;
StringView worker_type_string;
Vector<ByteString> certificates;
bool expose_experimental_interfaces = false;
bool enable_http_memory_cache = false;
bool wait_for_debugger = false;
@@ -58,6 +60,7 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
args_parser.add_option(image_decoder_socket, "File descriptor of the socket for the ImageDecoder connection", "image-decoder-socket", 'i', "image_decoder_socket");
args_parser.add_option(serenity_resource_root, "Absolute path to directory for serenity resources", "serenity-resource-root", 'r', "serenity-resource-root");
args_parser.add_option(certificates, "Path to a certificate file", "certificate", 'C', "certificate");
args_parser.add_option(expose_experimental_interfaces, "Expose experimental IDL interfaces", "expose-experimental-interfaces");
args_parser.add_option(enable_http_memory_cache, "Enable HTTP cache", "enable-http-memory-cache");
args_parser.add_option(wait_for_debugger, "Wait for debugger", "wait-for-debugger");
args_parser.add_option(worker_type_string, "Type of WebWorker to start (dedicated, shared, or service)", "type", 't', "type");
@@ -80,6 +83,8 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
TRY(initialize_image_decoder(image_decoder_socket));
Web::HTML::UniversalGlobalScopeMixin::set_experimental_interfaces_exposed(expose_experimental_interfaces);
Web::Platform::EventLoopPlugin::install(*new Web::Platform::EventLoopPluginSerenity);
Web::Platform::FontPlugin::install(*new WebView::FontPlugin(false));