diff --git a/addon-sdk/moz.build b/addon-sdk/moz.build index ae6f68bc3552..2ae1b36ba71d 100644 --- a/addon-sdk/moz.build +++ b/addon-sdk/moz.build @@ -32,6 +32,7 @@ addons = [ 'e10s-remote', 'e10s-tabs', 'e10s', + 'embedded-webextension', 'l10n-properties', 'l10n', 'layout-change', @@ -183,6 +184,10 @@ EXTRA_JS_MODULES.commonjs += [ 'source/lib/test.js', ] +EXTRA_JS_MODULES.commonjs.sdk += [ + 'source/lib/sdk/webextension.js', +] + EXTRA_JS_MODULES.commonjs.dev += [ 'source/lib/dev/debuggee.js', 'source/lib/dev/frame-script.js', diff --git a/addon-sdk/source/app-extension/bootstrap.js b/addon-sdk/source/app-extension/bootstrap.js index 31fc658add1b..3b1e13fe7865 100644 --- a/addon-sdk/source/app-extension/bootstrap.js +++ b/addon-sdk/source/app-extension/bootstrap.js @@ -257,6 +257,9 @@ function startup(data, reasonCode) { let module = cuddlefish.Module('sdk/loader/cuddlefish', cuddlefishURI); let require = cuddlefish.Require(loader, module); + // Init the 'sdk/webextension' module from the bootstrap addon parameter. + require("sdk/webextension").initFromBootstrapAddonParam(data); + require('sdk/addon/runner').startup(reason, { loader: loader, main: main, diff --git a/addon-sdk/source/lib/sdk/addon/bootstrap.js b/addon-sdk/source/lib/sdk/addon/bootstrap.js index 094f9123c043..e4c94eb299bf 100644 --- a/addon-sdk/source/lib/sdk/addon/bootstrap.js +++ b/addon-sdk/source/lib/sdk/addon/bootstrap.js @@ -123,7 +123,7 @@ Bootstrap.prototype = { manifest: metadata, metadata: metadata, modules: { - "@test/options": {} + "@test/options": {}, }, noQuit: prefs.get(`extensions.${id}.sdk.test.no-quit`, false) }); @@ -134,6 +134,9 @@ Bootstrap.prototype = { 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 => { diff --git a/addon-sdk/source/lib/sdk/webextension.js b/addon-sdk/source/lib/sdk/webextension.js new file mode 100644 index 000000000000..d1c4385e2bad --- /dev/null +++ b/addon-sdk/source/lib/sdk/webextension.js @@ -0,0 +1,43 @@ +/* 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" +}; + +let webExtension; +let waitForWebExtensionAPI; + +module.exports = { + initFromBootstrapAddonParam(data) { + if (webExtension) { + throw new Error("'sdk/webextension' module has been already initialized"); + } + + webExtension = data.webExtension; + }, + + startup() { + if (!webExtension) { + return Promise.reject(new Error( + "'sdk/webextension' module is currently disabled. " + + "('hasEmbeddedWebExtension' option is missing or set to false)" + )); + } + + // NOTE: calling `startup` more than once raises an "Embedded Extension already started" + // error, but given that SDK addons are going to have access to the startup method through + // an SDK module that can be required in any part of the addon, it will be nicer if any + // additional startup calls return the startup promise instead of raising an exception, + // so that the SDK addon can access the API object in the other addon modules without the + // need to manually pass this promise around. + if (!waitForWebExtensionAPI) { + waitForWebExtensionAPI = webExtension.startup(); + } + + return waitForWebExtensionAPI; + } +}; diff --git a/addon-sdk/source/python-lib/cuddlefish/rdf.py b/addon-sdk/source/python-lib/cuddlefish/rdf.py index fb9bb76f6d6f..2964897c199e 100644 --- a/addon-sdk/source/python-lib/cuddlefish/rdf.py +++ b/addon-sdk/source/python-lib/cuddlefish/rdf.py @@ -133,6 +133,11 @@ def gen_manifest(template_root_dir, target_cfg, jid, # booleans in the .json file, not strings. manifest.set("em:unpack", "true" if target_cfg.get("unpack") else "false") + if target_cfg.get('hasEmbeddedWebExtension', False): + elem = dom.createElement("em:hasEmbeddedWebExtension"); + elem.appendChild(dom.createTextNode("true")) + dom.documentElement.getElementsByTagName("Description")[0].appendChild(elem) + for translator in target_cfg.get("translators", [ ]): elem = dom.createElement("em:translator"); elem.appendChild(dom.createTextNode(translator)) diff --git a/addon-sdk/source/python-lib/cuddlefish/xpi.py b/addon-sdk/source/python-lib/cuddlefish/xpi.py index fa585e37b6d9..4ac497e89abc 100644 --- a/addon-sdk/source/python-lib/cuddlefish/xpi.py +++ b/addon-sdk/source/python-lib/cuddlefish/xpi.py @@ -48,13 +48,12 @@ def build_xpi(template_root_dir, manifest, xpi_path, if os.path.isfile(os.path.join(pkgdir, 'chrome.manifest')): files_to_copy['chrome.manifest'] = os.path.join(pkgdir, 'chrome.manifest') - # chrome folder (would contain content, skin, and locale folders typically) - folder = 'chrome' - if os.path.exists(os.path.join(pkgdir, folder)): - dirs_to_create.add('chrome') - # cp -r folder - abs_dirname = os.path.join(pkgdir, folder) - for dirpath, dirnames, filenames in os.walk(abs_dirname): + def add_special_dir(folder): + if os.path.exists(os.path.join(pkgdir, folder)): + dirs_to_create.add(folder) + # cp -r folder + abs_dirname = os.path.join(pkgdir, folder) + for dirpath, dirnames, filenames in os.walk(abs_dirname): goodfiles = list(filter_filenames(filenames, IGNORED_FILES)) dirnames[:] = filter_dirnames(dirnames) for dirname in dirnames: @@ -69,6 +68,12 @@ def build_xpi(template_root_dir, manifest, xpi_path, ]) files_to_copy[str(arcpath)] = str(abspath) + + # chrome folder (would contain content, skin, and locale folders typically) + add_special_dir('chrome') + # optionally include a `webextension/` dir from the add-on dir. + add_special_dir('webextension') + for dirpath, dirnames, filenames in os.walk(template_root_dir): if template_root_dir == dirpath: filenames = list(filter_filenames(filenames, IGNORED_TOP_LVL_FILES)) diff --git a/addon-sdk/source/test/addons/embedded-webextension/main.js b/addon-sdk/source/test/addons/embedded-webextension/main.js new file mode 100644 index 000000000000..4e51c2f9bb09 --- /dev/null +++ b/addon-sdk/source/test/addons/embedded-webextension/main.js @@ -0,0 +1,159 @@ +const tabs = require("sdk/tabs"); +const webExtension = require('sdk/webextension'); + +exports.testEmbeddedWebExtensionModuleInitializedException = function (assert) { + let actualErr; + + assert.throws( + () => webExtension.initFromBootstrapAddonParam({webExtension: null}), + /'sdk\/webextension' module has been already initialized/, + "Got the expected exception if the module is initialized twice" + ); +}; + +exports.testEmbeddedWebExtensionBackgroungPage = function* (assert) { + try { + const api = yield webExtension.startup(); + assert.ok(api, `webextension waitForStartup promise successfully resolved`); + + const apiSecondStartup = yield webExtension.startup(); + assert.equal(api, apiSecondStartup, "Got the same API object from the second startup call"); + + const {browser} = api; + + let messageListener; + let waitForBackgroundPageMessage = new Promise((resolve, reject) => { + let numExpectedMessage = 2; + messageListener = (msg, sender, sendReply) => { + numExpectedMessage -= 1; + if (numExpectedMessage == 1) { + assert.equal(msg, "bg->sdk message", + "Got the expected message from the background page"); + sendReply("sdk reply"); + } else if (numExpectedMessage == 0) { + assert.equal(msg, "sdk reply", + "The background page received the expected reply message"); + resolve(); + } else { + console.error("Unexpected message received", {msg,sender, numExpectedMessage}); + assert.ok(false, `unexpected message received`); + reject(); + } + }; + browser.runtime.onMessage.addListener(messageListener); + }); + + let portListener; + let waitForBackgroundPagePort = new Promise((resolve, reject) => { + portListener = (port) => { + let numExpectedMessages = 2; + port.onMessage.addListener((msg) => { + numExpectedMessages -= 1; + + if (numExpectedMessages == 1) { + // Check that the legacy context has been able to receive the first port message + // and reply with a port message to the background page. + assert.equal(msg, "bg->sdk port message", + "Got the expected port message from the background page"); + port.postMessage("sdk->bg port message"); + } else if (numExpectedMessages == 0) { + // Check that the background page has received the above port message. + assert.equal(msg, "bg received sdk->bg port message", + "The background page received the expected port message"); + } + }); + + port.onDisconnect.addListener(() => { + assert.equal(numExpectedMessages, 0, "Got the expected number of port messages"); + resolve(); + }); + }; + browser.runtime.onConnect.addListener(portListener); + }); + + yield Promise.all([ + waitForBackgroundPageMessage, + waitForBackgroundPagePort, + ]).then(() => { + browser.runtime.onMessage.removeListener(messageListener); + browser.runtime.onConnect.removeListener(portListener); + }); + + } catch (err) { + assert.fail(`Unexpected webextension startup exception: ${err} - ${err.stack}`); + } +}; + +exports.testEmbeddedWebExtensionContentScript = function* (assert, done) { + try { + const {browser} = yield webExtension.startup(); + assert.ok(browser, `webextension startup promise resolved successfully to the API object`); + + let messageListener; + let waitForContentScriptMessage = new Promise((resolve, reject) => { + let numExpectedMessage = 2; + messageListener = (msg, sender, sendReply) => { + numExpectedMessage -= 1; + if (numExpectedMessage == 1) { + assert.equal(msg, "content script->sdk message", + "Got the expected message from the content script"); + sendReply("sdk reply"); + } else if (numExpectedMessage == 0) { + assert.equal(msg, "sdk reply", + "The content script received the expected reply message"); + resolve(); + } else { + console.error("Unexpected message received", {msg,sender, numExpectedMessage}); + assert.ok(false, `unexpected message received`); + reject(); + } + }; + browser.runtime.onMessage.addListener(messageListener); + }); + + let portListener; + let waitForContentScriptPort = new Promise((resolve, reject) => { + portListener = (port) => { + let numExpectedMessages = 2; + port.onMessage.addListener((msg) => { + numExpectedMessages -= 1; + + if (numExpectedMessages == 1) { + assert.equal(msg, "content script->sdk port message", + "Got the expected message from the content script port"); + port.postMessage("sdk->content script port message"); + } else if (numExpectedMessages == 0) { + assert.equal(msg, "content script received sdk->content script port message", + "The content script received the expected port message"); + } + }); + port.onDisconnect.addListener(() => { + assert.equal(numExpectedMessages, 0, "Got the epected number of port messages"); + resolve(); + }); + }; + browser.runtime.onConnect.addListener(portListener); + }); + + let url = "data:text/html;charset=utf-8,