|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 "use strict"; |
|
5 |
|
6 module.metadata = { |
|
7 "stability": "unstable" |
|
8 }; |
|
9 |
|
10 const { Class } = require('../core/heritage'); |
|
11 const { EventTarget } = require('../event/target'); |
|
12 const { on, off, emit, setListeners } = require('../event/core'); |
|
13 const { |
|
14 attach, detach, destroy |
|
15 } = require('./utils'); |
|
16 const { method } = require('../lang/functional'); |
|
17 const { Ci, Cu, Cc } = require('chrome'); |
|
18 const unload = require('../system/unload'); |
|
19 const events = require('../system/events'); |
|
20 const { getInnerId } = require("../window/utils"); |
|
21 const { WorkerSandbox } = require('./sandbox'); |
|
22 const { getTabForWindow } = require('../tabs/helpers'); |
|
23 |
|
24 // A weak map of workers to hold private attributes that |
|
25 // should not be exposed |
|
26 const workers = new WeakMap(); |
|
27 |
|
28 let modelFor = (worker) => workers.get(worker); |
|
29 |
|
30 const ERR_DESTROYED = |
|
31 "Couldn't find the worker to receive this message. " + |
|
32 "The script may not be initialized yet, or may already have been unloaded."; |
|
33 |
|
34 const ERR_FROZEN = "The page is currently hidden and can no longer be used " + |
|
35 "until it is visible again."; |
|
36 |
|
37 /** |
|
38 * Message-passing facility for communication between code running |
|
39 * in the content and add-on process. |
|
40 * @see https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/content/worker.html |
|
41 */ |
|
42 const Worker = Class({ |
|
43 implements: [EventTarget], |
|
44 initialize: function WorkerConstructor (options) { |
|
45 // Save model in weak map to not expose properties |
|
46 let model = createModel(); |
|
47 workers.set(this, model); |
|
48 |
|
49 options = options || {}; |
|
50 |
|
51 if ('contentScriptFile' in options) |
|
52 this.contentScriptFile = options.contentScriptFile; |
|
53 if ('contentScriptOptions' in options) |
|
54 this.contentScriptOptions = options.contentScriptOptions; |
|
55 if ('contentScript' in options) |
|
56 this.contentScript = options.contentScript; |
|
57 if ('injectInDocument' in options) |
|
58 this.injectInDocument = !!options.injectInDocument; |
|
59 |
|
60 setListeners(this, options); |
|
61 |
|
62 unload.ensure(this, "destroy"); |
|
63 |
|
64 // Ensure that worker.port is initialized for contentWorker to be able |
|
65 // to send events during worker initialization. |
|
66 this.port = createPort(this); |
|
67 |
|
68 model.documentUnload = documentUnload.bind(this); |
|
69 model.pageShow = pageShow.bind(this); |
|
70 model.pageHide = pageHide.bind(this); |
|
71 |
|
72 if ('window' in options) |
|
73 attach(this, options.window); |
|
74 }, |
|
75 |
|
76 /** |
|
77 * Sends a message to the worker's global scope. Method takes single |
|
78 * argument, which represents data to be sent to the worker. The data may |
|
79 * be any primitive type value or `JSON`. Call of this method asynchronously |
|
80 * emits `message` event with data value in the global scope of this |
|
81 * symbiont. |
|
82 * |
|
83 * `message` event listeners can be set either by calling |
|
84 * `self.on` with a first argument string `"message"` or by |
|
85 * implementing `onMessage` function in the global scope of this worker. |
|
86 * @param {Number|String|JSON} data |
|
87 */ |
|
88 postMessage: function (...data) { |
|
89 let model = modelFor(this); |
|
90 let args = ['message'].concat(data); |
|
91 if (!model.inited) { |
|
92 model.earlyEvents.push(args); |
|
93 return; |
|
94 } |
|
95 processMessage.apply(null, [this].concat(args)); |
|
96 }, |
|
97 |
|
98 get url () { |
|
99 let model = modelFor(this); |
|
100 // model.window will be null after detach |
|
101 return model.window ? model.window.document.location.href : null; |
|
102 }, |
|
103 |
|
104 get contentURL () { |
|
105 let model = modelFor(this); |
|
106 return model.window ? model.window.document.URL : null; |
|
107 }, |
|
108 |
|
109 get tab () { |
|
110 let model = modelFor(this); |
|
111 // model.window will be null after detach |
|
112 if (model.window) |
|
113 return getTabForWindow(model.window); |
|
114 return null; |
|
115 }, |
|
116 |
|
117 // Implemented to provide some of the previous features of exposing sandbox |
|
118 // so that Worker can be extended |
|
119 getSandbox: function () { |
|
120 return modelFor(this).contentWorker; |
|
121 }, |
|
122 |
|
123 toString: function () { return '[object Worker]'; }, |
|
124 attach: method(attach), |
|
125 detach: method(detach), |
|
126 destroy: method(destroy) |
|
127 }); |
|
128 exports.Worker = Worker; |
|
129 |
|
130 attach.define(Worker, function (worker, window) { |
|
131 let model = modelFor(worker); |
|
132 model.window = window; |
|
133 // Track document unload to destroy this worker. |
|
134 // We can't watch for unload event on page's window object as it |
|
135 // prevents bfcache from working: |
|
136 // https://developer.mozilla.org/En/Working_with_BFCache |
|
137 model.windowID = getInnerId(model.window); |
|
138 events.on("inner-window-destroyed", model.documentUnload); |
|
139 |
|
140 // Listen to pagehide event in order to freeze the content script |
|
141 // while the document is frozen in bfcache: |
|
142 model.window.addEventListener("pageshow", model.pageShow, true); |
|
143 model.window.addEventListener("pagehide", model.pageHide, true); |
|
144 |
|
145 // will set model.contentWorker pointing to the private API: |
|
146 model.contentWorker = WorkerSandbox(worker, model.window); |
|
147 |
|
148 // Mainly enable worker.port.emit to send event to the content worker |
|
149 model.inited = true; |
|
150 model.frozen = false; |
|
151 |
|
152 // Fire off `attach` event |
|
153 emit(worker, 'attach', window); |
|
154 |
|
155 // Process all events and messages that were fired before the |
|
156 // worker was initialized. |
|
157 model.earlyEvents.forEach(args => processMessage.apply(null, [worker].concat(args))); |
|
158 }); |
|
159 |
|
160 /** |
|
161 * Remove all internal references to the attached document |
|
162 * Tells _port to unload itself and removes all the references from itself. |
|
163 */ |
|
164 detach.define(Worker, function (worker, reason) { |
|
165 let model = modelFor(worker); |
|
166 |
|
167 // maybe unloaded before content side is created |
|
168 if (model.contentWorker) { |
|
169 model.contentWorker.destroy(reason); |
|
170 } |
|
171 |
|
172 model.contentWorker = null; |
|
173 if (model.window) { |
|
174 model.window.removeEventListener("pageshow", model.pageShow, true); |
|
175 model.window.removeEventListener("pagehide", model.pageHide, true); |
|
176 } |
|
177 model.window = null; |
|
178 // This method may be called multiple times, |
|
179 // avoid dispatching `detach` event more than once |
|
180 if (model.windowID) { |
|
181 model.windowID = null; |
|
182 events.off("inner-window-destroyed", model.documentUnload); |
|
183 model.earlyEvents.length = 0; |
|
184 emit(worker, 'detach'); |
|
185 } |
|
186 model.inited = false; |
|
187 }); |
|
188 |
|
189 /** |
|
190 * Tells content worker to unload itself and |
|
191 * removes all the references from itself. |
|
192 */ |
|
193 destroy.define(Worker, function (worker, reason) { |
|
194 detach(worker, reason); |
|
195 modelFor(worker).inited = true; |
|
196 // Specifying no type or listener removes all listeners |
|
197 // from target |
|
198 off(worker); |
|
199 }); |
|
200 |
|
201 /** |
|
202 * Events fired by workers |
|
203 */ |
|
204 function documentUnload ({ subject, data }) { |
|
205 let model = modelFor(this); |
|
206 let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; |
|
207 if (innerWinID != model.windowID) return false; |
|
208 detach(this); |
|
209 return true; |
|
210 } |
|
211 |
|
212 function pageShow () { |
|
213 let model = modelFor(this); |
|
214 model.contentWorker.emitSync('pageshow'); |
|
215 emit(this, 'pageshow'); |
|
216 model.frozen = false; |
|
217 } |
|
218 |
|
219 function pageHide () { |
|
220 let model = modelFor(this); |
|
221 model.contentWorker.emitSync('pagehide'); |
|
222 emit(this, 'pagehide'); |
|
223 model.frozen = true; |
|
224 } |
|
225 |
|
226 /** |
|
227 * Fired from postMessage and emitEventToContent, or from the earlyMessage |
|
228 * queue when fired before the content is loaded. Sends arguments to |
|
229 * contentWorker if able |
|
230 */ |
|
231 |
|
232 function processMessage (worker, ...args) { |
|
233 let model = modelFor(worker) || {}; |
|
234 if (!model.contentWorker) |
|
235 throw new Error(ERR_DESTROYED); |
|
236 if (model.frozen) |
|
237 throw new Error(ERR_FROZEN); |
|
238 model.contentWorker.emit.apply(null, args); |
|
239 } |
|
240 |
|
241 function createModel () { |
|
242 return { |
|
243 // List of messages fired before worker is initialized |
|
244 earlyEvents: [], |
|
245 // Is worker connected to the content worker sandbox ? |
|
246 inited: false, |
|
247 // Is worker being frozen? i.e related document is frozen in bfcache. |
|
248 // Content script should not be reachable if frozen. |
|
249 frozen: true, |
|
250 /** |
|
251 * Reference to the content side of the worker. |
|
252 * @type {WorkerGlobalScope} |
|
253 */ |
|
254 contentWorker: null, |
|
255 /** |
|
256 * Reference to the window that is accessible from |
|
257 * the content scripts. |
|
258 * @type {Object} |
|
259 */ |
|
260 window: null |
|
261 }; |
|
262 } |
|
263 |
|
264 function createPort (worker) { |
|
265 let port = EventTarget(); |
|
266 port.emit = emitEventToContent.bind(null, worker); |
|
267 return port; |
|
268 } |
|
269 |
|
270 /** |
|
271 * Emit a custom event to the content script, |
|
272 * i.e. emit this event on `self.port` |
|
273 */ |
|
274 function emitEventToContent (worker, ...eventArgs) { |
|
275 let model = modelFor(worker); |
|
276 let args = ['event'].concat(eventArgs); |
|
277 if (!model.inited) { |
|
278 model.earlyEvents.push(args); |
|
279 return; |
|
280 } |
|
281 processMessage.apply(null, [worker].concat(args)); |
|
282 } |