new file mode 100644
--- /dev/null
+++ b/dom/webidl/AddonEvent.webidl
@@ -0,0 +1,12 @@
+[ Func="mozilla::AddonManagerWebAPI::IsAPIEnabled",
+ Constructor(DOMString type, AddonEventInit eventInitDict)]
+interface AddonEvent : Event {
+ readonly attribute DOMString id;
+ readonly attribute boolean needsRestart;
+};
+
+dictionary AddonEventInit : EventInit {
+ required DOMString id;
+ required boolean needsRestart;
+};
+
--- a/dom/webidl/AddonManager.webidl
+++ b/dom/webidl/AddonManager.webidl
@@ -46,17 +46,17 @@ interface AddonInstall : EventTarget {
dictionary addonInstallOptions {
required DOMString url;
};
[HeaderFile="mozilla/AddonManagerWebAPI.h",
Func="mozilla::AddonManagerWebAPI::IsAPIEnabled",
NavigatorProperty="mozAddonManager",
JSImplementation="@mozilla.org/addon-web-api/manager;1"]
-interface AddonManager {
+interface AddonManager : EventTarget {
/**
* Gets information about an add-on
*
* @param id
* The ID of the add-on to test for.
* @return A promise. It will resolve to an Addon if the add-on is installed.
*/
Promise<Addon> getAddonByID(DOMString id);
@@ -64,9 +64,15 @@ interface AddonManager {
/**
* Creates an AddonInstall object for a given URL.
*
* @param options
* Only one supported option: 'url', the URL of the addon to install.
* @return A promise that resolves to an instance of AddonInstall.
*/
Promise<AddonInstall> createInstall(optional addonInstallOptions options);
+
+ /* Hooks for managing event listeners */
+ [ChromeOnly]
+ void eventListenerWasAdded(DOMString type);
+ [ChromeOnly]
+ void eventListenerWasRemoved(DOMString type);
};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -754,16 +754,17 @@ else:
]
if CONFIG['MOZ_B2G_FM']:
WEBIDL_FILES += [
'FMRadio.webidl',
]
GENERATED_EVENTS_WEBIDL_FILES = [
+ 'AddonEvent.webidl',
'AnimationPlaybackEvent.webidl',
'AutocompleteErrorEvent.webidl',
'BlobEvent.webidl',
'CallEvent.webidl',
'CallGroupErrorEvent.webidl',
'CameraClosedEvent.webidl',
'CameraConfigurationEvent.webidl',
'CameraFacesDetectedEvent.webidl',
--- a/toolkit/mozapps/extensions/AddonManagerWebAPI.h
+++ b/toolkit/mozapps/extensions/AddonManagerWebAPI.h
@@ -1,19 +1,24 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/. */
+#ifndef addonmanagerwebapi_h_
+#define addonmanagerwebapi_h_
+
#include "nsPIDOMWindow.h"
namespace mozilla {
class AddonManagerWebAPI {
public:
static bool IsAPIEnabled(JSContext* cx, JSObject* obj);
private:
static bool IsValidSite(nsIURI* uri);
};
} // namespace mozilla
+
+#endif // addonmanagerwebapi_h_
--- a/toolkit/mozapps/extensions/addonManager.js
+++ b/toolkit/mozapps/extensions/addonManager.js
@@ -23,16 +23,18 @@ 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 MSG_INSTALL_EVENT = "WebAPIInstallEvent";
const MSG_INSTALL_CLEANUP = "WebAPICleanup";
+const MSG_ADDON_EVENT_REQ = "WebAPIAddonEventRequest";
+const MSG_ADDON_EVENT = "WebAPIAddonEvent";
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;
@@ -46,16 +48,17 @@ function amManager() {
let globalMM = Services.mm;
globalMM.loadFrameScript(CHILD_SCRIPT, true);
globalMM.addMessageListener(MSG_INSTALL_ADDONS, this);
gParentMM = Services.ppmm;
gParentMM.addMessageListener(MSG_INSTALL_ENABLED, this);
gParentMM.addMessageListener(MSG_PROMISE_REQUEST, this);
gParentMM.addMessageListener(MSG_INSTALL_CLEANUP, this);
+ gParentMM.addMessageListener(MSG_ADDON_EVENT_REQ, this);
Services.obs.addObserver(this, "message-manager-close", false);
Services.obs.addObserver(this, "message-manager-disconnect", false);
AddonManager.webAPI.setEventHandler(this.sendEvent);
// Needed so receiveMessage can be called directly by JS callers
this.wrappedJSObject = this;
@@ -156,16 +159,18 @@ amManager.prototype = {
return retval;
},
notify: function(aTimer) {
AddonManagerPrivate.backgroundUpdateTimerHandler();
},
+ addonListener: null,
+
/**
* messageManager callback function.
*
* Listens to requests from child processes for InstallTrigger
* activity, and sends back callbacks.
*/
receiveMessage: function(aMessage) {
let payload = aMessage.data;
@@ -216,16 +221,40 @@ amManager.prototype = {
}
break;
}
case MSG_INSTALL_CLEANUP: {
AddonManager.webAPI.clearInstalls(payload.ids);
break;
}
+
+ case MSG_ADDON_EVENT_REQ: {
+ if (payload.enabled) {
+ if (!this.addonListener) {
+ let target = aMessage.target;
+ let handler = (event, id, needsRestart) => {
+ target.sendAsyncMessage(MSG_ADDON_EVENT, {event, id, needsRestart});
+ };
+ this.addonListener = {
+ onEnabling: (addon, needsRestart) => handler("onEnabling", addon.id, needsRestart),
+ onEnabled: (addon) => handler("onEnabled", addon.id, false),
+ onDisabling: (addon, needsRestart) => handler("onDisabling", addon.id, needsRestart),
+ onDisabled: (addon) => handler("onDisabled", addon.id, false),
+ onInstalling: (addon, needsRestart) => handler("onInstalling", addon.id, needsRestart),
+ onInstalled: (addon) => handler("onInstalled", addon.id, false),
+ onUninstalling: (addon, needsRestart) => handler("onUninstalling", addon.id, needsRestart),
+ onUninstalled: (addon) => handler("onUninstalled", addon.id, false),
+ };
+ }
+ AddonManager.addAddonListener(this.addonListener);
+ } else {
+ AddonManager.removeAddonListener(this.addonListener);
+ }
+ }
}
return undefined;
},
sendEvent(target, data) {
target.sendAsyncMessage(MSG_INSTALL_EVENT, data);
},
--- a/toolkit/mozapps/extensions/amWebAPI.js
+++ b/toolkit/mozapps/extensions/amWebAPI.js
@@ -9,28 +9,32 @@ const {classes: Cc, interfaces: Ci, util
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 MSG_INSTALL_EVENT = "WebAPIInstallEvent";
const MSG_INSTALL_CLEANUP = "WebAPICleanup";
+const MSG_ADDON_EVENT_REQ = "WebAPIAddonEventRequest";
+const MSG_ADDON_EVENT = "WebAPIAddonEvent";
const APIBroker = {
_nextID: 0,
init() {
this._promises = new Map();
// _installMap maps integer ids to DOM AddonInstall instances
this._installMap = new Map();
Services.cpmm.addMessageListener(MSG_PROMISE_RESULT, this);
Services.cpmm.addMessageListener(MSG_INSTALL_EVENT, this);
+
+ this._eventListener = null;
},
receiveMessage(message) {
let payload = message.data;
switch (message.name) {
case MSG_PROMISE_RESULT: {
if (!this._promises.has(payload.callbackID)) {
@@ -52,29 +56,47 @@ const APIBroker = {
if (!install) {
let err = new Error(`Got install event for unknown install ${payload.id}`);
Cu.reportError(err);
return;
}
install._dispatch(payload);
break;
}
+
+ case MSG_ADDON_EVENT: {
+ if (this._eventListener) {
+ this._eventListener(payload);
+ }
+ }
}
},
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 });
});
},
+ setAddonListener(callback) {
+ this._eventListener = callback;
+ if (callback) {
+ Services.cpmm.addMessageListener(MSG_ADDON_EVENT, this);
+ Services.cpmm.sendAsyncMessage(MSG_ADDON_EVENT_REQ, {enabled: true});
+ } else {
+ Services.cpmm.removeMessageListener(MSG_ADDON_EVENT, this);
+ Services.cpmm.sendAsyncMessage(MSG_ADDON_EVENT_REQ, {enabled: false});
+ }
+ },
+
sendCleanup: function(ids) {
+ this.setAddonListener(null);
Services.cpmm.sendAsyncMessage(MSG_INSTALL_CLEANUP, { ids });
},
};
APIBroker.init();
function Addon(window, properties) {
this.window = window;
@@ -166,16 +188,17 @@ AddonInstall.prototype = {
function WebAPI() {
}
WebAPI.prototype = {
init(window) {
this.window = window;
this.allInstalls = [];
+ this.listenerCount = 0;
window.addEventListener("unload", event => {
APIBroker.sendCleanup(this.allInstalls);
});
},
getAddonByID: WebAPITask(function*(id) {
let addonInfo = yield APIBroker.sendRequest("getAddonByID", id);
@@ -187,14 +210,31 @@ WebAPI.prototype = {
if (!installInfo) {
return null;
}
let install = new AddonInstall(this.window, installInfo);
this.allInstalls.push(installInfo.id);
return install;
}),
+ eventListenerWasAdded(type) {
+ if (this.listenerCount == 0) {
+ APIBroker.setAddonListener(data => {
+ let event = new this.window.AddonEvent(data.event, data);
+ this.__DOM_IMPL__.dispatchEvent(event);
+ });
+ }
+ this.listenerCount++;
+ },
+
+ eventListenerWasRemoved(type) {
+ this.listenerCount--;
+ if (this.listenerCount == 0) {
+ APIBroker.setAddonListener(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
@@ -32,16 +32,17 @@ support-files =
browser_updatessl.rdf
browser_updatessl.rdf^headers^
browser_install.rdf
browser_install.rdf^headers^
browser_install.xml
browser_install1_3.xpi
browser_eula.xml
browser_purchase.xml
+ webapi_addon_listener.html
webapi_checkavailable.html
webapi_checkchromeframe.xul
webapi_checkframed.html
webapi_checknavigatedwindow.html
!/toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi
!/toolkit/mozapps/extensions/test/xpinstall/incompatible.xpi
!/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html
!/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi
@@ -61,12 +62,13 @@ skip-if = require_signing
[browser_installssl.js]
[browser_newaddon.js]
[browser_updatessl.js]
[browser_task_next_test.js]
[browser_discovery_install.js]
[browser_update.js]
[browser_webapi.js]
[browser_webapi_access.js]
+[browser_webapi_addon_listener.js]
[browser_webapi_install.js]
[browser_webapi_uninstall.js]
[include:browser-common.ini]
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js
@@ -0,0 +1,147 @@
+const TESTPAGE = `${SECURE_TESTROOT}webapi_addon_listener.html`;
+
+Services.prefs.setBoolPref("extensions.webapi.testing", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("extensions.webapi.testing");
+});
+
+
+function* getListenerEvents(browser) {
+ let result = yield ContentTask.spawn(browser, null, function*() {
+ return content.document.getElementById("result").textContent;
+ });
+
+ return result.split('\n').map(JSON.parse);
+}
+
+const RESTART_ID = "restart@tests.mozilla.org";
+const RESTART_DISABLED_ID = "restart_disabled@tests.mozilla.org";
+const RESTARTLESS_ID = "restartless@tests.mozilla.org";
+const INSTALL_ID = "install@tests.mozilla.org";
+
+let provider = new MockProvider(false);
+provider.createAddons([
+ {
+ id: RESTART_ID,
+ name: "Add-on that requires restart",
+ },
+ {
+ id: RESTART_DISABLED_ID,
+ name: "Disabled add-on that requires restart",
+ userDisabled: true,
+ },
+ {
+ id: RESTARTLESS_ID,
+ name: "Restartless add-on",
+ operationsRequiringRestart: AddonManager.OP_NEED_RESTART_NONE,
+ },
+]);
+
+// Test disable of add-on requiring restart
+add_task(function* test_disable() {
+ yield BrowserTestUtils.withNewTab(TESTPAGE, function*(browser) {
+ let addon = yield promiseAddonByID(RESTART_ID);
+ is(addon.userDisabled, false, "addon is enabled");
+
+ // disable it
+ addon.userDisabled = true;
+ is(addon.userDisabled, true, "addon was disabled successfully");
+
+ let events = yield getListenerEvents(browser);
+
+ // Just a single onDisabling since restart is needed to complete
+ let expected = [
+ {id: RESTART_ID, needsRestart: true, event: "onDisabling"},
+ ];
+ Assert.deepEqual(events, expected, "Got expected disable event");
+ });
+});
+
+// Test enable of add-on requiring restart
+add_task(function* test_enable() {
+ yield BrowserTestUtils.withNewTab(TESTPAGE, function*(browser) {
+ let addon = yield promiseAddonByID(RESTART_DISABLED_ID);
+ is(addon.userDisabled, true, "addon is disabled");
+
+ // enable it
+ addon.userDisabled = false;
+ is(addon.userDisabled, false, "addon was enabled successfully");
+
+ let events = yield getListenerEvents(browser);
+
+ // Just a single onEnabling since restart is needed to complete
+ let expected = [
+ {id: RESTART_DISABLED_ID, needsRestart: true, event: "onEnabling"},
+ ];
+ Assert.deepEqual(events, expected, "Got expected enable event");
+ });
+});
+
+// Test enable/disable events for restartless
+add_task(function* test_restartless() {
+ yield BrowserTestUtils.withNewTab(TESTPAGE, function*(browser) {
+ let addon = yield promiseAddonByID(RESTARTLESS_ID);
+ is(addon.userDisabled, false, "addon is enabled");
+
+ // disable it
+ addon.userDisabled = true;
+ is(addon.userDisabled, true, "addon was disabled successfully");
+
+ // re-enable it
+ addon.userDisabled = false;
+ is(addon.userDisabled, false, "addon was re-enabled successfuly");
+
+ let events = yield getListenerEvents(browser);
+ let expected = [
+ {id: RESTARTLESS_ID, needsRestart: false, event: "onDisabling"},
+ {id: RESTARTLESS_ID, needsRestart: false, event: "onDisabled"},
+ {id: RESTARTLESS_ID, needsRestart: false, event: "onEnabling"},
+ {id: RESTARTLESS_ID, needsRestart: false, event: "onEnabled"},
+ ];
+ Assert.deepEqual(events, expected, "Got expected disable/enable events");
+ });
+});
+
+// Test install events
+add_task(function* test_restartless() {
+ yield BrowserTestUtils.withNewTab(TESTPAGE, function*(browser) {
+ let addon = new MockAddon(INSTALL_ID, "installme", null,
+ AddonManager.OP_NEED_RESTART_NONE);
+ let install = new MockInstall(null, null, addon);
+
+ let installPromise = new Promise(resolve => {
+ install.addTestListener({
+ onInstallEnded: resolve,
+ });
+ });
+
+ provider.addInstall(install);
+ install.install();
+
+ yield installPromise;
+
+ let events = yield getListenerEvents(browser);
+ let expected = [
+ {id: INSTALL_ID, needsRestart: false, event: "onInstalling"},
+ {id: INSTALL_ID, needsRestart: false, event: "onInstalled"},
+ ];
+ Assert.deepEqual(events, expected, "Got expected install events");
+ });
+});
+
+// Test uninstall
+add_task(function* test_uninstall() {
+ yield BrowserTestUtils.withNewTab(TESTPAGE, function*(browser) {
+ let addon = yield promiseAddonByID(RESTARTLESS_ID);
+ isnot(addon, null, "Found add-on for uninstall");
+
+ addon.uninstall();
+
+ let events = yield getListenerEvents(browser);
+ let expected = [
+ {id: RESTARTLESS_ID, needsRestart: false, event: "onUninstalling"},
+ {id: RESTARTLESS_ID, needsRestart: false, event: "onUninstalled"},
+ ];
+ Assert.deepEqual(events, expected, "Got expected uninstall events");
+ });
+});
--- a/toolkit/mozapps/extensions/test/browser/head.js
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -526,16 +526,22 @@ function is_element_visible(aElement, aM
ok(!is_hidden(aElement), aMsg || (aElement + " should be visible"));
}
function is_element_hidden(aElement, aMsg) {
isnot(aElement, null, "Element should not be null, when checking visibility");
ok(is_hidden(aElement), aMsg || (aElement + " should be hidden"));
}
+function promiseAddonByID(aId) {
+ return new Promise(resolve => {
+ AddonManager.getAddonByID(aId, resolve);
+ });
+}
+
function promiseAddonsByIDs(aIDs) {
return new Promise(resolve => {
AddonManager.getAddonsByIDs(aIDs, resolve);
});
}
/**
* Install an add-on and call a callback when complete.
*
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<p id="result"></p>
+<script type="text/javascript">
+let events = [];
+let resultEl = document.getElementById("result");
+[ "onEnabling",
+ "onEnabled",
+ "onDisabling",
+ "onDisabled",
+ "onInstalling",
+ "onInstalled",
+ "onUninstalling",
+ "onUninstalled",
+].forEach(event => {
+ navigator.mozAddonManager.addEventListener(event, data => {
+ let obj = {event, id: data.id, needsRestart: data.needsRestart};
+ events.push(JSON.stringify(obj));
+ resultEl.textContent = events.join('\n');
+ });
+});
+</script>
+</body>
+</html>