diff --git a/moli-renderer-v8/src/context_bootstrap/assets/constructor_templates.rs b/moli-renderer-v8/src/context_bootstrap/assets/constructor_templates.rs index 506e5f2c07..22896b6298 100644 --- a/moli-renderer-v8/src/context_bootstrap/assets/constructor_templates.rs +++ b/moli-renderer-v8/src/context_bootstrap/assets/constructor_templates.rs @@ -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) } diff --git a/moli-renderer-v8/src/context_bootstrap/events/kind.rs b/moli-renderer-v8/src/context_bootstrap/events/kind.rs index 36368ffd6b..2bb25af341 100644 --- a/moli-renderer-v8/src/context_bootstrap/events/kind.rs +++ b/moli-renderer-v8/src/context_bootstrap/events/kind.rs @@ -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", } } } diff --git a/moli-renderer-v8/src/context_bootstrap/events/subclasses/constructor.rs b/moli-renderer-v8/src/context_bootstrap/events/subclasses/constructor.rs index c6d759b1bf..aa33ae8e2d 100644 --- a/moli-renderer-v8/src/context_bootstrap/events/subclasses/constructor.rs +++ b/moli-renderer-v8/src/context_bootstrap/events/subclasses/constructor.rs @@ -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); } diff --git a/moli-renderer-v8/src/context_bootstrap/events/subclasses/data.rs b/moli-renderer-v8/src/context_bootstrap/events/subclasses/data.rs index 6f86cfa093..a944f6e0b2 100644 --- a/moli-renderer-v8/src/context_bootstrap/events/subclasses/data.rs +++ b/moli-renderer-v8/src/context_bootstrap/events/subclasses/data.rs @@ -195,6 +195,25 @@ struct PopStateEventInitMembers<'s> { state: Option>, } +#[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>, +) -> bool { + let parsed = match init { + Some(init) => { + match webidl::parse_dictionary_object::(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>, diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_events.rs b/moli-renderer-v8/src/context_bootstrap/navigation_events.rs index 3095b40b97..fce18f300d 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_events.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_events.rs @@ -573,17 +573,20 @@ pub(super) fn dispatch_popstate_event<'s>( child_handle: Option, 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::::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> { - let global = scope.get_current_context().global(scope); - let event_ctor = global - .get(scope, v8str(scope, "Event").into()) - .and_then(|value| v8::Local::::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) } diff --git a/moli-renderer-v8/src/context_bootstrap/specs/registry.rs b/moli-renderer-v8/src/context_bootstrap/specs/registry.rs index 0b77cc2f32..6165739af1 100644 --- a/moli-renderer-v8/src/context_bootstrap/specs/registry.rs +++ b/moli-renderer-v8/src/context_bootstrap/specs/registry.rs @@ -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", diff --git a/moli-renderer-v8/src/context_bootstrap/specs/types.rs b/moli-renderer-v8/src/context_bootstrap/specs/types.rs index 57780ad39b..41bb304f30 100644 --- a/moli-renderer-v8/src/context_bootstrap/specs/types.rs +++ b/moli-renderer-v8/src/context_bootstrap/specs/types.rs @@ -31,6 +31,7 @@ pub(in crate::context_bootstrap) enum ConstructorKind { ToggleEvent, InterestEvent, PopStateEvent, + HashChangeEvent, PageTransitionEvent, DomException, DomError, diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/hash_change_delivery.rs b/moli-renderer-v8/src/runtime/page_vm/tests/hash_change_delivery.rs index 1ab12a39d7..a4d60059d5 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/hash_change_delivery.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/hash_change_delivery.rs @@ -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 { diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/history_traversal.rs b/moli-renderer-v8/src/runtime/page_vm/tests/history_traversal.rs index 969938113b..f3f4653968 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/history_traversal.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/history_traversal.rs @@ -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 {