diff --git a/moli-renderer-v8/src/native_bridge/named_access.rs b/moli-renderer-v8/src/native_bridge/named_access.rs index 16f0bc31d1..9265684b7d 100644 --- a/moli-renderer-v8/src/native_bridge/named_access.rs +++ b/moli-renderer-v8/src/native_bridge/named_access.rs @@ -1,5 +1,6 @@ -use crate::document_runtime::DomHandle; -use moli_dom::native::DomHost; +use crate::{document_runtime::DomHandle, util::v8_string}; +use indexmap::IndexSet; +use moli_dom::native::{DomHost, Element}; use super::{ JsContextHost, collections, @@ -55,8 +56,7 @@ pub(crate) fn document_all_named_item_handles(dom: &DomHost, name: &str) -> Vec< pub(crate) fn document_named_item_handles(dom: &DomHost, name: &str) -> Vec { // Document named access deliberately has narrower legacy matching than - // Window or HTMLCollection: form/embed/iframe match by name, object by - // name or id, and img by id only while it also has a non-empty name. + // Window or HTMLCollection. // https://html.spec.whatwg.org/multipage/dom.html#dom-document-nameditem dom.element_handles_by_id_or_name_matching(name, |_| true) .into_iter() @@ -67,22 +67,65 @@ pub(crate) fn document_named_item_handles(dom: &DomHost, name: &str) -> Vec( + dom: &DomHost, + handle: DomHandle, + element: &'a Element, +) -> [Option<&'a str>; 2] { + let name = element.name_attribute().filter(|name| !name.is_empty()); + let id = element.id().filter(|id| !id.is_empty()); + if dom.is_html_element_named(handle, "object") { + return [id, name]; + } + if dom.is_html_element_named(handle, "img") && name.is_some() { + return [id, name]; + } + if ["embed", "form", "iframe"] + .into_iter() + .any(|local_name| dom.is_html_element_named(handle, local_name)) + { + return [None, name]; + } + [None, None] +} + +fn document_supported_property_names(dom: &DomHost, document_handle: DomHandle) -> Vec { + if !dom + .node(document_handle) + .is_some_and(moli_dom::native::Node::is_document) + { + return Vec::new(); + } + + let mut names = IndexSet::new(); + let mut stack = dom + .child_handles_reversed(document_handle) + .collect::>(); + while let Some(handle) = stack.pop() { + if let Some(element) = dom + .node(handle) + .and_then(moli_dom::native::Node::as_element) + { + for name in document_named_element_property_names(dom, handle, element) + .into_iter() + .flatten() + { + names.insert(name.to_owned()); + } + } + stack.extend(dom.child_handles_reversed(handle)); + } + names.into_iter().collect() +} + pub(crate) fn build_window_named_items_collection<'s>( scope: &mut v8::PinScope<'s, '_>, runtime_ptr: *mut JsContextHost, @@ -127,6 +170,18 @@ fn is_document_legacy_unforgeable_property(name: &str) -> bool { name == "location" } +fn document_runtime_and_handle( + scope: &mut v8::PinScope<'_, '_>, + holder: v8::Local<'_, v8::Object>, +) -> Option<(*mut JsContextHost, DomHandle)> { + let (runtime_ptr, document_handle) = node_runtime_and_handle_from_object(scope, holder).ok()?; + unsafe { &*runtime_ptr } + .dom_host() + .node(document_handle) + .is_some_and(moli_dom::native::Node::is_document) + .then_some((runtime_ptr, document_handle)) +} + fn document_named_access_context_for_name( scope: &mut v8::PinScope<'_, '_>, name: String, @@ -135,15 +190,8 @@ fn document_named_access_context_for_name( if name.is_empty() || is_document_legacy_unforgeable_property(&name) { return None; } - let (runtime_ptr, document_handle) = node_runtime_and_handle_from_object(scope, holder).ok()?; + let (runtime_ptr, document_handle) = document_runtime_and_handle(scope, holder)?; let runtime = unsafe { &*runtime_ptr }; - if !runtime - .dom_host() - .node(document_handle) - .is_some_and(moli_dom::native::Node::is_document) - { - return None; - } let handles = document_named_item_handles(runtime.dom_host(), &name); (!handles.is_empty()).then_some((runtime_ptr, document_handle, name, handles)) } @@ -242,18 +290,58 @@ fn document_indexed_property_query<'s>( v8::Intercepted::kYes } +fn document_indexed_property_enumerator( + scope: &mut v8::PinScope<'_, '_>, + args: v8::PropertyCallbackArguments<'_>, + mut rv: v8::ReturnValue<'_, v8::Array>, +) { + let Some((runtime_ptr, document_handle)) = document_runtime_and_handle(scope, args.holder()) + else { + rv.set(v8::Array::new(scope, 0)); + return; + }; + let keys = + document_supported_property_names(unsafe { &*runtime_ptr }.dom_host(), document_handle) + .into_iter() + .filter_map(|name| collections::array_index_property_name(&name)) + .map(|index| v8::Integer::new_from_unsigned(scope, index).into()) + .collect::>(); + rv.set(v8::Array::new_with_elements(scope, &keys)); +} + +fn document_named_property_enumerator( + scope: &mut v8::PinScope<'_, '_>, + args: v8::PropertyCallbackArguments<'_>, + mut rv: v8::ReturnValue<'_, v8::Array>, +) { + let Some((runtime_ptr, document_handle)) = document_runtime_and_handle(scope, args.holder()) + else { + rv.set(v8::Array::new(scope, 0)); + return; + }; + let keys = + document_supported_property_names(unsafe { &*runtime_ptr }.dom_host(), document_handle) + .into_iter() + .filter(|name| collections::array_index_property_name(name).is_none()) + .filter_map(|name| v8_string(scope, &name).map(Into::into)) + .collect::>(); + rv.set(v8::Array::new_with_elements(scope, &keys)); +} + pub(in crate::native_bridge) fn install_document_named_property_handler( template: v8::Local<'_, v8::ObjectTemplate>, ) { template.set_indexed_property_handler( v8::IndexedPropertyHandlerConfiguration::new() .getter(document_indexed_property_getter) - .query(document_indexed_property_query), + .query(document_indexed_property_query) + .enumerator(document_indexed_property_enumerator), ); template.set_named_property_handler( v8::NamedPropertyHandlerConfiguration::new() .getter(document_named_property_getter) .query(document_named_property_query) + .enumerator(document_named_property_enumerator) .flags(v8::PropertyHandlerFlags::ONLY_INTERCEPT_STRINGS), ); } 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 f0ff472bf5..346df1eabe 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 @@ -6737,6 +6737,74 @@ fn live_document_named_properties_follow_html_candidate_and_liveness_rules() { ); } +#[test] +fn live_document_enumerates_supported_property_names_in_document_order() { + let mut vm = new_parsed_test_vm( + "https://example.com/", + r#" + +
+ + + + + + + + +
+ + + "#, + ); + + let result = vm + .eval( + r#" + (() => { + const supported = [ + "embedName", "formName", "frameName", "imageName", + "objectId", "objectName", "nestedObjectId", "nestedObjectName", + "imageId", "imageWithIdName", "duplicateName", "42" + ]; + const ownNames = Object.getOwnPropertyNames(document); + const initial = { + includesEverySupportedName: supported.every(name => ownNames.includes(name)), + nonIndexOrder: ownNames.filter(name => supported.includes(name) && name !== "42"), + duplicateCount: ownNames.filter(name => name === "duplicateName").length, + numericNamePresent: ownNames.includes("42"), + excludedNamesAbsent: [ + "formId", "frameId", "imageIdOnly", "templateId", "templateImage" + ].every(name => !ownNames.includes(name)) + }; + + const image = document.querySelector('[name="imageName"]'); + image.id = "dynamicImageId"; + image.name = "dynamicImageName"; + const changedNames = Object.getOwnPropertyNames(document); + const changed = [ + !changedNames.includes("imageName"), + changedNames.includes("dynamicImageId"), + changedNames.includes("dynamicImageName") + ]; + image.remove(); + const removedNames = Object.getOwnPropertyNames(document); + const removed = [ + !removedNames.includes("dynamicImageId"), + !removedNames.includes("dynamicImageName") + ]; + return JSON.stringify({ initial, changed, removed }); + })() + "#, + ) + .expect("Document supported-property-name enumeration should evaluate"); + + assert_eq!( + result, + r#"{"initial":{"includesEverySupportedName":true,"nonIndexOrder":["embedName","formName","frameName","imageName","objectId","objectName","nestedObjectId","nestedObjectName","imageId","imageWithIdName","duplicateName"],"duplicateCount":1,"numericNamePresent":true,"excludedNamesAbsent":true},"changed":[true,true,true],"removed":[true,true]}"# + ); +} + #[test] fn live_document_named_iframe_singletons_return_child_windows() { let mut vm = new_parsed_test_vm(