summaryrefslogtreecommitdiffstats
path: root/toolkit/jetpack/sdk/addon
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/jetpack/sdk/addon')
-rw-r--r--toolkit/jetpack/sdk/addon/bootstrap.js182
-rw-r--r--toolkit/jetpack/sdk/addon/events.js56
-rw-r--r--toolkit/jetpack/sdk/addon/host.js12
-rw-r--r--toolkit/jetpack/sdk/addon/installer.js121
-rw-r--r--toolkit/jetpack/sdk/addon/manager.js18
-rw-r--r--toolkit/jetpack/sdk/addon/runner.js180
-rw-r--r--toolkit/jetpack/sdk/addon/window.js66
7 files changed, 635 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/addon/bootstrap.js b/toolkit/jetpack/sdk/addon/bootstrap.js
new file mode 100644
index 000000000..0397d91e5
--- /dev/null
+++ b/toolkit/jetpack/sdk/addon/bootstrap.js
@@ -0,0 +1,182 @@
+/* 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 { Cu } = require("chrome");
+const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
+const { Task: { spawn } } = require("resource://gre/modules/Task.jsm");
+const { readURI } = require("sdk/net/url");
+const { mount, unmount } = require("sdk/uri/resource");
+const { setTimeout } = require("sdk/timers");
+const { Loader, Require, Module, main, unload } = require("toolkit/loader");
+const prefs = require("sdk/preferences/service");
+
+// load below now, so that it can be used by sdk/addon/runner
+// see bug https://bugzilla.mozilla.org/show_bug.cgi?id=1042239
+const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {});
+
+const REASON = [ "unknown", "startup", "shutdown", "enable", "disable",
+ "install", "uninstall", "upgrade", "downgrade" ];
+
+const UUID_PATTERN = /^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/;
+// Takes add-on ID and normalizes it to a domain name so that add-on
+// can be mapped to resource://domain/
+const readDomain = id =>
+ // If only `@` character is the first one, than just substract it,
+ // otherwise fallback to legacy normalization code path. Note: `.`
+ // is valid character for resource substitutaiton & we intend to
+ // make add-on URIs intuitive, so it's best to just stick to an
+ // add-on author typed input.
+ id.lastIndexOf("@") === 0 ? id.substr(1).toLowerCase() :
+ id.toLowerCase().
+ replace(/@/g, "-at-").
+ replace(/\./g, "-dot-").
+ replace(UUID_PATTERN, "$1");
+
+const readPaths = id => {
+ const base = `extensions.modules.${id}.path.`;
+ const domain = readDomain(id);
+ return prefs.keys(base).reduce((paths, key) => {
+ const value = prefs.get(key);
+ const name = key.replace(base, "");
+ const path = name.split(".").join("/");
+ const prefix = path.length ? `${path}/` : path;
+ const uri = value.endsWith("/") ? value : `${value}/`;
+ const root = `extensions.modules.${domain}.commonjs.path.${name}`;
+
+ mount(root, uri);
+
+ paths[prefix] = `resource://${root}/`;
+ return paths;
+ }, {});
+};
+
+const Bootstrap = function(mountURI) {
+ this.mountURI = mountURI;
+ this.install = this.install.bind(this);
+ this.uninstall = this.uninstall.bind(this);
+ this.startup = this.startup.bind(this);
+ this.shutdown = this.shutdown.bind(this);
+};
+Bootstrap.prototype = {
+ constructor: Bootstrap,
+ mount(domain, rootURI) {
+ mount(domain, rootURI);
+ this.domain = domain;
+ },
+ unmount() {
+ if (this.domain) {
+ unmount(this.domain);
+ this.domain = null;
+ }
+ },
+ install(addon, reason) {
+ return new Promise(resolve => resolve());
+ },
+ uninstall(addon, reason) {
+ return new Promise(resolve => {
+ const {id} = addon;
+
+ prefs.reset(`extensions.${id}.sdk.domain`);
+ prefs.reset(`extensions.${id}.sdk.version`);
+ prefs.reset(`extensions.${id}.sdk.rootURI`);
+ prefs.reset(`extensions.${id}.sdk.baseURI`);
+ prefs.reset(`extensions.${id}.sdk.load.reason`);
+
+ resolve();
+ });
+ },
+ startup(addon, reasonCode) {
+ const { id, version, resourceURI: { spec: addonURI } } = addon;
+ const rootURI = this.mountURI || addonURI;
+ const reason = REASON[reasonCode];
+ const self = this;
+
+ return spawn(function*() {
+ const metadata = JSON.parse(yield readURI(`${rootURI}package.json`));
+ const domain = readDomain(id);
+ const baseURI = `resource://${domain}/`;
+
+ this.mount(domain, rootURI);
+
+ prefs.set(`extensions.${id}.sdk.domain`, domain);
+ prefs.set(`extensions.${id}.sdk.version`, version);
+ prefs.set(`extensions.${id}.sdk.rootURI`, rootURI);
+ prefs.set(`extensions.${id}.sdk.baseURI`, baseURI);
+ prefs.set(`extensions.${id}.sdk.load.reason`, reason);
+
+ const command = prefs.get(`extensions.${id}.sdk.load.command`);
+
+ const loader = Loader({
+ id,
+ isNative: true,
+ checkCompatibility: true,
+ prefixURI: baseURI,
+ rootURI: baseURI,
+ name: metadata.name,
+ paths: Object.assign({
+ "": "resource://gre/modules/commonjs/",
+ "devtools/": "resource://devtools/",
+ "./": baseURI
+ }, readPaths(id)),
+ manifest: metadata,
+ metadata: metadata,
+ modules: {
+ "@test/options": {},
+ },
+ noQuit: prefs.get(`extensions.${id}.sdk.test.no-quit`, false)
+ });
+ self.loader = loader;
+
+ const module = Module("package.json", `${baseURI}package.json`);
+ const require = Require(loader, module);
+ const main = command === "test" ? "sdk/test/runner" : null;
+ const prefsURI = `${baseURI}defaults/preferences/prefs.js`;
+
+ // Init the 'sdk/webextension' module from the bootstrap addon parameter.
+ require("sdk/webextension").initFromBootstrapAddonParam(addon);
+
+ const { startup } = require("sdk/addon/runner");
+ startup(reason, {loader, main, prefsURI});
+ }.bind(this)).catch(error => {
+ console.error(`Failed to start ${id} addon`, error);
+ throw error;
+ });
+ },
+ shutdown(addon, code) {
+ this.unmount();
+ return this.unload(REASON[code]);
+ },
+ unload(reason) {
+ return new Promise(resolve => {
+ const { loader } = this;
+ if (loader) {
+ this.loader = null;
+ unload(loader, reason);
+
+ setTimeout(() => {
+ for (let uri of Object.keys(loader.sandboxes)) {
+ let sandbox = loader.sandboxes[uri];
+ if (Cu.getClassName(sandbox, true) == "Sandbox")
+ Cu.nukeSandbox(sandbox);
+ delete loader.sandboxes[uri];
+ delete loader.modules[uri];
+ }
+
+ try {
+ Cu.nukeSandbox(loader.sharedGlobalSandbox);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+
+ resolve();
+ }, 1000);
+ }
+ else {
+ resolve();
+ }
+ });
+ }
+};
+exports.Bootstrap = Bootstrap;
diff --git a/toolkit/jetpack/sdk/addon/events.js b/toolkit/jetpack/sdk/addon/events.js
new file mode 100644
index 000000000..45bada6e1
--- /dev/null
+++ b/toolkit/jetpack/sdk/addon/events.js
@@ -0,0 +1,56 @@
+/* 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'
+};
+
+var { request: hostReq, response: hostRes } = require('./host');
+var { defer: async } = require('../lang/functional');
+var { defer } = require('../core/promise');
+var { emit: emitSync, on, off } = require('../event/core');
+var { uuid } = require('../util/uuid');
+var emit = async(emitSync);
+
+// Map of IDs to deferreds
+var requests = new Map();
+
+// May not be necessary to wrap this in `async`
+// once promises are async via bug 881047
+var receive = async(function ({data, id, error}) {
+ let request = requests.get(id);
+ if (request) {
+ if (error) request.reject(error);
+ else request.resolve(clone(data));
+ requests.delete(id);
+ }
+});
+on(hostRes, 'data', receive);
+
+/*
+ * Send is a helper to be used in client APIs to send
+ * a request to host
+ */
+function send (eventName, data) {
+ let id = uuid();
+ let deferred = defer();
+ requests.set(id, deferred);
+ emit(hostReq, 'data', {
+ id: id,
+ data: clone(data),
+ event: eventName
+ });
+ return deferred.promise;
+}
+exports.send = send;
+
+/*
+ * Implement internal structured cloning algorithm in the future?
+ * http://www.whatwg.org/specs/web-apps/current-work/multipage/common-dom-interfaces.html#internal-structured-cloning-algorithm
+ */
+function clone (obj) {
+ return JSON.parse(JSON.stringify(obj || {}));
+}
diff --git a/toolkit/jetpack/sdk/addon/host.js b/toolkit/jetpack/sdk/addon/host.js
new file mode 100644
index 000000000..91aa0e869
--- /dev/null
+++ b/toolkit/jetpack/sdk/addon/host.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"
+};
+
+exports.request = {};
+exports.response = {};
diff --git a/toolkit/jetpack/sdk/addon/installer.js b/toolkit/jetpack/sdk/addon/installer.js
new file mode 100644
index 000000000..bb8cf8d16
--- /dev/null
+++ b/toolkit/jetpack/sdk/addon/installer.js
@@ -0,0 +1,121 @@
+/* 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/. */
+
+module.metadata = {
+ "stability": "experimental"
+};
+
+const { Cc, Ci, Cu } = require("chrome");
+const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm");
+const { defer } = require("../core/promise");
+const { setTimeout } = require("../timers");
+
+/**
+ * `install` method error codes:
+ *
+ * https://developer.mozilla.org/en/Addons/Add-on_Manager/AddonManager#AddonInstall_errors
+ */
+exports.ERROR_NETWORK_FAILURE = AddonManager.ERROR_NETWORK_FAILURE;
+exports.ERROR_INCORRECT_HASH = AddonManager.ERROR_INCORRECT_HASH;
+exports.ERROR_CORRUPT_FILE = AddonManager.ERROR_CORRUPT_FILE;
+exports.ERROR_FILE_ACCESS = AddonManager.ERROR_FILE_ACCESS;
+
+/**
+ * Immediatly install an addon.
+ *
+ * @param {String} xpiPath
+ * file path to an xpi file to install
+ * @return {Promise}
+ * A promise resolved when the addon is finally installed.
+ * Resolved with addon id as value or rejected with an error code.
+ */
+exports.install = function install(xpiPath) {
+ let { promise, resolve, reject } = defer();
+
+ // Create nsIFile for the xpi file
+ let file = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsILocalFile);
+ try {
+ file.initWithPath(xpiPath);
+ }
+ catch(e) {
+ reject(exports.ERROR_FILE_ACCESS);
+ return promise;
+ }
+
+ // Listen for installation end
+ let listener = {
+ onInstallEnded: function(aInstall, aAddon) {
+ aInstall.removeListener(listener);
+ // Bug 749745: on FF14+, onInstallEnded is called just before `startup()`
+ // is called, but we expect to resolve the promise only after it.
+ // As startup is called synchronously just after onInstallEnded,
+ // a simple setTimeout(0) is enough
+ setTimeout(resolve, 0, aAddon.id);
+ },
+ onInstallFailed: function (aInstall) {
+ aInstall.removeListener(listener);
+ reject(aInstall.error);
+ },
+ onDownloadFailed: function(aInstall) {
+ this.onInstallFailed(aInstall);
+ }
+ };
+
+ // Order AddonManager to install the addon
+ AddonManager.getInstallForFile(file, function(install) {
+ if (install.error == 0) {
+ install.addListener(listener);
+ install.install();
+ } else {
+ reject(install.error);
+ }
+ });
+
+ return promise;
+};
+
+exports.uninstall = function uninstall(addonId) {
+ let { promise, resolve, reject } = defer();
+
+ // Listen for uninstallation end
+ let listener = {
+ onUninstalled: function onUninstalled(aAddon) {
+ if (aAddon.id != addonId)
+ return;
+ AddonManager.removeAddonListener(listener);
+ resolve();
+ }
+ };
+ AddonManager.addAddonListener(listener);
+
+ // Order Addonmanager to uninstall the addon
+ getAddon(addonId).then(addon => addon.uninstall(), reject);
+
+ return promise;
+};
+
+exports.disable = function disable(addonId) {
+ return getAddon(addonId).then(addon => {
+ addon.userDisabled = true;
+ return addonId;
+ });
+};
+
+exports.enable = function enabled(addonId) {
+ return getAddon(addonId).then(addon => {
+ addon.userDisabled = false;
+ return addonId;
+ });
+};
+
+exports.isActive = function isActive(addonId) {
+ return getAddon(addonId).then(addon => addon.isActive && !addon.appDisabled);
+};
+
+const getAddon = function getAddon (id) {
+ let { promise, resolve, reject } = defer();
+ AddonManager.getAddonByID(id, addon => addon ? resolve(addon) : reject());
+ return promise;
+}
+exports.getAddon = getAddon;
diff --git a/toolkit/jetpack/sdk/addon/manager.js b/toolkit/jetpack/sdk/addon/manager.js
new file mode 100644
index 000000000..7ac0a7d6e
--- /dev/null
+++ b/toolkit/jetpack/sdk/addon/manager.js
@@ -0,0 +1,18 @@
+/* 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 { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
+const { defer } = require("../core/promise");
+
+function getAddonByID(id) {
+ let { promise, resolve } = defer();
+ AddonManager.getAddonByID(id, resolve);
+ return promise;
+}
+exports.getAddonByID = getAddonByID;
diff --git a/toolkit/jetpack/sdk/addon/runner.js b/toolkit/jetpack/sdk/addon/runner.js
new file mode 100644
index 000000000..3977a04e4
--- /dev/null
+++ b/toolkit/jetpack/sdk/addon/runner.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/. */
+
+module.metadata = {
+ "stability": "experimental"
+};
+
+const { Cc, Ci, Cu } = require('chrome');
+const { rootURI, metadata, isNative } = require('@loader/options');
+const { id, loadReason } = require('../self');
+const { descriptor, Sandbox, evaluate, main, resolveURI } = require('toolkit/loader');
+const { once } = require('../system/events');
+const { exit, env, staticArgs } = require('../system');
+const { when: unload } = require('../system/unload');
+const globals = require('../system/globals');
+const xulApp = require('../system/xul-app');
+const { get } = require('../preferences/service');
+const appShellService = Cc['@mozilla.org/appshell/appShellService;1'].
+ getService(Ci.nsIAppShellService);
+const { preferences } = metadata;
+
+const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {}).exports;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyGetter(this, "BrowserToolboxProcess", function () {
+ return Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {}).
+ BrowserToolboxProcess;
+});
+
+// Initializes default preferences
+function setDefaultPrefs(prefsURI) {
+ const prefs = Cc['@mozilla.org/preferences-service;1'].
+ getService(Ci.nsIPrefService).
+ QueryInterface(Ci.nsIPrefBranch2);
+ const branch = prefs.getDefaultBranch('');
+ const sandbox = Sandbox({
+ name: prefsURI,
+ prototype: {
+ pref: function(key, val) {
+ switch (typeof val) {
+ case 'boolean':
+ branch.setBoolPref(key, val);
+ break;
+ case 'number':
+ if (val % 1 == 0) // number must be a integer, otherwise ignore it
+ branch.setIntPref(key, val);
+ break;
+ case 'string':
+ branch.setCharPref(key, val);
+ break;
+ }
+ }
+ }
+ });
+ // load preferences.
+ evaluate(sandbox, prefsURI);
+}
+
+function definePseudo(loader, id, exports) {
+ let uri = resolveURI(id, loader.mapping);
+ loader.modules[uri] = { exports: exports };
+}
+
+function startup(reason, options) {
+ return Startup.onceInitialized.then(() => {
+ // Inject globals ASAP in order to have console API working ASAP
+ Object.defineProperties(options.loader.globals, descriptor(globals));
+
+ // NOTE: Module is intentionally required only now because it relies
+ // on existence of hidden window, which does not exists until startup.
+ let { ready } = require('../addon/window');
+ // Load localization manifest and .properties files.
+ // Run the addon even in case of error (best effort approach)
+ require('../l10n/loader').
+ load(rootURI).
+ then(null, function failure(error) {
+ if (!isNative)
+ console.info("Error while loading localization: " + error.message);
+ }).
+ then(function onLocalizationReady(data) {
+ // Exports data to a pseudo module so that api-utils/l10n/core
+ // can get access to it
+ definePseudo(options.loader, '@l10n/data', data ? data : null);
+ return ready;
+ }).then(function() {
+ run(options);
+ }).then(null, console.exception);
+ return void 0; // otherwise we raise a warning, see bug 910304
+ });
+}
+
+function run(options) {
+ try {
+ // Try initializing HTML localization before running main module. Just print
+ // an exception in case of error, instead of preventing addon to be run.
+ try {
+ // Do not enable HTML localization while running test as it is hard to
+ // disable. Because unit tests are evaluated in a another Loader who
+ // doesn't have access to this current loader.
+ if (options.main !== 'sdk/test/runner') {
+ require('../l10n/html').enable();
+ }
+ }
+ catch(error) {
+ console.exception(error);
+ }
+
+ // native-options does stuff directly with preferences key from package.json
+ if (preferences && preferences.length > 0) {
+ try {
+ require('../preferences/native-options').
+ enable({ preferences: preferences, id: id }).
+ catch(console.exception);
+ }
+ catch (error) {
+ console.exception(error);
+ }
+ }
+ else {
+ // keeping support for addons packaged with older SDK versions,
+ // when cfx didn't include the 'preferences' key in @loader/options
+
+ // Initialize inline options localization, without preventing addon to be
+ // run in case of error
+ try {
+ require('../l10n/prefs').enable();
+ }
+ catch(error) {
+ console.exception(error);
+ }
+
+ // TODO: When bug 564675 is implemented this will no longer be needed
+ // Always set the default prefs, because they disappear on restart
+ if (options.prefsURI) {
+ // Only set if `prefsURI` specified
+ try {
+ setDefaultPrefs(options.prefsURI);
+ }
+ catch (err) {
+ // cfx bootstrap always passes prefsURI, even in addons without prefs
+ }
+ }
+ }
+
+ // this is where the addon's main.js finally run.
+ let program = main(options.loader, options.main);
+
+ if (typeof(program.onUnload) === 'function')
+ unload(program.onUnload);
+
+ if (typeof(program.main) === 'function') {
+ program.main({
+ loadReason: loadReason,
+ staticArgs: staticArgs
+ }, {
+ print: function print(_) { dump(_ + '\n') },
+ quit: exit
+ });
+ }
+
+ if (get("extensions." + id + ".sdk.debug.show", false)) {
+ BrowserToolboxProcess.init({ addonID: id });
+ }
+ } catch (error) {
+ console.exception(error);
+ throw error;
+ }
+}
+exports.startup = startup;
+
+// If add-on is lunched via `cfx run` we need to use `system.exit` to let
+// cfx know we're done (`cfx test` will take care of exit so we don't do
+// anything here).
+if (env.CFX_COMMAND === 'run') {
+ unload(function(reason) {
+ if (reason === 'shutdown')
+ exit(0);
+ });
+}
diff --git a/toolkit/jetpack/sdk/addon/window.js b/toolkit/jetpack/sdk/addon/window.js
new file mode 100644
index 000000000..93ed1d8dc
--- /dev/null
+++ b/toolkit/jetpack/sdk/addon/window.js
@@ -0,0 +1,66 @@
+/* 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, Cc } = require("chrome");
+const { make: makeWindow, getHiddenWindow } = require("../window/utils");
+const { create: makeFrame, getDocShell } = require("../frame/utils");
+const { defer } = require("../core/promise");
+const { when: unload } = require("../system/unload");
+const cfxArgs = require("../test/options");
+
+var addonPrincipal = Cc["@mozilla.org/systemprincipal;1"].
+ createInstance(Ci.nsIPrincipal);
+
+var hiddenWindow = getHiddenWindow();
+
+if (cfxArgs.parseable) {
+ console.info("hiddenWindow document.documentURI:" +
+ hiddenWindow.document.documentURI);
+ console.info("hiddenWindow document.readyState:" +
+ hiddenWindow.document.readyState);
+}
+
+// Once Bug 565388 is fixed and shipped we'll be able to make invisible,
+// permanent docShells. Meanwhile we create hidden top level window and
+// use it's docShell.
+var frame = makeFrame(hiddenWindow.document, {
+ nodeName: "iframe",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ allowJavascript: true,
+ allowPlugins: true
+})
+var docShell = getDocShell(frame);
+var eventTarget = docShell.chromeEventHandler;
+
+// We need to grant docShell system principals in order to load XUL document
+// from data URI into it.
+docShell.createAboutBlankContentViewer(addonPrincipal);
+
+// Get a reference to the DOM window of the given docShell and load
+// such document into that would allow us to create XUL iframes, that
+// are necessary for hidden frames etc..
+var window = docShell.contentViewer.DOMDocument.defaultView;
+window.location = "data:application/vnd.mozilla.xul+xml;charset=utf-8,<window/>";
+
+// Create a promise that is delivered once add-on window is interactive,
+// used by add-on runner to defer add-on loading until window is ready.
+var { promise, resolve } = defer();
+eventTarget.addEventListener("DOMContentLoaded", function handler(event) {
+ eventTarget.removeEventListener("DOMContentLoaded", handler, false);
+ resolve();
+}, false);
+
+exports.ready = promise;
+exports.window = window;
+
+// Still close window on unload to claim memory back early.
+unload(function() {
+ window.close()
+ frame.parentNode.removeChild(frame);
+});