diff --git a/moli-renderer-v8/src/context_bootstrap/event_template.rs b/moli-renderer-v8/src/context_bootstrap/event_template.rs index 4e147f6f33..5557a7c127 100644 --- a/moli-renderer-v8/src/context_bootstrap/event_template.rs +++ b/moli-renderer-v8/src/context_bootstrap/event_template.rs @@ -37,7 +37,7 @@ pub(in crate::context_bootstrap) fn object_is_event_target<'s>( } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::Event, enumerable)] +#[webapi(interface = web_api_interfaces::Event, enumerable, receiver)] struct EventBaseTemplateMethodsDeclaration { #[webapi(accessor_property = "type", getter = event_type_getter_function)] event_type: (), @@ -131,7 +131,7 @@ struct EventBaseTemplateMethodsDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::CloseEvent, enumerable)] +#[webapi(interface = web_api_interfaces::CloseEvent, enumerable, receiver)] struct CloseEventTemplateAccessorsDeclaration { #[webapi(accessor_property = "wasClean", getter = close_event_was_clean_getter_function)] was_clean: (), @@ -144,28 +144,28 @@ struct CloseEventTemplateAccessorsDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::TrackEvent, enumerable)] +#[webapi(interface = web_api_interfaces::TrackEvent, enumerable, receiver)] struct TrackEventTemplateAccessorsDeclaration { #[webapi(accessor_property, getter = track_event_track_getter_function)] track: (), } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::SubmitEvent, enumerable)] +#[webapi(interface = web_api_interfaces::SubmitEvent, enumerable, receiver)] struct SubmitEventTemplateAccessorsDeclaration { #[webapi(accessor_property, getter = submit_event_submitter_getter_function)] submitter: (), } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::FormDataEvent, enumerable)] +#[webapi(interface = web_api_interfaces::FormDataEvent, enumerable, receiver)] struct FormDataEventTemplateAccessorsDeclaration { #[webapi(accessor_property = "formData", getter = form_data_event_form_data_getter_function)] form_data: (), } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::UIEvent, enumerable)] +#[webapi(interface = web_api_interfaces::UIEvent, enumerable, receiver)] struct UiEventTemplateMethodsDeclaration { #[webapi(method = "initUIEvent", length = 0, callback = ui_event_init_callback)] init_ui_event: (), @@ -178,7 +178,7 @@ struct UiEventTemplateMethodsDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::FocusEvent, enumerable)] +#[webapi(interface = web_api_interfaces::FocusEvent, enumerable, receiver)] struct FocusEventTemplateAccessorsDeclaration { #[webapi( accessor_property = "relatedTarget", @@ -188,14 +188,14 @@ struct FocusEventTemplateAccessorsDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::TextEvent, enumerable)] +#[webapi(interface = web_api_interfaces::TextEvent, enumerable, receiver)] struct TextEventTemplateMethodsDeclaration { #[webapi(method = "initTextEvent", length = 1, callback = text_event_init_callback)] init_text_event: (), } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::CompositionEvent, enumerable)] +#[webapi(interface = web_api_interfaces::CompositionEvent, enumerable, receiver)] struct CompositionEventTemplateMethodsDeclaration { #[webapi( method = "initCompositionEvent", @@ -206,14 +206,14 @@ struct CompositionEventTemplateMethodsDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::CustomEvent, enumerable)] +#[webapi(interface = web_api_interfaces::CustomEvent, enumerable, receiver)] struct CustomEventTemplateMethodsDeclaration { #[webapi(method = "initCustomEvent", length = 1, callback = custom_event_init_callback)] init_custom_event: (), } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::StorageEvent, enumerable)] +#[webapi(interface = web_api_interfaces::StorageEvent, enumerable, receiver)] struct StorageEventTemplateMethodsDeclaration { #[webapi( method = "initStorageEvent", @@ -224,7 +224,7 @@ struct StorageEventTemplateMethodsDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::KeyboardEvent, enumerable)] +#[webapi(interface = web_api_interfaces::KeyboardEvent, enumerable, receiver)] struct KeyboardEventTemplateMethodsDeclaration { #[webapi(method = "initKeyboardEvent", length = 7, callback = keyboard_event_init_callback)] init_keyboard_event: (), @@ -238,7 +238,7 @@ struct KeyboardEventTemplateMethodsDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::MouseEvent, enumerable)] +#[webapi(interface = web_api_interfaces::MouseEvent, enumerable, receiver)] struct MouseEventTemplateMethodsDeclaration { #[webapi( accessor_property = "relatedTarget", @@ -270,7 +270,7 @@ struct WheelEventTemplateConstantsDeclaration { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::PointerEvent, enumerable)] +#[webapi(interface = web_api_interfaces::PointerEvent, enumerable, receiver)] struct PointerEventTemplateMethodsDeclaration { #[webapi( method = "getPredictedEvents", diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage/event_dispatch_and_targets.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage/event_dispatch_and_targets.rs index a321ec653d..6b6936c9e8 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage/event_dispatch_and_targets.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage/event_dispatch_and_targets.rs @@ -1084,7 +1084,12 @@ fn event_composed_path_slot_ignores_reflection_and_spoofing() { }, { once: true }); simpleTarget.dispatchEvent(simple); - const fakePath = Event.prototype.composedPath.call({ __lmCp: ["fake"] }); + let fakePathThrowsTypeError = false; + try { + Event.prototype.composedPath.call({ __lmCp: ["fake"] }); + } catch (error) { + fakePathThrowsTypeError = error instanceof TypeError; + } return JSON.stringify({ cleanBeforeOwn, cleanDuring, @@ -1094,7 +1099,7 @@ fn event_composed_path_slot_ignores_reflection_and_spoofing() { spoofedAfterLength: spoofed.composedPath().length, simpleDuring, simpleAfterLength: simple.composedPath().length, - fakePathLength: fakePath.length + fakePathThrowsTypeError }); })() "#, @@ -1103,7 +1108,7 @@ fn event_composed_path_slot_ignores_reflection_and_spoofing() { assert_eq!( result, - r#"{"cleanBeforeOwn":false,"cleanDuring":{"ownNameVisible":false,"firstIsTarget":true,"hasPath":true},"cleanAfterLength":0,"spoofedBeforeLength":0,"spoofedDuring":{"ownValue":"spoofed-during","firstIsTarget":true,"containsSpoof":false},"spoofedAfterLength":0,"simpleDuring":{"ownValue":"spoofed-simple","firstIsSimpleTarget":true,"length":1,"containsSpoof":false},"simpleAfterLength":0,"fakePathLength":0}"# + r#"{"cleanBeforeOwn":false,"cleanDuring":{"ownNameVisible":false,"firstIsTarget":true,"hasPath":true},"cleanAfterLength":0,"spoofedBeforeLength":0,"spoofedDuring":{"ownValue":"spoofed-during","firstIsTarget":true,"containsSpoof":false},"spoofedAfterLength":0,"simpleDuring":{"ownValue":"spoofed-simple","firstIsSimpleTarget":true,"length":1,"containsSpoof":false},"simpleAfterLength":0,"fakePathThrowsTypeError":true}"# ); } @@ -1290,6 +1295,11 @@ fn event_subclass_slots_ignore_reflection_and_spoofing() { .eval( r#" (() => { + const rejectsReceiver = (getter, receiver) => { + try { getter.call(receiver); } + catch (error) { return error instanceof TypeError; } + return false; + }; const getterDescriptor = (prototype, name) => { const descriptor = Object.getOwnPropertyDescriptor(prototype, name); return [ @@ -1377,20 +1387,20 @@ fn event_subclass_slots_ignore_reflection_and_spoofing() { close.__moliCloseEventReason ], fakeClose: [ - closeWasCleanGetter.call(fakeClose), - closeCodeGetter.call(fakeClose), - closeReasonGetter.call(fakeClose) + rejectsReceiver(closeWasCleanGetter, fakeClose), + rejectsReceiver(closeCodeGetter, fakeClose), + rejectsReceiver(closeReasonGetter, fakeClose) ], submitOwnBefore, submitterDescriptor: getterDescriptor(SubmitEvent.prototype, "submitter"), submitterIsButton: submit.submitter === button, submitSpoofIsInput: submit.__moliSubmitEventSubmitter instanceof HTMLInputElement, - fakeSubmitterIsNull: submitterGetter.call(fakeSubmit) === null, + fakeSubmitterThrowsTypeError: rejectsReceiver(submitterGetter, fakeSubmit), formDataOwnBefore, formDataDescriptor: getterDescriptor(FormDataEvent.prototype, "formData"), formDataIsReal: formDataEvent.formData === formData, formDataSpoofIsSpoofed: formDataEvent.__moliFormDataEventFormData === spoofedFormData, - fakeFormDataIsUndefined: formDataGetter.call(fakeFormDataEvent) === undefined + fakeFormDataThrowsTypeError: rejectsReceiver(formDataGetter, fakeFormDataEvent) }); })() "#, @@ -1399,7 +1409,7 @@ fn event_subclass_slots_ignore_reflection_and_spoofing() { assert_eq!( result, - r#"{"closeDescriptors":["wasClean:function:get wasClean:0:undefined:true:true","code:function:get code:0:undefined:true:true","reason:function:get reason:0:undefined:true:true"],"closeOwnBefore":[],"closeValues":[true,1000,"done"],"closeSpoofValues":[false,4000,"spoofed"],"fakeClose":[false,0,""],"submitOwnBefore":[],"submitterDescriptor":"submitter:function:get submitter:0:undefined:true:true","submitterIsButton":true,"submitSpoofIsInput":true,"fakeSubmitterIsNull":true,"formDataOwnBefore":[],"formDataDescriptor":"formData:function:get formData:0:undefined:true:true","formDataIsReal":true,"formDataSpoofIsSpoofed":true,"fakeFormDataIsUndefined":true}"# + r#"{"closeDescriptors":["wasClean:function:get wasClean:0:undefined:true:true","code:function:get code:0:undefined:true:true","reason:function:get reason:0:undefined:true:true"],"closeOwnBefore":[],"closeValues":[true,1000,"done"],"closeSpoofValues":[false,4000,"spoofed"],"fakeClose":[true,true,true],"submitOwnBefore":[],"submitterDescriptor":"submitter:function:get submitter:0:undefined:true:true","submitterIsButton":true,"submitSpoofIsInput":true,"fakeSubmitterThrowsTypeError":true,"formDataOwnBefore":[],"formDataDescriptor":"formData:function:get formData:0:undefined:true:true","formDataIsReal":true,"formDataSpoofIsSpoofed":true,"fakeFormDataThrowsTypeError":true}"# ); } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/media.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/media.rs index 11ab83f9c8..f47e2e8190 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/media.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/media.rs @@ -1174,6 +1174,9 @@ fn track_event_track_uses_prototype_private_slot() { const ownBefore = Object.getOwnPropertyNames(event).filter(name => name === 'track'); event.track = second; const fake = { track: second }; + let fakeTrackThrowsTypeError = false; + try { descriptor.get.call(fake); } + catch (error) { fakeTrackThrowsTypeError = error instanceof TypeError; } return JSON.stringify({ hasGetter: typeof descriptor.get === 'function', @@ -1186,7 +1189,7 @@ fn track_event_track_uses_prototype_private_slot() { trackAfterAssignment: event.track === first, assignmentCreatedOwn: Object.prototype.hasOwnProperty.call(event, 'track'), emptyTrackIsNull: empty.track === null, - fakeTrackIsNull: descriptor.get.call(fake) === null + fakeTrackThrowsTypeError }); })() "#, @@ -1195,7 +1198,7 @@ fn track_event_track_uses_prototype_private_slot() { assert_eq!( result, - r#"{"hasGetter":true,"getterName":"get track","getterLength":0,"hasSetter":true,"enumerable":true,"configurable":true,"ownBefore":[],"trackAfterAssignment":true,"assignmentCreatedOwn":false,"emptyTrackIsNull":true,"fakeTrackIsNull":true}"# + r#"{"hasGetter":true,"getterName":"get track","getterLength":0,"hasSetter":true,"enumerable":true,"configurable":true,"ownBefore":[],"trackAfterAssignment":true,"assignmentCreatedOwn":false,"emptyTrackIsNull":true,"fakeTrackThrowsTypeError":true}"# ); } diff --git a/moli-renderer-v8/src/script_vm/tests/event_receivers.js b/moli-renderer-v8/src/script_vm/tests/event_receivers.js new file mode 100644 index 0000000000..7ccba7350f --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/event_receivers.js @@ -0,0 +1,125 @@ +(() => { + const other = document.getElementById('child').contentWindow; + const realms = [['main', globalThis], ['iframe', other]]; + const rows = []; + function check(name, expected, run) { + let actual; + try { actual = run(); } catch (error) { actual = {error: error.name, message: error.message}; } + rows.push({name, expected, actual, pass: JSON.stringify(actual) === JSON.stringify(expected)}); + } + const coreGetters = ['type', 'target', 'currentTarget', 'eventPhase', 'bubbles', 'cancelable', + 'defaultPrevented', 'composed', 'srcElement', 'cancelBubble', 'returnValue', 'timeStamp']; + const methods = ['preventDefault', 'stopPropagation', 'stopImmediatePropagation', 'composedPath']; + function invalidReceivers(realm, real, traps) { + const revoked = Proxy.revocable(real, {}); revoked.revoke(); + return [null, undefined, 0, {}, realm, realm.document, realm.document.createElement('div'), + Object.getPrototypeOf(real), Object.create(real), new Proxy(real, {}), revoked.proxy, + new Proxy(real, {get() {traps.push('get'); throw new Error('get trap');}, + getPrototypeOf() {traps.push('prototype'); throw new Error('prototype trap');}})]; + } + for (const [calleeName, callee] of realms) for (const [homeName, home] of realms) { + const label = calleeName + '/' + homeName; + for (const kind of ['Event', 'MouseEvent', 'CustomEvent']) { + const real = new home[kind]('before', {cancelable: true}); + for (const name of coreGetters) { + const getter = Object.getOwnPropertyDescriptor(callee.Event.prototype, name).get; + check(label + '/' + kind + '/' + name + '/genuine', true, () => getter.call(real) === real[name]); + const traps = []; + for (const [index, receiver] of invalidReceivers(home, real, traps).entries()) { + check(label + '/' + kind + '/' + name + '/invalid-' + index, true, () => { + try { getter.call(receiver); } catch(error) { return error instanceof callee.TypeError; } + return false; + }); + } + check(label + '/' + kind + '/' + name + '/no-traps', [], () => traps); + } + for (const name of methods) { + const method = callee.Event.prototype[name]; + check(label + '/' + kind + '/' + name + '/genuine', true, () => { + method.call(new home[kind]('before', {cancelable: true})); return true; + }); + const traps = []; + for (const [index, receiver] of invalidReceivers(home, real, traps).entries()) { + check(label + '/' + kind + '/' + name + '/invalid-' + index, true, () => { + try { method.call(receiver); } catch(error) { return error instanceof callee.TypeError; } + return false; + }); + } + check(label + '/' + kind + '/' + name + '/no-traps', [], () => traps); + } + for (const name of ['cancelBubble', 'returnValue']) { + const setter = Object.getOwnPropertyDescriptor(callee.Event.prototype, name).set; + const traps = []; + for (const [index, receiver] of invalidReceivers(home, real, traps).entries()) { + check(label + '/' + kind + '/' + name + '/setter-invalid-' + index, true, () => { + try { setter.call(receiver, false); } catch(error) { return error instanceof callee.TypeError; } + return false; + }); + } + check(label + '/' + kind + '/' + name + '/setter-no-traps', [], () => traps); + } + } + const legacy = [['Event', 'initEvent'], ['UIEvent', 'initUIEvent'], ['MouseEvent', 'initMouseEvent'], + ['KeyboardEvent', 'initKeyboardEvent'], ['CustomEvent', 'initCustomEvent'], + ['StorageEvent', 'initStorageEvent'], ['CompositionEvent', 'initCompositionEvent'], ['TextEvent', 'initTextEvent']]; + for (const [kind, name] of legacy) { + const make = () => { + if (kind !== 'TextEvent') return new home[kind]('before'); + const event = home.document.createEvent(kind); + event.initTextEvent('before'); + return event; + }; + const method = callee[kind].prototype[name]; + const args = type => name === 'initMouseEvent' + ? [type, true, true, null, 0, 0, 0, 0, 0, false, false, false, false, 0, null] + : [type, true, true]; + check(label + '/' + name + '/genuine', ['after', true], () => { + const real = make(); method.apply(real, args('after')); + return [real.type, real instanceof home[kind]]; + }); + const real = make(), traps = []; + for (const [index, receiver] of invalidReceivers(home, real, traps).entries()) { + check(label + '/' + name + '/invalid-' + index, [true, 0], () => { + let conversions = 0, rejected = false; + const type = {toString() {conversions++; return 'forged';}}; + try {method.apply(receiver, args(type));} catch(error) {rejected = error instanceof callee.TypeError;} + return [rejected, conversions]; + }); + } + check(label + '/' + name + '/no-traps', [], () => traps); + if (kind !== 'Event') { + check(label + '/' + name + '/wrong-event-brand', true, () => { + try {method.apply(new home.Event('before'), args('after'));} + catch(error) {return error instanceof callee.TypeError;} + return false; + }); + } + check(label + '/' + name + '/conversion-exception', true, () => { + const marker = new Error('conversion'); + try {method.apply(make(), args({toString() {throw marker;}}));} + catch(error) {return error === marker;} + return false; + }); + check(label + '/' + name + '/dispatch-no-mutation', ['before', false], () => { + const real = make(), target = new home.EventTarget(); + target.addEventListener(real.type, () => method.apply(real, args('after'))); + target.dispatchEvent(real); + return [real.type, real.bubbles]; + }); + } + check(label + '/borrowed-init-on-subclass', ['changed', true, true], () => { + const real = new home.MouseEvent('before'); + callee.Event.prototype.initEvent.call(real, 'changed', true, true); + return [real.type, real instanceof home.MouseEvent, real.cancelable]; + }); + check(label + '/inherited-ui-init-on-mouse', ['changed', true], () => { + const real = new home.MouseEvent('before'); + callee.UIEvent.prototype.initUIEvent.call(real, 'changed'); + return [real.type, real instanceof home.MouseEvent]; + }); + } + globalThis.__eventReceiverFailures = rows.filter(row => !row.pass); + globalThis.__nodeReplacementResults = {total: rows.length, passed: rows.filter(row => row.pass).length, + failures: __eventReceiverFailures, rows}; + return rows.every(row => row.pass); +})() diff --git a/moli-renderer-v8/src/script_vm/tests/event_receivers.rs b/moli-renderer-v8/src/script_vm/tests/event_receivers.rs new file mode 100644 index 0000000000..9884827495 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/event_receivers.rs @@ -0,0 +1,15 @@ +use super::*; + +#[test] +fn event_bindings_validate_native_receivers_before_conversion_in_the_callee_realm() { + let mut vm = new_parsed_test_vm( + "https://event-receivers.test/", + "
", + ); + vm.eval(include_str!("event_receivers.js")).unwrap(); + assert_eq!( + vm.eval("JSON.stringify(globalThis.__eventReceiverFailures)") + .unwrap(), + "[]" + ); +} diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 7cf085e16c..bf96eff0ba 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -2133,6 +2133,7 @@ mod device_events; mod dom_elements; mod dom_xhr; mod element_click; +mod event_receivers; mod headers_list; mod http_fixture; mod import_meta;