mirror of
https://github.com/lexmount/moli.git
synced 2026-09-28 08:01:37 +00:00
fix(html): enumerate document named properties
This commit is contained in:
@@ -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<DomHandle> {
|
||||
// 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<DomH
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let name_matches = element.name_attribute() == Some(name);
|
||||
if dom.is_html_element_named(*handle, "img") {
|
||||
return name_matches
|
||||
|| (element.id() == Some(name) && element.name_attribute().is_some());
|
||||
}
|
||||
if dom.is_html_element_named(*handle, "object") {
|
||||
return name_matches || element.id() == Some(name);
|
||||
}
|
||||
name_matches
|
||||
&& ["embed", "form", "iframe"]
|
||||
.into_iter()
|
||||
.any(|local_name| dom.is_html_element_named(*handle, local_name))
|
||||
document_named_element_property_names(dom, *handle, element)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.any(|candidate| candidate == name)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn document_named_element_property_names<'a>(
|
||||
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<String> {
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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#"<!doctype html><html><body>
|
||||
<embed name="embedName">
|
||||
<form id="formId" name="formName"></form>
|
||||
<iframe id="frameId" name="frameName"></iframe>
|
||||
<img name="imageName">
|
||||
<object id="objectId" name="objectName">
|
||||
<object id="nestedObjectId" name="nestedObjectName"></object>
|
||||
</object>
|
||||
<img id="imageId" name="imageWithIdName">
|
||||
<img id="imageIdOnly">
|
||||
<img name="duplicateName">
|
||||
<form name="duplicateName"></form>
|
||||
<img name="42">
|
||||
<template id="templateId"><img name="templateImage"></template>
|
||||
</body></html>"#,
|
||||
);
|
||||
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user