diff --git a/moli-renderer-v8/src/context_bootstrap/window_events.rs b/moli-renderer-v8/src/context_bootstrap/window_events.rs index e8d260c44c..81bd3aac62 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_events.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_events.rs @@ -63,6 +63,7 @@ pub(crate) const WINDOW_EVENT_HANDLER_PROPERTIES: &[&str] = &[ "onloadstart", "onlostpointercapture", "onmessage", + "onmessageerror", "onmousedown", "onmousemove", "onmouseenter", diff --git a/moli-renderer-v8/src/context_bootstrap/window_events/install.rs b/moli-renderer-v8/src/context_bootstrap/window_events/install.rs index f44a58d8d8..32fcd12875 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_events/install.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_events/install.rs @@ -56,7 +56,7 @@ pub(in crate::context_bootstrap) fn install_window_global_accessors<'s>( for name in WINDOW_EVENT_HANDLER_PROPERTIES { if matches!( *name, - "onerror" | "onunhandledrejection" | "onrejectionhandled" + "onerror" | "onmessageerror" | "onunhandledrejection" | "onrejectionhandled" ) { continue; } diff --git a/moli-renderer-v8/src/native_bridge/context_host/popups.rs b/moli-renderer-v8/src/native_bridge/context_host/popups.rs index 890ce0ea1c..30c9a070de 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/popups.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/popups.rs @@ -1295,6 +1295,43 @@ impl JsContextHost { .map(|record| v8::Local::new(scope, &record.window_proxy)) } + pub(crate) fn lightweight_popup_event_handler_property_value<'s>( + &self, + scope: &mut v8::PinScope<'s, '_>, + popup_id: u64, + property_name: &str, + ) -> Option> { + let window = self.lightweight_popup_window(scope, popup_id)?; + Some(lightweight_popup_event_handler_value( + scope, + window, + property_name, + )) + } + + pub(crate) fn set_lightweight_popup_event_handler_property<'s>( + &mut self, + scope: &mut v8::PinScope<'s, '_>, + popup_id: u64, + property_name: &str, + handler: Option>, + ) { + let Some(window) = self.lightweight_popup_window(scope, popup_id) else { + return; + }; + let Some(property_name) = WINDOW_EVENT_HANDLER_PROPERTIES + .iter() + .copied() + .find(|candidate| *candidate == property_name) + else { + return; + }; + let value = handler + .map(v8::Local::::from) + .unwrap_or_else(|| v8::null(scope).into()); + set_lightweight_popup_event_handler_value(scope, window, property_name, value); + } + pub(in crate::native_bridge::context_host) fn lightweight_popup_opener_endpoint( &self, popup_id: u64, @@ -5545,11 +5582,21 @@ fn lightweight_popup_event_handler_getter<'s>( rv.set_null(); return; }; - rv.set( - get_private_value(scope, args.this(), property_name) - .filter(|value| value.is_object()) - .unwrap_or_else(|| v8::null(scope).into()), - ); + rv.set(lightweight_popup_event_handler_value( + scope, + args.this(), + property_name, + )); +} + +fn lightweight_popup_event_handler_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + window: v8::Local<'s, v8::Object>, + property_name: &str, +) -> v8::Local<'s, v8::Value> { + get_private_value(scope, window, property_name) + .filter(|value| value.is_object()) + .unwrap_or_else(|| v8::null(scope).into()) } fn lightweight_popup_event_handler_setter<'s>( @@ -5565,22 +5612,30 @@ fn lightweight_popup_event_handler_setter<'s>( rv.set_undefined(); return; }; - let value = args.get(0); + set_lightweight_popup_event_handler_value(scope, args.this(), property_name, args.get(0)); + rv.set_undefined(); +} + +fn set_lightweight_popup_event_handler_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + window: v8::Local<'s, v8::Object>, + property_name: &'static str, + value: v8::Local<'s, v8::Value>, +) { let stored = if value.is_object() { value } else { v8::null(scope).into() }; - set_private_value(scope, args.this(), property_name, stored); + set_private_value(scope, window, property_name, stored); simple_object_event_set_ordered_handler( scope, - args.this(), + window, LIGHTWEIGHT_POPUP_EVENT_LISTENERS_SLOT, property_name.strip_prefix("on").unwrap_or(property_name), property_name, stored.is_object(), ); - rv.set_undefined(); } fn clear_lightweight_popup_window_document_event_state<'s>( @@ -5596,7 +5651,9 @@ fn clear_lightweight_popup_window_document_event_state<'s>( ); let null = v8::null(scope).into(); for name in WINDOW_EVENT_HANDLER_PROPERTIES { - let _ = window.set(scope, v8str(scope, name).into(), null); + // Reset the shared handler state even if script has replaced the + // public accessor. Document retirement must not invoke author setters. + set_private_value(scope, window, name, null); } } diff --git a/moli-renderer-v8/src/native_bridge/element/event_handlers/body_window.rs b/moli-renderer-v8/src/native_bridge/element/event_handlers/body_window.rs index c622b88283..adf85a0f21 100644 --- a/moli-renderer-v8/src/native_bridge/element/event_handlers/body_window.rs +++ b/moli-renderer-v8/src/native_bridge/element/event_handlers/body_window.rs @@ -74,7 +74,9 @@ fn body_window_event_handler_getter_function<'s>( } Some(OwnerDispatchScope::Child(child_handle)) => unsafe { &mut *runtime_ptr } .child_window_event_handler_property_value(scope, child_handle, &handler_name), - Some(OwnerDispatchScope::LightweightPopup(_)) | None => None, + Some(OwnerDispatchScope::LightweightPopup(popup_id)) => unsafe { &*runtime_ptr } + .lightweight_popup_event_handler_property_value(scope, popup_id, &handler_name), + None => None, }; match value { Some(value) => rv.set(value), @@ -122,7 +124,9 @@ fn body_window_event_handler_setter_function<'s>( relevant_context, ); } - Some(OwnerDispatchScope::LightweightPopup(_)) | None => {} + Some(OwnerDispatchScope::LightweightPopup(popup_id)) => unsafe { &mut *runtime_ptr } + .set_lightweight_popup_event_handler_property(scope, popup_id, &handler_name, handler), + None => {} } rv.set_undefined(); } diff --git a/moli-renderer-v8/src/native_bridge/element/event_handlers/mod.rs b/moli-renderer-v8/src/native_bridge/element/event_handlers/mod.rs index 7de052b4c5..816843fb8d 100644 --- a/moli-renderer-v8/src/native_bridge/element/event_handlers/mod.rs +++ b/moli-renderer-v8/src/native_bridge/element/event_handlers/mod.rs @@ -34,11 +34,7 @@ pub(super) fn body_or_frameset_window_owner( if !is_body_or_frameset_element(runtime, handle) { return None; } - match runtime.owner_dispatch_scope_for_node(handle)? { - owner @ (crate::native_bridge::OwnerDispatchScope::Top - | crate::native_bridge::OwnerDispatchScope::Child(_)) => Some(owner), - crate::native_bridge::OwnerDispatchScope::LightweightPopup(_) => None, - } + runtime.owner_dispatch_scope_for_node(handle) } pub(crate) fn body_or_frameset_uses_runtime_window( diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/event_handlers/popup.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/event_handlers/popup.rs index 0489c42cc0..5e10105087 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/event_handlers/popup.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/event_handlers/popup.rs @@ -1,5 +1,153 @@ use super::*; +#[test] +fn popup_body_and_frameset_idl_handlers_share_their_window_handler_state() { + let mut vm = new_storage_test_vm("https://popup-body-window-handlers.test/"); + let result = vm + .eval( + r#" +(() => { + const popup = open(); + try { + const names = ['onblur','onerror','onfocus','onload','onresize','onscroll', + 'onafterprint','onbeforeprint','onbeforeunload','onhashchange','onlanguagechange', + 'onmessage','onmessageerror','onoffline','ononline','onpagehide','onpagereveal', + 'onpageshow','onpageswap','onpopstate','onrejectionhandled','onstorage', + 'onunhandledrejection','onunload']; + const body = popup.document.body; + const detachedBody = popup.document.createElement('body'); + const frameset = popup.document.createElement('frameset'); + const aliases = [popup, body, detachedBody, frameset]; + const failures = []; + for (const name of names) { + for (const writer of aliases) { + const handler = function() {}; + writer[name] = handler; + if (!aliases.every(alias => alias[name] === handler)) failures.push(name + ':function'); + const objectHandler = {}; + writer[name] = objectHandler; + if (!aliases.every(alias => alias[name] === objectHandler)) failures.push(name + ':object'); + for (const value of [null, undefined, false, 1, '', Symbol(), 1n]) { + writer[name] = handler; + writer[name] = value; + if (!aliases.every(alias => alias[name] === null)) failures.push(name + ':null'); + } + if (window[name] !== null) failures.push(name + ':opener'); + } + } + return JSON.stringify(failures); + } finally { popup.close(); } +})() +"#, + ) + .expect("popup body and frameset IDL aliases should evaluate"); + assert_eq!(result, "[]"); +} + +#[test] +fn popup_body_idl_handlers_preserve_listener_order_replacement_and_cancellation() { + let mut vm = new_storage_test_vm("https://popup-body-handler-order.test/"); + let result = vm.eval(r#" +(() => { + const popup = open(); + try { + const body = popup.document.body; + const frameset = popup.document.createElement('frameset'); + const trace = []; + let count = 0; + popup.addEventListener('resize', () => { + trace.push('before:' + ++count); + if (count === 1) frameset.onresize = function(event) { + trace.push('replacement:' + (this === popup && event.currentTarget === popup)); + return false; + }; + if (count === 2) { + body.onresize = null; + body.onresize = () => trace.push('readded'); + } + }); + body.onresize = () => trace.push('stale'); + popup.addEventListener('resize', () => trace.push('after:' + count)); + for (let n = 0; n < 3; ++n) { + trace.push('result:' + popup.dispatchEvent(new Event('resize', {cancelable:true}))); + } + const errors = []; + const error = new Error('popup'); + body.onerror = function(message, source, line, column, value) { + errors.push([this === popup, arguments.length, message, source, line, column, value === error]); + return true; + }; + const errorCanceled = !popup.dispatchEvent(new ErrorEvent('error', { + message:'message', filename:'source', lineno:3, colno:4, error, cancelable:true + })); + return JSON.stringify({trace, errors, errorCanceled}); + } finally { popup.close(); } +})() +"#).expect("popup body handler dispatch should evaluate"); + assert_eq!( + serde_json::from_str::(&result).unwrap(), + serde_json::json!({ + "trace":["before:1", "replacement:true", "after:1", "result:false", + "before:2", "after:2", "result:true", "before:3", "after:3", + "readded", "result:true"], + "errors":[[true, 5, "message", "source", 3, 4, true]], + "errorCanceled":true, + }) + ); +} + +#[test] +fn popup_body_idl_handlers_follow_document_ownership_and_ignore_window_expandos() { + let mut vm = new_storage_test_vm("https://popup-body-handler-owner.test/"); + let result = vm + .eval( + r#" +(() => { + const popup = open('about:blank', 'body-handler-owner'); + try { + const oldDocument = popup.document; + const body = oldDocument.body; + const frameset = oldDocument.createElement('frameset'); + const first = function() {}; + const second = function() {}; + body.onresize = first; + oldDocument.documentElement.replaceChild(oldDocument.createElement('body'), body); + const replacement = oldDocument.body.onresize === first && body.onresize === first; + let traps = 0; + Object.defineProperty(popup, 'onresize', { + configurable:true, get() { ++traps; return second; }, set() { ++traps; } + }); + frameset.onresize = second; + const ignoresExpando = body.onresize === second && traps === 0; + window.onresize = first; + document.adoptNode(frameset); + const adoption = frameset.onresize === first && body.onresize === second; + frameset.onresize = null; + const independent = window.onresize === null && body.onresize === second; + const windowless = document.implementation.createHTMLDocument(''); + windowless.adoptNode(frameset); + frameset.onresize = first; + const noWindow = frameset.onresize === null; + open('about:blank', 'body-handler-owner'); + body.onresize = first; + const retired = popup.document !== oldDocument && body.onresize === null && + popup.document.body.onresize === null; + return JSON.stringify({replacement, ignoresExpando, adoption, independent, noWindow, retired, + cleanupSkippedExpando:traps === 0}); + } finally { popup.close(); } +})() +"#, + ) + .expect("popup body handler ownership should evaluate"); + assert_eq!( + serde_json::from_str::(&result).unwrap(), + serde_json::json!({ + "replacement":true, "ignoresExpando":true, "adoption":true, + "independent":true, "noWindow":true, "retired":true, "cleanupSkippedExpando":true, + }) + ); +} + #[test] fn popup_dom_events_propagate_through_their_window_without_reaching_the_opener() { let mut vm = new_storage_test_vm("https://popup-event-path.test/");