1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/addon-sdk/source/lib/sdk/content/content-worker.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,342 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +const ContentWorker = Object.freeze({ 1.9 + // TODO: Bug 727854 Use same implementation than common JS modules, 1.10 + // i.e. EventEmitter module 1.11 + 1.12 + /** 1.13 + * Create an EventEmitter instance. 1.14 + */ 1.15 + createEventEmitter: function createEventEmitter(emit) { 1.16 + let listeners = Object.create(null); 1.17 + let eventEmitter = Object.freeze({ 1.18 + emit: emit, 1.19 + on: function on(name, callback) { 1.20 + if (typeof callback !== "function") 1.21 + return this; 1.22 + if (!(name in listeners)) 1.23 + listeners[name] = []; 1.24 + listeners[name].push(callback); 1.25 + return this; 1.26 + }, 1.27 + once: function once(name, callback) { 1.28 + eventEmitter.on(name, function onceCallback() { 1.29 + eventEmitter.removeListener(name, onceCallback); 1.30 + callback.apply(callback, arguments); 1.31 + }); 1.32 + }, 1.33 + removeListener: function removeListener(name, callback) { 1.34 + if (!(name in listeners)) 1.35 + return; 1.36 + let index = listeners[name].indexOf(callback); 1.37 + if (index == -1) 1.38 + return; 1.39 + listeners[name].splice(index, 1); 1.40 + } 1.41 + }); 1.42 + function onEvent(name) { 1.43 + if (!(name in listeners)) 1.44 + return []; 1.45 + let args = Array.slice(arguments, 1); 1.46 + let results = []; 1.47 + for each (let callback in listeners[name]) { 1.48 + results.push(callback.apply(null, args)); 1.49 + } 1.50 + return results; 1.51 + } 1.52 + function hasListenerFor(name) { 1.53 + if (!(name in listeners)) 1.54 + return false; 1.55 + return listeners[name].length > 0; 1.56 + } 1.57 + return { 1.58 + eventEmitter: eventEmitter, 1.59 + emit: onEvent, 1.60 + hasListenerFor: hasListenerFor 1.61 + }; 1.62 + }, 1.63 + 1.64 + /** 1.65 + * Create an EventEmitter instance to communicate with chrome module 1.66 + * by passing only strings between compartments. 1.67 + * This function expects `emitToChrome` function, that allows to send 1.68 + * events to the chrome module. It returns the EventEmitter as `pipe` 1.69 + * attribute, and, `onChromeEvent` a function that allows chrome module 1.70 + * to send event into the EventEmitter. 1.71 + * 1.72 + * pipe.emit --> emitToChrome 1.73 + * onChromeEvent --> callback registered through pipe.on 1.74 + */ 1.75 + createPipe: function createPipe(emitToChrome) { 1.76 + function onEvent(type, ...args) { 1.77 + // JSON.stringify is buggy with cross-sandbox values, 1.78 + // it may return "{}" on functions. Use a replacer to match them correctly. 1.79 + let replacer = (k, v) => 1.80 + typeof(v) === "function" 1.81 + ? (type === "console" ? Function.toString.call(v) : void(0)) 1.82 + : v; 1.83 + 1.84 + let str = JSON.stringify([type, ...args], replacer); 1.85 + emitToChrome(str); 1.86 + } 1.87 + 1.88 + let { eventEmitter, emit, hasListenerFor } = 1.89 + ContentWorker.createEventEmitter(onEvent); 1.90 + 1.91 + return { 1.92 + pipe: eventEmitter, 1.93 + onChromeEvent: function onChromeEvent(array) { 1.94 + // We either receive a stringified array, or a real array. 1.95 + // We still allow to pass an array of objects, in WorkerSandbox.emitSync 1.96 + // in order to allow sending DOM node reference between content script 1.97 + // and modules (only used for context-menu API) 1.98 + let args = typeof array == "string" ? JSON.parse(array) : array; 1.99 + return emit.apply(null, args); 1.100 + }, 1.101 + hasListenerFor: hasListenerFor 1.102 + }; 1.103 + }, 1.104 + 1.105 + injectConsole: function injectConsole(exports, pipe) { 1.106 + exports.console = Object.freeze({ 1.107 + log: pipe.emit.bind(null, "console", "log"), 1.108 + info: pipe.emit.bind(null, "console", "info"), 1.109 + warn: pipe.emit.bind(null, "console", "warn"), 1.110 + error: pipe.emit.bind(null, "console", "error"), 1.111 + debug: pipe.emit.bind(null, "console", "debug"), 1.112 + exception: pipe.emit.bind(null, "console", "exception"), 1.113 + trace: pipe.emit.bind(null, "console", "trace"), 1.114 + time: pipe.emit.bind(null, "console", "time"), 1.115 + timeEnd: pipe.emit.bind(null, "console", "timeEnd") 1.116 + }); 1.117 + }, 1.118 + 1.119 + injectTimers: function injectTimers(exports, chromeAPI, pipe, console) { 1.120 + // wrapped functions from `'timer'` module. 1.121 + // Wrapper adds `try catch` blocks to the callbacks in order to 1.122 + // emit `error` event on a symbiont if exception is thrown in 1.123 + // the Worker global scope. 1.124 + // @see http://www.w3.org/TR/workers/#workerutils 1.125 + 1.126 + // List of all living timeouts/intervals 1.127 + let _timers = Object.create(null); 1.128 + 1.129 + // Keep a reference to original timeout functions 1.130 + let { 1.131 + setTimeout: chromeSetTimeout, 1.132 + setInterval: chromeSetInterval, 1.133 + clearTimeout: chromeClearTimeout, 1.134 + clearInterval: chromeClearInterval 1.135 + } = chromeAPI.timers; 1.136 + 1.137 + function registerTimer(timer) { 1.138 + let registerMethod = null; 1.139 + if (timer.kind == "timeout") 1.140 + registerMethod = chromeSetTimeout; 1.141 + else if (timer.kind == "interval") 1.142 + registerMethod = chromeSetInterval; 1.143 + else 1.144 + throw new Error("Unknown timer kind: " + timer.kind); 1.145 + 1.146 + if (typeof timer.fun == 'string') { 1.147 + let code = timer.fun; 1.148 + timer.fun = () => chromeAPI.sandbox.evaluate(exports, code); 1.149 + } else if (typeof timer.fun != 'function') { 1.150 + throw new Error('Unsupported callback type' + typeof timer.fun); 1.151 + } 1.152 + 1.153 + let id = registerMethod(onFire, timer.delay); 1.154 + function onFire() { 1.155 + try { 1.156 + if (timer.kind == "timeout") 1.157 + delete _timers[id]; 1.158 + timer.fun.apply(null, timer.args); 1.159 + } catch(e) { 1.160 + console.exception(e); 1.161 + let wrapper = { 1.162 + instanceOfError: instanceOf(e, Error), 1.163 + value: e, 1.164 + }; 1.165 + if (wrapper.instanceOfError) { 1.166 + wrapper.value = { 1.167 + message: e.message, 1.168 + fileName: e.fileName, 1.169 + lineNumber: e.lineNumber, 1.170 + stack: e.stack, 1.171 + name: e.name, 1.172 + }; 1.173 + } 1.174 + pipe.emit('error', wrapper); 1.175 + } 1.176 + } 1.177 + _timers[id] = timer; 1.178 + return id; 1.179 + } 1.180 + 1.181 + // copied from sdk/lang/type.js since modules are not available here 1.182 + function instanceOf(value, Type) { 1.183 + var isConstructorNameSame; 1.184 + var isConstructorSourceSame; 1.185 + 1.186 + // If `instanceof` returned `true` we know result right away. 1.187 + var isInstanceOf = value instanceof Type; 1.188 + 1.189 + // If `instanceof` returned `false` we do ducktype check since `Type` may be 1.190 + // from a different sandbox. If a constructor of the `value` or a constructor 1.191 + // of the value's prototype has same name and source we assume that it's an 1.192 + // instance of the Type. 1.193 + if (!isInstanceOf && value) { 1.194 + isConstructorNameSame = value.constructor.name === Type.name; 1.195 + isConstructorSourceSame = String(value.constructor) == String(Type); 1.196 + isInstanceOf = (isConstructorNameSame && isConstructorSourceSame) || 1.197 + instanceOf(Object.getPrototypeOf(value), Type); 1.198 + } 1.199 + return isInstanceOf; 1.200 + } 1.201 + 1.202 + function unregisterTimer(id) { 1.203 + if (!(id in _timers)) 1.204 + return; 1.205 + let { kind } = _timers[id]; 1.206 + delete _timers[id]; 1.207 + if (kind == "timeout") 1.208 + chromeClearTimeout(id); 1.209 + else if (kind == "interval") 1.210 + chromeClearInterval(id); 1.211 + else 1.212 + throw new Error("Unknown timer kind: " + kind); 1.213 + } 1.214 + 1.215 + function disableAllTimers() { 1.216 + Object.keys(_timers).forEach(unregisterTimer); 1.217 + } 1.218 + 1.219 + exports.setTimeout = function ContentScriptSetTimeout(callback, delay) { 1.220 + return registerTimer({ 1.221 + kind: "timeout", 1.222 + fun: callback, 1.223 + delay: delay, 1.224 + args: Array.slice(arguments, 2) 1.225 + }); 1.226 + }; 1.227 + exports.clearTimeout = function ContentScriptClearTimeout(id) { 1.228 + unregisterTimer(id); 1.229 + }; 1.230 + 1.231 + exports.setInterval = function ContentScriptSetInterval(callback, delay) { 1.232 + return registerTimer({ 1.233 + kind: "interval", 1.234 + fun: callback, 1.235 + delay: delay, 1.236 + args: Array.slice(arguments, 2) 1.237 + }); 1.238 + }; 1.239 + exports.clearInterval = function ContentScriptClearInterval(id) { 1.240 + unregisterTimer(id); 1.241 + }; 1.242 + 1.243 + // On page-hide, save a list of all existing timers before disabling them, 1.244 + // in order to be able to restore them on page-show. 1.245 + // These events are fired when the page goes in/out of bfcache. 1.246 + // https://developer.mozilla.org/En/Working_with_BFCache 1.247 + let frozenTimers = []; 1.248 + pipe.on("pageshow", function onPageShow() { 1.249 + frozenTimers.forEach(registerTimer); 1.250 + }); 1.251 + pipe.on("pagehide", function onPageHide() { 1.252 + frozenTimers = []; 1.253 + for (let id in _timers) 1.254 + frozenTimers.push(_timers[id]); 1.255 + disableAllTimers(); 1.256 + // Some other pagehide listeners may register some timers that won't be 1.257 + // frozen as this particular pagehide listener is called first. 1.258 + // So freeze these timers on next cycle. 1.259 + chromeSetTimeout(function () { 1.260 + for (let id in _timers) 1.261 + frozenTimers.push(_timers[id]); 1.262 + disableAllTimers(); 1.263 + }, 0); 1.264 + }); 1.265 + 1.266 + // Unregister all timers when the page is destroyed 1.267 + // (i.e. when it is removed from bfcache) 1.268 + pipe.on("detach", function clearTimeouts() { 1.269 + disableAllTimers(); 1.270 + _timers = {}; 1.271 + frozenTimers = []; 1.272 + }); 1.273 + }, 1.274 + 1.275 + injectMessageAPI: function injectMessageAPI(exports, pipe, console) { 1.276 + 1.277 + let { eventEmitter: port, emit : portEmit } = 1.278 + ContentWorker.createEventEmitter(pipe.emit.bind(null, "event")); 1.279 + pipe.on("event", portEmit); 1.280 + 1.281 + let self = { 1.282 + port: port, 1.283 + postMessage: pipe.emit.bind(null, "message"), 1.284 + on: pipe.on.bind(null), 1.285 + once: pipe.once.bind(null), 1.286 + removeListener: pipe.removeListener.bind(null), 1.287 + }; 1.288 + Object.defineProperty(exports, "self", { 1.289 + value: self 1.290 + }); 1.291 + 1.292 + exports.on = function deprecatedOn() { 1.293 + console.error("DEPRECATED: The global `on()` function in content " + 1.294 + "scripts is deprecated in favor of the `self.on()` " + 1.295 + "function, which works the same. Replace calls to `on()` " + 1.296 + "with calls to `self.on()`" + 1.297 + "For more info on `self.on`, see " + 1.298 + "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>."); 1.299 + return self.on.apply(null, arguments); 1.300 + }; 1.301 + 1.302 + // Deprecated use of `onMessage` from globals 1.303 + let onMessage = null; 1.304 + Object.defineProperty(exports, "onMessage", { 1.305 + get: function () onMessage, 1.306 + set: function (v) { 1.307 + if (onMessage) 1.308 + self.removeListener("message", onMessage); 1.309 + console.error("DEPRECATED: The global `onMessage` function in content" + 1.310 + "scripts is deprecated in favor of the `self.on()` " + 1.311 + "function. Replace `onMessage = function (data){}` " + 1.312 + "definitions with calls to `self.on('message', " + 1.313 + "function (data){})`. " + 1.314 + "For more info on `self.on`, see " + 1.315 + "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>."); 1.316 + onMessage = v; 1.317 + if (typeof onMessage == "function") 1.318 + self.on("message", onMessage); 1.319 + } 1.320 + }); 1.321 + }, 1.322 + 1.323 + injectOptions: function (exports, options) { 1.324 + Object.defineProperty( exports.self, "options", { value: JSON.parse( options ) }); 1.325 + }, 1.326 + 1.327 + inject: function (exports, chromeAPI, emitToChrome, options) { 1.328 + let { pipe, onChromeEvent, hasListenerFor } = 1.329 + ContentWorker.createPipe(emitToChrome); 1.330 + 1.331 + ContentWorker.injectConsole(exports, pipe); 1.332 + ContentWorker.injectTimers(exports, chromeAPI, pipe, exports.console); 1.333 + ContentWorker.injectMessageAPI(exports, pipe, exports.console); 1.334 + if ( options !== undefined ) { 1.335 + ContentWorker.injectOptions(exports, options); 1.336 + } 1.337 + 1.338 + Object.freeze( exports.self ); 1.339 + 1.340 + return { 1.341 + emitToContent: onChromeEvent, 1.342 + hasListenerFor: hasListenerFor 1.343 + }; 1.344 + } 1.345 +});