--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -6270,55 +6270,61 @@ var MailIntegration = {
Cc["@mozilla.org/uriloader/external-protocol-service;1"]
.getService(Ci.nsIExternalProtocolService);
if (extProtocolSvc)
extProtocolSvc.loadUrl(aURL);
}
};
function BrowserOpenAddonsMgr(aView) {
- if (aView) {
- let emWindow;
- let browserWindow;
-
- var receivePong = function receivePong(aSubject, aTopic, aData) {
- let browserWin = aSubject.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIWebNavigation)
- .QueryInterface(Ci.nsIDocShellTreeItem)
- .rootTreeItem
- .QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindow);
- if (!emWindow || browserWin == window /* favor the current window */) {
- emWindow = aSubject;
- browserWindow = browserWin;
+ return new Promise(resolve => {
+ if (aView) {
+ let emWindow;
+ let browserWindow;
+
+ var receivePong = function receivePong(aSubject, aTopic, aData) {
+ let browserWin = aSubject.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ if (!emWindow || browserWin == window /* favor the current window */) {
+ emWindow = aSubject;
+ browserWindow = browserWin;
+ }
}
- }
- Services.obs.addObserver(receivePong, "EM-pong", false);
- Services.obs.notifyObservers(null, "EM-ping", "");
- Services.obs.removeObserver(receivePong, "EM-pong");
-
- if (emWindow) {
- emWindow.loadView(aView);
- browserWindow.gBrowser.selectedTab =
- browserWindow.gBrowser._getTabForContentWindow(emWindow);
- emWindow.focus();
- return;
- }
- }
-
- var newLoad = !switchToTabHavingURI("about:addons", true);
-
- if (aView) {
- // This must be a new load, else the ping/pong would have
- // found the window above.
- Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
- Services.obs.removeObserver(observer, aTopic);
- aSubject.loadView(aView);
- }, "EM-loaded", false);
- }
+ Services.obs.addObserver(receivePong, "EM-pong", false);
+ Services.obs.notifyObservers(null, "EM-ping", "");
+ Services.obs.removeObserver(receivePong, "EM-pong");
+
+ if (emWindow) {
+ emWindow.loadView(aView);
+ browserWindow.gBrowser.selectedTab =
+ browserWindow.gBrowser._getTabForContentWindow(emWindow);
+ emWindow.focus();
+ resolve(emWindow);
+ return;
+ }
+ }
+
+ switchToTabHavingURI("about:addons", true);
+
+ if (aView) {
+ // This must be a new load, else the ping/pong would have
+ // found the window above.
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(observer, aTopic);
+ aSubject.loadView(aView);
+ resolve(aSubject);
+ }, "EM-loaded", false);
+ } else {
+ resolve();
+ }
+ });
}
function AddKeywordForSearchField() {
let mm = gBrowser.selectedBrowser.messageManager;
let onMessage = (message) => {
mm.removeMessageListener("ContextMenu:SearchFieldBookmarkData:Result", onMessage);
--- a/browser/components/extensions/ext-desktop-runtime.js
+++ b/browser/components/extensions/ext-desktop-runtime.js
@@ -1,10 +1,26 @@
"use strict";
/* eslint-disable mozilla/balanced-listeners */
extensions.on("uninstall", (msg, extension) => {
if (extension.uninstallURL) {
- let browser = Services.wm.getMostRecentWindow("navigator:browser").gBrowser;
+ let browser = WindowManager.topWindow.gBrowser;
browser.addTab(extension.uninstallURL, {relatedToCurrent: true});
}
});
+global.openOptionsPage = (extension) => {
+ let window = WindowManager.topWindow;
+ if (!window) {
+ return Promise.reject({message: "No browser window available"});
+ }
+
+ if (extension.manifest.options_ui.open_in_tab) {
+ window.switchToTabHavingURI(extension.manifest.options_ui.page, true);
+ return Promise.resolve();
+ }
+
+ let viewId = `addons://detail/${encodeURIComponent(extension.id)}/preferences`;
+
+ return window.BrowserOpenAddonsMgr(viewId);
+};
+
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -22,16 +22,17 @@ support-files =
[browser_ext_pageAction_popup.js]
[browser_ext_browserAction_popup.js]
[browser_ext_popup_api_injection.js]
[browser_ext_contextMenus.js]
[browser_ext_commands_getAll.js]
[browser_ext_commands_onCommand.js]
[browser_ext_getViews.js]
[browser_ext_lastError.js]
+[browser_ext_runtime_openOptionsPage.js]
[browser_ext_runtime_setUninstallURL.js]
[browser_ext_tabs_audio.js]
[browser_ext_tabs_captureVisibleTab.js]
[browser_ext_tabs_detectLanguage.js]
[browser_ext_tabs_events.js]
[browser_ext_tabs_executeScript.js]
[browser_ext_tabs_executeScript_good.js]
[browser_ext_tabs_executeScript_bad.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js
@@ -0,0 +1,228 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function* loadExtension(options) {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: true,
+
+ manifest: Object.assign({
+ "permissions": ["tabs"],
+ }, options.manifest),
+
+ files: {
+ "options.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ <script src="options.js" type="text/javascript"></script>
+ </head>
+ </html>`,
+
+ "options.js": function() {
+ browser.runtime.sendMessage("options.html");
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ if (msg == "ping") {
+ respond("pong");
+ }
+ });
+ },
+ },
+
+ background: options.background,
+ });
+
+ yield extension.startup();
+
+ return extension;
+}
+
+add_task(function* test_inline_options() {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+
+ let extension = yield loadExtension({
+ manifest: {
+ "options_ui": {
+ "page": "options.html",
+ },
+ },
+
+ background: function() {
+ let _optionsPromise;
+ let awaitOptions = () => {
+ browser.test.assertFalse(_optionsPromise, "Should not be awaiting options already");
+
+ return new Promise(resolve => {
+ _optionsPromise = {resolve};
+ });
+ };
+
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ if (msg == "options.html") {
+ if (_optionsPromise) {
+ _optionsPromise.resolve(sender.tab);
+ _optionsPromise = null;
+ } else {
+ browser.test.fail("Saw unexpected options page load");
+ }
+ }
+ });
+
+ let firstTab, optionsTab;
+ browser.tabs.query({currentWindow: true, active: true}).then(tabs => {
+ firstTab = tabs[0].id;
+
+ browser.test.log("Open options page. Expect fresh load.");
+ return Promise.all([
+ browser.runtime.openOptionsPage(),
+ awaitOptions(),
+ ]);
+ }).then(([, tab]) => {
+ browser.test.assertEq("about:addons", tab.url, "Tab contains AddonManager");
+ browser.test.assertTrue(tab.active, "Tab is active");
+ browser.test.assertTrue(tab.id != firstTab, "Tab is a new tab");
+
+ optionsTab = tab.id;
+
+ browser.test.log("Switch tabs.");
+ return browser.tabs.update(firstTab, {active: true});
+ }).then(() => {
+ browser.test.log("Open options page again. Expect tab re-selected, no new load.");
+
+ return browser.runtime.openOptionsPage();
+ }).then(() => {
+ return browser.tabs.query({currentWindow: true, active: true});
+ }).then(([tab]) => {
+ browser.test.assertEq(optionsTab, tab.id, "Tab is the same as the previous options tab");
+ browser.test.assertEq("about:addons", tab.url, "Tab contains AddonManager");
+
+ browser.test.log("Ping options page.");
+ return new Promise(resolve => browser.tabs.sendMessage(optionsTab, "ping", resolve));
+ }).then(() => {
+ browser.test.log("Got pong.");
+
+ browser.test.log("Remove options tab.");
+ return browser.tabs.remove(optionsTab);
+ }).then(() => {
+ browser.test.log("Open options page again. Expect fresh load.");
+ return Promise.all([
+ browser.runtime.openOptionsPage(),
+ awaitOptions(),
+ ]);
+ }).then(([, tab]) => {
+ browser.test.assertEq("about:addons", tab.url, "Tab contains AddonManager");
+ browser.test.assertTrue(tab.active, "Tab is active");
+ browser.test.assertTrue(tab.id != optionsTab, "Tab is a new tab");
+
+ 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() {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+
+ let extension = yield loadExtension({
+ manifest: {
+ "options_ui": {
+ "page": "options.html",
+ "open_in_tab": true,
+ },
+ },
+
+ background: function() {
+ let _optionsPromise;
+ let awaitOptions = () => {
+ browser.test.assertFalse(_optionsPromise, "Should not be awaiting options already");
+
+ return new Promise(resolve => {
+ _optionsPromise = {resolve};
+ });
+ };
+
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ if (msg == "options.html") {
+ if (_optionsPromise) {
+ _optionsPromise.resolve(sender.tab);
+ _optionsPromise = null;
+ } else {
+ browser.test.fail("Saw unexpected options page load");
+ }
+ }
+ });
+
+ let optionsURL = browser.extension.getURL("options.html");
+
+ let firstTab, optionsTab;
+ browser.tabs.query({currentWindow: true, active: true}).then(tabs => {
+ firstTab = tabs[0].id;
+
+ browser.test.log("Open options page. Expect fresh load.");
+ return Promise.all([
+ browser.runtime.openOptionsPage(),
+ awaitOptions(),
+ ]);
+ }).then(([, tab]) => {
+ browser.test.assertEq(optionsURL, tab.url, "Tab contains options.html");
+ browser.test.assertTrue(tab.active, "Tab is active");
+ browser.test.assertTrue(tab.id != firstTab, "Tab is a new tab");
+
+ optionsTab = tab.id;
+
+ browser.test.log("Switch tabs.");
+ return browser.tabs.update(firstTab, {active: true});
+ }).then(() => {
+ browser.test.log("Open options page again. Expect tab re-selected, no new load.");
+
+ return browser.runtime.openOptionsPage();
+ }).then(() => {
+ return browser.tabs.query({currentWindow: true, active: true});
+ }).then(([tab]) => {
+ browser.test.assertEq(optionsTab, tab.id, "Tab is the same as the previous options tab");
+ browser.test.assertEq(optionsURL, tab.url, "Tab contains options.html");
+
+ // Unfortunately, we can't currently do this, since onMessage doesn't
+ // currently support responses when there are multiple listeners.
+ //
+ // browser.test.log("Ping options page.");
+ // return new Promise(resolve => browser.runtime.sendMessage("ping", resolve));
+
+ browser.test.log("Remove options tab.");
+ return browser.tabs.remove(optionsTab);
+ }).then(() => {
+ browser.test.log("Open options page again. Expect fresh load.");
+ return Promise.all([
+ browser.runtime.openOptionsPage(),
+ awaitOptions(),
+ ]);
+ }).then(([, tab]) => {
+ browser.test.assertEq(optionsURL, tab.url, "Tab contains options.html");
+ browser.test.assertTrue(tab.active, "Tab is active");
+ browser.test.assertTrue(tab.id != optionsTab, "Tab is a new tab");
+
+ 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);
+});
--- a/toolkit/components/extensions/.eslintrc
+++ b/toolkit/components/extensions/.eslintrc
@@ -12,16 +12,17 @@
"TextEncoder": false,
// Specific to WebExtensions:
"extensions": true,
"global": true,
"Extension": true,
"ExtensionManagement": true,
"ExtensionPage": true,
"GlobalManager": true,
+ "openOptionsPage": true,
"runSafe": true,
"runSafeSync": true,
"runSafeSyncWithoutClone": true,
"NetUtil": true,
"Services": true,
"TabManager": true,
"XPCOMUtils": true,
},
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -985,28 +985,91 @@ this.Extension.generateXPI = function(id
}
zipW.close();
return file;
};
/**
+ * A skeleton Extension-like object, used for testing, which installs an
+ * add-on via the add-on manager when startup() is called, and
+ * uninstalles it on shutdown().
+ */
+function MockExtension(id, file, rootURI) {
+ this.id = id;
+ this.file = file;
+ this.rootURI = rootURI;
+
+ this._extension = null;
+ this._extensionPromise = new Promise(resolve => {
+ let onstartup = (msg, extension) => {
+ if (extension.id == this.id) {
+ Management.off("startup", onstartup);
+
+ this._extension = extension;
+ resolve(extension);
+ }
+ };
+ Management.on("startup", onstartup);
+ });
+}
+
+MockExtension.prototype = {
+ testMessage(...args) {
+ return this._extension.testMessage(...args);
+ },
+
+ on(...args) {
+ this._extensionPromise.then(extension => {
+ extension.on(...args);
+ });
+ },
+
+ off(...args) {
+ this._extensionPromise.then(extension => {
+ extension.off(...args);
+ });
+ },
+
+ startup() {
+ return AddonManager.installTemporaryAddon(this.file).then(addon => {
+ this.addon = addon;
+ return this._extensionPromise;
+ });
+ },
+
+ shutdown() {
+ this.addon.uninstall(true);
+ return this.cleanupGeneratedFile();
+ },
+
+ cleanupGeneratedFile() {
+ flushJarCache(this.file);
+ return OS.File.remove(this.file.path);
+ },
+};
+
+/**
* Generates a new extension using |Extension.generateXPI|, and initializes a
* new |Extension| instance which will execute it.
*/
this.Extension.generate = function(id, data) {
let file = this.generateXPI(id, data);
flushJarCache(file);
Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {path: file.path});
let fileURI = Services.io.newFileURI(file);
let jarURI = Services.io.newURI("jar:" + fileURI.spec + "!/", null, null);
+ if (data.useAddonManager) {
+ return new MockExtension(id, file, jarURI);
+ }
+
return new Extension({
id,
resourceURI: jarURI,
cleanupFile: file,
});
};
Extension.prototype = extend(Object.create(ExtensionData.prototype), {
--- a/toolkit/components/extensions/MessageChannel.jsm
+++ b/toolkit/components/extensions/MessageChannel.jsm
@@ -434,17 +434,19 @@ this.MessageChannel = {
* The message managers on which to stop listening.
* @param {string|number} messageName
* The name of the message to stop listening for.
* @param {MessageReceiver} handler
* The handler to stop dispatching to.
*/
removeListener(targets, messageName, handler) {
for (let target of [].concat(targets)) {
- this.messageManagers.get(target).removeHandler(messageName, handler);
+ if (this.messageManagers.has(target)) {
+ this.messageManagers.get(target).removeHandler(messageName, handler);
+ }
}
},
/**
* Sends a message via the given message manager. Returns a promise which
* resolves or rejects with the return value of the message receiver.
*
* The promise also rejects if there is no matching listener, or the other
--- a/toolkit/components/extensions/ext-runtime.js
+++ b/toolkit/components/extensions/ext-runtime.js
@@ -63,16 +63,24 @@ extensions.registerSchemaAPI("runtime",
getURL: function(url) {
return extension.baseURI.resolve(url);
},
getPlatformInfo: function() {
return Promise.resolve(ExtensionUtils.PlatformInfo);
},
+ openOptionsPage: function() {
+ if (!extension.manifest.options_ui) {
+ return Promise.reject({message: "No `options_ui` declared"});
+ }
+
+ return openOptionsPage(extension).then(() => {});
+ },
+
setUninstallURL: function(url) {
if (url.length == 0) {
return Promise.resolve();
}
let uri;
try {
uri = NetUtil.newURI(url);
--- a/toolkit/components/extensions/schemas/runtime.json
+++ b/toolkit/components/extensions/schemas/runtime.json
@@ -129,17 +129,16 @@
"description": "The JavaScript 'window' object for the background page."
}
]
}
]
},
{
"name": "openOptionsPage",
- "unsupported": true,
"type": "function",
"description": "<p>Open your Extension's options page, if possible.</p><p>The precise behavior may depend on your manifest's <code>$(topic:optionsV2)[options_ui]</code> or <code>$(topic:options)[options_page]</code> key, or what the browser happens to support at the time.</p><p>If your Extension does not declare an options page, or the browser failed to create one for some other reason, the callback will set $(ref:lastError).</p>",
"async": "callback",
"parameters": [{
"type": "function",
"name": "callback",
"parameters": [],
"optional": true
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -3813,18 +3813,19 @@ this.XPIProvider = {
/**
* Temporarily installs add-on from a local XPI file or directory.
* As this is intended for development, the signature is not checked and
* the add-on does not persist on application restart.
*
* @param aFile
* An nsIFile for the unpacked add-on directory or XPI file.
*
- * @return a Promise that rejects if the add-on is not a valid restartless
- * add-on or if the same ID is already temporarily installed
+ * @return a Promise that resolves to an Addon object on success, or rejects
+ * if the add-on is not a valid restartless add-on or if the
+ * same ID is already temporarily installed
*/
installTemporaryAddon: Task.async(function*(aFile) {
let addon = yield loadManifestFromFile(aFile, TemporaryInstallLocation);
if (!addon.bootstrap) {
throw new Error("Only restartless (bootstrap) add-ons"
+ " can be temporarily installed:", addon.id);
}