diff --git a/browser/base/content/browser-siteIdentity.js b/browser/base/content/browser-siteIdentity.js index b329cbfab5ed..ed8a10173e5a 100644 --- a/browser/base/content/browser-siteIdentity.js +++ b/browser/base/content/browser-siteIdentity.js @@ -966,7 +966,15 @@ var gIdentityHandler = { let hasBlockedPopupIndicator = false; for (let permission of permissions) { + if (permission.id == "storage-access") { + // Ignore storage access permissions here, they are made visible inside + // the Content Blocking UI. + continue; + } let item = this._createPermissionItem(permission); + if (!item) { + continue; + } this._permissionList.appendChild(item); if (permission.id == "popup" && @@ -1035,7 +1043,11 @@ var gIdentityHandler = { let nameLabel = document.createXULElement("label"); nameLabel.setAttribute("flex", "1"); nameLabel.setAttribute("class", "identity-popup-permission-label"); - nameLabel.textContent = SitePermissions.getPermissionLabel(aPermission.id); + let label = SitePermissions.getPermissionLabel(aPermission.id); + if (label === null) { + return null; + } + nameLabel.textContent = label; let nameLabelId = "identity-popup-permission-label-" + aPermission.id; nameLabel.setAttribute("id", nameLabelId); diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul index 41c8150d0f20..d42a4f6ece04 100644 --- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -977,6 +977,8 @@ xmlns="http://www.w3.org/1999/xhtml" tooltiptext="&urlbar.midiNotificationAnchor.tooltip;"/> + diff --git a/browser/base/content/pageinfo/permissions.js b/browser/base/content/pageinfo/permissions.js index 58666ab74203..15b45c02e69c 100644 --- a/browser/base/content/pageinfo/permissions.js +++ b/browser/base/content/pageinfo/permissions.js @@ -12,11 +12,13 @@ var gPermPrincipal; var gUsageRequest; // Array of permissionIDs sorted alphabetically by label. -var gPermissions = SitePermissions.listPermissions().sort((a, b) => { - let firstLabel = SitePermissions.getPermissionLabel(a); - let secondLabel = SitePermissions.getPermissionLabel(b); - return firstLabel.localeCompare(secondLabel); -}); +var gPermissions = SitePermissions.listPermissions() + .filter(p => SitePermissions.getPermissionLabel(p) != null) + .sort((a, b) => { + let firstLabel = SitePermissions.getPermissionLabel(a); + let secondLabel = SitePermissions.getPermissionLabel(b); + return firstLabel.localeCompare(secondLabel); + }); var permissionObserver = { observe(aSubject, aTopic, aData) { diff --git a/browser/base/content/popup-notifications.inc b/browser/base/content/popup-notifications.inc index b943d33110cf..e16207367b70 100644 --- a/browser/base/content/popup-notifications.inc +++ b/browser/base/content/popup-notifications.inc @@ -103,3 +103,18 @@ + + diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js index e523748c7130..f0cf94f621d4 100644 --- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -3135,6 +3135,9 @@ const ContentPermissionIntegration = { case "autoplay-media": { return new PermissionUI.AutoplayPermissionPrompt(request); } + case "storage-access": { + return new PermissionUI.StorageAccessPermissionPrompt(request); + } } return undefined; }, diff --git a/browser/locales/en-US/chrome/browser/browser.dtd b/browser/locales/en-US/chrome/browser/browser.dtd index 37f8c1d90627..e106ce7ff53b 100644 --- a/browser/locales/en-US/chrome/browser/browser.dtd +++ b/browser/locales/en-US/chrome/browser/browser.dtd @@ -282,6 +282,7 @@ These should match what Safari and other Apple applications use on OS X Lion. -- + diff --git a/browser/locales/en-US/chrome/browser/browser.properties b/browser/locales/en-US/chrome/browser/browser.properties index 6955d249c7de..680ff75fa9ea 100644 --- a/browser/locales/en-US/chrome/browser/browser.properties +++ b/browser/locales/en-US/chrome/browser/browser.properties @@ -965,6 +965,24 @@ autoplay.messageWithFile = Will you allow this file to autoplay media with sound # popup panels, including the sliding subviews of the main menu. panel.back = Back +storageAccess.Allow.label = Allow Access +storageAccess.Allow.accesskey = A +storageAccess.AllowOnAnySite.label = Allow access on any site +storageAccess.AllowOnAnySite.accesskey = w +storageAccess.DontAllow.label = Block Access +storageAccess.DontAllow.accesskey = B +# LOCALIZATION NOTE (storageAccess.message): +# %1$S is the name of the site URL (www.site1.example) trying to track the user's activity. +# %2$S is the name of the site URL (www.site2.example) that the user is visiting. This is the same domain name displayed in the address bar. +storageAccess.message = Will you give %1$S access to track your browsing activity on %2$S? +# LOCALIZATION NOTE (storageAccess.description.label): +# %1$S is the name of the site URL (www.site1.example) trying to track the user's activity. +# %2$S will be replaced with the localized version of storageAccess.description.learnmore. This text will be converted into a hyper-link linking to the SUMO page explaining the concept of third-party trackers. +storageAccess.description.label = You may want to block %1$S on this site if you don’t recognize or trust it. Learn more about %2$S +# LOCALIZATION NOTE (storageAccess.description.learnmore): +# The value of this string is embedded inside storageAccess.description.label. See the localization note for storageAccess.description.label. +storageAccess.description.learnmore = third-party trackers + confirmationHint.sendToDevice.label = Sent! confirmationHint.sendToDeviceOffline.label = Queued (offline) confirmationHint.copyURL.label = Copied to clipboard! diff --git a/browser/locales/en-US/chrome/browser/sitePermissions.properties b/browser/locales/en-US/chrome/browser/sitePermissions.properties index c6371f3baf1c..2f17abec4d34 100644 --- a/browser/locales/en-US/chrome/browser/sitePermissions.properties +++ b/browser/locales/en-US/chrome/browser/sitePermissions.properties @@ -44,4 +44,4 @@ permission.persistent-storage.label = Store Data in Persistent Storage permission.canvas.label = Extract Canvas Data permission.flash-plugin.label = Run Adobe Flash permission.midi.label = Access MIDI Devices -permission.midi-sysex.label = Access MIDI Devices with SysEx Support \ No newline at end of file +permission.midi-sysex.label = Access MIDI Devices with SysEx Support diff --git a/browser/modules/PermissionUI.jsm b/browser/modules/PermissionUI.jsm index 8bd9767af339..b84d02fb2970 100644 --- a/browser/modules/PermissionUI.jsm +++ b/browser/modules/PermissionUI.jsm @@ -257,8 +257,14 @@ var PermissionPromptPrototype = { onBeforeShow() {}, /** - * If the prompt was be shown to the user, this callback will - * be called just after its been hidden. + * If the prompt was shown to the user, this callback will be called just + * after it's been shown. + */ + onShown() {}, + + /** + * If the prompt was shown to the user, this callback will be called just + * after it's been hidden. */ onAfterShow() {}, @@ -423,6 +429,10 @@ var PermissionPromptPrototype = { if (topic == "swapping") { return true; } + // The prompt has been shown, notify the PermissionUI. + if (topic == "shown") { + this.onShown(); + } // The prompt has been removed, notify the PermissionUI. if (topic == "removed") { this.onAfterShow(); @@ -472,8 +482,8 @@ var PermissionPromptForRequestPrototype = { this.request.cancel(); }, - allow() { - this.request.allow(); + allow(choices) { + this.request.allow(choices); }, }; @@ -905,3 +915,99 @@ AutoplayPermissionPrompt.prototype = { }; PermissionUI.AutoplayPermissionPrompt = AutoplayPermissionPrompt; + +function StorageAccessPermissionPrompt(request) { + this.request = request; +} + +StorageAccessPermissionPrompt.prototype = { + __proto__: PermissionPromptForRequestPrototype, + + get usePermissionManager() { + return false; + }, + + get permissionKey() { + // Make sure this name is unique per each third-party tracker + return "storage-access-" + this.principal.origin; + }, + + get popupOptions() { + return { + displayURI: false, + name: this.principal.URI.hostPort, + secondName: this.topLevelPrincipal.URI.hostPort, + }; + }, + + onShown() { + let document = this.browser.ownerDocument; + let label = + gBrowserBundle.formatStringFromName("storageAccess.description.label", + [this.request.principal.URI.hostPort, "<>"], 2); + let parts = label.split("<>"); + if (parts.length == 1) { + parts.push(""); + } + let map = { + "storage-access-perm-label": parts[0], + "storage-access-perm-learnmore": + gBrowserBundle.GetStringFromName("storageAccess.description.learnmore"), + "storage-access-perm-endlabel": parts[1], + }; + for (let id in map) { + let str = map[id]; + document.getElementById(id).textContent = str; + } + let learnMoreURL = + Services.urlFormatter.formatURLPref("app.support.baseURL") + "third-party-cookies"; + document.getElementById("storage-access-perm-learnmore") + .href = learnMoreURL; + }, + + get notificationID() { + return "storage-access"; + }, + + get anchorID() { + return "storage-access-notification-icon"; + }, + + get message() { + return gBrowserBundle.formatStringFromName("storageAccess.message", ["<>", "<>"], 2); + }, + + get promptActions() { + let self = this; + return [{ + label: gBrowserBundle.GetStringFromName("storageAccess.DontAllow.label"), + accessKey: gBrowserBundle.GetStringFromName("storageAccess.DontAllow.accesskey"), + action: Ci.nsIPermissionManager.DENY_ACTION, + callback(state) { + self.cancel(); + }, + }, + { + label: gBrowserBundle.GetStringFromName("storageAccess.Allow.label"), + accessKey: gBrowserBundle.GetStringFromName("storageAccess.Allow.accesskey"), + action: Ci.nsIPermissionManager.ALLOW_ACTION, + callback(state) { + self.allow({"storage-access": "allow"}); + }, + }, + { + label: gBrowserBundle.GetStringFromName("storageAccess.AllowOnAnySite.label"), + accessKey: gBrowserBundle.GetStringFromName("storageAccess.AllowOnAnySite.accesskey"), + action: Ci.nsIPermissionManager.ALLOW_ACTION, + callback(state) { + self.allow({"storage-access": "allow-on-any-site"}); + }, + }]; + }, + + get topLevelPrincipal() { + return this.request.topLevelPrincipal; + }, +}; + +PermissionUI.StorageAccessPermissionPrompt = StorageAccessPermissionPrompt; diff --git a/browser/modules/SitePermissions.jsm b/browser/modules/SitePermissions.jsm index 003635a3fb4f..3e72127843c6 100644 --- a/browser/modules/SitePermissions.jsm +++ b/browser/modules/SitePermissions.jsm @@ -355,7 +355,7 @@ var SitePermissions = { * (e.g. SitePermissions.ALLOW) * - scope: a constant representing how long the permission will * be kept. - * - label: the localized label + * - label: the localized label, or null if none is available. */ getAllPermissionDetailsForBrowser(browser) { return this.getAllForBrowser(browser).map(({id, scope, state}) => @@ -653,9 +653,18 @@ var SitePermissions = { * @param {string} permissionID * The permission to get the label for. * - * @return {String} the localized label. + * @return {String} the localized label or null if none is available. */ getPermissionLabel(permissionID) { + if (!(permissionID in gPermissionObject)) { + // Permission can't be found. + return null; + } + if ("labelID" in gPermissionObject[permissionID] && + gPermissionObject[permissionID].labelID === null) { + // Permission doesn't support having a label. + return null; + } let labelID = gPermissionObject[permissionID].labelID || permissionID; return gStringBundle.GetStringFromName("permission." + labelID + ".label"); }, @@ -852,6 +861,13 @@ var gPermissionObject = { "midi-sysex": { exactHostMatch: true, }, + + "storage-access": { + labelID: null, + getDefault() { + return SitePermissions.UNKNOWN; + }, + }, }; if (!Services.prefs.getBoolPref("dom.webmidi.enabled")) { diff --git a/browser/modules/test/unit/test_SitePermissions.js b/browser/modules/test/unit/test_SitePermissions.js index 10ead30f3cf8..232e1063497f 100644 --- a/browser/modules/test/unit/test_SitePermissions.js +++ b/browser/modules/test/unit/test_SitePermissions.js @@ -12,7 +12,7 @@ const MIDI_ENABLED = Services.prefs.getBoolPref("dom.webmidi.enabled"); add_task(async function testPermissionsListing() { let expectedPermissions = ["autoplay-media", "camera", "cookie", "desktop-notification", "focus-tab-by-prompt", "geo", "image", "install", "microphone", "plugin:flash", "popup", "screen", "shortcuts", - "persistent-storage"]; + "persistent-storage", "storage-access"]; if (RESIST_FINGERPRINTING_ENABLED) { // Canvas permission should be hidden unless privacy.resistFingerprinting // is true. @@ -119,7 +119,8 @@ add_task(async function testExactHostMatch() { exactHostMatched.push("midi"); exactHostMatched.push("midi-sysex"); } - let nonExactHostMatched = ["image", "cookie", "plugin:flash", "popup", "install", "shortcuts"]; + let nonExactHostMatched = ["image", "cookie", "plugin:flash", "popup", "install", "shortcuts", + "storage-access"]; let permissions = SitePermissions.listPermissions(); for (let permission of permissions) { diff --git a/browser/themes/shared/notification-icons.inc.css b/browser/themes/shared/notification-icons.inc.css index 8fd1dd9435f9..0aa259d78348 100644 --- a/browser/themes/shared/notification-icons.inc.css +++ b/browser/themes/shared/notification-icons.inc.css @@ -32,7 +32,9 @@ } .popup-notification-icon[popupid="persistent-storage"], -.persistent-storage-icon { +.popup-notification-icon[popupid="storage-access"], +.persistent-storage-icon, +.storage-access-icon { list-style-image: url(chrome://browser/skin/notification-icons/persistent-storage.svg); } @@ -73,6 +75,24 @@ list-style-image: url(chrome://browser/skin/notification-icons/autoplay-media-blocked.svg); } +.storage-access-notification-content { + color: var(--panel-disabled-color); + font-style: italic; + margin-top: 15px; +} + +.storage-access-notification-content .text-link { + color: -moz-nativehyperlinktext; +} + +.storage-access-notification-content .text-link:hover { + text-decoration: underline; +} + +#storage-access-notification .popup-notification-body-container { + padding: 20px; +} + .popup-notification-icon[popupid="indexedDB-permissions-prompt"], .indexedDB-icon { list-style-image: url(chrome://browser/skin/notification-icons/indexedDB.svg); diff --git a/dom/base/StorageAccessPermissionRequest.cpp b/dom/base/StorageAccessPermissionRequest.cpp new file mode 100644 index 000000000000..40f48cd84b0e --- /dev/null +++ b/dom/base/StorageAccessPermissionRequest.cpp @@ -0,0 +1,94 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "StorageAccessPermissionRequest.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(StorageAccessPermissionRequest, + ContentPermissionRequestBase) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(StorageAccessPermissionRequest, + ContentPermissionRequestBase) + +StorageAccessPermissionRequest::StorageAccessPermissionRequest( + nsPIDOMWindowInner* aWindow, + nsIPrincipal* aNodePrincipal, + AllowCallback&& aAllowCallback, + AllowAnySiteCallback&& aAllowAnySiteCallback, + CancelCallback&& aCancelCallback) + : ContentPermissionRequestBase(aNodePrincipal, false, aWindow, + NS_LITERAL_CSTRING("dom.storage_access"), + NS_LITERAL_CSTRING("storage-access")), + mAllowCallback(std::move(aAllowCallback)), + mAllowAnySiteCallback(std::move(aAllowAnySiteCallback)), + mCancelCallback(std::move(aCancelCallback)), + mCallbackCalled(false) +{ + mPermissionRequests.AppendElement(PermissionRequest(mType, nsTArray())); +} + +StorageAccessPermissionRequest::~StorageAccessPermissionRequest() +{ + Cancel(); +} + +NS_IMETHODIMP +StorageAccessPermissionRequest::Cancel() +{ + if (!mCallbackCalled) { + mCallbackCalled = true; + mCancelCallback(); + } + return NS_OK; +} + +NS_IMETHODIMP +StorageAccessPermissionRequest::Allow(JS::HandleValue aChoices) +{ + nsTArray choices; + nsresult rv = TranslateChoices(aChoices, mPermissionRequests, choices); + if (NS_FAILED(rv)) { + return rv; + } + + if (!mCallbackCalled) { + mCallbackCalled = true; + if (choices.Length() == 1 && + choices[0].choice().EqualsLiteral("allow-on-any-site")) { + mAllowAnySiteCallback(); + } else { + mAllowCallback(); + } + } + return NS_OK; +} + +already_AddRefed +StorageAccessPermissionRequest::Create(nsPIDOMWindowInner* aWindow, + AllowCallback&& aAllowCallback, + AllowAnySiteCallback&& aAllowAnySiteCallback, + CancelCallback&& aCancelCallback) +{ + if (!aWindow) { + return nullptr; + } + nsGlobalWindowInner* win = nsGlobalWindowInner::Cast(aWindow); + if (!win->GetPrincipal()) { + return nullptr; + } + RefPtr request = + new StorageAccessPermissionRequest(aWindow, + win->GetPrincipal(), + std::move(aAllowCallback), + std::move(aAllowAnySiteCallback), + std::move(aCancelCallback)); + return request.forget(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/base/StorageAccessPermissionRequest.h b/dom/base/StorageAccessPermissionRequest.h new file mode 100644 index 000000000000..5af43b95b705 --- /dev/null +++ b/dom/base/StorageAccessPermissionRequest.h @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef StorageAccessPermissionRequest_h_ +#define StorageAccessPermissionRequest_h_ + +#include "nsContentPermissionHelper.h" + +#include + +class nsPIDOMWindowInner; + +namespace mozilla { +namespace dom { + +class StorageAccessPermissionRequest final : public ContentPermissionRequestBase +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(StorageAccessPermissionRequest, + ContentPermissionRequestBase) + + // nsIContentPermissionRequest + NS_IMETHOD Cancel(void) override; + NS_IMETHOD Allow(JS::HandleValue choices) override; + + typedef std::function AllowCallback; + typedef std::function AllowAnySiteCallback; + typedef std::function CancelCallback; + + static already_AddRefed Create( + nsPIDOMWindowInner* aWindow, + AllowCallback&& aAllowCallback, + AllowAnySiteCallback&& aAllowAnySiteCallback, + CancelCallback&& aCancelCallback); + +private: + StorageAccessPermissionRequest(nsPIDOMWindowInner* aWindow, + nsIPrincipal* aNodePrincipal, + AllowCallback&& aAllowCallback, + AllowAnySiteCallback&& aAllowAnySiteCallback, + CancelCallback&& aCancelCallback); + ~StorageAccessPermissionRequest(); + + AllowCallback mAllowCallback; + AllowAnySiteCallback mAllowAnySiteCallback; + CancelCallback mCancelCallback; + nsTArray mPermissionRequests; + bool mCallbackCalled; +}; + +} // namespace dom +} // namespace mozilla + +#endif // StorageAccessPermissionRequest_h_ diff --git a/dom/base/moz.build b/dom/base/moz.build index 0538413ee7ea..0fa4aabf6e5e 100644 --- a/dom/base/moz.build +++ b/dom/base/moz.build @@ -373,6 +373,7 @@ UNIFIED_SOURCES += [ 'Selection.cpp', 'SelectionChangeEventDispatcher.cpp', 'ShadowRoot.cpp', + 'StorageAccessPermissionRequest.cpp', 'StructuredCloneBlob.cpp', 'StructuredCloneHolder.cpp', 'StructuredCloneTester.cpp', diff --git a/dom/base/nsDocument.cpp b/dom/base/nsDocument.cpp index be942cbf7c1d..b01a8b37e2f2 100644 --- a/dom/base/nsDocument.cpp +++ b/dom/base/nsDocument.cpp @@ -284,6 +284,7 @@ #include "NodeUbiReporting.h" #include "nsICookieService.h" #include "mozilla/net/RequestContextService.h" +#include "StorageAccessPermissionRequest.h" using namespace mozilla; using namespace mozilla::dom; @@ -13906,7 +13907,7 @@ nsIDocument::RequestStorageAccess(mozilla::ErrorResult& aRv) } // Step 1. If the document already has been granted access, resolve. - nsPIDOMWindowInner* inner = GetInnerWindow(); + nsCOMPtr inner = GetInnerWindow(); RefPtr outer; if (inner) { outer = nsGlobalWindowOuter::Cast(inner->GetOuterWindow()); @@ -13976,10 +13977,9 @@ nsIDocument::RequestStorageAccess(mozilla::ErrorResult& aRv) return promise.forget(); } - bool granted = true; - bool isTrackingWindow = false; if (StaticPrefs::network_cookie_cookieBehavior() == - nsICookieService::BEHAVIOR_REJECT_TRACKER) { + nsICookieService::BEHAVIOR_REJECT_TRACKER && + inner) { // Only do something special for third-party tracking content. if (nsContentUtils::StorageDisabledByAntiTracking(this, nullptr)) { // Note: If this has returned true, the top-level document is guaranteed @@ -13996,33 +13996,68 @@ nsIDocument::RequestStorageAccess(mozilla::ErrorResult& aRv) isOnAllowList)), !isOnAllowList); - isTrackingWindow = true; - // TODO: prompt for permission + auto performFinalChecks = [inner] () -> RefPtr { + RefPtr p = + new AntiTrackingCommon::StorageAccessFinalCheckPromise::Private(__func__); + RefPtr sapr = + StorageAccessPermissionRequest::Create(inner, + // Allow + [p] { p->Resolve(false, __func__); }, + // Allow on any site + [p] { p->Resolve(true, __func__); }, + // Block + [p] { p->Reject(false, __func__); }); + + typedef ContentPermissionRequestBase::PromptResult PromptResult; + PromptResult pr = sapr->CheckPromptPrefs(); + bool onAnySite = false; + if (pr == PromptResult::Pending) { + // Also check our custom pref for the "Allow on any site" case + if (Preferences::GetBool("dom.storage_access.prompt.testing", false) && + Preferences::GetBool("dom.storage_access.prompt.testing.allowonanysite", false)) { + pr = PromptResult::Granted; + onAnySite = true; + } + } + + if (pr != PromptResult::Pending) { + MOZ_ASSERT_IF(pr != PromptResult::Granted, + pr == PromptResult::Denied); + if (pr == PromptResult::Granted) { + return AntiTrackingCommon::StorageAccessFinalCheckPromise:: + CreateAndResolve(onAnySite, __func__); + } + return AntiTrackingCommon::StorageAccessFinalCheckPromise:: + CreateAndReject(false, __func__); + } + + sapr->RequestDelayedTask(inner->EventTargetFor(TaskCategory::Other), + ContentPermissionRequestBase::DelayedTaskType::Request); + return p.forget(); + }; + AntiTrackingCommon::AddFirstPartyStorageAccessGrantedFor( + NodePrincipal(), + inner, + AntiTrackingCommon::eStorageAccessAPI, + performFinalChecks)->Then(GetCurrentThreadSerialEventTarget(), __func__, + [outer, promise] { + // Step 10. Grant the document access to cookies and store that fact for + // the purposes of future calls to hasStorageAccess() and + // requestStorageAccess(). + outer->SetHasStorageAccess(true); + promise->MaybeResolveWithUndefined(); + }, + [outer, promise] { + outer->SetHasStorageAccess(false); + promise->MaybeRejectWithUndefined(); + }); + + return promise.forget(); } } - // Step 10. Grant the document access to cookies and store that fact for - // the purposes of future calls to hasStorageAccess() and - // requestStorageAccess(). - if (granted && inner) { - if (isTrackingWindow) { - AntiTrackingCommon::AddFirstPartyStorageAccessGrantedFor(NodePrincipal(), - inner, - AntiTrackingCommon::eStorageAccessAPI) - ->Then(GetCurrentThreadSerialEventTarget(), __func__, - [outer, promise] (bool) { - outer->SetHasStorageAccess(true); - promise->MaybeResolveWithUndefined(); - }, - [outer, promise] (bool) { - outer->SetHasStorageAccess(false); - promise->MaybeRejectWithUndefined(); - }); - } else { - outer->SetHasStorageAccess(true); - promise->MaybeResolveWithUndefined(); - } - } + outer->SetHasStorageAccess(true); + promise->MaybeResolveWithUndefined(); return promise.forget(); } diff --git a/dom/ipc/ContentParent.cpp b/dom/ipc/ContentParent.cpp index ec3bfdf0562c..3b7b944ebebd 100644 --- a/dom/ipc/ContentParent.cpp +++ b/dom/ipc/ContentParent.cpp @@ -6010,14 +6010,23 @@ ContentParent::RecvBHRThreadHang(const HangDetails& aDetails) mozilla::ipc::IPCResult ContentParent::RecvFirstPartyStorageAccessGrantedForOrigin(const Principal& aParentPrincipal, + const Principal& aTrackingPrincipal, const nsCString& aTrackingOrigin, const nsCString& aGrantedOrigin, + const bool& aAnySite, FirstPartyStorageAccessGrantedForOriginResolver&& aResolver) { AntiTrackingCommon::SaveFirstPartyStorageAccessGrantedForOriginOnParentProcess(aParentPrincipal, + aTrackingPrincipal, aTrackingOrigin, aGrantedOrigin, - std::move(aResolver)); + aAnySite) + ->Then(GetCurrentThreadSerialEventTarget(), __func__, + [aResolver = std::move(aResolver)] + (AntiTrackingCommon::FirstPartyStorageAccessGrantPromise::ResolveOrRejectValue&& aValue) { + bool success = aValue.IsResolve() && NS_SUCCEEDED(aValue.ResolveValue()); + aResolver(success); + }); return IPC_OK(); } diff --git a/dom/ipc/ContentParent.h b/dom/ipc/ContentParent.h index b60adc65ff05..a143cc2322a9 100644 --- a/dom/ipc/ContentParent.h +++ b/dom/ipc/ContentParent.h @@ -1248,8 +1248,10 @@ public: virtual mozilla::ipc::IPCResult RecvFirstPartyStorageAccessGrantedForOrigin(const Principal& aParentPrincipal, + const Principal& aTrackingPrincipal, const nsCString& aTrackingOrigin, const nsCString& aGrantedOrigin, + const bool& aAnySite, FirstPartyStorageAccessGrantedForOriginResolver&& aResolver) override; virtual mozilla::ipc::IPCResult diff --git a/dom/ipc/PContent.ipdl b/dom/ipc/PContent.ipdl index 14eba1791976..1bff1c94d796 100644 --- a/dom/ipc/PContent.ipdl +++ b/dom/ipc/PContent.ipdl @@ -1157,8 +1157,10 @@ parent: * granted to have access to aGrantedOrigin when loaded by aParentPrincipal. */ async FirstPartyStorageAccessGrantedForOrigin(Principal aParentPrincipal, + Principal aTrackingPrincipal, nsCString aTrackingOrigin, - nsCString aGrantedOrigin) + nsCString aGrantedOrigin, + bool aAnySite) returns (bool unused); async StoreUserInteractionAsPermission(Principal aPrincipal); diff --git a/toolkit/components/antitracking/AntiTrackingCommon.cpp b/toolkit/components/antitracking/AntiTrackingCommon.cpp index b84054d6f1a2..ee29ced04691 100644 --- a/toolkit/components/antitracking/AntiTrackingCommon.cpp +++ b/toolkit/components/antitracking/AntiTrackingCommon.cpp @@ -417,19 +417,20 @@ CompareBaseDomains(nsIURI* aTrackingURI, /* static */ RefPtr AntiTrackingCommon::AddFirstPartyStorageAccessGrantedFor(nsIPrincipal* aPrincipal, nsPIDOMWindowInner* aParentWindow, - StorageAccessGrantedReason aReason) + StorageAccessGrantedReason aReason, + const AntiTrackingCommon::PerformFinalChecks& aPerformFinalChecks) { MOZ_ASSERT(aParentWindow); nsCOMPtr uri; - nsresult rv = aPrincipal->GetURI(getter_AddRefs(uri)); + aPrincipal->GetURI(getter_AddRefs(uri)); if (NS_WARN_IF(!uri)) { LOG(("Can't get the URI from the principal")); return StorageAccessGrantPromise::CreateAndReject(false, __func__); } nsAutoString origin; - rv = nsContentUtils::GetUTFOrigin(uri, origin); + nsresult rv = nsContentUtils::GetUTFOrigin(uri, origin); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Can't get the origin from the URI")); return StorageAccessGrantPromise::CreateAndReject(false, __func__); @@ -454,7 +455,7 @@ AntiTrackingCommon::AddFirstPartyStorageAccessGrantedFor(nsIPrincipal* aPrincipa nsAutoCString trackingOrigin; nsCOMPtr trackingPrincipal; - nsGlobalWindowInner* parentWindow = nsGlobalWindowInner::Cast(aParentWindow); + RefPtr parentWindow = nsGlobalWindowInner::Cast(aParentWindow); nsGlobalWindowOuter* outerParentWindow = nsGlobalWindowOuter::Cast(parentWindow->GetOuterWindow()); if (NS_WARN_IF(!outerParentWindow)) { @@ -510,7 +511,13 @@ AntiTrackingCommon::AddFirstPartyStorageAccessGrantedFor(nsIPrincipal* aPrincipa // user-interaction state, because it could be that the current process has // just sent the request to store the user-interaction permission into the // parent, without having received the permission itself yet. - const uint32_t blockReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER; + // + // We define this as an enum, since without that MSVC fails to capturing this + // name inside the lambda without the explicit capture and clang warns if + // there is an explicit capture with -Wunused-lambda-capture. + enum : uint32_t { + blockReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER + }; if ((aReason != eOpenerAfterUserInteraction || nsContentUtils::IsURIInPrefList(trackingURI, "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts")) && @@ -528,65 +535,88 @@ AntiTrackingCommon::AddFirstPartyStorageAccessGrantedFor(nsIPrincipal* aPrincipa return StorageAccessGrantPromise::CreateAndReject(false, __func__); } - NS_ConvertUTF16toUTF8 grantedOrigin(origin); + auto storePermission = [pwin, parentWindow, origin, trackingOrigin, + trackingPrincipal, trackingURI, topInnerWindow, + topLevelStoragePrincipal, aReason] + (bool aAnySite) -> RefPtr { + NS_ConvertUTF16toUTF8 grantedOrigin(origin); - nsAutoCString permissionKey; - CreatePermissionKey(trackingOrigin, grantedOrigin, permissionKey); + nsAutoCString permissionKey; + CreatePermissionKey(trackingOrigin, grantedOrigin, permissionKey); - // Let's store the permission in the current parent window. - topInnerWindow->SaveStorageAccessGranted(permissionKey); + // Let's store the permission in the current parent window. + topInnerWindow->SaveStorageAccessGranted(permissionKey); - // Let's inform the parent window. - parentWindow->StorageAccessGranted(); + // Let's inform the parent window. + parentWindow->StorageAccessGranted(); - nsIChannel* channel = - pwin->GetCurrentInnerWindow()->GetExtantDoc()->GetChannel(); + nsIChannel* channel = + pwin->GetCurrentInnerWindow()->GetExtantDoc()->GetChannel(); - pwin->NotifyContentBlockingState(blockReason, channel, false, trackingURI); + pwin->NotifyContentBlockingState(blockReason, channel, false, trackingURI); - ReportUnblockingConsole(parentWindow, NS_ConvertUTF8toUTF16(trackingOrigin), - origin, aReason); + ReportUnblockingConsole(parentWindow, NS_ConvertUTF8toUTF16(trackingOrigin), + origin, aReason); - if (XRE_IsParentProcess()) { - LOG(("Saving the permission: trackingOrigin=%s, grantedOrigin=%s", + if (XRE_IsParentProcess()) { + LOG(("Saving the permission: trackingOrigin=%s, grantedOrigin=%s", + trackingOrigin.get(), grantedOrigin.get())); + + return SaveFirstPartyStorageAccessGrantedForOriginOnParentProcess(topLevelStoragePrincipal, + trackingPrincipal, + trackingOrigin, + grantedOrigin, + aAnySite) + ->Then(GetCurrentThreadSerialEventTarget(), __func__, + [] (FirstPartyStorageAccessGrantPromise::ResolveOrRejectValue&& aValue) { + if (aValue.IsResolve()) { + return StorageAccessGrantPromise::CreateAndResolve(NS_SUCCEEDED(aValue.ResolveValue()), __func__); + } + return StorageAccessGrantPromise::CreateAndReject(false, __func__); + }); + } + + ContentChild* cc = ContentChild::GetSingleton(); + MOZ_ASSERT(cc); + + LOG(("Asking the parent process to save the permission for us: trackingOrigin=%s, grantedOrigin=%s", trackingOrigin.get(), grantedOrigin.get())); - RefPtr p = new StorageAccessGrantPromise::Private(__func__); - SaveFirstPartyStorageAccessGrantedForOriginOnParentProcess(topLevelStoragePrincipal, - trackingOrigin, - grantedOrigin, - [p] (bool success) { - p->Resolve(success, __func__); - }); - return p; + // This is not really secure, because here we have the content process sending + // the request of storing a permission. + return cc->SendFirstPartyStorageAccessGrantedForOrigin(IPC::Principal(topLevelStoragePrincipal), + IPC::Principal(trackingPrincipal), + trackingOrigin, + grantedOrigin, + aAnySite) + ->Then(GetCurrentThreadSerialEventTarget(), __func__, + [] (const ContentChild::FirstPartyStorageAccessGrantedForOriginPromise::ResolveOrRejectValue& aValue) { + if (aValue.IsResolve()) { + return StorageAccessGrantPromise::CreateAndResolve(aValue.ResolveValue(), __func__); + } + return StorageAccessGrantPromise::CreateAndReject(false, __func__); + }); + }; + + if (aPerformFinalChecks) { + return aPerformFinalChecks() + ->Then(GetCurrentThreadSerialEventTarget(), __func__, + [storePermission] (StorageAccessGrantPromise::ResolveOrRejectValue&& aValue) { + if (aValue.IsResolve()) { + return storePermission(aValue.ResolveValue()); + } + return StorageAccessGrantPromise::CreateAndReject(false, __func__); + }); } - - ContentChild* cc = ContentChild::GetSingleton(); - MOZ_ASSERT(cc); - - LOG(("Asking the parent process to save the permission for us: trackingOrigin=%s, grantedOrigin=%s", - trackingOrigin.get(), grantedOrigin.get())); - - // This is not really secure, because here we have the content process sending - // the request of storing a permission. - RefPtr p = new StorageAccessGrantPromise::Private(__func__); - cc->SendFirstPartyStorageAccessGrantedForOrigin(IPC::Principal(topLevelStoragePrincipal), - trackingOrigin, - grantedOrigin) - ->Then(GetCurrentThreadSerialEventTarget(), __func__, - [p] (bool success) { - p->Resolve(success, __func__); - }, [p] (ipc::ResponseRejectReason aReason) { - p->Reject(false, __func__); - }); - return p; + return storePermission(false); } -/* static */ void +/* static */ RefPtr AntiTrackingCommon::SaveFirstPartyStorageAccessGrantedForOriginOnParentProcess(nsIPrincipal* aParentPrincipal, + nsIPrincipal* aTrackingPrincipal, const nsCString& aTrackingOrigin, const nsCString& aGrantedOrigin, - FirstPartyStorageAccessGrantedForOriginResolver&& aResolver) + bool aAnySite) { MOZ_ASSERT(XRE_IsParentProcess()); @@ -598,15 +628,13 @@ AntiTrackingCommon::SaveFirstPartyStorageAccessGrantedForOriginOnParentProcess(n if (NS_WARN_IF(!aParentPrincipal)) { // The child process is sending something wrong. Let's ignore it. LOG(("aParentPrincipal is null, bailing out early")); - aResolver(false); - return; + return FirstPartyStorageAccessGrantPromise::CreateAndReject(false, __func__); } nsCOMPtr pm = services::GetPermissionManager(); if (NS_WARN_IF(!pm)) { LOG(("Permission manager is null, bailing out early")); - aResolver(false); - return; + return FirstPartyStorageAccessGrantPromise::CreateAndReject(false, __func__); } // Remember that this pref is stored in seconds! @@ -615,28 +643,47 @@ AntiTrackingCommon::SaveFirstPartyStorageAccessGrantedForOriginOnParentProcess(n StaticPrefs::privacy_restrict3rdpartystorage_expiration() * 1000; int64_t when = (PR_Now() / PR_USEC_PER_MSEC) + expirationTime; - uint32_t privateBrowsingId = 0; - nsresult rv = aParentPrincipal->GetPrivateBrowsingId(&privateBrowsingId); - if (!NS_WARN_IF(NS_FAILED(rv)) && privateBrowsingId > 0) { - // If we are coming from a private window, make sure to store a session-only - // permission which won't get persisted to disk. - expirationType = nsIPermissionManager::EXPIRE_SESSION; - when = 0; + nsresult rv; + if (aAnySite) { + uint32_t privateBrowsingId = 0; + rv = aTrackingPrincipal->GetPrivateBrowsingId(&privateBrowsingId); + if (!NS_WARN_IF(NS_FAILED(rv)) && privateBrowsingId > 0) { + // If we are coming from a private window, make sure to store a session-only + // permission which won't get persisted to disk. + expirationType = nsIPermissionManager::EXPIRE_SESSION; + when = 0; + } + + LOG(("Setting 'any site' permission expiry: %u, proceeding to save in the permission manager", + expirationTime)); + + rv = pm->AddFromPrincipal(aTrackingPrincipal, "cookie", + nsICookiePermission::ACCESS_ALLOW, + expirationType, when); + } else { + uint32_t privateBrowsingId = 0; + rv = aParentPrincipal->GetPrivateBrowsingId(&privateBrowsingId); + if (!NS_WARN_IF(NS_FAILED(rv)) && privateBrowsingId > 0) { + // If we are coming from a private window, make sure to store a session-only + // permission which won't get persisted to disk. + expirationType = nsIPermissionManager::EXPIRE_SESSION; + when = 0; + } + + nsAutoCString type; + CreatePermissionKey(aTrackingOrigin, aGrantedOrigin, type); + + LOG(("Computed permission key: %s, expiry: %u, proceeding to save in the permission manager", + type.get(), expirationTime)); + + rv = pm->AddFromPrincipal(aParentPrincipal, type.get(), + nsIPermissionManager::ALLOW_ACTION, + expirationType, when); } - - nsAutoCString type; - CreatePermissionKey(aTrackingOrigin, aGrantedOrigin, type); - - LOG(("Computed permission key: %s, expiry: %u, proceeding to save in the permission manager", - type.get(), expirationTime)); - - rv = pm->AddFromPrincipal(aParentPrincipal, type.get(), - nsIPermissionManager::ALLOW_ACTION, - expirationType, when); Unused << NS_WARN_IF(NS_FAILED(rv)); - aResolver(NS_SUCCEEDED(rv)); LOG(("Result: %s", NS_SUCCEEDED(rv) ? "success" : "failure")); + return FirstPartyStorageAccessGrantPromise::CreateAndResolve(rv, __func__); } // static diff --git a/toolkit/components/antitracking/AntiTrackingCommon.h b/toolkit/components/antitracking/AntiTrackingCommon.h index 46dce0bd4d17..6e71e10b4e0b 100644 --- a/toolkit/components/antitracking/AntiTrackingCommon.h +++ b/toolkit/components/antitracking/AntiTrackingCommon.h @@ -93,11 +93,14 @@ public: // Ex: example.net import tracker.com/script.js which does opens a popup and // the user interacts with it. tracker.com is allowed when loaded by // example.net. - typedef MozPromise StorageAccessGrantPromise; + typedef MozPromise StorageAccessFinalCheckPromise; + typedef std::function()> PerformFinalChecks; + typedef MozPromise StorageAccessGrantPromise; static MOZ_MUST_USE RefPtr AddFirstPartyStorageAccessGrantedFor(nsIPrincipal* aPrincipal, nsPIDOMWindowInner* aParentWindow, - StorageAccessGrantedReason aReason); + StorageAccessGrantedReason aReason, + const PerformFinalChecks& aPerformFinalChecks = nullptr); // Returns true if the permission passed in is a storage access permission // for the passed in principal argument. @@ -111,11 +114,13 @@ public: HasUserInteraction(nsIPrincipal* aPrincipal); // For IPC only. - static void + typedef MozPromise FirstPartyStorageAccessGrantPromise; + static RefPtr SaveFirstPartyStorageAccessGrantedForOriginOnParentProcess(nsIPrincipal* aPrincipal, + nsIPrincipal* aTrackingPrinciapl, const nsCString& aParentOrigin, const nsCString& aGrantedOrigin, - FirstPartyStorageAccessGrantedForOriginResolver&& aResolver); + bool aAnySite); enum ContentBlockingAllowListPurpose { eStorageChecks, diff --git a/toolkit/components/antitracking/test/browser/browser.ini b/toolkit/components/antitracking/test/browser/browser.ini index fb8cd3ceac74..6e70bd4d50ff 100644 --- a/toolkit/components/antitracking/test/browser/browser.ini +++ b/toolkit/components/antitracking/test/browser/browser.ini @@ -1,4 +1,9 @@ [DEFAULT] +prefs = + # Disable the Storage Access API prompts for all of the tests in this directory + dom.storage_access.prompt.testing=true + dom.storage_access.prompt.testing.allow=true + support-files = embedder.html head.js diff --git a/toolkit/content/widgets/notification.xml b/toolkit/content/widgets/notification.xml index 337e15104f07..0cadb8a92f47 100644 --- a/toolkit/content/widgets/notification.xml +++ b/toolkit/content/widgets/notification.xml @@ -35,7 +35,8 @@ localization file, if necessary). --> + xbl:inherits="xbl:text=endlabel,popupid"/> " as a placeholder which is later - * replaced by a host name or an addon name that is formatted to look bold, - * in which case the options.name property needs to be specified. + * A string containing the text to be displayed as the notification + * header. The string may optionally contain one or two "<>" as a + * placeholder which is later replaced by a host name or an addon name + * that is formatted to look bold, in which case the options.name + * property (as well as options.secondName if passing two "<>" + * placeholders) needs to be specified. * @param anchorID * The ID of the element that should be used as this notification * popup's anchor. May be null, in which case the notification will be @@ -460,6 +462,11 @@ PopupNotifications.prototype = { * An optional string formatted to look bold and used in the * notifiation description header text. Usually a host name or * addon name. + * secondName: + * An optional string formatted to look bold and used in the + * notification description header text. Usually a host name or + * addon name. This is similar to name, and only used in case + * where message contains two "<>" placeholders. * @returns the Notification object corresponding to the added notification. */ show: function PopupNotifications_show(browser, id, message, anchorID, @@ -781,6 +788,13 @@ PopupNotifications.prototype = { text.start = array[0] || ""; text.name = n.options.name || ""; text.end = array[1] || ""; + if (array.length == 3) { + text.secondName = n.options.secondName || ""; + text.secondEnd = array[2] || ""; + } else if (array.length > 3) { + Cu.reportError("Unexpected array length encountered in " + + "_formatDescriptionMessage: " + array.length); + } return text; }, @@ -807,6 +821,11 @@ PopupNotifications.prototype = { popupnotification.setAttribute("label", desc.start); popupnotification.setAttribute("name", desc.name); popupnotification.setAttribute("endlabel", desc.end); + if (("secondName" in desc) && + ("secondEnd" in desc)) { + popupnotification.setAttribute("secondname", desc.secondName); + popupnotification.setAttribute("secondendlabel", desc.secondEnd); + } popupnotification.setAttribute("id", popupnotificationID); popupnotification.setAttribute("popupid", n.id);