mirror of
https://github.com/lexmount/moli.git
synced 2026-10-09 08:01:05 +00:00
fix(page-visibility): keep synthetic iframe documents hidden
Reading contentWindow on an iframe in a windowless document must not make its synthetic child document visible. Determine browsing-context ownership from native top-level, child frame, and popup registrations, preserving the document's retained visibility state separately from defaultView. Cover both getter access orders, nested synthetic frames, and cross-realm visibility/defaultView getters on live documents and popups. Follow-up to #501.
This commit is contained in:
@@ -1450,18 +1450,25 @@ fn current_script_belongs_to_document(
|
||||
.is_some_and(|owner_document| owner_document == document_handle)
|
||||
}
|
||||
|
||||
fn document_is_hidden<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
handle: DomHandle,
|
||||
document: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
unsafe { &*runtime_ptr }
|
||||
fn document_has_browsing_context(runtime: &JsContextHost, handle: DomHandle) -> bool {
|
||||
// Detached iframe compatibility windows can become a document's defaultView
|
||||
// without registering a browsing context. They must not make it visible.
|
||||
runtime.dom_host().document_handle() == handle
|
||||
|| runtime
|
||||
.child_browsing_context_host_for_document_handle(handle)
|
||||
.is_some()
|
||||
|| runtime
|
||||
.lightweight_popup_id_for_document_handle(handle)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn document_is_hidden(runtime: &JsContextHost, handle: DomHandle) -> bool {
|
||||
runtime
|
||||
.dom_host()
|
||||
.node(handle)
|
||||
.and_then(Node::as_document)
|
||||
.is_some_and(|document| document.visibility_hidden())
|
||||
|| document_associated_window_for_object(scope, runtime_ptr, handle, document).is_none()
|
||||
.is_none_or(|document| document.visibility_hidden())
|
||||
|| !document_has_browsing_context(runtime, handle)
|
||||
}
|
||||
|
||||
fn document_hidden_getter_function<'s>(
|
||||
@@ -1474,7 +1481,7 @@ fn document_hidden_getter_function<'s>(
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
rv.set_bool(document_is_hidden(scope, runtime_ptr, handle, args.this()));
|
||||
rv.set_bool(document_is_hidden(unsafe { &*runtime_ptr }, handle));
|
||||
}
|
||||
|
||||
fn document_visibility_state_getter_function<'s>(
|
||||
@@ -1487,7 +1494,7 @@ fn document_visibility_state_getter_function<'s>(
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
let state = if document_is_hidden(scope, runtime_ptr, handle, args.this()) {
|
||||
let state = if document_is_hidden(unsafe { &*runtime_ptr }, handle) {
|
||||
"hidden"
|
||||
} else {
|
||||
"visible"
|
||||
|
||||
@@ -3474,6 +3474,58 @@ fn detached_document_view_uses_document_prototype_accessors() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detached_iframe_windows_do_not_change_document_visibility() {
|
||||
let mut vm = new_storage_test_vm("https://detached-iframe-visibility.test/");
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const errors = [];
|
||||
const check = (condition, message) => { if (!condition) errors.push(message); };
|
||||
const factories = [
|
||||
["createHTMLDocument", () => document.implementation.createHTMLDocument("")],
|
||||
["DOMParser", () => new DOMParser().parseFromString("<body></body>", "text/html")],
|
||||
["createDocument", () => document.implementation.createDocument("urn:test", "root", null)]
|
||||
];
|
||||
for (const [name, create] of factories) {
|
||||
for (const first of ["document", "window"]) {
|
||||
const outer = create();
|
||||
const frame = outer.createElementNS("http://www.w3.org/1999/xhtml", "iframe");
|
||||
frame.srcdoc = "<p>hello</p>";
|
||||
outer.documentElement.appendChild(frame);
|
||||
const label = `${name}/${first}`;
|
||||
check(outer.defaultView === null, `${label}: windowless owner`);
|
||||
let child;
|
||||
if (first === "document") {
|
||||
child = frame.contentDocument;
|
||||
check(child.hidden && child.visibilityState === "hidden", `${label}: before contentWindow`);
|
||||
}
|
||||
const view = frame.contentWindow;
|
||||
child ||= frame.contentDocument;
|
||||
check(view !== null && view.document === child, `${label}: synthetic window`);
|
||||
check(child.defaultView === view, `${label}: associated window retained`);
|
||||
check(child.hidden && child.visibilityState === "hidden", `${label}: after contentWindow`);
|
||||
check(outer.hidden && outer.visibilityState === "hidden", `${label}: owner stays hidden`);
|
||||
const nested = child.createElement("iframe");
|
||||
nested.srcdoc = "<p>nested</p>";
|
||||
child.body.appendChild(nested);
|
||||
const nestedChild = nested.contentDocument;
|
||||
check(nestedChild.hidden && nestedChild.visibilityState === "hidden", `${label}: nested before contentWindow`);
|
||||
check(nested.contentWindow.document === nestedChild, `${label}: nested window`);
|
||||
check(nestedChild.hidden && nestedChild.visibilityState === "hidden", `${label}: nested after contentWindow`);
|
||||
}
|
||||
}
|
||||
return JSON.stringify(errors);
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("detached iframe visibility probe should evaluate");
|
||||
|
||||
assert_eq!(result, "[]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detached_document_state_and_collections_use_document_prototype_accessors() {
|
||||
let mut vm = new_storage_test_vm("https://detached-document-prototype-state.test/");
|
||||
|
||||
@@ -52,6 +52,65 @@ fn input_show_picker_enforces_brand_without_rejecting_inherited_child_origin() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_visibility_and_default_view_follow_receiver_across_realms() {
|
||||
let mut vm = new_storage_test_vm("https://document-visibility-realms.test/");
|
||||
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const frame = document.createElement("iframe");
|
||||
frame.id = "visibility-live-frame";
|
||||
(document.body || document.documentElement || document).appendChild(frame);
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("visibility child frame should be created");
|
||||
materialize_single_child_default_realm_for_test(
|
||||
&mut vm,
|
||||
"document visibility and defaultView child Realm",
|
||||
);
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const errors = [];
|
||||
const check = (condition, message) => { if (!condition) errors.push(message); };
|
||||
const childWindow = document.getElementById("visibility-live-frame").contentWindow;
|
||||
const child = childWindow.document;
|
||||
const parentView = Object.getOwnPropertyDescriptor(Document.prototype, "defaultView").get;
|
||||
const childView = Object.getOwnPropertyDescriptor(childWindow.Document.prototype, "defaultView").get;
|
||||
const parentHidden = Object.getOwnPropertyDescriptor(Document.prototype, "hidden").get;
|
||||
const childHidden = Object.getOwnPropertyDescriptor(childWindow.Document.prototype, "hidden").get;
|
||||
const parentVisibility = Object.getOwnPropertyDescriptor(Document.prototype, "visibilityState").get;
|
||||
const childVisibility = Object.getOwnPropertyDescriptor(childWindow.Document.prototype, "visibilityState").get;
|
||||
check(parentView.call(document) === window, "parent defaultView");
|
||||
check(parentView.call(child) === childWindow, "parent getter on child defaultView");
|
||||
check(childView.call(document) === window, "child getter on parent defaultView");
|
||||
check(childView.call(child) === childWindow, "child defaultView");
|
||||
check(!parentHidden.call(child) && parentVisibility.call(child) === "visible", "child is visible through parent getters");
|
||||
check(!childHidden.call(document) && childVisibility.call(document) === "visible", "parent is visible through child getters");
|
||||
const windowless = child.implementation.createHTMLDocument("");
|
||||
check(childView.call(windowless) === null, "windowless defaultView through child getter");
|
||||
check(parentHidden.call(windowless) && parentVisibility.call(windowless) === "hidden", "windowless remains hidden through parent getters");
|
||||
const popup = window.open("about:blank", "visibility-popup");
|
||||
check(popup !== null, "popup created");
|
||||
try {
|
||||
check(!popup.document.hidden && popup.document.visibilityState === "visible", "live popup is visible");
|
||||
check(parentView.call(popup.document) === popup, "popup defaultView through parent getter");
|
||||
} finally {
|
||||
popup.close();
|
||||
}
|
||||
return JSON.stringify(errors);
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("cross-realm document visibility and view probe should evaluate");
|
||||
|
||||
assert_eq!(result, "[]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cross_realm_dom_bindings_reject_incompatible_receivers_in_their_own_realm() {
|
||||
let mut vm = new_storage_test_vm("https://cross-realm-dom-receivers.test/");
|
||||
|
||||
Reference in New Issue
Block a user