Source code

Revision control

Other Tools

1
/* vim: set ts=2 sw=2 sts=2 et tw=80: */
2
/* This Source Code Form is subject to the terms of the Mozilla Public
3
* License, v. 2.0. If a copy of the MPL was not distributed with this
4
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
"use strict";
6
7
var EXPORTED_SYMBOLS = ["ContentEventListenerChild"];
8
9
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
10
11
class ContentEventListenerChild extends JSWindowActorChild {
12
actorCreated() {
13
this._contentEvents = new Map();
14
this._shutdown = false;
15
this._chromeEventHandler = null;
16
Services.cpmm.sharedData.addEventListener("change", this);
17
}
18
19
willDestroy() {
20
this._shutdown = true;
21
Services.cpmm.sharedData.removeEventListener("change", this);
22
this._updateContentEventListeners(/* clearListeners = */ true);
23
if (this._contentEvents.size != 0) {
24
throw new Error(`Didn't expect content events after willDestroy`);
25
}
26
}
27
28
handleEvent(event) {
29
switch (event.type) {
30
case "DOMWindowCreated": {
31
this._updateContentEventListeners();
32
break;
33
}
34
35
case "change": {
36
if (
37
!event.changedKeys.includes("BrowserTestUtils:ContentEventListener")
38
) {
39
return;
40
}
41
this._updateContentEventListeners();
42
break;
43
}
44
}
45
}
46
47
/**
48
* This method first determines the desired set of content event listeners
49
* for the window. This is either the empty set, if clearListeners is true,
50
* or is retrieved from the message manager's shared data. It then compares
51
* this event listener data to the existing set of listeners that we have
52
* registered, as recorded in this._contentEvents. Each content event listener
53
* has been assigned a unique id by the parent process. If a listener was
54
* added, but is not in the new event data, it is removed. If a listener was
55
* not present, but is in the new event data, it is added. If it is in both,
56
* then a basic check is done to see if they are the same.
57
*
58
* @param {bool} clearListeners [optional]
59
* If this is true, then instead of checking shared data to decide
60
* what the desired set of listeners is, just use the empty set. This
61
* will result in any existing listeners being cleared, and is used
62
* when the window is going away.
63
*/
64
_updateContentEventListeners(clearListeners = false) {
65
// If we've already begun the destruction process, any new event
66
// listeners for our bc id can't possibly really be for us, so ignore them.
67
if (this._shutdown && !clearListeners) {
68
throw new Error(
69
"Tried to update after we shut down content event listening"
70
);
71
}
72
73
let newEventData;
74
if (!clearListeners) {
75
newEventData = Services.cpmm.sharedData.get(
76
"BrowserTestUtils:ContentEventListener"
77
);
78
}
79
if (!newEventData) {
80
newEventData = new Map();
81
}
82
83
// Check that entries that continue to exist are the same and remove entries
84
// that no longer exist.
85
for (let [
86
listenerId,
87
{ eventName, listener, listenerOptions },
88
] of this._contentEvents.entries()) {
89
let newData = newEventData.get(listenerId);
90
if (newData) {
91
if (newData.eventName !== eventName) {
92
// Could potentially check if listenerOptions are the same, but
93
// checkFnSource can't be checked unless we store it, and this is
94
// just a smoke test anyways, so don't bother.
95
throw new Error(
96
"Got new content event listener that disagreed with existing data"
97
);
98
}
99
continue;
100
}
101
if (!this._chromeEventHandler) {
102
throw new Error(
103
"Trying to remove an event listener for waitForContentEvent without a cached event handler"
104
);
105
}
106
this._chromeEventHandler.removeEventListener(
107
eventName,
108
listener,
109
listenerOptions
110
);
111
this._contentEvents.delete(listenerId);
112
}
113
114
let actorChild = this;
115
116
// Add in new entries.
117
for (let [
118
listenerId,
119
{ eventName, listenerOptions, checkFnSource },
120
] of newEventData.entries()) {
121
let oldData = this._contentEvents.get(listenerId);
122
if (oldData) {
123
// We checked that the data is the same in the previous loop.
124
continue;
125
}
126
127
/* eslint-disable no-eval */
128
let checkFn;
129
if (checkFnSource) {
130
checkFn = eval(`(() => (${unescape(checkFnSource)}))()`);
131
}
132
/* eslint-enable no-eval */
133
134
function listener(event) {
135
if (checkFn && !checkFn(event)) {
136
return;
137
}
138
actorChild.sendAsyncMessage("ContentEventListener:Run", {
139
listenerId,
140
});
141
}
142
143
// Cache the chrome event handler because this.docShell won't be
144
// available during shut down.
145
if (!this._chromeEventHandler) {
146
try {
147
this._chromeEventHandler = this.docShell.chromeEventHandler;
148
} catch (error) {
149
if (error.name === "InvalidStateError") {
150
// We'll arrive here if we no longer have our manager, so we can
151
// just swallow this error.
152
continue;
153
}
154
throw error;
155
}
156
}
157
158
// Some windows, like top-level browser windows, maybe not have a chrome
159
// event handler set up as this point, but we don't actually care about
160
// events on those windows, so ignore them.
161
if (!this._chromeEventHandler) {
162
continue;
163
}
164
165
this._chromeEventHandler.addEventListener(
166
eventName,
167
listener,
168
listenerOptions
169
);
170
this._contentEvents.set(listenerId, {
171
eventName,
172
listener,
173
listenerOptions,
174
});
175
}
176
177
// If there are no active content events, clear our reference to the chrome
178
// event handler to prevent leaks.
179
if (this._contentEvents.size == 0) {
180
this._chromeEventHandler = null;
181
}
182
}
183
}