mirror of
https://github.com/lexmount/moli.git
synced 2026-11-04 08:07:27 +00:00
fix(renderer): honor document contexts when dispatching Node events
Project Node receivers, targets and Events into the listener world using the current native ownerDocument, while preserving adopted wrapper identity. Record inert Document Window provenance independently of execution authority; retired framed documents reject dispatch before click preactivation. Preserve constructor Window sources for DOMParser and Document, including borrowed factories, newTarget prototypes and retained factories after navigation. Use popup owner membership after publishing its Document and release inert provenance without retaining native V8 Context roots. Cover all three layout modes, cache/Realm retirement and fourteen factory variants. Align the initial same-origin popup regression with Chromium's retired Document dispatch behavior while preserving all Window and TreeWalker controls. Validation: cargo fmt --all; full all-feature Clippy; cargo nextest run --no-fail-fast (20,537 passed, 17 existing skipped).
This commit is contained in:
+21
-4
@@ -1170,8 +1170,22 @@ async fn initial_same_origin_popup_commit_preserves_window_and_replaces_document
|
||||
)
|
||||
.await;
|
||||
wait_for_value(&mut child, "document.readyState", json!("complete")).await;
|
||||
let observed = evaluate_window_name_probe(&mut opener, 4,
|
||||
"oldDocument.dispatchEvent(new p.Event('retained'));[p.marker??null,p.serverMarker,p.Array===oldArray,p.document!==oldDocument,oldDocument.URL,oldDocument.getElementById('old').textContent,p.onloadMarker,oldWalker.filter===oldFilter,oldWalker.nextNode().textContent,p.listenerMarker,p.oldDocumentMarker]").await;
|
||||
// Chromium reuses this initial Window, but shuts down the outgoing
|
||||
// Document's execution context. Retaining that Document preserves its
|
||||
// DOM and TreeWalker identity without granting event-dispatch authority.
|
||||
let observed = evaluate_window_name_probe(
|
||||
&mut opener,
|
||||
4,
|
||||
r#"(() => {
|
||||
const event = new p.Event('retained');
|
||||
const dispatched = oldDocument.dispatchEvent(event);
|
||||
return [p.marker??null,p.serverMarker,p.Array===oldArray,p.document!==oldDocument,
|
||||
oldDocument.URL,oldDocument.getElementById('old').textContent,p.onloadMarker,
|
||||
oldWalker.filter===oldFilter,oldWalker.nextNode().textContent,p.listenerMarker,
|
||||
p.oldDocumentMarker??null,dispatched,event.target===null,event.currentTarget===null];
|
||||
})()"#,
|
||||
)
|
||||
.await;
|
||||
navigate_dynamic_page_and_wait_for_load(&mut child, 2, &format!("{base}/replacement")).await;
|
||||
let subsequent = evaluate_window_name_probe(
|
||||
&mut opener,
|
||||
@@ -1193,9 +1207,12 @@ async fn initial_same_origin_popup_commit_preserves_window_and_replaces_document
|
||||
true,
|
||||
"initial",
|
||||
92,
|
||||
93
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]),
|
||||
"first same-origin commit must keep the original Window and intrinsics"
|
||||
"first same-origin commit keeps the Window and intrinsics, while retiring the old Document"
|
||||
);
|
||||
assert_eq!(
|
||||
subsequent,
|
||||
|
||||
@@ -285,8 +285,9 @@ pub(crate) use self::events::{
|
||||
event_initialized, event_internal_bool_flag, event_is_dispatching, event_is_error_event,
|
||||
event_is_mouse_event, event_private_value, initialize_event_object, initialize_event_wrapper,
|
||||
mark_agent_submit_event, mark_event_trusted, new_event_state, new_event_wrapper,
|
||||
set_event_composed_path, set_event_internal_flag, set_event_private_value, set_event_trusted,
|
||||
submit_event_submitter_value, window_event_callback_values,
|
||||
node_event_callback_values, set_event_composed_path, set_event_internal_flag,
|
||||
set_event_private_value, set_event_trusted, submit_event_submitter_value,
|
||||
window_event_callback_values,
|
||||
};
|
||||
pub(crate) use self::file_api::{
|
||||
DataTransferStringCallbackTask, DataTransferStringCallbackTaskEffect,
|
||||
|
||||
@@ -15,9 +15,9 @@ struct CommentConstructorArgs {
|
||||
data: String,
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn document_constructor_callback(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
args: v8::FunctionCallbackArguments<'_>,
|
||||
pub(in crate::context_bootstrap) fn document_constructor_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
if !args.is_construct_call() {
|
||||
@@ -27,8 +27,31 @@ pub(in crate::context_bootstrap) fn document_constructor_callback(
|
||||
);
|
||||
return;
|
||||
}
|
||||
// The native source is the constructor Realm's Document; newTarget may
|
||||
// choose a prototype from an entirely different Window.
|
||||
let context = scope.get_current_context();
|
||||
let Some(host_ptr) = crate::util::context_host_ptr_from_context_slot(context) else {
|
||||
return;
|
||||
};
|
||||
let execution_context = unsafe { &*host_ptr }.current_realm_document_execution_context(scope);
|
||||
match call_global_bridge_method(scope, "__createDetachedDocument", &[]) {
|
||||
Some(value) => rv.set(value),
|
||||
Some(value) => {
|
||||
if let Ok(document) = v8::Local::<v8::Object>::try_from(value)
|
||||
&& let Ok((host, handle)) =
|
||||
crate::native_bridge::node_runtime_and_handle_from_object_or_detached(
|
||||
scope, document,
|
||||
)
|
||||
{
|
||||
unsafe { &mut *host }
|
||||
.set_inert_document_execution_context(handle, execution_context);
|
||||
if let Some(prototype) = args.this().get_prototype(scope)
|
||||
&& document.set_prototype(scope, prototype) != Some(true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
rv.set(value);
|
||||
}
|
||||
None => rv.set_undefined(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,17 +180,39 @@ fn dom_implementation_create_html_document_callback<'s>(
|
||||
return;
|
||||
};
|
||||
let owner_document = dom_implementation_owner_document(scope, args.this());
|
||||
let execution_context = owner_document.and_then(|document| {
|
||||
let (host, handle) =
|
||||
crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, document)
|
||||
.ok()?;
|
||||
unsafe { &*host }.document_execution_context_for_node(handle)
|
||||
});
|
||||
let relevant_context = owner_document
|
||||
.and_then(|document| crate::native_bridge::node_relevant_context(scope, document))
|
||||
.or_else(|| args.this().get_creation_context(scope))
|
||||
.unwrap_or_else(|| scope.get_current_context());
|
||||
let target_scope = &mut v8::ContextScope::new(scope, relevant_context);
|
||||
match create_html_document_value(target_scope, parsed.title.as_deref()) {
|
||||
Some(value) => rv.set(value),
|
||||
Some(value) => {
|
||||
set_created_document_execution_context(target_scope, value, execution_context);
|
||||
rv.set(value);
|
||||
}
|
||||
None => rv.set_undefined(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_created_document_execution_context<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
value: v8::Local<'s, v8::Value>,
|
||||
execution_context: Option<crate::native_bridge::DocumentExecutionContext>,
|
||||
) {
|
||||
if let Ok(document) = v8::Local::<v8::Object>::try_from(value)
|
||||
&& let Ok((host, handle)) =
|
||||
crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, document)
|
||||
{
|
||||
unsafe { &mut *host }.set_inert_document_execution_context(handle, execution_context);
|
||||
}
|
||||
}
|
||||
|
||||
fn create_html_document_value<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
title: Option<&str>,
|
||||
@@ -232,6 +254,12 @@ fn dom_implementation_create_document_callback<'s>(
|
||||
return;
|
||||
};
|
||||
let owner_document = dom_implementation_owner_document(scope, args.this());
|
||||
let execution_context = owner_document.and_then(|document| {
|
||||
let (host, handle) =
|
||||
crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, document)
|
||||
.ok()?;
|
||||
unsafe { &*host }.document_execution_context_for_node(handle)
|
||||
});
|
||||
let relevant_context = owner_document
|
||||
.and_then(|document| crate::native_bridge::node_relevant_context(scope, document))
|
||||
.or_else(|| args.this().get_creation_context(scope))
|
||||
@@ -243,7 +271,10 @@ fn dom_implementation_create_document_callback<'s>(
|
||||
&parsed.qualified_name,
|
||||
parsed.doctype,
|
||||
) {
|
||||
Some(value) => rv.set(value),
|
||||
Some(value) => {
|
||||
set_created_document_execution_context(target_scope, value, execution_context);
|
||||
rv.set(value);
|
||||
}
|
||||
None => rv.set_undefined(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,7 +495,8 @@ pub(super) fn track_event_track_getter_function<'s>(
|
||||
|
||||
pub(crate) use wrappers::{
|
||||
event_attribute, event_bool_attribute, event_private_value, initialize_event_wrapper,
|
||||
new_event_state, new_event_wrapper, set_event_private_value, window_event_callback_values,
|
||||
new_event_state, new_event_wrapper, node_event_callback_values, set_event_private_value,
|
||||
window_event_callback_values,
|
||||
};
|
||||
|
||||
pub(in crate::context_bootstrap) use message::message_event_init_callback;
|
||||
|
||||
@@ -31,8 +31,8 @@ impl AttributeProjection {
|
||||
}
|
||||
"from" => Self::NavigationEntry,
|
||||
"destination" => Self::NavigationDestination,
|
||||
// Arbitrary JS payloads (detail, data, reason, info, error, state)
|
||||
// retain their identity; they are not platform-object projections.
|
||||
// Arbitrary JS payloads have their own value access semantics;
|
||||
// they are separate from platform-object world projection.
|
||||
_ => Self::Value,
|
||||
}
|
||||
}
|
||||
@@ -153,11 +153,9 @@ pub(crate) fn new_event_wrapper<'s>(
|
||||
Some(wrapper)
|
||||
}
|
||||
|
||||
/// Project another Realm of the same live Window through the existing weak
|
||||
/// Event cache. The dispatch state stays shared and same-world identity stays
|
||||
/// intact. Foreign-Window callbacks retain their existing behavior: resolving
|
||||
/// an isolated world of another target Window requires an alias/world resolver
|
||||
/// that this path does not yet provide.
|
||||
/// Use the target Window's member of the listener world. Native dispatch and
|
||||
/// cancellation remain shared; an existing wrapper in that world keeps its
|
||||
/// identity, including when its creation Window differs from the target.
|
||||
pub(crate) fn window_event_callback_values<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
receiver: v8::Local<'s, v8::Value>,
|
||||
@@ -175,8 +173,42 @@ pub(crate) fn window_event_callback_values<'s>(
|
||||
// Blink wraps native events in the target Window's Context in the
|
||||
// listener's world, while invoking the callback in its relevant Realm.
|
||||
let context = target.get_creation_context(scope)?;
|
||||
Some((target.into(), event_in_realm(scope, event, context)?))
|
||||
}
|
||||
|
||||
pub(crate) fn node_event_callback_values<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
receiver: v8::Local<'s, v8::Value>,
|
||||
event: v8::Local<'s, v8::Object>,
|
||||
target_context: v8::Local<'s, v8::Context>,
|
||||
listener_context: v8::Local<'s, v8::Context>,
|
||||
) -> Option<(v8::Local<'s, v8::Value>, v8::Local<'s, v8::Object>)> {
|
||||
let context = match crate::native_bridge::window_context_in_wrapper_world(
|
||||
scope,
|
||||
target_context,
|
||||
listener_context,
|
||||
) {
|
||||
Ok(context) => context?,
|
||||
Err(error) => {
|
||||
super::super::throw_error(
|
||||
scope,
|
||||
&format!("Node event world projection failed: {error}"),
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let target = v8::Local::<v8::Object>::try_from(receiver).ok()?;
|
||||
let receiver = super::super::shared_event_targets::target_in_realm(scope, target, context);
|
||||
Some((receiver.into(), event_in_realm(scope, event, context)?))
|
||||
}
|
||||
|
||||
fn event_in_realm<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
event: v8::Local<'s, v8::Object>,
|
||||
context: v8::Local<'s, v8::Context>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
if world_wrappers::belongs_to_world(scope, event, context) {
|
||||
return Some((target.into(), event));
|
||||
return Some(event);
|
||||
}
|
||||
let backing = event_backing(scope, event);
|
||||
let wrapper = if let Some(wrapper) = world_wrappers::get(scope, backing, context) {
|
||||
@@ -185,7 +217,7 @@ pub(crate) fn window_event_callback_values<'s>(
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
new_event_wrapper(scope, backing)?
|
||||
};
|
||||
Some((target.into(), wrapper))
|
||||
Some(wrapper)
|
||||
}
|
||||
|
||||
fn bind_attribute<'s>(
|
||||
|
||||
@@ -36,10 +36,10 @@ pub(super) fn in_realm<'s>(
|
||||
let (host_ptr, handle) =
|
||||
crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, object)
|
||||
.ok()?;
|
||||
return unsafe { &mut *host_ptr }
|
||||
.native_bridge_mut()
|
||||
.wrap_handle(scope, host_ptr, handle)
|
||||
.map(Into::into);
|
||||
return crate::native_bridge::document::detached_native_object_for_handle(
|
||||
scope, host_ptr, handle,
|
||||
)
|
||||
.map(Into::into);
|
||||
} else if web_api_interfaces::Blob::is_instance(scope, object) {
|
||||
let wrapper = crate::blob::world_wrapper(scope, object)?;
|
||||
if web_api_interfaces::File::is_instance(scope, object) {
|
||||
|
||||
@@ -63,6 +63,11 @@ pub(super) fn target_in_realm<'s>(
|
||||
if let Some(window) = window_target_in_realm(scope, target, context) {
|
||||
return window;
|
||||
}
|
||||
if crate::web_api_interfaces::Node::is_instance(scope, target) {
|
||||
return super::platform_object_worlds::in_realm(scope, target.into(), context)
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
|
||||
.unwrap_or(target);
|
||||
}
|
||||
let Some(owner) = get_private_object(scope, target, OWNER) else {
|
||||
return target;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ use super::{
|
||||
};
|
||||
|
||||
pub(crate) const DOM_PARSER_FOREIGN_NODE_SLOT: &str = "__moliDomParserForeignNode";
|
||||
const DOM_PARSER_CONSTRUCTION_REALM_SLOT: &str = "__moliDomParserConstructionRealm";
|
||||
const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml";
|
||||
const PARSER_ERROR_STYLE: &str = "display: block; white-space: pre; border: 2px solid #c77; padding: 0 1em 0 1em; margin: 1em; background-color: #fdd; color: black";
|
||||
const PARSER_ERROR_DETAIL_STYLE: &str = "font-family:monospace;font-size:12px";
|
||||
@@ -118,6 +119,16 @@ pub(super) fn dom_parser_constructor_callback(
|
||||
throw_type_error(scope, "DOMParser constructor must be called with new");
|
||||
return;
|
||||
}
|
||||
// The constructor's Window is independent of newTarget.prototype. Keep
|
||||
// a V8-traced creation-Realm witness, not a native persistent Context root
|
||||
// or a WindowProxy that can be rebound to another Window on navigation.
|
||||
let realm = crate::util::new_null_prototype_object(scope);
|
||||
crate::util::set_private_value(
|
||||
scope,
|
||||
args.this(),
|
||||
DOM_PARSER_CONSTRUCTION_REALM_SLOT,
|
||||
realm.into(),
|
||||
);
|
||||
rv.set(args.this().into());
|
||||
}
|
||||
|
||||
@@ -150,11 +161,30 @@ pub(super) fn dom_parser_parse_from_string_callback<'s>(
|
||||
source
|
||||
}
|
||||
};
|
||||
// Borrowing parseFromString from another Realm does not change the
|
||||
// parser's associated Window or the returned Document's wrapper Realm.
|
||||
let Some(context) = get_private_object(scope, args.this(), DOM_PARSER_CONSTRUCTION_REALM_SLOT)
|
||||
.and_then(|realm| realm.get_creation_context(scope))
|
||||
else {
|
||||
throw_type_error(scope, "Illegal invocation");
|
||||
return;
|
||||
};
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
|
||||
return;
|
||||
};
|
||||
let execution_context = unsafe { &*host_ptr }.current_realm_window_for_inert_document(scope);
|
||||
let Some(obj) = parse_detached_document_from_string(scope, &source, parsed.mime.as_mime())
|
||||
else {
|
||||
rv.set(v8::null(scope).into());
|
||||
return;
|
||||
};
|
||||
if let Ok((runtime_ptr, handle)) =
|
||||
crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, obj)
|
||||
{
|
||||
unsafe { &mut *runtime_ptr }
|
||||
.set_inert_document_execution_context(handle, execution_context);
|
||||
}
|
||||
rv.set(obj.into());
|
||||
}
|
||||
|
||||
|
||||
@@ -156,21 +156,29 @@ pub(crate) fn invoke_prepared_event_callback<'s>(
|
||||
) -> Option<v8::Global<v8::Value>> {
|
||||
let receiver = event_target_receiver(scope, host_ptr, target, event);
|
||||
let has_event_argument = arguments.len() == 1 && arguments[0].strict_equals(event.into());
|
||||
let (receiver, event) = if matches!(
|
||||
target,
|
||||
EventTargetHandle::Window | EventTargetHandle::ChildWindow(_)
|
||||
) {
|
||||
let (receiver, event) = {
|
||||
let context = callback.relevant_context(scope);
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let catch = std::pin::pin!(v8::TryCatch::new(scope));
|
||||
let scope = &mut catch.init();
|
||||
if let Ok(window) = v8::Local::<v8::Object>::try_from(receiver) {
|
||||
// The current native ownerDocument supplies a Node's execution
|
||||
// context. Its wrapper can have been created in another Window or
|
||||
// adopted there, and an inert Document need not have a frame.
|
||||
let node_context = if let EventTargetHandle::Node(node) = target {
|
||||
Some(unsafe { &*host_ptr }.node_event_execution_context(scope, node)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let window = node_context
|
||||
.map(|context| context.global(scope))
|
||||
.or_else(|| v8::Local::<v8::Object>::try_from(receiver).ok());
|
||||
if let Some(window) = window {
|
||||
match crate::native_bridge::window_event_access(scope, window, context) {
|
||||
crate::native_bridge::WindowEventAccess::Retired => return None,
|
||||
crate::native_bridge::WindowEventAccess::Denied => {
|
||||
let exception = crate::context_bootstrap::new_dom_exception_value(
|
||||
scope,
|
||||
"Blocked a Window event listener from accessing a cross-origin target.",
|
||||
"Blocked an event listener from accessing a cross-origin target.",
|
||||
"SecurityError",
|
||||
);
|
||||
return Some(v8::Global::new(scope, exception));
|
||||
@@ -179,14 +187,21 @@ pub(crate) fn invoke_prepared_event_callback<'s>(
|
||||
| crate::native_bridge::WindowEventAccess::Unmanaged => {}
|
||||
}
|
||||
}
|
||||
let projection =
|
||||
crate::context_bootstrap::window_event_callback_values(scope, receiver, event, context);
|
||||
let projection = if let Some(node_context) = node_context {
|
||||
crate::context_bootstrap::node_event_callback_values(
|
||||
scope,
|
||||
receiver,
|
||||
event,
|
||||
node_context,
|
||||
context,
|
||||
)
|
||||
} else {
|
||||
crate::context_bootstrap::window_event_callback_values(scope, receiver, event, context)
|
||||
};
|
||||
if let Some(exception) = scope.exception() {
|
||||
return Some(v8::Global::new(scope, exception));
|
||||
}
|
||||
projection?
|
||||
} else {
|
||||
(receiver, event)
|
||||
};
|
||||
// Window.onerror's five arguments are JS values, not an Event conversion.
|
||||
let event_arguments = [event.into()];
|
||||
@@ -640,11 +655,12 @@ fn event_listener_error_event_from_report<'s>(
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub(crate) struct PublicEventDispatchResult {
|
||||
pub default_prevented: bool,
|
||||
canceled_before_dispatch: bool,
|
||||
}
|
||||
|
||||
impl PublicEventDispatchResult {
|
||||
pub(crate) fn allows_default(self) -> bool {
|
||||
!self.default_prevented
|
||||
!self.default_prevented && !self.canceled_before_dispatch
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_event_return_value(self) -> bool {
|
||||
@@ -1299,6 +1315,16 @@ pub(crate) fn dispatch_public_event_with_original_target<'s, 'i>(
|
||||
event: v8::Local<'s, v8::Object>,
|
||||
) -> std::result::Result<PublicEventDispatchResult, String> {
|
||||
unsafe { &*host_ptr }.debug_assert_not_in_structural_mutation("public event dispatch");
|
||||
if let EventTargetHandle::Node(node) = dispatch_target
|
||||
&& unsafe { &*host_ptr }
|
||||
.document_execution_context_for_node(node)
|
||||
.is_none()
|
||||
{
|
||||
return Ok(PublicEventDispatchResult {
|
||||
canceled_before_dispatch: true,
|
||||
default_prevented: event_default_prevented(scope, event),
|
||||
});
|
||||
}
|
||||
let target_object = event_target_object(scope, host_ptr, dispatch_target)?;
|
||||
let event_type = normalize_public_event_record(scope, event, target_object.into())?;
|
||||
|
||||
@@ -1690,6 +1716,7 @@ pub(crate) fn dispatch_public_event_with_original_target<'s, 'i>(
|
||||
clear_event_composed_path(scope, event);
|
||||
Ok(PublicEventDispatchResult {
|
||||
default_prevented: event_default_prevented(scope, event),
|
||||
canceled_before_dispatch: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -382,6 +382,7 @@ impl JsContextHost {
|
||||
text_codecs: TextCodecStore::default(),
|
||||
child_browsing_contexts: IndexMap::new(),
|
||||
frame_owner_store,
|
||||
inert_document_execution_contexts: None,
|
||||
frame_parser_classic_scripts: FrameParserClassicScriptRunnerStore::default(),
|
||||
frame_parser_deferred_script_order: FrameParserDeferredScriptOrderStore::default(),
|
||||
frame_document_blocking_stylesheets: FrameDocumentBlockingStylesheetStore::default(),
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
//! A Document's native Window association is distinct from a Node wrapper's
|
||||
//! creation Realm and from permission to execute a callback in that Window.
|
||||
|
||||
use super::{
|
||||
DomHandle, JsContextHost, OwnerDispatchScope, WindowEnvironmentSettings,
|
||||
WindowExecutionContextIdentity, WindowExecutionContextOwner,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Allocate the Document provenance ledger only when an inert factory is used;
|
||||
/// ordinary Hosts retain a single optional pointer, not a resident map header.
|
||||
#[derive(Default)]
|
||||
pub(super) struct InertDocumentExecutionContexts {
|
||||
documents: HashMap<DomHandle, Option<DocumentExecutionContext>>,
|
||||
}
|
||||
|
||||
/// Scalar provenance only. Live execution remains authorized by the existing
|
||||
/// Window/Realm registry; this record never keeps a V8 Context or Host alive.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct DocumentExecutionContext {
|
||||
owner: WindowExecutionContextOwner,
|
||||
dispatch_scope: OwnerDispatchScope,
|
||||
}
|
||||
|
||||
impl From<WindowExecutionContextIdentity> for DocumentExecutionContext {
|
||||
fn from(identity: WindowExecutionContextIdentity) -> Self {
|
||||
Self {
|
||||
owner: identity.owner(),
|
||||
dispatch_scope: identity.dispatch_scope(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JsContextHost {
|
||||
pub(crate) fn document_execution_context_for_node(
|
||||
&self,
|
||||
node: DomHandle,
|
||||
) -> Option<DocumentExecutionContext> {
|
||||
let document = self.dom_host().owner_document_handle(node)?;
|
||||
if let Some(source) = self
|
||||
.inert_document_execution_contexts
|
||||
.as_ref()
|
||||
.and_then(|sources| sources.documents.get(&document))
|
||||
{
|
||||
// Inert documents retain the association even after the Window
|
||||
// is destroyed. The original framed Document instead loses its
|
||||
// execution context when it leaves the current frame membership.
|
||||
return *source;
|
||||
}
|
||||
if self.page_context_resources_closed {
|
||||
return None;
|
||||
}
|
||||
let dispatch_scope = self.owner_dispatch_scope_for_node(document)?;
|
||||
Some(DocumentExecutionContext {
|
||||
owner: self.current_window_execution_context_owner(dispatch_scope)?,
|
||||
dispatch_scope,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn current_realm_document_execution_context(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
) -> Option<DocumentExecutionContext> {
|
||||
self.current_runtime_window_execution_context_identity(scope)
|
||||
.map(Into::into)
|
||||
.or_else(|| {
|
||||
WindowEnvironmentSettings::for_current_realm(scope).and_then(|settings| {
|
||||
self.document_execution_context_for_node(settings.document_handle())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// DOMParser owns a Window, rather than borrowing the current Document's
|
||||
/// execution context. A retained parser can still construct inert DOM
|
||||
/// after that Window's framed Document has shut down.
|
||||
pub(crate) fn current_realm_window_for_inert_document(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
) -> Option<DocumentExecutionContext> {
|
||||
self.current_runtime_window_execution_context_identity(scope)
|
||||
.map(Into::into)
|
||||
.or_else(|| {
|
||||
WindowEnvironmentSettings::for_current_realm(scope)
|
||||
.map(|settings| settings.execution_context_identity().into())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn set_inert_document_execution_context(
|
||||
&mut self,
|
||||
document: DomHandle,
|
||||
source: Option<DocumentExecutionContext>,
|
||||
) {
|
||||
debug_assert!(
|
||||
self.dom_host()
|
||||
.node(document)
|
||||
.is_some_and(|node| node.is_document())
|
||||
);
|
||||
self.inert_document_execution_contexts
|
||||
.get_or_insert_with(Default::default)
|
||||
.documents
|
||||
.insert(document, source);
|
||||
}
|
||||
|
||||
pub(super) fn forget_inert_document_execution_context(&mut self, document: DomHandle) {
|
||||
if let Some(sources) = self.inert_document_execution_contexts.as_mut() {
|
||||
sources.documents.remove(&document);
|
||||
if sources.documents.is_empty() {
|
||||
self.inert_document_execution_contexts = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn node_event_execution_context<'s>(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
node: DomHandle,
|
||||
) -> Option<v8::Local<'s, v8::Context>> {
|
||||
let source = self.document_execution_context_for_node(node)?;
|
||||
if !self.window_execution_context_owner_is_current(source.owner, source.dispatch_scope) {
|
||||
return None;
|
||||
}
|
||||
let (_, context) =
|
||||
self.window_execution_context(scope, source.owner, source.dispatch_scope)?;
|
||||
let identity = self.window_execution_context_identity_for_access_check(context)?;
|
||||
self.window_execution_context_identity_is_current(identity)
|
||||
.then_some(context)
|
||||
}
|
||||
}
|
||||
@@ -156,6 +156,7 @@ mod security_policy;
|
||||
mod selection_records;
|
||||
mod service_workers;
|
||||
pub(crate) use service_workers::ServiceWorkerWindowOwner;
|
||||
mod document_execution_context;
|
||||
mod shared_workers;
|
||||
mod signal_bridge;
|
||||
mod storage_events;
|
||||
@@ -199,6 +200,7 @@ pub(crate) use child_frame_snapshots::{
|
||||
};
|
||||
use child_frames::ChildBrowsingContextEntry;
|
||||
pub(in crate::native_bridge::context_host) use child_frames::ChildParserClassicScriptCandidate;
|
||||
pub(crate) use document_execution_context::DocumentExecutionContext;
|
||||
pub(crate) use event_callbacks::{EventCallbackId, PreparedEventCallback};
|
||||
pub(crate) use host_loads::ChildFrameAttachmentSnapshot;
|
||||
#[cfg(test)]
|
||||
@@ -249,8 +251,8 @@ pub(crate) use window_world::install_isolated_window_world_for_context;
|
||||
pub(crate) use window_world::{
|
||||
IsolatedWindowWorld, IsolatedWindowWorldId, PrebootstrappedIsolatedWindowContext,
|
||||
PrebootstrappedWindowContexts, WindowEventAccess, install_isolated_window_world_definition,
|
||||
isolated_window_world_for_context, remember_isolated_window_context, window_event_access,
|
||||
window_in_wrapper_world,
|
||||
isolated_window_world_for_context, remember_isolated_window_context,
|
||||
window_context_in_wrapper_world, window_event_access, window_in_wrapper_world,
|
||||
};
|
||||
use workers::WorkerConnectionState;
|
||||
pub(crate) use workers::WorkerOwnerScope;
|
||||
@@ -877,6 +879,8 @@ pub(crate) struct JsContextHost {
|
||||
// Key order is browsing-context insertion order and therefore frame-tree sibling order.
|
||||
child_browsing_contexts: IndexMap<DomHandle, ChildBrowsingContextEntry>,
|
||||
frame_owner_store: FrameOwnerStore,
|
||||
inert_document_execution_contexts:
|
||||
Option<Box<document_execution_context::InertDocumentExecutionContexts>>,
|
||||
frame_parser_classic_scripts: FrameParserClassicScriptRunnerStore,
|
||||
frame_parser_deferred_script_order: FrameParserDeferredScriptOrderStore,
|
||||
frame_document_blocking_stylesheets: FrameDocumentBlockingStylesheetStore,
|
||||
|
||||
@@ -2834,6 +2834,10 @@ impl JsContextHost {
|
||||
scope, document,
|
||||
)
|
||||
{
|
||||
// This projection now represents a framed Window Document. Its
|
||||
// lifetime follows the popup owner, rather than its parser's
|
||||
// former inert-document association with the opener.
|
||||
self.forget_inert_document_execution_context(document_handle);
|
||||
if self.lightweight_popup_document_handle(popup_id) != Some(document_handle) {
|
||||
self.forget_lightweight_popup_document_handle(popup_id);
|
||||
}
|
||||
|
||||
@@ -347,15 +347,15 @@ impl JsContextHost {
|
||||
&self,
|
||||
handle: DomHandle,
|
||||
) -> Option<OwnerDispatchScope> {
|
||||
if let Some(popup_id) = self.lightweight_popup_id_for_node_owner_document(handle) {
|
||||
return Some(OwnerDispatchScope::LightweightPopup(popup_id));
|
||||
}
|
||||
let node = self.dom_host().node(handle)?;
|
||||
let owner_document = if node.is_document() {
|
||||
handle
|
||||
} else {
|
||||
node.owner_document()?
|
||||
};
|
||||
if let Some(popup_id) = self.lightweight_popup_id_for_document_handle(owner_document) {
|
||||
return Some(OwnerDispatchScope::LightweightPopup(popup_id));
|
||||
}
|
||||
if owner_document == self.document_handle() {
|
||||
return Some(OwnerDispatchScope::Top);
|
||||
}
|
||||
|
||||
@@ -132,6 +132,12 @@ impl WindowEnvironmentSettings {
|
||||
self.document
|
||||
}
|
||||
|
||||
/// Metadata for native inert Document construction, never execution
|
||||
/// authority. Callers must independently authorize any callback/task.
|
||||
pub(crate) fn execution_context_identity(&self) -> WindowExecutionContextIdentity {
|
||||
self.execution_identity
|
||||
}
|
||||
|
||||
pub(crate) fn api_base_url(&self, host: &JsContextHost) -> Option<Url> {
|
||||
let document = host.dom_host().node(self.document)?.as_document()?;
|
||||
// Preserve the existing bootstrap view while an initial empty Document
|
||||
|
||||
@@ -63,11 +63,14 @@ fn create_native_detached_document_handle_with_url(
|
||||
) -> Option<DomHandle> {
|
||||
let runtime_ptr = context_host_ptr_from_global_bridge(scope)?;
|
||||
let runtime = unsafe { &mut *runtime_ptr };
|
||||
Some(if kind == "html" {
|
||||
let source = runtime.current_realm_document_execution_context(scope);
|
||||
let handle = if kind == "html" {
|
||||
runtime.create_detached_html_document_with_url(url)
|
||||
} else {
|
||||
runtime.create_detached_xml_document_with_url(url)
|
||||
})
|
||||
};
|
||||
runtime.set_inert_document_execution_context(handle, source);
|
||||
Some(handle)
|
||||
}
|
||||
|
||||
fn detached_document_url(parsed: &DomHost) -> Url {
|
||||
|
||||
@@ -47,6 +47,11 @@ fn clone_detached_document_shell<'s>(
|
||||
document: v8::Local<'s, v8::Object>,
|
||||
deep: bool,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
let (runtime_ptr, source_handle) =
|
||||
crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, document)
|
||||
.ok()?;
|
||||
let execution_context =
|
||||
unsafe { &*runtime_ptr }.document_execution_context_for_node(source_handle)?;
|
||||
let document_kind = detached_state_string(scope, document, "documentKind");
|
||||
let root_is_html = detached_document_root_local_name(scope, document)
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case("html"));
|
||||
@@ -64,6 +69,9 @@ fn clone_detached_document_shell<'s>(
|
||||
};
|
||||
let cloned = call_global_bridge_method(scope, helper, &[])
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())?;
|
||||
let cloned_handle = detached_native_handle_for_runtime(scope, runtime_ptr, cloned)?;
|
||||
unsafe { &mut *runtime_ptr }
|
||||
.set_inert_document_execution_context(cloned_handle, Some(execution_context));
|
||||
let content_type = detached_document_content_type_value(scope, document)?;
|
||||
set_detached_document_content_type(scope, cloned, &content_type)?;
|
||||
if html_shell {
|
||||
|
||||
@@ -54,17 +54,18 @@ pub(crate) use collections::{
|
||||
build_static_handle_node_list_wrapper, install_collection_template_bindings,
|
||||
};
|
||||
pub(crate) use context_host::{
|
||||
DetachedChildBrowsingContextDocumentSnapshot, ImageDecodeRequestId, IsolatedWindowWorld,
|
||||
RuntimeObservableContextToken, WindowEventAccess, cross_origin_lightweight_popup_id,
|
||||
current_runtime_observable_context_token, defer_active_lightweight_popup_restore,
|
||||
enter_active_lightweight_popup_scope, enter_top_level_lightweight_popup_scope,
|
||||
install_child_window_proxy_access_check_handlers, install_isolated_window_world_definition,
|
||||
install_runtime_observable_context_token_for_context, is_cross_origin_top_window_proxy,
|
||||
isolated_window_world_for_context, lightweight_popup_id_from_window,
|
||||
remember_isolated_window_context, restore_active_lightweight_popup_scope,
|
||||
DetachedChildBrowsingContextDocumentSnapshot, DocumentExecutionContext, ImageDecodeRequestId,
|
||||
IsolatedWindowWorld, RuntimeObservableContextToken, WindowEventAccess,
|
||||
cross_origin_lightweight_popup_id, current_runtime_observable_context_token,
|
||||
defer_active_lightweight_popup_restore, enter_active_lightweight_popup_scope,
|
||||
enter_top_level_lightweight_popup_scope, install_child_window_proxy_access_check_handlers,
|
||||
install_isolated_window_world_definition, install_runtime_observable_context_token_for_context,
|
||||
is_cross_origin_top_window_proxy, isolated_window_world_for_context,
|
||||
lightweight_popup_id_from_window, remember_isolated_window_context,
|
||||
restore_active_lightweight_popup_scope,
|
||||
restore_deferred_active_lightweight_popup_scope_if_present,
|
||||
throw_cross_origin_location_security_error, throw_cross_origin_type_error, window_event_access,
|
||||
window_in_wrapper_world,
|
||||
throw_cross_origin_location_security_error, throw_cross_origin_type_error,
|
||||
window_context_in_wrapper_world, window_event_access, window_in_wrapper_world,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use context_host::{IsolatedWindowWorldId, install_isolated_window_world_for_context};
|
||||
|
||||
@@ -36,9 +36,21 @@ pub(in crate::native_bridge) fn node_clone_node_callback<'s>(
|
||||
}
|
||||
let deep = args.get(0).boolean_value(scope);
|
||||
let runtime = unsafe { &mut *runtime_ptr };
|
||||
let is_document = runtime
|
||||
.dom_host()
|
||||
.node(handle)
|
||||
.is_some_and(|node| node.is_document());
|
||||
let execution_context = runtime.document_execution_context_for_node(handle);
|
||||
if is_document && execution_context.is_none() {
|
||||
rv.set_null();
|
||||
return;
|
||||
}
|
||||
let Some(clone) = runtime.clone_node(scope, runtime_ptr, handle, deep) else {
|
||||
throw_dom_exception(scope, "NotSupportedError", 9, "Not supported");
|
||||
return;
|
||||
};
|
||||
if is_document {
|
||||
runtime.set_inert_document_execution_context(clone, execution_context);
|
||||
}
|
||||
set_wrapped_handle_or_null_for_receiver(scope, &mut rv, runtime_ptr, args.this(), Some(clone));
|
||||
}
|
||||
|
||||
@@ -2230,6 +2230,7 @@ mod websocket;
|
||||
mod webtransport_stream_interface_exposure;
|
||||
mod window_execution_context;
|
||||
mod window_getter_consolidation;
|
||||
mod window_node_events;
|
||||
mod window_node_worlds;
|
||||
mod worklet_interfaces;
|
||||
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
use super::window_node_worlds::child_node_world;
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn isolated_node_events_project_receivers_targets_and_share_dispatch_state() {
|
||||
for policy in [
|
||||
moli_page_types::LayoutPolicy::Mock,
|
||||
moli_page_types::LayoutPolicy::OnDemand,
|
||||
moli_page_types::LayoutPolicy::Realtime,
|
||||
] {
|
||||
let mut vm = new_storage_test_vm("https://node-events.test/");
|
||||
vm.set_layout_policy(policy);
|
||||
let child = child_node_world(&mut vm, "node-events").await;
|
||||
vm.eval(
|
||||
r#"
|
||||
globalThis.defaultFacts=[];
|
||||
const target=frame.contentDocument.getElementById('childTarget');
|
||||
target.defaultOnly='default-node';
|
||||
target.addEventListener('node-world',function(e){
|
||||
defaultFacts.push([this===target,e===original,e.target===target,
|
||||
e.currentTarget===target,e.isolatedOnly===undefined]);
|
||||
});'ready'
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
vm.eval_in_isolated_context(child,r#"
|
||||
globalThis.node=document.getElementById('childTarget');
|
||||
globalThis.facts=[];
|
||||
node.addEventListener('node-world',function(e){
|
||||
globalThis.savedEvent=e;
|
||||
facts.push([this===node,this.defaultOnly===undefined,e.defaultOnly===undefined,
|
||||
e instanceof CustomEvent,e.target===node,e.currentTarget===node,
|
||||
e.composedPath()[0]===node,e.composedPath().at(-1)===window,
|
||||
window.event===e,e.detail==='scalar-payload']);
|
||||
this.isolatedOnly='isolated-node';e.isolatedOnly='isolated-event';e.preventDefault();
|
||||
});
|
||||
const listener={handleEvent(e){facts.push([this===listener,e===savedEvent,
|
||||
e.defaultPrevented,e.currentTarget===node]);}};
|
||||
node.addEventListener('node-world',listener);'ready'
|
||||
"#).unwrap();
|
||||
let builds = vm.layout_pass_observability_for_test().1;
|
||||
assert_eq!(vm.eval(r#"
|
||||
globalThis.original=new CustomEvent('node-world',{bubbles:true,cancelable:true,detail:'scalar-payload'});
|
||||
original.defaultOnly='default-event';
|
||||
JSON.stringify([frame.contentDocument.getElementById('childTarget').dispatchEvent(original),
|
||||
original.defaultPrevented,original.currentTarget===null,original.composedPath().length===0,
|
||||
original.isolatedOnly===undefined,frame.contentDocument.getElementById('childTarget').isolatedOnly===undefined])
|
||||
"#).unwrap(),"[false,true,true,true,true,true]","{policy:?}");
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify(defaultFacts)").unwrap(),
|
||||
"[[true,true,true,true,true]]",
|
||||
"{policy:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval_in_isolated_context(child, "JSON.stringify(facts)")
|
||||
.unwrap(),
|
||||
"[[true,true,true,true,true,true,true,true,true,true],[true,true,true,true]]",
|
||||
"{policy:?}"
|
||||
);
|
||||
assert_eq!(vm.eval_in_isolated_context(child,"JSON.stringify([savedEvent.currentTarget===null,savedEvent.composedPath().length===0,savedEvent.defaultPrevented])").unwrap(),"[true,true,true]");
|
||||
assert_eq!(
|
||||
vm.layout_pass_observability_for_test().1,
|
||||
builds,
|
||||
"projection must not request layout"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn adopted_node_events_use_current_document_window_in_the_listener_world() {
|
||||
for policy in [
|
||||
moli_page_types::LayoutPolicy::Mock,
|
||||
moli_page_types::LayoutPolicy::OnDemand,
|
||||
moli_page_types::LayoutPolicy::Realtime,
|
||||
] {
|
||||
let mut vm = new_storage_test_vm("https://adopted-node-events.test/");
|
||||
vm.set_layout_policy(policy);
|
||||
let child = child_node_world(&mut vm, "adopted-node-events").await;
|
||||
vm.eval_in_isolated_context(
|
||||
child,
|
||||
r#"
|
||||
globalThis.adopted=parent.document.createElement('p');
|
||||
document.adoptNode(adopted);document.body.append(adopted);
|
||||
globalThis.facts=[];
|
||||
adopted.addEventListener('adopt-world',function(e){facts.push([
|
||||
this===adopted,this instanceof parent.HTMLParagraphElement,
|
||||
!(this instanceof HTMLParagraphElement),e instanceof CustomEvent,
|
||||
!(e instanceof parent.CustomEvent),e.target===adopted,
|
||||
e.currentTarget===adopted,e.target.ownerDocument===document,
|
||||
e.composedPath()[0]===adopted,e.composedPath().at(-1)===window]);});'ready'
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(vm.eval("String(frame.contentDocument.querySelector('p').dispatchEvent(new CustomEvent('adopt-world',{bubbles:true})))").unwrap(),"true");
|
||||
assert_eq!(
|
||||
vm.eval_in_isolated_context(child, "JSON.stringify(facts)")
|
||||
.unwrap(),
|
||||
"[[true,true,true,true,true,true,true,true,true,true]]",
|
||||
"{policy:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn inert_document_factories_preserve_native_execution_context_for_node_events() {
|
||||
for policy in [
|
||||
moli_page_types::LayoutPolicy::Mock,
|
||||
moli_page_types::LayoutPolicy::OnDemand,
|
||||
moli_page_types::LayoutPolicy::Realtime,
|
||||
] {
|
||||
let mut vm = new_storage_test_vm("https://inert-node-events.test/");
|
||||
vm.set_layout_policy(policy);
|
||||
let child = child_node_world(&mut vm, "inert-node-events").await;
|
||||
vm.eval(
|
||||
r#"
|
||||
globalThis.injectFacts=[];
|
||||
addEventListener('factory-inject',e=>{
|
||||
const node=e.relatedTarget;
|
||||
const event=new CustomEvent('factory-node');event.defaultOnly='default';
|
||||
injectFacts.push([node instanceof Node,node.dispatchEvent(event)]);
|
||||
});'ready'
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let facts=vm.eval_in_isolated_context(child,r#"(()=>{
|
||||
const childParser=new DOMParser(),parentParser=new parent.DOMParser();
|
||||
const childImpl=document.implementation,parentImpl=parent.document.implementation;
|
||||
const factories=[
|
||||
[true,true,()=>childParser.parseFromString('<p/>','text/html')],
|
||||
[false,false,()=>parentParser.parseFromString('<p/>','text/html')],
|
||||
[true,true,()=>childImpl.createHTMLDocument('child')],
|
||||
[false,false,()=>parentImpl.createHTMLDocument('parent')],
|
||||
[true,true,()=>parentImpl.createHTMLDocument.call(childImpl,'borrowed')],
|
||||
[true,true,()=>parentParser.parseFromString.call(childParser,'<p/>','text/html')],
|
||||
[true,true,()=>document.cloneNode(false)],
|
||||
[true,true,()=>parent.Node.prototype.cloneNode.call(document,false)],
|
||||
[true,true,()=>new Document()],[false,false,()=>new parent.Document()],
|
||||
[true,true,()=>Reflect.construct(DOMParser,[],parent.DOMParser).parseFromString('<p/>','text/html')],
|
||||
[false,false,()=>Reflect.construct(parent.DOMParser,[],DOMParser).parseFromString('<p/>','text/html')],
|
||||
[false,true,()=>Reflect.construct(Document,[],parent.Document)],
|
||||
[true,false,()=>Reflect.construct(parent.Document,[],Document)]];
|
||||
const facts=[];
|
||||
for(const [wrapperChild,eventChild,factory] of factories){
|
||||
const doc=factory(),node=doc.createElement('button');
|
||||
const row=[doc.defaultView===null,wrapperChild===(doc instanceof Document),wrapperChild!== (doc instanceof parent.Document)];
|
||||
node.addEventListener('factory-node',function(e){row.push(this===node,e.target===node,
|
||||
eventChild===(e instanceof CustomEvent),eventChild!==(e instanceof parent.CustomEvent),e.defaultOnly===undefined);});
|
||||
parent.dispatchEvent(new FocusEvent('factory-inject',{relatedTarget:node}));facts.push(row);
|
||||
}
|
||||
return JSON.stringify(facts);
|
||||
})()"#).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&facts).unwrap(),
|
||||
serde_json::to_value(vec![vec![true; 8]; 14]).unwrap(),
|
||||
"{policy:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify(injectFacts)").unwrap(),
|
||||
"[[true,true],[true,true],[true,true],[true,true],[true,true],[true,true],[true,true],[true,true],[true,true],[true,true],[true,true],[true,true],[true,true],[true,true]]"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn retired_frame_nodes_and_inert_factories_distinguish_execution_context_presence() {
|
||||
for policy in [
|
||||
moli_page_types::LayoutPolicy::Mock,
|
||||
moli_page_types::LayoutPolicy::OnDemand,
|
||||
moli_page_types::LayoutPolicy::Realtime,
|
||||
] {
|
||||
let mut vm = new_storage_test_vm("https://retired-node-events.test/");
|
||||
vm.set_layout_policy(policy);
|
||||
let child = child_node_world(&mut vm, "retired-node-events").await;
|
||||
vm.eval_in_isolated_context(
|
||||
child,
|
||||
r#"
|
||||
parent.savedFactories={parser:new DOMParser(),implementation:document.implementation,
|
||||
sourceDocument:document,sourceNode:document.createElement('input'),
|
||||
inert:document.implementation.createHTMLDocument('before'),constructor:Document};
|
||||
parent.savedFactories.sourceNode.type='checkbox';'ready'
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let parent = vm
|
||||
.page_isolated_world_contexts
|
||||
.contexts_with_ids()
|
||||
.find_map(|(id, world)| (!world.explicit && world.child_handle.is_none()).then_some(id))
|
||||
.unwrap();
|
||||
vm.eval("frame.srcdoc='<body>replacement</body>'; 'navigate'")
|
||||
.unwrap();
|
||||
run_child_navigation_commit_and_host_load_for_test(&mut vm, "retire Node source Document")
|
||||
.await;
|
||||
let facts=vm.eval_in_isolated_context(parent,r#"(()=>{
|
||||
const old=savedFactories.sourceNode;
|
||||
let invalid;try{old.dispatchEvent({});}catch(e){invalid=e.name;}
|
||||
let nested;document.addEventListener('nested',e=>{try{old.dispatchEvent(e);}catch(error){nested=error.name;}},{once:true});
|
||||
document.dispatchEvent(new Event('nested'));
|
||||
const documents=[savedFactories.sourceDocument,savedFactories.inert,
|
||||
savedFactories.parser.parseFromString('<p/>','text/html'),
|
||||
savedFactories.implementation.createHTMLDocument('after'),
|
||||
savedFactories.inert.implementation.createHTMLDocument('again'),
|
||||
savedFactories.inert.cloneNode(false),new savedFactories.constructor()];
|
||||
const dispatches=documents.map(doc=>{let called=0;const node=doc.createElement('button');
|
||||
node.addEventListener('after',()=>called++);const e=new Event('after');
|
||||
const result=node.dispatchEvent(e);return [result,called,e.currentTarget===null];});
|
||||
const click=old.dispatchEvent(new MouseEvent('click',{bubbles:true}));
|
||||
return JSON.stringify({invalid,nested,dispatches,click,checked:old.checked,
|
||||
cloneNull:savedFactories.sourceDocument.cloneNode(false)===null});
|
||||
})()"#).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&facts).unwrap(),
|
||||
serde_json::json!({
|
||||
"invalid":"TypeError","nested":"InvalidStateError",
|
||||
"dispatches":[[false,0,true],[true,0,true],[true,0,true],[false,0,true],
|
||||
[true,0,true],[true,0,true],[false,0,true]],
|
||||
"click":false,"checked":false,"cloneNull":true
|
||||
}),
|
||||
"{policy:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inert_node_execution_context_survives_creation_realm_and_wrapper_cache_retirement() {
|
||||
for policy in [
|
||||
moli_page_types::LayoutPolicy::Mock,
|
||||
moli_page_types::LayoutPolicy::OnDemand,
|
||||
moli_page_types::LayoutPolicy::Realtime,
|
||||
] {
|
||||
let mut vm = new_storage_test_vm("https://inert-node-realm-retirement.test/");
|
||||
vm.set_layout_policy(policy);
|
||||
let isolated = vm
|
||||
.create_isolated_world("inert-source-realm", false)
|
||||
.unwrap();
|
||||
vm.eval_in_isolated_context(isolated,
|
||||
"globalThis.node=document.implementation.createHTMLDocument('inert').createElement('button');'ready'").unwrap();
|
||||
let context_ptr = &vm
|
||||
.page_isolated_world_contexts
|
||||
.context(isolated)
|
||||
.unwrap()
|
||||
.context as *const _;
|
||||
let handle = vm
|
||||
.with_context_scope_by_ptr_and_checkpoint_for_test(context_ptr, |scope, _| {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let node = global
|
||||
.get(scope, crate::util::v8str(scope, "node").into())
|
||||
.unwrap();
|
||||
let node = v8::Local::<v8::Object>::try_from(node).unwrap();
|
||||
Ok(
|
||||
crate::native_bridge::node_runtime_and_handle_from_object_or_detached(
|
||||
scope, node,
|
||||
)?
|
||||
.1,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
vm.destroy_isolated_world_context(isolated);
|
||||
vm.collect_renderer_document_isolate_garbage().unwrap();
|
||||
assert_eq!(
|
||||
vm._context_host
|
||||
.borrow()
|
||||
.native_bridge()
|
||||
.isolated_world_wrapper_cache_state_for_test(),
|
||||
None
|
||||
);
|
||||
vm.with_default_context_scope_and_checkpoint_for_test(|scope, host_ptr| {
|
||||
let node = crate::native_bridge::document::detached_native_object_for_handle(
|
||||
scope, host_ptr, handle,
|
||||
)
|
||||
.unwrap();
|
||||
let global = scope.get_current_context().global(scope);
|
||||
assert_eq!(
|
||||
global.set(
|
||||
scope,
|
||||
crate::util::v8str(scope, "retainedNode").into(),
|
||||
node.into()
|
||||
),
|
||||
Some(true)
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(vm.eval(r#"(()=>{
|
||||
const facts=[];const event=new Event('after-realm');
|
||||
retainedNode.addEventListener('after-realm',function(e){facts.push(this===retainedNode,e===event,e.target===retainedNode);});
|
||||
return JSON.stringify([retainedNode.dispatchEvent(event),facts]);
|
||||
})()"#).unwrap(),"[true,[true,true,true]]","the source Window remains live: {policy:?}");
|
||||
let host = vm.context_host_weak_for_test();
|
||||
let isolate = vm.renderer_document_isolate.clone();
|
||||
drop(vm);
|
||||
for _ in 0..2 {
|
||||
isolate.with_renderer_document_isolate_mut(|isolate| isolate.low_memory_notification());
|
||||
}
|
||||
assert!(
|
||||
host.upgrade().is_none(),
|
||||
"native Document lineage must not root the Host"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn popup_document_event_authority_follows_its_window_instead_of_parser_provenance() {
|
||||
for policy in [
|
||||
moli_page_types::LayoutPolicy::Mock,
|
||||
moli_page_types::LayoutPolicy::OnDemand,
|
||||
moli_page_types::LayoutPolicy::Realtime,
|
||||
] {
|
||||
let mut vm = new_storage_test_vm("https://popup-node-events.test/");
|
||||
vm.set_layout_policy(policy);
|
||||
let facts=vm.eval(r#"(()=>{
|
||||
const popup=window.open('about:blank');
|
||||
const oldDocument=popup.document,node=oldDocument.createElement('input');node.type='checkbox';
|
||||
let called=0;node.addEventListener('click',()=>called++);
|
||||
const before=node.dispatchEvent(new MouseEvent('click'));
|
||||
popup.close();
|
||||
const after=node.dispatchEvent(new MouseEvent('click'));
|
||||
return JSON.stringify([before,after,called,node.checked,
|
||||
oldDocument.cloneNode(false)===null,popup.closed]);
|
||||
})()"#).unwrap();
|
||||
assert_eq!(facts, "[true,false,1,true,true,true]", "{policy:?}");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
|
||||
async fn child_node_world(vm: &mut ScriptVm, name: &str) -> i64 {
|
||||
pub(super) async fn child_node_world(vm: &mut ScriptVm, name: &str) -> i64 {
|
||||
vm.eval(
|
||||
"globalThis.frame=document.createElement('iframe');\
|
||||
frame.srcdoc='<!doctype html><body><span id=childTarget>child</span></body>';\
|
||||
|
||||
@@ -390,6 +390,14 @@ pub(super) fn event_target_dispatch_event_callback<'s>(
|
||||
rv.set_bool(false);
|
||||
return;
|
||||
};
|
||||
// Binding validation precedes this test. Reject a retired framed
|
||||
// Document before click pre-activation, counters or Event mutation.
|
||||
if let EventTargetHandle::Node(node) = target
|
||||
&& host.document_execution_context_for_node(node).is_none()
|
||||
{
|
||||
rv.set_bool(false);
|
||||
return;
|
||||
}
|
||||
let click_activation_target = if event_type.as_deref() == Some("click")
|
||||
&& event_is_mouse_event(scope, event)
|
||||
&& let EventTargetHandle::Node(handle) = target
|
||||
|
||||
Reference in New Issue
Block a user