Bug 1245571: Allow AMO to be able to query details about an add-on. r=rhelmer
This adds a bunch of structure supporting a promise-based API on the
AddonManager object that is exposed to webpages and adds the first example,
getAddonByID.
MozReview-Commit-ID: CCEFl4R1o81
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -400,16 +400,17 @@
@RESPATH@/components/nsHelperAppDlg.manifest
@RESPATH@/components/nsHelperAppDlg.js
@RESPATH@/components/NetworkGeolocationProvider.manifest
@RESPATH@/components/NetworkGeolocationProvider.js
@RESPATH@/components/extensions.manifest
@RESPATH@/components/addonManager.js
@RESPATH@/components/amContentHandler.js
@RESPATH@/components/amInstallTrigger.js
+@RESPATH@/components/amWebAPI.js
@RESPATH@/components/amWebInstallListener.js
@RESPATH@/components/nsBlocklistService.js
@RESPATH@/components/nsBlocklistServiceContent.js
#ifdef MOZ_UPDATER
@RESPATH@/components/nsUpdateService.manifest
@RESPATH@/components/nsUpdateService.js
@RESPATH@/components/nsUpdateServiceStub.js
#endif
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -302,16 +302,37 @@ function getLocale() {
try {
return Services.prefs.getCharPref(PREF_SELECTED_LOCALE);
}
catch (e) { }
return "en-US";
}
+function webAPIForAddon(addon) {
+ if (!addon) {
+ return null;
+ }
+
+ let result = {};
+
+ // By default just pass through any plain property, the webidl will control
+ // access.
+ for (let prop in addon) {
+ if (typeof(addon[prop]) != "function") {
+ result[prop] = addon[prop];
+ }
+ }
+
+ // A few properties are computed for a nicer API
+ result.isEnabled = !addon.userDisabled;
+
+ return result;
+}
+
/**
* A helper class to repeatedly call a listener with each object in an array
* optionally checking whether the object has a method in it.
*
* @param aObjects
* The array of objects to iterate through
* @param aMethod
* An optional method name, if not null any objects without this method
@@ -2756,16 +2777,26 @@ var AddonManagerInternal = {
if (aValue != gUpdateEnabled)
Services.prefs.setBoolPref(PREF_EM_UPDATE_ENABLED, aValue);
return aValue;
},
get hotfixID() {
return gHotfixID;
},
+
+ webAPI: {
+ getAddonByID(id) {
+ return new Promise(resolve => {
+ AddonManager.getAddonByID(id, (addon) => {
+ resolve(webAPIForAddon(addon));
+ });
+ });
+ }
+ },
};
/**
* Should not be used outside of core Mozilla code. This is a private API for
* the startup and platform integration code to use. Refer to the methods on
* AddonManagerInternal for documentation however note that these methods are
* subject to change at any time.
*/
@@ -3337,16 +3368,20 @@ this.AddonManager = {
escapeAddonURI: function(aAddon, aUri, aAppVersion) {
return AddonManagerInternal.escapeAddonURI(aAddon, aUri, aAppVersion);
},
getPreferredIconURL: function(aAddon, aSize, aWindow = undefined) {
return AddonManagerInternal.getPreferredIconURL(aAddon, aSize, aWindow);
},
+ get webAPI() {
+ return AddonManagerInternal.webAPI;
+ },
+
get shutdown() {
return gShutdownBarrier.client;
},
};
// load the timestamps module into AddonManagerInternal
Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", AddonManagerInternal);
Object.freeze(AddonManagerInternal);
--- a/toolkit/mozapps/extensions/addonManager.js
+++ b/toolkit/mozapps/extensions/addonManager.js
@@ -19,38 +19,40 @@ const USER_CANCELLED = -210;
const DOWNLOAD_ERROR = -228;
const UNSUPPORTED_TYPE = -244;
const SUCCESS = 0;
const MSG_INSTALL_ENABLED = "WebInstallerIsInstallEnabled";
const MSG_INSTALL_ADDONS = "WebInstallerInstallAddonsFromWebpage";
const MSG_INSTALL_CALLBACK = "WebInstallerInstallCallback";
+const MSG_PROMISE_REQUEST = "WebAPIPromiseRequest";
+const MSG_PROMISE_RESULT = "WebAPIPromiseResult";
+
const CHILD_SCRIPT = "resource://gre/modules/addons/Content.js";
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
var gSingleton = null;
var gParentMM = null;
function amManager() {
Cu.import("resource://gre/modules/AddonManager.jsm");
/*globals AddonManagerPrivate*/
- let globalMM = Cc["@mozilla.org/globalmessagemanager;1"]
- .getService(Ci.nsIMessageListenerManager);
+ let globalMM = Services.mm;
globalMM.loadFrameScript(CHILD_SCRIPT, true);
globalMM.addMessageListener(MSG_INSTALL_ADDONS, this);
- gParentMM = Cc["@mozilla.org/parentprocessmessagemanager;1"]
- .getService(Ci.nsIMessageListenerManager);
+ gParentMM = Services.ppmm;
gParentMM.addMessageListener(MSG_INSTALL_ENABLED, this);
+ gParentMM.addMessageListener(MSG_PROMISE_REQUEST, this);
// Needed so receiveMessage can be called directly by JS callers
this.wrappedJSObject = this;
}
amManager.prototype = {
observe: function(aSubject, aTopic, aData) {
if (aTopic == "addons-startup")
@@ -169,16 +171,40 @@ amManager.prototype = {
},
};
}
return this.installAddonsFromWebpage(payload.mimetype,
aMessage.target, payload.triggeringPrincipal, payload.uris,
payload.hashes, payload.names, payload.icons, callback);
}
+
+ case MSG_PROMISE_REQUEST: {
+ let resolve = (value) => {
+ aMessage.target.sendAsyncMessage(MSG_PROMISE_RESULT, {
+ callbackID: payload.callbackID,
+ resolve: value
+ });
+ }
+ let reject = (value) => {
+ aMessage.target.sendAsyncMessage(MSG_PROMISE_RESULT, {
+ callbackID: payload.callbackID,
+ reject: value
+ });
+ }
+
+ let API = AddonManager.webAPI;
+ if (payload.type in API) {
+ API[payload.type](...payload.args).then(resolve, reject);
+ }
+ else {
+ reject("Unknown Add-on API request.");
+ }
+ break;
+ }
}
return undefined;
},
classID: Components.ID("{4399533d-08d1-458c-a87a-235f74451cfa}"),
_xpcom_factory: {
createInstance: function(aOuter, aIid) {
if (aOuter != null)
--- a/toolkit/mozapps/extensions/amWebAPI.js
+++ b/toolkit/mozapps/extensions/amWebAPI.js
@@ -5,26 +5,104 @@
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
+const MSG_PROMISE_REQUEST = "WebAPIPromiseRequest";
+const MSG_PROMISE_RESULT = "WebAPIPromiseResult";
+
+const APIBroker = {
+ _nextID: 0,
+
+ init() {
+ this._promises = new Map();
+
+ Services.cpmm.addMessageListener(MSG_PROMISE_RESULT, this);
+ },
+
+ receiveMessage(message) {
+ let payload = message.data;
+
+ switch (message.name) {
+ case MSG_PROMISE_RESULT: {
+ if (!this._promises.has(payload.callbackID)) {
+ return;
+ }
+
+ let { resolve, reject } = this._promises.get(payload.callbackID);
+ this._promises.delete(payload.callbackID);
+
+ if ("resolve" in payload)
+ resolve(payload.resolve);
+ else
+ reject(payload.reject);
+ break;
+ }
+ }
+ },
+
+ sendRequest: function(type, ...args) {
+ return new Promise((resolve, reject) => {
+ let callbackID = this._nextID++;
+
+ this._promises.set(callbackID, { resolve, reject });
+ Services.cpmm.sendAsyncMessage(MSG_PROMISE_REQUEST, { type, callbackID, args });
+ });
+ },
+};
+
+APIBroker.init();
+
+function Addon(properties) {
+ // We trust the webidl binding to broker access to our properties.
+ for (let key of Object.keys(properties)) {
+ this[key] = properties[key];
+ }
+}
+
+/**
+ * API methods should return promises from the page, this is a simple wrapper
+ * to make sure of that. It also automatically wraps objects when necessary.
+ */
+function WebAPITask(generator) {
+ let task = Task.async(generator);
+
+ return function(...args) {
+ let win = this.window;
+
+ let wrapForContent = (obj) => {
+ if (obj instanceof Addon) {
+ return win.Addon._create(win, obj);
+ }
+
+ return obj;
+ }
+
+ return new win.Promise((resolve, reject) => {
+ task(...args).then(wrapForContent)
+ .then(resolve, reject);
+ });
+ }
+}
+
function WebAPI() {
}
WebAPI.prototype = {
init(window) {
this.window = window;
},
- getAddonByID(id) {
- return this.window.Promise.reject("Not yet implemented");
- },
+ getAddonByID: WebAPITask(function*(id) {
+ let addonInfo = yield APIBroker.sendRequest("getAddonByID", id);
+ return addonInfo ? new Addon(addonInfo) : null;
+ }),
classID: Components.ID("{8866d8e3-4ea5-48b7-a891-13ba0ac15235}"),
contractID: "@mozilla.org/addon-web-api/manager;1",
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIDOMGlobalPropertyInitializer])
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WebAPI]);
--- a/toolkit/mozapps/extensions/test/browser/browser.ini
+++ b/toolkit/mozapps/extensions/test/browser/browser.ini
@@ -57,11 +57,12 @@ support-files =
[browser_hotfix.js]
# Verifies the old style of signing hotfixes
skip-if = require_signing
[browser_installssl.js]
[browser_newaddon.js]
[browser_updatessl.js]
[browser_task_next_test.js]
[browser_discovery_install.js]
+[browser_webapi.js]
[browser_webapi_access.js]
[include:browser-common.ini]
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
+
+Services.prefs.setBoolPref("extensions.webapi.testing", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("extensions.webapi.testing");
+});
+
+function testWithAPI(task) {
+ return function*() {
+ yield BrowserTestUtils.withNewTab(TESTPAGE, task);
+ }
+}
+
+let gProvider = new MockProvider();
+
+gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "Test add-on 1",
+ version: "2.1",
+ description: "Short description",
+ type: "extension",
+ userDisabled: false,
+ isActive: true,
+}, {
+ id: "addon2@tests.mozilla.org",
+ name: "Test add-on 2",
+ version: "5.3.7ab",
+ description: null,
+ type: "theme",
+ userDisabled: false,
+ isActive: false,
+}, {
+ id: "addon3@tests.mozilla.org",
+ name: "Test add-on 3",
+ version: "1",
+ description: "Longer description",
+ type: "extension",
+ userDisabled: true,
+ isActive: false,
+}]);
+
+function API_getAddonByID(browser, id) {
+ return ContentTask.spawn(browser, id, function*(id) {
+ let addon = yield content.navigator.mozAddonManager.getAddonByID(id);
+
+ // We can't send native objects back so clone its properties.
+ let result = {};
+ for (let prop in addon) {
+ result[prop] = addon[prop];
+ }
+
+ return result;
+ });
+}
+
+add_task(testWithAPI(function*(browser) {
+ function compareObjects(web, real) {
+ for (let prop of Object.keys(web)) {
+ let webVal = web[prop];
+ let realVal = real[prop];
+
+ switch (prop) {
+ case "isEnabled":
+ realVal = !real.userDisabled;
+ break;
+ }
+
+ // null and undefined don't compare well so stringify them first
+ if (realVal === null || realVal === undefined) {
+ realVal = `${realVal}`;
+ webVal = `${webVal}`;
+ }
+
+ is(webVal, realVal, `Property ${prop} should have the right value in add-on ${real.id}`);
+ }
+ }
+
+ let [a1, a2, a3] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org"]);
+ let w1 = yield API_getAddonByID(browser, "addon1@tests.mozilla.org");
+ let w2 = yield API_getAddonByID(browser, "addon2@tests.mozilla.org");
+ let w3 = yield API_getAddonByID(browser, "addon3@tests.mozilla.org");
+
+ compareObjects(w1, a1);
+ compareObjects(w2, a2);
+ compareObjects(w3, a3);
+}));