--- a/browser/base/content/test/webextensions/browser_extension_sideloading.js
+++ b/browser/base/content/test/webextensions/browser_extension_sideloading.js
@@ -1,160 +1,98 @@
const {AddonManagerPrivate} = Cu.import("resource://gre/modules/AddonManager.jsm", {});
+const {AddonTestUtils} = Cu.import("resource://testing-common/AddonTestUtils.jsm", {});
+
+AddonTestUtils.initMochitest(this);
+
// MockAddon mimics the AddonInternal interface and MockProvider implements
// just enough of the AddonManager provider interface to make it look like
// we have sideloaded webextensions so the sideloading flow can be tested.
-// MockAddon -> callback
-let setCallbacks = new Map();
+async function createWebExtension(details) {
+ let options = {
+ manifest: {
+ applications: {gecko: {id: details.id}},
-class MockAddon {
- constructor(props) {
- this._userDisabled = false;
- this.pendingOperations = 0;
- this.type = "extension";
-
- for (let name in props) {
- if (name == "userDisabled") {
- this._userDisabled = props[name];
- }
- this[name] = props[name];
- }
- }
+ name: details.name,
- markAsSeen() {
- this.seen = true;
- }
-
- get userDisabled() {
- return this._userDisabled;
- }
+ permissions: details.permissions,
+ },
+ };
- set userDisabled(val) {
- this._userDisabled = val;
- AddonManagerPrivate.callAddonListeners(val ? "onDisabled" : "onEnabled", this);
- let fn = setCallbacks.get(this);
- if (fn) {
- setCallbacks.delete(this);
- fn(val);
- }
- return val;
+ if (details.iconURL) {
+ options.manifest.icons = {"64": details.iconURL};
}
- get permissions() {
- return this._userDisabled ? AddonManager.PERM_CAN_ENABLE : AddonManager.PERM_CAN_DISABLE;
- }
+ let xpi = AddonTestUtils.createTempWebExtensionFile(options);
+
+ await AddonTestUtils.manuallyInstall(xpi);
}
-class MockProvider {
- constructor(...addons) {
- this.addons = new Set(addons);
- }
-
- startup() { }
- shutdown() { }
+async function createXULExtension(details) {
+ let xpi = AddonTestUtils.createTempXPIFile({
+ "install.rdf": {
+ id: details.id,
+ name: details.name,
+ version: "0.1",
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "0",
+ maxVersion: "*",
+ }],
+ },
+ });
- getAddonByID(id, callback) {
- for (let addon of this.addons) {
- if (addon.id == id) {
- callback(addon);
- return;
- }
- }
- callback(null);
- }
-
- getAddonsByTypes(types, callback) {
- let addons = [];
- if (!types || types.includes("extension")) {
- addons = [...this.addons];
- }
- callback(addons);
- }
-}
-
-function promiseSetDisabled(addon) {
- return new Promise(resolve => {
- setCallbacks.set(addon, resolve);
- });
+ await AddonTestUtils.manuallyInstall(xpi);
}
let cleanup;
-add_task(function* () {
- // ICON_URL wouldn't ever appear as an actual webextension icon, but
- // we're just mocking out the addon here, so all we care about is that
- // that it propagates correctly to the popup.
- const ICON_URL = "chrome://mozapps/skin/extensions/category-extensions.svg";
+add_task(function* test_sideloading() {
const DEFAULT_ICON_URL = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+ yield SpecialPowers.pushPrefEnv({
+ set: [
+ ["xpinstall.signatures.required", false],
+ ["extensions.autoDisableScopes", 15],
+ ["extensions.ui.ignoreUnsigned", true],
+ ],
+ });
+
const ID1 = "addon1@tests.mozilla.org";
- let mock1 = new MockAddon({
+ yield createWebExtension({
id: ID1,
name: "Test 1",
userDisabled: true,
- seen: false,
- userPermissions: {
- permissions: ["history"],
- origins: ["https://*/*"],
- },
- iconURL: ICON_URL,
+ permissions: ["history", "https://*/*"],
+ iconURL: "foo-icon.png",
});
const ID2 = "addon2@tests.mozilla.org";
- let mock2 = new MockAddon({
+ yield createXULExtension({
id: ID2,
name: "Test 2",
- userDisabled: true,
- seen: false,
- userPermissions: {
- permissions: [],
- origins: [],
- },
});
const ID3 = "addon3@tests.mozilla.org";
- let mock3 = new MockAddon({
+ yield createWebExtension({
id: ID3,
name: "Test 3",
- isWebExtension: true,
- userDisabled: true,
- seen: false,
- userPermissions: {
- permissions: [],
- origins: ["<all_urls>"],
- }
+ permissions: ["<all_urls>"],
});
const ID4 = "addon4@tests.mozilla.org";
- let mock4 = new MockAddon({
+ yield createWebExtension({
id: ID4,
name: "Test 4",
- isWebExtension: true,
- userDisabled: true,
- seen: false,
- userPermissions: {
- permissions: [],
- origins: ["<all_urls>"],
- }
+ permissions: ["<all_urls>"],
});
- let provider = new MockProvider(mock1, mock2, mock3, mock4);
- AddonManagerPrivate.registerProvider(provider, [{
- id: "extension",
- name: "Extensions",
- uiPriority: 4000,
- flags: AddonManager.TYPE_UI_VIEW_LIST |
- AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL,
- }]);
-
testCleanup = async function() {
- AddonManagerPrivate.unregisterProvider(provider);
-
// clear out ExtensionsUI state about sideloaded extensions so
// subsequent tests don't get confused.
ExtensionsUI.sideloaded.clear();
ExtensionsUI.emit("change");
};
// Navigate away from the starting page to force about:addons to load
// in a new tab during the tests below.
@@ -198,27 +136,23 @@ add_task(function* () {
is(gBrowser.currentURI.spec, "about:addons", "Foreground tab is at about:addons");
const VIEW = "addons://list/extension";
let win = gBrowser.selectedBrowser.contentWindow;
ok(!win.gViewController.isLoading, "about:addons view is fully loaded");
is(win.gViewController.currentViewId, VIEW, "about:addons is at extensions list");
// Check the contents of the notification, then choose "Cancel"
- checkNotification(panel, ICON_URL, [
+ checkNotification(panel, /\/foo-icon\.png$/, [
["webextPerms.hostDescription.allUrls"],
["webextPerms.description.history"],
]);
- let disablePromise = promiseSetDisabled(mock1);
panel.secondaryButton.click();
- let value = yield disablePromise;
- is(value, true, "Addon should remain disabled");
-
let [addon1, addon2, addon3, addon4] = yield AddonManager.getAddonsByIDs([ID1, ID2, ID3, ID4]);
ok(addon1.seen, "Addon should be marked as seen");
is(addon1.userDisabled, true, "Addon 1 should still be disabled");
is(addon2.userDisabled, true, "Addon 2 should still be disabled");
is(addon3.userDisabled, true, "Addon 3 should still be disabled");
is(addon4.userDisabled, true, "Addon 4 should still be disabled");
yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
@@ -240,22 +174,18 @@ add_task(function* () {
win = gBrowser.selectedBrowser.contentWindow;
ok(!win.gViewController.isLoading, "about:addons view is fully loaded");
is(win.gViewController.currentViewId, VIEW, "about:addons is at extensions list");
// Check the notification contents.
checkNotification(panel, DEFAULT_ICON_URL, []);
// This time accept the install.
- disablePromise = promiseSetDisabled(mock2);
panel.button.click();
- value = yield disablePromise;
- is(value, false, "Addon should be set to enabled");
-
[addon1, addon2, addon3, addon4] = yield AddonManager.getAddonsByIDs([ID1, ID2, ID3, ID4]);
is(addon1.userDisabled, true, "Addon 1 should still be disabled");
is(addon2.userDisabled, false, "Addon 2 should now be enabled");
is(addon3.userDisabled, true, "Addon 3 should still be disabled");
is(addon4.userDisabled, true, "Addon 4 should still be disabled");
// Should still have 2 entries in the hamburger menu
yield PanelUI.show();
@@ -283,20 +213,17 @@ add_task(function* () {
// When clicking enable we should see the permissions notification
popupPromise = promisePopupNotificationShown("addon-webext-permissions");
BrowserTestUtils.synthesizeMouseAtCenter(item._enableBtn, {},
gBrowser.selectedBrowser);
panel = yield popupPromise;
checkNotification(panel, DEFAULT_ICON_URL, [["webextPerms.hostDescription.allUrls"]]);
// Accept the permissions
- disablePromise = promiseSetDisabled(mock3);
panel.button.click();
- value = yield disablePromise;
- is(value, false, "userDisabled should be set on addon 3");
addon3 = yield AddonManager.getAddonByID(ID3);
is(addon3.userDisabled, false, "Addon 3 should be enabled");
// Should still have 1 entry in the hamburger menu
yield PanelUI.show();
addons = document.getElementById("PanelUI-footer-addons");
@@ -311,23 +238,26 @@ add_task(function* () {
// When clicking enable we should see the permissions notification
popupPromise = promisePopupNotificationShown("addon-webext-permissions");
BrowserTestUtils.synthesizeMouseAtCenter(button, {},
gBrowser.selectedBrowser);
panel = yield popupPromise;
checkNotification(panel, DEFAULT_ICON_URL, [["webextPerms.hostDescription.allUrls"]]);
// Accept the permissions
- disablePromise = promiseSetDisabled(mock4);
panel.button.click();
- value = yield disablePromise;
- is(value, false, "userDisabled should be set on addon 4");
addon4 = yield AddonManager.getAddonByID(ID4);
is(addon4.userDisabled, false, "Addon 4 should be enabled");
// We should have recorded 1 cancelled followed by 3 accepted sideloads.
expectTelemetry(["sideloadRejected", "sideloadAccepted", "sideloadAccepted", "sideloadAccepted"]);
isnot(menuButton.getAttribute("badge-status"), "addon-alert", "Should no longer have addon alert badge");
+ yield new Promise(resolve => setTimeout(resolve, 100));
+
+ for (let addon of [addon1, addon2, addon3, addon4]) {
+ addon.uninstall();
+ }
+
yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
--- a/browser/base/content/test/webextensions/head.js
+++ b/browser/base/content/test/webextensions/head.js
@@ -213,17 +213,17 @@ function checkPermissionString(string, k
* optional formatting parameter.
*/
function checkNotification(panel, checkIcon, permissions) {
let icon = panel.getAttribute("icon");
let ul = document.getElementById("addon-webext-perm-list");
let header = document.getElementById("addon-webext-perm-intro");
if (checkIcon instanceof RegExp) {
- ok(checkIcon.test(icon), "Notification icon is correct");
+ ok(checkIcon.test(icon), `Notification icon is correct ${JSON.stringify(icon)} ~= ${checkIcon}`);
} else if (typeof checkIcon == "function") {
ok(checkIcon(icon), "Notification icon is correct");
} else {
is(icon, checkIcon, "Notification icon is correct");
}
is(ul.childElementCount, permissions.length, `Permissions list has ${permissions.length} entries`);
if (permissions.length == 0) {
--- a/browser/modules/ExtensionsUI.jsm
+++ b/browser/modules/ExtensionsUI.jsm
@@ -7,16 +7,18 @@ const {classes: Cc, interfaces: Ci, resu
this.EXPORTED_SYMBOLS = ["ExtensionsUI"];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://devtools/shared/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
+ "resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
"resource:///modules/RecentWindow.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyPreferenceGetter(this, "WEBEXT_PERMISSION_PROMPTS",
@@ -43,60 +45,60 @@ this.ExtensionsUI = {
Services.obs.addObserver(this, "webextension-install-notify");
Services.obs.addObserver(this, "webextension-optional-permission-prompt");
await RecentWindow.getMostRecentBrowserWindow().delayedStartupPromise;
this._checkForSideloaded();
},
- _checkForSideloaded() {
- AddonManager.getAllAddons(addons => {
- // Check for any side-loaded addons that the user is allowed
- // to enable.
- let sideloaded = addons.filter(
- addon => addon.seen === false && (addon.permissions & AddonManager.PERM_CAN_ENABLE));
+ async _checkForSideloaded() {
+ let sideloaded = await AddonManagerPrivate.getNewSideloads();
+
+ if (!sideloaded.length) {
+ // No new side-loads. We're done.
+ return;
+ }
+
+ // The ordering shouldn't matter, but tests depend on notifications
+ // happening in a specific order.
+ sideloaded.sort((a, b) => a.id.localeCompare(b.id));
- if (!sideloaded.length) {
- return;
+ if (WEBEXT_PERMISSION_PROMPTS) {
+ if (!this.sideloadListener) {
+ this.sideloadListener = {
+ onEnabled: addon => {
+ if (!this.sideloaded.has(addon)) {
+ return;
+ }
+
+ this.sideloaded.delete(addon);
+ this.emit("change");
+
+ if (this.sideloaded.size == 0) {
+ AddonManager.removeAddonListener(this.sideloadListener);
+ this.sideloadListener = null;
+ }
+ },
+ };
+ AddonManager.addAddonListener(this.sideloadListener);
}
- if (WEBEXT_PERMISSION_PROMPTS) {
- if (!this.sideloadListener) {
- this.sideloadListener = {
- onEnabled: addon => {
- if (!this.sideloaded.has(addon)) {
- return;
- }
-
- this.sideloaded.delete(addon);
- this.emit("change");
-
- if (this.sideloaded.size == 0) {
- AddonManager.removeAddonListener(this.sideloadListener);
- this.sideloadListener = null;
- }
- },
- };
- AddonManager.addAddonListener(this.sideloadListener);
- }
-
- for (let addon of sideloaded) {
- this.sideloaded.add(addon);
- }
- this.emit("change");
- } else {
- // This and all the accompanying about:newaddon code can eventually
- // be removed. See bug 1331521.
- let win = RecentWindow.getMostRecentBrowserWindow();
- for (let addon of sideloaded) {
- win.openUILinkIn(`about:newaddon?id=${addon.id}`, "tab");
- }
+ for (let addon of sideloaded) {
+ this.sideloaded.add(addon);
}
- });
+ this.emit("change");
+ } else {
+ // This and all the accompanying about:newaddon code can eventually
+ // be removed. See bug 1331521.
+ let win = RecentWindow.getMostRecentBrowserWindow();
+ for (let addon of sideloaded) {
+ win.openUILinkIn(`about:newaddon?id=${addon.id}`, "tab");
+ }
+ }
},
showAddonsManager(browser, strings, icon, histkey) {
let global = browser.selectedBrowser.ownerGlobal;
return global.BrowserOpenAddonsMgr("addons://list/extension").then(aomWin => {
let aomBrowser = aomWin.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
.chromeEventHandler;
@@ -143,16 +145,21 @@ this.ExtensionsUI = {
// there are multiple simultaneous installs happening, see
// bug 1329884 for a longer explanation.
let progressNotification = target.ownerGlobal.PopupNotifications.getNotification("addon-progress", target);
if (progressNotification) {
progressNotification.remove();
}
info.unsigned = info.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING;
+ if (info.unsigned && Cu.isInAutomation &&
+ Services.prefs.getBoolPref("extensions.ui.ignoreUnsigned", false)) {
+ info.unsigned = false;
+ }
+
let strings = this._buildStrings(info);
// If this is an update with no promptable permissions, just apply it
if (info.type == "update" && strings.msgs.length == 0) {
info.resolve();
return;
}
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -3099,16 +3099,27 @@ this.AddonManagerPrivate = {
AddonManagerInternal.startup();
},
addonIsActive(addonId) {
return AddonManagerInternal._getProviderByName("XPIProvider")
.addonIsActive(addonId);
},
+ /**
+ * Gets an array of add-ons which were side-loaded prior to the last
+ * startup, and are currently disabled.
+ *
+ * @returns {Promise<Array<Addon>>}
+ */
+ getNewSideloads() {
+ return AddonManagerInternal._getProviderByName("XPIProvider")
+ .getNewSideloads();
+ },
+
get browserUpdated() {
return gBrowserUpdated;
},
registerProvider(aProvider, aTypes) {
AddonManagerInternal.registerProvider(aProvider, aTypes);
},
--- a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
@@ -213,16 +213,19 @@ var AddonTestUtils = {
useRealCertChecks: false,
init(testScope) {
this.testScope = testScope;
// Get the profile directory for tests to use.
this.profileDir = testScope.do_get_profile();
+ this.profileExtensions = this.profileDir.clone();
+ this.profileExtensions.append("extensions");
+
this.extensionsINI = this.profileDir.clone();
this.extensionsINI.append("extensions.ini");
// Register a temporary directory for the tests.
this.tempDir = this.profileDir.clone();
this.tempDir.append("temp");
this.tempDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
this.registerDirectory("TmpD", this.tempDir);
@@ -282,20 +285,17 @@ var AddonTestUtils = {
// Make sure that a given path does not exist
function pathShouldntExist(file) {
if (file.exists()) {
throw new Error(`Test cleanup: path ${file.path} exists when it should not`);
}
}
testScope.do_register_cleanup(() => {
- for (let file of this.tempXPIs) {
- if (file.exists())
- file.remove(false);
- }
+ this.cleanupTempXPIs();
// Check that the temporary directory is empty
var dirEntries = this.tempDir.directoryEntries
.QueryInterface(Ci.nsIDirectoryEnumerator);
var entries = [];
while (dirEntries.hasMoreElements())
entries.push(dirEntries.nextFile.leafName);
if (entries.length)
@@ -337,16 +337,43 @@ var AddonTestUtils = {
testDir.leafName = "staged";
pathShouldntExist(testDir);
return this.promiseShutdownManager();
});
},
+ initMochitest(testScope) {
+ this.profileDir = FileUtils.getDir("ProfD", []);
+
+ this.profileExtensions = FileUtils.getDir("ProfD", ["extensions"]);
+
+ this.tempDir = FileUtils.getDir("TmpD", []);
+ this.tempDir.append("addons-mochitest");
+ this.tempDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ testScope.registerCleanupFunction(() => {
+ this.cleanupTempXPIs();
+ this.tempDir.remove(true);
+ });
+ },
+
+ cleanupTempXPIs() {
+ for (let file of this.tempXPIs.splice(0)) {
+ if (file.exists()) {
+ try {
+ file.remove(false);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ }
+ },
+
/**
* Helper to spin the event loop until a promise resolves or rejects
*
* @param {Promise} promise
* The promise to wait on.
* @returns {*} The promise's resolution value.
* @throws The promise's rejection value, if it rejects.
*/
@@ -407,16 +434,20 @@ var AddonTestUtils = {
}
throw new Error("No manifest file present");
} finally {
zip.close();
}
},
+ getIDFromExtension(file) {
+ return this.getIDFromManifest(this.getManifestURI(file));
+ },
+
getIDFromManifest: Task.async(function*(manifestURI) {
let body = yield fetch(manifestURI.spec);
if (manifestURI.spec.endsWith(".rdf")) {
let data = yield body.text();
let ds = new RDFDataSource();
new RDFXMLParser(ds, manifestURI, data);
@@ -847,28 +878,32 @@ var AddonTestUtils = {
/**
* Manually installs an XPI file into an install location by either copying the
* XPI there or extracting it depending on whether unpacking is being tested
* or not.
*
* @param {nsIFile} xpiFile
* The XPI file to install.
- * @param {nsIFile} installLocation
+ * @param {nsIFile} [installLocation = this.profileExtensions]
* The install location (an nsIFile) to install into.
- * @param {string} id
+ * @param {string} [id]
* The ID to install as.
* @param {boolean} [unpacked = this.testUnpacked]
* If true, install as an unpacked directory, rather than a
* packed XPI.
* @returns {nsIFile}
* A file pointing to the installed location of the XPI file or
* unpacked directory.
*/
- manuallyInstall(xpiFile, installLocation, id, unpacked = this.testUnpacked) {
+ async manuallyInstall(xpiFile, installLocation = this.profileExtensions, id = null, unpacked = this.testUnpacked) {
+ if (id == null) {
+ id = await this.getIDFromExtension(xpiFile);
+ }
+
if (unpacked) {
let dir = installLocation.clone();
dir.append(id);
dir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
let zip = ZipReader(xpiFile);
let entries = zip.findEntries(null);
while (entries.hasMore()) {
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -2223,16 +2223,24 @@ function SerializableMap(arg) {
/**
* Keeps track of the state of XPI add-ons on the file system.
*/
this.XPIStates = {
// Map(location name -> Map(add-on ID -> XPIState))
db: null,
+ /**
+ * @property {Map<string, XPIState>} sideLoadedAddons
+ * A map of new add-ons detected during install location
+ * directory scans. Keys are add-on IDs, values are XPIState
+ * objects corresponding to those add-ons.
+ */
+ sideLoadedAddons: new Map(),
+
get size() {
if (!this.db) {
return 0;
}
let count = 0;
for (let location of this.db.values()) {
count += location.size;
}
@@ -2303,16 +2311,17 @@ this.XPIStates = {
}
for (let [id, file] of addons) {
if (!(id in locState)) {
logger.debug("New add-on ${id} in ${location}", {id, location: location.name});
let xpiState = new XPIState({d: file.persistentDescriptor});
changed = xpiState.getModTime(file, id) || changed;
foundAddons.set(id, xpiState);
+ this.sideLoadedAddons.set(id, xpiState);
} else {
let xpiState = new XPIState(locState[id]);
// We found this add-on in the file system
delete locState[id];
changed = xpiState.getModTime(file, id) || changed;
if (file.persistentDescriptor != xpiState.descriptor) {
@@ -3848,22 +3857,17 @@ this.XPIProvider = {
} catch (e) {
logger.warn("Unable to remove old extension cache " + oldCache.path, e);
}
}
// If the application crashed before completing any pending operations then
// we should perform them now.
if (extensionListChanged || hasPendingChanges) {
- logger.debug("Updating database with changes to installed add-ons");
- XPIDatabase.updateActiveAddons();
- Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
- !XPIDatabase.writeAddonsList());
- Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
- JSON.stringify(this.bootstrappedAddons));
+ this._updateActiveAddons();
return true;
}
logger.debug("No changes found");
} catch (e) {
logger.error("Error during startup file checks", e);
}
@@ -3874,16 +3878,48 @@ this.XPIProvider = {
if (addonsList.exists() != haveAnyAddons) {
logger.debug("Add-ons list is invalid, rebuilding");
XPIDatabase.writeAddonsList();
}
return false;
},
+ _updateActiveAddons() {
+ logger.debug("Updating database with changes to installed add-ons");
+ XPIDatabase.updateActiveAddons();
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
+ !XPIDatabase.writeAddonsList());
+ Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
+ JSON.stringify(this.bootstrappedAddons));
+ },
+
+ /**
+ * Gets an array of add-ons which were placed in a known install location
+ * prior to startup of the current session, were detected by a directory scan
+ * of those locations, and are currently disabled.
+ *
+ * @returns {Promise<Array<Addon>>}
+ */
+ async getNewSideloads() {
+ if (XPIStates.getInstallState(false)) {
+ // We detected changes. Update the database to account for them.
+ await XPIDatabase.asyncLoadDB(false);
+ XPIDatabaseReconcile.processFileChanges({}, false);
+ this._updateActiveAddons();
+ }
+
+ let addons = await Promise.all(
+ Array.from(XPIStates.sideLoadedAddons.keys(),
+ id => AddonManager.getAddonByID(id)));
+
+ return addons.filter(addon => (addon.seen === false &&
+ addon.permissions & AddonManager.PERM_CAN_ENABLE));
+ },
+
/**
* Called to test whether this provider supports installing a particular
* mimetype.
*
* @param aMimetype
* The mimetype to check for
* @return true if the mimetype is application/x-xpinstall
*/
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -1700,17 +1700,18 @@ this.XPIDatabaseReconcile = {
let disablingScopes = Preferences.get(PREF_EM_AUTO_DISABLED_SCOPES, 0);
if (aInstallLocation.scope & disablingScopes) {
logger.warn("Disabling foreign installed add-on " + aNewAddon.id + " in "
+ aInstallLocation.name);
aNewAddon.userDisabled = true;
// If we don't have an old app version then this is a new profile in
// which case just mark any sideloaded add-ons as already seen.
- aNewAddon.seen = !aOldAppVersion;
+ aNewAddon.seen = (aInstallLocation.name != KEY_APP_PROFILE &&
+ !aOldAppVersion);
}
}
return XPIDatabase.addAddonMetadata(aNewAddon, aAddonState.descriptor);
},
/**
* Called when an add-on has been removed.
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -65,17 +65,16 @@ XPCOMUtils.defineLazyModuleGetter(this,
const {
awaitPromise,
createAppInfo,
createInstallRDF,
createTempWebExtensionFile,
createUpdateRDF,
getFileForAddon,
- manuallyInstall,
manuallyUninstall,
promiseAddonEvent,
promiseCompleteAllInstalls,
promiseCompleteInstall,
promiseConsoleOutput,
promiseFindAddonUpdates,
promiseInstallAllFiles,
promiseInstallFile,
@@ -84,16 +83,21 @@ const {
promiseShutdownManager,
promiseStartupManager,
promiseWriteProxyFileToDir,
registerDirectory,
setExtensionModifiedTime,
writeFilesToZip
} = AddonTestUtils;
+function manuallyInstall(...args) {
+ return AddonTestUtils.awaitPromise(
+ AddonTestUtils.manuallyInstall(...args));
+}
+
// WebExtension wrapper for ease of testing
ExtensionTestUtils.init(this);
AddonTestUtils.init(this);
AddonTestUtils.overrideCertDB();
Object.defineProperty(this, "gAppInfo", {
get() {
--- a/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
@@ -13,16 +13,17 @@ const IGNORE = ["getPreferredIconURL", "
"mapURIToAddonID", "shutdown", "init",
"stateToString", "errorToString", "getUpgradeListener",
"addUpgradeListener", "removeUpgradeListener"];
const IGNORE_PRIVATE = ["AddonAuthor", "AddonCompatibilityOverride",
"AddonScreenshot", "AddonType", "startup", "shutdown",
"addonIsActive", "registerProvider", "unregisterProvider",
"addStartupChange", "removeStartupChange",
+ "getNewSideloads",
"recordTimestamp", "recordSimpleMeasure",
"recordException", "getSimpleMeasures", "simpleTimer",
"setTelemetryDetails", "getTelemetryDetails",
"callNoUpdateListeners", "backgroundUpdateTimerHandler",
"hasUpgradeListener", "getUpgradeListener"];
function test_functions() {
for (let prop in AddonManager) {