aboutsummaryrefslogtreecommitdiff
path: root/tools/addon-sdk-1.3/packages/api-utils/lib/content/worker.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/addon-sdk-1.3/packages/api-utils/lib/content/worker.js')
-rw-r--r--tools/addon-sdk-1.3/packages/api-utils/lib/content/worker.js597
1 files changed, 597 insertions, 0 deletions
diff --git a/tools/addon-sdk-1.3/packages/api-utils/lib/content/worker.js b/tools/addon-sdk-1.3/packages/api-utils/lib/content/worker.js
new file mode 100644
index 0000000..e3126b6
--- /dev/null
+++ b/tools/addon-sdk-1.3/packages/api-utils/lib/content/worker.js
@@ -0,0 +1,597 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Jetpack.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Irakli Gozalishvili <gozala@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+"use strict";
+
+const { shims } = require('../cuddlefish');
+const { Trait } = require('../traits');
+const { EventEmitter, EventEmitterTrait } = require('../events');
+const { Ci, Cu, Cc } = require('chrome');
+const timer = require('../timer');
+const { toFilename } = require('../url');
+const file = require('../file');
+const unload = require('../unload');
+const observers = require('../observer-service');
+const { Cortex } = require('../cortex');
+const { Enqueued } = require('../utils/function');
+const proxy = require('./content-proxy');
+
+const JS_VERSION = '1.8';
+
+const ERR_DESTROYED =
+ "The page has been destroyed and can no longer be used.";
+
+
+function ensureArgumentsAreJSON(args) {
+ // First convert to real array
+ let array = Array.prototype.slice.call(args);
+ // JSON.stringify is buggy with cross-sandbox values,
+ // it may return "{}" on functions. Use a replacer to match them correctly.
+ function replacer(k, v) {
+ return typeof v === "function" ? undefined : v;
+ }
+ return JSON.parse(JSON.stringify(array, replacer));
+}
+
+/**
+ * Extended `EventEmitter` allowing us to emit events asynchronously.
+ */
+const AsyncEventEmitter = EventEmitter.compose({
+ /**
+ * Emits event in the next turn of event loop.
+ */
+ _asyncEmit: function _asyncEmit() {
+ timer.setTimeout(function emitter(emit, scope, params) {
+ emit.apply(scope, params);
+ }, 0, this._emit, this, arguments)
+ }
+});
+
+/**
+ * Local trait providing implementation of the workers global scope.
+ * Used to configure global object in the sandbox.
+ * @see http://www.w3.org/TR/workers/#workerglobalscope
+ */
+const WorkerGlobalScope = AsyncEventEmitter.compose({
+ on: Trait.required,
+ _removeAllListeners: Trait.required,
+
+ // wrapped functions from `'timer'` module.
+ // Wrapper adds `try catch` blocks to the callbacks in order to
+ // emit `error` event on a symbiont if exception is thrown in
+ // the Worker global scope.
+ // @see http://www.w3.org/TR/workers/#workerutils
+
+ // List of all living timeouts/intervals
+ _timers: null,
+
+ setTimeout: function setTimeout(callback, delay) {
+ let params = Array.slice(arguments, 2);
+ let id = timer.setTimeout(function(self) {
+ try {
+ delete self._timers[id];
+ callback.apply(null, params);
+ } catch(e) {
+ self._addonWorker._asyncEmit('error', e);
+ }
+ }, delay, this);
+ this._timers[id] = true;
+ return id;
+ },
+ clearTimeout: function clearTimeout(id){
+ delete this._timers[id];
+ return timer.clearTimeout(id);
+ },
+
+ setInterval: function setInterval(callback, delay) {
+ let params = Array.slice(arguments, 2);
+ let id = timer.setInterval(function(self) {
+ try {
+ callback.apply(null, params);
+ } catch(e) {
+ self._addonWorker._asyncEmit('error', e);
+ }
+ }, delay, this);
+ this._timers[id] = true;
+ return id;
+ },
+ clearInterval: function clearInterval(id) {
+ delete this._timers[id];
+ return timer.clearInterval(id);
+ },
+
+ /**
+ * `onMessage` function defined in the global scope of the worker context.
+ */
+ get _onMessage() this.__onMessage,
+ set _onMessage(value) {
+ let listener = this.__onMessage;
+ if (listener && value !== listener) {
+ this.removeListener('message', listener);
+ this.__onMessage = undefined;
+ }
+ if (value)
+ this.on('message', this.__onMessage = value);
+ },
+ __onMessage: undefined,
+
+ /**
+ * Function for sending data to the addon side.
+ * Validates that data is a `JSON` or primitive value and emits
+ * 'message' event on the worker in the next turn of the event loop.
+ * _Later this will be sending data across process boundaries._
+ * @param {JSON|String|Number|Boolean} data
+ */
+ postMessage: function postMessage(data) {
+ if (!this._addonWorker)
+ throw new Error(ERR_DESTROYED);
+ this._addonWorker._asyncEmit('message',
+ JSON.parse(JSON.stringify(data)));
+ },
+
+ /**
+ * EventEmitter, that behaves (calls listeners) asynchronously.
+ * A way to send customized messages to / from the worker.
+ * Events from in the worker can be observed / emitted via self.on / self.emit
+ */
+ get port() this._port._public,
+
+ /**
+ * Same object than this.port but private API.
+ * Allow access to _asyncEmit, in order to send event to port.
+ */
+ _port: null,
+
+ /**
+ * Alias to the global scope in the context of worker. Similar to
+ * `window` concept.
+ */
+ get self() this._public,
+
+ /**
+ * Configures sandbox and loads content scripts into it.
+ * @param {Worker} worker
+ * content worker
+ */
+ constructor: function WorkerGlobalScope(worker) {
+ this._addonWorker = worker;
+
+ // Hack in order to allow addon worker to access _asyncEmit
+ // as this is the private object of WorkerGlobalScope
+ worker._contentWorker = this;
+
+ // create an event emitter that receive and send events from/to the addon
+ let contentWorker = this;
+ this._port = EventEmitterTrait.create({
+ emit: function () {
+ let addonWorker = contentWorker._addonWorker;
+ if (!addonWorker)
+ throw new Error(ERR_DESTROYED);
+ addonWorker._onContentScriptEvent.apply(addonWorker, arguments);
+ }
+ });
+ // create emit that executes in next turn of event loop.
+ this._port._asyncEmit = Enqueued(this._port._emit);
+ // expose wrapped port, that exposes only public properties.
+ this._port._public = Cortex(this._port);
+
+ // We receive an unwrapped window, with raw js access
+ let window = worker._window;
+
+ // Create the sandbox and bind it to window in order for content scripts to
+ // have access to all standard globals (window, document, ...)
+ let sandbox = this._sandbox = new Cu.Sandbox(window, {
+ sandboxPrototype: proxy.create(window),
+ wantXrays: false
+ });
+ Object.defineProperties(sandbox, {
+ window: { get: function() sandbox },
+ top: { get: function() sandbox },
+ // Use the Greasemonkey naming convention to provide access to the
+ // unwrapped window object so the content script can access document
+ // JavaScript values.
+ // NOTE: this functionality is experimental and may change or go away
+ // at any time!
+ unsafeWindow: { get: function () window.wrappedJSObject }
+ });
+
+ // Overriding / Injecting some natives into sandbox.
+ Cu.evalInSandbox(shims.contents, sandbox, JS_VERSION, shims.filename);
+
+ // Initialize timer lists
+ this._timers = {};
+
+ let publicAPI = this._public;
+
+ // List of content script globals:
+ let keys = ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval',
+ 'self'];
+ for each (let key in keys) {
+ Object.defineProperty(
+ sandbox, key, Object.getOwnPropertyDescriptor(publicAPI, key)
+ );
+ }
+ let self = this;
+ Object.defineProperties(sandbox, {
+ onMessage: {
+ get: function() self._onMessage,
+ set: function(value) {
+ console.warn("The global `onMessage` function in content scripts " +
+ "is deprecated in favor of the `self.on()` function. " +
+ "Replace `onMessage = function (data){}` definitions " +
+ "with calls to `self.on('message', function (data){})`. " +
+ "For more info on `self.on`, see " +
+ "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>.");
+ self._onMessage = value;
+ },
+ configurable: true
+ },
+ console: { value: console, configurable: true },
+
+ // Deprecated use of on/postMessage from globals
+ on: {
+ value: function () {
+ console.warn("The global `on()` function in content scripts is " +
+ "deprecated in favor of the `self.on()` function, " +
+ "which works the same. Replace calls to `on()` with " +
+ "calls to `self.on()`" +
+ "For more info on `self.on`, see " +
+ "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>.");
+ publicAPI.on.apply(publicAPI, arguments);
+ },
+ configurable: true
+ },
+ postMessage: {
+ value: function () {
+ console.warn("The global `postMessage()` function in content " +
+ "scripts is deprecated in favor of the " +
+ "`self.postMessage()` function, which works the same. " +
+ "Replace calls to `postMessage()` with calls to " +
+ "`self.postMessage()`." +
+ "For more info on `self.on`, see " +
+ "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>.");
+ publicAPI.postMessage.apply(publicAPI, arguments);
+ },
+ configurable: true
+ }
+ });
+
+ // The order of `contentScriptFile` and `contentScript` evaluation is
+ // intentional, so programs can load libraries like jQuery from script URLs
+ // and use them in scripts.
+ let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile
+ : null,
+ contentScript = ('contentScript' in worker) ? worker.contentScript : null;
+
+ if (contentScriptFile) {
+ if (Array.isArray(contentScriptFile))
+ this._importScripts.apply(this, contentScriptFile);
+ else
+ this._importScripts(contentScriptFile);
+ }
+ if (contentScript) {
+ this._evaluate(
+ Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript
+ );
+ }
+ },
+ _destructor: function _destructor() {
+ this._removeAllListeners();
+ // Unregister all setTimeout/setInterval
+ // We can use `clearTimeout` for both setTimeout/setInterval
+ // as internal implementation of timer module use same method for both.
+ for (let id in this._timers)
+ timer.clearTimeout(id);
+ let publicAPI = this._public,
+ sandbox = this._sandbox;
+ delete sandbox.__proto__;
+ for (let key in publicAPI)
+ delete sandbox[key];
+ this._sandbox = null;
+ this._addonWorker = null;
+ this.__onMessage = undefined;
+ },
+
+ /**
+ * JavaScript sandbox where all the content scripts are evaluated.
+ * {Sandbox}
+ */
+ _sandbox: null,
+
+ /**
+ * Reference to the addon side of the worker.
+ * @type {Worker}
+ */
+ _addonWorker: null,
+
+ /**
+ * Evaluates code in the sandbox.
+ * @param {String} code
+ * JavaScript source to evaluate.
+ * @param {String} [filename='javascript:' + code]
+ * Name of the file
+ */
+ _evaluate: function(code, filename) {
+ filename = filename || 'javascript:' + code;
+ try {
+ Cu.evalInSandbox(code, this._sandbox, JS_VERSION, filename, 1);
+ }
+ catch(e) {
+ this._addonWorker._asyncEmit('error', e);
+ }
+ },
+ /**
+ * Imports scripts to the sandbox by reading files under urls and
+ * evaluating its source. If exception occurs during evaluation
+ * `"error"` event is emitted on the worker.
+ * This is actually an analog to the `importScript` method in web
+ * workers but in our case it's not exposed even though content
+ * scripts may be able to do it synchronously since IO operation
+ * takes place in the UI process.
+ */
+ _importScripts: function _importScripts(url) {
+ let urls = Array.slice(arguments, 0);
+ for each (let contentScriptFile in urls) {
+ try {
+ let filename = toFilename(contentScriptFile);
+ this._evaluate(file.read(filename), filename);
+ }
+ catch(e) {
+ this._addonWorker._asyncEmit('error', e)
+ }
+ }
+ }
+});
+
+/**
+ * Message-passing facility for communication between code running
+ * in the content and add-on process.
+ * @see https://jetpack.mozillalabs.com/sdk/latest/docs/#module/api-utils/content/worker
+ */
+const Worker = AsyncEventEmitter.compose({
+ on: Trait.required,
+ _asyncEmit: Trait.required,
+ _removeAllListeners: Trait.required,
+
+ /**
+ * Sends a message to the worker's global scope. Method takes single
+ * argument, which represents data to be sent to the worker. The data may
+ * be any primitive type value or `JSON`. Call of this method asynchronously
+ * emits `message` event with data value in the global scope of this
+ * symbiont.
+ *
+ * `message` event listeners can be set either by calling
+ * `self.on` with a first argument string `"message"` or by
+ * implementing `onMessage` function in the global scope of this worker.
+ * @param {Number|String|JSON} data
+ */
+ postMessage: function postMessage(data) {
+ if (!this._contentWorker)
+ throw new Error(ERR_DESTROYED);
+ this._contentWorker._asyncEmit('message', JSON.parse(JSON.stringify(data)));
+ },
+
+ /**
+ * EventEmitter, that behaves (calls listeners) asynchronously.
+ * A way to send customized messages to / from the worker.
+ * Events from in the worker can be observed / emitted via
+ * worker.on / worker.emit.
+ */
+ get port() {
+ // We generate dynamically this attribute as it needs to be accessible
+ // before Worker.constructor gets called. (For ex: Panel)
+
+ // create an event emitter that receive and send events from/to the worker
+ let self = this;
+ this._port = EventEmitterTrait.create({
+ emit: function () self._emitEventToContent(arguments)
+ });
+ // create emit that executes in next turn of event loop.
+ this._port._asyncEmit = Enqueued(this._port._emit);
+ // expose wrapped port, that exposes only public properties:
+ // We need to destroy this getter in order to be able to set the
+ // final value. We need to update only public port attribute as we never
+ // try to access port attribute from private API.
+ delete this._public.port;
+ this._public.port = Cortex(this._port);
+ // Replicate public port to the private object
+ delete this.port;
+ this.port = this._public.port;
+
+ return this._port;
+ },
+
+ /**
+ * Same object than this.port but private API.
+ * Allow access to _asyncEmit, in order to send event to port.
+ */
+ _port: null,
+
+ /**
+ * Emit a custom event to the content script,
+ * i.e. emit this event on `self.port`
+ */
+ _emitEventToContent: function _emitEventToContent(args) {
+ // We need to save events that are emitted before the worker is
+ // initialized
+ if (!this._inited) {
+ this._earlyEvents.push(args);
+ return;
+ }
+
+ // We throw exception when the worker has been destroyed
+ if (!this._contentWorker) {
+ throw new Error(ERR_DESTROYED);
+ }
+
+ let scope = this._contentWorker._port;
+ // Ensure that we pass only JSON values
+ scope._asyncEmit.apply(scope, ensureArgumentsAreJSON(args));
+ },
+
+ // Is worker connected to the content worker (i.e. WorkerGlobalScope) ?
+ _inited: false,
+
+ // List of custom events fired before worker is initialized
+ get _earlyEvents() {
+ delete this._earlyEvents;
+ this._earlyEvents = [];
+ return this._earlyEvents;
+ },
+
+ constructor: function Worker(options) {
+ options = options || {};
+
+ if ('window' in options)
+ this._window = options.window;
+ if ('contentScriptFile' in options)
+ this.contentScriptFile = options.contentScriptFile;
+ if ('contentScript' in options)
+ this.contentScript = options.contentScript;
+ if ('onError' in options)
+ this.on('error', options.onError);
+ if ('onMessage' in options)
+ this.on('message', options.onMessage);
+ if ('onDetach' in options)
+ this.on('detach', options.onDetach);
+
+ // Track document unload to destroy this worker.
+ // We can't watch for unload event on page's window object as it
+ // prevents bfcache from working:
+ // https://developer.mozilla.org/En/Working_with_BFCache
+ this._windowID = this._window.
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils).
+ currentInnerWindowID;
+ observers.add("inner-window-destroyed",
+ this._documentUnload = this._documentUnload.bind(this));
+
+ unload.ensure(this._public, "destroy");
+
+ // Ensure that worker._port is initialized for contentWorker to be able
+ // to send use event during WorkerGlobalScope(this)
+ this.port;
+
+ // will set this._contentWorker pointing to the private API:
+ WorkerGlobalScope(this);
+
+ // Mainly enable worker.port.emit to send event to the content worker
+ this._inited = true;
+
+ // Flush all events that have been fired before the worker is initialized.
+ this._earlyEvents.forEach((function (args) this._emitEventToContent(args)).
+ bind(this));
+ },
+
+ _documentUnload: function _documentUnload(subject, topic, data) {
+ let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ if (innerWinID != this._windowID) return false;
+ this._workerCleanup();
+ return true;
+ },
+
+ get url() {
+ // this._window will be null after detach
+ return this._window ? this._window.document.location.href : null;
+ },
+
+ get tab() {
+ if (this._window) {
+ let tab = require("../tabs/tab");
+ // this._window will be null after detach
+ return tab.getTabForWindow(this._window);
+ }
+ return null;
+ },
+
+ /**
+ * Tells content worker to unload itself and
+ * removes all the references from itself.
+ */
+ destroy: function destroy() {
+ this._workerCleanup();
+ this._removeAllListeners('message');
+ this._removeAllListeners('error');
+ this._removeAllListeners('detach');
+ },
+
+ /**
+ * Remove all internal references to the attached document
+ * Tells _port to unload itself and removes all the references from itself.
+ */
+ _workerCleanup: function _workerCleanup() {
+ // maybe unloaded before content side is created
+ // As Symbiont call worker.constructor on document load
+ if (this._contentWorker)
+ this._contentWorker._destructor();
+ this._contentWorker = null;
+ this._window = null;
+ // This method may be called multiple times,
+ // avoid dispatching `detach` event more than once
+ if (this._windowID) {
+ this._windowID = null;
+ observers.remove("inner-window-destroyed", this._documentUnload);
+ this._earlyEvents.slice(0, this._earlyEvents.length);
+ this._emit("detach");
+ }
+ },
+
+ /**
+ * Receive an event from the content script that need to be sent to
+ * worker.port. Provide a way for composed object to catch all events.
+ */
+ _onContentScriptEvent: function _onContentScriptEvent() {
+ // Ensure that we pass only JSON values
+ this._port._asyncEmit.apply(this._port, ensureArgumentsAreJSON(arguments));
+ },
+
+ /**
+ * Reference to the content side of the worker.
+ * @type {WorkerGlobalScope}
+ */
+ _contentWorker: null,
+
+ /**
+ * Reference to the window that is accessible from
+ * the content scripts.
+ * @type {Object}
+ */
+ _window: null,
+});
+exports.Worker = Worker;
+