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

--HG--
extra : rebase_source : 6206982c687a8e1733ef323488fc2710a4967688
This commit is contained in:
Dave Townsend 2016-03-10 09:50:07 -08:00
Родитель 4acb23615f
Коммит 9885c5b8b1
6 изменённых файлов: 240 добавлений и 7 удалений

Просмотреть файл

@ -404,6 +404,7 @@
@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

Просмотреть файл

@ -307,6 +307,27 @@ function getLocale() {
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.
@ -2761,6 +2782,16 @@ var AddonManagerInternal = {
get hotfixID() {
return gHotfixID;
},
webAPI: {
getAddonByID(id) {
return new Promise(resolve => {
AddonManager.getAddonByID(id, (addon) => {
resolve(webAPIForAddon(addon));
});
});
}
},
};
/**
@ -3342,6 +3373,10 @@ this.AddonManager = {
return AddonManagerInternal.getPreferredIconURL(aAddon, aSize, aWindow);
},
get webAPI() {
return AddonManagerInternal.webAPI;
},
get shutdown() {
return gShutdownBarrier.client;
},

Просмотреть файл

@ -24,6 +24,9 @@ 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");
@ -38,14 +41,13 @@ 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;
@ -174,6 +176,30 @@ amManager.prototype = {
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;
},

Просмотреть файл

@ -10,6 +10,83 @@ 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() {
}
@ -18,9 +95,10 @@ WebAPI.prototype = {
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",

Просмотреть файл

@ -64,6 +64,7 @@ skip-if = require_signing
[browser_task_next_test.js]
[browser_discovery_install.js]
[browser_update.js]
[browser_webapi.js]
[browser_webapi_access.js]
[include:browser-common.ini]

Просмотреть файл

@ -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);
}));