events: add brand checks for detached accessors

PR-URL: https://github.com/nodejs/node/pull/39773
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
James M Snell
2021-08-15 08:37:20 -07:00
parent 9c16305a3b
commit 4ec64e320f
2 changed files with 237 additions and 40 deletions

View File

@@ -8,6 +8,7 @@ const {
FunctionPrototypeCall,
NumberIsInteger,
ObjectAssign,
ObjectCreate,
ObjectDefineProperties,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptor,
@@ -39,6 +40,7 @@ const { customInspectSymbol } = require('internal/util');
const { inspect } = require('util');
const kIsEventTarget = SymbolFor('nodejs.event_target');
const kIsNodeEventTarget = Symbol('kIsNodeEventTarget');
const EventEmitter = require('events');
const {
@@ -80,6 +82,10 @@ const isTrusted = ObjectGetOwnPropertyDescriptor({
}
}, 'isTrusted').get;
function isEvent(value) {
return typeof value?.[kType] === 'string';
}
class Event {
constructor(type, options = null) {
if (arguments.length === 0)
@@ -110,6 +116,8 @@ class Event {
}
[customInspectSymbol](depth, options) {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
const name = this.constructor.name;
if (depth < 0)
return name;
@@ -127,46 +135,111 @@ class Event {
}
stopImmediatePropagation() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
this[kStop] = true;
}
preventDefault() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
this[kDefaultPrevented] = true;
}
get target() { return this[kTarget]; }
get currentTarget() { return this[kTarget]; }
get srcElement() { return this[kTarget]; }
get target() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this[kTarget];
}
get type() { return this[kType]; }
get currentTarget() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this[kTarget];
}
get cancelable() { return this[kCancelable]; }
get srcElement() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this[kTarget];
}
get type() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this[kType];
}
get cancelable() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this[kCancelable];
}
get defaultPrevented() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this[kCancelable] && this[kDefaultPrevented];
}
get timeStamp() { return this[kTimestamp]; }
get timeStamp() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this[kTimestamp];
}
// The following are non-op and unused properties/methods from Web API Event.
// These are not supported in Node.js and are provided purely for
// API completeness.
composedPath() { return this[kIsBeingDispatched] ? [this[kTarget]] : []; }
get returnValue() { return !this.defaultPrevented; }
get bubbles() { return this[kBubbles]; }
get composed() { return this[kComposed]; }
composedPath() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this[kIsBeingDispatched] ? [this[kTarget]] : [];
}
get returnValue() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return !this.defaultPrevented;
}
get bubbles() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this[kBubbles];
}
get composed() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this[kComposed];
}
get eventPhase() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this[kIsBeingDispatched] ? Event.AT_TARGET : Event.NONE;
}
get cancelBubble() { return this[kPropagationStopped]; }
get cancelBubble() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
return this[kPropagationStopped];
}
set cancelBubble(value) {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
if (value) {
this.stopPropagation();
}
}
stopPropagation() {
if (!isEvent(this))
throw new ERR_INVALID_THIS('Event');
this[kPropagationStopped] = true;
}
@@ -176,12 +249,34 @@ class Event {
static BUBBLING_PHASE = 3;
}
ObjectDefineProperty(Event.prototype, SymbolToStringTag, {
writable: false,
enumerable: false,
configurable: true,
value: 'Event',
});
const kEnumerableProperty = ObjectCreate(null);
kEnumerableProperty.enumerable = true;
ObjectDefineProperties(
Event.prototype, {
[SymbolToStringTag]: {
writable: false,
enumerable: false,
configurable: true,
value: 'Event',
},
stopImmediatePropagation: kEnumerableProperty,
preventDefault: kEnumerableProperty,
target: kEnumerableProperty,
currentTarget: kEnumerableProperty,
srcElement: kEnumerableProperty,
type: kEnumerableProperty,
cancelable: kEnumerableProperty,
defaultPrevented: kEnumerableProperty,
timeStamp: kEnumerableProperty,
composedPath: kEnumerableProperty,
returnValue: kEnumerableProperty,
bubbles: kEnumerableProperty,
composed: kEnumerableProperty,
eventPhase: kEnumerableProperty,
cancelBubble: kEnumerableProperty,
stopPropagation: kEnumerableProperty,
});
class NodeCustomEvent extends Event {
constructor(type, options) {
@@ -297,6 +392,8 @@ class EventTarget {
[kRemoveListener](size, type, listener, capture) {}
addEventListener(type, listener, options = {}) {
if (!isEventTarget(this))
throw new ERR_INVALID_THIS('EventTarget');
if (arguments.length < 2)
throw new ERR_MISSING_ARGS('type', 'listener');
@@ -368,6 +465,8 @@ class EventTarget {
}
removeEventListener(type, listener, options = {}) {
if (!isEventTarget(this))
throw new ERR_INVALID_THIS('EventTarget');
if (!shouldAddListener(listener))
return;
@@ -393,12 +492,12 @@ class EventTarget {
}
dispatchEvent(event) {
if (!(event instanceof Event))
throw new ERR_INVALID_ARG_TYPE('event', 'Event', event);
if (!isEventTarget(this))
throw new ERR_INVALID_THIS('EventTarget');
if (!(event instanceof Event))
throw new ERR_INVALID_ARG_TYPE('event', 'Event', event);
if (event[kIsBeingDispatched])
throw new ERR_EVENT_RECURSION(event.type);
@@ -479,6 +578,8 @@ class EventTarget {
return new NodeCustomEvent(type, { detail: nodeValue });
}
[customInspectSymbol](depth, options) {
if (!isEventTarget(this))
throw new ERR_INVALID_THIS('EventTarget');
const name = this.constructor.name;
if (depth < 0)
return name;
@@ -492,15 +593,15 @@ class EventTarget {
}
ObjectDefineProperties(EventTarget.prototype, {
addEventListener: { enumerable: true },
removeEventListener: { enumerable: true },
dispatchEvent: { enumerable: true }
});
ObjectDefineProperty(EventTarget.prototype, SymbolToStringTag, {
writable: false,
enumerable: false,
configurable: true,
value: 'EventTarget',
addEventListener: kEnumerableProperty,
removeEventListener: kEnumerableProperty,
dispatchEvent: kEnumerableProperty,
[SymbolToStringTag]: {
writable: false,
enumerable: false,
configurable: true,
value: 'EventTarget',
}
});
function initNodeEventTarget(self) {
@@ -508,6 +609,7 @@ function initNodeEventTarget(self) {
}
class NodeEventTarget extends EventTarget {
static [kIsNodeEventTarget] = true;
static defaultMaxListeners = 10;
constructor() {
@@ -516,42 +618,60 @@ class NodeEventTarget extends EventTarget {
}
setMaxListeners(n) {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
EventEmitter.setMaxListeners(n, this);
}
getMaxListeners() {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
return this[kMaxEventTargetListeners];
}
eventNames() {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
return ArrayFrom(this[kEvents].keys());
}
listenerCount(type) {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
const root = this[kEvents].get(String(type));
return root !== undefined ? root.size : 0;
}
off(type, listener, options) {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
this.removeEventListener(type, listener, options);
return this;
}
removeListener(type, listener, options) {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
this.removeEventListener(type, listener, options);
return this;
}
on(type, listener) {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
this.addEventListener(type, listener, { [kIsNodeStyleListener]: true });
return this;
}
addListener(type, listener) {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
this.addEventListener(type, listener, { [kIsNodeStyleListener]: true });
return this;
}
emit(type, arg) {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
validateString(type, 'type');
const hadListeners = this.listenerCount(type) > 0;
this[kHybridDispatch](arg, type);
@@ -559,12 +679,16 @@ class NodeEventTarget extends EventTarget {
}
once(type, listener) {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
this.addEventListener(type, listener,
{ once: true, [kIsNodeStyleListener]: true });
return this;
}
removeAllListeners(type) {
if (!isNodeEventTarget(this))
throw new ERR_INVALID_THIS('NodeEventTarget');
if (type !== undefined) {
this[kEvents].delete(String(type));
} else {
@@ -576,17 +700,17 @@ class NodeEventTarget extends EventTarget {
}
ObjectDefineProperties(NodeEventTarget.prototype, {
setMaxListeners: { enumerable: true },
getMaxListeners: { enumerable: true },
eventNames: { enumerable: true },
listenerCount: { enumerable: true },
off: { enumerable: true },
removeListener: { enumerable: true },
on: { enumerable: true },
addListener: { enumerable: true },
once: { enumerable: true },
emit: { enumerable: true },
removeAllListeners: { enumerable: true },
setMaxListeners: kEnumerableProperty,
getMaxListeners: kEnumerableProperty,
eventNames: kEnumerableProperty,
listenerCount: kEnumerableProperty,
off: kEnumerableProperty,
removeListener: kEnumerableProperty,
on: kEnumerableProperty,
addListener: kEnumerableProperty,
once: kEnumerableProperty,
emit: kEnumerableProperty,
removeAllListeners: kEnumerableProperty,
});
// EventTarget API
@@ -631,6 +755,10 @@ function isEventTarget(obj) {
return obj?.constructor?.[kIsEventTarget];
}
function isNodeEventTarget(obj) {
return obj?.constructor?.[kIsNodeEventTarget];
}
function addCatch(promise) {
const then = promise.then;
if (typeof then === 'function') {

View File

@@ -0,0 +1,69 @@
// Flags: --expose-internals
'use strict';
require('../common');
const assert = require('assert');
const {
Event,
EventTarget,
NodeEventTarget,
} = require('internal/event_target');
[
'target',
'currentTarget',
'srcElement',
'type',
'cancelable',
'defaultPrevented',
'timeStamp',
'returnValue',
'bubbles',
'composed',
'eventPhase',
].forEach((i) => {
assert.throws(() => Reflect.get(Event.prototype, i, {}), {
code: 'ERR_INVALID_THIS',
});
});
[
'stopImmediatePropagation',
'preventDefault',
'composedPath',
'cancelBubble',
'stopPropagation',
].forEach((i) => {
assert.throws(() => Reflect.apply(Event.prototype[i], [], {}), {
code: 'ERR_INVALID_THIS',
});
});
[
'addEventListener',
'removeEventListener',
'dispatchEvent',
].forEach((i) => {
assert.throws(() => Reflect.apply(EventTarget.prototype[i], [], {}), {
code: 'ERR_INVALID_THIS',
});
});
[
'setMaxListeners',
'getMaxListeners',
'eventNames',
'listenerCount',
'off',
'removeListener',
'on',
'addListener',
'once',
'emit',
'removeAllListeners',
].forEach((i) => {
assert.throws(() => Reflect.apply(NodeEventTarget.prototype[i], [], {}), {
code: 'ERR_INVALID_THIS',
});
});