mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 16:01:00 +00:00
fix(webidl): validate Event receivers with native brands
An invalid receiver can reach Event callbacks, including a native initEvent path that can abort the process. Enable the existing WebIDL receiver checks on Event and the event interfaces already present on main, before callbacks and argument conversion. Genuine events and inherited event interfaces remain usable across realms. The regression matrix rejects forged objects, author and revoked Proxies without evaluating conversion hooks or Proxy traps, and checks TypeError in the callee realm. Existing spoofing tests retain their real-object assertions and now require TypeError for invalid receivers.
This commit is contained in:
@@ -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",
|
||||
|
||||
+19
-9
@@ -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}"#
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}"#
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
})()
|
||||
@@ -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/",
|
||||
"<!doctype html><body><iframe id=child></iframe></body>",
|
||||
);
|
||||
vm.eval(include_str!("event_receivers.js")).unwrap();
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify(globalThis.__eventReceiverFailures)")
|
||||
.unwrap(),
|
||||
"[]"
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user