Bug 1306037: Support options_ui in embedded WebExtensions. r?aswan f?rpl
MozReview-Commit-ID: KZVPz52qrTS
--- a/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js
+++ b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js
@@ -1,16 +1,24 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
+function add_tasks(task) {
+ add_task(task.bind(null, {embedded: false}));
+
+ add_task(task.bind(null, {embedded: true}));
+}
+
function* loadExtension(options) {
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "temporary",
+ embedded: options.embedded,
+
manifest: Object.assign({
"permissions": ["tabs"],
}, options.manifest),
files: {
"options.html": `<!DOCTYPE html>
<html>
<head>
@@ -32,20 +40,22 @@ function* loadExtension(options) {
background: options.background,
});
yield extension.startup();
return extension;
}
-add_task(function* test_inline_options() {
+add_tasks(function* test_inline_options(extraOptions) {
+ info(`Test options opened inline (${JSON.stringify(extraOptions)})`);
+
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
- let extension = yield loadExtension({
+ let extension = yield loadExtension(Object.assign({}, extraOptions, {
manifest: {
applications: {gecko: {id: "inline_options@tests.mozilla.org"}},
"options_ui": {
"page": "options.html",
},
},
background: function() {
@@ -118,28 +128,30 @@ add_task(function* test_inline_options()
return browser.tabs.remove(tab.id);
}).then(() => {
browser.test.notifyPass("options-ui");
}).catch(error => {
browser.test.log(`Error: ${error} :: ${error.stack}`);
browser.test.notifyFail("options-ui");
});
},
- });
+ }));
yield extension.awaitFinish("options-ui");
yield extension.unload();
yield BrowserTestUtils.removeTab(tab);
});
-add_task(function* test_tab_options() {
+add_tasks(function* test_tab_options(extraOptions) {
+ info(`Test options opened in a tab (${JSON.stringify(extraOptions)})`);
+
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
- let extension = yield loadExtension({
+ let extension = yield loadExtension(Object.assign({}, extraOptions, {
manifest: {
applications: {gecko: {id: "tab_options@tests.mozilla.org"}},
"options_ui": {
"page": "options.html",
"open_in_tab": true,
},
},
@@ -216,26 +228,28 @@ add_task(function* test_tab_options() {
return browser.tabs.remove(tab.id);
}).then(() => {
browser.test.notifyPass("options-ui-tab");
}).catch(error => {
browser.test.log(`Error: ${error} :: ${error.stack}`);
browser.test.notifyFail("options-ui-tab");
});
},
- });
+ }));
yield extension.awaitFinish("options-ui-tab");
yield extension.unload();
yield BrowserTestUtils.removeTab(tab);
});
-add_task(function* test_options_no_manifest() {
- let extension = yield loadExtension({
+add_tasks(function* test_options_no_manifest(extraOptions) {
+ info(`Test with no manifest key (${JSON.stringify(extraOptions)})`);
+
+ let extension = yield loadExtension(Object.assign({}, extraOptions, {
manifest: {
applications: {gecko: {id: "no_options@tests.mozilla.org"}},
},
background: function() {
browser.test.log("Try to open options page when not specified in the manifest.");
browser.runtime.openOptionsPage().then(
@@ -251,13 +265,13 @@ add_task(function* test_options_no_manif
}
).then(() => {
browser.test.notifyPass("options-no-manifest");
}).catch(error => {
browser.test.log(`Error: ${error} :: ${error.stack}`);
browser.test.notifyFail("options-no-manifest");
});
},
- });
+ }));
yield extension.awaitFinish("options-no-manifest");
yield extension.unload();
});
--- a/testing/specialpowers/content/SpecialPowersObserverAPI.js
+++ b/testing/specialpowers/content/SpecialPowersObserverAPI.js
@@ -606,22 +606,30 @@ SpecialPowersObserverAPI.prototype = {
}
};
Management.on("startup", startupListener);
// Make sure the extension passes the packaging checks when
// they're run on a bare archive rather than a running instance,
// as the add-on manager runs them.
let extensionData = new ExtensionData(extension.rootURI);
- extensionData.readManifest().then(() => {
- return extensionData.initAllLocales();
- }).then(() => {
- if (extensionData.errors.length) {
- return Promise.reject("Extension contains packaging errors");
+ extensionData.readManifest().then(
+ () => {
+ return extensionData.initAllLocales().then(() => {
+ if (extensionData.errors.length) {
+ return Promise.reject("Extension contains packaging errors");
+ }
+ });
+ },
+ () => {
+ // readManifest() will throw if we're loading an embedded
+ // extension, so don't worry about locale errors in that
+ // case.
}
+ ).then(() => {
return extension.startup();
}).then(() => {
this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionStarted", args: []});
}).catch(e => {
dump(`Extension startup failed: ${e}\n${e.stack}`);
Management.off("startup", startupListener);
this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionFailed", args: []});
});
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -1356,16 +1356,62 @@ this.Extension = class extends Extension
let bgScript = uuidGen.generateUUID().number + ".js";
provide(manifest, ["background", "scripts"], [bgScript], true);
files[bgScript] = data.background;
}
provide(files, ["manifest.json"], manifest);
+ if (data.embedded) {
+ // Package this as a webextension embedded inside a legacy
+ // extension.
+
+ let xpiFiles = {
+ "install.rdf": `<?xml version="1.0" encoding="UTF-8"?>
+ <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest"
+ em:id="${manifest.applications.gecko.id}"
+ em:name="${manifest.name}"
+ em:type="2"
+ em:version="${manifest.version}"
+ em:description=""
+ em:hasEmbeddedWebExtension="true"
+ em:bootstrap="true">
+
+ <!-- Firefox -->
+ <em:targetApplication>
+ <Description
+ em:id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"
+ em:minVersion="51.0a1"
+ em:maxVersion="*"/>
+ </em:targetApplication>
+ </Description>
+ </RDF>
+ `,
+
+ "bootstrap.js": `
+ function install() {}
+ function uninstall() {}
+ function shutdown() {}
+
+ function startup(data) {
+ data.webExtension.startup();
+ }
+ `,
+ };
+
+ for (let [path, data] of Object.entries(files)) {
+ xpiFiles[`webextension/${path}`] = data;
+ }
+
+ files = xpiFiles;
+ }
+
return this.generateZipFile(files);
}
static generateZipFile(files, baseName = "generated-extension.xpi") {
let ZipWriter = Components.Constructor("@mozilla.org/zipwriter;1", "nsIZipWriter");
let zipW = new ZipWriter();
let file = FileUtils.getFile("TmpD", [baseName]);
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -1014,17 +1014,17 @@ var loadManifestFromWebManifest = Task.a
* @param aUri
* The URI that the manifest is being read from
* @param aStream
* An open stream to read the RDF from
* @return an AddonInternal object
* @throws if the install manifest in the RDF stream is corrupt or could not
* be read
*/
-function loadManifestFromRDF(aUri, aStream) {
+let loadManifestFromRDF = Task.async(function*(aUri, aStream) {
function getPropertyArray(aDs, aSource, aProperty) {
let values = [];
let targets = aDs.GetTargets(aSource, EM_R(aProperty), true);
while (targets.hasMoreElements())
values.push(getRDFValue(targets.getNext()));
return values;
}
@@ -1151,23 +1151,33 @@ function loadManifestFromRDF(aUri, aStre
addon.strictCompatibility = !(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) ||
getRDFProperty(ds, root, "strictCompatibility") == "true";
// Only read these properties for extensions.
if (addon.type == "extension") {
addon.bootstrap = getRDFProperty(ds, root, "bootstrap") == "true";
addon.multiprocessCompatible = getRDFProperty(ds, root, "multiprocessCompatible") == "true";
addon.hasEmbeddedWebExtension = getRDFProperty(ds, root, "hasEmbeddedWebExtension") == "true";
+
if (addon.optionsType &&
addon.optionsType != AddonManager.OPTIONS_TYPE_DIALOG &&
addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE &&
addon.optionsType != AddonManager.OPTIONS_TYPE_TAB &&
addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_INFO) {
throw new Error("Install manifest specifies unknown type: " + addon.optionsType);
}
+
+ if (addon.hasEmbeddedWebExtension) {
+ let uri = NetUtil.newURI("webextension/manifest.json", null, aUri);
+ let embeddedAddon = yield loadManifestFromWebManifest(uri);
+ if (embeddedAddon.optionsURL) {
+ addon.optionsURL = embeddedAddon.optionsURL;
+ addon.optionsType = embeddedAddon.optionsType;
+ }
+ }
}
else {
// Some add-on types are always restartless.
if (RESTARTLESS_TYPES.has(addon.type)) {
addon.bootstrap = true;
}
// Only extensions are allowed to provide an optionsURL, optionsType or aboutURL. For
@@ -1275,17 +1285,17 @@ function loadManifestFromRDF(aUri, aStre
addon.updateURL = null;
addon.updateKey = null;
}
// icons will be filled by the calling function
addon.icons = {};
return addon;
-}
+});
function defineSyncGUID(aAddon) {
// Define .syncGUID as a lazy property which is also settable
Object.defineProperty(aAddon, "syncGUID", {
get: () => {
// Generate random GUID used for Sync.
let guid = Cc["@mozilla.org/uuid-generator;1"]
.getService(Ci.nsIUUIDGenerator)
@@ -1339,25 +1349,25 @@ var loadManifestFromDir = Task.async(fun
let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
let entry;
while ((entry = entries.nextFile))
size += getFileSize(entry);
entries.close();
return size;
}
- function loadFromRDF(aUri) {
+ function* loadFromRDF(aUri) {
let fis = Cc["@mozilla.org/network/file-input-stream;1"].
createInstance(Ci.nsIFileInputStream);
fis.init(aUri.file, -1, -1, false);
let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
createInstance(Ci.nsIBufferedInputStream);
bis.init(fis, 4096);
try {
- var addon = loadManifestFromRDF(aUri, bis);
+ var addon = yield loadManifestFromRDF(aUri, bis);
} finally {
bis.close();
fis.close();
}
let iconFile = aDir.clone();
iconFile.append("icon.png");
@@ -1395,17 +1405,17 @@ var loadManifestFromDir = Task.async(fun
if (!addon.id) {
if (aInstallLocation == TemporaryInstallLocation) {
addon.id = generateTemporaryInstallID(aDir);
} else {
addon.id = aDir.leafName;
}
}
} else {
- addon = loadFromRDF(uri);
+ addon = yield loadFromRDF(uri);
}
addon._sourceBundle = aDir.clone();
addon._installLocation = aInstallLocation;
addon.size = getFileSize(aDir);
addon.signedState = yield verifyDirSignedState(aDir, addon)
.then(({signedState}) => signedState);
addon.appDisabled = !isUsableAddon(addon);
@@ -1419,23 +1429,23 @@ var loadManifestFromDir = Task.async(fun
* Loads an AddonInternal object from an nsIZipReader for an add-on.
*
* @param aZipReader
* An open nsIZipReader for the add-on's files
* @return an AddonInternal object
* @throws if the XPI file does not contain a valid install manifest
*/
var loadManifestFromZipReader = Task.async(function*(aZipReader, aInstallLocation) {
- function loadFromRDF(aUri) {
+ function* loadFromRDF(aUri) {
let zis = aZipReader.getInputStream(entry);
let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
createInstance(Ci.nsIBufferedInputStream);
bis.init(zis, 4096);
try {
- var addon = loadManifestFromRDF(aUri, bis);
+ var addon = yield loadManifestFromRDF(aUri, bis);
} finally {
bis.close();
zis.close();
}
if (aZipReader.hasEntry("icon.png")) {
addon.icons[32] = "icon.png";
addon.icons[48] = "icon.png";
@@ -1465,17 +1475,17 @@ var loadManifestFromZipReader = Task.asy
}
let uri = buildJarURI(aZipReader.file, entry);
let isWebExtension = (entry == FILE_WEB_MANIFEST);
let addon = isWebExtension ?
yield loadManifestFromWebManifest(uri) :
- loadFromRDF(uri);
+ yield loadFromRDF(uri);
addon._sourceBundle = aZipReader.file;
addon._installLocation = aInstallLocation;
addon.size = 0;
let entries = aZipReader.findEntries(null);
while (entries.hasMore())
addon.size += aZipReader.getEntry(entries.getNext()).realSize;
@@ -7319,17 +7329,17 @@ AddonWrapper.prototype = {
get optionsURL() {
if (!this.isActive) {
return null;
}
let addon = addonFor(this);
if (addon.optionsURL) {
- if (this.isWebExtension) {
+ if (this.isWebExtension || this.hasEmbeddedWebExtension) {
// The internal object's optionsURL property comes from the addons
// DB and should be a relative URL. However, extensions with
// options pages installed before bug 1293721 was fixed got absolute
// URLs in the addons db. This code handles both cases.
let base = ExtensionManagement.getURLForExtension(addon.id);
if (!base) {
return null;
}