fix(html): scope noopener browsing context targets

This commit is contained in:
ldm0
2026-10-02 03:28:07 +08:00
parent 85d72d999a
commit bef9ff72b6
8 changed files with 225 additions and 61 deletions
@@ -106,7 +106,7 @@ pub(in crate::context_bootstrap) fn child_browsing_context_handle_for_runtime_ow
})
}
pub(super) fn runtime_window_dispatch_scope<'s>(
pub(in crate::context_bootstrap) fn runtime_window_dispatch_scope<'s>(
scope: &mut v8::PinScope<'s, '_>,
window: v8::Local<'s, v8::Object>,
) -> Option<crate::native_bridge::OwnerDispatchScope> {
@@ -663,9 +663,23 @@ fn window_length_replaceable_getter<'s>(
return;
};
let host = unsafe { &mut *host_ptr };
let count = child_context_handle_from_owner(scope, args.this())
.map(|handle| host.child_browsing_context_child_frame_count(handle))
.unwrap_or_else(|| host.child_browsing_context_count());
let document = match super::navigation_window::runtime_window_dispatch_scope(scope, args.this())
{
Some(crate::native_bridge::OwnerDispatchScope::Top) => Some(host.document_handle()),
Some(crate::native_bridge::OwnerDispatchScope::Child(handle)) => {
host.child_browsing_context_document_handle(handle)
}
Some(crate::native_bridge::OwnerDispatchScope::LightweightPopup(popup_id)) => {
host.lightweight_popup_document_handle(popup_id)
}
None => None,
};
let count = document
.map(|document| {
host.sync_child_browsing_context_subtree(scope, document);
host.child_browsing_context_count_for_document(document)
})
.unwrap_or(0);
rv.set(v8::Number::new(scope, count as f64).into());
}
@@ -1,5 +1,6 @@
use super::helpers::{
window_child_context_handle, window_hidden_value, window_host_ptr, window_receiver,
window_child_context_handle, window_document_handle, window_hidden_value, window_host_ptr,
window_receiver,
};
use super::*;
@@ -16,11 +17,12 @@ pub(in crate::context_bootstrap) fn window_length_getter<'s>(
return;
};
let runtime = unsafe { &mut *host_ptr };
let count = if let Some(handle) = window_child_context_handle(scope, receiver) {
runtime.child_browsing_context_child_frame_count(handle)
} else {
runtime.child_browsing_context_count()
};
let count = window_document_handle(scope, receiver, runtime)
.map(|document| {
runtime.sync_child_browsing_context_subtree(scope, document);
runtime.child_browsing_context_count_for_document(document)
})
.unwrap_or(0);
rv.set(v8::Number::new(scope, count as f64).into());
}
@@ -92,6 +92,33 @@ pub(in crate::context_bootstrap) fn window_child_context_handle<'s>(
None
}
pub(super) fn window_owner_dispatch_scope<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<crate::native_bridge::OwnerDispatchScope> {
super::super::navigation_window::runtime_window_dispatch_scope(scope, receiver).or_else(|| {
receiver
.strict_equals(scope.get_current_context().global(scope).into())
.then_some(crate::native_bridge::OwnerDispatchScope::Top)
})
}
pub(super) fn window_document_handle<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
host: &JsContextHost,
) -> Option<crate::document_runtime::DomHandle> {
match window_owner_dispatch_scope(scope, receiver)? {
crate::native_bridge::OwnerDispatchScope::Top => Some(host.document_handle()),
crate::native_bridge::OwnerDispatchScope::Child(handle) => {
host.child_browsing_context_document_handle(handle)
}
crate::native_bridge::OwnerDispatchScope::LightweightPopup(popup_id) => {
host.lightweight_popup_document_handle(popup_id)
}
}
}
pub(crate) fn window_host_ptr(
scope: &mut v8::PinScope<'_, '_>,
receiver: v8::Local<'_, v8::Object>,
@@ -1,4 +1,7 @@
use super::helpers::{window_child_context_handle, window_host_ptr};
use super::helpers::{
window_child_context_handle, window_document_handle, window_host_ptr,
window_owner_dispatch_scope,
};
use crate::native_bridge::named_access::{
build_window_named_items_collection, window_named_item_handles,
};
@@ -11,24 +14,34 @@ fn is_reserved_window_name(name: &str) -> bool {
fn window_indexed_child_handle<'s>(
scope: &mut v8::PinScope<'s, '_>,
holder: v8::Local<'s, v8::Object>,
receiver: v8::Local<'s, v8::Object>,
index: u32,
) -> Option<(
*mut crate::native_bridge::JsContextHost,
crate::document_runtime::DomHandle,
crate::native_bridge::OwnerDispatchScope,
)> {
let host_ptr = window_host_ptr(scope, holder)?;
let host_ptr = window_host_ptr(scope, receiver)?;
let host = unsafe { &mut *host_ptr };
let handle = if let Some(parent) = window_child_context_handle(scope, holder) {
if let Some(document) = host.child_browsing_context_document_handle(parent) {
host.sync_child_browsing_context_subtree(scope, document);
}
host.child_browsing_context_child_frame_handle_by_index(parent, index as usize)
let owner_scope = window_owner_dispatch_scope(scope, receiver)?;
let document = window_document_handle(scope, receiver, host)?;
host.sync_child_browsing_context_subtree(scope, document);
let handle =
host.child_browsing_context_handle_by_index_for_document(document, index as usize)?;
Some((host_ptr, handle, owner_scope))
}
fn window_child_projection_for_owner<'s>(
scope: &mut v8::PinScope<'s, '_>,
host: &mut crate::native_bridge::JsContextHost,
handle: crate::document_runtime::DomHandle,
owner_scope: crate::native_bridge::OwnerDispatchScope,
) -> Option<v8::Local<'s, v8::Object>> {
if owner_scope == crate::native_bridge::OwnerDispatchScope::Top {
host.child_browsing_context_window_proxy_for_top(scope, handle)
} else {
host.sync_child_browsing_context_subtree(scope, host.document_handle());
host.child_browsing_context_handle_by_index(index as usize)
}?;
Some((host_ptr, handle))
host.child_browsing_context_window_wrapper(scope, handle)
}
}
pub(in crate::context_bootstrap) fn window_indexed_property_getter<'s>(
@@ -37,16 +50,13 @@ pub(in crate::context_bootstrap) fn window_indexed_property_getter<'s>(
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) -> v8::Intercepted {
let holder = args.holder();
let Some((host_ptr, handle)) = window_indexed_child_handle(scope, holder, index) else {
let Some((host_ptr, handle, owner_scope)) =
window_indexed_child_handle(scope, args.holder(), index)
else {
return v8::Intercepted::kNo;
};
let host = unsafe { &mut *host_ptr };
let window = if window_child_context_handle(scope, holder).is_none() {
host.child_browsing_context_window_proxy_for_top(scope, handle)
} else {
host.child_browsing_context_window_wrapper(scope, handle)
};
let window = window_child_projection_for_owner(scope, host, handle, owner_scope);
let Some(window) = window else {
return v8::Intercepted::kNo;
};
@@ -72,22 +82,18 @@ pub(in crate::context_bootstrap) fn window_indexed_property_enumerator<'s>(
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Array>,
) {
let holder = args.holder();
let Some(host_ptr) = window_host_ptr(scope, holder) else {
let receiver = args.holder();
let Some(host_ptr) = window_host_ptr(scope, receiver) else {
rv.set(v8::Array::new(scope, 0));
return;
};
let host = unsafe { &mut *host_ptr };
let count = if let Some(parent) = window_child_context_handle(scope, holder) {
if let Some(document) = host.child_browsing_context_document_handle(parent) {
let count = window_document_handle(scope, receiver, host)
.map(|document| {
host.sync_child_browsing_context_subtree(scope, document);
}
host.child_browsing_context_child_frame_handles(parent)
.len()
} else {
host.sync_child_browsing_context_subtree(scope, host.document_handle());
host.child_browsing_context_count()
};
host.child_browsing_context_count_for_document(document)
})
.unwrap_or(0);
let array = serialize_v8_iter_array(scope, (0..count).map(|index| index as u32))
.unwrap_or_else(|| v8::Array::new(scope, 0));
rv.set(array);
@@ -99,16 +105,13 @@ pub(in crate::context_bootstrap) fn window_indexed_property_descriptor<'s>(
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) -> v8::Intercepted {
let Some((host_ptr, handle)) = window_indexed_child_handle(scope, args.holder(), index) else {
let Some((host_ptr, handle, owner_scope)) =
window_indexed_child_handle(scope, args.holder(), index)
else {
return v8::Intercepted::kNo;
};
let holder = args.holder();
let host = unsafe { &mut *host_ptr };
let window = if window_child_context_handle(scope, holder).is_none() {
host.child_browsing_context_window_proxy_for_top(scope, handle)
} else {
host.child_browsing_context_window_wrapper(scope, handle)
};
let window = window_child_projection_for_owner(scope, host, handle, owner_scope);
let Some(window) = window else {
return v8::Intercepted::kNo;
};
@@ -18,6 +18,11 @@ impl JsContextHost {
.len()
}
pub(crate) fn child_browsing_context_count_for_document(&self, document: DomHandle) -> usize {
self.child_browsing_context_direct_frame_handles_for_document(document)
.len()
}
pub(crate) fn child_browsing_context_handles_in_document_order(&self) -> Vec<DomHandle> {
let mut handles = Vec::new();
self.collect_child_browsing_context_handles_in_document_order_from_document(
@@ -118,22 +123,38 @@ impl JsContextHost {
.collect()
}
fn child_browsing_context_direct_frame_handles_for_document(
&self,
document: DomHandle,
) -> Vec<DomHandle> {
self.child_browsing_contexts
.keys()
.copied()
.filter(|handle| {
self.dom_host().node(*handle).and_then(Node::owner_document) == Some(document)
})
.collect()
}
#[cfg(test)]
pub(crate) fn child_browsing_context_handle_by_index(&self, index: usize) -> Option<DomHandle> {
// Window indexed/named interceptors hit this on every miss.
if self.child_browsing_contexts.is_empty() {
return None;
}
// Tests use this to inspect the global frame-tree projection rather
// than one Window's scoped indexed properties.
self.top_level_child_browsing_context_handles_in_frame_tree_order()
.into_iter()
.nth(index)
}
pub(crate) fn child_browsing_context_child_frame_handle_by_index(
pub(crate) fn child_browsing_context_handle_by_index_for_document(
&self,
parent: DomHandle,
document: DomHandle,
index: usize,
) -> Option<DomHandle> {
self.child_browsing_context_child_frame_handles(parent)
// Window indexed/named interceptors hit this on every miss.
if self.child_browsing_contexts.is_empty() {
return None;
}
self.child_browsing_context_direct_frame_handles_for_document(document)
.into_iter()
.nth(index)
}
@@ -142,11 +163,9 @@ impl JsContextHost {
&self,
parent: DomHandle,
) -> Vec<DomHandle> {
self.child_browsing_contexts
.keys()
.copied()
.filter(|handle| self.child_browsing_context_parent_handle(*handle) == Some(parent))
.collect()
self.child_browsing_context_document_handle(parent)
.map(|document| self.child_browsing_context_direct_frame_handles_for_document(document))
.unwrap_or_default()
}
pub(crate) fn child_browsing_context_direct_host_handles(
@@ -653,8 +653,7 @@ impl JsContextHost {
creator_base_url: Url,
creator_policy_container: DocumentPolicyContainer,
) -> Option<OpenedLightweightPopup<'s>> {
if opener.is_some()
&& let Some(name) = trackable_lightweight_popup_window_name(target_name)
if let Some(name) = trackable_lightweight_popup_window_name(target_name)
&& let Some(popup_id) = self.lightweight_popup_window_names.get(&name).copied()
&& self.lightweight_popup_is_open(popup_id)
&& let Some(window) = self.reopen_lightweight_popup_window(
@@ -2901,3 +2901,103 @@ async fn lightweight_popup_external_script_redirect_final_url_obeys_csp() {
format!("{final_script_url}|script-src-elem|enforce|true||load:true")
);
}
#[tokio::test]
async fn lightweight_popup_window_child_queries_stay_in_the_popup_document() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
let mut vm =
new_page_task_executor_test_vm_with_loader("https://example.com/base/page.html", &loader);
let result = vm
.eval(
r#"
(() => {
globalThis.__popupFrameScopeProbe = "pending";
const topFrame = document.createElement("iframe");
topFrame.name = "top-child";
document.body.appendChild(topFrame);
topFrame.contentDocument.body.innerHTML = '<p id="top-only"></p>';
const popupMarkup = `
<!doctype html>
<iframe name="popup-child" srcdoc="<p id='popup-only'></p>"></iframe>
<script>
addEventListener("load", () => {
const childDocument = frames[0].document;
opener.__popupFrameScopeProbe = JSON.stringify({
length: window.length,
name: frames[0].name,
popupNode: childDocument.getElementById("popup-only") !== null,
topNode: childDocument.getElementById("top-only") !== null
});
});
<\/script>
`;
open(URL.createObjectURL(new Blob([popupMarkup], { type: "text/html" })));
return __popupFrameScopeProbe;
})()
"#,
)
.expect("popup child-query scope setup should evaluate");
assert_eq!(result, "pending");
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"__popupFrameScopeProbe",
r#"{"length":1,"name":"popup-child","popupNode":true,"topNode":false}"#,
"popup child-query document scope",
)
.await;
}
#[tokio::test]
async fn noopener_hyperlink_reuses_an_existing_named_popup_and_preserves_its_opener() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
let mut vm =
new_page_task_executor_test_vm_with_loader("https://example.com/base/page.html", &loader);
let result = vm
.eval(
r#"
(() => {
const popupName = "moli-noopener-named-target";
const popup = open("about:blank", popupName);
const targetMarkup = '<!doctype html><p id="named-target"></p>';
const targetUrl = URL.createObjectURL(new Blob([targetMarkup], { type: "text/html" }));
const link = document.createElement("a");
link.rel = "noopener";
link.target = popupName;
link.href = targetUrl;
document.body.appendChild(link);
globalThis.__namedNoopenerPopup = popup;
globalThis.__namedNoopenerTargetUrl = targetUrl;
link.click();
return String(popup.location.href === targetUrl && popup.opener === window);
})()
"#,
)
.expect("named noopener popup setup should evaluate");
assert_eq!(result, "true");
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(__namedNoopenerPopup.document.getElementById('named-target') !== null)",
"true",
"named noopener target navigation",
)
.await;
assert_eq!(
vm.eval(
r#"JSON.stringify({
openerPreserved: __namedNoopenerPopup.opener === window,
namePreserved: __namedNoopenerPopup.name === "moli-noopener-named-target",
targetCommitted: __namedNoopenerPopup.location.href === __namedNoopenerTargetUrl
})"#,
)
.expect("named noopener popup result should evaluate"),
r#"{"openerPreserved":true,"namePreserved":true,"targetCommitted":true}"#
);
}