diff --git a/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd b/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd
index 65d1504bbb50..022ac95b7983 100644
--- a/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd
+++ b/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd
@@ -39,6 +39,7 @@
to the add-on name for extensions that are not webextensions, which
will stop working in Firefox 57. -->
+
diff --git a/toolkit/locales/en-US/toolkit/about/aboutAddons.ftl b/toolkit/locales/en-US/toolkit/about/aboutAddons.ftl
index 148d0ede2dce..c48082f3289e 100644
--- a/toolkit/locales/en-US/toolkit/about/aboutAddons.ftl
+++ b/toolkit/locales/en-US/toolkit/about/aboutAddons.ftl
@@ -126,6 +126,20 @@ detail-update-manual =
.label = Off
.tooltiptext = Don’t automatically install updates
+# Used as a description for the option to allow or block an add-on in private windows.
+detail-private-browsing =
+ .value = Run in Private Windows
+
+detail-private-browsing-description = Extension will work in Private Windows, and have access to your online activities.
+
+detail-private-browsing-on =
+ .label = Allow
+ .tooltiptext = Enable in Private Browsing
+
+detail-private-browsing-off =
+ .label = Don’t Allow
+ .tooltiptext = Disable in Private Browsing
+
detail-home =
.label = Homepage
diff --git a/toolkit/mozapps/extensions/content/extensions.css b/toolkit/mozapps/extensions/content/extensions.css
index 1af940ce874c..ba49d4e46876 100644
--- a/toolkit/mozapps/extensions/content/extensions.css
+++ b/toolkit/mozapps/extensions/content/extensions.css
@@ -107,6 +107,21 @@ row[unsupported="true"] {
display: none;
}
+.addon .privateBrowsing-notice {
+ display: none;
+}
+.addon[privateBrowsing="true"] .privateBrowsing-notice-container {
+ /* 40px is width and margin of .icon-container */
+ margin-inline-start: 40px;
+}
+.addon[privateBrowsing="true"] .privateBrowsing-notice {
+ margin: 4px 0 0;
+ display: inline-block;
+}
+.addon[active="false"] .privateBrowsing-notice {
+ background-color: var(--purple-70-a40);
+}
+
#addons-page:not([warning]) #list-view > .global-warning-container {
display: none;
}
diff --git a/toolkit/mozapps/extensions/content/extensions.js b/toolkit/mozapps/extensions/content/extensions.js
index fd1785051880..72282d3d9ee7 100644
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -20,6 +20,8 @@ ChromeUtils.defineModuleGetter(this, "Extension",
"resource://gre/modules/Extension.jsm");
ChromeUtils.defineModuleGetter(this, "ExtensionParent",
"resource://gre/modules/ExtensionParent.jsm");
+ChromeUtils.defineModuleGetter(this, "ExtensionPermissions",
+ "resource://gre/modules/ExtensionPermissions.jsm");
ChromeUtils.defineModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
ChromeUtils.defineModuleGetter(this, "Preferences",
@@ -34,6 +36,9 @@ XPCOMUtils.defineLazyPreferenceGetter(this, "WEBEXT_PERMISSION_PROMPTS",
XPCOMUtils.defineLazyPreferenceGetter(this, "XPINSTALL_ENABLED",
"xpinstall.enabled", true);
+XPCOMUtils.defineLazyPreferenceGetter(this, "allowPrivateBrowsingByDefault",
+ "extensions.allowPrivateBrowsingByDefault", true);
+
XPCOMUtils.defineLazyPreferenceGetter(this, "SUPPORT_URL", "app.support.baseURL",
"", null, val => Services.urlFormatter.formatURL(val));
@@ -2634,13 +2639,13 @@ var gListView = {
},
};
-
var gDetailView = {
node: null,
_addon: null,
_loadingTimer: null,
_autoUpdate: null,
isRoot: false,
+ restartingAddon: false,
initialize() {
this.node = document.getElementById("detail-view");
@@ -2651,6 +2656,36 @@ var gDetailView = {
this._autoUpdate.addEventListener("command", () => {
this._addon.applyBackgroundUpdates = this._autoUpdate.value;
}, true);
+
+ document.getElementById("detail-private-browsing-learnmore-link")
+ .setAttribute("href", SUPPORT_URL + "extensions-pb");
+
+ this._privateBrowsing = document.getElementById("detail-privateBrowsing");
+ this._privateBrowsing.addEventListener("command", async () => {
+ let addon = this._addon;
+ let policy = WebExtensionPolicy.getByID(addon.id);
+ let extension = policy && policy.extension;
+
+ let perms = {permissions: ["internal:privateBrowsingAllowed"], origins: []};
+ if (this._privateBrowsing.value == "1") {
+ await ExtensionPermissions.add(addon.id, perms, extension);
+ } else {
+ await ExtensionPermissions.remove(addon.id, perms, extension);
+ }
+
+ // Reload the extension if it is already enabled. This ensures any change
+ // on the private browsing permission is properly handled.
+ if (addon.isActive) {
+ try {
+ this.restartingAddon = true;
+ await addon.reload();
+ } finally {
+ this.restartingAddon = false;
+ this.updateState();
+ this._updateView(addon, false);
+ }
+ }
+ }, true);
},
shutdown() {
@@ -2661,7 +2696,12 @@ var gDetailView = {
this.onPropertyChanged(["applyBackgroundUpdates"]);
},
- _updateView(aAddon, aIsRemote, aScrollToPreferences) {
+ async _updateView(aAddon, aIsRemote, aScrollToPreferences) {
+ // Skip updates to avoid flickering while restarting the addon.
+ if (this.restartingAddon) {
+ return;
+ }
+
setSearchLabel(aAddon.type);
// Set the preview image for themes, if available.
@@ -2791,6 +2831,25 @@ var gDetailView = {
document.getElementById("detail-findUpdates-btn").hidden = false;
}
+ // Only type = "extension" will ever get privateBrowsingAllowed, other types have
+ // no code that would be affected by the setting. The permission is read directly
+ // from ExtensionPermissions so we can get it whether or not the extension is
+ // currently active.
+ let privateBrowsingRow = document.getElementById("detail-privateBrowsing-row");
+ let privateBrowsingFooterRow = document.getElementById("detail-privateBrowsing-row-footer");
+ if (allowPrivateBrowsingByDefault || aAddon.type != "extension" ||
+ aAddon.incognito == "not_allowed") {
+ this._privateBrowsing.hidden = true;
+ privateBrowsingRow.hidden = true;
+ privateBrowsingFooterRow.hidden = true;
+ } else {
+ let perms = await ExtensionPermissions.get(aAddon.id);
+ this._privateBrowsing.hidden = false;
+ privateBrowsingRow.hidden = false;
+ privateBrowsingFooterRow.hidden = false;
+ this._privateBrowsing.value = perms.permissions.includes("internal:privateBrowsingAllowed") ? "1" : "0";
+ }
+
document.getElementById("detail-prefs-btn").hidden = !aIsRemote &&
!gViewController.commands.cmd_showItemPreferences.isEnabled(aAddon);
@@ -2869,6 +2928,11 @@ var gDetailView = {
},
updateState() {
+ // Skip updates to avoid flickering while restarting the addon.
+ if (this.restartingAddon) {
+ return;
+ }
+
gViewController.updateCommands();
var pending = this._addon.pendingOperations;
diff --git a/toolkit/mozapps/extensions/content/extensions.xml b/toolkit/mozapps/extensions/content/extensions.xml
index 94b9ad675c5e..6fabe5171d99 100644
--- a/toolkit/mozapps/extensions/content/extensions.xml
+++ b/toolkit/mozapps/extensions/content/extensions.xml
@@ -686,6 +686,9 @@
+
+
+
@@ -902,6 +905,13 @@
this.setAttribute("legacy", legacyWarning);
document.getAnonymousElementByAttribute(this, "anonid", "legacy").href = SUPPORT_URL + "webextensions";
+ if (!allowPrivateBrowsingByDefault) {
+ ExtensionPermissions.get(this.mAddon.id).then((perms) => {
+ let allowed = perms.permissions.includes("internal:privateBrowsingAllowed");
+ this.setAttribute("privateBrowsing", allowed);
+ });
+ }
+
if (!("applyBackgroundUpdates" in this.mAddon) ||
(this.mAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_DISABLE ||
(this.mAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_DEFAULT &&
diff --git a/toolkit/mozapps/extensions/content/extensions.xul b/toolkit/mozapps/extensions/content/extensions.xul
index d1aef5e1274a..8943ebe4a8b1 100644
--- a/toolkit/mozapps/extensions/content/extensions.xul
+++ b/toolkit/mozapps/extensions/content/extensions.xul
@@ -536,6 +536,22 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
index 817d470cdfdc..c852dc37ba08 100644
--- a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
@@ -108,7 +108,7 @@ const PROP_JSON_FIELDS = ["id", "syncGUID", "version", "type",
"softDisabled", "foreignInstall",
"strictCompatibility", "locales", "targetApplications",
"targetPlatforms", "signedState",
- "seen", "dependencies",
+ "seen", "dependencies", "incognito",
"userPermissions", "icons", "iconURL",
"blocklistState", "blocklistURL", "startupData",
"previewImage", "hidden", "installTelemetryInfo"];
@@ -732,6 +732,10 @@ AddonWrapper = class {
return addon.optionsBrowserStyle;
}
+ get incognito() {
+ return addonFor(this).incognito;
+ }
+
async getBlocklistURL() {
return addonFor(this).blocklistURL;
}
diff --git a/toolkit/mozapps/extensions/internal/XPIInstall.jsm b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
index c3c0dd4b1223..e9e056425416 100644
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -434,6 +434,7 @@ async function loadManifestFromWebManifest(aUri, aPackage) {
addon.dependencies = Object.freeze(Array.from(extension.dependencies));
addon.startupData = extension.startupData;
addon.hidden = manifest.hidden;
+ addon.incognito = manifest.incognito;
if (addon.type === "theme" && await aPackage.hasResource("preview.png")) {
addon.previewImage = "preview.png";
diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
index 9950c34bff0f..5cb3e3ee3892 100644
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -104,7 +104,7 @@ const XPI_PERMISSION = "install";
const XPI_SIGNATURE_CHECK_PERIOD = 24 * 60 * 60;
-const DB_SCHEMA = 28;
+const DB_SCHEMA = 29;
function encoded(strings, ...values) {
let result = [];
diff --git a/toolkit/mozapps/extensions/test/browser/browser.ini b/toolkit/mozapps/extensions/test/browser/browser.ini
index 233231769a80..7e2db860aac1 100644
--- a/toolkit/mozapps/extensions/test/browser/browser.ini
+++ b/toolkit/mozapps/extensions/test/browser/browser.ini
@@ -107,6 +107,7 @@ skip-if = verify
[browser_webapi_theme.js]
[browser_webapi_uninstall.js]
[browser_webext_icon.js]
+[browser_webext_incognito.js]
[browser_webext_options.js]
tags = webextensions
skip-if = os == 'linux' || (os == 'mac' && debug) # bug 1483347
diff --git a/toolkit/mozapps/extensions/test/browser/browser_details.js b/toolkit/mozapps/extensions/test/browser/browser_details.js
index 0289d0b68b8f..f2ce57c16dcf 100644
--- a/toolkit/mozapps/extensions/test/browser/browser_details.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_details.js
@@ -75,6 +75,18 @@ async function test() {
contributionAmount: null,
updateDate: gDate,
permissions: 0,
+ }, {
+ id: "addon-theme@tests.mozilla.org",
+ name: "Test add-on theme",
+ version: "2.3",
+ description: "Short description",
+ creator: { name: "Mozilla", url: null },
+ type: "theme",
+ iconURL: "chrome://foo/skin/icon.png",
+ contributionURL: "http://foo.com",
+ contributionAmount: null,
+ updateDate: gDate,
+ permissions: 0,
}, {
id: "addon3@tests.mozilla.org",
name: "Test add-on 3",
@@ -143,8 +155,9 @@ async function end_test() {
}
// Opens and tests the details view for add-on 2
-add_test(function() {
- open_details("addon2@tests.mozilla.org", "extension", function() {
+add_test(async function() {
+ await SpecialPowers.pushPrefEnv({set: [["extensions.allowPrivateBrowsingByDefault", false]]});
+ open_details("addon2@tests.mozilla.org", "extension", async () => {
is(get("detail-name").textContent, "Test add-on 2", "Name should be correct");
is_element_visible(get("detail-version"), "Version should not be hidden");
is(get("detail-version").value, "2.2", "Version should be correct");
@@ -163,6 +176,10 @@ add_test(function() {
is_element_visible(get("detail-dateUpdated"), "Update date should not be hidden");
is(get("detail-dateUpdated").value, formatDate(gDate), "Update date should be correct");
+ is_element_visible(get("detail-privateBrowsing-row"), "Private browsing should not be hidden");
+ is_element_visible(get("detail-privateBrowsing-row-footer"), "Private browsing footer should not be hidden");
+ is(get("detail-privateBrowsing").value, "0", "Private browsing should be off");
+
is_element_hidden(get("detail-rating-row"), "Rating should be hidden");
is_element_hidden(get("detail-homepage-row"), "Homepage should not be visible");
@@ -181,6 +198,28 @@ add_test(function() {
is_element_hidden(get("detail-error-link"), "Error link should be hidden");
is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+ await SpecialPowers.popPrefEnv();
+ run_next_test();
+ });
+});
+
+
+// Opens and tests the details view for add-on theme
+add_test(async function() {
+ // This is a duplicate of addon-2, so we're only testing that private browsing is
+ // not visible.
+ await SpecialPowers.pushPrefEnv({set: [["extensions.allowPrivateBrowsingByDefault", false]]});
+ open_details("addon-theme@tests.mozilla.org", "theme", async () => {
+ is(get("detail-name").textContent, "Test add-on theme", "Name should be correct");
+ is_element_visible(get("detail-version"), "Version should not be hidden");
+ is(get("detail-version").value, "2.3", "Version should be correct");
+ is(get("detail-icon").src, "chrome://foo/skin/icon.png", "Icon should be correct");
+
+ is_element_hidden(get("detail-privateBrowsing-row"), "Private browsing should be hidden");
+ is_element_hidden(get("detail-privateBrowsing-row-footer"), "Private browsing footer should be hidden");
+ is(get("detail-privateBrowsing").value, "0", "Private browsing should be off");
+
+ await SpecialPowers.popPrefEnv();
run_next_test();
});
});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webext_incognito.js b/toolkit/mozapps/extensions/test/browser/browser_webext_incognito.js
new file mode 100644
index 000000000000..caf01b3ba37d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webext_incognito.js
@@ -0,0 +1,124 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const {ExtensionPermissions} = ChromeUtils.import("resource://gre/modules/ExtensionPermissions.jsm", {});
+
+var gManagerWindow;
+
+function get_test_items() {
+ var items = {};
+
+ for (let item of gManagerWindow.document.getElementById("addon-list").childNodes) {
+ items[item.mAddon.id] = item;
+ }
+
+ return items;
+}
+
+function get(aId) {
+ return gManagerWindow.document.getElementById(aId);
+}
+
+async function hasPrivateAllowed(id) {
+ let perms = await ExtensionPermissions.get(id);
+ return perms.permissions.length == 1 &&
+ perms.permissions[0] == "internal:privateBrowsingAllowed";
+}
+
+add_task(async function test_addon() {
+ await SpecialPowers.pushPrefEnv({set: [["extensions.allowPrivateBrowsingByDefault", false]]});
+
+ let addons = new Map([
+ ["@test-default", {
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: {id: "@test-default"},
+ },
+ },
+ }],
+ ["@test-override", {
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: {id: "@test-override"},
+ },
+ },
+ incognitoOverride: "spanning",
+ }],
+ ["@test-override-permanent", {
+ useAddonManager: "permanent",
+ manifest: {
+ applications: {
+ gecko: {id: "@test-override-permanent"},
+ },
+ },
+ incognitoOverride: "spanning",
+ }],
+ ["@test-not-allowed", {
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: {id: "@test-not-allowed"},
+ },
+ incognito: "not_allowed",
+ },
+ }],
+ ]);
+ let extensions = [];
+ for (let definition of addons.values()) {
+ let extension = ExtensionTestUtils.loadExtension(definition);
+ extensions.push(extension);
+ await extension.startup();
+ }
+
+ gManagerWindow = await open_manager("addons://list/extension");
+ let doc = gManagerWindow.document;
+ let items = get_test_items();
+ for (let [id, definition] of addons.entries()) {
+ ok(items[id], `${id} listed`);
+ let badge = doc.getAnonymousElementByAttribute(items[id], "anonid", "privateBrowsing");
+ if (definition.incognitoOverride == "spanning") {
+ is_element_visible(badge, `private browsing badge is visible`);
+ } else {
+ is_element_hidden(badge, `private browsing badge is hidden`);
+ }
+ }
+ await close_manager(gManagerWindow);
+
+ for (let [id, definition] of addons.entries()) {
+ gManagerWindow = await open_manager("addons://detail/" + encodeURIComponent(id));
+ ok(true, `==== ${id} detail opened`);
+ if (definition.manifest.incognito == "not_allowed") {
+ is_element_hidden(get("detail-privateBrowsing-row"), "Private browsing should be hidden");
+ is_element_hidden(get("detail-privateBrowsing-row-footer"), "Private browsing footer should be hidden");
+ ok(!await hasPrivateAllowed(id), "Private browsing permission not set");
+ } else {
+ is_element_visible(get("detail-privateBrowsing-row"), "Private browsing should be visible");
+ is_element_visible(get("detail-privateBrowsing-row-footer"), "Private browsing footer should be visible");
+ let privateBrowsing = gManagerWindow.document.getElementById("detail-privateBrowsing");
+ if (definition.incognitoOverride == "spanning") {
+ is(privateBrowsing.value, "1", "Private browsing should be on");
+ ok(await hasPrivateAllowed(id), "Private browsing permission set");
+ EventUtils.synthesizeMouseAtCenter(privateBrowsing.lastChild, { clickCount: 1 }, gManagerWindow);
+ await TestUtils.waitForCondition(() => privateBrowsing.value == "0");
+ is(privateBrowsing.value, "0", "Private browsing should be off");
+ ok(!await hasPrivateAllowed(id), "Private browsing permission removed");
+ } else {
+ is(privateBrowsing.value, "0", "Private browsing should be off");
+ ok(!await hasPrivateAllowed(id), "Private browsing permission not set");
+ EventUtils.synthesizeMouseAtCenter(privateBrowsing.firstChild, { clickCount: 1 }, gManagerWindow);
+ await TestUtils.waitForCondition(() => privateBrowsing.value == "1");
+ is(privateBrowsing.value, "1", "Private browsing should be on");
+ ok(await hasPrivateAllowed(id), "Private browsing permission set");
+ }
+ }
+ await close_manager(gManagerWindow);
+ }
+
+ for (let extension of extensions) {
+ await extension.unload();
+ }
+ Services.prefs.clearUserPref("extensions.allowPrivateBrowsingByDefault");
+});
diff --git a/toolkit/themes/shared/extensions/extensions.inc.css b/toolkit/themes/shared/extensions/extensions.inc.css
index ff3733d3909a..d2b514a91140 100644
--- a/toolkit/themes/shared/extensions/extensions.inc.css
+++ b/toolkit/themes/shared/extensions/extensions.inc.css
@@ -399,6 +399,19 @@ button.warning {
-moz-box-flex: 1;
}
+.privateBrowsing-notice {
+ background-color: var(--purple-70);
+ color: #fff;
+ margin: 4px 0 0;
+ padding: 4px 5px 3px;
+ font-size: 0.9rem;
+ font-weight: 600;
+ -moz-user-focus: ignore;
+ transition-property: color, background-color;
+ transition-timing-function: var(--animation-curve);
+ transition-duration: 150ms;
+}
+
.legacy-warning {
background-color: #FFE900;
color: #3E2800;
@@ -407,7 +420,7 @@ button.warning {
font-weight: 600;
-moz-user-focus: ignore;
transition-property: color, background-color;
- transition-timing-function: cubic-bezier(.07,.95,0,1);
+ transition-timing-function: var(--animation-curve);
transition-duration: 150ms;
}
@@ -793,7 +806,7 @@ button.warning {
}
.preferences-description {
- font-size: 90.9%;
+ font-size: 1.1rem;
color: graytext;
margin-top: -2px;
margin-inline-start: 2em;
@@ -804,6 +817,18 @@ button.warning {
display: none;
}
+.detail-row-footer {
+ padding-bottom: 6px;
+}
+
+.detail-row-footer > .preferences-description {
+ margin-inline-start: 6px;
+ margin-top: 0;
+ margin-bottom: 0;
+ /* card-width - card-padding - description-margins */
+ width: calc(664px - 32px - 11px);
+ color: var(--grey-60);
+}
/*** creator ***/
diff --git a/toolkit/themes/shared/in-content/common.inc.css b/toolkit/themes/shared/in-content/common.inc.css
index a084e6fc4be1..880ab0a958c3 100644
--- a/toolkit/themes/shared/in-content/common.inc.css
+++ b/toolkit/themes/shared/in-content/common.inc.css
@@ -48,6 +48,9 @@
--in-content-table-border-dark-color: #d1d1d1;
--in-content-table-header-background: #0a84ff;
+ /* The photon animation curve */
+ --animation-curve: cubic-bezier(.07,.95,0,1);
+
--blue-40: #45a1ff;
--blue-40-a10: rgb(69, 161, 255, 0.1);
--blue-50: #0a84ff;
@@ -64,6 +67,8 @@
--grey-90-a30: rgba(12, 12, 13, 0.3);
--grey-90-a40: rgba(12, 12, 13, 0.4);
--grey-90-a50: rgba(12, 12, 13, 0.5);
+ --purple-70: #6200a4;
+ --purple-70-a40: rgba(98, 0, 164, 0.4);
--red-50: #ff0039;
--red-50-a30: rgba(255, 0, 57, 0.3);
--red-60: #d70022;