From 39a6ecbf8fd2fce3682a76339e3fb617fa1a822b Mon Sep 17 00:00:00 2001 From: ldm0 Date: Mon, 31 Aug 2026 10:44:11 +0800 Subject: [PATCH] fix(webidl): honor lenient event handler receivers --- .../element/event_handlers/generic.rs | 63 ++++++++++++++----- .../src/script_vm/tests/browser_api/misc.rs | 51 ++++++++++++++- 2 files changed, 99 insertions(+), 15 deletions(-) diff --git a/moli-renderer-v8/src/native_bridge/element/event_handlers/generic.rs b/moli-renderer-v8/src/native_bridge/element/event_handlers/generic.rs index 8df830f8d2..8b68582582 100644 --- a/moli-renderer-v8/src/native_bridge/element/event_handlers/generic.rs +++ b/moli-renderer-v8/src/native_bridge/element/event_handlers/generic.rs @@ -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::::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", diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs index 2220ad7a87..a837d87eae 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs @@ -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/");