addon-sdk/source/lib/sdk/content/content-worker.js

branch
TOR_BUG_3246
changeset 7
129ffea94266
equal deleted inserted replaced
-1:000000000000 0:38b16f7b74d4
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 });

mercurial