mirror of
https://github.com/lexmount/moli.git
synced 2026-10-25 08:01:49 +00:00
fix(window): keep current events in private realm state
This commit is contained in:
@@ -5,7 +5,7 @@ use crate::exception_reporting::{
|
||||
use crate::{
|
||||
host::WINDOW_EVENT_SLOT,
|
||||
native_bridge::{JsContextHost, RuntimeObservableContextToken, WindowExecutionContextIdentity},
|
||||
util::v8str,
|
||||
util::{get_private_value, set_private_value},
|
||||
};
|
||||
use moli_webidl_callback::{
|
||||
PreparedWebIdlCallbackFunction, PreparedWebIdlCallbackInterface, WebIdlCallbackInvocation,
|
||||
@@ -244,11 +244,9 @@ impl CallbackInvoker {
|
||||
.and(invocation.current_event)
|
||||
.map(|event| {
|
||||
let global = relevant_context.global(scope);
|
||||
let event_key = v8str(scope, WINDOW_EVENT_SLOT);
|
||||
let previous = global
|
||||
.get(scope, event_key.into())
|
||||
let previous = get_private_value(scope, global, WINDOW_EVENT_SLOT)
|
||||
.unwrap_or_else(|| v8::undefined(scope).into());
|
||||
let _ = global.set(scope, event_key.into(), event.into());
|
||||
set_private_value(scope, global, WINDOW_EVENT_SLOT, event.into());
|
||||
previous
|
||||
});
|
||||
|
||||
@@ -287,7 +285,7 @@ impl CallbackInvoker {
|
||||
|
||||
if let Some(previous) = previous_window_event {
|
||||
let global = relevant_context.global(scope);
|
||||
let _ = global.set(scope, v8str(scope, WINDOW_EVENT_SLOT).into(), previous);
|
||||
set_private_value(scope, global, WINDOW_EVENT_SLOT, previous);
|
||||
}
|
||||
result
|
||||
},
|
||||
|
||||
@@ -461,7 +461,7 @@ struct ConsoleObjectDeclaration {
|
||||
struct WindowBootstrapGlobalSlotsDeclaration<'scope> {
|
||||
#[webapi(slot = WINDOW_CONSOLE_SLOT)]
|
||||
console: v8::Local<'scope, v8::Object>,
|
||||
#[webapi(data_property = WINDOW_EVENT_SLOT, init = "undefined")]
|
||||
#[webapi(slot = WINDOW_EVENT_SLOT, init = "undefined")]
|
||||
event: (),
|
||||
#[webapi(data_property = WINDOW_ONERROR_SLOT, init = "null")]
|
||||
on_error: (),
|
||||
@@ -581,7 +581,7 @@ fn document_fullscreen_enabled_lenient_setter<'s>(
|
||||
) {
|
||||
}
|
||||
|
||||
fn define_replaceable_window_property(
|
||||
pub(in crate::context_bootstrap) fn define_replaceable_window_property(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
receiver: v8::Local<'_, v8::Object>,
|
||||
name: &'static str,
|
||||
@@ -641,11 +641,11 @@ fn window_outer_height_replaceable_getter<'s>(
|
||||
|
||||
fn window_event_replaceable_getter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
_args: v8::FunctionCallbackArguments<'s>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
rv.set(
|
||||
global_hidden_value(scope, WINDOW_EVENT_SLOT)
|
||||
super::window_events::window_event_value_for_receiver(scope, args.this())
|
||||
.unwrap_or_else(|| v8::undefined(scope).into()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -89,9 +89,10 @@ pub(crate) use accessors::{
|
||||
};
|
||||
pub(super) use accessors::{
|
||||
window_console_getter, window_event_getter, window_event_setter,
|
||||
window_onerror_getter_function, window_onerror_setter_function,
|
||||
window_onrejectionhandled_getter_function, window_onrejectionhandled_setter_function,
|
||||
window_onunhandledrejection_getter_function, window_onunhandledrejection_setter_function,
|
||||
window_event_value_for_receiver, window_onerror_getter_function,
|
||||
window_onerror_setter_function, window_onrejectionhandled_getter_function,
|
||||
window_onrejectionhandled_setter_function, window_onunhandledrejection_getter_function,
|
||||
window_onunhandledrejection_setter_function,
|
||||
};
|
||||
pub(super) use console::{
|
||||
console_assert_callback, console_debug_callback, console_error_callback,
|
||||
|
||||
@@ -141,12 +141,53 @@ pub(in crate::context_bootstrap) fn window_event_getter<'s>(
|
||||
if !require_window_receiver(scope, &args) {
|
||||
return;
|
||||
}
|
||||
match global_hidden_value(scope, WINDOW_EVENT_SLOT) {
|
||||
match window_event_value_for_receiver(scope, args.this()) {
|
||||
Some(value) => rv.set(value),
|
||||
None => rv.set(v8::undefined(scope).into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn window_event_value_for_receiver<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
receiver: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Value>> {
|
||||
// Real globals keep the current event in their concrete callback realm.
|
||||
// Resolving only the Document owner would read the default world's slot
|
||||
// even when this receiver is an isolated world's Window.
|
||||
if let Some(context) = receiver.get_creation_context(scope)
|
||||
&& receiver.strict_equals(context.global(scope).into())
|
||||
{
|
||||
return get_private_value(scope, receiver, WINDOW_EVENT_SLOT);
|
||||
}
|
||||
let target_event = context_host_ptr_from_window_object(scope, receiver)
|
||||
.or_else(|| context_host_ptr_from_global_bridge(scope))
|
||||
.and_then(|host_ptr| {
|
||||
let dispatch_scope = if let Some(popup_id) =
|
||||
crate::native_bridge::lightweight_popup_id_from_window(scope, receiver)
|
||||
{
|
||||
crate::native_bridge::OwnerDispatchScope::LightweightPopup(popup_id)
|
||||
} else if let Some(handle) = window_child_context_handle(scope, receiver) {
|
||||
crate::native_bridge::OwnerDispatchScope::Child(handle)
|
||||
} else {
|
||||
crate::native_bridge::OwnerDispatchScope::Top
|
||||
};
|
||||
let host = unsafe { &*host_ptr };
|
||||
let owner = host.current_window_execution_context_owner(dispatch_scope)?;
|
||||
let (_, context) = host.window_execution_context(scope, owner, dispatch_scope)?;
|
||||
let global = context.global(scope);
|
||||
// An empty target Window current event is still authoritative;
|
||||
// do not fall back to the caller Window's active event.
|
||||
Some(
|
||||
get_private_value(scope, global, WINDOW_EVENT_SLOT)
|
||||
.unwrap_or_else(|| v8::undefined(scope).into()),
|
||||
)
|
||||
});
|
||||
target_event.or_else(|| {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
get_private_value(scope, global, WINDOW_EVENT_SLOT)
|
||||
})
|
||||
}
|
||||
|
||||
pub(in crate::context_bootstrap) fn window_event_setter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
@@ -155,9 +196,12 @@ pub(in crate::context_bootstrap) fn window_event_setter<'s>(
|
||||
if !require_window_receiver(scope, &args) {
|
||||
return;
|
||||
}
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let key = v8str(scope, WINDOW_EVENT_SLOT);
|
||||
let _ = global.set(scope, key.into(), args.get(0));
|
||||
super::super::runtime_state::define_replaceable_window_property(
|
||||
scope,
|
||||
args.this(),
|
||||
"event",
|
||||
args.get(0),
|
||||
);
|
||||
rv.set_undefined();
|
||||
}
|
||||
|
||||
|
||||
@@ -64,3 +64,5 @@ mod worker_listener_invocation;
|
||||
|
||||
mod document_domain_lifetime;
|
||||
mod document_domain_setter;
|
||||
|
||||
mod window_current_event_private;
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
(() => {
|
||||
const child = document.getElementById('child').contentWindow;
|
||||
const frame = child.document.createElement('iframe');
|
||||
child.document.body.appendChild(frame);
|
||||
const realms = [['root', window], ['child', child], ['nested', frame.contentWindow]];
|
||||
const descriptors = realms.map(([label, realm]) => ({label, realm,
|
||||
descriptor: Object.getOwnPropertyDescriptor(realm, 'event')}));
|
||||
const rows = [];
|
||||
for (const {label, realm, descriptor} of descriptors) {
|
||||
for (const mode of ['ordinary', 'overwrite', 'accessor', 'prototype']) {
|
||||
const checks = [];
|
||||
const check = (name, pass) => checks.push({name, pass: !!pass});
|
||||
const oldName = '__moliWindowEvent';
|
||||
const previousSlot = Object.getOwnPropertyDescriptor(realm, oldName);
|
||||
const prototype = Object.getPrototypeOf(realm);
|
||||
const previousPrototypeSlot = Object.getOwnPropertyDescriptor(prototype, oldName);
|
||||
const previousError = realm.onerror;
|
||||
const authorValue = {};
|
||||
const publicValue = {};
|
||||
let reads = 0, writes = 0, caught = 0;
|
||||
const trace = [];
|
||||
const read = () => descriptor.get.call(realm);
|
||||
check('no internal own name', !Object.getOwnPropertyNames(realm).includes(oldName));
|
||||
check('no internal reflected key', !Reflect.ownKeys(realm).includes(oldName));
|
||||
check('public accessor', typeof descriptor.get === 'function' && typeof descriptor.set === 'function');
|
||||
try {
|
||||
const map = new realm.WeakMap();
|
||||
for (const name of Object.getOwnPropertyNames(realm)) {
|
||||
if (/[A-Z][A-Za-z0-9]+Event$/.test(name)) map.set(realm[name], name);
|
||||
}
|
||||
check('EventRecorder constructor scan', true);
|
||||
} catch (error) { check('EventRecorder constructor scan', false); }
|
||||
if (mode !== 'ordinary') {
|
||||
delete realm[oldName];
|
||||
if (mode === 'overwrite') Object.defineProperty(realm, oldName,
|
||||
{value: authorValue, writable: true, configurable: true});
|
||||
else Object.defineProperty(mode === 'prototype' ? prototype : realm, oldName,
|
||||
{configurable: true, get() { reads++; return authorValue; }, set() { writes++; }});
|
||||
}
|
||||
const target = new realm.EventTarget();
|
||||
const state = {read, check, target, trace, outer: null, replaced: false,
|
||||
visible: () => realm.event, publicValue,
|
||||
otherWindowsClear: () => descriptors.filter(d => d.realm !== realm)
|
||||
.every(d => d.descriptor.get.call(d.realm) === undefined)};
|
||||
const callbacks = realm.Function('s', `return {
|
||||
outer(event) {
|
||||
s.trace.push('outer'); s.outer = event;
|
||||
s.check('outer current', s.read() === event);
|
||||
s.check('other windows clear', s.otherWindowsClear());
|
||||
s.check('outer public value', s.visible() === (s.replaced ? s.publicValue : event));
|
||||
s.target.dispatchEvent(new Event('inner'));
|
||||
s.check('outer restored', s.read() === event);
|
||||
s.trace.push('restored');
|
||||
},
|
||||
inner(event) {
|
||||
s.trace.push('inner');
|
||||
s.check('inner current', s.read() === event && event !== s.outer);
|
||||
s.check('inner other windows clear', s.otherWindowsClear());
|
||||
},
|
||||
object: { get handleEvent() {
|
||||
s.trace.push('lookup'); s.check('current before handleEvent lookup', s.read()?.type === 'lookup');
|
||||
return event => s.check('object callback current', s.read() === event);
|
||||
} },
|
||||
fail() { throw new Error('expected current-event probe failure'); },
|
||||
report() {
|
||||
s.trace.push('error');
|
||||
s.check('exception report current', s.read()?.type === 'error');
|
||||
return true;
|
||||
}
|
||||
}`)(state);
|
||||
target.addEventListener('outer', callbacks.outer);
|
||||
target.addEventListener('inner', callbacks.inner);
|
||||
target.addEventListener('lookup', callbacks.object);
|
||||
target.addEventListener('fail', callbacks.fail);
|
||||
realm.onerror = callbacks.report;
|
||||
try {
|
||||
check('undefined before dispatch', read() === undefined);
|
||||
target.dispatchEvent(new realm.Event('outer'));
|
||||
check('undefined after nested dispatch', read() === undefined);
|
||||
target.dispatchEvent(new realm.Event('lookup'));
|
||||
check('undefined after object callback', read() === undefined);
|
||||
target.dispatchEvent(new realm.Event('fail'));
|
||||
check('undefined after exception reporting', read() === undefined);
|
||||
realm.event = publicValue;
|
||||
check('public Replaceable value', realm.event === publicValue);
|
||||
check('replacement does not change native current event', read() === undefined);
|
||||
const replacement = Object.getOwnPropertyDescriptor(realm, 'event');
|
||||
check('Replaceable data descriptor', replacement.value === publicValue &&
|
||||
replacement.writable && replacement.enumerable && replacement.configurable);
|
||||
state.replaced = true;
|
||||
target.dispatchEvent(new realm.Event('outer'));
|
||||
check('replacement survives callback', realm.event === publicValue);
|
||||
check('native current cleared with replacement', read() === undefined);
|
||||
check('trace', trace.join(',') === 'outer,inner,restored,lookup,error,outer,inner,restored');
|
||||
check('author getter ignored', reads === 0);
|
||||
check('author setter ignored', writes === 0);
|
||||
if (mode === 'overwrite') check('author slot value unchanged', realm[oldName] === authorValue);
|
||||
if (mode === 'ordinary') check('dispatch does not publish internal state', !Object.hasOwn(realm, oldName));
|
||||
} catch (error) { caught++; check('dispatch completed without uncaught exception', false); }
|
||||
finally {
|
||||
Object.defineProperty(realm, 'event', descriptor);
|
||||
realm.onerror = previousError;
|
||||
delete realm[oldName];
|
||||
if (previousSlot) Object.defineProperty(realm, oldName, previousSlot);
|
||||
if (mode === 'prototype') {
|
||||
delete prototype[oldName];
|
||||
if (previousPrototypeSlot) Object.defineProperty(prototype, oldName, previousPrototypeSlot);
|
||||
}
|
||||
}
|
||||
rows.push({label, mode, complete: caught === 0 && checks.every(c => c.pass),
|
||||
passed: checks.filter(c => c.pass).length, total: checks.length, checks, trace, reads, writes, caught});
|
||||
}
|
||||
}
|
||||
globalThis.__uiEventResults = {rows, complete: rows.every(r => r.complete),
|
||||
passed: rows.reduce((n, r) => n + r.passed, 0), total: rows.reduce((n, r) => n + r.total, 0)};
|
||||
return __uiEventResults.complete;
|
||||
})();
|
||||
@@ -0,0 +1,70 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn window_current_events_ignore_author_slot_names_and_public_replacements() {
|
||||
let mut vm = new_storage_html_test_vm("https://private-window-event.test/");
|
||||
vm.eval("document.body.innerHTML = '<iframe id=child></iframe>'; 'ready'")
|
||||
.unwrap();
|
||||
vm.eval(include_str!("window_current_event_private.js"))
|
||||
.unwrap();
|
||||
let facts: serde_json::Value =
|
||||
serde_json::from_str(&vm.eval("JSON.stringify(__uiEventResults)").unwrap()).unwrap();
|
||||
assert_eq!(facts["rows"].as_array().unwrap().len(), 12);
|
||||
assert_eq!(facts["total"], 378);
|
||||
assert_eq!(facts["complete"], true, "{facts}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_current_events_use_private_state_in_each_listener_world() {
|
||||
let mut vm = new_storage_html_test_vm("https://private-window-event-worlds.test/");
|
||||
vm.eval(
|
||||
r#"
|
||||
document.body.innerHTML = '<div id=target></div>';
|
||||
globalThis.target = document.getElementById('target');
|
||||
globalThis.facts = [];
|
||||
Object.defineProperty(globalThis, '__moliWindowEvent', {
|
||||
get() { throw new Error('author slot getter'); },
|
||||
set() { throw new Error('author slot setter'); }
|
||||
});
|
||||
target.addEventListener('outer', event => {
|
||||
facts.push(window.event === event);
|
||||
target.dispatchEvent(new Event('inner'));
|
||||
facts.push(window.event === event);
|
||||
});
|
||||
'ready'
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let isolated = vm
|
||||
.create_isolated_world("private-current-event", false)
|
||||
.unwrap();
|
||||
vm.eval_in_isolated_context(
|
||||
isolated,
|
||||
r#"
|
||||
globalThis.target = document.getElementById('target');
|
||||
globalThis.facts = [];
|
||||
Object.defineProperty(globalThis, '__moliWindowEvent', {
|
||||
get() { throw new Error('isolated author slot getter'); },
|
||||
set() { throw new Error('isolated author slot setter'); }
|
||||
});
|
||||
target.addEventListener('inner', event => {
|
||||
facts.push(window.event === event);
|
||||
});
|
||||
'ready'
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
vm.eval("target.dispatchEvent(new Event('outer')); JSON.stringify([facts, window.event === undefined])")
|
||||
.unwrap(),
|
||||
"[[true,true],true]"
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval_in_isolated_context(
|
||||
isolated,
|
||||
"JSON.stringify([facts, window.event === undefined])"
|
||||
)
|
||||
.unwrap(),
|
||||
"[[true],true]"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user