diff --git a/moli-renderer-v8/src/native_bridge/document.rs b/moli-renderer-v8/src/native_bridge/document.rs index fca0cb7409..e819bbb62a 100644 --- a/moli-renderer-v8/src/native_bridge/document.rs +++ b/moli-renderer-v8/src/native_bridge/document.rs @@ -181,8 +181,8 @@ pub(in crate::native_bridge) use detached_objects::{ detached_clone_node_method_callback, detached_doctype_name, detached_doctype_public_id, detached_doctype_system_id, detached_insert_before_method_callback, detached_parent_node_object, detached_processing_instruction_target, - detached_remove_child_method_callback, detached_replace_child_method_callback, - detached_set_owner_document, + detached_record_tree_mutation, detached_remove_child_method_callback, + detached_replace_child_method_callback, detached_set_owner_document, }; pub(in crate::native_bridge) use detached_objects::{ detached_attach_shadow_method_callback, detached_blur_method_callback, @@ -330,7 +330,7 @@ struct DocumentMetadataPrototypeDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::Document, enumerable)] +#[webapi(interface = web_api_interfaces::Document, enumerable, receiver)] struct DocumentStructurePrototypeDeclaration { #[webapi( accessor_property, @@ -657,7 +657,7 @@ fn document_title_setter_function<'s>( ) { let Some((runtime_ptr, handle)) = document_receiver_runtime_and_handle(scope, args.this()) else { - rv.set_undefined(); + throw_type_error(scope, "Illegal invocation"); return; }; let Some(value) = args.get(0).to_string(scope) else { @@ -726,7 +726,7 @@ fn document_head_getter_function<'s>( ) { let receiver = args.this(); let Some((runtime_ptr, handle)) = document_receiver_runtime_and_handle(scope, receiver) else { - rv.set_null(); + throw_type_error(scope, "Illegal invocation"); return; }; let runtime = unsafe { &*runtime_ptr }; diff --git a/moli-renderer-v8/src/native_bridge/document/detached_objects.rs b/moli-renderer-v8/src/native_bridge/document/detached_objects.rs index 44af2f4c3a..0876db9993 100644 --- a/moli-renderer-v8/src/native_bridge/document/detached_objects.rs +++ b/moli-renderer-v8/src/native_bridge/document/detached_objects.rs @@ -49,7 +49,8 @@ pub(crate) use self::state_tree::{ pub(in crate::native_bridge) use self::state_tree::{ define_detached_native_handle, detached_doctype_name, detached_doctype_public_id, detached_doctype_system_id, detached_parent_node_object, - detached_processing_instruction_target, detached_set_owner_document, + detached_processing_instruction_target, detached_record_tree_mutation, + detached_set_owner_document, }; pub(crate) use self::state_tree::{ detached_native_handle_for_runtime, detached_native_object_for_handle, diff --git a/moli-renderer-v8/src/native_bridge/document/detached_objects/state_tree.rs b/moli-renderer-v8/src/native_bridge/document/detached_objects/state_tree.rs index 687334d119..7b59e2151f 100644 --- a/moli-renderer-v8/src/native_bridge/document/detached_objects/state_tree.rs +++ b/moli-renderer-v8/src/native_bridge/document/detached_objects/state_tree.rs @@ -1191,7 +1191,7 @@ pub(in crate::native_bridge::document) fn detached_tree_query_version<'s>( value.uint32_value(scope).map(u64::from) } -pub(in crate::native_bridge::document) fn detached_record_tree_mutation<'s>( +pub(in crate::native_bridge) fn detached_record_tree_mutation<'s>( scope: &mut v8::PinScope<'s, '_>, node: v8::Local<'s, v8::Object>, ) { diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs index 417d80119c..785aa6198a 100644 --- a/moli-renderer-v8/src/native_bridge/element.rs +++ b/moli-renderer-v8/src/native_bridge/element.rs @@ -1546,7 +1546,7 @@ struct HtmlElementStandardPrototypeDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::HTMLElement)] +#[webapi(interface = web_api_interfaces::HTMLElement, receiver)] struct HtmlElementActionPrototypeDeclaration { #[webapi(method, length = 0, enumerable, callback = node_focus_callback)] focus: (), @@ -4136,7 +4136,7 @@ struct HtmlLinkElementUrlPrototypeDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::HTMLStyleElement, enumerable)] +#[webapi(interface = web_api_interfaces::HTMLStyleElement, enumerable, receiver)] struct HtmlStyleElementPrototypeDeclaration { #[webapi( accessor_property, @@ -5139,7 +5139,7 @@ struct HtmlOptionElementLabelPrototypeDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::HTMLTrackElement, enumerable)] +#[webapi(interface = web_api_interfaces::HTMLTrackElement, enumerable, receiver)] struct HtmlTrackElementPrototypeDeclaration { #[webapi( accessor_property = "default", 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 8ca0c6e6d6..828774067e 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 @@ -1,6 +1,9 @@ use crate::{ document_runtime::EventTargetHandle, - util::{context_host_ptr_from_global_bridge, node_wrapper_from_handle, v8_string, v8str}, + util::{ + context_host_ptr_from_global_bridge, node_wrapper_from_handle, throw_type_error, v8_string, + v8str, + }, }; use super::super::super::node::{ @@ -208,14 +211,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 { - rv.set_null(); + handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name); return; }; if !node_is_document(unsafe { &*runtime_ptr }, handle) { - rv.set_null(); + handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name); return; } rv.set(event_handler_property_value_for_target( @@ -231,18 +238,27 @@ fn document_event_handler_setter_function<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'s, v8::Value>, ) { - if let Ok((runtime_ptr, handle)) = + 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()) - && node_is_document(unsafe { &*runtime_ptr }, handle) - { - set_event_handler_property_for_target( - scope, - runtime_ptr, - EventTargetHandle::Node(handle), - args.data(), - args.get(0), - ); + else { + handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name); + return; + }; + if !node_is_document(unsafe { &*runtime_ptr }, handle) { + handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name); + return; } + set_event_handler_property_for_target( + scope, + runtime_ptr, + EventTargetHandle::Node(handle), + args.data(), + args.get(0), + ); rv.set_undefined(); } @@ -258,11 +274,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 { - rv.set_null(); + handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name); return; }; - if !node_is_element(unsafe { &*runtime_ptr }, handle) || !handler_name.starts_with("on") { - rv.set_null(); + if !node_is_element(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) @@ -287,6 +303,10 @@ pub(crate) fn node_event_handler_getter_function<'s>( rv.set(current); return; } + if !handler_name.starts_with("on") { + rv.set_null(); + return; + } let Some(source) = element_attribute(unsafe { &*runtime_ptr }, handle, &handler_name) else { rv.set(v8::null(scope).into()); return; @@ -432,16 +452,22 @@ pub(crate) fn node_event_handler_setter_function<'s>( }; let object = args.this(); let value = args.get(0); + 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_is_element(unsafe { &*runtime_ptr }, handle) { + handle_invalid_event_handler_receiver(scope, &mut rv, &handler_name); + return; + } let stored = if value.is_function() { value } else { v8::null(scope).into() }; let _ = object.set(scope, slot_key.into(), stored); - let runtime_and_handle = node_runtime_and_handle_from_object_or_detached(scope, object).ok(); - 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( @@ -453,7 +479,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") @@ -481,6 +506,22 @@ fn event_handler_event_type(name: &str) -> Option<&str> { .filter(|event_type| !event_type.is_empty()) } +fn legacy_lenient_this_event_handler(name: &str) -> bool { + matches!(name, "onmouseenter" | "onmouseleave") +} + +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(super) fn invalidate_node_event_attribute_handler( runtime: &mut super::super::super::JsContextHost, handle: crate::document_runtime::DomHandle, diff --git a/moli-renderer-v8/src/native_bridge/element/global_attributes.rs b/moli-renderer-v8/src/native_bridge/element/global_attributes.rs index be5c372d21..545dad98e6 100644 --- a/moli-renderer-v8/src/native_bridge/element/global_attributes.rs +++ b/moli-renderer-v8/src/native_bridge/element/global_attributes.rs @@ -2372,13 +2372,8 @@ pub(in crate::native_bridge) fn node_hidden_getter_function<'s>( let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, args.this()) else { - rv.set_undefined(); return; }; - if !node_is_element(unsafe { &*runtime_ptr }, handle) { - rv.set_undefined(); - return; - } match element_attribute(unsafe { &*runtime_ptr }, handle, "hidden") { Some(value) if value.eq_ignore_ascii_case("until-found") => { if let Some(value) = v8_string(scope, "until-found") { @@ -2398,13 +2393,8 @@ pub(in crate::native_bridge) fn node_hidden_setter_function<'s>( let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, args.this()) else { - rv.set_undefined(); return; }; - if !node_is_element(unsafe { &*runtime_ptr }, handle) { - rv.set_undefined(); - return; - } let value = args.get(0); if value.is_null_or_undefined() || value.is_boolean() || value.is_number() { set_reflected_boolean_attribute( diff --git a/moli-renderer-v8/src/native_bridge/element/reflection.rs b/moli-renderer-v8/src/native_bridge/element/reflection.rs index 20f2dfb0d3..fde02a8bc3 100644 --- a/moli-renderer-v8/src/native_bridge/element/reflection.rs +++ b/moli-renderer-v8/src/native_bridge/element/reflection.rs @@ -1171,11 +1171,10 @@ pub(super) fn set_dom_string_attribute_property_on_object<'s>( owner: &'static str, property: &'static str, ) { - let Some(value) = property_dom_string_value(scope, value, owner, property) else { + let Some((runtime_ptr, handle)) = element_reflection_receiver_or_throw(scope, object) else { return; }; - let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, object) - else { + let Some(value) = property_dom_string_value(scope, value, owner, property) else { return; }; set_reflected_attribute(scope, runtime_ptr, handle, name, &value); @@ -1208,15 +1207,9 @@ pub(super) fn attribute_property_getter_from_object_or_detached<'s>( name: &str, mut rv: v8::ReturnValue<'s, v8::Value>, ) { - let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, object) - else { - rv.set_null(); + let Some((runtime_ptr, handle)) = element_reflection_receiver_or_throw(scope, object) else { return; }; - if !node_is_element(unsafe { &*runtime_ptr }, handle) { - rv.set_undefined(); - return; - } let value = element_attribute(unsafe { &*runtime_ptr }, handle, name).unwrap_or_default(); let Some(value) = v8_string(scope, &value) else { rv.set_null(); diff --git a/moli-renderer-v8/src/native_bridge/node/character_data.rs b/moli-renderer-v8/src/native_bridge/node/character_data.rs index 8e2fe55be6..39d18daebb 100644 --- a/moli-renderer-v8/src/native_bridge/node/character_data.rs +++ b/moli-renderer-v8/src/native_bridge/node/character_data.rs @@ -19,5 +19,5 @@ pub(in crate::native_bridge) use helpers::{ }; pub(crate) use prototype::install_character_data_template_bindings; pub(in crate::native_bridge) use text::{ - node_split_text_callback, node_whole_text_value_from_object, + node_split_text_callback, node_whole_text_utf16_units_from_object, }; diff --git a/moli-renderer-v8/src/native_bridge/node/character_data/data.rs b/moli-renderer-v8/src/native_bridge/node/character_data/data.rs index 918417ba3e..8bfb70b37e 100644 --- a/moli-renderer-v8/src/native_bridge/node/character_data/data.rs +++ b/moli-renderer-v8/src/native_bridge/node/character_data/data.rs @@ -1,11 +1,12 @@ use super::helpers::character_data_utf16_units; use super::*; -pub(in crate::native_bridge) fn node_character_data_length_from_object( - scope: &mut v8::PinScope<'_, '_>, - object: v8::Local<'_, v8::Object>, +pub(in crate::native_bridge) fn node_character_data_length_from_object<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, ) -> i32 { - let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object(scope, object) else { + let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, object) + else { return 0; }; character_data_utf16_units(unsafe { &*runtime_ptr }, handle) diff --git a/moli-renderer-v8/src/native_bridge/node/character_data/edits.rs b/moli-renderer-v8/src/native_bridge/node/character_data/edits.rs index 63ffb45203..22f5d68a64 100644 --- a/moli-renderer-v8/src/native_bridge/node/character_data/edits.rs +++ b/moli-renderer-v8/src/native_bridge/node/character_data/edits.rs @@ -1,4 +1,4 @@ -use crate::{context_bootstrap, webidl}; +use crate::{context_bootstrap, native_bridge::document, webidl}; use super::helpers::{ character_data_utf16_units, dom_string_utf16_value_or_throw, set_utf16_return_value, @@ -43,7 +43,8 @@ pub(in crate::native_bridge) fn node_append_data_callback<'s>( if !require_argument_count(scope, &args, "CharacterData", "appendData", 1) { return; } - let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else { + let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args_or_detached(scope, &args) + else { return; }; let Some(mut next) = character_data_utf16_units(unsafe { &*runtime_ptr }, handle) else { @@ -59,7 +60,9 @@ pub(in crate::native_bridge) fn node_append_data_callback<'s>( }; next.extend_from_slice(&data); let runtime = unsafe { &mut *runtime_ptr }; - let _ = runtime.set_character_data_utf16_units_for_edit(scope, runtime_ptr, handle, &next); + if runtime.set_character_data_utf16_units_for_edit(scope, runtime_ptr, handle, &next) { + document::detached_record_tree_mutation(scope, args.this()); + } } pub(in crate::native_bridge) fn node_delete_data_callback<'s>( @@ -71,7 +74,8 @@ pub(in crate::native_bridge) fn node_delete_data_callback<'s>( else { return; }; - let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else { + let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args_or_detached(scope, &args) + else { return; }; let Some(units) = character_data_utf16_units(unsafe { &*runtime_ptr }, handle) else { @@ -86,7 +90,9 @@ pub(in crate::native_bridge) fn node_delete_data_callback<'s>( next.extend_from_slice(&units[..start]); next.extend_from_slice(&units[end..]); let runtime = unsafe { &mut *runtime_ptr }; - let _ = runtime.set_character_data_utf16_units_for_edit(scope, runtime_ptr, handle, &next); + if runtime.set_character_data_utf16_units_for_edit(scope, runtime_ptr, handle, &next) { + document::detached_record_tree_mutation(scope, args.this()); + } context_bootstrap::live_ranges_character_data_edit( scope, handle, @@ -104,7 +110,8 @@ pub(in crate::native_bridge) fn node_insert_data_callback<'s>( if !require_argument_count(scope, &args, "CharacterData", "insertData", 2) { return; } - let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else { + let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args_or_detached(scope, &args) + else { return; }; let Some(units) = character_data_utf16_units(unsafe { &*runtime_ptr }, handle) else { @@ -131,7 +138,9 @@ pub(in crate::native_bridge) fn node_insert_data_callback<'s>( next.extend_from_slice(&insert); next.extend_from_slice(&units[start..]); let runtime = unsafe { &mut *runtime_ptr }; - let _ = runtime.set_character_data_utf16_units_for_edit(scope, runtime_ptr, handle, &next); + if runtime.set_character_data_utf16_units_for_edit(scope, runtime_ptr, handle, &next) { + document::detached_record_tree_mutation(scope, args.this()); + } context_bootstrap::live_ranges_character_data_edit( scope, handle, @@ -149,7 +158,8 @@ pub(in crate::native_bridge) fn node_replace_data_callback<'s>( if !require_argument_count(scope, &args, "CharacterData", "replaceData", 3) { return; } - let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else { + let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args_or_detached(scope, &args) + else { return; }; let Some(units) = character_data_utf16_units(unsafe { &*runtime_ptr }, handle) else { @@ -184,7 +194,9 @@ pub(in crate::native_bridge) fn node_replace_data_callback<'s>( next.extend_from_slice(&replacement); next.extend_from_slice(&units[end..]); let runtime = unsafe { &mut *runtime_ptr }; - let _ = runtime.set_character_data_utf16_units_for_edit(scope, runtime_ptr, handle, &next); + if runtime.set_character_data_utf16_units_for_edit(scope, runtime_ptr, handle, &next) { + document::detached_record_tree_mutation(scope, args.this()); + } context_bootstrap::live_ranges_character_data_edit( scope, handle, @@ -203,7 +215,8 @@ pub(in crate::native_bridge) fn node_substring_data_callback<'s>( else { return; }; - let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else { + let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args_or_detached(scope, &args) + else { return; }; let Some(units) = character_data_utf16_units(unsafe { &*runtime_ptr }, handle) else { diff --git a/moli-renderer-v8/src/native_bridge/node/character_data/prototype.rs b/moli-renderer-v8/src/native_bridge/node/character_data/prototype.rs index 6a893e7292..591edbf1e7 100644 --- a/moli-renderer-v8/src/native_bridge/node/character_data/prototype.rs +++ b/moli-renderer-v8/src/native_bridge/node/character_data/prototype.rs @@ -1,6 +1,6 @@ use crate::web_api_interfaces; use crate::{ - native_bridge::{document, node::node_runtime_and_handle_from_args}, + native_bridge::{document, node::node_runtime_and_handle_from_args_or_detached}, webidl, }; use moli_webapi_declare::WebApiFunctionTemplate; @@ -8,7 +8,7 @@ use moli_webapi_declare::WebApiFunctionTemplate; use super::*; #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::CharacterData, enumerable)] +#[webapi(interface = web_api_interfaces::CharacterData, enumerable, receiver)] struct CharacterDataPrototypeDeclaration { #[webapi( accessor_property, @@ -31,7 +31,7 @@ struct CharacterDataPrototypeDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::Text, enumerable)] +#[webapi(interface = web_api_interfaces::Text, enumerable, receiver)] struct TextPrototypeDeclaration { #[webapi(accessor_property, getter = text_whole_text_getter_callback)] whole_text: (), @@ -40,17 +40,17 @@ struct TextPrototypeDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::ProcessingInstruction, enumerable)] +#[webapi(interface = web_api_interfaces::ProcessingInstruction, enumerable, receiver)] struct ProcessingInstructionPrototypeDeclaration { #[webapi(accessor_property, getter = processing_instruction_target_getter_callback)] target: (), } -fn receiver_is_live_node<'a>( +fn receiver_has_native_node<'a>( scope: &mut v8::PinScope<'a, '_>, args: &v8::FunctionCallbackArguments<'a>, ) -> bool { - node_runtime_and_handle_from_args(scope, args).is_ok() + node_runtime_and_handle_from_args_or_detached(scope, args).is_ok() } fn character_data_append_data_callback<'a>( @@ -58,7 +58,7 @@ fn character_data_append_data_callback<'a>( args: v8::FunctionCallbackArguments<'a>, rv: v8::ReturnValue<'a, v8::Value>, ) { - if receiver_is_live_node(scope, &args) { + if receiver_has_native_node(scope, &args) { node_append_data_callback(scope, args, rv); } else { document::detached_character_data_append_data_callback(scope, args, rv); @@ -70,7 +70,7 @@ fn character_data_delete_data_callback<'a>( args: v8::FunctionCallbackArguments<'a>, rv: v8::ReturnValue<'a, v8::Value>, ) { - if receiver_is_live_node(scope, &args) { + if receiver_has_native_node(scope, &args) { node_delete_data_callback(scope, args, rv); } else { document::detached_character_data_delete_data_callback(scope, args, rv); @@ -82,7 +82,7 @@ fn character_data_insert_data_callback<'a>( args: v8::FunctionCallbackArguments<'a>, rv: v8::ReturnValue<'a, v8::Value>, ) { - if receiver_is_live_node(scope, &args) { + if receiver_has_native_node(scope, &args) { node_insert_data_callback(scope, args, rv); } else { document::detached_character_data_insert_data_callback(scope, args, rv); @@ -94,7 +94,7 @@ fn character_data_replace_data_callback<'a>( args: v8::FunctionCallbackArguments<'a>, rv: v8::ReturnValue<'a, v8::Value>, ) { - if receiver_is_live_node(scope, &args) { + if receiver_has_native_node(scope, &args) { node_replace_data_callback(scope, args, rv); } else { document::detached_character_data_replace_data_callback(scope, args, rv); @@ -106,7 +106,7 @@ fn character_data_substring_data_callback<'a>( args: v8::FunctionCallbackArguments<'a>, rv: v8::ReturnValue<'a, v8::Value>, ) { - if receiver_is_live_node(scope, &args) { + if receiver_has_native_node(scope, &args) { node_substring_data_callback(scope, args, rv); } else { document::detached_character_data_substring_data_callback(scope, args, rv); @@ -118,7 +118,7 @@ fn text_split_text_callback<'a>( args: v8::FunctionCallbackArguments<'a>, rv: v8::ReturnValue<'a, v8::Value>, ) { - if receiver_is_live_node(scope, &args) { + if receiver_has_native_node(scope, &args) { node_split_text_callback(scope, args, rv); } else { document::detached_text_split_text_callback(scope, args, rv); @@ -130,8 +130,9 @@ fn character_data_data_getter_callback<'a>( args: v8::FunctionCallbackArguments<'a>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - if receiver_is_live_node(scope, &args) { - let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object(scope, args.this()) + if receiver_has_native_node(scope, &args) { + let Ok((runtime_ptr, handle)) = + node_runtime_and_handle_from_object_or_detached(scope, args.this()) else { rv.set_undefined(); return; @@ -145,11 +146,7 @@ fn character_data_data_getter_callback<'a>( super::helpers::set_utf16_return_value(scope, &mut rv, &units); return; } - let value = Some(document::detached_character_data_value(scope, args.this())); - let Some(value) = value else { - rv.set_undefined(); - return; - }; + let value = document::detached_character_data_value(scope, args.this()); let Some(value) = v8_string(scope, &value) else { rv.set_null(); return; @@ -162,7 +159,7 @@ fn character_data_data_setter_callback<'a>( args: v8::FunctionCallbackArguments<'a>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - if receiver_is_live_node(scope, &args) { + if receiver_has_native_node(scope, &args) { let Some(value) = super::helpers::dom_string_utf16_value_or_throw( scope, args.get(0), @@ -171,7 +168,8 @@ fn character_data_data_setter_callback<'a>( ) else { return; }; - let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object(scope, args.this()) + let Ok((runtime_ptr, handle)) = + node_runtime_and_handle_from_object_or_detached(scope, args.this()) else { return; }; @@ -181,7 +179,9 @@ fn character_data_data_setter_callback<'a>( .map(|units| units.len() as u32) .unwrap_or(0); let inserted_count = value.len() as u32; - let _ = runtime.set_character_data_utf16_units(scope, runtime_ptr, handle, &value); + if runtime.set_character_data_utf16_units(scope, runtime_ptr, handle, &value) { + document::detached_record_tree_mutation(scope, args.this()); + } crate::context_bootstrap::live_ranges_character_data_reset( scope, handle, @@ -216,7 +216,7 @@ fn character_data_length_getter_callback<'a>( args: v8::FunctionCallbackArguments<'a>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - let length = if receiver_is_live_node(scope, &args) { + let length = if receiver_has_native_node(scope, &args) { node_character_data_length_from_object(scope, args.this()) } else { document::detached_character_data_length(scope, args.this()) @@ -229,12 +229,15 @@ fn text_whole_text_getter_callback<'a>( args: v8::FunctionCallbackArguments<'a>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - let value = if receiver_is_live_node(scope, &args) { - node_whole_text_value_from_object(scope, args.this()) - } else { - document::detached_text_whole_text_value(scope, args.this()) - }; - let Some(value) = value else { + if receiver_has_native_node(scope, &args) { + let Some(units) = node_whole_text_utf16_units_from_object(scope, args.this()) else { + rv.set_undefined(); + return; + }; + super::helpers::set_utf16_return_value(scope, &mut rv, &units); + return; + } + let Some(value) = document::detached_text_whole_text_value(scope, args.this()) else { rv.set_undefined(); return; }; @@ -254,7 +257,8 @@ fn processing_instruction_target_getter_callback<'a>( if let Some(value) = document::detached_processing_instruction_target(scope, args.this()) { value } else { - let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object(scope, args.this()) + let Ok((runtime_ptr, handle)) = + node_runtime_and_handle_from_object_or_detached(scope, args.this()) else { webidl::throw_type_error( scope, diff --git a/moli-renderer-v8/src/native_bridge/node/character_data/text.rs b/moli-renderer-v8/src/native_bridge/node/character_data/text.rs index 97dc849761..58f9a5673b 100644 --- a/moli-renderer-v8/src/native_bridge/node/character_data/text.rs +++ b/moli-renderer-v8/src/native_bridge/node/character_data/text.rs @@ -1,25 +1,30 @@ -use super::helpers::{character_data_string, require_argument_count, utf16_index_value_or_throw}; +use super::helpers::{ + character_data_string, character_data_utf16_units, require_argument_count, + utf16_index_value_or_throw, +}; use super::*; -use crate::{util::utf16_len, webidl}; +use crate::{native_bridge::document, util::utf16_len, webidl}; use moli_dom::native::NodeType; -pub(in crate::native_bridge) fn node_whole_text_value_from_object( - scope: &mut v8::PinScope<'_, '_>, - object: v8::Local<'_, v8::Object>, -) -> Option { - let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object(scope, object) else { +pub(in crate::native_bridge) fn node_whole_text_utf16_units_from_object<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> Option> { + let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, object) + else { return None; }; let runtime = unsafe { &*runtime_ptr }; let dom = runtime.dom_host(); let node = dom.node(handle)?; - if !node.is_text() { + let is_text = |node: &Node| matches!(node.node_type(), NodeType::Text | NodeType::CDataSection); + if !is_text(node) { return None; } - let mut parts = Vec::new(); + let mut units = Vec::new(); let mut start = handle; while let Some(prev_id) = dom.node(start).and_then(|n| n.prev_sibling()) { - if dom.node(prev_id).is_some_and(Node::is_text) { + if dom.node(prev_id).is_some_and(is_text) { start = prev_id; } else { break; @@ -28,15 +33,13 @@ pub(in crate::native_bridge) fn node_whole_text_value_from_object( let mut current = Some(start); while let Some(h) = current { let Some(n) = dom.node(h) else { break }; - if !n.is_text() { + if !is_text(n) { break; } - if let Some(data) = n.node_value() { - parts.push(data.to_owned()); - } + units.extend(character_data_utf16_units(runtime, h)?); current = n.next_sibling(); } - Some(parts.join("")) + Some(units) } pub(in crate::native_bridge) fn node_split_text_callback<'s>( @@ -47,7 +50,8 @@ pub(in crate::native_bridge) fn node_split_text_callback<'s>( if !require_argument_count(scope, &args, "Text", "splitText", 1) { return; } - let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args(scope, &args) else { + let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_args_or_detached(scope, &args) + else { rv.set_undefined(); return; }; @@ -81,6 +85,7 @@ pub(in crate::native_bridge) fn node_split_text_callback<'s>( rv.set_undefined(); return; }; + document::detached_record_tree_mutation(scope, args.this()); set_wrapped_handle_or_null_for_receiver( scope, &mut rv, diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage.rs index 6d0f274485..19c086ab44 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage.rs @@ -598,6 +598,15 @@ fn element_prototype_accessors_are_hookable() { Object.defineProperty(copiedIframe, "src", iframeSrcDescriptor); copiedIframe.src = "/copied.html"; + const outcome = callback => { + try { + callback(); + return "return"; + } catch (error) { + return error.name; + } + }; + return JSON.stringify({ clickGet: typeof clickDescriptor.get, clickSet: typeof clickDescriptor.set, @@ -610,7 +619,7 @@ fn element_prototype_accessors_are_hookable() { divHandler: div.onclick === handler, otherNull: other.onclick === null, formHandler: form.onsubmit === handler, - prototypeNull: HTMLElement.prototype.onsubmit === null, + prototypeError: outcome(() => HTMLElement.prototype.onsubmit), iframePrototypeSrc: HTMLIFrameElement.prototype.src, iframeSrcValue: originalIframeSrcGet.apply(iframe, []), iframeSrcAttribute: iframe.getAttribute("src"), @@ -624,7 +633,7 @@ fn element_prototype_accessors_are_hookable() { assert_eq!( result, - r#"{"clickGet":"function","clickSet":"function","clickConfigurable":true,"submitGet":"function","submitSet":"function","iframeSrcGet":"function","iframeSrcSet":"function","iframeSrcConfigurable":true,"divHandler":true,"otherNull":true,"formHandler":true,"prototypeNull":true,"iframePrototypeSrc":"","iframeSrcValue":"https://element-prototype-accessors.test/child.html","iframeSrcAttribute":"/child.html","copiedIframeSrcValue":"https://element-prototype-accessors.test/copied.html","copiedIframeSrcAttribute":"/copied.html"}"# + r#"{"clickGet":"function","clickSet":"function","clickConfigurable":true,"submitGet":"function","submitSet":"function","iframeSrcGet":"function","iframeSrcSet":"function","iframeSrcConfigurable":true,"divHandler":true,"otherNull":true,"formHandler":true,"prototypeError":"TypeError","iframePrototypeSrc":"","iframeSrcValue":"https://element-prototype-accessors.test/child.html","iframeSrcAttribute":"/child.html","copiedIframeSrcValue":"https://element-prototype-accessors.test/copied.html","copiedIframeSrcAttribute":"/copied.html"}"# ); } 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 0d210808a1..de8e9068b8 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 @@ -3537,7 +3537,7 @@ fn webidl_attribute_setters_preserve_undefined_and_replaceable_semantics() { return JSON.stringify({ animation: [animation.id, outcome(() => animationId.set.call({}))].join(","), document: [outcome(() => bodySetter.call({})), detached.fullscreenEnabled].join(","), - lenient: [outcome(() => mouseEnter.set.call({})), element.onmouseenter].join(","), + eventHandler: [outcome(() => mouseEnter.set.call({})), element.onmouseenter].join(","), replaceableShape, replacements: [window.scrollX, window.screenLeft, window.screenTop].join(","), failures: [selfFailure, screenFailure].join(",") @@ -3549,7 +3549,84 @@ fn webidl_attribute_setters_preserve_undefined_and_replaceable_semantics() { assert_eq!( result, - r#"{"animation":"undefined,TypeError","document":"TypeError,false","lenient":"return,","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 invalidObject = {}; + const element = document.createElement("div"); + const shadow = element.attachShadow({mode: "open"}); + const invalidReceivers = [ + undefined, null, 1, invalidObject, text, shadow, + Object.create(element), new Proxy(element, {}), new Proxy(document, {}) + ]; + const lenientDescriptors = [ + Object.getOwnPropertyDescriptor(HTMLElement.prototype, "onmouseenter"), + Object.getOwnPropertyDescriptor(HTMLElement.prototype, "onmouseleave"), + Object.getOwnPropertyDescriptor(Document.prototype, "onmouseenter"), + Object.getOwnPropertyDescriptor(Document.prototype, "onmouseleave") + ]; + 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 documentStrict = Object.getOwnPropertyDescriptor(Document.prototype, "onclick"); + const outcome = callback => { + try { + callback(); + return "return"; + } catch (error) { + return error && error.name; + } + }; + + let documentCalls = 0; + const handler = () => documentCalls++; + documentStrict.set.call(document, handler); + const documentHandlerPreserved = documentStrict.get.call(document) === handler; + document.dispatchEvent(new Event("click")); + documentStrict.set.call(document, null); + document.dispatchEvent(new Event("click")); + + return [ + lenient, + outcome(() => strict.get.call({})), + outcome(() => strict.set.call({})), + outcome(() => strict.get.call(text)), + outcome(() => strict.set.call(text)), + outcome(() => strict.get.call(shadow)), + outcome(() => strict.set.call(shadow)), + outcome(() => documentStrict.get.call(element)), + outcome(() => documentStrict.set.call(element)), + outcome(() => documentStrict.get.call({})), + outcome(() => documentStrict.set.call({})), + Object.getOwnPropertyNames(invalidObject).length, + documentHandlerPreserved, + documentCalls + ].join("|"); +})() +"#, + ) + .expect("LegacyLenientThis event handler probe should evaluate"); + + assert_eq!( + result, + "true|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|0|true|1" ); } diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs index 2b3302ef9c..cf32cd18b1 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/detached.rs @@ -6643,10 +6643,21 @@ fn detached_global_event_handler_accessors_use_owner_prototypes() { assert(descriptor.configurable === true, `${name} configurable`); return descriptor; }; + const throwsTypeError = callback => { + try { + callback(); + return false; + } catch (error) { + return error instanceof TypeError; + } + }; const click = accessor(HTMLElement.prototype, "onclick"); accessor(HTMLElement.prototype, "onsubmit"); - assert(HTMLElement.prototype.onclick === null, "HTMLElement.prototype.onclick default"); + assert( + throwsTypeError(() => HTMLElement.prototype.onclick), + "HTMLElement.prototype.onclick receiver brand" + ); assert(Object.getOwnPropertyDescriptor(HTMLBodyElement.prototype, "onload").get !== Object.getOwnPropertyDescriptor(HTMLElement.prototype, "onload").get, "HTMLBodyElement.onload should keep body/window override"); @@ -6671,7 +6682,7 @@ fn detached_global_event_handler_accessors_use_owner_prototypes() { assert(element[name] === handler, `${label}.${name} after delete`); } - assert(click.get.call({}) === null, "forged getter is lenient null"); + assert(throwsTypeError(() => click.get.call({})), "forged getter receiver brand"); return "ok"; })() "#, diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document.rs index 3942c57794..c268cd70fc 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/live_document.rs @@ -1,5 +1,88 @@ use super::*; +#[test] +fn cross_realm_dom_bindings_reject_incompatible_receivers_in_their_own_realm() { + let mut vm = new_storage_test_vm("https://cross-realm-dom-receivers.test/"); + + vm.eval( + r#" +(() => { + const root = document.documentElement || + document.appendChild(document.createElement("html")); + const body = document.body || root.appendChild(document.createElement("body")); + const frame = document.createElement("iframe"); + frame.id = "cross-realm-dom-receivers"; + body.appendChild(frame); +})() +"#, + ) + .expect("cross-realm receiver child frame should be created"); + materialize_single_child_default_realm_for_test( + &mut vm, + "cross-realm DOM receiver child Realm", + ); + + let result = vm + .eval( + r#" +(() => { + const other = document.getElementById("cross-realm-dom-receivers").contentWindow; + const notElement = Object.create(other.HTMLElement.prototype); + const notText = Object.create(other.Text.prototype); + const notDocument = Object.create(other.HTMLDocument.prototype); + const element = other.document.createElement("button"); + const text = other.document.createTextNode("foo"); + const outcome = callback => { + try { + callback(); + return "no throw"; + } catch (error) { + return [ + error.name, + error instanceof other.TypeError, + error instanceof TypeError + ].join(":"); + } + }; + + return [ + outcome(() => { Object.create(other.document).head; }), + outcome(() => { + Object.getOwnPropertyDescriptor(other.HTMLElement.prototype, "title") + .get.call(notElement); + }), + outcome(() => { + Reflect.get(other.document.createElement("div"), "hidden", notElement); + }), + outcome(() => { new Proxy(text, {}).nodeType; }), + outcome(() => { Object.create(element).innerHTML = ""; }), + outcome(() => { + Object.getOwnPropertyDescriptor(other.HTMLElement.prototype, "onclick") + .set.call(notElement, null); + }), + outcome(() => { Reflect.set(new other.Text("foo"), "data", "foo", notText); }), + outcome(() => { new Proxy(other.document, {}).title = ""; }), + outcome(() => { Object.create(element).click(); }), + outcome(() => { other.document.querySelector.call(notDocument, "*"); }), + outcome(() => { Reflect.apply(text.remove, notText, []); }), + outcome(() => { + new Proxy(other.document.createElement("a"), {}) + .addEventListener("foo", () => {}); + }) + ].join("|"); +})() +"#, + ) + .expect("cross-realm receiver brand checks should evaluate"); + + assert_eq!( + result, + std::iter::repeat_n("TypeError:true:false", 12) + .collect::>() + .join("|") + ); +} + #[test] fn document_compat_mode_reflects_parser_quirks_mode() { let cases = [ @@ -4163,13 +4246,24 @@ fn global_event_handler_accessors_live_on_owner_prototypes() { assert(descriptor.configurable === true, `${name} configurable`); return descriptor; }; + const throwsTypeError = callback => { + try { + callback(); + return false; + } catch (error) { + return error instanceof TypeError; + } + }; const click = accessor(HTMLElement.prototype, "onclick"); const submit = accessor(HTMLElement.prototype, "onsubmit"); const load = accessor(HTMLElement.prototype, "onload"); - assert(HTMLElement.prototype.onclick === null, "HTMLElement.prototype.onclick default"); - assert(HTMLElement.prototype.onsubmit === null, "HTMLElement.prototype.onsubmit default"); - assert(HTMLElement.prototype.onload === null, "HTMLElement.prototype.onload default"); + assert(throwsTypeError(() => click.get.call(HTMLElement.prototype)), + "HTMLElement.prototype.onclick receiver brand"); + assert(throwsTypeError(() => submit.get.call(HTMLElement.prototype)), + "HTMLElement.prototype.onsubmit receiver brand"); + assert(throwsTypeError(() => load.get.call(HTMLElement.prototype)), + "HTMLElement.prototype.onload receiver brand"); assert(Object.getOwnPropertyDescriptor(HTMLBodyElement.prototype, "onload").get !== load.get, "HTMLBodyElement.onload should keep body/window override"); diff --git a/moli-renderer-v8/src/script_vm/tests/webidl_receivers.rs b/moli-renderer-v8/src/script_vm/tests/webidl_receivers.rs index da8cfff85f..0a20819f4c 100644 --- a/moli-renderer-v8/src/script_vm/tests/webidl_receivers.rs +++ b/moli-renderer-v8/src/script_vm/tests/webidl_receivers.rs @@ -1,5 +1,7 @@ use super::*; +mod dom; + #[test] fn webidl_receiver_checks_reject_prototypes_plain_objects_and_forged_instances() { let mut vm = new_storage_test_vm("https://receiver-check.test/"); diff --git a/moli-renderer-v8/src/script_vm/tests/webidl_receivers/dom.rs b/moli-renderer-v8/src/script_vm/tests/webidl_receivers/dom.rs new file mode 100644 index 0000000000..2153a0a722 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/webidl_receivers/dom.rs @@ -0,0 +1,246 @@ +use super::*; + +#[test] +fn dom_receiver_templates_reject_invalid_interfaces_before_conversion() { + let mut vm = new_storage_test_vm("https://dom-receiver-templates.test/"); + let result = vm.eval(r#" +(() => { + const failures = []; + let conversions = 0; + const value = {toString() { conversions++; return 'changed'; }, valueOf() { conversions++; return 0; }}; + const element = document.createElement('div'); + const text = document.createTextNode('original'); + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + const track = document.createElement('track'); + const style = document.createElement('style'); + const cases = []; + for (const key of ['title', 'head', 'body', 'doctype', 'documentElement']) { + cases.push([Document, key, 'get', document, element, []]); + } + cases.push([Document, 'title', 'set', document, element, [value]]); + for (const key of ['data', 'length']) { + cases.push([CharacterData, key, 'get', text, element, []]); + } + cases.push([CharacterData, 'data', 'set', text, element, [value]]); + for (const key of ['appendData', 'insertData', 'deleteData', 'replaceData', 'substringData']) { + cases.push([CharacterData, key, 'method', text, element, [value, value, value]]); + } + cases.push([Text, 'wholeText', 'get', text, document.createComment('comment'), []]); + cases.push([Text, 'splitText', 'method', text, document.createComment('comment'), [value]]); + for (const key of ['click', 'focus', 'blur']) { + cases.push([HTMLElement, key, 'method', element, svg, []]); + } + for (const [C, key, real] of [[HTMLTrackElement, 'srclang', track], [HTMLStyleElement, 'media', style]]) { + cases.push([C, key, 'get', real, element, []]); + cases.push([C, key, 'set', real, element, [value]]); + } + let traps = 0; + for (const [C, key, kind, real, wrong, args] of cases) { + const descriptor = Object.getOwnPropertyDescriptor(C.prototype, key); + const fn = kind === 'method' ? descriptor.value : descriptor[kind]; + const revoked = Proxy.revocable(real, {}); + revoked.revoke(); + const authorProxy = new Proxy(real, {get() { traps++; throw new Error('proxy trap'); }}); + for (const receiver of [wrong, {}, C.prototype, Object.create(C.prototype), Object.create(real), authorProxy, revoked.proxy, null]) { + try { + Reflect.apply(fn, receiver, args); + failures.push(`${C.name}.${key} ${kind}: accepted`); + } catch (error) { + if (!(error instanceof TypeError)) failures.push(`${C.name}.${key} ${kind}: ${error.name}`); + } + } + } + if (text.data !== 'original') failures.push('mutated real target'); + return JSON.stringify([conversions, traps, failures]); +})() +"#).unwrap(); + assert_eq!(result, "[0,0,[]]"); +} + +#[test] +fn dom_receiver_templates_preserve_cross_realm_native_identity() { + let mut vm = new_storage_test_vm("https://dom-receiver-realms.test/"); + vm.eval( + r#" +const root = document.appendChild(document.createElement('html')); +const body = root.appendChild(document.createElement('body')); +body.appendChild(document.createElement('iframe')).id = 'receiver-frame'; +"#, + ) + .unwrap(); + materialize_single_child_default_realm_for_test(&mut vm, "DOM receiver template child Realm"); + let result = vm.eval(r#" +(() => { + const child = document.getElementById('receiver-frame').contentWindow; + const failures = []; + for (const [callee, owner] of [[window, child], [child, window]]) { + const doc = owner.document.implementation.createHTMLDocument('original'); + const title = Object.getOwnPropertyDescriptor(callee.Document.prototype, 'title'); + const data = Object.getOwnPropertyDescriptor(callee.CharacterData.prototype, 'data'); + const length = Object.getOwnPropertyDescriptor(callee.CharacterData.prototype, 'length').get; + const text = doc.createTextNode('a'); + const button = doc.createElement('button'); + const click = callee.HTMLElement.prototype.click; + const hidden = Object.getOwnPropertyDescriptor(callee.HTMLElement.prototype, 'hidden'); + Object.setPrototypeOf(text, null); + Object.setPrototypeOf(button, null); + Object.setPrototypeOf(doc, null); + title.set.call(doc, 'changed'); + if (title.get.call(doc) !== 'changed') failures.push('Document native identity'); + data.set.call(text, 'a\uD800'); + callee.CharacterData.prototype.appendData.call(text, 'b'); + if (data.get.call(text) !== 'a\uD800b' || length.call(text) !== 3) failures.push('CharacterData native identity'); + hidden.set.call(button, true); + if (hidden.get.call(button) !== true) failures.push('HTMLElement native identity'); + click.call(button); + for (const invoke of [ + () => title.set.call({}, 'bad'), + () => length.call({}), + () => callee.CharacterData.prototype.appendData.call({}, 'bad'), + () => click.call({}) + ]) { + try { invoke(); failures.push('accepted invalid cross-realm receiver'); } + catch (error) { + if (!(error instanceof callee.TypeError) || error instanceof owner.TypeError) failures.push('wrong error realm'); + } + } + } + return JSON.stringify(failures); +})() +"#).unwrap(); + assert_eq!(result, "[]"); +} + +#[test] +fn dom_receiver_templates_allow_registered_native_proxy_event_targets() { + let mut vm = new_storage_test_vm("https://native-proxy-event-target.test/"); + vm.eval( + r#" +globalThis.nativeReceiver = document.implementation.createHTMLDocument('').createElement('select'); +"#, + ) + .unwrap(); + vm.with_default_context_scope_and_checkpoint_for_test(|scope, _host_ptr| { + let global = scope.get_current_context().global(scope); + let key = v8::String::new(scope, "nativeReceiver").unwrap(); + let value = global.get(scope, key.into()).unwrap(); + assert!( + value.is_proxy(), + "fixture must exercise a registered native Proxy" + ); + let object = v8::Local::::try_from(value).unwrap(); + assert!(crate::web_api_interfaces::EventTarget::is_instance( + scope, object + )); + Ok(()) + }) + .unwrap(); + let result = vm.eval(r#" +(() => { + const receiver = nativeReceiver; + const failures = []; + const {addEventListener: add, removeEventListener: remove, dispatchEvent: dispatch} = EventTarget.prototype; + let calls = 0; + const listener = () => calls++; + add.call(receiver, 'test', listener); + if (dispatch.call(receiver, new Event('test')) !== true) failures.push('first dispatch'); + remove.call(receiver, 'test', listener); + if (dispatch.call(receiver, new Event('test')) !== true) failures.push('second dispatch'); + if (calls !== 1) failures.push(`listener calls: ${calls}`); + let conversions = 0; + const type = {toString() { conversions++; return 'test'; }}; + for (const proxy of [new Proxy(receiver, {}), Object.create(receiver)]) { + for (const invoke of [() => add.call(proxy, type, listener), () => remove.call(proxy, type, listener), () => dispatch.call(proxy, new Event('test'))]) { + try { invoke(); failures.push('accepted author receiver'); } + catch (error) { if (!(error instanceof TypeError)) failures.push(error.name); } + } + } + if (conversions !== 0) failures.push('converted author receiver arguments'); + return JSON.stringify(failures); +})() +"#).unwrap(); + assert_eq!(result, "[]"); +} + +#[test] +fn dom_receiver_templates_whole_text_preserves_cdata_and_utf16() { + let mut vm = new_parsed_test_vm( + "https://dom-receiver-whole-text.test/", + "", + ); + let result = vm + .eval( + r#" +(() => { + const failures = []; + const xml = document.implementation.createDocument(null, 'root'); + const get = Object.getOwnPropertyDescriptor(Text.prototype, 'wholeText').get; + const data = Object.getOwnPropertyDescriptor(CharacterData.prototype, 'data'); + for (const doc of [document, document.implementation.createHTMLDocument(''), xml]) { + const parent = doc.createElement('section'); + (doc.body || doc.documentElement).appendChild(parent); + const left = doc.createTextNode('a'); + const middle = doc.adoptNode(xml.createCDATASection('b')); + const right = doc.createTextNode('c'); + parent.append(left, middle, right); + for (const node of [left, middle, right]) { + if (get.call(node) !== 'abc') failures.push('contiguous Text and CDATA'); + } + data.set.call(left, 'a\uD800'); + data.set.call(middle, '\uDC00b'); + data.set.call(right, 'c\uD800'); + for (const node of [left, middle, right]) { + if (get.call(node) !== 'a\uD800\uDC00bc\uD800') failures.push('UTF-16 across nodes'); + } + const barrier = parent.insertBefore(doc.createComment('barrier'), middle); + if (get.call(left) !== 'a\uD800') failures.push('left boundary'); + for (const node of [middle, right]) { + if (get.call(node) !== '\uDC00bc\uD800') failures.push('right boundary'); + } + parent.removeChild(barrier); + if (get.call(middle) !== 'a\uD800\uDC00bc\uD800') failures.push('removed boundary'); + } + return JSON.stringify(failures); +})() +"#, + ) + .unwrap(); + assert_eq!(result, "[]"); +} + +#[test] +fn dom_receiver_templates_character_data_edits_invalidate_detached_xpath_iterators() { + let mut vm = new_storage_test_vm("https://dom-receiver-text-mutations.test/"); + let result = vm.eval(r#" +(() => { + const failures = []; + const iterate = doc => doc.evaluate('//div', doc, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE); + const otherDoc = document.implementation.createHTMLDocument(''); + const other = iterate(otherDoc); + for (const [name, edit, expected] of [ + ['data', text => { text.data = 'after'; }, 'after'], + ['appendData', text => text.appendData('!'), 'before!'], + ['insertData', text => text.insertData(1, '!'), 'b!efore'], + ['deleteData', text => text.deleteData(1, 2), 'bore'], + ['replaceData', text => text.replaceData(1, 2, '!'), 'b!ore'], + ['splitText', text => text.splitText(3), 'before'] + ]) { + const doc = document.implementation.createHTMLDocument(''); + const parent = doc.body.appendChild(doc.createElement('div')); + const text = parent.appendChild(doc.createTextNode('before')); + const iterator = iterate(doc); + const snapshot = doc.evaluate('//div', doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE); + edit(text); + if (!iterator.invalidIteratorState) failures.push(`${name}: iterator remained valid`); + try { iterator.iterateNext(); failures.push(`${name}: iterateNext succeeded`); } + catch (error) { if (error.name !== 'InvalidStateError') failures.push(`${name}: ${error.name}`); } + if (snapshot.invalidIteratorState || snapshot.snapshotItem(0) !== parent) failures.push(`${name}: snapshot changed`); + const value = doc.evaluate('string(//div)', doc, null, XPathResult.STRING_TYPE).stringValue; + if (value !== expected) failures.push(`${name}: stale data ${value}`); + } + if (other.invalidIteratorState) failures.push('invalidated unrelated document'); + return JSON.stringify(failures); +})() +"#).unwrap(); + assert_eq!(result, "[]"); +} diff --git a/moli-renderer-v8/src/window_host.rs b/moli-renderer-v8/src/window_host.rs index 6677906cf8..7b1a65794b 100644 --- a/moli-renderer-v8/src/window_host.rs +++ b/moli-renderer-v8/src/window_host.rs @@ -296,6 +296,7 @@ pub(super) fn event_target_add_event_listener_callback<'s>( event_target_handle_from_this(scope, &args, host_ptr, host) }; let Some(target) = target else { + throw_type_error(scope, "Illegal invocation"); return; }; let passive = call @@ -400,6 +401,7 @@ pub(super) fn event_target_remove_event_listener_callback<'s>( event_target_handle_from_this(scope, &args, host_ptr, host) }; let Some(target) = target else { + throw_type_error(scope, "Illegal invocation"); return; }; host.remove_registered_event_listener(scope, target, &call.event_type, call.callback, capture); @@ -426,7 +428,7 @@ pub(super) fn event_target_dispatch_event_callback<'s>( None }; if child_window_target.is_none() && target.is_none() { - rv.set_bool(false); + throw_type_error(scope, "Illegal invocation"); return; }; let event_value = args.get(0);