Source code

Revision control

Copy as Markdown

Other Tools

/* Any copyright is dedicated to the Public Domain.
*/
const { AddonTestUtils } = ChromeUtils.importESModule(
);
const { ExtensionPermissions } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionPermissions.sys.mjs"
);
const lazy = {};
ChromeUtils.defineLazyGetter(lazy, "l10n", function () {
return new Localization(
[
"browser/addonNotifications.ftl",
"branding/brand.ftl",
"toolkit/global/extensions.ftl",
],
true
);
});
const PROGRESS_NOTIFICATION = "addon-progress";
const CHROMEROOT = extractChromeRoot(gTestPath);
AddonTestUtils.initMochitest(this);
let needsCleanupBlocklist = true;
const cleanupBlocklist = async () => {
if (!needsCleanupBlocklist) {
return;
}
await AddonTestUtils.loadBlocklistRawData({
extensionsMLBF: [
{
stash: { blocked: [], unblocked: [] },
stash_time: 0,
},
],
});
needsCleanupBlocklist = false;
};
function getObserverTopic(aNotificationId) {
let topic = aNotificationId;
if (topic == "xpinstall-disabled") {
topic = "addon-install-disabled";
} else if (topic == "addon-progress") {
topic = "addon-install-started";
} else if (topic == "addon-installed") {
topic = "webextension-install-notify";
} else if (topic == "addon-install-failed-blocklist") {
topic = "addon-install-failed";
}
return topic;
}
async function waitForProgressNotification(
aPanelOpen = false,
aExpectedCount = 1,
wantDisabled = true,
expectedAnchorID = "unified-extensions-button",
win = window
) {
let notificationId = PROGRESS_NOTIFICATION;
info("Waiting for " + notificationId + " notification");
let topic = getObserverTopic(notificationId);
let observerPromise = new Promise(resolve => {
Services.obs.addObserver(function observer(aSubject, aTopic) {
// Ignore the progress notification unless that is the notification we want
if (
notificationId != PROGRESS_NOTIFICATION &&
aTopic == getObserverTopic(PROGRESS_NOTIFICATION)
) {
return;
}
Services.obs.removeObserver(observer, topic);
resolve();
}, topic);
});
let panelEventPromise;
if (aPanelOpen) {
panelEventPromise = Promise.resolve();
} else {
panelEventPromise = new Promise(resolve => {
win.PopupNotifications.panel.addEventListener(
"popupshowing",
function () {
resolve();
},
{ once: true }
);
});
}
await observerPromise;
await panelEventPromise;
await TestUtils.waitForTick();
info("Saw a notification");
ok(win.PopupNotifications.isPanelOpen, "Panel should be open");
is(
win.PopupNotifications.panel.childNodes.length,
aExpectedCount,
"Should be the right number of notifications"
);
if (win.PopupNotifications.panel.childNodes.length) {
let nodes = Array.from(win.PopupNotifications.panel.childNodes);
let notification = nodes.find(
n => n.id == notificationId + "-notification"
);
ok(notification, `Should have seen the right notification`);
is(
notification.button.hasAttribute("disabled"),
wantDisabled,
"The install button should be disabled?"
);
let n = win.PopupNotifications.getNotification(PROGRESS_NOTIFICATION);
is(
n?.anchorElement?.id || n?.anchorElement?.parentElement?.id,
expectedAnchorID,
"expected the right anchor ID"
);
}
return win.PopupNotifications.panel;
}
function testInstallDialogIncognitoCheckbox(
installDialog,
{
toggleIncognito = false,
incognitoChecked = false,
incognitoHidden = false,
} = {}
) {
// If the incognito toggle is expected to be in the first install dialog
// verify that it is found and visible and toggle it.
const privateBrowsingCheckbox = installDialog.querySelector(
".webext-perm-privatebrowsing moz-checkbox"
);
is(
!privateBrowsingCheckbox,
incognitoHidden,
incognitoHidden
? "Expect private browsing checkbox to NOT be found in the first dialog"
: "Expect private browsing checkbox to be found in the first install dialog"
);
if (!incognitoHidden) {
if (privateBrowsingCheckbox) {
ok(
BrowserTestUtils.isVisible(privateBrowsingCheckbox),
"private browsing checkbox should be visible"
);
}
// SUMO link should always be visible if the incognito checkbox is expected to be
// shown too (even when there are no other permissions being granted as part of the
// same install dialog).
let permsLearnMore = installDialog.querySelector(
".popup-notification-learnmore-link"
);
is(
permsLearnMore.href,
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"extension-permissions",
"Learn more link has desired URL"
);
ok(
BrowserTestUtils.isVisible(permsLearnMore),
"SUMO link expected to be visible"
);
} else {
if (incognitoChecked) {
throw new Error(
"incognitoChecked can't be set to true when incognitoHidden is set to true"
);
}
if (toggleIncognito) {
throw new Error(
"toggleIncognito can't be set to true when incognitoHidden is set to true"
);
}
return;
}
is(
privateBrowsingCheckbox.checked,
incognitoChecked,
incognitoChecked
? "Expect private browsing checkbox to be checked"
: "Expect private browsing checkbox to NOT be checked"
);
if (toggleIncognito === true) {
privateBrowsingCheckbox.click();
}
}
function acceptAppMenuNotificationWhenShown(
id,
extensionId,
{ dismiss = false, global = window } = {}
) {
const { AppMenuNotifications, PanelUI, document } = global;
return new Promise(resolve => {
let permissionChangePromise = null;
function appMenuPopupHidden() {
PanelUI.panel.removeEventListener("popuphidden", appMenuPopupHidden);
ok(
!PanelUI.menuButton.hasAttribute("badge-status"),
"badge is not set after addon-installed"
);
resolve(permissionChangePromise);
}
function appMenuPopupShown() {
PanelUI.panel.removeEventListener("popupshown", appMenuPopupShown);
PanelUI.menuButton.click();
}
function popupshown() {
let notification = AppMenuNotifications.activeNotification;
if (!notification) {
return;
}
is(notification.id, id, `${id} notification shown`);
ok(PanelUI.isNotificationPanelOpen, "notification panel open");
PanelUI.notificationPanel.removeEventListener("popupshown", popupshown);
if (dismiss) {
// Dismiss the panel by clicking on the appMenu button.
PanelUI.panel.addEventListener("popupshown", appMenuPopupShown);
PanelUI.panel.addEventListener("popuphidden", appMenuPopupHidden);
PanelUI.menuButton.click();
return;
}
// Dismiss the panel by clicking the primary button.
let popupnotificationID = PanelUI._getPopupId(notification);
let popupnotification = document.getElementById(popupnotificationID);
popupnotification.button.click();
resolve(permissionChangePromise);
}
PanelUI.notificationPanel.addEventListener("popupshown", popupshown);
});
}
async function waitForNotification(
aId,
aExpectedCount = 1,
expectedAnchorID = "unified-extensions-button",
win = window
) {
info("Waiting for " + aId + " notification");
let topic = getObserverTopic(aId);
let observerPromise;
if (aId !== "addon-webext-permissions") {
observerPromise = new Promise(resolve => {
Services.obs.addObserver(function observer(aSubject, aTopic) {
// Ignore the progress notification unless that is the notification we want
if (
aId != PROGRESS_NOTIFICATION &&
aTopic == getObserverTopic(PROGRESS_NOTIFICATION)
) {
return;
}
Services.obs.removeObserver(observer, topic);
resolve();
}, topic);
});
}
let panelEventPromise = new Promise(resolve => {
win.PopupNotifications.panel.addEventListener(
"PanelUpdated",
function eventListener(e) {
// Skip notifications that are not the one that we are supposed to be looking for
if (!e.detail.includes(aId)) {
return;
}
win.PopupNotifications.panel.removeEventListener(
"PanelUpdated",
eventListener
);
resolve();
}
);
});
await observerPromise;
await panelEventPromise;
await TestUtils.waitForTick();
info("Saw a " + aId + " notification");
ok(win.PopupNotifications.isPanelOpen, "Panel should be open");
is(
win.PopupNotifications.panel.childNodes.length,
aExpectedCount,
"Should be the right number of notifications"
);
if (win.PopupNotifications.panel.childNodes.length) {
let nodes = Array.from(win.PopupNotifications.panel.childNodes);
let notification = nodes.find(n => n.id == aId + "-notification");
ok(notification, "Should have seen the " + aId + " notification");
let n = win.PopupNotifications.getNotification(aId);
is(
n?.anchorElement?.id || n?.anchorElement?.parentElement?.id,
expectedAnchorID,
"expected the right anchor ID"
);
}
await SimpleTest.promiseFocus(win.PopupNotifications.window);
return win.PopupNotifications.panel;
}
function waitForNotificationClose(win = window) {
if (!win.PopupNotifications.isPanelOpen) {
return Promise.resolve();
}
return new Promise(resolve => {
info("Waiting for notification to close");
win.PopupNotifications.panel.addEventListener(
"popuphidden",
function () {
resolve();
},
{ once: true }
);
});
}
async function waitForInstallDialog(id = "addon-webext-permissions") {
let panel = await waitForNotification(id);
// NOTE: the panel may intermittently still be in the "showing" state, and
// so we explicitly await for the state to become "open" before proceeding
// with asserting the visibility of the elements we expected to be in the
// panel.
if (panel.state === "showing") {
await TestUtils.waitForCondition(
() => panel.state === "open",
`Wait for ${id} panel state to become open`
);
is(panel.state, "open", "Panel.state should be open");
}
return panel.childNodes[0];
}
function removeTabAndWaitForNotificationClose() {
let closePromise = waitForNotificationClose();
BrowserTestUtils.removeTab(gBrowser.selectedTab);
return closePromise;
}
function acceptInstallDialog(installDialog) {
installDialog.button.click();
}
async function waitForSingleNotification() {
while (PopupNotifications.panel.childNodes.length != 1) {
await new Promise(resolve => executeSoon(resolve));
info("Waiting for single notification");
// Notification should never close while we wait
ok(PopupNotifications.isPanelOpen, "Notification should still be open");
}
}
async function setupRedirect(aSettings) {
var baseURL = `${SECURE_TESTROOT}/redirect.sjs`;
const params = new URLSearchParams(aSettings);
params.set("mode", "setup");
await fetch(`${baseURL}?${params}`);
}
async function installAddonWithPrivateBrowsingAccess(xpiUrl, addonId) {
// Note: this used to be effective, but changed in bug 1974419. Since then,
// the private browsing permission is only read from the database if an
// add-on was already installed at the time of prompting.
await ExtensionPermissions.add(addonId, {
permissions: ["internal:privateBrowsingAllowed"],
origins: [],
});
let progressPromise = waitForProgressNotification();
let dialogPromise = waitForInstallDialog();
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, {
wantLoad: "about:blank",
});
gURLBar.value = xpiUrl;
gURLBar.focus();
EventUtils.synthesizeKey("KEY_Enter");
await progressPromise;
let installDialog = await dialogPromise;
testInstallDialogIncognitoCheckbox(installDialog, {
incognitoHidden: false,
// Note: before the change in bug 1974419, this expectation was true.
incognitoChecked: false,
toggleIncognito: true,
});
let notificationPromise = acceptAppMenuNotificationWhenShown(
"addon-installed",
addonId
);
let readyPromise = AddonTestUtils.promiseWebExtensionStartup(addonId);
installDialog.button.click();
await notificationPromise;
let installs = await AddonManager.getAllInstalls();
is(installs.length, 0, "Should be no pending installs");
await readyPromise;
let policy = WebExtensionPolicy.getByID(addonId);
ok(policy.privateBrowsingAllowed, "private browsing permission granted");
await removeTabAndWaitForNotificationClose();
}
add_setup(async function () {
requestLongerTimeout(4);
await SpecialPowers.pushPrefEnv({
set: [
["extensions.logging.enabled", true],
["security.dialog_enable_delay", 0],
],
});
const XPInstallObserver = {
observe(aSubject, aTopic) {
let installInfo = aSubject.wrappedJSObject;
info(`Observed ${aTopic} for ${installInfo.installs.length} installs`);
installInfo.installs.forEach(aInstall =>
info(
`Install of ${aInstall.sourceURI.spec} was in state ${aInstall.state}`
)
);
},
};
Services.obs.addObserver(XPInstallObserver, "addon-install-started");
Services.obs.addObserver(XPInstallObserver, "addon-install-blocked");
Services.obs.addObserver(XPInstallObserver, "addon-install-failed");
registerCleanupFunction(async function () {
let aInstalls = await AddonManager.getAllInstalls();
aInstalls.forEach(function (aInstall) {
aInstall.cancel();
});
Services.obs.removeObserver(XPInstallObserver, "addon-install-started");
Services.obs.removeObserver(XPInstallObserver, "addon-install-blocked");
Services.obs.removeObserver(XPInstallObserver, "addon-install-failed");
});
});
describe("Add-on installation doorhangers", function () {
async function beforeAndAfterEach() {
ok(!PopupNotifications.isPanelOpen, "Notification should be closed");
let installs = await AddonManager.getAllInstalls();
is(installs.length, 0, "Should be no active installs");
}
beforeEach(beforeAndAfterEach);
afterEach(beforeAndAfterEach);
it("should show an 'install disabled' notification when xpinstall.enabled=false that lets the user enable installation", async function test_disabledInstall() {
await SpecialPowers.pushPrefEnv({
set: [["xpinstall.enabled", false]],
});
let notificationPromise = waitForNotification("xpinstall-disabled");
let xpiURL = TESTROOT + "amosigned.xpi";
BrowserTestUtils.openNewForegroundTab(
gBrowser,
TESTROOT + "navigate.html?" + xpiURL
);
let panel = await notificationPromise;
let notification = panel.childNodes[0];
is(
notification.button.label,
"Enable",
"Should have seen the right button"
);
is(
notification.getAttribute("label"),
"Software installation is currently disabled. Click Enable and try again.",
"notification label is correct"
);
let closePromise = waitForNotificationClose();
// Click on Enable
EventUtils.synthesizeMouseAtCenter(notification.button, {});
await closePromise;
try {
ok(
Services.prefs.getBoolPref("xpinstall.enabled"),
"Installation should be enabled"
);
} catch (e) {
ok(false, "xpinstall.enabled should be set");
}
BrowserTestUtils.removeTab(gBrowser.selectedTab);
let installs = await AddonManager.getAllInstalls();
is(installs.length, 0, "Shouldn't be any pending installs");
await SpecialPowers.popPrefEnv();
});
it("should show addon-install-blocked for 3rd party installs and proceed to installation after being allowed by the user", async function test_blockedInstall() {
await SpecialPowers.pushPrefEnv({
set: [["extensions.postDownloadThirdPartyPrompt", false]],
});
let notificationPromise = waitForNotification("addon-install-blocked");
let xpiURL = TESTROOT + "amosigned.xpi";
BrowserTestUtils.openNewForegroundTab(
gBrowser,
TESTROOT + "navigate.html?" + xpiURL
);
let panel = await notificationPromise;
let notification = panel.childNodes[0];
is(
notification.button.label,
"Continue to Installation",
"Should have seen the right button"
);
is(
notification
.querySelector("#addon-install-blocked-info")
.getAttribute("href"),
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"unlisted-extensions-risks",
"Got the expected SUMO page as a learn more link in the addon-install-blocked panel"
);
let message = panel.ownerDocument.getElementById(
"addon-install-blocked-message"
);
is(
message.textContent,
"You are attempting to install an add-on from example.com. Make sure you trust this site before continuing.",
"Should have seen the right message"
);
let dialogPromise = waitForInstallDialog();
// Click on Allow
EventUtils.synthesizeMouse(notification.button, 20, 10, {});
// Notification should have changed to progress notification
ok(PopupNotifications.isPanelOpen, "Notification should still be open");
notification = panel.childNodes[0];
is(
notification.id,
"addon-progress-notification",
"Should have seen the progress notification"
);
let installDialog = await dialogPromise;
notificationPromise = acceptAppMenuNotificationWhenShown(
"addon-installed",
"amosigned-xpi@tests.mozilla.org"
);
installDialog.button.click();
await notificationPromise;
let installs = await AddonManager.getAllInstalls();
is(installs.length, 0, "Should be no pending installs");
let addon = await AddonManager.getAddonByID(
"amosigned-xpi@tests.mozilla.org"
);
await addon.uninstall();
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
await SpecialPowers.popPrefEnv();
});
it("should show the 3rd party install panel when installing add-ons from websites", async function test_blockedPostDownload() {
await SpecialPowers.pushPrefEnv({
set: [["extensions.postDownloadThirdPartyPrompt", true]],
});
let notificationPromise = waitForNotification("addon-install-blocked");
let xpiURL = TESTROOT + "amosigned.xpi";
BrowserTestUtils.openNewForegroundTab(
gBrowser,
TESTROOT + "navigate.html?" + xpiURL
);
let panel = await notificationPromise;
let notification = panel.childNodes[0];
is(
notification.button.label,
"Continue to Installation",
"Should have seen the right button"
);
let message = panel.ownerDocument.getElementById(
"addon-install-blocked-message"
);
is(
message.textContent,
"You are attempting to install an add-on from example.com. Make sure you trust this site before continuing.",
"Should have seen the right message"
);
let dialogPromise = waitForInstallDialog();
// Click on Allow
EventUtils.synthesizeMouse(notification.button, 20, 10, {});
let installDialog = await dialogPromise;
notificationPromise = acceptAppMenuNotificationWhenShown(
"addon-installed",
"amosigned-xpi@tests.mozilla.org"
);
installDialog.button.click();
await notificationPromise;
let installs = await AddonManager.getAllInstalls();
is(installs.length, 0, "Should be no pending installs");
let addon = await AddonManager.getAddonByID(
"amosigned-xpi@tests.mozilla.org"
);
await addon.uninstall();
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
await SpecialPowers.popPrefEnv();
});
it("should not show the 3rd party install panel when installing recommended add-ons", async function test_recommendedPostDownload() {
await SpecialPowers.pushPrefEnv({
set: [["extensions.postDownloadThirdPartyPrompt", true]],
});
let xpiURL = TESTROOT + "recommended.xpi";
BrowserTestUtils.openNewForegroundTab(
gBrowser,
TESTROOT + "navigate.html?" + xpiURL
);
let installDialog = await waitForInstallDialog();
let notificationPromise = acceptAppMenuNotificationWhenShown(
"addon-installed",
"{811d77f1-f306-4187-9251-b4ff99bad60b}"
);
installDialog.button.click();
await notificationPromise;
let installs = await AddonManager.getAllInstalls();
is(installs.length, 0, "Should be no pending installs");
let addon = await AddonManager.getAddonByID(
"{811d77f1-f306-4187-9251-b4ff99bad60b}"
);
await addon.uninstall();
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
await SpecialPowers.popPrefEnv();
});
it("should not show the 3rd party install panel when installing privileged add-ons", async function test_priviledgedNo3rdPartyPrompt() {
await SpecialPowers.pushPrefEnv({
set: [["extensions.postDownloadThirdPartyPrompt", true]],
});
AddonManager.checkUpdateSecurity = false;
registerCleanupFunction(() => {
AddonManager.checkUpdateSecurity = true;
});
let xpiURL = TESTROOT + "privileged.xpi";
let installDialogPromise = waitForInstallDialog();
try {
// Prevent install to fail due to privileged.xpi version using
// an addon version that hits a manifest warning (see PRIV_ADDON_VERSION).
// TODO(Bug 1824240): remove this once privileged.xpi can be resigned with a
// version format that does not hit a manifest warning.
ExtensionTestUtils.failOnSchemaWarnings(false);
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
TESTROOT + "navigate.html?" + xpiURL
);
let notificationPromise = acceptAppMenuNotificationWhenShown(
"addon-installed",
"test@tests.mozilla.org"
);
const installDialog = await installDialogPromise;
testInstallDialogIncognitoCheckbox(installDialog, {
incognitoHidden: true,
});
installDialog.button.click();
await notificationPromise;
let installs = await AddonManager.getAllInstalls();
is(installs.length, 0, "Should be no pending installs");
let addon = await AddonManager.getAddonByID("test@tests.mozilla.org");
await addon.uninstall();
await BrowserTestUtils.removeTab(tab);
} finally {
ExtensionTestUtils.failOnSchemaWarnings(true);
}
await SpecialPowers.popPrefEnv();
AddonManager.checkUpdateSecurity = true;
});
it("should permanently block installations from a site when the user chooses 'Never Allow'", async function test_permaBlockInstall() {
let notificationPromise = waitForNotification("addon-install-blocked");
let xpiURL = TESTROOT + "amosigned.xpi";
let target = TESTROOT + "navigate.html?" + xpiURL;
BrowserTestUtils.openNewForegroundTab(gBrowser, target);
let notification = (await notificationPromise).firstElementChild;
let neverAllowBtn = notification.menupopup.firstElementChild;
neverAllowBtn.click();
await TestUtils.waitForCondition(
() => !PopupNotifications.isPanelOpen,
"Waiting for notification to close"
);
let installs = await AddonManager.getAllInstalls();
is(installs.length, 0, "Should be no pending installs");
let installPerm = PermissionTestUtils.testPermission(
gBrowser.currentURI,
"install"
);
is(
installPerm,
Ci.nsIPermissionManager.DENY_ACTION,
"Addon installation should be blocked for site"
);
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
PermissionTestUtils.remove(target, "install");
});
it("should not show any installation prompt for a site that is permanently blocked", async function test_permaBlockedInstallNoPrompt() {
let xpiURL = TESTROOT + "amosigned.xpi";
let target = TESTROOT + "navigate.html?" + xpiURL;
PermissionTestUtils.add(target, "install", Services.perms.DENY_ACTION);
await BrowserTestUtils.openNewForegroundTab(gBrowser, target);
let panelOpened;
try {
panelOpened = await TestUtils.waitForCondition(
() => PopupNotifications.isPanelOpen,
100,
10
);
} catch (ex) {
panelOpened = false;
}
is(panelOpened, false, "Addon prompt should not open");
let installs = await AddonManager.getAllInstalls();
is(installs.length, 0, "Should be no pending installs");
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
PermissionTestUtils.remove(target, "install");
});
it("should proceed directly to the install dialog for a site that already has permission to install add-ons", async function test_allowedInstall() {
let originalTab = gBrowser.selectedTab;
let tab;
gBrowser.selectedTab = originalTab;
PermissionTestUtils.add(
"install",
Services.perms.ALLOW_ACTION
);
let progressPromise = waitForProgressNotification();
let dialogPromise = waitForInstallDialog();
let xpiURL = TESTROOT + "amosigned.xpi";
BrowserTestUtils.openNewForegroundTab(
gBrowser,
SECURE_TESTROOT + "navigate.html?" + xpiURL
).then(newTab => (tab = newTab));
await progressPromise;
let installDialog = await dialogPromise;
await BrowserTestUtils.waitForCondition(
() => !!tab,
"tab should be present"
);
is(
gBrowser.selectedTab,
tab,
"tab selected in response to the addon-install-confirmation notification"
);
let notificationPromise = acceptAppMenuNotificationWhenShown(
"addon-installed",
"amosigned-xpi@tests.mozilla.org",
{ dismiss: true }
);
let readyPromise = AddonTestUtils.promiseWebExtensionStartup(
"amosigned-xpi@tests.mozilla.org"
);
acceptInstallDialog(installDialog);
await notificationPromise;
let installs = await AddonManager.getAllInstalls();
is(installs.length, 0, "Should be no pending installs");
let addon = await AddonManager.getAddonByID(
"amosigned-xpi@tests.mozilla.org"
);
await readyPromise;
let readyPromise2 = AddonTestUtils.promiseWebExtensionStartup(
"amosigned-xpi@tests.mozilla.org"
);
// Test that the addon does not have permission. Reload it to ensure it would
// have been set if possible.
await addon.reload();
await readyPromise2;
let policy = WebExtensionPolicy.getByID(addon.id);
ok(
!policy.privateBrowsingAllowed,
"private browsing permission was not granted"
);
await addon.uninstall();
PermissionTestUtils.remove("https://example.com/", "install");
await removeTabAndWaitForNotificationClose();
});
it("should show a connection failure notification when the add-on download fails", async function test_failedDownload() {
PermissionTestUtils.add(
"install",
Services.perms.ALLOW_ACTION
);
let gotFirstRequest = false;
const server = AddonTestUtils.createHttpServer({
hosts: ["fail.example.com"],
});
server.registerPathHandler(
"/failed-xpi-download.xpi",
(request, response) => {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Type", "application/x-xpinstall");
if (!gotFirstRequest) {
// The first request is expected to be the one intercepted by amContentHandler.sys.mjs,
// which will just look to the content-type to detect it as an add-on installation,
// then the request is cancelled and a new one retriggered as part of the actual
// add-on install flow.
gotFirstRequest = true;
} else {
// This second request is the actual download. We respond with an
// error to simulate a network failure.
response.setStatusLine(request.httpVersion, 502, "Bad Gateway");
}
}
);
let progressPromise = waitForProgressNotification();
let failPromise = waitForNotification("addon-install-failed");
BrowserTestUtils.openNewForegroundTab(
gBrowser,
TESTROOT + "navigate.html?" + xpiURL
);
await progressPromise;
let panel = await failPromise;
let notification = panel.childNodes[0];
is(
notification.getAttribute("label"),
"The add-on could not be downloaded because of a connection failure.",
"Should have seen the right message"
);
PermissionTestUtils.remove("http://example.com/", "install");
await removeTabAndWaitForNotificationClose();
});
it("should show a 'corrupt file' notification when the downloaded add-on is corrupt", async function test_corruptFile() {
PermissionTestUtils.add(
"install",
Services.perms.ALLOW_ACTION
);
let progressPromise = waitForProgressNotification();
let failPromise = waitForNotification("addon-install-failed");
let xpiURL = TESTROOT + "corrupt.xpi";
BrowserTestUtils.openNewForegroundTab(
gBrowser,
TESTROOT + "navigate.html?" + xpiURL
);
await progressPromise;
let panel = await failPromise;
let notification = panel.childNodes[0];
is(
notification.getAttribute("label"),
"The add-on downloaded from this site could not be installed " +
"because it appears to be corrupt.",
"Should have seen the right message"
);
PermissionTestUtils.remove("http://example.com/", "install");
await removeTabAndWaitForNotificationClose();
});
it("should show an incompatibility notification when the add-on is not compatible with the Firefox version", async function test_incompatible() {
PermissionTestUtils.add(
"install",
Services.perms.ALLOW_ACTION
);
let progressPromise = waitForProgressNotification();
let failPromise = waitForNotification("addon-install-failed");
let xpiURL = TESTROOT + "incompatible.xpi";
BrowserTestUtils.openNewForegroundTab(
gBrowser,
TESTROOT + "navigate.html?" + xpiURL
);
await progressPromise;
let panel = await failPromise;
let notification = panel.childNodes[0];
let brandBundle = Services.strings.createBundle(
"chrome://branding/locale/brand.properties"
);
let brandShortName = brandBundle.GetStringFromName("brandShortName");
let message = `XPI Test could not be installed because it is not compatible with ${brandShortName} ${Services.appinfo.version}.`;
is(
notification.getAttribute("label"),
message,
"Should have seen the right message"
);
PermissionTestUtils.remove("http://example.com/", "install");
await removeTabAndWaitForNotificationClose();
});
it.skipIf(
"should show a warning in the installation dialog when the add-on is not signed",
() => AppConstants.MOZ_REQUIRE_SIGNING,
async function test_unsigned() {
let progressPromise = waitForProgressNotification();
let notificationPromise = waitForNotification("addon-install-blocked");
let xpiURL = TESTROOT + "unsigned.xpi";
BrowserTestUtils.openNewForegroundTab(
gBrowser,
TESTROOT + "navigate.html?" + xpiURL
);
await progressPromise;
let panel = await notificationPromise;
let notification = panel.childNodes[0];
let message = panel.ownerDocument.getElementById(
"addon-install-blocked-message"
);
is(
message.textContent,
"You are attempting to install an add-on from example.com. Make sure you trust this site before continuing.",
"Should have seen the right message"
);
let dialogPromise = waitForInstallDialog();
// Click on Allow
EventUtils.synthesizeMouse(notification.button, 20, 10, {});
let installDialog = await dialogPromise;
message = lazy.l10n.formatValueSync(`webext-perms-list-intro-unsigned`);
is(
installDialog.introEl.textContent,
message,
"Should have seen the right message"
);
is(
installDialog.getAttribute("icon"),
"chrome://global/skin/icons/warning.svg",
"Expect panel icon to be set to the warning icon"
);
await removeTabAndWaitForNotificationClose();
}
);
it("should show the appropriate blocklist notification for hard-blocked and soft-blocked add-ons", async function test_blocklisted() {
let addonName = "XPI Test";
let id = "amosigned-xpi@tests.mozilla.org";
let version = "2.2";
const {
BlocklistPrivate: { ExtensionBlocklistMLBF },
} = ChromeUtils.importESModule("resource://gre/modules/Blocklist.sys.mjs");
await SpecialPowers.pushPrefEnv({
set: [
[
"extensions.blocklist.addonItemURL",
],
],
});
const blocklistURL = ExtensionBlocklistMLBF.createBlocklistURL(id, version);
info("Verify addon-install-failed on hard-blocked addon");
await testBlocklistedAddon({
stash: { blocked: [`${id}:${version}`], unblocked: [] },
expected: {
fluentId: "addon-install-error-hard-blocked",
blocklistURL,
},
});
info("Verify addon-install-failed on soft-blocked blocked addon");
await SpecialPowers.pushPrefEnv({
set: [["extensions.blocklist.softblock.enabled", true]],
});
await testBlocklistedAddon({
stash: { softblocked: [`${id}:${version}`], blocked: [], unblocked: [] },
expected: {
fluentId: "addon-install-error-soft-blocked",
blocklistURL,
},
});
// Clear extensions.blocklist.softblock.enabled pref.
await SpecialPowers.popPrefEnv();
// Clear extensions.blocklist.addonItemURL pref.
await SpecialPowers.popPrefEnv();
async function testBlocklistedAddon({ stash, expected }) {
await AddonTestUtils.loadBlocklistRawData({
extensionsMLBF: [{ stash, stash_time: 0 }],
});
needsCleanupBlocklist = true;
registerCleanupFunction(cleanupBlocklist);
PermissionTestUtils.add(
"install",
Services.perms.ALLOW_ACTION
);
let progressPromise = waitForProgressNotification();
let failPromise = waitForNotification("addon-install-failed-blocklist");
let xpiURL = TESTROOT + "amosigned.xpi";
BrowserTestUtils.openNewForegroundTab(
gBrowser,
TESTROOT + "navigate.html?" + xpiURL
);
await progressPromise;
info("Wait for addon-install-failed notification");
let panel = await failPromise;
let notification = panel.childNodes[0];
let message = lazy.l10n.formatValueSync(expected.fluentId, { addonName });
is(
notification.getAttribute("label"),
message,
"Should have seen the right message"
);
await BrowserTestUtils.waitForCondition(
() => panel.state === "open",
"Wait for the panel to reach the open state"
);
let blocklistURLEl = panel.querySelector(
"#addon-install-failed-blocklist-info"
);
ok(
BrowserTestUtils.isVisible(blocklistURLEl),
"Expect blocklist info link to be visible"
);
is(
blocklistURLEl.getAttribute("href"),
expected.blocklistURL,
"Blocklist info link href should be set to the expected url"
);
is(
blocklistURLEl.textContent,
await panel.ownerDocument.l10n.formatValue(
"popup-notification-xpinstall-prompt-block-url"
),
"Blocklist info link should have the expected localized string"
);
// Clicking on the the blocklistURL link is expected to dismiss the
// popup.
let closePromise = waitForNotificationClose();
let newTabPromise = BrowserTestUtils.waitForNewTab(
gBrowser,
expected.blocklistURL,
true
);
info(
`Click on the blocklist "See details" link ${expected.blocklistURL}`
);
blocklistURLEl.click();
const newTab = await newTabPromise;
is(
newTab,
gBrowser.selectedTab,
"Blocklist info tab is currrently selected"
);
BrowserTestUtils.removeTab(newTab);
await cleanupBlocklist();
PermissionTestUtils.remove("http://example.com/", "install");
BrowserTestUtils.removeTab(gBrowser.selectedTab);
await closePromise;
}
});
it("should show a 'corrupt file' failure notification when installing a local corrupt file", async function test_localFile() {
let cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
Ci.nsIChromeRegistry
);
let path;
try {
path = cr.convertChromeURL(makeURI(CHROMEROOT + "corrupt.xpi")).spec;
} catch (ex) {
path = CHROMEROOT + "corrupt.xpi";
}
let failPromise = new Promise(resolve => {
Services.obs.addObserver(function observer() {
Services.obs.removeObserver(observer, "addon-install-failed");
resolve();
}, "addon-install-failed");
});
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, {
wantLoad: "about:blank",
});
BrowserTestUtils.startLoadingURIString(gBrowser, path);
await failPromise;
// Wait for the browser code to add the failure notification
await waitForSingleNotification();
let notification = PopupNotifications.panel.childNodes[0];
is(
notification.id,
"addon-install-failed-notification",
"Should have seen the install fail"
);
is(
notification.getAttribute("label"),
"This add-on could not be installed because it appears to be corrupt.",
"Should have seen the right message"
);
await removeTabAndWaitForNotificationClose();
});
it("should handle add-on installation initiated from the address bar", async function test_urlBar() {
let progressPromise = waitForProgressNotification();
let dialogPromise = waitForInstallDialog();
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, {
wantLoad: "about:blank",
});
gURLBar.value = TESTROOT + "amosigned.xpi";
gURLBar.focus();
EventUtils.synthesizeKey("KEY_Enter");
await progressPromise;
let installDialog = await dialogPromise;
testInstallDialogIncognitoCheckbox(installDialog, {
incognitoHidden: false,
incognitoChecked: false,
toggleIncognito: true,
});
let notificationPromise = acceptAppMenuNotificationWhenShown(
"addon-installed",
"amosigned-xpi@tests.mozilla.org"
);
installDialog.button.click();
await notificationPromise;
let installs = await AddonManager.getAllInstalls();
is(installs.length, 0, "Should be no pending installs");
let addon = await AddonManager.getAddonByID(
"amosigned-xpi@tests.mozilla.org"
);
// The panel is reloading the addon due to the permission change, we need some way
// to wait for the reload to finish. addon.startupPromise doesn't do it for
// us, so we'll just restart again.
await addon.reload();
// This addon should have private browsing permission.
let policy = WebExtensionPolicy.getByID(addon.id);
ok(policy.privateBrowsingAllowed, "private browsing permission granted");
await addon.uninstall();
await removeTabAndWaitForNotificationClose();
});
it("should show addon-install-failed when failing to install on a navigation that is triggered by the system principal", async function test_wrongHost() {
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
let loadedPromise = BrowserTestUtils.browserLoaded(
gBrowser.selectedBrowser,
false,
TESTROOT2
);
BrowserTestUtils.startLoadingURIString(gBrowser, TESTROOT2);
await loadedPromise;
let progressPromise = waitForProgressNotification();
let notificationPromise = waitForNotification("addon-install-failed");
BrowserTestUtils.startLoadingURIString(gBrowser, TESTROOT + "corrupt.xpi");
await progressPromise;
let panel = await notificationPromise;
let notification = panel.childNodes[0];
is(
notification.getAttribute("label"),
"The add-on downloaded from this site could not be installed " +
"because it appears to be corrupt.",
"Should have seen the right message"
);
await removeTabAndWaitForNotificationClose();
});
it("should re-notify the user about blocked install if they dismiss the panel then trigger it again", async function test_renotifyBlocked() {
let notificationPromise = waitForNotification("addon-install-blocked");
let xpiURL = TESTROOT + "amosigned.xpi";
BrowserTestUtils.openNewForegroundTab(
gBrowser,
TESTROOT + "navigate.html?" + xpiURL
);
let panel = await notificationPromise;
let closePromise = waitForNotificationClose();
// hide the panel (this simulates the user dismissing it)
panel.hidePopup();
await closePromise;
info("Timeouts after this probably mean bug 589954 regressed");
await new Promise(resolve => executeSoon(resolve));
notificationPromise = waitForNotification("addon-install-blocked");
BrowserTestUtils.startLoadingURIString(
gBrowser,
TESTROOT + "navigate.html?" + xpiURL
);
await notificationPromise;
let installs = await AddonManager.getAllInstalls();
is(installs.length, 2, "Should be two pending installs");
await removeTabAndWaitForNotificationClose(gBrowser.selectedTab);
installs = await AddonManager.getAllInstalls();
is(installs.length, 0, "Should have cancelled the installs");
});
it("should cancel the installation when the user clicks cancel on the download progress notification", async function test_cancel() {
PermissionTestUtils.add(
"install",
Services.perms.ALLOW_ACTION
);
let gotFirstRequest = false;
let hangingResponse;
let hangingResponseCleanup = () => {
hangingResponse?.finish();
hangingResponse = null;
};
registerCleanupFunction(hangingResponseCleanup);
const server = AddonTestUtils.createHttpServer({
hosts: ["slow.example.com"],
});
server.registerPathHandler(
"/slow-xpi-download.xpi",
(request, response) => {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Type", "application/x-xpinstall");
if (!gotFirstRequest) {
// The first request is expected to be the one intercepted by amContentHandler.sys.mjs,
// which will just look to the content-type to detect it as an add-on installation,
// then the request is cancelled and a new one retriggered as part of the actual
// add-on install flow.
gotFirstRequest = true;
} else {
// This second request is meant to be the one that is part of the actual add-on
// install flow and we leave it pending so that the test can then reliably
// cancel it from the download progress notification.
hangingResponse = response;
response.processAsync();
}
}
);
let notificationPromise = waitForNotification(PROGRESS_NOTIFICATION);
BrowserTestUtils.openNewForegroundTab(
gBrowser,
TESTROOT + "navigate.html?" + xpiURL
);
let panel = await notificationPromise;
let notification = panel.childNodes[0];
ok(PopupNotifications.isPanelOpen, "Notification should still be open");
is(
PopupNotifications.panel.childNodes.length,
1,
"Should be only one notification"
);
is(
notification.id,
"addon-progress-notification",
"Should have seen the progress notification"
);
// Cancel the download
let install = notification.notification.options.installs[0];
let cancelledPromise = new Promise(resolve => {
install.addListener({
onDownloadCancelled() {
install.removeListener(this);
resolve();
},
});
});
EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {});
await cancelledPromise;
await TestUtils.waitForTick();
ok(!PopupNotifications.isPanelOpen, "Notification should be closed");
let installs = await AddonManager.getAllInstalls();
is(installs.length, 0, "Should be no pending install");
PermissionTestUtils.remove("http://example.com/", "install");
BrowserTestUtils.removeTab(gBrowser.selectedTab);
hangingResponseCleanup();
});
it("should fail the installation for an https redirect to an insecure http URL without a fallback hash", async function test_failedSecurity() {
await SpecialPowers.pushPrefEnv({
set: [
[PREF_INSTALL_REQUIREBUILTINCERTS, false],
["extensions.postDownloadThirdPartyPrompt", false],
],
});
// Configures redirect.sjs to redirect to a http url through the Location
// header, this is expected to be triggering a download failure with an
// ERROR_NETWORK_FAILURE error due to the https url redirecting to an xpi
// to be downloaded from an insecure http url
// (and no hash provided to allow installing it from an insecure url).
setupRedirect({
Location: TESTROOT + "amosigned.xpi",
});
let notificationPromise = waitForNotification("addon-install-failed");
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
SECURE_TESTROOT
);
let install = await AddonManager.getInstallForURL(
SECURE_TESTROOT + "redirect.sjs?mode=redirect"
);
AddonManager.installAddonFromWebpage(
"application/x-xpinstall",
tab.linkedBrowser,
Services.scriptSecurityManager.getSystemPrincipal(),
install
);
await notificationPromise;
ok(PopupNotifications.isPanelOpen, "Notification should still be open");
is(
PopupNotifications.panel.childNodes.length,
1,
"Should be only one notification"
);
let notification = PopupNotifications.panel.childNodes[0];
is(
notification.id,
"addon-install-failed-notification",
"Should have seen the install fail"
);
await removeTabAndWaitForNotificationClose();
await SpecialPowers.popPrefEnv();
});
// Verifies that incognito checkbox is checked if add-on was already
// installed before, with private access. Regression test for bug 1581852.
it("should revoke incognito permission when users uncheck the checkbox on re-installing add-on with granted incognito permission", async function test_incognito_checkbox() {
// Grant permission up front.
await installAddonWithPrivateBrowsingAccess(
TESTROOT + "amosigned.xpi",
"amosigned-xpi@tests.mozilla.org"
);
// ^ the above add-on will be overwritten by the install below, and removed
// at the end of this task.
let progressPromise = waitForProgressNotification();
let dialogPromise = waitForInstallDialog();
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, {
wantLoad: "about:blank",
});
gURLBar.value = TESTROOT + "amosigned.xpi";
gURLBar.focus();
EventUtils.synthesizeKey("KEY_Enter");
await progressPromise;
let installDialog = await dialogPromise;
testInstallDialogIncognitoCheckbox(installDialog, {
incognitoHidden: false,
incognitoChecked: true,
toggleIncognito: true,
});
let notificationPromise = acceptAppMenuNotificationWhenShown(
"addon-installed",
"amosigned-xpi@tests.mozilla.org"
);
let readyPromise = AddonTestUtils.promiseWebExtensionStartup(
"amosigned-xpi@tests.mozilla.org"
);
installDialog.button.click();
await notificationPromise;
let installs = await AddonManager.getAllInstalls();
is(installs.length, 0, "Should be no pending installs");
let addon = await AddonManager.getAddonByID(
"amosigned-xpi@tests.mozilla.org"
);
await readyPromise;
// This addon should no longer have private browsing permission.
let policy = WebExtensionPolicy.getByID(addon.id);
ok(!policy.privateBrowsingAllowed, "private browsing permission removed");
await addon.uninstall();
await removeTabAndWaitForNotificationClose();
});
it("should correctly handle the incognito checkbox toggle during re-install in a new window", async function test_incognito_checkbox_new_window() {
// Grant permission up front.
await installAddonWithPrivateBrowsingAccess(
TESTROOT + "amosigned.xpi",
"amosigned-xpi@tests.mozilla.org"
);
// ^ the above add-on will be overwritten by the install below, and removed
// at the end of this task.
let win = await BrowserTestUtils.openNewBrowserWindow();
await SimpleTest.promiseFocus(win);
let panelEventPromise = new Promise(resolve => {
win.PopupNotifications.panel.addEventListener(
"PanelUpdated",
function eventListener(e) {
if (e.detail.includes("addon-webext-permissions")) {
win.PopupNotifications.panel.removeEventListener(
"PanelUpdated",
eventListener
);
// NOTE: the panel may intermittently still be in the "showing" state, and
// so we explicitly await for the state to become "open" before proceeding
// with asserting the visibility of the elements we expected to be in the
// panel.
if (win.PopupNotifications.panel.state === "showing") {
TestUtils.waitForCondition(
() => win.PopupNotifications.panel.state === "open",
"Wait for addon-webext-permissions panel state to become open"
).then(resolve);
} else {
is(
win.PopupNotifications.panel.state,
"open",
"Expect addon-webext-permissions panel state to be open"
);
resolve();
}
}
}
);
});
win.gBrowser.selectedTab = BrowserTestUtils.addTab(
win.gBrowser,
"about:blank"
);
await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser, {
wantLoad: "about:blank",
});
win.gURLBar.value = TESTROOT + "amosigned.xpi";
win.gURLBar.focus();
EventUtils.synthesizeKey("KEY_Enter", {}, win);
await panelEventPromise;
await TestUtils.waitForTick();
let panel = win.PopupNotifications.panel;
let installDialog = panel.childNodes[0];
testInstallDialogIncognitoCheckbox(installDialog, {
incognitoHidden: false,
incognitoChecked: true,
toggleIncognito: true,
});
let notificationPromise = acceptAppMenuNotificationWhenShown(
"addon-installed",
"amosigned-xpi@tests.mozilla.org",
{ global: win }
);
let readyPromise = AddonTestUtils.promiseWebExtensionStartup(
"amosigned-xpi@tests.mozilla.org"
);
acceptInstallDialog(installDialog);
await notificationPromise;
let installs = await AddonManager.getAllInstalls();
is(installs.length, 0, "Should be no pending installs");
let addon = await AddonManager.getAddonByID(
"amosigned-xpi@tests.mozilla.org"
);
await readyPromise;
// This addon should no longer have private browsing permission.
let policy = WebExtensionPolicy.getByID(addon.id);
ok(!policy.privateBrowsingAllowed, "private browsing permission removed");
await addon.uninstall();
await BrowserTestUtils.closeWindow(win);
});
it("should fail to install an MV3 add-on without matching install_origins when extensions.install_origins.enabled=true", async function test_mv3_installOrigins_disallowed_with_unified_extensions() {
await SpecialPowers.pushPrefEnv({
set: [
// Disable signature check because we load an unsigned MV3 extension.
["xpinstall.signatures.required", false],
["extensions.install_origins.enabled", true],
],
});
let win = await BrowserTestUtils.openNewBrowserWindow();
await SimpleTest.promiseFocus(win);
let notificationPromise = waitForNotification(
"addon-install-failed",
1,
"unified-extensions-button",
win
);
// This XPI does not have any `install_origins` in its manifest.
let xpiURL = TESTROOT + "unsigned_mv3.xpi";
await BrowserTestUtils.openNewForegroundTab(
win.gBrowser,
TESTROOT + "navigate.html?" + xpiURL
);
await notificationPromise;
await BrowserTestUtils.closeWindow(win);
await SpecialPowers.popPrefEnv();
});
it("should allow installing an MV3 add-on without matching install_origins when extensions.install_origins.enabled=false", async function test_mv3_installOrigins_allowed_with_unified_extensions() {
await SpecialPowers.pushPrefEnv({
set: [
// Disable signature check because we load an unsigned MV3 extension.
["xpinstall.signatures.required", false],
// When this pref is disabled, install should be possible.
["extensions.install_origins.enabled", false],
],
});
let win = await BrowserTestUtils.openNewBrowserWindow();
await SimpleTest.promiseFocus(win);
let notificationPromise = waitForNotification(
"addon-install-blocked",
1,
"unified-extensions-button",
win
);
// This XPI does not have any `install_origins` in its manifest.
let xpiURL = TESTROOT + "unsigned_mv3.xpi";
await BrowserTestUtils.openNewForegroundTab(
win.gBrowser,
TESTROOT + "navigate.html?" + xpiURL
);
let panel = await notificationPromise;
let closePromise = waitForNotificationClose(win);
// hide the panel (this simulates the user dismissing it)
panel.hidePopup();
await closePromise;
await BrowserTestUtils.closeWindow(win);
await SpecialPowers.popPrefEnv();
});
});