Skip to content

Commit

Permalink
event_target: add brand checks for detached accessors
Browse files Browse the repository at this point in the history
  • Loading branch information
jasnell committed Aug 16, 2021
1 parent a28436b commit 5a9deb4
Show file tree
Hide file tree
Showing 2 changed files with 237 additions and 40 deletions.
208 changes: 168 additions & 40 deletions lib/internal/event_target.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const {
FunctionPrototypeCall,
NumberIsInteger,
ObjectAssign,
ObjectCreate,
ObjectDefineProperties,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptor,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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');

Expand Down Expand Up @@ -368,6 +465,8 @@ class EventTarget {
}

removeEventListener(type, listener, options = {}) {
if (!isEventTarget(this))
throw new ERR_INVALID_THIS('EventTarget');
if (!shouldAddListener(listener))
return;

Expand All @@ -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);

Expand Down Expand Up @@ -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;
Expand All @@ -492,22 +593,23 @@ 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) {
initEventTarget(self);
}

class NodeEventTarget extends EventTarget {
static [kIsNodeEventTarget] = true;
static defaultMaxListeners = 10;

constructor() {
Expand All @@ -516,55 +618,77 @@ 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);
return hadListeners;
}

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 {
Expand All @@ -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
Expand Down Expand Up @@ -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') {
Expand Down
Loading

0 comments on commit 5a9deb4

Please sign in to comment.