summaryrefslogtreecommitdiffstats
path: root/toolkit/jetpack/sdk/content
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2018-02-09 06:46:43 -0500
committerMatt A. Tobin <email@mattatobin.com>2018-02-09 06:46:43 -0500
commitac46df8daea09899ce30dc8fd70986e258c746bf (patch)
tree2750d3125fc253fd5b0671e4bd268eff1fd97296 /toolkit/jetpack/sdk/content
parent8cecf8d5208f3945b35f879bba3015bb1a11bec6 (diff)
downloadUXP-ac46df8daea09899ce30dc8fd70986e258c746bf.tar
UXP-ac46df8daea09899ce30dc8fd70986e258c746bf.tar.gz
UXP-ac46df8daea09899ce30dc8fd70986e258c746bf.tar.lz
UXP-ac46df8daea09899ce30dc8fd70986e258c746bf.tar.xz
UXP-ac46df8daea09899ce30dc8fd70986e258c746bf.zip
Move Add-on SDK source to toolkit/jetpack
Diffstat (limited to 'toolkit/jetpack/sdk/content')
-rw-r--r--toolkit/jetpack/sdk/content/content-worker.js305
-rw-r--r--toolkit/jetpack/sdk/content/content.js17
-rw-r--r--toolkit/jetpack/sdk/content/context-menu.js408
-rw-r--r--toolkit/jetpack/sdk/content/events.js57
-rw-r--r--toolkit/jetpack/sdk/content/l10n-html.js133
-rw-r--r--toolkit/jetpack/sdk/content/loader.js74
-rw-r--r--toolkit/jetpack/sdk/content/mod.js68
-rw-r--r--toolkit/jetpack/sdk/content/page-mod.js236
-rw-r--r--toolkit/jetpack/sdk/content/page-worker.js154
-rw-r--r--toolkit/jetpack/sdk/content/sandbox.js426
-rw-r--r--toolkit/jetpack/sdk/content/sandbox/events.js12
-rw-r--r--toolkit/jetpack/sdk/content/tab-events.js58
-rw-r--r--toolkit/jetpack/sdk/content/thumbnail.js51
-rw-r--r--toolkit/jetpack/sdk/content/utils.js105
-rw-r--r--toolkit/jetpack/sdk/content/worker-child.js158
-rw-r--r--toolkit/jetpack/sdk/content/worker.js180
16 files changed, 2442 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/content/content-worker.js b/toolkit/jetpack/sdk/content/content-worker.js
new file mode 100644
index 000000000..0a8225733
--- /dev/null
+++ b/toolkit/jetpack/sdk/content/content-worker.js
@@ -0,0 +1,305 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Object.freeze({
+ // TODO: Bug 727854 Use same implementation than common JS modules,
+ // i.e. EventEmitter module
+
+ /**
+ * Create an EventEmitter instance.
+ */
+ createEventEmitter: function createEventEmitter(emit) {
+ let listeners = Object.create(null);
+ let eventEmitter = Object.freeze({
+ emit: emit,
+ on: function on(name, callback) {
+ if (typeof callback !== "function")
+ return this;
+ if (!(name in listeners))
+ listeners[name] = [];
+ listeners[name].push(callback);
+ return this;
+ },
+ once: function once(name, callback) {
+ eventEmitter.on(name, function onceCallback() {
+ eventEmitter.removeListener(name, onceCallback);
+ callback.apply(callback, arguments);
+ });
+ },
+ removeListener: function removeListener(name, callback) {
+ if (!(name in listeners))
+ return;
+ let index = listeners[name].indexOf(callback);
+ if (index == -1)
+ return;
+ listeners[name].splice(index, 1);
+ }
+ });
+ function onEvent(name) {
+ if (!(name in listeners))
+ return [];
+ let args = Array.slice(arguments, 1);
+ let results = [];
+ for (let callback of listeners[name]) {
+ results.push(callback.apply(null, args));
+ }
+ return results;
+ }
+ return {
+ eventEmitter: eventEmitter,
+ emit: onEvent
+ };
+ },
+
+ /**
+ * Create an EventEmitter instance to communicate with chrome module
+ * by passing only strings between compartments.
+ * This function expects `emitToChrome` function, that allows to send
+ * events to the chrome module. It returns the EventEmitter as `pipe`
+ * attribute, and, `onChromeEvent` a function that allows chrome module
+ * to send event into the EventEmitter.
+ *
+ * pipe.emit --> emitToChrome
+ * onChromeEvent --> callback registered through pipe.on
+ */
+ createPipe: function createPipe(emitToChrome) {
+ let ContentWorker = this;
+ function onEvent(type, ...args) {
+ // JSON.stringify is buggy with cross-sandbox values,
+ // it may return "{}" on functions. Use a replacer to match them correctly.
+ let replacer = (k, v) =>
+ typeof(v) === "function"
+ ? (type === "console" ? Function.toString.call(v) : void(0))
+ : v;
+
+ let str = JSON.stringify([type, ...args], replacer);
+ emitToChrome(str);
+ }
+
+ let { eventEmitter, emit } =
+ ContentWorker.createEventEmitter(onEvent);
+
+ return {
+ pipe: eventEmitter,
+ onChromeEvent: function onChromeEvent(array) {
+ // We either receive a stringified array, or a real array.
+ // We still allow to pass an array of objects, in WorkerSandbox.emitSync
+ // in order to allow sending DOM node reference between content script
+ // and modules (only used for context-menu API)
+ let args = typeof array == "string" ? JSON.parse(array) : array;
+ return emit.apply(null, args);
+ }
+ };
+ },
+
+ injectConsole: function injectConsole(exports, pipe) {
+ exports.console = Object.freeze({
+ log: pipe.emit.bind(null, "console", "log"),
+ info: pipe.emit.bind(null, "console", "info"),
+ warn: pipe.emit.bind(null, "console", "warn"),
+ error: pipe.emit.bind(null, "console", "error"),
+ debug: pipe.emit.bind(null, "console", "debug"),
+ exception: pipe.emit.bind(null, "console", "exception"),
+ trace: pipe.emit.bind(null, "console", "trace"),
+ time: pipe.emit.bind(null, "console", "time"),
+ timeEnd: pipe.emit.bind(null, "console", "timeEnd")
+ });
+ },
+
+ injectTimers: function injectTimers(exports, chromeAPI, pipe, console) {
+ // wrapped functions from `'timer'` module.
+ // Wrapper adds `try catch` blocks to the callbacks in order to
+ // emit `error` event if exception is thrown in
+ // the Worker global scope.
+ // @see http://www.w3.org/TR/workers/#workerutils
+
+ // List of all living timeouts/intervals
+ let _timers = Object.create(null);
+
+ // Keep a reference to original timeout functions
+ let {
+ setTimeout: chromeSetTimeout,
+ setInterval: chromeSetInterval,
+ clearTimeout: chromeClearTimeout,
+ clearInterval: chromeClearInterval
+ } = chromeAPI.timers;
+
+ function registerTimer(timer) {
+ let registerMethod = null;
+ if (timer.kind == "timeout")
+ registerMethod = chromeSetTimeout;
+ else if (timer.kind == "interval")
+ registerMethod = chromeSetInterval;
+ else
+ throw new Error("Unknown timer kind: " + timer.kind);
+
+ if (typeof timer.fun == 'string') {
+ let code = timer.fun;
+ timer.fun = () => chromeAPI.sandbox.evaluate(exports, code);
+ } else if (typeof timer.fun != 'function') {
+ throw new Error('Unsupported callback type' + typeof timer.fun);
+ }
+
+ let id = registerMethod(onFire, timer.delay);
+ function onFire() {
+ try {
+ if (timer.kind == "timeout")
+ delete _timers[id];
+ timer.fun.apply(null, timer.args);
+ } catch(e) {
+ console.exception(e);
+ let wrapper = {
+ instanceOfError: instanceOf(e, Error),
+ value: e,
+ };
+ if (wrapper.instanceOfError) {
+ wrapper.value = {
+ message: e.message,
+ fileName: e.fileName,
+ lineNumber: e.lineNumber,
+ stack: e.stack,
+ name: e.name,
+ };
+ }
+ pipe.emit('error', wrapper);
+ }
+ }
+ _timers[id] = timer;
+ return id;
+ }
+
+ // copied from sdk/lang/type.js since modules are not available here
+ function instanceOf(value, Type) {
+ var isConstructorNameSame;
+ var isConstructorSourceSame;
+
+ // If `instanceof` returned `true` we know result right away.
+ var isInstanceOf = value instanceof Type;
+
+ // If `instanceof` returned `false` we do ducktype check since `Type` may be
+ // from a different sandbox. If a constructor of the `value` or a constructor
+ // of the value's prototype has same name and source we assume that it's an
+ // instance of the Type.
+ if (!isInstanceOf && value) {
+ isConstructorNameSame = value.constructor.name === Type.name;
+ isConstructorSourceSame = String(value.constructor) == String(Type);
+ isInstanceOf = (isConstructorNameSame && isConstructorSourceSame) ||
+ instanceOf(Object.getPrototypeOf(value), Type);
+ }
+ return isInstanceOf;
+ }
+
+ function unregisterTimer(id) {
+ if (!(id in _timers))
+ return;
+ let { kind } = _timers[id];
+ delete _timers[id];
+ if (kind == "timeout")
+ chromeClearTimeout(id);
+ else if (kind == "interval")
+ chromeClearInterval(id);
+ else
+ throw new Error("Unknown timer kind: " + kind);
+ }
+
+ function disableAllTimers() {
+ Object.keys(_timers).forEach(unregisterTimer);
+ }
+
+ exports.setTimeout = function ContentScriptSetTimeout(callback, delay) {
+ return registerTimer({
+ kind: "timeout",
+ fun: callback,
+ delay: delay,
+ args: Array.slice(arguments, 2)
+ });
+ };
+ exports.clearTimeout = function ContentScriptClearTimeout(id) {
+ unregisterTimer(id);
+ };
+
+ exports.setInterval = function ContentScriptSetInterval(callback, delay) {
+ return registerTimer({
+ kind: "interval",
+ fun: callback,
+ delay: delay,
+ args: Array.slice(arguments, 2)
+ });
+ };
+ exports.clearInterval = function ContentScriptClearInterval(id) {
+ unregisterTimer(id);
+ };
+
+ // On page-hide, save a list of all existing timers before disabling them,
+ // in order to be able to restore them on page-show.
+ // These events are fired when the page goes in/out of bfcache.
+ // https://developer.mozilla.org/En/Working_with_BFCache
+ let frozenTimers = [];
+ pipe.on("pageshow", function onPageShow() {
+ frozenTimers.forEach(registerTimer);
+ });
+ pipe.on("pagehide", function onPageHide() {
+ frozenTimers = [];
+ for (let id in _timers)
+ frozenTimers.push(_timers[id]);
+ disableAllTimers();
+ // Some other pagehide listeners may register some timers that won't be
+ // frozen as this particular pagehide listener is called first.
+ // So freeze these timers on next cycle.
+ chromeSetTimeout(function () {
+ for (let id in _timers)
+ frozenTimers.push(_timers[id]);
+ disableAllTimers();
+ }, 0);
+ });
+
+ // Unregister all timers when the page is destroyed
+ // (i.e. when it is removed from bfcache)
+ pipe.on("detach", function clearTimeouts() {
+ disableAllTimers();
+ _timers = {};
+ frozenTimers = [];
+ });
+ },
+
+ injectMessageAPI: function injectMessageAPI(exports, pipe, console) {
+
+ let ContentWorker = this;
+ let { eventEmitter: port, emit : portEmit } =
+ ContentWorker.createEventEmitter(pipe.emit.bind(null, "event"));
+ pipe.on("event", portEmit);
+
+ let self = {
+ port: port,
+ postMessage: pipe.emit.bind(null, "message"),
+ on: pipe.on.bind(null),
+ once: pipe.once.bind(null),
+ removeListener: pipe.removeListener.bind(null),
+ };
+ Object.defineProperty(exports, "self", {
+ value: self
+ });
+ },
+
+ injectOptions: function (exports, options) {
+ Object.defineProperty( exports.self, "options", { value: JSON.parse( options ) });
+ },
+
+ inject: function (exports, chromeAPI, emitToChrome, options) {
+ let ContentWorker = this;
+ let { pipe, onChromeEvent } =
+ ContentWorker.createPipe(emitToChrome);
+
+ ContentWorker.injectConsole(exports, pipe);
+ ContentWorker.injectTimers(exports, chromeAPI, pipe, exports.console);
+ ContentWorker.injectMessageAPI(exports, pipe, exports.console);
+ if ( options !== undefined ) {
+ ContentWorker.injectOptions(exports, options);
+ }
+
+ Object.freeze( exports.self );
+
+ return onChromeEvent;
+ }
+});
diff --git a/toolkit/jetpack/sdk/content/content.js b/toolkit/jetpack/sdk/content/content.js
new file mode 100644
index 000000000..9655223a3
--- /dev/null
+++ b/toolkit/jetpack/sdk/content/content.js
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "deprecated"
+};
+
+const { deprecateUsage } = require('../util/deprecate');
+
+Object.defineProperty(exports, "Worker", {
+ get: function() {
+ deprecateUsage('`sdk/content/content` is deprecated. Please use `sdk/content/worker` directly.');
+ return require('./worker').Worker;
+ }
+});
diff --git a/toolkit/jetpack/sdk/content/context-menu.js b/toolkit/jetpack/sdk/content/context-menu.js
new file mode 100644
index 000000000..2955e2f09
--- /dev/null
+++ b/toolkit/jetpack/sdk/content/context-menu.js
@@ -0,0 +1,408 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { Class } = require("../core/heritage");
+const self = require("../self");
+const { WorkerChild } = require("./worker-child");
+const { getInnerId } = require("../window/utils");
+const { Ci } = require("chrome");
+const { Services } = require("resource://gre/modules/Services.jsm");
+const system = require('../system/events');
+const { process } = require('../remote/child');
+
+// These functions are roughly copied from sdk/selection which doesn't work
+// in the content process
+function getElementWithSelection(window) {
+ let element = Services.focus.getFocusedElementForWindow(window, false, {});
+ if (!element)
+ return null;
+
+ try {
+ // Accessing selectionStart and selectionEnd on e.g. a button
+ // results in an exception thrown as per the HTML5 spec. See
+ // http://www.whatwg.org/specs/web-apps/current-work/multipage/association-of-controls-and-forms.html#textFieldSelection
+
+ let { value, selectionStart, selectionEnd } = element;
+
+ let hasSelection = typeof value === "string" &&
+ !isNaN(selectionStart) &&
+ !isNaN(selectionEnd) &&
+ selectionStart !== selectionEnd;
+
+ return hasSelection ? element : null;
+ }
+ catch (err) {
+ console.exception(err);
+ return null;
+ }
+}
+
+function safeGetRange(selection, rangeNumber) {
+ try {
+ let { rangeCount } = selection;
+ let range = null;
+
+ for (let rangeNumber = 0; rangeNumber < rangeCount; rangeNumber++ ) {
+ range = selection.getRangeAt(rangeNumber);
+
+ if (range && range.toString())
+ break;
+
+ range = null;
+ }
+
+ return range;
+ }
+ catch (e) {
+ return null;
+ }
+}
+
+function getSelection(window) {
+ let selection = window.getSelection();
+ let range = safeGetRange(selection);
+ if (range)
+ return range.toString();
+
+ let node = getElementWithSelection(window);
+ if (!node)
+ return null;
+
+ return node.value.substring(node.selectionStart, node.selectionEnd);
+}
+
+//These are used by PageContext.isCurrent below. If the popupNode or any of
+//its ancestors is one of these, Firefox uses a tailored context menu, and so
+//the page context doesn't apply.
+const NON_PAGE_CONTEXT_ELTS = [
+ Ci.nsIDOMHTMLAnchorElement,
+ Ci.nsIDOMHTMLAppletElement,
+ Ci.nsIDOMHTMLAreaElement,
+ Ci.nsIDOMHTMLButtonElement,
+ Ci.nsIDOMHTMLCanvasElement,
+ Ci.nsIDOMHTMLEmbedElement,
+ Ci.nsIDOMHTMLImageElement,
+ Ci.nsIDOMHTMLInputElement,
+ Ci.nsIDOMHTMLMapElement,
+ Ci.nsIDOMHTMLMediaElement,
+ Ci.nsIDOMHTMLMenuElement,
+ Ci.nsIDOMHTMLObjectElement,
+ Ci.nsIDOMHTMLOptionElement,
+ Ci.nsIDOMHTMLSelectElement,
+ Ci.nsIDOMHTMLTextAreaElement,
+];
+
+// List all editable types of inputs. Or is it better to have a list
+// of non-editable inputs?
+var editableInputs = {
+ email: true,
+ number: true,
+ password: true,
+ search: true,
+ tel: true,
+ text: true,
+ textarea: true,
+ url: true
+};
+
+var CONTEXTS = {};
+
+var Context = Class({
+ initialize: function(id) {
+ this.id = id;
+ },
+
+ adjustPopupNode: function adjustPopupNode(popupNode) {
+ return popupNode;
+ },
+
+ // Gets state to pass through to the parent process for the node the user
+ // clicked on
+ getState: function(popupNode) {
+ return false;
+ }
+});
+
+// Matches when the context-clicked node doesn't have any of
+// NON_PAGE_CONTEXT_ELTS in its ancestors
+CONTEXTS.PageContext = Class({
+ extends: Context,
+
+ getState: function(popupNode) {
+ // If there is a selection in the window then this context does not match
+ if (!popupNode.ownerDocument.defaultView.getSelection().isCollapsed)
+ return false;
+
+ // If the clicked node or any of its ancestors is one of the blocked
+ // NON_PAGE_CONTEXT_ELTS then this context does not match
+ while (!(popupNode instanceof Ci.nsIDOMDocument)) {
+ if (NON_PAGE_CONTEXT_ELTS.some(type => popupNode instanceof type))
+ return false;
+
+ popupNode = popupNode.parentNode;
+ }
+
+ return true;
+ }
+});
+
+// Matches when there is an active selection in the window
+CONTEXTS.SelectionContext = Class({
+ extends: Context,
+
+ getState: function(popupNode) {
+ if (!popupNode.ownerDocument.defaultView.getSelection().isCollapsed)
+ return true;
+
+ try {
+ // The node may be a text box which has selectionStart and selectionEnd
+ // properties. If not this will throw.
+ let { selectionStart, selectionEnd } = popupNode;
+ return !isNaN(selectionStart) && !isNaN(selectionEnd) &&
+ selectionStart !== selectionEnd;
+ }
+ catch (e) {
+ return false;
+ }
+ }
+});
+
+// Matches when the context-clicked node or any of its ancestors matches the
+// selector given
+CONTEXTS.SelectorContext = Class({
+ extends: Context,
+
+ initialize: function initialize(id, selector) {
+ Context.prototype.initialize.call(this, id);
+ this.selector = selector;
+ },
+
+ adjustPopupNode: function adjustPopupNode(popupNode) {
+ let selector = this.selector;
+
+ while (!(popupNode instanceof Ci.nsIDOMDocument)) {
+ if (popupNode.matches(selector))
+ return popupNode;
+
+ popupNode = popupNode.parentNode;
+ }
+
+ return null;
+ },
+
+ getState: function(popupNode) {
+ return !!this.adjustPopupNode(popupNode);
+ }
+});
+
+// Matches when the page url matches any of the patterns given
+CONTEXTS.URLContext = Class({
+ extends: Context,
+
+ getState: function(popupNode) {
+ return popupNode.ownerDocument.URL;
+ }
+});
+
+// Matches when the user-supplied predicate returns true
+CONTEXTS.PredicateContext = Class({
+ extends: Context,
+
+ getState: function(node) {
+ let window = node.ownerDocument.defaultView;
+ let data = {};
+
+ data.documentType = node.ownerDocument.contentType;
+
+ data.documentURL = node.ownerDocument.location.href;
+ data.targetName = node.nodeName.toLowerCase();
+ data.targetID = node.id || null ;
+
+ if ((data.targetName === 'input' && editableInputs[node.type]) ||
+ data.targetName === 'textarea') {
+ data.isEditable = !node.readOnly && !node.disabled;
+ }
+ else {
+ data.isEditable = node.isContentEditable;
+ }
+
+ data.selectionText = getSelection(window, "TEXT");
+
+ data.srcURL = node.src || null;
+ data.value = node.value || null;
+
+ while (!data.linkURL && node) {
+ data.linkURL = node.href || null;
+ node = node.parentNode;
+ }
+
+ return data;
+ },
+});
+
+function instantiateContext({ id, type, args }) {
+ if (!(type in CONTEXTS)) {
+ console.error("Attempt to use unknown context " + type);
+ return;
+ }
+ return new CONTEXTS[type](id, ...args);
+}
+
+var ContextWorker = Class({
+ implements: [ WorkerChild ],
+
+ // Calls the context workers context listeners and returns the first result
+ // that is either a string or a value that evaluates to true. If all of the
+ // listeners returned false then returns false. If there are no listeners,
+ // returns true (show the menu item by default).
+ getMatchedContext: function getCurrentContexts(popupNode) {
+ let results = this.sandbox.emitSync("context", popupNode);
+ if (!results.length)
+ return true;
+ return results.reduce((val, result) => val || result);
+ },
+
+ // Emits a click event in the worker's port. popupNode is the node that was
+ // context-clicked, and clickedItemData is the data of the item that was
+ // clicked.
+ fireClick: function fireClick(popupNode, clickedItemData) {
+ this.sandbox.emitSync("click", popupNode, clickedItemData);
+ }
+});
+
+// Gets the item's content script worker for a window, creating one if necessary
+// Once created it will be automatically destroyed when the window unloads.
+// If there is not content scripts for the item then null will be returned.
+function getItemWorkerForWindow(item, window) {
+ if (!item.contentScript && !item.contentScriptFile)
+ return null;
+
+ let id = getInnerId(window);
+ let worker = item.workerMap.get(id);
+
+ if (worker)
+ return worker;
+
+ worker = ContextWorker({
+ id: item.id,
+ window,
+ manager: item.manager,
+ contentScript: item.contentScript,
+ contentScriptFile: item.contentScriptFile,
+ onDetach: function() {
+ item.workerMap.delete(id);
+ }
+ });
+
+ item.workerMap.set(id, worker);
+
+ return worker;
+}
+
+// A very simple remote proxy for every item. It's job is to provide data for
+// the main process to use to determine visibility state and to call into
+// content scripts when clicked.
+var RemoteItem = Class({
+ initialize: function(options, manager) {
+ this.id = options.id;
+ this.contexts = options.contexts.map(instantiateContext);
+ this.contentScript = options.contentScript;
+ this.contentScriptFile = options.contentScriptFile;
+
+ this.manager = manager;
+
+ this.workerMap = new Map();
+ keepAlive.set(this.id, this);
+ },
+
+ destroy: function() {
+ for (let worker of this.workerMap.values()) {
+ worker.destroy();
+ }
+ keepAlive.delete(this.id);
+ },
+
+ activate: function(popupNode, data) {
+ let worker = getItemWorkerForWindow(this, popupNode.ownerDocument.defaultView);
+ if (!worker)
+ return;
+
+ for (let context of this.contexts)
+ popupNode = context.adjustPopupNode(popupNode);
+
+ worker.fireClick(popupNode, data);
+ },
+
+ // Fills addonInfo with state data to send through to the main process
+ getContextState: function(popupNode, addonInfo) {
+ if (!(self.id in addonInfo)) {
+ addonInfo[self.id] = {
+ processID: process.id,
+ items: {}
+ };
+ }
+
+ let worker = getItemWorkerForWindow(this, popupNode.ownerDocument.defaultView);
+ let contextStates = {};
+ for (let context of this.contexts)
+ contextStates[context.id] = context.getState(popupNode);
+
+ addonInfo[self.id].items[this.id] = {
+ // It isn't ideal to create a PageContext for every item but there isn't
+ // a good shared place to do it.
+ pageContext: (new CONTEXTS.PageContext()).getState(popupNode),
+ contextStates,
+ hasWorker: !!worker,
+ workerContext: worker ? worker.getMatchedContext(popupNode) : true
+ }
+ }
+});
+exports.RemoteItem = RemoteItem;
+
+// Holds remote items for this frame.
+var keepAlive = new Map();
+
+// Called to create remote proxies for items. If they already exist we destroy
+// and recreate. This can happen if the item changes in some way or in odd
+// timing cases where the frame script is create around the same time as the
+// item is created in the main process
+process.port.on('sdk/contextmenu/createitems', (process, items) => {
+ for (let itemoptions of items) {
+ let oldItem = keepAlive.get(itemoptions.id);
+ if (oldItem) {
+ oldItem.destroy();
+ }
+
+ let item = new RemoteItem(itemoptions, this);
+ }
+});
+
+process.port.on('sdk/contextmenu/destroyitems', (process, items) => {
+ for (let id of items) {
+ let item = keepAlive.get(id);
+ item.destroy();
+ }
+});
+
+var lastPopupNode = null;
+
+system.on('content-contextmenu', ({ subject }) => {
+ let { event: { target: popupNode }, addonInfo } = subject.wrappedJSObject;
+ lastPopupNode = popupNode;
+
+ for (let item of keepAlive.values()) {
+ item.getContextState(popupNode, addonInfo);
+ }
+}, true);
+
+process.port.on('sdk/contextmenu/activateitems', (process, items, data) => {
+ for (let id of items) {
+ let item = keepAlive.get(id);
+ if (!item)
+ continue;
+
+ item.activate(lastPopupNode, data);
+ }
+});
diff --git a/toolkit/jetpack/sdk/content/events.js b/toolkit/jetpack/sdk/content/events.js
new file mode 100644
index 000000000..c085b6179
--- /dev/null
+++ b/toolkit/jetpack/sdk/content/events.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.metadata = {
+ "stability": "experimental"
+};
+
+const { Ci } = require("chrome");
+const { open } = require("../event/dom");
+const { observe } = require("../event/chrome");
+const { filter, merge, map, expand } = require("../event/utils");
+const { windows } = require("../window/utils");
+const { events: windowEvents } = require("sdk/window/events");
+
+// Note: Please note that even though pagehide event is included
+// it's not observable reliably since it's not always triggered
+// when closing tabs. Implementation can be imrpoved once that
+// event will be necessary.
+var TYPES = ["DOMContentLoaded", "load", "pageshow", "pagehide"];
+
+var insert = observe("document-element-inserted");
+var windowCreate = merge([
+ observe("content-document-global-created"),
+ observe("chrome-document-global-created")
+]);
+var create = map(windowCreate, function({target, data, type}) {
+ return { target: target.document, type: type, data: data }
+});
+
+function streamEventsFrom({document}) {
+ // Map supported event types to a streams of those events on the given
+ // `window` for the inserted document and than merge these streams into
+ // single form stream off all window state change events.
+ let stateChanges = TYPES.map(function(type) {
+ return open(document, type, { capture: true });
+ });
+
+ // Since load events on document occur for every loded resource
+ return filter(merge(stateChanges), function({target}) {
+ return target instanceof Ci.nsIDOMDocument
+ })
+}
+exports.streamEventsFrom = streamEventsFrom;
+
+var opened = windows(null, { includePrivate: true });
+var state = merge(opened.map(streamEventsFrom));
+
+
+var futureReady = filter(windowEvents, ({type}) =>
+ type === "DOMContentLoaded");
+var futureWindows = map(futureReady, ({target}) => target);
+var futureState = expand(futureWindows, streamEventsFrom);
+
+exports.events = merge([insert, create, state, futureState]);
diff --git a/toolkit/jetpack/sdk/content/l10n-html.js b/toolkit/jetpack/sdk/content/l10n-html.js
new file mode 100644
index 000000000..f324623dc
--- /dev/null
+++ b/toolkit/jetpack/sdk/content/l10n-html.js
@@ -0,0 +1,133 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const { Ci, Cc, Cu } = require("chrome");
+const core = require("../l10n/core");
+const { loadSheet, removeSheet } = require("../stylesheet/utils");
+const { process, frames } = require("../remote/child");
+var observerService = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm");
+const addObserver = ShimWaiver.getProperty(observerService, "addObserver");
+const removeObserver = ShimWaiver.getProperty(observerService, "removeObserver");
+
+const assetsURI = require('../self').data.url();
+
+const hideSheetUri = "data:text/css,:root {visibility: hidden !important;}";
+
+function translateElementAttributes(element) {
+ // Translateable attributes
+ const attrList = ['title', 'accesskey', 'alt', 'label', 'placeholder'];
+ const ariaAttrMap = {
+ 'ariaLabel': 'aria-label',
+ 'ariaValueText': 'aria-valuetext',
+ 'ariaMozHint': 'aria-moz-hint'
+ };
+ const attrSeparator = '.';
+
+ // Try to translate each of the attributes
+ for (let attribute of attrList) {
+ const data = core.get(element.dataset.l10nId + attrSeparator + attribute);
+ if (data)
+ element.setAttribute(attribute, data);
+ }
+
+ // Look for the aria attribute translations that match fxOS's aliases
+ for (let attrAlias in ariaAttrMap) {
+ const data = core.get(element.dataset.l10nId + attrSeparator + attrAlias);
+ if (data)
+ element.setAttribute(ariaAttrMap[attrAlias], data);
+ }
+}
+
+// Taken from Gaia:
+// https://github.com/andreasgal/gaia/blob/04fde2640a7f40314643016a5a6c98bf3755f5fd/webapi.js#L1470
+function translateElement(element) {
+ element = element || document;
+
+ // check all translatable children (= w/ a `data-l10n-id' attribute)
+ var children = element.querySelectorAll('*[data-l10n-id]');
+ var elementCount = children.length;
+ for (var i = 0; i < elementCount; i++) {
+ var child = children[i];
+
+ // translate the child
+ var key = child.dataset.l10nId;
+ var data = core.get(key);
+ if (data)
+ child.textContent = data;
+
+ translateElementAttributes(child);
+ }
+}
+exports.translateElement = translateElement;
+
+function onDocumentReady2Translate(event) {
+ let document = event.target;
+ document.removeEventListener("DOMContentLoaded", onDocumentReady2Translate,
+ false);
+
+ translateElement(document);
+
+ try {
+ // Finally display document when we finished replacing all text content
+ if (document.defaultView)
+ removeSheet(document.defaultView, hideSheetUri, 'user');
+ }
+ catch(e) {
+ console.exception(e);
+ }
+}
+
+function onContentWindow(document) {
+ // Accept only HTML documents
+ if (!(document instanceof Ci.nsIDOMHTMLDocument))
+ return;
+
+ // Bug 769483: data:URI documents instanciated with nsIDOMParser
+ // have a null `location` attribute at this time
+ if (!document.location)
+ return;
+
+ // Accept only document from this addon
+ if (document.location.href.indexOf(assetsURI) !== 0)
+ return;
+
+ try {
+ // First hide content of the document in order to have content blinking
+ // between untranslated and translated states
+ loadSheet(document.defaultView, hideSheetUri, 'user');
+ }
+ catch(e) {
+ console.exception(e);
+ }
+ // Wait for DOM tree to be built before applying localization
+ document.addEventListener("DOMContentLoaded", onDocumentReady2Translate,
+ false);
+}
+
+// Listen to creation of content documents in order to translate them as soon
+// as possible in their loading process
+const ON_CONTENT = "document-element-inserted";
+let enabled = false;
+function enable() {
+ if (enabled)
+ return;
+ addObserver(onContentWindow, ON_CONTENT, false);
+ enabled = true;
+}
+process.port.on("sdk/l10n/html/enable", enable);
+
+function disable() {
+ if (!enabled)
+ return;
+ removeObserver(onContentWindow, ON_CONTENT);
+ enabled = false;
+}
+process.port.on("sdk/l10n/html/disable", disable);
diff --git a/toolkit/jetpack/sdk/content/loader.js b/toolkit/jetpack/sdk/content/loader.js
new file mode 100644
index 000000000..e4f0dd2aa
--- /dev/null
+++ b/toolkit/jetpack/sdk/content/loader.js
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const { isValidURI, isLocalURL, URL } = require('../url');
+const { contract } = require('../util/contract');
+const { isString, isNil, instanceOf, isJSONable } = require('../lang/type');
+const { validateOptions,
+ string, array, object, either, required } = require('../deprecated/api-utils');
+
+const isValidScriptFile = (value) =>
+ (isString(value) || instanceOf(value, URL)) && isLocalURL(value);
+
+// map of property validations
+const valid = {
+ contentURL: {
+ is: either(string, object),
+ ok: url => isNil(url) || isLocalURL(url) || isValidURI(url),
+ msg: 'The `contentURL` option must be a valid URL.'
+ },
+ contentScriptFile: {
+ is: either(string, object, array),
+ ok: value => isNil(value) || [].concat(value).every(isValidScriptFile),
+ msg: 'The `contentScriptFile` option must be a local URL or an array of URLs.'
+ },
+ contentScript: {
+ is: either(string, array),
+ ok: value => isNil(value) || [].concat(value).every(isString),
+ msg: 'The `contentScript` option must be a string or an array of strings.'
+ },
+ contentScriptWhen: {
+ is: required(string),
+ map: value => value || 'end',
+ ok: value => ~['start', 'ready', 'end'].indexOf(value),
+ msg: 'The `contentScriptWhen` option must be either "start", "ready" or "end".'
+ },
+ contentScriptOptions: {
+ ok: value => isNil(value) || isJSONable(value),
+ msg: 'The contentScriptOptions should be a jsonable value.'
+ }
+};
+exports.validationAttributes = valid;
+
+/**
+ * Shortcut function to validate property with validation.
+ * @param {Object|Number|String} suspect
+ * value to validate
+ * @param {Object} validation
+ * validation rule passed to `api-utils`
+ */
+function validate(suspect, validation) {
+ return validateOptions(
+ { $: suspect },
+ { $: validation }
+ ).$;
+}
+
+function Allow(script) {
+ return {
+ get script() {
+ return script;
+ },
+ set script(value) {
+ script = !!value;
+ }
+ };
+}
+
+exports.contract = contract(valid);
diff --git a/toolkit/jetpack/sdk/content/mod.js b/toolkit/jetpack/sdk/content/mod.js
new file mode 100644
index 000000000..81fe9ee42
--- /dev/null
+++ b/toolkit/jetpack/sdk/content/mod.js
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "experimental"
+};
+
+const { Ci } = require("chrome");
+const { dispatcher } = require("../util/dispatcher");
+const { add, remove, iterator } = require("../lang/weak-set");
+
+var getTargetWindow = dispatcher("getTargetWindow");
+
+getTargetWindow.define(function (target) {
+ if (target instanceof Ci.nsIDOMWindow)
+ return target;
+ if (target instanceof Ci.nsIDOMDocument)
+ return target.defaultView || null;
+
+ return null;
+});
+
+exports.getTargetWindow = getTargetWindow;
+
+var attachTo = dispatcher("attachTo");
+exports.attachTo = attachTo;
+
+var detachFrom = dispatcher("detatchFrom");
+exports.detachFrom = detachFrom;
+
+function attach(modification, target) {
+ if (!modification)
+ return;
+
+ let window = getTargetWindow(target);
+
+ attachTo(modification, window);
+
+ // modification are stored per content; `window` reference can still be the
+ // same even if the content is changed, therefore `document` is used instead.
+ add(modification, window.document);
+}
+exports.attach = attach;
+
+function detach(modification, target) {
+ if (!modification)
+ return;
+
+ if (target) {
+ let window = getTargetWindow(target);
+ detachFrom(modification, window);
+ remove(modification, window.document);
+ }
+ else {
+ let documents = iterator(modification);
+ for (let document of documents) {
+ let window = document.defaultView;
+ // The window might have already gone away
+ if (!window)
+ continue;
+ detachFrom(modification, document.defaultView);
+ remove(modification, document);
+ }
+ }
+}
+exports.detach = detach;
diff --git a/toolkit/jetpack/sdk/content/page-mod.js b/toolkit/jetpack/sdk/content/page-mod.js
new file mode 100644
index 000000000..8ff9b1e7b
--- /dev/null
+++ b/toolkit/jetpack/sdk/content/page-mod.js
@@ -0,0 +1,236 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "stable"
+};
+
+const { getAttachEventType } = require('../content/utils');
+const { Class } = require('../core/heritage');
+const { Disposable } = require('../core/disposable');
+const { WeakReference } = require('../core/reference');
+const { WorkerChild } = require('./worker-child');
+const { EventTarget } = require('../event/target');
+const { on, emit, once, setListeners } = require('../event/core');
+const { on: domOn, removeListener: domOff } = require('../dom/events');
+const { isRegExp, isUndefined } = require('../lang/type');
+const { merge } = require('../util/object');
+const { isBrowser, getFrames } = require('../window/utils');
+const { getTabs, getURI: getTabURI } = require('../tabs/utils');
+const { ignoreWindow } = require('../private-browsing/utils');
+const { Style } = require("../stylesheet/style");
+const { attach, detach } = require("../content/mod");
+const { has, hasAny } = require("../util/array");
+const { Rules } = require("../util/rules");
+const { List, addListItem, removeListItem } = require('../util/list');
+const { when } = require("../system/unload");
+const { uuid } = require('../util/uuid');
+const { frames, process } = require('../remote/child');
+
+const pagemods = new Map();
+const styles = new WeakMap();
+var styleFor = (mod) => styles.get(mod);
+
+// Helper functions
+var modMatchesURI = (mod, uri) => mod.include.matchesAny(uri) && !mod.exclude.matchesAny(uri);
+
+/**
+ * PageMod constructor (exported below).
+ * @constructor
+ */
+const ChildPageMod = Class({
+ implements: [
+ EventTarget,
+ Disposable,
+ ],
+ setup: function PageMod(model) {
+ merge(this, model);
+
+ // Set listeners on {PageMod} itself, not the underlying worker,
+ // like `onMessage`, as it'll get piped.
+ setListeners(this, model);
+
+ function deserializeRules(rules) {
+ for (let rule of rules) {
+ yield rule.type == "string" ? rule.value
+ : new RegExp(rule.pattern, rule.flags);
+ }
+ }
+
+ let include = [...deserializeRules(this.include)];
+ this.include = Rules();
+ this.include.add.apply(this.include, include);
+
+ let exclude = [...deserializeRules(this.exclude)];
+ this.exclude = Rules();
+ this.exclude.add.apply(this.exclude, exclude);
+
+ if (this.contentStyle || this.contentStyleFile) {
+ styles.set(this, Style({
+ uri: this.contentStyleFile,
+ source: this.contentStyle
+ }));
+ }
+
+ pagemods.set(this.id, this);
+ this.seenDocuments = new WeakMap();
+
+ // `applyOnExistingDocuments` has to be called after `pagemods.add()`
+ // otherwise its calls to `onContent` method won't do anything.
+ if (has(this.attachTo, 'existing'))
+ applyOnExistingDocuments(this);
+ },
+
+ dispose: function() {
+ let style = styleFor(this);
+ if (style)
+ detach(style);
+
+ for (let i in this.include)
+ this.include.remove(this.include[i]);
+
+ pagemods.delete(this.id);
+ }
+});
+
+function onContentWindow({ target: document }) {
+ // Return if we have no pagemods
+ if (pagemods.size === 0)
+ return;
+
+ let window = document.defaultView;
+ // XML documents don't have windows, and we don't yet support them.
+ if (!window)
+ return;
+
+ // Frame event listeners are bound to the frame the event came from by default
+ let frame = this;
+ // We apply only on documents in tabs of Firefox
+ if (!frame.isTab)
+ return;
+
+ // When the tab is private, only addons with 'private-browsing' flag in
+ // their package.json can apply content script to private documents
+ if (ignoreWindow(window))
+ return;
+
+ for (let pagemod of pagemods.values()) {
+ if (modMatchesURI(pagemod, window.location.href))
+ onContent(pagemod, window);
+ }
+}
+frames.addEventListener("DOMDocElementInserted", onContentWindow, true);
+
+function applyOnExistingDocuments (mod) {
+ for (let frame of frames) {
+ // Fake a newly created document
+ let window = frame.content;
+ // on startup with e10s, contentWindow might not exist yet,
+ // in which case we will get notified by "document-element-inserted".
+ if (!window || !window.frames)
+ return;
+ let uri = window.location.href;
+ if (has(mod.attachTo, "top") && modMatchesURI(mod, uri))
+ onContent(mod, window);
+ if (has(mod.attachTo, "frame"))
+ getFrames(window).
+ filter(iframe => modMatchesURI(mod, iframe.location.href)).
+ forEach(frame => onContent(mod, frame));
+ }
+}
+
+function createWorker(mod, window) {
+ let workerId = String(uuid());
+
+ // Instruct the parent to connect to this worker. Do this first so the parent
+ // side is connected before the worker attempts to send any messages there
+ let frame = frames.getFrameForWindow(window.top);
+ frame.port.emit('sdk/page-mod/worker-create', mod.id, {
+ id: workerId,
+ url: window.location.href
+ });
+
+ // Create a child worker and notify the parent
+ let worker = WorkerChild({
+ id: workerId,
+ window: window,
+ contentScript: mod.contentScript,
+ contentScriptFile: mod.contentScriptFile,
+ contentScriptOptions: mod.contentScriptOptions
+ });
+
+ once(worker, 'detach', () => worker.destroy());
+}
+
+function onContent (mod, window) {
+ let isTopDocument = window.top === window;
+ // Is a top level document and `top` is not set, ignore
+ if (isTopDocument && !has(mod.attachTo, "top"))
+ return;
+ // Is a frame document and `frame` is not set, ignore
+ if (!isTopDocument && !has(mod.attachTo, "frame"))
+ return;
+
+ // ensure we attach only once per document
+ let seen = mod.seenDocuments;
+ if (seen.has(window.document))
+ return;
+ seen.set(window.document, true);
+
+ let style = styleFor(mod);
+ if (style)
+ attach(style, window);
+
+ // Immediately evaluate content script if the document state is already
+ // matching contentScriptWhen expectations
+ if (isMatchingAttachState(mod, window)) {
+ createWorker(mod, window);
+ return;
+ }
+
+ let eventName = getAttachEventType(mod) || 'load';
+ domOn(window, eventName, function onReady (e) {
+ if (e.target.defaultView !== window)
+ return;
+ domOff(window, eventName, onReady, true);
+ createWorker(mod, window);
+
+ // Attaching is asynchronous so if the document is already loaded we will
+ // miss the pageshow event so send a synthetic one.
+ if (window.document.readyState == "complete") {
+ mod.on('attach', worker => {
+ try {
+ worker.send('pageshow');
+ emit(worker, 'pageshow');
+ }
+ catch (e) {
+ // This can fail if an earlier attach listener destroyed the worker
+ }
+ });
+ }
+ }, true);
+}
+
+function isMatchingAttachState (mod, window) {
+ let state = window.document.readyState;
+ return 'start' === mod.contentScriptWhen ||
+ // Is `load` event already dispatched?
+ 'complete' === state ||
+ // Is DOMContentLoaded already dispatched and waiting for it?
+ ('ready' === mod.contentScriptWhen && state === 'interactive')
+}
+
+process.port.on('sdk/page-mod/create', (process, model) => {
+ if (pagemods.has(model.id))
+ return;
+
+ new ChildPageMod(model);
+});
+
+process.port.on('sdk/page-mod/destroy', (process, id) => {
+ let mod = pagemods.get(id);
+ if (mod)
+ mod.destroy();
+});
diff --git a/toolkit/jetpack/sdk/content/page-worker.js b/toolkit/jetpack/sdk/content/page-worker.js
new file mode 100644
index 000000000..e9e741120
--- /dev/null
+++ b/toolkit/jetpack/sdk/content/page-worker.js
@@ -0,0 +1,154 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { frames } = require("../remote/child");
+const { Class } = require("../core/heritage");
+const { Disposable } = require('../core/disposable');
+const { data } = require("../self");
+const { once } = require("../dom/events");
+const { getAttachEventType } = require("./utils");
+const { Rules } = require('../util/rules');
+const { uuid } = require('../util/uuid');
+const { WorkerChild } = require("./worker-child");
+const { Cc, Ci, Cu } = require("chrome");
+const { observe } = require("../event/chrome");
+const { on } = require("../event/core");
+
+const appShell = Cc["@mozilla.org/appshell/appShellService;1"].getService(Ci.nsIAppShellService);
+
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+
+const pages = new Map();
+
+const DOC_INSERTED = "document-element-inserted";
+
+function isValidURL(page, url) {
+ return !page.rules || page.rules.matchesAny(url);
+}
+
+const ChildPage = Class({
+ implements: [ Disposable ],
+ setup: function(frame, id, options) {
+ this.id = id;
+ this.frame = frame;
+ this.options = options;
+
+ this.webNav = appShell.createWindowlessBrowser(false);
+ this.docShell.allowJavascript = this.options.allow.script;
+
+ // Accessing the browser's window forces the initial about:blank document to
+ // be created before we start listening for notifications
+ this.contentWindow;
+
+ this.webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
+
+ pages.set(this.id, this);
+
+ this.contentURL = options.contentURL;
+
+ if (options.include) {
+ this.rules = Rules();
+ this.rules.add.apply(this.rules, [].concat(options.include));
+ }
+ },
+
+ dispose: function() {
+ pages.delete(this.id);
+ this.webProgress.removeProgressListener(this);
+ this.webNav.close();
+ this.webNav = null;
+ },
+
+ attachWorker: function() {
+ if (!isValidURL(this, this.contentWindow.location.href))
+ return;
+
+ this.options.id = uuid().toString();
+ this.options.window = this.contentWindow;
+ this.frame.port.emit("sdk/frame/connect", this.id, {
+ id: this.options.id,
+ url: this.contentWindow.document.documentURIObject.spec
+ });
+ new WorkerChild(this.options);
+ },
+
+ get docShell() {
+ return this.webNav.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell);
+ },
+
+ get webProgress() {
+ return this.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ },
+
+ get contentWindow() {
+ return this.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ },
+
+ get contentURL() {
+ return this.options.contentURL;
+ },
+ set contentURL(url) {
+ this.options.contentURL = url;
+
+ url = this.options.contentURL ? data.url(this.options.contentURL) : "about:blank";
+ this.webNav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null);
+ },
+
+ onLocationChange: function(progress, request, location, flags) {
+ // Ignore inner-frame events
+ if (progress != this.webProgress)
+ return;
+ // Ignore events that don't change the document
+ if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
+ return;
+
+ let event = getAttachEventType(this.options);
+ // Attaching at the start of the load is handled by the
+ // document-element-inserted listener.
+ if (event == DOC_INSERTED)
+ return;
+
+ once(this.contentWindow, event, () => {
+ this.attachWorker();
+ }, false);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI(["nsIWebProgressListener", "nsISupportsWeakReference"])
+});
+
+on(observe(DOC_INSERTED), "data", ({ target }) => {
+ let page = Array.from(pages.values()).find(p => p.contentWindow.document === target);
+ if (!page)
+ return;
+
+ if (getAttachEventType(page.options) == DOC_INSERTED)
+ page.attachWorker();
+});
+
+frames.port.on("sdk/frame/create", (frame, id, options) => {
+ new ChildPage(frame, id, options);
+});
+
+frames.port.on("sdk/frame/set", (frame, id, params) => {
+ let page = pages.get(id);
+ if (!page)
+ return;
+
+ if ("allowScript" in params)
+ page.docShell.allowJavascript = params.allowScript;
+ if ("contentURL" in params)
+ page.contentURL = params.contentURL;
+});
+
+frames.port.on("sdk/frame/destroy", (frame, id) => {
+ let page = pages.get(id);
+ if (!page)
+ return;
+
+ page.destroy();
+});
diff --git a/toolkit/jetpack/sdk/content/sandbox.js b/toolkit/jetpack/sdk/content/sandbox.js
new file mode 100644
index 000000000..096ba5c87
--- /dev/null
+++ b/toolkit/jetpack/sdk/content/sandbox.js
@@ -0,0 +1,426 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+module.metadata = {
+ 'stability': 'unstable'
+};
+
+const { Class } = require('../core/heritage');
+const { EventTarget } = require('../event/target');
+const { on, off, emit } = require('../event/core');
+const { events } = require('./sandbox/events');
+const { requiresAddonGlobal } = require('./utils');
+const { delay: async } = require('../lang/functional');
+const { Ci, Cu, Cc } = require('chrome');
+const timer = require('../timers');
+const { URL } = require('../url');
+const { sandbox, evaluate, load } = require('../loader/sandbox');
+const { merge } = require('../util/object');
+const { getTabForContentWindowNoShim } = require('../tabs/utils');
+const { getInnerId } = require('../window/utils');
+const { PlainTextConsole } = require('../console/plain-text');
+const { data } = require('../self');const { isChildLoader } = require('../remote/core');
+// WeakMap of sandboxes so we can access private values
+const sandboxes = new WeakMap();
+
+/* Trick the linker in order to ensure shipping these files in the XPI.
+ require('./content-worker.js');
+ Then, retrieve URL of these files in the XPI:
+*/
+var prefix = module.uri.split('sandbox.js')[0];
+const CONTENT_WORKER_URL = prefix + 'content-worker.js';
+const metadata = require('@loader/options').metadata;
+
+// Fetch additional list of domains to authorize access to for each content
+// script. It is stored in manifest `metadata` field which contains
+// package.json data. This list is originaly defined by authors in
+// `permissions` attribute of their package.json addon file.
+const permissions = (metadata && metadata['permissions']) || {};
+const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
+
+const waiveSecurityMembrane = !!permissions['unsafe-content-script'];
+
+const nsIScriptSecurityManager = Ci.nsIScriptSecurityManager;
+const secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].
+ getService(Ci.nsIScriptSecurityManager);
+
+const JS_VERSION = '1.8';
+
+// Tests whether this window is loaded in a tab
+function isWindowInTab(window) {
+ if (isChildLoader) {
+ let { frames } = require('../remote/child');
+ let frame = frames.getFrameForWindow(window.top);
+ return frame && frame.isTab;
+ }
+ else {
+ // The deprecated sync worker API still does everything in the main process
+ return getTabForContentWindowNoShim(window);
+ }
+}
+
+const WorkerSandbox = Class({
+ implements: [ EventTarget ],
+
+ /**
+ * Emit a message to the worker content sandbox
+ */
+ emit: function emit(type, ...args) {
+ // JSON.stringify is buggy with cross-sandbox values,
+ // it may return "{}" on functions. Use a replacer to match them correctly.
+ let replacer = (k, v) =>
+ typeof(v) === "function"
+ ? (type === "console" ? Function.toString.call(v) : void(0))
+ : v;
+
+ // Ensure having an asynchronous behavior
+ async(() =>
+ emitToContent(this, JSON.stringify([type, ...args], replacer))
+ );
+ },
+
+ /**
+ * Synchronous version of `emit`.
+ * /!\ Should only be used when it is strictly mandatory /!\
+ * Doesn't ensure passing only JSON values.
+ * Mainly used by context-menu in order to avoid breaking it.
+ */
+ emitSync: function emitSync(...args) {
+ // because the arguments could be also non JSONable values,
+ // we need to ensure the array instance is created from
+ // the content's sandbox
+ return emitToContent(this, new modelFor(this).sandbox.Array(...args));
+ },
+
+ /**
+ * Configures sandbox and loads content scripts into it.
+ * @param {Worker} worker
+ * content worker
+ */
+ initialize: function WorkerSandbox(worker, window) {
+ let model = {};
+ sandboxes.set(this, model);
+ model.worker = worker;
+ // We receive a wrapped window, that may be an xraywrapper if it's content
+ let proto = window;
+
+ // TODO necessary?
+ // Ensure that `emit` has always the right `this`
+ this.emit = this.emit.bind(this);
+ this.emitSync = this.emitSync.bind(this);
+
+ // Use expanded principal for content-script if the content is a
+ // regular web content for better isolation.
+ // (This behavior can be turned off for now with the unsafe-content-script
+ // flag to give addon developers time for making the necessary changes)
+ // But prevent it when the Worker isn't used for a content script but for
+ // injecting `addon` object into a Panel scope, for example.
+ // That's because:
+ // 1/ It is useless to use multiple domains as the worker is only used
+ // to communicate with the addon,
+ // 2/ By using it it would prevent the document to have access to any JS
+ // value of the worker. As JS values coming from multiple domain principals
+ // can't be accessed by 'mono-principals' (principal with only one domain).
+ // Even if this principal is for a domain that is specified in the multiple
+ // domain principal.
+ let principals = window;
+ let wantGlobalProperties = [];
+ let isSystemPrincipal = secMan.isSystemPrincipal(
+ window.document.nodePrincipal);
+ if (!isSystemPrincipal && !requiresAddonGlobal(worker)) {
+ if (EXPANDED_PRINCIPALS.length > 0) {
+ // We have to replace XHR constructor of the content document
+ // with a custom cross origin one, automagically added by platform code:
+ delete proto.XMLHttpRequest;
+ wantGlobalProperties.push('XMLHttpRequest');
+ }
+ if (!waiveSecurityMembrane)
+ principals = EXPANDED_PRINCIPALS.concat(window);
+ }
+
+ // Create the sandbox and bind it to window in order for content scripts to
+ // have access to all standard globals (window, document, ...)
+ let content = sandbox(principals, {
+ sandboxPrototype: proto,
+ wantXrays: !requiresAddonGlobal(worker),
+ wantGlobalProperties: wantGlobalProperties,
+ wantExportHelpers: true,
+ sameZoneAs: window,
+ metadata: {
+ SDKContentScript: true,
+ 'inner-window-id': getInnerId(window)
+ }
+ });
+ model.sandbox = content;
+
+ // We have to ensure that window.top and window.parent are the exact same
+ // object than window object, i.e. the sandbox global object. But not
+ // always, in case of iframes, top and parent are another window object.
+ let top = window.top === window ? content : content.top;
+ let parent = window.parent === window ? content : content.parent;
+ merge(content, {
+ // We need 'this === window === top' to be true in toplevel scope:
+ get window() {
+ return content;
+ },
+ get top() {
+ return top;
+ },
+ get parent() {
+ return parent;
+ }
+ });
+
+ // 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!
+ //
+ // Note that because waivers aren't propagated between origins, we
+ // need the unsafeWindow getter to live in the sandbox.
+ var unsafeWindowGetter =
+ new content.Function('return window.wrappedJSObject || window;');
+ Object.defineProperty(content, 'unsafeWindow', {get: unsafeWindowGetter});
+
+ // Load trusted code that will inject content script API.
+ let ContentWorker = load(content, CONTENT_WORKER_URL);
+
+ // prepare a clean `self.options`
+ let options = 'contentScriptOptions' in worker ?
+ JSON.stringify(worker.contentScriptOptions) :
+ undefined;
+
+ // Then call `inject` method and communicate with this script
+ // by trading two methods that allow to send events to the other side:
+ // - `onEvent` called by content script
+ // - `result.emitToContent` called by addon script
+ let onEvent = Cu.exportFunction(onContentEvent.bind(null, this), ContentWorker);
+ let chromeAPI = createChromeAPI(ContentWorker);
+ let result = Cu.waiveXrays(ContentWorker).inject(content, chromeAPI, onEvent, options);
+
+ // Merge `emitToContent` into our private model of the
+ // WorkerSandbox so we can communicate with content script
+ model.emitToContent = result;
+
+ let console = new PlainTextConsole(null, getInnerId(window));
+
+ // Handle messages send by this script:
+ setListeners(this, console);
+
+ // Inject `addon` global into target document if document is trusted,
+ // `addon` in document is equivalent to `self` in content script.
+ if (requiresAddonGlobal(worker)) {
+ Object.defineProperty(getUnsafeWindow(window), 'addon', {
+ value: content.self,
+ configurable: true
+ }
+ );
+ }
+
+ // Inject our `console` into target document if worker doesn't have a tab
+ // (e.g Panel, PageWorker).
+ // `worker.tab` can't be used because bug 804935.
+ if (!isWindowInTab(window)) {
+ let win = getUnsafeWindow(window);
+
+ // export our chrome console to content window, as described here:
+ // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn
+ let con = Cu.createObjectIn(win);
+
+ let genPropDesc = function genPropDesc(fun) {
+ return { enumerable: true, configurable: true, writable: true,
+ value: console[fun] };
+ }
+
+ const properties = {
+ log: genPropDesc('log'),
+ info: genPropDesc('info'),
+ warn: genPropDesc('warn'),
+ error: genPropDesc('error'),
+ debug: genPropDesc('debug'),
+ trace: genPropDesc('trace'),
+ dir: genPropDesc('dir'),
+ group: genPropDesc('group'),
+ groupCollapsed: genPropDesc('groupCollapsed'),
+ groupEnd: genPropDesc('groupEnd'),
+ time: genPropDesc('time'),
+ timeEnd: genPropDesc('timeEnd'),
+ profile: genPropDesc('profile'),
+ profileEnd: genPropDesc('profileEnd'),
+ exception: genPropDesc('exception'),
+ assert: genPropDesc('assert'),
+ count: genPropDesc('count'),
+ table: genPropDesc('table'),
+ clear: genPropDesc('clear'),
+ dirxml: genPropDesc('dirxml'),
+ markTimeline: genPropDesc('markTimeline'),
+ timeline: genPropDesc('timeline'),
+ timelineEnd: genPropDesc('timelineEnd'),
+ timeStamp: genPropDesc('timeStamp'),
+ };
+
+ Object.defineProperties(con, properties);
+ Cu.makeObjectPropsNormal(con);
+
+ win.console = con;
+ };
+
+ emit(events, "content-script-before-inserted", {
+ window: window,
+ worker: worker
+ });
+
+ // 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)
+ importScripts.apply(null, [this].concat(contentScriptFile));
+
+ if (contentScript) {
+ evaluateIn(
+ this,
+ Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript
+ );
+ }
+ },
+ destroy: function destroy(reason) {
+ if (typeof reason != 'string')
+ reason = '';
+ this.emitSync('event', 'detach', reason);
+ let model = modelFor(this);
+ model.sandbox = null
+ model.worker = null;
+ },
+
+});
+
+exports.WorkerSandbox = WorkerSandbox;
+
+/**
+ * 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.
+ */
+function importScripts (workerSandbox, ...urls) {
+ let { worker, sandbox } = modelFor(workerSandbox);
+ for (let i in urls) {
+ let contentScriptFile = data.url(urls[i]);
+
+ try {
+ let uri = URL(contentScriptFile);
+ if (uri.scheme === 'resource')
+ load(sandbox, String(uri));
+ else
+ throw Error('Unsupported `contentScriptFile` url: ' + String(uri));
+ }
+ catch(e) {
+ emit(worker, 'error', e);
+ }
+ }
+}
+
+function setListeners (workerSandbox, console) {
+ let { worker } = modelFor(workerSandbox);
+ // console.xxx calls
+ workerSandbox.on('console', function consoleListener (kind, ...args) {
+ console[kind].apply(console, args);
+ });
+
+ // self.postMessage calls
+ workerSandbox.on('message', function postMessage(data) {
+ // destroyed?
+ if (worker)
+ emit(worker, 'message', data);
+ });
+
+ // self.port.emit calls
+ workerSandbox.on('event', function portEmit (...eventArgs) {
+ // If not destroyed, emit event information to worker
+ // `eventArgs` has the event name as first element,
+ // and remaining elements are additional arguments to pass
+ if (worker)
+ emit.apply(null, [worker.port].concat(eventArgs));
+ });
+
+ // unwrap, recreate and propagate async Errors thrown from content-script
+ workerSandbox.on('error', function onError({instanceOfError, value}) {
+ if (worker) {
+ let error = value;
+ if (instanceOfError) {
+ error = new Error(value.message, value.fileName, value.lineNumber);
+ error.stack = value.stack;
+ error.name = value.name;
+ }
+ emit(worker, 'error', error);
+ }
+ });
+}
+
+/**
+ * Evaluates code in the sandbox.
+ * @param {String} code
+ * JavaScript source to evaluate.
+ * @param {String} [filename='javascript:' + code]
+ * Name of the file
+ */
+function evaluateIn (workerSandbox, code, filename) {
+ let { worker, sandbox } = modelFor(workerSandbox);
+ try {
+ evaluate(sandbox, code, filename || 'javascript:' + code);
+ }
+ catch(e) {
+ emit(worker, 'error', e);
+ }
+}
+
+/**
+ * Method called by the worker sandbox when it needs to send a message
+ */
+function onContentEvent (workerSandbox, args) {
+ // As `emit`, we ensure having an asynchronous behavior
+ async(function () {
+ // We emit event to chrome/addon listeners
+ emit.apply(null, [workerSandbox].concat(JSON.parse(args)));
+ });
+}
+
+
+function modelFor (workerSandbox) {
+ return sandboxes.get(workerSandbox);
+}
+
+function getUnsafeWindow (win) {
+ return win.wrappedJSObject || win;
+}
+
+function emitToContent (workerSandbox, args) {
+ return modelFor(workerSandbox).emitToContent(args);
+}
+
+function createChromeAPI (scope) {
+ return Cu.cloneInto({
+ timers: {
+ setTimeout: timer.setTimeout.bind(timer),
+ setInterval: timer.setInterval.bind(timer),
+ clearTimeout: timer.clearTimeout.bind(timer),
+ clearInterval: timer.clearInterval.bind(timer),
+ },
+ sandbox: {
+ evaluate: evaluate,
+ },
+ }, scope, {cloneFunctions: true});
+}
diff --git a/toolkit/jetpack/sdk/content/sandbox/events.js b/toolkit/jetpack/sdk/content/sandbox/events.js
new file mode 100644
index 000000000..d6f7eb004
--- /dev/null
+++ b/toolkit/jetpack/sdk/content/sandbox/events.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.metadata = {
+ "stability": "experimental"
+};
+
+const events = {};
+exports.events = events;
diff --git a/toolkit/jetpack/sdk/content/tab-events.js b/toolkit/jetpack/sdk/content/tab-events.js
new file mode 100644
index 000000000..9e244a853
--- /dev/null
+++ b/toolkit/jetpack/sdk/content/tab-events.js
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { Ci } = require('chrome');
+const system = require('sdk/system/events');
+const { frames } = require('sdk/remote/child');
+const { WorkerChild } = require('sdk/content/worker-child');
+
+// map observer topics to tab event names
+const EVENTS = {
+ 'content-document-global-created': 'create',
+ 'chrome-document-global-created': 'create',
+ 'content-document-interactive': 'ready',
+ 'chrome-document-interactive': 'ready',
+ 'content-document-loaded': 'load',
+ 'chrome-document-loaded': 'load',
+// 'content-page-shown': 'pageshow', // bug 1024105
+}
+
+function topicListener({ subject, type }) {
+ // NOTE detect the window from the subject:
+ // - on *-global-created the subject is the window
+ // - in the other cases it is the document object
+ let window = subject instanceof Ci.nsIDOMWindow ? subject : subject.defaultView;
+ if (!window){
+ return;
+ }
+ let frame = frames.getFrameForWindow(window);
+ if (frame) {
+ let readyState = frame.content.document.readyState;
+ frame.port.emit('sdk/tab/event', EVENTS[type], { readyState });
+ }
+}
+
+for (let topic in EVENTS)
+ system.on(topic, topicListener, true);
+
+// bug 1024105 - content-page-shown notification doesn't pass persisted param
+function eventListener({target, type, persisted}) {
+ let frame = this;
+ if (target === frame.content.document) {
+ frame.port.emit('sdk/tab/event', type, persisted);
+ }
+}
+frames.addEventListener('pageshow', eventListener, true);
+
+frames.port.on('sdk/tab/attach', (frame, options) => {
+ options.window = frame.content;
+ new WorkerChild(options);
+});
+
+// Forward the existent frames's readyState.
+for (let frame of frames) {
+ let readyState = frame.content.document.readyState;
+ frame.port.emit('sdk/tab/event', 'init', { readyState });
+}
diff --git a/toolkit/jetpack/sdk/content/thumbnail.js b/toolkit/jetpack/sdk/content/thumbnail.js
new file mode 100644
index 000000000..783615fc6
--- /dev/null
+++ b/toolkit/jetpack/sdk/content/thumbnail.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+module.metadata = {
+ 'stability': 'unstable'
+};
+
+const { Cc, Ci, Cu } = require('chrome');
+const AppShellService = Cc['@mozilla.org/appshell/appShellService;1'].
+ getService(Ci.nsIAppShellService);
+
+const NS = 'http://www.w3.org/1999/xhtml';
+const COLOR = 'rgb(255,255,255)';
+
+/**
+ * Creates canvas element with a thumbnail of the passed window.
+ * @param {Window} window
+ * @returns {Element}
+ */
+function getThumbnailCanvasForWindow(window) {
+ let aspectRatio = 0.5625; // 16:9
+ let thumbnail = AppShellService.hiddenDOMWindow.document
+ .createElementNS(NS, 'canvas');
+ thumbnail.mozOpaque = true;
+ thumbnail.width = Math.ceil(window.screen.availWidth / 5.75);
+ thumbnail.height = Math.round(thumbnail.width * aspectRatio);
+ let ctx = thumbnail.getContext('2d');
+ let snippetWidth = window.innerWidth * .6;
+ let scale = thumbnail.width / snippetWidth;
+ ctx.scale(scale, scale);
+ ctx.drawWindow(window, window.scrollX, window.scrollY, snippetWidth,
+ snippetWidth * aspectRatio, COLOR);
+ return thumbnail;
+}
+exports.getThumbnailCanvasForWindow = getThumbnailCanvasForWindow;
+
+/**
+ * Creates Base64 encoded data URI of the thumbnail for the passed window.
+ * @param {Window} window
+ * @returns {String}
+ */
+exports.getThumbnailURIForWindow = function getThumbnailURIForWindow(window) {
+ return getThumbnailCanvasForWindow(window).toDataURL()
+};
+
+// default 80x45 blank when not available
+exports.BLANK = 'data:image/png;base64,' +
+ 'iVBORw0KGgoAAAANSUhEUgAAAFAAAAAtCAYAAAA5reyyAAAAJElEQVRoge3BAQ'+
+ 'EAAACCIP+vbkhAAQAAAAAAAAAAAAAAAADXBjhtAAGQ0AF/AAAAAElFTkSuQmCC';
diff --git a/toolkit/jetpack/sdk/content/utils.js b/toolkit/jetpack/sdk/content/utils.js
new file mode 100644
index 000000000..90995a614
--- /dev/null
+++ b/toolkit/jetpack/sdk/content/utils.js
@@ -0,0 +1,105 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+module.metadata = {
+ 'stability': 'unstable'
+};
+
+var { merge } = require('../util/object');
+var { data } = require('../self');
+var assetsURI = data.url();
+var isArray = Array.isArray;
+var method = require('../../method/core');
+var { uuid } = require('../util/uuid');
+
+const isAddonContent = ({ contentURL }) =>
+ contentURL && data.url(contentURL).startsWith(assetsURI);
+
+exports.isAddonContent = isAddonContent;
+
+function hasContentScript({ contentScript, contentScriptFile }) {
+ return (isArray(contentScript) ? contentScript.length > 0 :
+ !!contentScript) ||
+ (isArray(contentScriptFile) ? contentScriptFile.length > 0 :
+ !!contentScriptFile);
+}
+exports.hasContentScript = hasContentScript;
+
+function requiresAddonGlobal(model) {
+ return model.injectInDocument || (isAddonContent(model) && !hasContentScript(model));
+}
+exports.requiresAddonGlobal = requiresAddonGlobal;
+
+function getAttachEventType(model) {
+ if (!model) return null;
+ let when = model.contentScriptWhen;
+ return requiresAddonGlobal(model) ? 'document-element-inserted' :
+ when === 'start' ? 'document-element-inserted' :
+ when === 'ready' ? 'DOMContentLoaded' :
+ when === 'end' ? 'load' :
+ null;
+}
+exports.getAttachEventType = getAttachEventType;
+
+var attach = method('worker-attach');
+exports.attach = attach;
+
+var connect = method('worker-connect');
+exports.connect = connect;
+
+var detach = method('worker-detach');
+exports.detach = detach;
+
+var destroy = method('worker-destroy');
+exports.destroy = destroy;
+
+function WorkerHost (workerFor) {
+ // Define worker properties that just proxy to underlying worker
+ return ['postMessage', 'port', 'url', 'tab'].reduce(function(proto, name) {
+ // Use descriptor properties instead so we can call
+ // the worker function in the context of the worker so we
+ // don't have to create new functions with `fn.bind(worker)`
+ let descriptorProp = {
+ value: function (...args) {
+ let worker = workerFor(this);
+ return worker[name].apply(worker, args);
+ }
+ };
+
+ let accessorProp = {
+ get: function () { return workerFor(this)[name]; },
+ set: function (value) { workerFor(this)[name] = value; }
+ };
+
+ Object.defineProperty(proto, name, merge({
+ enumerable: true,
+ configurable: false,
+ }, isDescriptor(name) ? descriptorProp : accessorProp));
+ return proto;
+ }, {});
+
+ function isDescriptor (prop) {
+ return ~['postMessage'].indexOf(prop);
+ }
+}
+exports.WorkerHost = WorkerHost;
+
+function makeChildOptions(options) {
+ function makeStringArray(arrayOrValue) {
+ if (!arrayOrValue)
+ return [];
+ return [].concat(arrayOrValue).map(String);
+ }
+
+ return {
+ id: String(uuid()),
+ contentScript: makeStringArray(options.contentScript),
+ contentScriptFile: makeStringArray(options.contentScriptFile),
+ contentScriptOptions: options.contentScriptOptions ?
+ JSON.stringify(options.contentScriptOptions) :
+ null,
+ }
+}
+exports.makeChildOptions = makeChildOptions;
diff --git a/toolkit/jetpack/sdk/content/worker-child.js b/toolkit/jetpack/sdk/content/worker-child.js
new file mode 100644
index 000000000..dbf65a933
--- /dev/null
+++ b/toolkit/jetpack/sdk/content/worker-child.js
@@ -0,0 +1,158 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+const { merge } = require('../util/object');
+const { Class } = require('../core/heritage');
+const { emit } = require('../event/core');
+const { EventTarget } = require('../event/target');
+const { getInnerId, getByInnerId } = require('../window/utils');
+const { instanceOf, isObject } = require('../lang/type');
+const system = require('../system/events');
+const { when } = require('../system/unload');
+const { WorkerSandbox } = require('./sandbox');
+const { Ci } = require('chrome');
+const { process, frames } = require('../remote/child');
+
+const EVENTS = {
+ 'chrome-page-shown': 'pageshow',
+ 'content-page-shown': 'pageshow',
+ 'chrome-page-hidden': 'pagehide',
+ 'content-page-hidden': 'pagehide',
+ 'inner-window-destroyed': 'detach',
+}
+
+// The parent Worker must have been created (or an async message sent to spawn
+// its creation) before creating the WorkerChild or messages from the content
+// script to the parent will get lost.
+const WorkerChild = Class({
+ implements: [EventTarget],
+
+ initialize(options) {
+ merge(this, options);
+ keepAlive.set(this.id, this);
+
+ this.windowId = getInnerId(this.window);
+ if (this.contentScriptOptions)
+ this.contentScriptOptions = JSON.parse(this.contentScriptOptions);
+
+ this.port = EventTarget();
+ this.port.on('*', this.send.bind(this, 'event'));
+ this.on('*', this.send.bind(this));
+
+ this.observe = this.observe.bind(this);
+
+ for (let topic in EVENTS)
+ system.on(topic, this.observe);
+
+ this.receive = this.receive.bind(this);
+ process.port.on('sdk/worker/message', this.receive);
+
+ this.sandbox = WorkerSandbox(this, this.window);
+
+ // If the document has an unexpected readyState, its worker-child instance is initialized
+ // as frozen until one of the known readyState is reached.
+ let initialDocumentReadyState = this.window.document.readyState;
+ this.frozen = [
+ "loading", "interactive", "complete"
+ ].includes(initialDocumentReadyState) ? false : true;
+
+ if (this.frozen) {
+ console.warn("SDK worker-child started as frozen on unexpected initial document.readyState", {
+ initialDocumentReadyState, windowLocation: this.window.location.href,
+ });
+ }
+
+ this.frozenMessages = [];
+ this.on('pageshow', () => {
+ this.frozen = false;
+ this.frozenMessages.forEach(args => this.sandbox.emit(...args));
+ this.frozenMessages = [];
+ });
+ this.on('pagehide', () => {
+ this.frozen = true;
+ });
+ },
+
+ // messages
+ receive(process, id, args) {
+ if (id !== this.id)
+ return;
+ args = JSON.parse(args);
+
+ if (this.frozen)
+ this.frozenMessages.push(args);
+ else
+ this.sandbox.emit(...args);
+
+ if (args[0] === 'detach')
+ this.destroy(args[1]);
+ },
+
+ send(...args) {
+ process.port.emit('sdk/worker/event', this.id, JSON.stringify(args, exceptions));
+ },
+
+ // notifications
+ observe({ type, subject }) {
+ if (!this.sandbox)
+ return;
+
+ if (subject.defaultView && getInnerId(subject.defaultView) === this.windowId) {
+ this.sandbox.emitSync(EVENTS[type]);
+ emit(this, EVENTS[type]);
+ }
+
+ if (type === 'inner-window-destroyed' &&
+ subject.QueryInterface(Ci.nsISupportsPRUint64).data === this.windowId) {
+ this.destroy();
+ }
+ },
+
+ get frame() {
+ return frames.getFrameForWindow(this.window.top);
+ },
+
+ // detach/destroy: unload and release the sandbox
+ destroy(reason) {
+ if (!this.sandbox)
+ return;
+
+ for (let topic in EVENTS)
+ system.off(topic, this.observe);
+ process.port.off('sdk/worker/message', this.receive);
+
+ this.sandbox.destroy(reason);
+ this.sandbox = null;
+ keepAlive.delete(this.id);
+
+ this.send('detach');
+ }
+})
+exports.WorkerChild = WorkerChild;
+
+// Error instances JSON poorly
+function exceptions(key, value) {
+ if (!isObject(value) || !instanceOf(value, Error))
+ return value;
+ let _errorType = value.constructor.name;
+ let { message, fileName, lineNumber, stack, name } = value;
+ return { _errorType, message, fileName, lineNumber, stack, name };
+}
+
+// workers for windows in this tab
+var keepAlive = new Map();
+
+process.port.on('sdk/worker/create', (process, options, cpows) => {
+ options.window = cpows.window;
+ let worker = new WorkerChild(options);
+
+ let frame = frames.getFrameForWindow(options.window.top);
+ frame.port.emit('sdk/worker/connect', options.id, options.window.location.href);
+});
+
+when(reason => {
+ for (let worker of keepAlive.values())
+ worker.destroy(reason);
+});
diff --git a/toolkit/jetpack/sdk/content/worker.js b/toolkit/jetpack/sdk/content/worker.js
new file mode 100644
index 000000000..39b940a88
--- /dev/null
+++ b/toolkit/jetpack/sdk/content/worker.js
@@ -0,0 +1,180 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const { emit } = require('../event/core');
+const { omit, merge } = require('../util/object');
+const { Class } = require('../core/heritage');
+const { method } = require('../lang/functional');
+const { getInnerId } = require('../window/utils');
+const { EventTarget } = require('../event/target');
+const { isPrivate } = require('../private-browsing/utils');
+const { getTabForBrowser, getTabForContentWindowNoShim, getBrowserForTab } = require('../tabs/utils');
+const { attach, connect, detach, destroy, makeChildOptions } = require('./utils');
+const { ensure } = require('../system/unload');
+const { on: observe } = require('../system/events');
+const { Ci, Cu } = require('chrome');
+const { modelFor: tabFor } = require('sdk/model/core');
+const { remoteRequire, processes, frames } = require('../remote/parent');
+remoteRequire('sdk/content/worker-child');
+
+const workers = new WeakMap();
+var modelFor = (worker) => workers.get(worker);
+
+const ERR_DESTROYED = "Couldn't find the worker to receive this message. " +
+ "The script may not be initialized yet, or may already have been unloaded.";
+
+// a handle for communication between content script and addon code
+const Worker = Class({
+ implements: [EventTarget],
+
+ initialize(options = {}) {
+ ensure(this, 'detach');
+
+ let model = {
+ attached: false,
+ destroyed: false,
+ earlyEvents: [], // fired before worker was attached
+ frozen: true, // document is not yet active
+ options,
+ };
+ workers.set(this, model);
+
+ this.on('detach', this.detach);
+ EventTarget.prototype.initialize.call(this, options);
+
+ this.receive = this.receive.bind(this);
+
+ this.port = EventTarget();
+ this.port.emit = this.send.bind(this, 'event');
+ this.postMessage = this.send.bind(this, 'message');
+
+ if ('window' in options) {
+ let window = options.window;
+ delete options.window;
+ attach(this, window);
+ }
+ },
+
+ // messages
+ receive(process, id, args) {
+ let model = modelFor(this);
+ if (id !== model.id || !model.attached)
+ return;
+ args = JSON.parse(args);
+ if (model.destroyed && args[0] != 'detach')
+ return;
+
+ if (args[0] === 'event')
+ emit(this.port, ...args.slice(1))
+ else
+ emit(this, ...args);
+ },
+
+ send(...args) {
+ let model = modelFor(this);
+ if (model.destroyed && args[0] !== 'detach')
+ throw new Error(ERR_DESTROYED);
+
+ if (!model.attached) {
+ model.earlyEvents.push(args);
+ return;
+ }
+
+ processes.port.emit('sdk/worker/message', model.id, JSON.stringify(args));
+ },
+
+ // properties
+ get url() {
+ let { url } = modelFor(this);
+ return url;
+ },
+
+ get contentURL() {
+ return this.url;
+ },
+
+ get tab() {
+ require('sdk/tabs');
+ let { frame } = modelFor(this);
+ if (!frame)
+ return null;
+ let rawTab = getTabForBrowser(frame.frameElement);
+ return rawTab && tabFor(rawTab);
+ },
+
+ toString: () => '[object Worker]',
+
+ detach: method(detach),
+ destroy: method(destroy),
+})
+exports.Worker = Worker;
+
+attach.define(Worker, function(worker, window) {
+ let model = modelFor(worker);
+ if (model.attached)
+ detach(worker);
+
+ let childOptions = makeChildOptions(model.options);
+ processes.port.emitCPOW('sdk/worker/create', [childOptions], { window });
+
+ let listener = (frame, id, url) => {
+ if (id != childOptions.id)
+ return;
+ frames.port.off('sdk/worker/connect', listener);
+ connect(worker, frame, { id, url });
+ };
+ frames.port.on('sdk/worker/connect', listener);
+});
+
+connect.define(Worker, function(worker, frame, { id, url }) {
+ let model = modelFor(worker);
+ if (model.attached)
+ detach(worker);
+
+ model.id = id;
+ model.frame = frame;
+ model.url = url;
+
+ // Messages from content -> chrome come through the process message manager
+ // since that lives longer than the frame message manager
+ processes.port.on('sdk/worker/event', worker.receive);
+
+ model.attached = true;
+ model.destroyed = false;
+ model.frozen = false;
+
+ model.earlyEvents.forEach(args => worker.send(...args));
+ model.earlyEvents = [];
+ emit(worker, 'attach');
+});
+
+// unload and release the child worker, release window reference
+detach.define(Worker, function(worker) {
+ let model = modelFor(worker);
+ if (!model.attached)
+ return;
+
+ processes.port.off('sdk/worker/event', worker.receive);
+ model.attached = false;
+ model.destroyed = true;
+ emit(worker, 'detach');
+});
+
+isPrivate.define(Worker, ({ tab }) => isPrivate(tab));
+
+// Something in the parent side has destroyed the worker, tell the child to
+// detach, the child will respond when it has detached
+destroy.define(Worker, function(worker, reason) {
+ let model = modelFor(worker);
+ model.destroyed = true;
+ if (!model.attached)
+ return;
+
+ worker.send('detach', reason);
+});