diff --git a/dom/chrome-webidl/WebExtensionPolicy.webidl b/dom/chrome-webidl/WebExtensionPolicy.webidl index cb88b3648a1a..806e6e991af1 100644 --- a/dom/chrome-webidl/WebExtensionPolicy.webidl +++ b/dom/chrome-webidl/WebExtensionPolicy.webidl @@ -94,6 +94,23 @@ interface WebExtensionPolicy { */ static readonly attribute boolean isExtensionProcess; + /** + * Set based on the manifest.incognito value: + * If "spanning" or "split" will be true. + * If "not_allowed" will be false. + */ + [Pure] + attribute boolean privateBrowsingAllowed; + + /** + * Returns true if the extension can access a window. Access is + * determined by matching the windows private browsing context + * with privateBrowsingMode. This does not, and is not meant to + * handle specific differences between spanning and split mode. + */ + [Affects=Nothing] + boolean canAccessWindow(WindowProxy window); + /** * Returns true if the extension has cross-origin access to the given URI. */ @@ -193,4 +210,6 @@ dictionary WebExtensionInit { DOMString? contentSecurityPolicy = null; sequence? backgroundScripts = null; + + boolean privateBrowsingAllowed = true; }; diff --git a/toolkit/components/extensions/Extension.jsm b/toolkit/components/extensions/Extension.jsm index 18505b12e626..b77c11ec04e0 100644 --- a/toolkit/components/extensions/Extension.jsm +++ b/toolkit/components/extensions/Extension.jsm @@ -636,6 +636,7 @@ class ExtensionData { schemaURLs: null, type: this.type, webAccessibleResources, + privateBrowsingAllowed: manifest.incognito !== "not_allowed", }; if (this.type === "extension") { @@ -1599,6 +1600,14 @@ class Extension extends ExtensionData { return this.manifest.optional_permissions; } + get privateBrowsingAllowed() { + return this.policy.privateBrowsingAllowed; + } + + canAccessWindow(window) { + return this.policy.canAccessWindow(window); + } + // Representation of the extension to send to content // processes. This should include anything the content process might // need. @@ -1615,6 +1624,7 @@ class Extension extends ExtensionData { whiteListedHosts: this.whiteListedHosts.patterns.map(pat => pat.pattern), permissions: this.permissions, optionalPermissions: this.optionalPermissions, + privateBrowsingAllowed: this.privateBrowsingAllowed, }; } @@ -1850,6 +1860,12 @@ class Extension extends ExtensionData { return; } + if (this.addonData && this.addonData.incognitoOverride !== undefined) { + this.policy.privateBrowsingAllowed = this.addonData.incognitoOverride !== "not_allowed"; + } else { + this.policy.privateBrowsingAllowed = this.manifest.incognito !== "not_allowed"; + } + GlobalManager.init(this); this.initSharedData(); diff --git a/toolkit/components/extensions/ExtensionChild.jsm b/toolkit/components/extensions/ExtensionChild.jsm index 3b628537393b..2e96faa710b7 100644 --- a/toolkit/components/extensions/ExtensionChild.jsm +++ b/toolkit/components/extensions/ExtensionChild.jsm @@ -712,6 +712,14 @@ class BrowserExtensionContent extends EventEmitter { return this._manifest; } + get privateBrowsingAllowed() { + return this.policy.privateBrowsingAllowed; + } + + canAccessWindow(window) { + return this.policy.canAccessWindow(window); + } + getAPIManager() { let apiManagers = [ExtensionPageChild.apiManager]; diff --git a/toolkit/components/extensions/ExtensionCommon.jsm b/toolkit/components/extensions/ExtensionCommon.jsm index 745fb9a55a02..259ea7b1bf14 100644 --- a/toolkit/components/extensions/ExtensionCommon.jsm +++ b/toolkit/components/extensions/ExtensionCommon.jsm @@ -445,6 +445,14 @@ class BaseContext { return this.cloneScopePromise || this.cloneScope.Promise; } + get privateBrowsingAllowed() { + return this.extension.privateBrowsingAllowed; + } + + canAccessWindow(window) { + return this.extension.canAccessWindow(window); + } + setContentWindow(contentWindow) { this.innerWindowID = getInnerWindowID(contentWindow); this.messageManager = contentWindow.docShell.messageManager; diff --git a/toolkit/components/extensions/ExtensionTestCommon.jsm b/toolkit/components/extensions/ExtensionTestCommon.jsm index 24d0daada154..6bb53dfed329 100644 --- a/toolkit/components/extensions/ExtensionTestCommon.jsm +++ b/toolkit/components/extensions/ExtensionTestCommon.jsm @@ -368,6 +368,7 @@ var ExtensionTestCommon = class ExtensionTestCommon { resourceURI: jarURI, cleanupFile: file, signedState, + incognitoOverride: data.incognitoOverride, temporarilyInstalled: !!data.temporarilyInstalled, TEST_NO_ADDON_MANAGER: true, }); diff --git a/toolkit/components/extensions/WebExtensionContentScript.h b/toolkit/components/extensions/WebExtensionContentScript.h index 7d856a164cf7..efbb8268147b 100644 --- a/toolkit/components/extensions/WebExtensionContentScript.h +++ b/toolkit/components/extensions/WebExtensionContentScript.h @@ -18,6 +18,7 @@ #include "nsCOMPtr.h" #include "nsCycleCollectionParticipant.h" #include "nsISupports.h" +#include "nsIDocShell.h" #include "nsWrapperCache.h" class nsILoadInfo; @@ -67,6 +68,18 @@ class MOZ_STACK_CLASS DocInfo final { return nullptr; } + already_AddRefed GetLoadContext() const { + nsCOMPtr loadContext; + if (nsPIDOMWindowOuter* window = GetWindow()) { + nsIDocShell* docShell = window->GetDocShell(); + loadContext = do_QueryInterface(docShell); + } else if (nsILoadInfo* loadInfo = GetLoadInfo()) { + nsCOMPtr requestingContext = loadInfo->GetLoadingContext(); + loadContext = do_QueryInterface(requestingContext); + } + return loadContext.forget(); + } + private: void SetURL(const URLInfo& aURL); diff --git a/toolkit/components/extensions/WebExtensionPolicy.cpp b/toolkit/components/extensions/WebExtensionPolicy.cpp index b401a24ed69f..9c66bd471a47 100644 --- a/toolkit/components/extensions/WebExtensionPolicy.cpp +++ b/toolkit/components/extensions/WebExtensionPolicy.cpp @@ -11,7 +11,6 @@ #include "mozilla/AddonManagerWebAPI.h" #include "mozilla/ResultExtensions.h" #include "nsEscape.h" -#include "nsIDocShell.h" #include "nsIObserver.h" #include "nsISubstitutingProtocolHandler.h" #include "nsNetUtil.h" @@ -133,7 +132,8 @@ WebExtensionPolicy::WebExtensionPolicy(GlobalObject& aGlobal, mName(aInit.mName), mContentSecurityPolicy(aInit.mContentSecurityPolicy), mLocalizeCallback(aInit.mLocalizeCallback), - mPermissions(new AtomSet(aInit.mPermissions)) { + mPermissions(new AtomSet(aInit.mPermissions)), + mPrivateBrowsingAllowed(aInit.mPrivateBrowsingAllowed) { if (!ParseGlobs(aGlobal, aInit.mWebAccessibleResources, mWebAccessiblePaths, aRv)) { return; @@ -440,6 +440,21 @@ void WebExtensionPolicy::GetContentScripts( aScripts.AppendElements(mContentScripts); } +bool WebExtensionPolicy::CanAccessContext(nsILoadContext* aContext) const { + MOZ_ASSERT(aContext); + return mPrivateBrowsingAllowed || !aContext->UsePrivateBrowsing(); +} + +bool WebExtensionPolicy::CanAccessWindow(nsPIDOMWindowOuter* aWindow) const { + if (mPrivateBrowsingAllowed) { + return true; + } + // match browsing mode with policy + nsIDocShell* docShell = aWindow->GetDocShell(); + nsCOMPtr loadContext = do_QueryInterface(docShell); + return !(loadContext && loadContext->UsePrivateBrowsing()); +} + NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(WebExtensionPolicy, mParent, mLocalizeCallback, mHostPermissions, mWebAccessiblePaths, mContentScripts) @@ -545,6 +560,12 @@ bool MozDocumentMatcher::Matches(const DocInfo& aDoc) const { } } + // match browsing mode with policy + nsCOMPtr loadContext = aDoc.GetLoadContext(); + if (loadContext && mExtension && !mExtension->CanAccessContext(loadContext)) { + return false; + } + if (!mMatchAboutBlank && aDoc.URL().InheritsPrincipal()) { return false; } diff --git a/toolkit/components/extensions/WebExtensionPolicy.h b/toolkit/components/extensions/WebExtensionPolicy.h index cca37baad0d2..f82a2643df5e 100644 --- a/toolkit/components/extensions/WebExtensionPolicy.h +++ b/toolkit/components/extensions/WebExtensionPolicy.h @@ -121,6 +121,15 @@ class WebExtensionPolicy final : public nsISupports, bool Active() const { return mActive; } void SetActive(bool aActive, ErrorResult& aRv); + bool PrivateBrowsingAllowed() const { return mPrivateBrowsingAllowed; } + void SetPrivateBrowsingAllowed(bool aPrivateBrowsingAllowed) { + mPrivateBrowsingAllowed = aPrivateBrowsingAllowed; + }; + + bool CanAccessContext(nsILoadContext* aContext) const; + + bool CanAccessWindow(nsPIDOMWindowOuter* aWindow) const; + static void GetActiveExtensions( dom::GlobalObject& aGlobal, nsTArray>& aResults); @@ -173,6 +182,8 @@ class WebExtensionPolicy final : public nsISupports, RefPtr mHostPermissions; MatchGlobSet mWebAccessiblePaths; + bool mPrivateBrowsingAllowed = false; + dom::Nullable> mBackgroundScripts; nsTArray> mContentScripts; diff --git a/toolkit/components/extensions/extension-process-script.js b/toolkit/components/extensions/extension-process-script.js index 83fae9b16729..586ffe3f5574 100644 --- a/toolkit/components/extensions/extension-process-script.js +++ b/toolkit/components/extensions/extension-process-script.js @@ -145,6 +145,8 @@ ExtensionManager = { allowedOrigins: extension.whiteListedHosts, webAccessibleResources: extension.webAccessibleResources, + privateBrowsingAllowed: extension.privateBrowsingAllowed, + contentSecurityPolicy: extension.contentSecurityPolicy, localizeCallback, diff --git a/toolkit/components/extensions/parent/ext-extension.js b/toolkit/components/extensions/parent/ext-extension.js index 7afc828d9564..a4b54c0eed7d 100644 --- a/toolkit/components/extensions/parent/ext-extension.js +++ b/toolkit/components/extensions/parent/ext-extension.js @@ -9,11 +9,11 @@ this.extension = class extends ExtensionAPI { }, isAllowedIncognitoAccess() { - return Promise.resolve(true); + return context.privateBrowsingAllowed; }, isAllowedFileSchemeAccess() { - return Promise.resolve(false); + return false; }, }, }; diff --git a/toolkit/components/extensions/schemas/manifest.json b/toolkit/components/extensions/schemas/manifest.json index aef24b5e1074..10238018e401 100644 --- a/toolkit/components/extensions/schemas/manifest.json +++ b/toolkit/components/extensions/schemas/manifest.json @@ -102,8 +102,8 @@ "incognito": { "type": "string", "enum": ["spanning"], - "optional": true, - "onError": "warn" + "default": "spanning", + "optional": true }, "background": { diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_extension.js b/toolkit/components/extensions/test/xpcshell/test_ext_extension.js index 97ca3c9ee488..5b9572db50a4 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_extension.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_extension.js @@ -12,7 +12,6 @@ add_task(async function test_is_allowed_incognito_access() { let extension = ExtensionTestUtils.loadExtension({ background, - manifest: {}, }); await extension.startup(); @@ -20,6 +19,24 @@ add_task(async function test_is_allowed_incognito_access() { await extension.unload(); }); +add_task(async function test_is_denied_incognito_access() { + async function background() { + let allowed = await browser.extension.isAllowedIncognitoAccess(); + + browser.test.assertEq(false, allowed, "isAllowedIncognitoAccess is false"); + browser.test.notifyPass("isNotAllowedIncognitoAccess"); + } + + let extension = ExtensionTestUtils.loadExtension({ + background, + incognitoOverride: "not_allowed", + }); + + await extension.startup(); + await extension.awaitFinish("isNotAllowedIncognitoAccess"); + await extension.unload(); +}); + add_task(async function test_in_incognito_context_false() { function background() { browser.test.assertEq(false, browser.extension.inIncognitoContext, "inIncognitoContext returned false"); @@ -28,7 +45,6 @@ add_task(async function test_in_incognito_context_false() { let extension = ExtensionTestUtils.loadExtension({ background, - manifest: {}, }); await extension.startup(); @@ -46,7 +62,6 @@ add_task(async function test_is_allowed_file_scheme_access() { let extension = ExtensionTestUtils.loadExtension({ background, - manifest: {}, }); await extension.startup(); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_incognito.js b/toolkit/components/extensions/test/xpcshell/test_ext_incognito.js new file mode 100644 index 000000000000..947772f701cd --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_incognito.js @@ -0,0 +1,30 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(async function test_extension_incognito_spanning() { + let wrapper = ExtensionTestUtils.loadExtension({}); + await wrapper.startup(); + + let extension = wrapper.extension; + let data = extension.serialize(); + equal(data.privateBrowsingAllowed, true, "Should have privateBrowsingAllowed in serialized extension"); + equal(extension.privateBrowsingAllowed, true, "Should have privateBrowsingAllowed"); + equal(extension.policy.privateBrowsingAllowed, true, "Should have privateBrowsingAllowed on policy"); + await wrapper.unload(); +}); + +// This tests setting an extension to private-only mode which is useful for testing. +add_task(async function test_extension_incognito_test_mode() { + let wrapper = ExtensionTestUtils.loadExtension({ + incognitoOverride: "not_allowed", + }); + await wrapper.startup(); + + let extension = wrapper.extension; + let data = extension.serialize(); + equal(data.privateBrowsingAllowed, false, "Should not have privateBrowsingAllowed in serialized extension"); + equal(extension.privateBrowsingAllowed, false, "Should not have privateBrowsingAllowed"); + equal(extension.policy.privateBrowsingAllowed, false, "Should not have privateBrowsingAllowed on policy"); + await wrapper.unload(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js index 12e07ad5af81..c8adcf864b63 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_manifest_incognito.js @@ -14,14 +14,25 @@ add_task(async function test_manifest_incognito() { "spanning", "Should have the expected incognito string"); + normalized = await ExtensionTestUtils.normalizeManifest({ + "incognito": "not_allowed", + }); + + equal(normalized.error, + 'Error processing incognito: Invalid enumeration value "not_allowed"', + "Should have an error"); + Assert.deepEqual(normalized.errors, [], "Should not have a warning"); + equal(normalized.value, undefined, + "Invalid incognito string should be undefined"); + normalized = await ExtensionTestUtils.normalizeManifest({ "incognito": "split", }); - equal(normalized.error, undefined, "Should not have an error"); - Assert.deepEqual(normalized.errors, - ['Error processing incognito: Invalid enumeration value "split"'], - "Should have the expected warning"); - equal(normalized.value.incognito, null, - "Invalid incognito string should be omitted"); + equal(normalized.error, + 'Error processing incognito: Invalid enumeration value "split"', + "Should have an error"); + Assert.deepEqual(normalized.errors, [], "Should not have a warning"); + equal(normalized.value, undefined, + "Invalid incognito string should be undefined"); }); diff --git a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini index 8b013bb42947..b5742483adb4 100644 --- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini +++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini @@ -60,6 +60,7 @@ skip-if = os == "android" # checking for telemetry needs to be updated: 1384923 [test_ext_extension_startup_telemetry.js] [test_ext_geturl.js] [test_ext_idle.js] +[test_ext_incognito.js] [test_ext_localStorage.js] [test_ext_management.js] skip-if = (os == "win" && !debug) #Bug 1419183 disable on Windows