mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-10-28 09:44:53 +00:00
LibWeb: Let site-compatibility rules expose experimental interfaces
Problem: Al Jazeera article pages rendered only the page footer. The site’s browser gate feature-tests navigator.permissions, and refused to hydrate the page when it wasn’t there. And in Ladybird, it wasn’t — because the Permissions API sits behind [Experimental]. Exposing the API to the web at large has to wait for its UI to be wired up — so the site stayed unusable by Ladybird users for as long as that takes. Cause: [Experimental] was a process-wide switch and nothing more: The --expose-experimental-interfaces flag, which the generated bindings read via WindowOrWorkerGlobalScopeMixin::expose_experimental_interfaces() — once for a global’s constructor names, and once per gated attribute or operation. The site-compatibility rules that already tailor the User-Agent string per site had no say in it — so, nothing could hand just one site the one API it wanted, without handing it to *every* site. Fix: Give site-compatibility rules an “expose_experimental_interfaces” key, listing the gated names a matching site gets: An interface name for a constructor (PermissionStatus), or Interface.member for an attribute or operation (Navigator.permissions). The generated bindings now call expose_experimental_interface(realm, name) on that mixin at both gating sites — which is true under the global flag, as before. It otherwise asks ResourceLoader if a rule matching the URL the realm’s environment was created with lists that name. So, the decision holds for the life of the global — exactly like the flag. Internals gains a switch for the process-wide flag — since test-web turns it on for everything.
This commit is contained in:
1 parent
62d2b76120
commit
7173b6f1dd
16 files changed
+235
-19
Notes:
github-actions[bot]
2026-09-05 00:56:53 +00:00
Author: https://github.com/sideshowbarker Commit: https://github.com/LadybirdBrowser/ladybird/commit/7173b6f1dd1 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/11539 Reviewed-by: https://github.com/Calme1709 ✅
No files matched your search
@@ -58,6 +58,7 @@
|
||||
#include <LibWeb/IndexedDB/Internal/Algorithms.h>
|
||||
#include <LibWeb/Infra/SerializedURL.h>
|
||||
#include <LibWeb/Infra/Strings.h>
|
||||
#include <LibWeb/Loader/ResourceLoader.h>
|
||||
#include <LibWeb/NavigationTiming/PerformanceNavigationTiming.h>
|
||||
#include <LibWeb/Page/Page.h>
|
||||
#include <LibWeb/PerformanceTimeline/EntryTypes.h>
|
||||
@@ -1569,6 +1570,21 @@ bool WindowOrWorkerGlobalScopeMixin::expose_experimental_interfaces()
|
||||
return s_experimental_interfaces_exposed;
|
||||
}
|
||||
|
||||
// A site-compatibility rule can expose a gated interface or member to the documents and workers it matches, without
|
||||
// exposing it to the web at large. The rule is keyed by the URL the environment was created with, so it holds for the
|
||||
// lifetime of that global, the same way the global switch does.
|
||||
bool WindowOrWorkerGlobalScopeMixin::expose_experimental_interface(EnvironmentSettingsObject& settings, StringView name)
|
||||
{
|
||||
if (s_experimental_interfaces_exposed)
|
||||
return true;
|
||||
|
||||
// The first global of a page is set up before the loader exists, and holds nothing a rule could match anyway.
|
||||
if (!ResourceLoader::is_initialized())
|
||||
return false;
|
||||
|
||||
return ResourceLoader::the().site_compatibility_exposes_experimental_interface(settings.creation_url, name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace Web::Bindings {
|
||||
|
||||
@@ -121,6 +121,7 @@ public:
|
||||
|
||||
static void set_experimental_interfaces_exposed(bool);
|
||||
static bool expose_experimental_interfaces();
|
||||
static bool expose_experimental_interface(EnvironmentSettingsObject&, StringView name);
|
||||
|
||||
[[nodiscard]] GC::Ref<Crypto::Crypto> crypto();
|
||||
[[nodiscard]] GC::Ref<ServiceWorker::CacheStorage> caches();
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
#include <LibWeb/HTML/SessionHistoryEntry.h>
|
||||
#include <LibWeb/HTML/SharedResourceRequest.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
#include <LibWeb/HTML/WindowOrWorkerGlobalScope.h>
|
||||
#include <LibWeb/Internals/InternalGamepad.h>
|
||||
#include <LibWeb/Internals/Internals.h>
|
||||
#include <LibWeb/Layout/NodeArena.h>
|
||||
@@ -847,6 +848,12 @@ WebIDL::ExceptionOr<void> Internals::set_site_compatibility_data(Utf16String con
|
||||
return {};
|
||||
}
|
||||
|
||||
// Tests run with every [Experimental] interface exposed, so this is the only way one can watch the gate itself.
|
||||
void Internals::set_experimental_interfaces_exposed(bool exposed)
|
||||
{
|
||||
HTML::WindowOrWorkerGlobalScopeMixin::set_experimental_interfaces_exposed(exposed);
|
||||
}
|
||||
|
||||
void Internals::set_content_blocking_enabled(bool enabled)
|
||||
{
|
||||
page().set_content_blocking_enabled(enabled);
|
||||
|
||||
@@ -129,6 +129,7 @@ public:
|
||||
void simulate_worker_request_server_connection_loss();
|
||||
WebIDL::ExceptionOr<void> set_content_blockers(Utf16String const& patterns);
|
||||
WebIDL::ExceptionOr<void> set_site_compatibility_data(Utf16String const& source);
|
||||
void set_experimental_interfaces_exposed(bool exposed);
|
||||
void set_content_blocking_enabled(bool enabled);
|
||||
WebIDL::UnsignedLongLong partial_layout_count();
|
||||
WebIDL::UnsignedLongLong full_layout_count();
|
||||
|
||||
@@ -114,6 +114,7 @@ interface Internals {
|
||||
undefined simulateWorkerRequestServerConnectionLoss();
|
||||
undefined setContentBlockers(DOMString patterns);
|
||||
undefined setSiteCompatibilityData(DOMString source);
|
||||
undefined setExperimentalInterfacesExposed(boolean exposed);
|
||||
undefined setContentBlockingEnabled(boolean enabled);
|
||||
unsigned long long partialLayoutCount();
|
||||
unsigned long long fullLayoutCount();
|
||||
|
||||
@@ -55,6 +55,7 @@ public:
|
||||
String user_agent_for_websocket_url(URL::URL const& url) const { return m_site_compatibility_data.user_agent_for_websocket_url(url, m_user_agent); }
|
||||
void set_user_agent(String user_agent) { m_user_agent = move(user_agent); }
|
||||
void set_site_compatibility_data(SiteCompatibilityData data) { m_site_compatibility_data = move(data); }
|
||||
bool site_compatibility_exposes_experimental_interface(URL::URL const& url, StringView name) const { return m_site_compatibility_data.exposes_experimental_interface(url, name); }
|
||||
|
||||
String const& platform() const { return m_platform; }
|
||||
void set_platform(String platform) { m_platform = move(platform); }
|
||||
|
||||
@@ -15,14 +15,14 @@ static ErrorOr<Vector<String>> parse_patterns(JsonObject const& object)
|
||||
{
|
||||
auto patterns = object.get_array("matches"sv);
|
||||
if (!patterns.has_value() || patterns->is_empty())
|
||||
return Error::from_string_literal("Site compatibility rule must contain a non-empty 'matches' array");
|
||||
return Error::from_string_literal("Site-compatibility rule must contain a non-empty 'matches' array");
|
||||
|
||||
Vector<String> result;
|
||||
TRY(result.try_ensure_capacity(patterns->size()));
|
||||
for (size_t i = 0; i < patterns->size(); ++i) {
|
||||
auto const& pattern = patterns->at(i);
|
||||
if (!pattern.is_string() || pattern.as_string().is_empty())
|
||||
return Error::from_string_literal("Site compatibility rule patterns must be non-empty strings");
|
||||
return Error::from_string_literal("Site-compatibility rule patterns must be non-empty strings");
|
||||
TRY(result.try_append(pattern.as_string()));
|
||||
}
|
||||
return result;
|
||||
@@ -30,9 +30,14 @@ static ErrorOr<Vector<String>> parse_patterns(JsonObject const& object)
|
||||
|
||||
static ErrorOr<Vector<UserAgentTransformation>> parse_user_agent_transformations(JsonObject const& object)
|
||||
{
|
||||
if (!object.has("user_agent"sv))
|
||||
return Vector<UserAgentTransformation> {};
|
||||
|
||||
// A present key must hold a non-empty array: JsonObject::get_array() alone can't tell a missing key from a value
|
||||
// of the wrong type, and a rule with a broken half must fail, not run with the half that parsed.
|
||||
auto transformations = object.get_array("user_agent"sv);
|
||||
if (!transformations.has_value() || transformations->is_empty())
|
||||
return Error::from_string_literal("Site compatibility rule must contain a non-empty 'user_agent' array");
|
||||
return Error::from_string_literal("Site-compatibility rule 'user_agent' must be a non-empty array");
|
||||
|
||||
Vector<UserAgentTransformation> result;
|
||||
TRY(result.try_ensure_capacity(transformations->size()));
|
||||
@@ -48,16 +53,45 @@ static ErrorOr<Vector<UserAgentTransformation>> parse_user_agent_transformations
|
||||
return result;
|
||||
}
|
||||
|
||||
// The names are the ones the IDL bindings gate behind [Experimental]: an interface name for a constructor the global
|
||||
// would otherwise hide, or Interface.member for an attribute or operation. A name nothing gates simply never matches.
|
||||
static ErrorOr<Vector<String>> parse_exposed_interfaces(JsonObject const& object)
|
||||
{
|
||||
if (!object.has("expose_experimental_interfaces"sv))
|
||||
return Vector<String> {};
|
||||
|
||||
auto names = object.get_array("expose_experimental_interfaces"sv);
|
||||
if (!names.has_value() || names->is_empty())
|
||||
return Error::from_string_literal("Site-compatibility rule 'expose_experimental_interfaces' must be a non-empty array");
|
||||
|
||||
Vector<String> result;
|
||||
TRY(result.try_ensure_capacity(names->size()));
|
||||
|
||||
for (size_t i = 0; i < names->size(); ++i) {
|
||||
auto const& name = names->at(i);
|
||||
if (!name.is_string() || name.as_string().is_empty())
|
||||
return Error::from_string_literal("Site-compatibility rule 'expose_experimental_interfaces' entries must be non-empty strings");
|
||||
TRY(result.try_append(name.as_string()));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ErrorOr<SiteCompatibilityRule> SiteCompatibilityRule::from_json(JsonValue const& value)
|
||||
{
|
||||
if (!value.is_object())
|
||||
return Error::from_string_literal("Site compatibility rule must be a JSON object");
|
||||
return Error::from_string_literal("Site-compatibility rule must be a JSON object");
|
||||
|
||||
auto const& object = value.as_object();
|
||||
return create(TRY(parse_patterns(object)), TRY(parse_user_agent_transformations(object)));
|
||||
auto transformations = TRY(parse_user_agent_transformations(object));
|
||||
auto exposed_interfaces = TRY(parse_exposed_interfaces(object));
|
||||
if (transformations.is_empty() && exposed_interfaces.is_empty())
|
||||
return Error::from_string_literal("Site-compatibility rule must contain a 'user_agent' or an 'expose_experimental_interfaces' array");
|
||||
|
||||
return create(TRY(parse_patterns(object)), move(transformations), move(exposed_interfaces));
|
||||
}
|
||||
|
||||
ErrorOr<SiteCompatibilityRule> SiteCompatibilityRule::create(Vector<String> patterns, Vector<UserAgentTransformation> transformations)
|
||||
ErrorOr<SiteCompatibilityRule> SiteCompatibilityRule::create(Vector<String> patterns, Vector<UserAgentTransformation> transformations, Vector<String> exposed_interfaces)
|
||||
{
|
||||
Vector<URL::RustIntegration::URLPattern> compiled_patterns;
|
||||
TRY(compiled_patterns.try_ensure_capacity(patterns.size()));
|
||||
@@ -65,20 +99,26 @@ ErrorOr<SiteCompatibilityRule> SiteCompatibilityRule::create(Vector<String> patt
|
||||
for (auto const& pattern : patterns) {
|
||||
auto compiled_pattern = URL::RustIntegration::URLPattern::create(pattern);
|
||||
if (compiled_pattern.is_error())
|
||||
return Error::from_string_literal("Site compatibility rule contains an invalid URL pattern");
|
||||
return Error::from_string_literal("Site-compatibility rule contains an invalid URL pattern");
|
||||
TRY(compiled_patterns.try_append(compiled_pattern.release_value()));
|
||||
}
|
||||
|
||||
return SiteCompatibilityRule { move(patterns), move(compiled_patterns), move(transformations) };
|
||||
return SiteCompatibilityRule { move(patterns), move(compiled_patterns), move(transformations), move(exposed_interfaces) };
|
||||
}
|
||||
|
||||
SiteCompatibilityRule::SiteCompatibilityRule(Vector<String> patterns, Vector<URL::RustIntegration::URLPattern> compiled_patterns, Vector<UserAgentTransformation> transformations)
|
||||
SiteCompatibilityRule::SiteCompatibilityRule(Vector<String> patterns, Vector<URL::RustIntegration::URLPattern> compiled_patterns, Vector<UserAgentTransformation> transformations, Vector<String> exposed_interfaces)
|
||||
: m_patterns(move(patterns))
|
||||
, m_compiled_patterns(move(compiled_patterns))
|
||||
, m_user_agent_transformations(move(transformations))
|
||||
, m_exposed_interfaces(move(exposed_interfaces))
|
||||
{
|
||||
}
|
||||
|
||||
bool SiteCompatibilityRule::exposes_experimental_interface(StringView name) const
|
||||
{
|
||||
return m_exposed_interfaces.contains_slow(name);
|
||||
}
|
||||
|
||||
bool SiteCompatibilityRule::matches(URL::URL const& url) const
|
||||
{
|
||||
auto serialized_url = url.serialize();
|
||||
@@ -138,10 +178,19 @@ String SiteCompatibilityData::user_agent_for_websocket_url(URL::URL const& url,
|
||||
return user_agent_for_url(http_url, default_user_agent);
|
||||
}
|
||||
|
||||
bool SiteCompatibilityData::exposes_experimental_interface(URL::URL const& url, StringView name) const
|
||||
{
|
||||
for (auto const& rule : m_rules) {
|
||||
if (rule.exposes_experimental_interface(name) && rule.matches(url))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ErrorOr<SiteCompatibilityData> SiteCompatibilityData::from_json(JsonValue const& value)
|
||||
{
|
||||
if (!value.is_array())
|
||||
return Error::from_string_literal("Site compatibility data must be a JSON array");
|
||||
return Error::from_string_literal("Site-compatibility data must be a JSON array");
|
||||
|
||||
SiteCompatibilityData data;
|
||||
auto const& rules = value.as_array();
|
||||
|
||||
@@ -23,20 +23,23 @@ enum class UserAgentTransformation : u8 {
|
||||
class WEB_API SiteCompatibilityRule {
|
||||
public:
|
||||
static ErrorOr<SiteCompatibilityRule> from_json(JsonValue const&);
|
||||
static ErrorOr<SiteCompatibilityRule> create(Vector<String> patterns, Vector<UserAgentTransformation> transformations);
|
||||
static ErrorOr<SiteCompatibilityRule> create(Vector<String> patterns, Vector<UserAgentTransformation> transformations, Vector<String> exposed_interfaces = {});
|
||||
|
||||
Vector<String> const& patterns() const { return m_patterns; }
|
||||
Vector<UserAgentTransformation> const& user_agent_transformations() const { return m_user_agent_transformations; }
|
||||
Vector<String> const& exposed_interfaces() const { return m_exposed_interfaces; }
|
||||
|
||||
bool matches(URL::URL const&) const;
|
||||
String apply_user_agent_transformations(StringView user_agent) const;
|
||||
bool exposes_experimental_interface(StringView name) const;
|
||||
|
||||
private:
|
||||
SiteCompatibilityRule(Vector<String> patterns, Vector<URL::RustIntegration::URLPattern> compiled_patterns, Vector<UserAgentTransformation> transformations);
|
||||
SiteCompatibilityRule(Vector<String> patterns, Vector<URL::RustIntegration::URLPattern> compiled_patterns, Vector<UserAgentTransformation> transformations, Vector<String> exposed_interfaces);
|
||||
|
||||
Vector<String> m_patterns;
|
||||
Vector<URL::RustIntegration::URLPattern> m_compiled_patterns;
|
||||
Vector<UserAgentTransformation> m_user_agent_transformations;
|
||||
Vector<String> m_exposed_interfaces;
|
||||
};
|
||||
|
||||
class WEB_API SiteCompatibilityData {
|
||||
@@ -48,6 +51,7 @@ public:
|
||||
Vector<SiteCompatibilityRule> const& rules() const { return m_rules; }
|
||||
String user_agent_for_url(URL::URL const&, StringView default_user_agent) const;
|
||||
String user_agent_for_websocket_url(URL::URL const&, StringView default_user_agent) const;
|
||||
bool exposes_experimental_interface(URL::URL const&, StringView name) const;
|
||||
|
||||
private:
|
||||
Vector<SiteCompatibilityRule> m_rules;
|
||||
|
||||
@@ -294,6 +294,7 @@ def define_the_attributes(
|
||||
includes,
|
||||
attribute.extended_attributes,
|
||||
definition.getvalue(),
|
||||
f"{interface.name}.{attribute.name}",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -305,7 +306,11 @@ def define_the_static_attributes(out: TextIO, includes: GeneratedIncludes, inter
|
||||
out.write(wrap_with_extended_attribute_exposure_checks(includes, attribute.extended_attributes, definition))
|
||||
continue
|
||||
definition = f' object.define_native_accessor(realm, "{attribute.name}"_utf16_fly_string, {attribute_getter_callback_name(attribute)}, nullptr, default_attributes);\n'
|
||||
out.write(wrap_with_extended_attribute_exposure_checks(includes, attribute.extended_attributes, definition))
|
||||
out.write(
|
||||
wrap_with_extended_attribute_exposure_checks(
|
||||
includes, attribute.extended_attributes, definition, f"{interface.name}.{attribute.name}"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def write_attribute_getters(
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Generators.libweb_bindings.includes import GeneratedIncludes
|
||||
|
||||
|
||||
def wrap_with_extended_attribute_exposure_checks(
|
||||
includes: GeneratedIncludes, extended_attributes: dict[str, str], text: str
|
||||
includes: GeneratedIncludes,
|
||||
extended_attributes: dict[str, str],
|
||||
text: str,
|
||||
experimental_name: str | None = None,
|
||||
) -> str:
|
||||
if "SecureContext" in extended_attributes:
|
||||
includes.add("LibWeb/Bindings/PrincipalHostDefined.h")
|
||||
@@ -23,9 +28,13 @@ def wrap_with_extended_attribute_exposure_checks(
|
||||
"""
|
||||
|
||||
if "Experimental" in extended_attributes:
|
||||
# The name is what a site-compatibility rule lists to expose this member for the sites it matches.
|
||||
if experimental_name is None:
|
||||
raise ValueError("[Experimental] members need a name for site-compatibility rules")
|
||||
includes.add("LibWeb/Bindings/PrincipalHostDefined.h")
|
||||
includes.add("LibWeb/HTML/WindowOrWorkerGlobalScope.h")
|
||||
text = text.replace("\n", "\n ")
|
||||
text = f""" if (HTML::WindowOrWorkerGlobalScopeMixin::expose_experimental_interfaces()) {{
|
||||
text = f""" if (HTML::WindowOrWorkerGlobalScopeMixin::expose_experimental_interface(Bindings::principal_host_defined_environment_settings_object(realm), "{experimental_name}"sv)) {{
|
||||
{text} }}
|
||||
"""
|
||||
|
||||
|
||||
@@ -251,6 +251,7 @@ namespace Web::Bindings {
|
||||
|
||||
write_secure_context_switch(out, interface_sets.intrinsics)
|
||||
write_experimental_switch(out, interface_sets.intrinsics)
|
||||
write_experimental_name_switch(out, interface_sets.intrinsics)
|
||||
write_global_exposed_switch(out, "window", interface_sets.window_exposed)
|
||||
write_global_exposed_switch(out, "dedicated_worker", interface_sets.dedicated_worker_exposed)
|
||||
write_global_exposed_switch(out, "shared_worker", interface_sets.shared_worker_exposed)
|
||||
@@ -281,8 +282,8 @@ 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::WindowOrWorkerGlobalScopeMixin::expose_experimental_interfaces() && is_experimental_interface(name))
|
||||
// AD-HOC: Do not expose experimental interfaces unless the global switch or a site-compatibility rule says so.
|
||||
if (is_experimental_interface(name) && !HTML::WindowOrWorkerGlobalScopeMixin::expose_experimental_interface(principal_host_defined_environment_settings_object(realm), experimental_interface_name(name)))
|
||||
return false;
|
||||
|
||||
// FIXME: 3. If realm’s settings object’s cross-origin isolated capability is false, and construct is
|
||||
@@ -329,6 +330,29 @@ def write_secure_context_switch(out: TextIO, interfaces: List[Interface]) -> Non
|
||||
)
|
||||
|
||||
|
||||
def write_experimental_name_switch(out: TextIO, interfaces: List[Interface]) -> None:
|
||||
# The name a site-compatibility rule lists to expose the interface for the sites it matches.
|
||||
out.write(
|
||||
"""static constexpr StringView experimental_interface_name(InterfaceName name)
|
||||
{
|
||||
switch (name) {
|
||||
"""
|
||||
)
|
||||
for interface in interfaces:
|
||||
if "Experimental" not in interface.extended_attributes:
|
||||
continue
|
||||
out.write(f' case InterfaceName::{interface.name}:\n return "{interface.name}"sv;\n')
|
||||
|
||||
out.write(
|
||||
""" default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def write_experimental_switch(out: TextIO, interfaces: List[Interface]) -> None:
|
||||
out.write(
|
||||
"""static constexpr bool is_experimental_interface(InterfaceName name)
|
||||
@@ -602,7 +626,7 @@ void add_{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_settings_object(global));
|
||||
[[maybe_unused]] bool expose_experimental_interfaces = HTML::WindowOrWorkerGlobalScopeMixin::expose_experimental_interfaces();
|
||||
[[maybe_unused]] auto& global_settings = HTML::relevant_settings_object(global);
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -647,7 +671,7 @@ def write_interface_global_accessor(out: TextIO, class_name: str, interface: Int
|
||||
|
||||
if "Experimental" in interface.extended_attributes:
|
||||
out.write(
|
||||
f"""{indentation}if (expose_experimental_interfaces) {{
|
||||
f"""{indentation}if (HTML::WindowOrWorkerGlobalScopeMixin::expose_experimental_interface(global_settings, "{interface.name}"sv)) {{
|
||||
"""
|
||||
)
|
||||
indentation += " "
|
||||
|
||||
@@ -178,6 +178,7 @@ def define_the_regular_operations(
|
||||
f""" object.define_native_function(realm, "{name}"_utf16_fly_string, {idl_identifier_cpp_name(operation)}, {overload_resolution.operation_overload_set_length(operations)}, default_attributes);
|
||||
|
||||
""",
|
||||
f"{interface.name}.{name}",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -212,6 +213,7 @@ def define_the_static_operations(out: TextIO, includes: GeneratedIncludes, inter
|
||||
f""" object.define_native_function(realm, "{name}"_utf16_fly_string, {idl_identifier_cpp_name(operation)}, {overload_resolution.operation_overload_set_length(operations)}, JS::Attribute::Enumerable | JS::Attribute::Configurable | JS::Attribute::Writable);
|
||||
|
||||
""",
|
||||
f"{interface.name}.{name}",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -281,6 +283,7 @@ def define_the_stringifier(
|
||||
extended_attributes,
|
||||
""" object.define_native_function(realm, "toString"_utf16_fly_string, to_string, 0, default_attributes);
|
||||
""",
|
||||
f"{interface.name}.toString",
|
||||
)
|
||||
)
|
||||
out.write(
|
||||
@@ -700,6 +703,7 @@ def collect_attribute_values(
|
||||
// 3. Perform ! CreateDataPropertyOrThrow(result, k, v).
|
||||
MUST(result->create_data_property({key_name}, {js_value_name}));
|
||||
""",
|
||||
f"{interface.name}.{attribute.name}",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
expose_experimental_interfaces as a string: rejected (InternalError)
|
||||
user_agent as a string: rejected (InternalError)
|
||||
matched document: navigator.permissions is object, PermissionStatus is function
|
||||
unmatched document: navigator.permissions is undefined, PermissionStatus is undefined
|
||||
@@ -0,0 +1,78 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../include.js"></script>
|
||||
<script>
|
||||
// A site-compatibility rule can hand named [Experimental] interfaces and members to the documents it matches — and
|
||||
// to nothing else. Tests run with every experimental interface exposed, so the gate is switched off first; only
|
||||
// the documents created after that see the real gate, and only the one a rule matches gets navigator.permissions
|
||||
// back. (Workers live in a process this switch can't reach, so they aren't probed here; they take the same path
|
||||
// with their own creation URL.)
|
||||
asyncTest(async done => {
|
||||
const server = httpTestServer();
|
||||
const testID = crypto.randomUUID();
|
||||
|
||||
async function createDocument(name) {
|
||||
const script = `parent.postMessage({ permissions: typeof navigator.permissions, status: typeof PermissionStatus }, "*");`;
|
||||
return server.createEcho("GET", `/site-compatibility-expose-${name}-${testID}`, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/html" },
|
||||
body: `<!DOCTYPE html><script>${script}<\/script>`,
|
||||
});
|
||||
}
|
||||
|
||||
function loadAndReport(name, url) {
|
||||
return new Promise(resolve => {
|
||||
const frame = document.createElement("iframe");
|
||||
window.addEventListener("message", function handler(event) {
|
||||
if (event.source !== frame.contentWindow)
|
||||
return;
|
||||
window.removeEventListener("message", handler);
|
||||
frame.remove();
|
||||
println(`${name}: navigator.permissions is ${event.data.permissions}, PermissionStatus is ${event.data.status}`);
|
||||
resolve();
|
||||
});
|
||||
frame.src = url;
|
||||
document.body.appendChild(frame);
|
||||
});
|
||||
}
|
||||
|
||||
const matchedURL = await createDocument("matched");
|
||||
const unmatchedURL = await createDocument("unmatched");
|
||||
|
||||
// A rule with a field of the wrong type is rejected as a whole — not run with the half that parsed.
|
||||
for (const [label, rule] of [
|
||||
["expose_experimental_interfaces as a string", {
|
||||
matches: [matchedURL],
|
||||
user_agent: ["hide_Ladybird"],
|
||||
expose_experimental_interfaces: "PermissionStatus",
|
||||
}],
|
||||
["user_agent as a string", {
|
||||
matches: [matchedURL],
|
||||
expose_experimental_interfaces: ["PermissionStatus"],
|
||||
user_agent: "hide_Ladybird",
|
||||
}],
|
||||
]) {
|
||||
try {
|
||||
internals.setSiteCompatibilityData(JSON.stringify([rule]));
|
||||
println(`${label}: accepted`);
|
||||
} catch (error) {
|
||||
println(`${label}: rejected (${error.name})`);
|
||||
}
|
||||
}
|
||||
|
||||
internals.setSiteCompatibilityData(JSON.stringify([{
|
||||
matches: [matchedURL],
|
||||
expose_experimental_interfaces: ["Navigator.permissions", "PermissionStatus"],
|
||||
}]));
|
||||
internals.setExperimentalInterfacesExposed(false);
|
||||
|
||||
try {
|
||||
await loadAndReport("matched document", matchedURL);
|
||||
await loadAndReport("unmatched document", unmatchedURL);
|
||||
} finally {
|
||||
internals.setExperimentalInterfacesExposed(true);
|
||||
internals.setSiteCompatibilityData("[]");
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
</script>
|
||||
@@ -30,6 +30,7 @@ set(INTERNAL_RESOURCES
|
||||
list(TRANSFORM INTERNAL_RESOURCES PREPEND "${LADYBIRD_SOURCE_DIR}/Base/res/ladybird/")
|
||||
|
||||
set(SITE_COMPATIBILITY_RESOURCES
|
||||
aljazeera.com.json
|
||||
cnn.com.json
|
||||
nytimes.com.json
|
||||
)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"matches": [
|
||||
"https://aljazeera.com/*",
|
||||
"https://*.aljazeera.com/*"
|
||||
],
|
||||
"expose_experimental_interfaces": [
|
||||
"Navigator.permissions",
|
||||
"WorkerNavigator.permissions",
|
||||
"PermissionStatus"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user