fix(events): construct trusted history event subclasses

This commit is contained in:
ldm0
2026-09-09 06:52:22 +08:00
parent baaa6a9bf6
commit 60cd2522ed
9 changed files with 162 additions and 14 deletions
@@ -186,6 +186,9 @@ pub(in crate::context_bootstrap) fn build_constructor_template_for_profile<'s>(
ConstructorKind::PopStateEvent => {
build_event_subclass_template(scope, EventSubclassKind::PopStateEvent)
}
ConstructorKind::HashChangeEvent => {
build_event_subclass_template(scope, EventSubclassKind::HashChangeEvent)
}
ConstructorKind::PageTransitionEvent => {
build_event_subclass_template(scope, EventSubclassKind::PageTransitionEvent)
}
@@ -31,6 +31,7 @@ pub(in crate::context_bootstrap) enum EventSubclassKind {
ClipboardEvent = 29,
CapturedMouseEvent = 30,
FontFaceSetLoadEvent = 31,
HashChangeEvent = 32,
}
impl EventSubclassKind {
@@ -67,6 +68,7 @@ impl EventSubclassKind {
29 => Some(Self::ClipboardEvent),
30 => Some(Self::CapturedMouseEvent),
31 => Some(Self::FontFaceSetLoadEvent),
32 => Some(Self::HashChangeEvent),
_ => None,
}
}
@@ -104,6 +106,7 @@ impl EventSubclassKind {
Self::ClipboardEvent => "ClipboardEvent",
Self::CapturedMouseEvent => "CapturedMouseEvent",
Self::FontFaceSetLoadEvent => "FontFaceSetLoadEvent",
Self::HashChangeEvent => "HashChangeEvent",
}
}
}
@@ -163,6 +163,11 @@ fn event_subclass_constructor_callback<'s>(
return;
}
}
EventSubclassKind::HashChangeEvent => {
if !data::initialize_hash_change_event(scope, event, init) {
return;
}
}
EventSubclassKind::PageTransitionEvent => {
data::initialize_page_transition_event(scope, event, init);
}
@@ -195,6 +195,25 @@ struct PopStateEventInitMembers<'s> {
state: Option<v8::Local<'s, v8::Value>>,
}
#[derive(WebApiObject)]
#[webapi(interface = "Object", data_properties, enumerable)]
struct HashChangeEventInitDeclaration {
#[webapi(data_property = "oldURL")]
old_url: String,
#[webapi(data_property = "newURL")]
new_url: String,
}
/// Members are declared in Web IDL lexicographic order.
#[derive(Default, webidl::WebIdlDictionary)]
#[webidl(prefix = "HashChangeEventInit")]
struct HashChangeEventInitMembers {
#[webidl(name = "newURL", default = "", converter = "usv_string")]
new_url: String,
#[webidl(name = "oldURL", default = "", converter = "usv_string")]
old_url: String,
}
#[derive(WebApiObject)]
#[webapi(interface = "Object", data_properties, enumerable)]
struct PageTransitionEventOwnInitDeclaration {
@@ -535,6 +554,29 @@ pub(in crate::context_bootstrap::events::subclasses) fn initialize_pop_state_eve
true
}
pub(in crate::context_bootstrap::events::subclasses) fn initialize_hash_change_event<'s>(
scope: &mut v8::PinScope<'s, '_>,
event: v8::Local<'s, v8::Object>,
init: Option<v8::Local<'s, v8::Object>>,
) -> bool {
let parsed = match init {
Some(init) => {
match webidl::parse_dictionary_object::<HashChangeEventInitMembers>(scope, init) {
Ok(parsed) => parsed,
Err(error) => {
webidl::throw_error(scope, &error);
return false;
}
}
}
None => HashChangeEventInitMembers::default(),
};
HashChangeEventInitDeclaration::new(parsed.old_url, parsed.new_url)
.initialize(scope, event)
.expect("HashChangeEvent init declaration should initialize");
true
}
pub(in crate::context_bootstrap::events::subclasses) fn initialize_page_transition_event<'s>(
scope: &mut v8::PinScope<'s, '_>,
event: v8::Local<'s, v8::Object>,
@@ -573,17 +573,20 @@ pub(super) fn dispatch_popstate_event<'s>(
child_handle: Option<crate::document_runtime::DomHandle>,
state: v8::Local<'s, v8::Value>,
) {
let global = scope.get_current_context().global(scope);
let Some(event_ctor) = global
.get(scope, v8str(scope, "Event").into())
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
let Ok(event_ctor) =
super::exposed_interfaces::ensure_intrinsic_interface_constructor(scope, "PopStateEvent")
else {
return;
};
let Some(event) = event_ctor.new_instance(scope, &[v8str(scope, "popstate").into()]) else {
let init = PopStateEventStateDeclaration::new(state)
.bind(scope)
.expect("PopStateEvent init declaration should bind");
let Some(event) =
event_ctor.new_instance(scope, &[v8str(scope, "popstate").into(), init.into()])
else {
return;
};
let _ = PopStateEventStateDeclaration::new(state).initialize(scope, event);
mark_event_trusted(scope, event);
let runtime = unsafe { &mut *host_ptr };
if let Some(child_handle) = child_handle {
runtime.dispatch_child_window_event(scope, child_handle, "popstate", event);
@@ -603,13 +606,15 @@ pub(crate) fn construct_original_hash_change_event<'s>(
old_url: &str,
new_url: &str,
) -> Option<v8::Local<'s, v8::Object>> {
let global = scope.get_current_context().global(scope);
let event_ctor = global
.get(scope, v8str(scope, "Event").into())
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())?;
let event = event_ctor.new_instance(scope, &[v8str(scope, "hashchange").into()])?;
let _ = HashChangeEventStateDeclaration::new(old_url.to_owned(), new_url.to_owned())
.initialize(scope, event);
let event_ctor =
super::exposed_interfaces::ensure_intrinsic_interface_constructor(scope, "HashChangeEvent")
.ok()?;
let init = HashChangeEventStateDeclaration::new(old_url.to_owned(), new_url.to_owned())
.bind(scope)
.expect("HashChangeEvent init declaration should bind");
let event =
event_ctor.new_instance(scope, &[v8str(scope, "hashchange").into(), init.into()])?;
mark_event_trusted(scope, event);
Some(event)
}
@@ -421,7 +421,7 @@ const CONSTRUCTOR_SPECS_BEFORE_STREAMS: &[ConstructorSpec] = &[
ConstructorSpec {
name: "HashChangeEvent",
parent: Some("Event"),
kind: ConstructorKind::Illegal,
kind: ConstructorKind::HashChangeEvent,
},
ConstructorSpec {
name: "MediaQueryListEvent",
@@ -31,6 +31,7 @@ pub(in crate::context_bootstrap) enum ConstructorKind {
ToggleEvent,
InterestEvent,
PopStateEvent,
HashChangeEvent,
PageTransitionEvent,
DomException,
DomError,
@@ -15,6 +15,52 @@ fn take_next_hash_change_task_for_authorization_test(
Some(task)
}
#[tokio::test(flavor = "current_thread")]
async fn user_agent_hashchange_is_trusted_but_author_constructed_event_is_not() {
run_page_vm_async_test(async move {
let loader =
crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
let document_url = Url::parse("https://example.com/hashchange-event-trust").unwrap();
let (mut page_vm, _resource_source, _owner_wake_rx) =
page_vm_with_bound_task_sources_and_owner_wake(&loader, document_url);
page_vm.vm_mut().eval(
r##"
globalThis.__authorHashChange = new HashChangeEvent("author", {
oldURL: "https://old.example/",
newURL: "https://new.example/"
});
globalThis.__hashChangeTrust = null;
addEventListener("hashchange", event => {
__hashChangeTrust = [event.isTrusted, event instanceof HashChangeEvent];
}, { once: true });
location.hash = "#trusted";
"queued"
"##,
)?;
assert!(
page_vm
.run_exact_selected_page_task_for_test(
PageSelectedTaskTestSelector::DomManipulation(
PageDomManipulationTestFamily::HashChange
),
&loader,
)
.await?,
"one exact hashchange task should dispatch"
);
assert_eq!(
page_vm
.vm_mut()
.eval("JSON.stringify([...__hashChangeTrust, __authorHashChange instanceof HashChangeEvent, __authorHashChange.isTrusted, __authorHashChange.oldURL, __authorHashChange.newURL])")?,
r#"[true,true,true,false,"https://old.example/","https://new.example/"]"#
);
Ok::<_, anyhow::Error>(())
})
.await
.expect("hashchange trust test should run");
}
#[tokio::test(flavor = "current_thread")]
async fn hashchange_body_leaves_reactions_for_selected_callback_completion() {
run_page_vm_async_test(async move {
@@ -5,6 +5,49 @@ use crate::{
runtime::{IntoPageTaskCompletion, PageTaskCompletion},
};
#[tokio::test(flavor = "current_thread")]
async fn user_agent_popstate_is_trusted_but_author_constructed_event_is_not() {
run_page_vm_async_test(async move {
let loader =
crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
let document_url = Url::parse("https://example.com/popstate-event-trust").unwrap();
let (mut page_vm, _resource_source, _owner_wake_rx) =
page_vm_with_bound_task_sources_and_owner_wake(&loader, document_url);
page_vm.vm_mut().eval(
r##"
history.pushState(null, "", "#one");
globalThis.__popStateTrust = null;
addEventListener("popstate", event => {
__popStateTrust = JSON.stringify([
event.isTrusted,
event instanceof PopStateEvent,
new PopStateEvent("popstate").isTrusted
]);
}, { once: true });
history.back();
"queued"
"##,
)?;
assert!(
page_vm
.run_exact_selected_page_task_for_test(
PageSelectedTaskTestSelector::HistoryTraversal,
&loader
)
.await?,
"one exact history traversal should dispatch popstate"
);
assert_eq!(
page_vm.vm_mut().eval("__popStateTrust")?,
"[true,true,false]"
);
Ok::<_, anyhow::Error>(())
})
.await
.expect("popstate trust test should run");
}
#[tokio::test(flavor = "current_thread")]
async fn history_traversal_body_leaves_reaction_for_selected_completion() {
run_page_vm_async_test(async move {