diff options
author | Matt A. Tobin <email@mattatobin.com> | 2018-02-09 06:46:43 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2018-02-09 06:46:43 -0500 |
commit | ac46df8daea09899ce30dc8fd70986e258c746bf (patch) | |
tree | 2750d3125fc253fd5b0671e4bd268eff1fd97296 /toolkit/jetpack/sdk/addon | |
parent | 8cecf8d5208f3945b35f879bba3015bb1a11bec6 (diff) | |
download | UXP-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/addon')
-rw-r--r-- | toolkit/jetpack/sdk/addon/bootstrap.js | 182 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/addon/events.js | 56 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/addon/host.js | 12 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/addon/installer.js | 121 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/addon/manager.js | 18 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/addon/runner.js | 180 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/addon/window.js | 66 |
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); +}); |