mirror of
https://github.com/lexmount/moli.git
synced 2026-09-30 00:01:36 +00:00
fix(webidl): validate DOM receivers with native brands
Enable shared receiver checks for Document structure, CharacterData, Text, ProcessingInstruction and affected HTML element templates. Keep registered native Proxies valid EventTargets while rejecting author-created Proxies. Preserve windowless node identity and UTF-16 data when routing CharacterData through native handles, including Text/CDATA wholeText, live ranges and XPath iterator invalidation. Validate raw reflected/event handler receivers before conversion or mutation, with the LegacyLenientThis mouse event exceptions. Cover cross-realm error constructors, forged and proxy receivers, conversion ordering, native Proxy event dispatch and detached CharacterData mutation.
This commit is contained in:
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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::<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(
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<String> {
|
||||
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<Vec<u16>> {
|
||||
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,
|
||||
|
||||
@@ -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"}"#
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
})()
|
||||
"#,
|
||||
|
||||
@@ -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::<Vec<_>>()
|
||||
.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");
|
||||
|
||||
|
||||
@@ -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/");
|
||||
|
||||
@@ -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::<v8::Object>::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/",
|
||||
"<html><body></body></html>",
|
||||
);
|
||||
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, "[]");
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user