mirror of
https://github.com/lexmount/moli.git
synced 2026-10-05 16:00:54 +00:00
fix(dom): preserve document domains until replacement
This commit is contained in:
@@ -700,7 +700,8 @@ impl JsContextHost {
|
||||
}
|
||||
let host = unsafe { &mut *host_ptr };
|
||||
if let Some(entry) = host.child_browsing_contexts.get_mut(&child_handle) {
|
||||
entry.clear_document_runtime_state();
|
||||
// document.open() reuses this Document and its mutable origin.
|
||||
entry.clear_script_execution_state();
|
||||
}
|
||||
host.request_child_frame_realm_materialization(child_handle);
|
||||
host.install_empty_child_classic_script_runner_for_current_document(
|
||||
|
||||
@@ -425,13 +425,11 @@ impl JsContextHost {
|
||||
.as_ref()
|
||||
.map(|entry| entry.classic_script_document_state.clone())
|
||||
.unwrap_or_default(),
|
||||
document_domain_override: if attribute_bootstrap_changed {
|
||||
None
|
||||
} else {
|
||||
existing
|
||||
.as_ref()
|
||||
.and_then(|entry| entry.document_domain_override())
|
||||
},
|
||||
// Attribute changes can schedule navigation, but the active
|
||||
// Document keeps its mutable origin until replacement commits.
|
||||
document_domain_override: existing
|
||||
.as_ref()
|
||||
.and_then(|entry| entry.document_domain_override()),
|
||||
credentialless,
|
||||
service_worker_client_id: existing
|
||||
.as_ref()
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn document_domain_survives_stream_replacement_and_pending_navigation() {
|
||||
const HOST: &str = "document-domain-lifetime.test";
|
||||
let child = "<!doctype html><body><p id=original>original</p>\
|
||||
<script>document.domain = location.hostname;</script>";
|
||||
let same = StaticHttpServer::spawn_with_bodies(vec![child.to_owned()]).await;
|
||||
let cross = StaticHttpServer::spawn_with_bodies(vec![
|
||||
child.to_owned(),
|
||||
"<!doctype html><body>replacement".to_owned(),
|
||||
])
|
||||
.await;
|
||||
let loader = static_http_loader([same.resolve_entry(HOST), cross.resolve_entry(HOST)]);
|
||||
let mut vm = new_parsed_page_task_executor_test_vm(
|
||||
same.url_for_host(HOST, "/entry.html").as_str(),
|
||||
"<!doctype html><body>parent",
|
||||
&loader,
|
||||
);
|
||||
let script = include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/document-domain-lifetime.js"
|
||||
));
|
||||
vm.exec(
|
||||
&format!(
|
||||
r#"
|
||||
globalThis.__documentDomainLifetimeResult = null;
|
||||
({script})({{sameURL:{same_url:?},crossURL:{cross_url:?},replacementURL:{replacement_url:?}}}).then(
|
||||
result => {{__documentDomainLifetimeResult = result}},
|
||||
error => {{__documentDomainLifetimeResult = {{error:String(error)}}}}
|
||||
);
|
||||
"#,
|
||||
same_url = same.url_for_host(HOST, "/child.html").as_str(),
|
||||
cross_url = cross.url_for_host(HOST, "/child.html").as_str(),
|
||||
replacement_url = cross.url_for_host(HOST, "/replacement.html").as_str(),
|
||||
),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
advance_page_task_executor_until_eval_equals(
|
||||
&mut vm,
|
||||
&loader,
|
||||
"String(__documentDomainLifetimeResult !== null)",
|
||||
"true",
|
||||
"document.domain lifetime checks should finish",
|
||||
)
|
||||
.await;
|
||||
let result: serde_json::Value = serde_json::from_str(
|
||||
&vm.eval("JSON.stringify(__documentDomainLifetimeResult)")
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result["checks"], 48, "{result}");
|
||||
assert_eq!(result["failures"], serde_json::json!([]), "{result}");
|
||||
assert_eq!(same.finish_targets().await, ["/child.html"]);
|
||||
assert_eq!(
|
||||
cross.finish_targets().await,
|
||||
["/child.html", "/replacement.html"]
|
||||
);
|
||||
}
|
||||
@@ -54,3 +54,5 @@ mod web_audio;
|
||||
mod webrtc;
|
||||
|
||||
mod worker_listener_invocation;
|
||||
|
||||
mod document_domain_lifetime;
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
async ({sameURL, crossURL, replacementURL}) => {
|
||||
document.domain = location.hostname;
|
||||
let checks = 0;
|
||||
const failures = [];
|
||||
const check = (label, predicate) => {
|
||||
checks++;
|
||||
try {
|
||||
if (!predicate()) failures.push(label);
|
||||
} catch (error) {
|
||||
failures.push(label + ': ' + error);
|
||||
}
|
||||
};
|
||||
const securityError = callback => {
|
||||
try { callback(); }
|
||||
catch (error) { return error instanceof DOMException && error.name === 'SecurityError'; }
|
||||
return false;
|
||||
};
|
||||
const create = async url => {
|
||||
const frame = document.createElement('iframe');
|
||||
const loaded = new Promise(resolve => { frame.onload = resolve; });
|
||||
frame.src = url;
|
||||
document.body.appendChild(frame);
|
||||
await loaded;
|
||||
return frame;
|
||||
};
|
||||
|
||||
const openedFrame = await create(sameURL);
|
||||
const openedWindow = openedFrame.contentWindow;
|
||||
const openedDocument = openedWindow.document;
|
||||
check('explicit open returns the same Document', () => openedDocument.open() === openedDocument);
|
||||
check('explicit open keeps domain access', () => openedWindow.document === openedDocument);
|
||||
openedDocument.write('<!doctype html><body><p>explicit');
|
||||
openedDocument.close();
|
||||
check('closing the stream keeps domain access', () => openedWindow.document === openedDocument);
|
||||
check('explicit open replaces content', () => openedDocument.body.textContent === 'explicit');
|
||||
openedDocument.write('<!doctype html><body><p>implicit');
|
||||
openedDocument.close();
|
||||
check('implicit open keeps domain access', () => openedWindow.document === openedDocument);
|
||||
check('implicit open replaces content', () => openedDocument.body.textContent === 'implicit');
|
||||
openedFrame.remove();
|
||||
|
||||
const frame = await create(crossURL);
|
||||
const window = frame.contentWindow;
|
||||
const original = window.document;
|
||||
const originalRoot = original.documentElement;
|
||||
const Exception = window.DOMException;
|
||||
const listen = window.addEventListener;
|
||||
const snapshot = phase => {
|
||||
check(phase + ': contentDocument', () => frame.contentDocument === original);
|
||||
check(phase + ': Window.document', () => window.document === original);
|
||||
check(phase + ': original realm', () => window.DOMException === Exception);
|
||||
check(phase + ': relaxed domain', () => original.domain === document.domain);
|
||||
check(phase + ': raw origins still differ', () => window.location.origin !== location.origin);
|
||||
check(phase + ': active Document preserves content', () => original.documentElement === originalRoot);
|
||||
};
|
||||
snapshot('loaded');
|
||||
|
||||
const events = [];
|
||||
for (const type of ['beforeunload', 'pagehide', 'unload']) {
|
||||
listen.call(window, type, () => {
|
||||
events.push(type);
|
||||
snapshot(type);
|
||||
});
|
||||
}
|
||||
let loaded = new Promise(resolve => { frame.onload = resolve; });
|
||||
frame.src = replacementURL;
|
||||
snapshot('pending committed navigation');
|
||||
await loaded;
|
||||
for (const type of ['beforeunload', 'pagehide', 'unload']) {
|
||||
check(type + ' ran', () => events.filter(event => event === type).length === 1);
|
||||
}
|
||||
check('new network Document has no domain override', () => frame.contentDocument === null);
|
||||
check('new network Window denies domain access', () => securityError(() => window.document));
|
||||
|
||||
loaded = new Promise(resolve => { frame.onload = resolve; });
|
||||
frame.src = 'about:blank';
|
||||
await loaded;
|
||||
const blank = frame.contentDocument;
|
||||
check('new about:blank inherits domain access', () => blank !== null && window.document === blank);
|
||||
check('new about:blank replaces the old Document', () => blank !== original);
|
||||
check('new about:blank inherits domain', () => blank.domain === document.domain);
|
||||
|
||||
loaded = new Promise(resolve => { frame.onload = resolve; });
|
||||
frame.srcdoc = '<!doctype html><body>srcdoc';
|
||||
check('pending srcdoc keeps the current Document', () => window.document === blank);
|
||||
await loaded;
|
||||
check('new srcdoc inherits domain access', () => frame.contentDocument !== null && window.document === frame.contentDocument);
|
||||
check('new srcdoc replaces about:blank', () => frame.contentDocument !== blank);
|
||||
check('new srcdoc inherits domain', () => frame.contentDocument.domain === document.domain);
|
||||
frame.remove();
|
||||
return {checks, failures};
|
||||
}
|
||||
Reference in New Issue
Block a user