mirror of
https://github.com/lexmount/moli.git
synced 2026-10-08 00:01:00 +00:00
fix(html): scope noopener browsing context targets
This commit is contained in:
@@ -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}"#
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user