michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: const ContentWorker = Object.freeze({ michael@0: // TODO: Bug 727854 Use same implementation than common JS modules, michael@0: // i.e. EventEmitter module michael@0: michael@0: /** michael@0: * Create an EventEmitter instance. michael@0: */ michael@0: createEventEmitter: function createEventEmitter(emit) { michael@0: let listeners = Object.create(null); michael@0: let eventEmitter = Object.freeze({ michael@0: emit: emit, michael@0: on: function on(name, callback) { michael@0: if (typeof callback !== "function") michael@0: return this; michael@0: if (!(name in listeners)) michael@0: listeners[name] = []; michael@0: listeners[name].push(callback); michael@0: return this; michael@0: }, michael@0: once: function once(name, callback) { michael@0: eventEmitter.on(name, function onceCallback() { michael@0: eventEmitter.removeListener(name, onceCallback); michael@0: callback.apply(callback, arguments); michael@0: }); michael@0: }, michael@0: removeListener: function removeListener(name, callback) { michael@0: if (!(name in listeners)) michael@0: return; michael@0: let index = listeners[name].indexOf(callback); michael@0: if (index == -1) michael@0: return; michael@0: listeners[name].splice(index, 1); michael@0: } michael@0: }); michael@0: function onEvent(name) { michael@0: if (!(name in listeners)) michael@0: return []; michael@0: let args = Array.slice(arguments, 1); michael@0: let results = []; michael@0: for each (let callback in listeners[name]) { michael@0: results.push(callback.apply(null, args)); michael@0: } michael@0: return results; michael@0: } michael@0: function hasListenerFor(name) { michael@0: if (!(name in listeners)) michael@0: return false; michael@0: return listeners[name].length > 0; michael@0: } michael@0: return { michael@0: eventEmitter: eventEmitter, michael@0: emit: onEvent, michael@0: hasListenerFor: hasListenerFor michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Create an EventEmitter instance to communicate with chrome module michael@0: * by passing only strings between compartments. michael@0: * This function expects `emitToChrome` function, that allows to send michael@0: * events to the chrome module. It returns the EventEmitter as `pipe` michael@0: * attribute, and, `onChromeEvent` a function that allows chrome module michael@0: * to send event into the EventEmitter. michael@0: * michael@0: * pipe.emit --> emitToChrome michael@0: * onChromeEvent --> callback registered through pipe.on michael@0: */ michael@0: createPipe: function createPipe(emitToChrome) { michael@0: function onEvent(type, ...args) { michael@0: // JSON.stringify is buggy with cross-sandbox values, michael@0: // it may return "{}" on functions. Use a replacer to match them correctly. michael@0: let replacer = (k, v) => michael@0: typeof(v) === "function" michael@0: ? (type === "console" ? Function.toString.call(v) : void(0)) michael@0: : v; michael@0: michael@0: let str = JSON.stringify([type, ...args], replacer); michael@0: emitToChrome(str); michael@0: } michael@0: michael@0: let { eventEmitter, emit, hasListenerFor } = michael@0: ContentWorker.createEventEmitter(onEvent); michael@0: michael@0: return { michael@0: pipe: eventEmitter, michael@0: onChromeEvent: function onChromeEvent(array) { michael@0: // We either receive a stringified array, or a real array. michael@0: // We still allow to pass an array of objects, in WorkerSandbox.emitSync michael@0: // in order to allow sending DOM node reference between content script michael@0: // and modules (only used for context-menu API) michael@0: let args = typeof array == "string" ? JSON.parse(array) : array; michael@0: return emit.apply(null, args); michael@0: }, michael@0: hasListenerFor: hasListenerFor michael@0: }; michael@0: }, michael@0: michael@0: injectConsole: function injectConsole(exports, pipe) { michael@0: exports.console = Object.freeze({ michael@0: log: pipe.emit.bind(null, "console", "log"), michael@0: info: pipe.emit.bind(null, "console", "info"), michael@0: warn: pipe.emit.bind(null, "console", "warn"), michael@0: error: pipe.emit.bind(null, "console", "error"), michael@0: debug: pipe.emit.bind(null, "console", "debug"), michael@0: exception: pipe.emit.bind(null, "console", "exception"), michael@0: trace: pipe.emit.bind(null, "console", "trace"), michael@0: time: pipe.emit.bind(null, "console", "time"), michael@0: timeEnd: pipe.emit.bind(null, "console", "timeEnd") michael@0: }); michael@0: }, michael@0: michael@0: injectTimers: function injectTimers(exports, chromeAPI, pipe, console) { michael@0: // wrapped functions from `'timer'` module. michael@0: // Wrapper adds `try catch` blocks to the callbacks in order to michael@0: // emit `error` event on a symbiont if exception is thrown in michael@0: // the Worker global scope. michael@0: // @see http://www.w3.org/TR/workers/#workerutils michael@0: michael@0: // List of all living timeouts/intervals michael@0: let _timers = Object.create(null); michael@0: michael@0: // Keep a reference to original timeout functions michael@0: let { michael@0: setTimeout: chromeSetTimeout, michael@0: setInterval: chromeSetInterval, michael@0: clearTimeout: chromeClearTimeout, michael@0: clearInterval: chromeClearInterval michael@0: } = chromeAPI.timers; michael@0: michael@0: function registerTimer(timer) { michael@0: let registerMethod = null; michael@0: if (timer.kind == "timeout") michael@0: registerMethod = chromeSetTimeout; michael@0: else if (timer.kind == "interval") michael@0: registerMethod = chromeSetInterval; michael@0: else michael@0: throw new Error("Unknown timer kind: " + timer.kind); michael@0: michael@0: if (typeof timer.fun == 'string') { michael@0: let code = timer.fun; michael@0: timer.fun = () => chromeAPI.sandbox.evaluate(exports, code); michael@0: } else if (typeof timer.fun != 'function') { michael@0: throw new Error('Unsupported callback type' + typeof timer.fun); michael@0: } michael@0: michael@0: let id = registerMethod(onFire, timer.delay); michael@0: function onFire() { michael@0: try { michael@0: if (timer.kind == "timeout") michael@0: delete _timers[id]; michael@0: timer.fun.apply(null, timer.args); michael@0: } catch(e) { michael@0: console.exception(e); michael@0: let wrapper = { michael@0: instanceOfError: instanceOf(e, Error), michael@0: value: e, michael@0: }; michael@0: if (wrapper.instanceOfError) { michael@0: wrapper.value = { michael@0: message: e.message, michael@0: fileName: e.fileName, michael@0: lineNumber: e.lineNumber, michael@0: stack: e.stack, michael@0: name: e.name, michael@0: }; michael@0: } michael@0: pipe.emit('error', wrapper); michael@0: } michael@0: } michael@0: _timers[id] = timer; michael@0: return id; michael@0: } michael@0: michael@0: // copied from sdk/lang/type.js since modules are not available here michael@0: function instanceOf(value, Type) { michael@0: var isConstructorNameSame; michael@0: var isConstructorSourceSame; michael@0: michael@0: // If `instanceof` returned `true` we know result right away. michael@0: var isInstanceOf = value instanceof Type; michael@0: michael@0: // If `instanceof` returned `false` we do ducktype check since `Type` may be michael@0: // from a different sandbox. If a constructor of the `value` or a constructor michael@0: // of the value's prototype has same name and source we assume that it's an michael@0: // instance of the Type. michael@0: if (!isInstanceOf && value) { michael@0: isConstructorNameSame = value.constructor.name === Type.name; michael@0: isConstructorSourceSame = String(value.constructor) == String(Type); michael@0: isInstanceOf = (isConstructorNameSame && isConstructorSourceSame) || michael@0: instanceOf(Object.getPrototypeOf(value), Type); michael@0: } michael@0: return isInstanceOf; michael@0: } michael@0: michael@0: function unregisterTimer(id) { michael@0: if (!(id in _timers)) michael@0: return; michael@0: let { kind } = _timers[id]; michael@0: delete _timers[id]; michael@0: if (kind == "timeout") michael@0: chromeClearTimeout(id); michael@0: else if (kind == "interval") michael@0: chromeClearInterval(id); michael@0: else michael@0: throw new Error("Unknown timer kind: " + kind); michael@0: } michael@0: michael@0: function disableAllTimers() { michael@0: Object.keys(_timers).forEach(unregisterTimer); michael@0: } michael@0: michael@0: exports.setTimeout = function ContentScriptSetTimeout(callback, delay) { michael@0: return registerTimer({ michael@0: kind: "timeout", michael@0: fun: callback, michael@0: delay: delay, michael@0: args: Array.slice(arguments, 2) michael@0: }); michael@0: }; michael@0: exports.clearTimeout = function ContentScriptClearTimeout(id) { michael@0: unregisterTimer(id); michael@0: }; michael@0: michael@0: exports.setInterval = function ContentScriptSetInterval(callback, delay) { michael@0: return registerTimer({ michael@0: kind: "interval", michael@0: fun: callback, michael@0: delay: delay, michael@0: args: Array.slice(arguments, 2) michael@0: }); michael@0: }; michael@0: exports.clearInterval = function ContentScriptClearInterval(id) { michael@0: unregisterTimer(id); michael@0: }; michael@0: michael@0: // On page-hide, save a list of all existing timers before disabling them, michael@0: // in order to be able to restore them on page-show. michael@0: // These events are fired when the page goes in/out of bfcache. michael@0: // https://developer.mozilla.org/En/Working_with_BFCache michael@0: let frozenTimers = []; michael@0: pipe.on("pageshow", function onPageShow() { michael@0: frozenTimers.forEach(registerTimer); michael@0: }); michael@0: pipe.on("pagehide", function onPageHide() { michael@0: frozenTimers = []; michael@0: for (let id in _timers) michael@0: frozenTimers.push(_timers[id]); michael@0: disableAllTimers(); michael@0: // Some other pagehide listeners may register some timers that won't be michael@0: // frozen as this particular pagehide listener is called first. michael@0: // So freeze these timers on next cycle. michael@0: chromeSetTimeout(function () { michael@0: for (let id in _timers) michael@0: frozenTimers.push(_timers[id]); michael@0: disableAllTimers(); michael@0: }, 0); michael@0: }); michael@0: michael@0: // Unregister all timers when the page is destroyed michael@0: // (i.e. when it is removed from bfcache) michael@0: pipe.on("detach", function clearTimeouts() { michael@0: disableAllTimers(); michael@0: _timers = {}; michael@0: frozenTimers = []; michael@0: }); michael@0: }, michael@0: michael@0: injectMessageAPI: function injectMessageAPI(exports, pipe, console) { michael@0: michael@0: let { eventEmitter: port, emit : portEmit } = michael@0: ContentWorker.createEventEmitter(pipe.emit.bind(null, "event")); michael@0: pipe.on("event", portEmit); michael@0: michael@0: let self = { michael@0: port: port, michael@0: postMessage: pipe.emit.bind(null, "message"), michael@0: on: pipe.on.bind(null), michael@0: once: pipe.once.bind(null), michael@0: removeListener: pipe.removeListener.bind(null), michael@0: }; michael@0: Object.defineProperty(exports, "self", { michael@0: value: self michael@0: }); michael@0: michael@0: exports.on = function deprecatedOn() { michael@0: console.error("DEPRECATED: The global `on()` function in content " + michael@0: "scripts is deprecated in favor of the `self.on()` " + michael@0: "function, which works the same. Replace calls to `on()` " + michael@0: "with calls to `self.on()`" + michael@0: "For more info on `self.on`, see " + michael@0: "."); michael@0: return self.on.apply(null, arguments); michael@0: }; michael@0: michael@0: // Deprecated use of `onMessage` from globals michael@0: let onMessage = null; michael@0: Object.defineProperty(exports, "onMessage", { michael@0: get: function () onMessage, michael@0: set: function (v) { michael@0: if (onMessage) michael@0: self.removeListener("message", onMessage); michael@0: console.error("DEPRECATED: The global `onMessage` function in content" + michael@0: "scripts is deprecated in favor of the `self.on()` " + michael@0: "function. Replace `onMessage = function (data){}` " + michael@0: "definitions with calls to `self.on('message', " + michael@0: "function (data){})`. " + michael@0: "For more info on `self.on`, see " + michael@0: "."); michael@0: onMessage = v; michael@0: if (typeof onMessage == "function") michael@0: self.on("message", onMessage); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: injectOptions: function (exports, options) { michael@0: Object.defineProperty( exports.self, "options", { value: JSON.parse( options ) }); michael@0: }, michael@0: michael@0: inject: function (exports, chromeAPI, emitToChrome, options) { michael@0: let { pipe, onChromeEvent, hasListenerFor } = michael@0: ContentWorker.createPipe(emitToChrome); michael@0: michael@0: ContentWorker.injectConsole(exports, pipe); michael@0: ContentWorker.injectTimers(exports, chromeAPI, pipe, exports.console); michael@0: ContentWorker.injectMessageAPI(exports, pipe, exports.console); michael@0: if ( options !== undefined ) { michael@0: ContentWorker.injectOptions(exports, options); michael@0: } michael@0: michael@0: Object.freeze( exports.self ); michael@0: michael@0: return { michael@0: emitToContent: onChromeEvent, michael@0: hasListenerFor: hasListenerFor michael@0: }; michael@0: } michael@0: });