|
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 |
|
5 const ContentWorker = Object.freeze({ |
|
6 // TODO: Bug 727854 Use same implementation than common JS modules, |
|
7 // i.e. EventEmitter module |
|
8 |
|
9 /** |
|
10 * Create an EventEmitter instance. |
|
11 */ |
|
12 createEventEmitter: function createEventEmitter(emit) { |
|
13 let listeners = Object.create(null); |
|
14 let eventEmitter = Object.freeze({ |
|
15 emit: emit, |
|
16 on: function on(name, callback) { |
|
17 if (typeof callback !== "function") |
|
18 return this; |
|
19 if (!(name in listeners)) |
|
20 listeners[name] = []; |
|
21 listeners[name].push(callback); |
|
22 return this; |
|
23 }, |
|
24 once: function once(name, callback) { |
|
25 eventEmitter.on(name, function onceCallback() { |
|
26 eventEmitter.removeListener(name, onceCallback); |
|
27 callback.apply(callback, arguments); |
|
28 }); |
|
29 }, |
|
30 removeListener: function removeListener(name, callback) { |
|
31 if (!(name in listeners)) |
|
32 return; |
|
33 let index = listeners[name].indexOf(callback); |
|
34 if (index == -1) |
|
35 return; |
|
36 listeners[name].splice(index, 1); |
|
37 } |
|
38 }); |
|
39 function onEvent(name) { |
|
40 if (!(name in listeners)) |
|
41 return []; |
|
42 let args = Array.slice(arguments, 1); |
|
43 let results = []; |
|
44 for each (let callback in listeners[name]) { |
|
45 results.push(callback.apply(null, args)); |
|
46 } |
|
47 return results; |
|
48 } |
|
49 function hasListenerFor(name) { |
|
50 if (!(name in listeners)) |
|
51 return false; |
|
52 return listeners[name].length > 0; |
|
53 } |
|
54 return { |
|
55 eventEmitter: eventEmitter, |
|
56 emit: onEvent, |
|
57 hasListenerFor: hasListenerFor |
|
58 }; |
|
59 }, |
|
60 |
|
61 /** |
|
62 * Create an EventEmitter instance to communicate with chrome module |
|
63 * by passing only strings between compartments. |
|
64 * This function expects `emitToChrome` function, that allows to send |
|
65 * events to the chrome module. It returns the EventEmitter as `pipe` |
|
66 * attribute, and, `onChromeEvent` a function that allows chrome module |
|
67 * to send event into the EventEmitter. |
|
68 * |
|
69 * pipe.emit --> emitToChrome |
|
70 * onChromeEvent --> callback registered through pipe.on |
|
71 */ |
|
72 createPipe: function createPipe(emitToChrome) { |
|
73 function onEvent(type, ...args) { |
|
74 // JSON.stringify is buggy with cross-sandbox values, |
|
75 // it may return "{}" on functions. Use a replacer to match them correctly. |
|
76 let replacer = (k, v) => |
|
77 typeof(v) === "function" |
|
78 ? (type === "console" ? Function.toString.call(v) : void(0)) |
|
79 : v; |
|
80 |
|
81 let str = JSON.stringify([type, ...args], replacer); |
|
82 emitToChrome(str); |
|
83 } |
|
84 |
|
85 let { eventEmitter, emit, hasListenerFor } = |
|
86 ContentWorker.createEventEmitter(onEvent); |
|
87 |
|
88 return { |
|
89 pipe: eventEmitter, |
|
90 onChromeEvent: function onChromeEvent(array) { |
|
91 // We either receive a stringified array, or a real array. |
|
92 // We still allow to pass an array of objects, in WorkerSandbox.emitSync |
|
93 // in order to allow sending DOM node reference between content script |
|
94 // and modules (only used for context-menu API) |
|
95 let args = typeof array == "string" ? JSON.parse(array) : array; |
|
96 return emit.apply(null, args); |
|
97 }, |
|
98 hasListenerFor: hasListenerFor |
|
99 }; |
|
100 }, |
|
101 |
|
102 injectConsole: function injectConsole(exports, pipe) { |
|
103 exports.console = Object.freeze({ |
|
104 log: pipe.emit.bind(null, "console", "log"), |
|
105 info: pipe.emit.bind(null, "console", "info"), |
|
106 warn: pipe.emit.bind(null, "console", "warn"), |
|
107 error: pipe.emit.bind(null, "console", "error"), |
|
108 debug: pipe.emit.bind(null, "console", "debug"), |
|
109 exception: pipe.emit.bind(null, "console", "exception"), |
|
110 trace: pipe.emit.bind(null, "console", "trace"), |
|
111 time: pipe.emit.bind(null, "console", "time"), |
|
112 timeEnd: pipe.emit.bind(null, "console", "timeEnd") |
|
113 }); |
|
114 }, |
|
115 |
|
116 injectTimers: function injectTimers(exports, chromeAPI, pipe, console) { |
|
117 // wrapped functions from `'timer'` module. |
|
118 // Wrapper adds `try catch` blocks to the callbacks in order to |
|
119 // emit `error` event on a symbiont if exception is thrown in |
|
120 // the Worker global scope. |
|
121 // @see http://www.w3.org/TR/workers/#workerutils |
|
122 |
|
123 // List of all living timeouts/intervals |
|
124 let _timers = Object.create(null); |
|
125 |
|
126 // Keep a reference to original timeout functions |
|
127 let { |
|
128 setTimeout: chromeSetTimeout, |
|
129 setInterval: chromeSetInterval, |
|
130 clearTimeout: chromeClearTimeout, |
|
131 clearInterval: chromeClearInterval |
|
132 } = chromeAPI.timers; |
|
133 |
|
134 function registerTimer(timer) { |
|
135 let registerMethod = null; |
|
136 if (timer.kind == "timeout") |
|
137 registerMethod = chromeSetTimeout; |
|
138 else if (timer.kind == "interval") |
|
139 registerMethod = chromeSetInterval; |
|
140 else |
|
141 throw new Error("Unknown timer kind: " + timer.kind); |
|
142 |
|
143 if (typeof timer.fun == 'string') { |
|
144 let code = timer.fun; |
|
145 timer.fun = () => chromeAPI.sandbox.evaluate(exports, code); |
|
146 } else if (typeof timer.fun != 'function') { |
|
147 throw new Error('Unsupported callback type' + typeof timer.fun); |
|
148 } |
|
149 |
|
150 let id = registerMethod(onFire, timer.delay); |
|
151 function onFire() { |
|
152 try { |
|
153 if (timer.kind == "timeout") |
|
154 delete _timers[id]; |
|
155 timer.fun.apply(null, timer.args); |
|
156 } catch(e) { |
|
157 console.exception(e); |
|
158 let wrapper = { |
|
159 instanceOfError: instanceOf(e, Error), |
|
160 value: e, |
|
161 }; |
|
162 if (wrapper.instanceOfError) { |
|
163 wrapper.value = { |
|
164 message: e.message, |
|
165 fileName: e.fileName, |
|
166 lineNumber: e.lineNumber, |
|
167 stack: e.stack, |
|
168 name: e.name, |
|
169 }; |
|
170 } |
|
171 pipe.emit('error', wrapper); |
|
172 } |
|
173 } |
|
174 _timers[id] = timer; |
|
175 return id; |
|
176 } |
|
177 |
|
178 // copied from sdk/lang/type.js since modules are not available here |
|
179 function instanceOf(value, Type) { |
|
180 var isConstructorNameSame; |
|
181 var isConstructorSourceSame; |
|
182 |
|
183 // If `instanceof` returned `true` we know result right away. |
|
184 var isInstanceOf = value instanceof Type; |
|
185 |
|
186 // If `instanceof` returned `false` we do ducktype check since `Type` may be |
|
187 // from a different sandbox. If a constructor of the `value` or a constructor |
|
188 // of the value's prototype has same name and source we assume that it's an |
|
189 // instance of the Type. |
|
190 if (!isInstanceOf && value) { |
|
191 isConstructorNameSame = value.constructor.name === Type.name; |
|
192 isConstructorSourceSame = String(value.constructor) == String(Type); |
|
193 isInstanceOf = (isConstructorNameSame && isConstructorSourceSame) || |
|
194 instanceOf(Object.getPrototypeOf(value), Type); |
|
195 } |
|
196 return isInstanceOf; |
|
197 } |
|
198 |
|
199 function unregisterTimer(id) { |
|
200 if (!(id in _timers)) |
|
201 return; |
|
202 let { kind } = _timers[id]; |
|
203 delete _timers[id]; |
|
204 if (kind == "timeout") |
|
205 chromeClearTimeout(id); |
|
206 else if (kind == "interval") |
|
207 chromeClearInterval(id); |
|
208 else |
|
209 throw new Error("Unknown timer kind: " + kind); |
|
210 } |
|
211 |
|
212 function disableAllTimers() { |
|
213 Object.keys(_timers).forEach(unregisterTimer); |
|
214 } |
|
215 |
|
216 exports.setTimeout = function ContentScriptSetTimeout(callback, delay) { |
|
217 return registerTimer({ |
|
218 kind: "timeout", |
|
219 fun: callback, |
|
220 delay: delay, |
|
221 args: Array.slice(arguments, 2) |
|
222 }); |
|
223 }; |
|
224 exports.clearTimeout = function ContentScriptClearTimeout(id) { |
|
225 unregisterTimer(id); |
|
226 }; |
|
227 |
|
228 exports.setInterval = function ContentScriptSetInterval(callback, delay) { |
|
229 return registerTimer({ |
|
230 kind: "interval", |
|
231 fun: callback, |
|
232 delay: delay, |
|
233 args: Array.slice(arguments, 2) |
|
234 }); |
|
235 }; |
|
236 exports.clearInterval = function ContentScriptClearInterval(id) { |
|
237 unregisterTimer(id); |
|
238 }; |
|
239 |
|
240 // On page-hide, save a list of all existing timers before disabling them, |
|
241 // in order to be able to restore them on page-show. |
|
242 // These events are fired when the page goes in/out of bfcache. |
|
243 // https://developer.mozilla.org/En/Working_with_BFCache |
|
244 let frozenTimers = []; |
|
245 pipe.on("pageshow", function onPageShow() { |
|
246 frozenTimers.forEach(registerTimer); |
|
247 }); |
|
248 pipe.on("pagehide", function onPageHide() { |
|
249 frozenTimers = []; |
|
250 for (let id in _timers) |
|
251 frozenTimers.push(_timers[id]); |
|
252 disableAllTimers(); |
|
253 // Some other pagehide listeners may register some timers that won't be |
|
254 // frozen as this particular pagehide listener is called first. |
|
255 // So freeze these timers on next cycle. |
|
256 chromeSetTimeout(function () { |
|
257 for (let id in _timers) |
|
258 frozenTimers.push(_timers[id]); |
|
259 disableAllTimers(); |
|
260 }, 0); |
|
261 }); |
|
262 |
|
263 // Unregister all timers when the page is destroyed |
|
264 // (i.e. when it is removed from bfcache) |
|
265 pipe.on("detach", function clearTimeouts() { |
|
266 disableAllTimers(); |
|
267 _timers = {}; |
|
268 frozenTimers = []; |
|
269 }); |
|
270 }, |
|
271 |
|
272 injectMessageAPI: function injectMessageAPI(exports, pipe, console) { |
|
273 |
|
274 let { eventEmitter: port, emit : portEmit } = |
|
275 ContentWorker.createEventEmitter(pipe.emit.bind(null, "event")); |
|
276 pipe.on("event", portEmit); |
|
277 |
|
278 let self = { |
|
279 port: port, |
|
280 postMessage: pipe.emit.bind(null, "message"), |
|
281 on: pipe.on.bind(null), |
|
282 once: pipe.once.bind(null), |
|
283 removeListener: pipe.removeListener.bind(null), |
|
284 }; |
|
285 Object.defineProperty(exports, "self", { |
|
286 value: self |
|
287 }); |
|
288 |
|
289 exports.on = function deprecatedOn() { |
|
290 console.error("DEPRECATED: The global `on()` function in content " + |
|
291 "scripts is deprecated in favor of the `self.on()` " + |
|
292 "function, which works the same. Replace calls to `on()` " + |
|
293 "with calls to `self.on()`" + |
|
294 "For more info on `self.on`, see " + |
|
295 "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>."); |
|
296 return self.on.apply(null, arguments); |
|
297 }; |
|
298 |
|
299 // Deprecated use of `onMessage` from globals |
|
300 let onMessage = null; |
|
301 Object.defineProperty(exports, "onMessage", { |
|
302 get: function () onMessage, |
|
303 set: function (v) { |
|
304 if (onMessage) |
|
305 self.removeListener("message", onMessage); |
|
306 console.error("DEPRECATED: The global `onMessage` function in content" + |
|
307 "scripts is deprecated in favor of the `self.on()` " + |
|
308 "function. Replace `onMessage = function (data){}` " + |
|
309 "definitions with calls to `self.on('message', " + |
|
310 "function (data){})`. " + |
|
311 "For more info on `self.on`, see " + |
|
312 "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>."); |
|
313 onMessage = v; |
|
314 if (typeof onMessage == "function") |
|
315 self.on("message", onMessage); |
|
316 } |
|
317 }); |
|
318 }, |
|
319 |
|
320 injectOptions: function (exports, options) { |
|
321 Object.defineProperty( exports.self, "options", { value: JSON.parse( options ) }); |
|
322 }, |
|
323 |
|
324 inject: function (exports, chromeAPI, emitToChrome, options) { |
|
325 let { pipe, onChromeEvent, hasListenerFor } = |
|
326 ContentWorker.createPipe(emitToChrome); |
|
327 |
|
328 ContentWorker.injectConsole(exports, pipe); |
|
329 ContentWorker.injectTimers(exports, chromeAPI, pipe, exports.console); |
|
330 ContentWorker.injectMessageAPI(exports, pipe, exports.console); |
|
331 if ( options !== undefined ) { |
|
332 ContentWorker.injectOptions(exports, options); |
|
333 } |
|
334 |
|
335 Object.freeze( exports.self ); |
|
336 |
|
337 return { |
|
338 emitToContent: onChromeEvent, |
|
339 hasListenerFor: hasListenerFor |
|
340 }; |
|
341 } |
|
342 }); |