mirror of
https://github.com/lexmount/moli.git
synced 2026-10-09 08:01:05 +00:00
fix(history): enforce document URL rewrite rules
This commit is contained in:
@@ -24,6 +24,7 @@ use super::navigation_result::{
|
||||
};
|
||||
use super::navigation_serialize::sync_child_navigation_entry_seed_from_owner;
|
||||
use super::navigation_window::{
|
||||
child_browsing_context_handle_for_runtime_owner,
|
||||
runtime_window_is_global, window_location_for_holder, window_navigation_for_holder,
|
||||
};
|
||||
use super::*;
|
||||
@@ -107,6 +108,7 @@ fn mutate_history_object<'s>(
|
||||
let Some(owner) = require_fully_active_history_owner(scope, history) else {
|
||||
return;
|
||||
};
|
||||
let kind = effective_history_mutation_kind(scope, owner, kind);
|
||||
let Some(state) = structured_clone_value_for_storage(scope, parsed.state) else {
|
||||
return;
|
||||
};
|
||||
@@ -116,9 +118,9 @@ fn mutate_history_object<'s>(
|
||||
};
|
||||
let current_href =
|
||||
location_href_slot(scope, location).unwrap_or_else(|| "about:blank".to_owned());
|
||||
let current_url = match history_same_origin_reference_url(scope, owner, ¤t_href) {
|
||||
Ok(url) => url,
|
||||
Err(_) => {
|
||||
let current_url = match history_document_url(scope, owner, ¤t_href) {
|
||||
Some(url) => url,
|
||||
None => {
|
||||
throw_history_security_error(
|
||||
scope,
|
||||
"Failed to execute 'pushState' or 'replaceState' on 'History': The current URL is invalid.",
|
||||
@@ -126,13 +128,9 @@ fn mutate_history_object<'s>(
|
||||
return;
|
||||
}
|
||||
};
|
||||
let resolve_base_href = if history_url_inherits_origin(scope, owner, ¤t_href) {
|
||||
current_url.as_str()
|
||||
} else {
|
||||
¤t_href
|
||||
};
|
||||
let api_base_url = history_api_base_url(scope, owner, ¤t_url);
|
||||
let url = match parsed.url {
|
||||
Some(target) if !target.is_empty() => resolve_history_state_url(resolve_base_href, &target),
|
||||
Some(target) if !target.is_empty() => resolve_history_state_url(&api_base_url, &target),
|
||||
_ => Some(current_url.clone()),
|
||||
};
|
||||
let Some(url) = url else {
|
||||
@@ -142,7 +140,7 @@ fn mutate_history_object<'s>(
|
||||
);
|
||||
return;
|
||||
};
|
||||
if !moli_url::same_origin(&url, ¤t_url) {
|
||||
if !document_can_have_url_rewritten(¤t_url, &url) {
|
||||
throw_history_security_error(
|
||||
scope,
|
||||
"Failed to execute 'pushState' or 'replaceState' on 'History': A history state object with URL of a different origin cannot be created in a document with origin.",
|
||||
@@ -305,18 +303,16 @@ fn mutate_history_object<'s>(
|
||||
} else if let Some(popup_id) =
|
||||
crate::native_bridge::lightweight_popup_id_from_window(scope, owner)
|
||||
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
|
||||
&& let Some(document_handle) =
|
||||
unsafe { &*host_ptr }.lightweight_popup_document_handle(popup_id)
|
||||
{
|
||||
let _ = unsafe { &mut *host_ptr }.set_dom_document_url_for_handle(document_handle, url);
|
||||
let _ = unsafe { &mut *host_ptr }.set_lightweight_popup_same_document_url(popup_id, url);
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_history_state_url(base_href: &str, target: &str) -> Option<url::Url> {
|
||||
fn resolve_history_state_url(base_url: &url::Url, target: &str) -> Option<url::Url> {
|
||||
if let Ok(absolute) = url::Url::parse(target) {
|
||||
return Some(absolute);
|
||||
}
|
||||
url::Url::parse(base_href).ok()?.join(target).ok()
|
||||
base_url.join(target).ok()
|
||||
}
|
||||
|
||||
fn parse_history_mutation_args<'s>(
|
||||
@@ -344,28 +340,155 @@ fn parse_history_mutation_args<'s>(
|
||||
}
|
||||
}
|
||||
|
||||
fn history_same_origin_reference_url<'s>(
|
||||
fn history_api_base_url<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
current_href: &str,
|
||||
) -> Result<url::Url, url::ParseError> {
|
||||
if history_url_inherits_origin(scope, owner, current_href)
|
||||
&& let Some(host_ptr) = context_host_ptr_from_global_bridge(scope)
|
||||
{
|
||||
return Ok(unsafe { &mut *host_ptr }.host_document().url().clone());
|
||||
current_url: &url::Url,
|
||||
) -> url::Url {
|
||||
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
|
||||
return current_url.clone();
|
||||
};
|
||||
let host = unsafe { &*host_ptr };
|
||||
if runtime_window_is_global(scope, owner) {
|
||||
return host
|
||||
.dom_host()
|
||||
.document_base_url()
|
||||
.unwrap_or_else(|| current_url.clone());
|
||||
}
|
||||
url::Url::parse(current_href)
|
||||
if let Some(handle) = child_browsing_context_handle_for_runtime_owner(scope, owner) {
|
||||
return host
|
||||
.child_browsing_context_base_url(handle)
|
||||
.unwrap_or_else(|| current_url.clone());
|
||||
}
|
||||
if let Some(popup_id) = crate::native_bridge::lightweight_popup_id_from_window(scope, owner) {
|
||||
return host
|
||||
.lightweight_popup_request_base_url(scope, popup_id)
|
||||
.unwrap_or_else(|| current_url.clone());
|
||||
}
|
||||
current_url.clone()
|
||||
}
|
||||
|
||||
fn history_url_inherits_origin<'s>(
|
||||
fn history_document_url<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
current_href: &str,
|
||||
) -> bool {
|
||||
!runtime_window_is_global(scope, owner)
|
||||
&& matches!(current_href, "about:blank" | "about:srcdoc")
|
||||
) -> Option<url::Url> {
|
||||
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
|
||||
return url::Url::parse(current_href).ok();
|
||||
};
|
||||
let host = unsafe { &*host_ptr };
|
||||
if runtime_window_is_global(scope, owner) {
|
||||
return Some(host.document_url().clone());
|
||||
}
|
||||
if let Some(handle) = child_browsing_context_handle_for_runtime_owner(scope, owner) {
|
||||
return host.child_browsing_context_current_url(handle);
|
||||
}
|
||||
if let Some(popup_id) = crate::native_bridge::lightweight_popup_id_from_window(scope, owner) {
|
||||
if host.lightweight_popup_current_document_is_initial_empty(popup_id) {
|
||||
return url::Url::parse("about:blank").ok();
|
||||
}
|
||||
return host.lightweight_popup_document_url(popup_id);
|
||||
}
|
||||
url::Url::parse(current_href).ok()
|
||||
}
|
||||
|
||||
fn effective_history_mutation_kind<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
kind: HistoryMutationKind,
|
||||
) -> HistoryMutationKind {
|
||||
if !matches!(kind, HistoryMutationKind::Push) {
|
||||
return kind;
|
||||
}
|
||||
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
|
||||
return kind;
|
||||
};
|
||||
let host = unsafe { &*host_ptr };
|
||||
let is_initial_empty = child_browsing_context_handle_for_runtime_owner(scope, owner)
|
||||
.is_some_and(|handle| host.child_browsing_context_is_on_initial_about_blank_entry(handle))
|
||||
|| crate::native_bridge::lightweight_popup_id_from_window(scope, owner).is_some_and(
|
||||
|popup_id| host.lightweight_popup_current_document_is_initial_empty(popup_id),
|
||||
);
|
||||
if is_initial_empty {
|
||||
HistoryMutationKind::Replace
|
||||
} else {
|
||||
kind
|
||||
}
|
||||
}
|
||||
|
||||
fn document_can_have_url_rewritten(document_url: &url::Url, target_url: &url::Url) -> bool {
|
||||
if document_url.scheme() != target_url.scheme()
|
||||
|| document_url.username() != target_url.username()
|
||||
|| document_url.password() != target_url.password()
|
||||
|| document_url.host() != target_url.host()
|
||||
|| document_url.port() != target_url.port()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if matches!(target_url.scheme(), "http" | "https") {
|
||||
return true;
|
||||
}
|
||||
if target_url.scheme() == "file" {
|
||||
return document_url.path() == target_url.path();
|
||||
}
|
||||
document_url.path() == target_url.path() && document_url.query() == target_url.query()
|
||||
}
|
||||
|
||||
fn throw_history_security_error(scope: &mut v8::PinScope<'_, '_>, message: &str) {
|
||||
crate::context_bootstrap::throw_dom_exception_value(scope, message, "SecurityError");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::document_can_have_url_rewritten;
|
||||
use url::Url;
|
||||
|
||||
fn url(value: &str) -> Url {
|
||||
Url::parse(value).expect("test URL should parse")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_url_rewrite_rules_match_url_component_boundaries() {
|
||||
let cases = [
|
||||
(
|
||||
"https://example.test/home",
|
||||
"https://example.test/shop",
|
||||
true,
|
||||
),
|
||||
(
|
||||
"https://example.test/home",
|
||||
"https://user:pass@example.test/home",
|
||||
false,
|
||||
),
|
||||
("file:///path/to/x", "file:///path/to/x?search", true),
|
||||
("file:///path/to/x", "file:///path/to/y", false),
|
||||
("about:blank", "about:blank#hash", true),
|
||||
("about:blank", "about:blank?search", false),
|
||||
("about:blank", "about:srcdoc", false),
|
||||
("data:text/html,body", "data:text/html,body#hash", true),
|
||||
("data:text/html,body", "data:text/html,body?search", false),
|
||||
(
|
||||
"blob:https://example.test/00000000-0000-0000-0000-000000000001",
|
||||
"blob:https://example.test/00000000-0000-0000-0000-000000000001#hash",
|
||||
true,
|
||||
),
|
||||
(
|
||||
"blob:https://example.test/00000000-0000-0000-0000-000000000001",
|
||||
"blob:https://example.test/00000000-0000-0000-0000-000000000001?search",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"blob:https://example.test/00000000-0000-0000-0000-000000000001",
|
||||
"blob:https://example.test/00000000-0000-0000-0000-000000000002",
|
||||
false,
|
||||
),
|
||||
];
|
||||
for (document, target, expected) in cases {
|
||||
assert_eq!(
|
||||
document_can_have_url_rewritten(&url(document), &url(target)),
|
||||
expected,
|
||||
"rewrite result for {document} -> {target}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +217,8 @@ fn entry_origin_url<'s>(
|
||||
}
|
||||
|
||||
fn child_navigation_entry_url_inherits_origin(url: &url::Url) -> bool {
|
||||
url.scheme() == "about" && matches!(url.as_str(), "about:blank" | "about:srcdoc")
|
||||
url.scheme() == "about"
|
||||
&& (url.path().eq_ignore_ascii_case("blank") || url.path().eq_ignore_ascii_case("srcdoc"))
|
||||
}
|
||||
|
||||
pub(super) fn set_history_length_from_visible_entries<'s>(
|
||||
@@ -278,3 +279,26 @@ fn history_length_floor_from_visible_entries<'s>(
|
||||
.max(0.0);
|
||||
visible_length.max(top_length)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::child_navigation_entry_url_inherits_origin;
|
||||
use url::Url;
|
||||
|
||||
#[test]
|
||||
fn about_document_history_urls_inherit_origin_with_fragments() {
|
||||
for raw_url in [
|
||||
"about:blank",
|
||||
"about:blank#history",
|
||||
"about:srcdoc",
|
||||
"about:srcdoc#history",
|
||||
] {
|
||||
assert!(child_navigation_entry_url_inherits_origin(
|
||||
&Url::parse(raw_url).expect("about URL should parse")
|
||||
));
|
||||
}
|
||||
assert!(!child_navigation_entry_url_inherits_origin(
|
||||
&Url::parse("about:other#history").expect("about URL should parse")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,8 +728,9 @@ impl ChildBrowsingContextEntry {
|
||||
self.navigation_entry_seed = entry_seed.clone();
|
||||
if same_document_update {
|
||||
self.committed_navigation_entry_seed = entry_seed;
|
||||
self.pending_attribute_bootstrap_commit = false;
|
||||
self.pending_live_navigation_reflects_window_state = false;
|
||||
if !self.pending_attribute_bootstrap_commit {
|
||||
self.pending_live_navigation_reflects_window_state = false;
|
||||
}
|
||||
}
|
||||
same_document_update
|
||||
}
|
||||
|
||||
@@ -386,6 +386,7 @@ pub(super) struct LightweightPopupDocumentState {
|
||||
struct LightweightPopupDocumentRecord {
|
||||
owner: LightweightPopupDocumentOwner,
|
||||
local_window_id: LightweightPopupLocalWindowId,
|
||||
is_initial_empty_document: bool,
|
||||
url: Url,
|
||||
access_origin: super::window_security_tokens::WindowAccessOrigin,
|
||||
state: LightweightPopupDocumentState,
|
||||
@@ -685,15 +686,25 @@ impl JsContextHost {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_lightweight_popup_same_document_url(&mut self, popup_id: u64, url: Url) -> bool {
|
||||
let Some(record) = self.lightweight_popup_record_mut(popup_id) else {
|
||||
return false;
|
||||
pub(crate) fn set_lightweight_popup_same_document_url(
|
||||
&mut self,
|
||||
popup_id: u64,
|
||||
url: Url,
|
||||
) -> bool {
|
||||
let document_handle = {
|
||||
let Some(record) = self.lightweight_popup_record_mut(popup_id) else {
|
||||
return false;
|
||||
};
|
||||
let LightweightPopupLifecycle::Open(open) = &mut record.lifecycle else {
|
||||
return false;
|
||||
};
|
||||
record.location_url = url.clone();
|
||||
open.document.url = url.clone();
|
||||
open.document.handle
|
||||
};
|
||||
let LightweightPopupLifecycle::Open(open) = &mut record.lifecycle else {
|
||||
return false;
|
||||
};
|
||||
record.location_url = url.clone();
|
||||
open.document.url = url;
|
||||
if let Some(document_handle) = document_handle {
|
||||
let _ = self.set_dom_document_url_for_handle(document_handle, url);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -933,6 +944,7 @@ impl JsContextHost {
|
||||
document: LightweightPopupDocumentRecord {
|
||||
owner: initial_document_owner,
|
||||
local_window_id: initial_local_window_id,
|
||||
is_initial_empty_document: true,
|
||||
url: initial_url.clone(),
|
||||
access_origin: initial_origin,
|
||||
state: initial_document_state.clone(),
|
||||
@@ -1081,7 +1093,6 @@ impl JsContextHost {
|
||||
window,
|
||||
&target_url,
|
||||
crate::context_bootstrap::LocationNavigationKind::Assign,
|
||||
Some(&previous_url),
|
||||
);
|
||||
let queue_synthetic_load = if moli_url::is_about_blank(&target_url) {
|
||||
let storage_scope = self.lightweight_popup_storage_scope_for_initiated_navigation(
|
||||
@@ -1228,6 +1239,14 @@ impl JsContextHost {
|
||||
.map(|document| document.url.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn lightweight_popup_current_document_is_initial_empty(
|
||||
&self,
|
||||
popup_id: u64,
|
||||
) -> bool {
|
||||
self.lightweight_popup_document_record(popup_id)
|
||||
.is_some_and(|document| document.is_initial_empty_document)
|
||||
}
|
||||
|
||||
pub(crate) fn lightweight_popup_session_storage_store(
|
||||
&self,
|
||||
popup_id: u64,
|
||||
@@ -1495,9 +1514,7 @@ impl JsContextHost {
|
||||
window: v8::Local<'s, v8::Object>,
|
||||
target_url: &Url,
|
||||
kind: crate::context_bootstrap::LocationNavigationKind,
|
||||
current_url: Option<&Url>,
|
||||
) {
|
||||
let effective_kind = lightweight_popup_effective_navigation_kind(current_url, kind);
|
||||
let base_url = self
|
||||
.lightweight_popup_base_url(scope, popup_id)
|
||||
.unwrap_or_else(|| target_url.clone());
|
||||
@@ -1512,7 +1529,7 @@ impl JsContextHost {
|
||||
&base_url,
|
||||
&document_referrer,
|
||||
);
|
||||
apply_local_window_location_navigation(scope, window, target_url, effective_kind);
|
||||
apply_local_window_location_navigation(scope, window, target_url, kind);
|
||||
sync_window_location_history_navigation_runtime_surface(scope, window);
|
||||
}
|
||||
|
||||
@@ -1566,6 +1583,10 @@ impl JsContextHost {
|
||||
let Some(window) = self.lightweight_popup_window(scope, popup_id) else {
|
||||
return false;
|
||||
};
|
||||
let kind = lightweight_popup_effective_navigation_kind(
|
||||
self.lightweight_popup_current_document_is_initial_empty(popup_id),
|
||||
kind,
|
||||
);
|
||||
let current_url = lightweight_popup_location_href(scope, window)
|
||||
.or_else(|| self.lightweight_popup_location_url(popup_id));
|
||||
if !matches!(
|
||||
@@ -1696,7 +1717,6 @@ impl JsContextHost {
|
||||
window,
|
||||
&target_url,
|
||||
kind,
|
||||
current_url.as_ref(),
|
||||
);
|
||||
self.install_lightweight_popup_empty_document(scope, popup_id, window, target_url);
|
||||
self.queue_lightweight_popup_load_event(navigation_task);
|
||||
@@ -1709,7 +1729,6 @@ impl JsContextHost {
|
||||
window,
|
||||
&target_url,
|
||||
kind,
|
||||
current_url.as_ref(),
|
||||
);
|
||||
if self
|
||||
.start_lightweight_popup_document_load(
|
||||
@@ -1951,6 +1970,7 @@ impl JsContextHost {
|
||||
LightweightPopupDocumentRecord {
|
||||
owner: commit.owner,
|
||||
local_window_id: current_local_window_id,
|
||||
is_initial_empty_document: false,
|
||||
url: commit.location_url.clone(),
|
||||
access_origin,
|
||||
state: commit.state,
|
||||
@@ -4926,13 +4946,13 @@ fn set_lightweight_popup_persisted_script_global<'s>(
|
||||
}
|
||||
|
||||
fn lightweight_popup_effective_navigation_kind(
|
||||
current_url: Option<&Url>,
|
||||
current_document_is_initial_empty: bool,
|
||||
kind: crate::context_bootstrap::LocationNavigationKind,
|
||||
) -> crate::context_bootstrap::LocationNavigationKind {
|
||||
if matches!(
|
||||
kind,
|
||||
crate::context_bootstrap::LocationNavigationKind::Assign
|
||||
) && current_url.is_some_and(moli_url::is_about_blank)
|
||||
) && current_document_is_initial_empty
|
||||
{
|
||||
return crate::context_bootstrap::LocationNavigationKind::Replace;
|
||||
}
|
||||
|
||||
@@ -398,10 +398,17 @@ async fn hashchange_discards_a_retired_child_local_window() {
|
||||
r#"
|
||||
const frame = document.createElement("iframe");
|
||||
frame.id = "stale-hashchange-recipient";
|
||||
frame.srcdoc = "<!doctype html><p>child</p>";
|
||||
document.body.appendChild(frame);
|
||||
"created"
|
||||
"#,
|
||||
)?;
|
||||
run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
|
||||
&mut page_vm,
|
||||
ChildFrameSemanticTurnKind::NavigationCommit,
|
||||
"stale hashchange child srcdoc commit",
|
||||
)
|
||||
.await;
|
||||
materialize_child_realm_through_page_turn_for_test(
|
||||
&mut page_vm,
|
||||
"stale-hashchange-recipient",
|
||||
@@ -411,7 +418,7 @@ document.body.appendChild(frame);
|
||||
globalThis.__staleHashChanges = 0;
|
||||
const staleFrame = document.getElementById("stale-hashchange-recipient");
|
||||
staleFrame.contentWindow.addEventListener("hashchange", () => parent.__staleHashChanges++);
|
||||
staleFrame.contentWindow.history.replaceState(null, "", "/child-hashchange");
|
||||
staleFrame.contentWindow.history.replaceState(null, "", "about:srcdoc#child-hashchange");
|
||||
staleFrame.contentWindow.location.hash = "#queued";
|
||||
staleFrame.remove();
|
||||
"retired"
|
||||
@@ -447,7 +454,7 @@ async fn hashchange_discards_a_retired_lightweight_popup_local_window() {
|
||||
r##"
|
||||
globalThis.__popupHashChanges = [];
|
||||
globalThis.__hashPopup = open("about:blank", "hashchange-owner-popup");
|
||||
__hashPopup.history.replaceState(null, "", "/popup-hashchange");
|
||||
__hashPopup.history.replaceState(null, "", "about:blank#popup-hashchange");
|
||||
__hashPopup.addEventListener("hashchange", () => __popupHashChanges.push("retired"));
|
||||
__hashPopup.location.hash = "#queued-before-replacement";
|
||||
open("about:blank", "hashchange-owner-popup");
|
||||
@@ -479,7 +486,7 @@ open("about:blank", "hashchange-owner-popup");
|
||||
|
||||
page_vm.vm_mut().eval(
|
||||
r##"
|
||||
__hashPopup.history.replaceState(null, "", "/replacement-popup-hashchange");
|
||||
__hashPopup.history.replaceState(null, "", "about:blank#replacement-popup-hashchange");
|
||||
__hashPopup.addEventListener("hashchange", () => __popupHashChanges.push("current"));
|
||||
__hashPopup.location.hash = "#queued-after-replacement";
|
||||
"queued-current"
|
||||
|
||||
@@ -313,10 +313,17 @@ async fn history_traversal_discards_a_retired_child_local_window() {
|
||||
r##"
|
||||
const frame = document.createElement("iframe");
|
||||
frame.id = "history-stale-child";
|
||||
frame.srcdoc = "<!doctype html><p>child</p>";
|
||||
document.body.appendChild(frame);
|
||||
"created"
|
||||
"##,
|
||||
)?;
|
||||
run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
|
||||
&mut page_vm,
|
||||
ChildFrameSemanticTurnKind::NavigationCommit,
|
||||
"history stale-child srcdoc commit",
|
||||
)
|
||||
.await;
|
||||
materialize_child_realm_through_page_turn_for_test(&mut page_vm, "history-stale-child")?;
|
||||
page_vm.vm_mut().eval(
|
||||
r##"
|
||||
@@ -325,7 +332,7 @@ const child = document.getElementById("history-stale-child").contentWindow;
|
||||
child.addEventListener("popstate", () => {
|
||||
parent.__retiredChildHistoryEvents += 1;
|
||||
});
|
||||
child.history.pushState(null, "", "#queued");
|
||||
child.history.pushState(null, "", "about:srcdoc#queued");
|
||||
child.history.back();
|
||||
document.getElementById("history-stale-child").remove();
|
||||
"retired"
|
||||
|
||||
@@ -26279,6 +26279,39 @@ JSON.stringify({
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_open_named_reuse_before_first_commit_pushes_history() {
|
||||
let mut vm = new_storage_test_vm("https://example.com/base/page.html");
|
||||
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const firstUrl = URL.createObjectURL(
|
||||
new Blob(['<!doctype html><p>first</p>'], { type: 'text/html' })
|
||||
);
|
||||
const secondUrl = URL.createObjectURL(
|
||||
new Blob(['<!doctype html><p>second</p>'], { type: 'text/html' })
|
||||
);
|
||||
const popup = open(firstUrl, 'pendingNamedPopup');
|
||||
const reopened = open(secondUrl, 'pendingNamedPopup');
|
||||
const result = [
|
||||
popup === reopened,
|
||||
reopened.location.href === secondUrl,
|
||||
reopened.history.length,
|
||||
].join('|');
|
||||
popup.close();
|
||||
URL.revokeObjectURL(firstUrl);
|
||||
URL.revokeObjectURL(secondUrl);
|
||||
return result;
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("pending named popup reuse should evaluate"),
|
||||
"true|true|2"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lightweight_popup_cross_document_navigation_clears_old_onload_handler() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
||||
@@ -27580,6 +27613,32 @@ async fn window_open_204_popup_ignores_navigation_and_preserves_initial_empty_hi
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_open_initial_empty_history_push_is_a_replacement() {
|
||||
let mut vm = new_storage_test_vm("https://initial-popup-history.test/page.html");
|
||||
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const popup = open();
|
||||
popup.history.pushState({ step: 1 }, '', 'about:blank#pushed');
|
||||
popup.history.replaceState({ step: 2 }, '', 'about:blank#replaced');
|
||||
const result = [
|
||||
popup.location.href,
|
||||
popup.history.state.step,
|
||||
popup.history.length,
|
||||
].join('|');
|
||||
popup.close();
|
||||
return result;
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("initial empty popup history mutation should evaluate"),
|
||||
"about:blank#replaced|2|1"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn window_open_without_url_replaces_initial_empty_history_on_first_navigation() {
|
||||
let (origin, server) = spawn_lightweight_popup_relative_navigation_server().await;
|
||||
|
||||
@@ -4192,7 +4192,7 @@ async fn reset_navigation_history_updates_all_live_window_realms() {
|
||||
.clone();
|
||||
vm.eval_in_child_default_context(
|
||||
child_context_id,
|
||||
r##"history.pushState({ realm: "child-default" }, "", "#child-default")"##,
|
||||
r##"history.pushState({ realm: "child-default" }, "", "about:srcdoc#child-default")"##,
|
||||
)
|
||||
.expect("child default history setup should evaluate");
|
||||
|
||||
@@ -4209,7 +4209,7 @@ async fn reset_navigation_history_updates_all_live_window_realms() {
|
||||
.expect("top isolated history setup should evaluate");
|
||||
vm.eval_in_isolated_context(
|
||||
child_isolated_context_id,
|
||||
r##"history.pushState({ realm: "child-isolated" }, "", "#child-isolated")"##,
|
||||
r##"history.pushState({ realm: "child-isolated" }, "", "about:srcdoc#child-isolated")"##,
|
||||
)
|
||||
.expect("child isolated history setup should evaluate");
|
||||
|
||||
@@ -4347,7 +4347,11 @@ async fn reset_navigation_history_preserves_child_entry_created_by_top_dispose_l
|
||||
child_context_id,
|
||||
r##"
|
||||
(() => {
|
||||
history.pushState({ realm: "child-before-reset" }, "", "#child-before-reset");
|
||||
history.pushState(
|
||||
{ realm: "child-before-reset" },
|
||||
"",
|
||||
"about:srcdoc#child-before-reset"
|
||||
);
|
||||
globalThis.__lmChildCurrentBeforeReset = navigation.currentEntry;
|
||||
globalThis.__lmChildEntriesBeforeReset = navigation.entries();
|
||||
globalThis.__lmChildDisposed = [];
|
||||
@@ -4364,7 +4368,7 @@ navigation.entries()[0].addEventListener("dispose", () => {
|
||||
document.querySelector("iframe").contentWindow.history.pushState(
|
||||
{ realm: "child-during-top-dispose" },
|
||||
"",
|
||||
"#child-during-top-dispose"
|
||||
"about:srcdoc#child-during-top-dispose"
|
||||
);
|
||||
});
|
||||
"##,
|
||||
|
||||
@@ -6709,7 +6709,7 @@ async fn element_matches_delegates_loaded_child_document_elements() {
|
||||
r##"
|
||||
(() => {
|
||||
const doc = document.querySelector("iframe").contentDocument;
|
||||
doc.defaultView.history.replaceState(null, "", "#target");
|
||||
doc.defaultView.history.replaceState(null, "", "about:srcdoc#target");
|
||||
const code = doc.getElementById("code");
|
||||
return [
|
||||
code.ownerDocument === doc,
|
||||
@@ -11000,9 +11000,9 @@ const frame = document.createElement('iframe');
|
||||
frame.name = 'target';
|
||||
const root = document.body || document.documentElement || document;
|
||||
root.appendChild(frame);
|
||||
frame.contentWindow.history.pushState(null, '', '/child.html');
|
||||
frame.contentWindow.history.replaceState(null, '', 'about:blank#child');
|
||||
const link = document.createElement('a');
|
||||
link.href = '/child.html#next';
|
||||
link.href = 'about:blank#next';
|
||||
link.target = 'target';
|
||||
root.appendChild(link);
|
||||
let seen = [];
|
||||
@@ -11020,10 +11020,7 @@ seen.join('|')
|
||||
)
|
||||
.expect("targeted same-document anchor click should dispatch child navigate");
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
"true,true,https://targeted-child-hash.test/child.html#next,-1"
|
||||
);
|
||||
assert_eq!(result, "true,true,about:blank#next,-1");
|
||||
}
|
||||
#[test]
|
||||
fn anchor_click_to_identical_url_dispatches_replace_navigate_event() {
|
||||
@@ -11933,7 +11930,7 @@ globalThis.__firstJointLengthFrame = first;
|
||||
(() => {
|
||||
const child = __firstJointLengthFrame.contentWindow;
|
||||
const before = history.length;
|
||||
child.history.pushState(null, '', '#first');
|
||||
child.history.pushState(null, '', 'about:srcdoc#first');
|
||||
return [before, history.length, child.history.length].join('|');
|
||||
})()
|
||||
"#,
|
||||
@@ -11960,7 +11957,7 @@ globalThis.__secondJointLengthFrame = second;
|
||||
(() => {
|
||||
const child = __secondJointLengthFrame.contentWindow;
|
||||
const before = history.length;
|
||||
child.history.pushState(null, '', '#second');
|
||||
child.history.pushState(null, '', 'about:srcdoc#second');
|
||||
return [before, history.length, child.history.length].join('|');
|
||||
})()
|
||||
"#,
|
||||
@@ -11970,6 +11967,65 @@ globalThis.__secondJointLengthFrame = second;
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_empty_child_history_can_rewrite_about_blank_fragment() {
|
||||
let mut vm = new_storage_test_vm("https://initial-empty-history.test/page.html");
|
||||
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const frame = document.createElement('iframe');
|
||||
(document.body || document.documentElement || document).appendChild(frame);
|
||||
const child = frame.contentWindow;
|
||||
const before = history.length;
|
||||
child.history.pushState({ step: 1 }, '', 'about:blank#pushed');
|
||||
child.history.replaceState({ step: 2 }, '', 'about:blank#replaced');
|
||||
return [
|
||||
child.location.href,
|
||||
child.history.state.step,
|
||||
history.length,
|
||||
before,
|
||||
].join('|');
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("initial empty child history mutation should evaluate"),
|
||||
"about:blank#replaced|2|1|1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_empty_child_history_mutation_preserves_pending_srcdoc_navigation() {
|
||||
let mut vm = new_storage_test_vm("https://pending-child-history.test/page.html");
|
||||
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const frame = document.createElement('iframe');
|
||||
frame.srcdoc = '<p id="committed">child</p>';
|
||||
(document.body || document.documentElement || document).appendChild(frame);
|
||||
frame.contentWindow.history.pushState(null, '', 'about:blank#before-commit');
|
||||
globalThis.__pendingHistoryFrame = frame;
|
||||
return frame.contentWindow.location.href;
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("pending child history mutation should evaluate"),
|
||||
"about:blank#before-commit"
|
||||
);
|
||||
|
||||
vm.drain_pending_child_frame_work_for_test();
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
"[__pendingHistoryFrame.contentWindow.location.href, Boolean(__pendingHistoryFrame.contentDocument.getElementById('committed'))].join('|')"
|
||||
)
|
||||
.expect("pending srcdoc navigation should still commit"),
|
||||
"about:srcdoc|true"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn top_history_back_routes_to_child_joint_history_entry() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
||||
@@ -12004,7 +12060,11 @@ async fn top_history_back_routes_to_child_joint_history_entry() {
|
||||
r#"
|
||||
(() => {
|
||||
const frame = document.querySelector('iframe');
|
||||
frame.contentWindow.history.pushState({ child: true }, '', '#child');
|
||||
frame.contentWindow.history.pushState(
|
||||
{ child: true },
|
||||
'',
|
||||
'about:srcdoc#child'
|
||||
);
|
||||
return [
|
||||
location.href,
|
||||
history.length,
|
||||
@@ -12018,7 +12078,7 @@ async fn top_history_back_routes_to_child_joint_history_entry() {
|
||||
|
||||
assert_eq!(
|
||||
setup,
|
||||
"https://joint-child-back.test/page.html|2|2|https://joint-child-back.test/page.html#child"
|
||||
"https://joint-child-back.test/page.html|2|2|about:srcdoc#child"
|
||||
);
|
||||
let _ = vm
|
||||
.run_one_oldest_ready_page_task_executor_turn(&loader)
|
||||
@@ -12084,7 +12144,11 @@ async fn top_history_back_ignores_removed_child_joint_history_entry() {
|
||||
r#"
|
||||
(() => {
|
||||
const frame = document.querySelector('iframe');
|
||||
frame.contentWindow.history.pushState({ child: true }, '', '#child');
|
||||
frame.contentWindow.history.pushState(
|
||||
{ child: true },
|
||||
'',
|
||||
'about:srcdoc#child'
|
||||
);
|
||||
return [
|
||||
location.href,
|
||||
navigation.entries().length,
|
||||
@@ -12100,7 +12164,7 @@ async fn top_history_back_ignores_removed_child_joint_history_entry() {
|
||||
|
||||
assert_eq!(
|
||||
setup,
|
||||
"https://removed-child-joint-back.test/page.html|1|0|2|2|https://removed-child-joint-back.test/page.html#child"
|
||||
"https://removed-child-joint-back.test/page.html|1|0|2|2|about:srcdoc#child"
|
||||
);
|
||||
|
||||
vm.eval("document.querySelector('iframe').remove(); history.back(); 'queued'")
|
||||
@@ -12208,7 +12272,7 @@ async fn detached_child_navigation_error_exposes_committed_entry_during_dispatch
|
||||
(() => {
|
||||
globalThis.__lmDetachedChildNavigateErrorLog = [];
|
||||
const child = document.querySelector('iframe').contentWindow;
|
||||
child.history.pushState({ child: true }, "", "#one");
|
||||
child.history.pushState({ child: true }, "", "about:srcdoc#one");
|
||||
return [
|
||||
child.navigation.entries().length,
|
||||
child.navigation.currentEntry.index,
|
||||
@@ -12218,10 +12282,7 @@ async fn detached_child_navigation_error_exposes_committed_entry_during_dispatch
|
||||
"##,
|
||||
)
|
||||
.expect("child initial same-document navigation should evaluate");
|
||||
assert_eq!(
|
||||
initial,
|
||||
"2|1|https://child-detach-navigation-error.test/page.html#one"
|
||||
);
|
||||
assert_eq!(initial, "2|1|about:srcdoc#one");
|
||||
|
||||
let setup = vm
|
||||
.eval(
|
||||
|
||||
@@ -9823,7 +9823,7 @@ fn child_frame_target_selector_invalidation_uses_child_document_world() {
|
||||
</body>
|
||||
`);
|
||||
childDocument.close();
|
||||
childWindow.history.replaceState(null, '', '#old');
|
||||
childWindow.history.replaceState(null, '', 'about:blank#old');
|
||||
globalThis.__childTargetFrame = frame;
|
||||
globalThis.__childTargetOldStyle =
|
||||
childWindow.getComputedStyle(childDocument.getElementById('old'));
|
||||
@@ -9855,7 +9855,7 @@ fn child_frame_target_selector_invalidation_uses_child_document_world() {
|
||||
r#"
|
||||
(() => {
|
||||
const childWindow = globalThis.__childTargetFrame.contentWindow;
|
||||
childWindow.history.replaceState(null, '', '#new');
|
||||
childWindow.history.replaceState(null, '', 'about:blank#new');
|
||||
const result = [
|
||||
globalThis.__childTargetOldStyle.color,
|
||||
globalThis.__childTargetNewStyle.color
|
||||
@@ -9923,7 +9923,7 @@ fn popup_target_selector_invalidation_uses_popup_document_world() {
|
||||
newTarget.id = 'popup-new-target';
|
||||
newTarget.className = 'probe';
|
||||
popupBody.append(oldTarget, newTarget);
|
||||
popup.history.replaceState(null, '', '#popup-old-target');
|
||||
popup.history.replaceState(null, '', 'about:blank#popup-old-target');
|
||||
globalThis.__popupTargetOldStyle = popup.getComputedStyle(oldTarget);
|
||||
globalThis.__popupTargetNewStyle = popup.getComputedStyle(newTarget);
|
||||
|
||||
@@ -9952,7 +9952,7 @@ fn popup_target_selector_invalidation_uses_popup_document_world() {
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
__popupTargetWindow.history.replaceState(null, '', '#popup-new-target');
|
||||
__popupTargetWindow.history.replaceState(null, '', 'about:blank#popup-new-target');
|
||||
const result = [
|
||||
globalThis.__popupTargetOldStyle.color,
|
||||
globalThis.__popupTargetNewStyle.color
|
||||
|
||||
@@ -9614,7 +9614,7 @@ fn dom_parser_uses_its_constructor_realm_associated_document() {
|
||||
const frame = document.createElement('iframe');
|
||||
(document.body || document.documentElement || document).appendChild(frame);
|
||||
const child = frame.contentWindow;
|
||||
child.history.replaceState(null, '', '/child-v1.html');
|
||||
child.history.replaceState(null, '', 'about:blank#child-v1');
|
||||
|
||||
const topParser = new DOMParser();
|
||||
const childParser = new child.DOMParser();
|
||||
@@ -9635,7 +9635,7 @@ fn dom_parser_uses_its_constructor_realm_associated_document() {
|
||||
'text/html'
|
||||
).URL;
|
||||
const beforeSameDocumentUpdate = parseUrl(childParser);
|
||||
child.history.replaceState(null, '', '/child-v2.html');
|
||||
child.history.replaceState(null, '', 'about:blank#child-v2');
|
||||
|
||||
let invalidReceiver;
|
||||
try {
|
||||
@@ -9664,7 +9664,7 @@ fn dom_parser_uses_its_constructor_realm_associated_document() {
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"top":"https://dom-parser-realm.test/top.html","child":"https://dom-parser-realm.test/child-v2.html","childWithTopNewTarget":"https://dom-parser-realm.test/child-v2.html","topWithChildNewTarget":"https://dom-parser-realm.test/top.html","beforeSameDocumentUpdate":"https://dom-parser-realm.test/child-v1.html","invalidReceiver":"TypeError"}"#
|
||||
r#"{"top":"https://dom-parser-realm.test/top.html","child":"about:blank#child-v2","childWithTopNewTarget":"about:blank#child-v2","topWithChildNewTarget":"https://dom-parser-realm.test/top.html","beforeSameDocumentUpdate":"about:blank#child-v1","invalidReceiver":"TypeError"}"#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9679,7 +9679,7 @@ fn dom_parser_non_object_new_target_prototype_uses_new_target_realm_default() {
|
||||
const frame = document.createElement('iframe');
|
||||
(document.body || document.documentElement || document).appendChild(frame);
|
||||
const child = frame.contentWindow;
|
||||
child.history.replaceState(null, '', '/child.html');
|
||||
child.history.replaceState(null, '', 'about:blank#child');
|
||||
|
||||
const TopBad = new Function();
|
||||
TopBad.prototype = 7;
|
||||
|
||||
Reference in New Issue
Block a user