From 49b4e87db21471bbb0bb3d7929c333f77f9062a3 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 23 Sep 2026 01:20:12 +0800 Subject: [PATCH] fix(popup): share body and frameset Window handler properties Route body/frameset IDL attributes through their owning popup Window's existing handler values and ordered registrations. Include onmessageerror in the shared Window handler list and clear private handler state on document retirement without invoking author-replaced setters. Cover shared values, listener replacement and cancellation, onerror arguments, adoption, windowless ownership, and document retirement. --- .../src/context_bootstrap/window_events.rs | 1 + .../window_events/install.rs | 2 +- .../src/native_bridge/context_host/popups.rs | 77 +++++++-- .../element/event_handlers/body_window.rs | 8 +- .../element/event_handlers/mod.rs | 6 +- .../tests/browser_api/event_handlers/popup.rs | 148 ++++++++++++++++++ 6 files changed, 224 insertions(+), 18 deletions(-) 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/");