fix(webidl): honor lenient event handler receivers

This commit is contained in:
ldm0
2026-09-08 02:00:32 +08:00
parent 688a626205
commit 39a6ecbf8f
2 changed files with 99 additions and 15 deletions
@@ -278,14 +278,18 @@ fn document_event_handler_getter_function<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let Some(handler_name) = event_handler_name_from_data(scope, args.data()) else {
rv.set_undefined();
return;
};
let Ok((runtime_ptr, handle)) =
node_runtime_and_handle_from_object_or_detached(scope, args.this())
else {
throw_type_error(scope, "Illegal invocation");
handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name);
return;
};
if !node_is_document(unsafe { &*runtime_ptr }, handle) {
throw_type_error(scope, "Illegal invocation");
handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name);
return;
}
rv.set(event_handler_property_value_for_target(
@@ -301,14 +305,18 @@ fn document_event_handler_setter_function<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let Some(handler_name) = event_handler_name_from_data(scope, args.data()) else {
rv.set_undefined();
return;
};
let Ok((runtime_ptr, handle)) =
node_runtime_and_handle_from_object_or_detached(scope, args.this())
else {
throw_type_error(scope, "Illegal invocation");
handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name);
return;
};
if !node_is_document(unsafe { &*runtime_ptr }, handle) {
throw_type_error(scope, "Illegal invocation");
handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name);
return;
}
set_event_handler_property_for_target(
@@ -333,7 +341,11 @@ pub(crate) fn node_event_handler_getter_function<'s>(
let object = args.this();
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, object)
else {
throw_type_error(scope, "Illegal invocation");
handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name);
return;
};
if !node_event_handler_receiver_is_supported(unsafe { &*runtime_ptr }, handle) {
handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name);
return;
};
if let Some(event_type) = event_handler_event_type(&handler_name)
@@ -347,8 +359,7 @@ pub(crate) fn node_event_handler_getter_function<'s>(
rv.set(current);
return;
}
if !node_is_element(unsafe { &*runtime_ptr }, handle)
|| !handler_name.starts_with("on")
if !handler_name.starts_with("on")
|| !is_element_event_handler_content_attribute_name(&handler_name)
{
rv.set_null();
@@ -503,14 +514,16 @@ pub(crate) fn node_event_handler_setter_function<'s>(
};
let object = args.this();
let value = args.get(0);
let runtime_and_handle = node_runtime_and_handle_from_object_or_detached(scope, object).ok();
if runtime_and_handle.is_none() {
throw_type_error(scope, "Illegal invocation");
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, object)
else {
handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name);
return;
};
if !node_event_handler_receiver_is_supported(unsafe { &*runtime_ptr }, handle) {
handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name);
return;
}
if let Some(event_type) = event_handler_event_type(&handler_name)
&& let Some((_runtime_ptr, handle)) = runtime_and_handle
{
if let Some(event_type) = event_handler_event_type(&handler_name) {
let handler = v8::Local::<v8::Function>::try_from(value).ok();
if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) {
unsafe { &mut *host_ptr }.set_registered_event_handler_property(
@@ -522,7 +535,6 @@ pub(crate) fn node_event_handler_setter_function<'s>(
}
}
if matches!(handler_name.as_str(), "onload" | "onerror")
&& let Some((runtime_ptr, handle)) = runtime_and_handle
&& unsafe { &*runtime_ptr }
.dom_host()
.is_html_element_named(handle, "track")
@@ -547,6 +559,29 @@ fn event_handler_event_type(name: &str) -> Option<&str> {
.map(canonical_event_handler_event_type)
}
fn legacy_lenient_this_event_handler(name: &str) -> bool {
matches!(name, "onmouseenter" | "onmouseleave" | "onreadystatechange")
}
fn node_event_handler_receiver_is_supported(
runtime: &super::super::super::JsContextHost,
handle: crate::document_runtime::DomHandle,
) -> bool {
node_is_element(runtime, handle) || runtime.dom_host().is_shadow_root(handle)
}
fn handle_invalid_event_handler_receiver<'s>(
scope: &mut v8::PinScope<'s, '_>,
rv: &mut v8::ReturnValue<'s, v8::Value>,
handler_name: &str,
) {
if legacy_lenient_this_event_handler(handler_name) {
rv.set_undefined();
} else {
throw_type_error(scope, "Illegal invocation");
}
}
pub(crate) fn canonical_event_handler_event_type(event_type: &str) -> &str {
match event_type {
"webkitanimationend" => "webkitAnimationEnd",
@@ -3693,10 +3693,59 @@ fn webidl_attribute_setters_preserve_undefined_and_replaceable_semantics() {
assert_eq!(
result,
r#"{"animation":"undefined,TypeError","document":"TypeError,false","eventHandler":"TypeError,","replaceableShape":true,"replacements":",,foo","failures":"TypeError,TypeError"}"#
r#"{"animation":"undefined,TypeError","document":"TypeError,false","eventHandler":"return,","replaceableShape":true,"replacements":",,foo","failures":"TypeError,TypeError"}"#
);
}
#[test]
fn legacy_lenient_this_event_handlers_ignore_incompatible_receivers() {
let mut vm = new_storage_test_vm("https://legacy-lenient-this.test/");
let result = vm
.eval(
r#"
(() => {
const text = document.createTextNode("text");
const invalidReceivers = [undefined, null, 1, {}, text];
const lenientDescriptors = [
Object.getOwnPropertyDescriptor(HTMLElement.prototype, "onmouseenter"),
Object.getOwnPropertyDescriptor(HTMLElement.prototype, "onmouseleave"),
Object.getOwnPropertyDescriptor(Document.prototype, "onreadystatechange")
];
const lenient = lenientDescriptors.every(descriptor =>
invalidReceivers.every(receiver =>
descriptor.get.call(receiver) === undefined &&
descriptor.set.call(receiver) === undefined &&
descriptor.set.call(receiver, undefined) === undefined &&
descriptor.set.call(receiver, "ignored") === undefined
)
);
const strict = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "onclick");
const outcome = callback => {
try {
callback();
return "return";
} catch (error) {
return error && error.name;
}
};
return [
lenient,
outcome(() => strict.get.call({})),
outcome(() => strict.set.call({})),
outcome(() => strict.get.call(text)),
outcome(() => strict.set.call(text))
].join("|");
})()
"#,
)
.expect("LegacyLenientThis event handler probe should evaluate");
assert_eq!(result, "true|TypeError|TypeError|TypeError|TypeError");
}
#[test]
fn native_bridge_helper_template_preserves_declared_descriptors() {
let mut vm = new_storage_test_vm("https://native-bridge-template.test/");