Bug 1490811 - Part 1: Add a permission doorhanger for the storage access API r=baku,johannh
authorEhsan Akhgari <ehsan@mozilla.com>
Mon, 26 Nov 2018 21:23:16 +0000
changeset 504562 8e3bf28192dff3eea2e163c4dbf56bc02da86430
parent 504561 79ae8ec996e64730196a994093f3ff8bb4af1384
child 504563 f2bf86926c655278bd1d0319b70eef55450bcee5
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku, johannh
bugs1490811
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1490811 - Part 1: Add a permission doorhanger for the storage access API r=baku,johannh Differential Revision: https://phabricator.services.mozilla.com/D12467
browser/base/content/browser-siteIdentity.js
browser/base/content/browser.xul
browser/base/content/pageinfo/permissions.js
browser/base/content/popup-notifications.inc
browser/components/nsBrowserGlue.js
browser/locales/en-US/chrome/browser/browser.dtd
browser/locales/en-US/chrome/browser/browser.properties
browser/locales/en-US/chrome/browser/sitePermissions.properties
browser/modules/PermissionUI.jsm
browser/modules/SitePermissions.jsm
browser/modules/test/unit/test_SitePermissions.js
browser/themes/shared/notification-icons.inc.css
dom/base/StorageAccessPermissionRequest.cpp
dom/base/StorageAccessPermissionRequest.h
dom/base/moz.build
dom/base/nsDocument.cpp
dom/ipc/ContentParent.cpp
dom/ipc/ContentParent.h
dom/ipc/PContent.ipdl
toolkit/components/antitracking/AntiTrackingCommon.cpp
toolkit/components/antitracking/AntiTrackingCommon.h
toolkit/components/antitracking/test/browser/browser.ini
toolkit/content/widgets/notification.xml
toolkit/modules/PopupNotifications.jsm
--- a/browser/base/content/browser-siteIdentity.js
+++ b/browser/base/content/browser-siteIdentity.js
@@ -961,17 +961,25 @@ 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" &&
           gBrowser.selectedBrowser.blockedPopups &&
           gBrowser.selectedBrowser.blockedPopups.length) {
         this._createBlockedPopupIndicator();
         hasBlockedPopupIndicator = true;
       }
@@ -1030,17 +1038,21 @@ var gIdentityHandler = {
           imgBlink.startTime = sharingIconBlink.startTime;
         }
       });
     }
 
     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);
 
     let isPolicyPermission = [
       SitePermissions.SCOPE_POLICY, SitePermissions.SCOPE_GLOBAL,
     ].includes(aPermission.scope);
 
     if (aPermission.id == "popup" && !isPolicyPermission) {
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -972,16 +972,18 @@ xmlns="http://www.w3.org/1999/xhtml"
                   <image id="eme-notification-icon" class="notification-anchor-icon drm-icon" role="button"
                          tooltiptext="&urlbar.emeNotificationAnchor.tooltip;"/>
                   <image id="persistent-storage-notification-icon" class="notification-anchor-icon persistent-storage-icon" role="button"
                          tooltiptext="&urlbar.persistentStorageNotificationAnchor.tooltip;"/>
                   <image id="midi-notification-icon" class="notification-anchor-icon midi-icon" role="button"
                          tooltiptext="&urlbar.midiNotificationAnchor.tooltip;"/>
                   <image id="webauthn-notification-icon" class="notification-anchor-icon" role="button"
                          tooltiptext="&urlbar.webAuthnAnchor.tooltip;"/>
+                  <image id="storage-access-notification-icon" class="notification-anchor-icon storage-access-icon" role="button"
+                         tooltiptext="&urlbar.storageAccessAnchor.tooltip;"/>
                 </box>
                 <image id="connection-icon"/>
                 <image id="extension-icon"/>
                 <image id="remote-control-icon"
                        tooltiptext="&urlbar.remoteControlNotificationAnchor.tooltip;"/>
                 <hbox id="identity-icon-labels">
                   <label id="identity-icon-label" class="plain" flex="1"/>
                   <label id="identity-icon-country-label" class="plain"/>
--- a/browser/base/content/pageinfo/permissions.js
+++ b/browser/base/content/pageinfo/permissions.js
@@ -7,21 +7,23 @@
 ChromeUtils.import("resource:///modules/SitePermissions.jsm");
 ChromeUtils.import("resource://gre/modules/BrowserUtils.jsm");
 
 var gPermURI;
 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) {
     if (aTopic == "perm-changed") {
       var permission = aSubject.QueryInterface(Ci.nsIPermission);
       if (permission.matchesURI(gPermURI, true) && gPermissions.includes(permission.type)) {
           initRow(permission.type);
       }
--- a/browser/base/content/popup-notifications.inc
+++ b/browser/base/content/popup-notifications.inc
@@ -98,8 +98,23 @@
           <hbox id="cfr-notification-footer-filled-stars"/>
           <hbox id="cfr-notification-footer-empty-stars"/>
           <label id="cfr-notification-footer-users"/>
           <spacer id="cfr-notification-footer-spacer" hidden="true"/>
           <label id="cfr-notification-footer-learn-more-link" class="text-link"/>
         </hbox>
       </popupnotificationfooter>
     </popupnotification>
+
+    <popupnotification id="storage-access-notification" hidden="true">
+      <popupnotificationcontent class="storage-access-notification-content">
+        <xul:vbox flex="1">
+          <!-- These need to be on the same line to avoid creating
+               whitespace between them (whitespace is added in the
+               localization file, if necessary). -->
+          <xul:description class="storage-access-perm-text"><html:span
+            id="storage-access-perm-label"/><html:a id="storage-access-perm-learnmore"
+            onclick="openTrustedLinkIn(this.href, 'tab'); return false;"
+            class="text-link popup-notification-learnmore-link"/><html:span
+            id="storage-access-perm-endlabel"/></xul:description>
+        </xul:vbox>
+      </popupnotificationcontent>
+    </popupnotification>
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -3130,16 +3130,19 @@ const ContentPermissionIntegration = {
         return new PermissionUI.PersistentStoragePermissionPrompt(request);
       }
       case "midi": {
         return new PermissionUI.MIDIPermissionPrompt(request);
       }
       case "autoplay-media": {
         return new PermissionUI.AutoplayPermissionPrompt(request);
       }
+      case "storage-access": {
+        return new PermissionUI.StorageAccessPermissionPrompt(request);
+      }
     }
     return undefined;
   },
 };
 
 function ContentPermissionPrompt() {}
 
 ContentPermissionPrompt.prototype = {
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -277,16 +277,17 @@ These should match what Safari and other
 <!ENTITY urlbar.canvasNotificationAnchor.tooltip          "Manage canvas extraction permission">
 <!ENTITY urlbar.indexedDBNotificationAnchor.tooltip       "Open offline storage message panel">
 <!ENTITY urlbar.passwordNotificationAnchor.tooltip        "Open save password message panel">
 <!ENTITY urlbar.pluginsNotificationAnchor.tooltip         "Manage plug-in use">
 <!ENTITY urlbar.webNotificationAnchor.tooltip             "Change whether you can receive notifications from the site">
 <!ENTITY urlbar.persistentStorageNotificationAnchor.tooltip     "Store data in Persistent Storage">
 <!ENTITY urlbar.remoteControlNotificationAnchor.tooltip   "Browser is under remote control">
 <!ENTITY urlbar.webAuthnAnchor.tooltip                    "Open Web Authentication panel">
+<!ENTITY urlbar.storageAccessAnchor.tooltip               "Open browsing activity permission panel">
 
 <!ENTITY urlbar.webRTCShareDevicesNotificationAnchor.tooltip      "Manage sharing your camera and/or microphone with the site">
 <!ENTITY urlbar.webRTCShareMicrophoneNotificationAnchor.tooltip   "Manage sharing your microphone with the site">
 <!ENTITY urlbar.webRTCShareScreenNotificationAnchor.tooltip       "Manage sharing your windows or screen with the site">
 
 <!ENTITY urlbar.servicesNotificationAnchor.tooltip        "Open install message panel">
 <!ENTITY urlbar.translateNotificationAnchor.tooltip       "Translate this page">
 <!ENTITY urlbar.translatedNotificationAnchor.tooltip      "Manage page translation">
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -960,16 +960,34 @@ autoplay.remember-private = Remember for
 # LOCALIZATION NOTE (autoplay.message): %S is the name of the site URL (https://...) trying to autoplay media
 autoplay.message = Will you allow %S to autoplay media with sound?
 autoplay.messageWithFile = Will you allow this file to autoplay media with sound?
 # LOCALIZATION NOTE (panel.back):
 # This is used by screen readers to label the "back" button in various browser
 # 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!
 confirmationHint.pageBookmarked.label = Saved to Library!
 confirmationHint.addSearchEngine.label = Search engine added!
 
 # LOCALIZATION NOTE (livebookmarkMigration.title):
 # Used by the export of user's live bookmarks to an OPML file as a title for the file.
--- a/browser/locales/en-US/chrome/browser/sitePermissions.properties
+++ b/browser/locales/en-US/chrome/browser/sitePermissions.properties
@@ -39,9 +39,9 @@ permission.install.label = Install Add-o
 permission.popup.label = Open Pop-up Windows
 permission.geo.label = Access Your Location
 permission.shortcuts.label = Override Keyboard Shortcuts
 permission.focus-tab-by-prompt.label = Switch to this Tab
 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
--- a/browser/modules/PermissionUI.jsm
+++ b/browser/modules/PermissionUI.jsm
@@ -252,18 +252,24 @@ var PermissionPromptPrototype = {
    * If the prompt will be shown to the user, this callback will
    * be called just before. Subclasses may want to override this
    * in order to, for example, bump a counter Telemetry probe for
    * how often a particular permission request is seen.
    */
   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() {},
 
   /**
    * Will determine if a prompt should be shown to the user, and if so,
    * will show it.
    *
    * If a permissionKey is defined prompt() might automatically
@@ -418,16 +424,20 @@ var PermissionPromptPrototype = {
     options.hideClose = !Services.prefs.getBoolPref("privacy.permissionPrompts.showCloseButton");
     options.eventCallback = (topic) => {
       // When the docshell of the browser is aboout to be swapped to another one,
       // the "swapping" event is called. Returning true causes the notification
       // to be moved to the new browser.
       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();
       }
       return false;
     };
 
     this.onBeforeShow();
@@ -467,18 +477,18 @@ var PermissionPromptForRequestPrototype 
   get principal() {
     return this.request.principal;
   },
 
   cancel() {
     this.request.cancel();
   },
 
-  allow() {
-    this.request.allow();
+  allow(choices) {
+    this.request.allow(choices);
   },
 };
 
 PermissionUI.PermissionPromptForRequestPrototype =
   PermissionPromptForRequestPrototype;
 
 /**
  * Creates a PermissionPrompt for a nsIContentPermissionRequest for
@@ -900,8 +910,104 @@ AutoplayPermissionPrompt.prototype = {
       }
     };
     this.browser.addEventListener(
       "DOMAudioPlaybackStarted", this.handlePlaybackStart);
   },
 };
 
 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;
--- a/browser/modules/SitePermissions.jsm
+++ b/browser/modules/SitePermissions.jsm
@@ -350,17 +350,17 @@ var SitePermissions = {
    *        The browser to fetch permission for.
    *
    * @return {Array<Object>} a list of objects with the keys:
    *           - id: the permissionID of the permission
    *           - state: a constant representing the current permission state
    *             (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}) =>
       ({id, scope, state, label: this.getPermissionLabel(id)}));
   },
 
   /**
    * Checks whether a UI for managing permissions should be exposed for a given
@@ -648,19 +648,28 @@ var SitePermissions = {
 
   /**
    * Returns the localized label for the permission with the given ID, to be
    * used in a UI for managing permissions.
    *
    * @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");
   },
 
   /**
    * Returns the localized label for the given permission state, to be used in
    * a UI for managing permissions.
    *
@@ -847,16 +856,23 @@ var gPermissionObject = {
 
   "midi": {
     exactHostMatch: true,
   },
 
   "midi-sysex": {
     exactHostMatch: true,
   },
+
+  "storage-access": {
+    labelID: null,
+    getDefault() {
+      return SitePermissions.UNKNOWN;
+    },
+  },
 };
 
 if (!Services.prefs.getBoolPref("dom.webmidi.enabled")) {
   // ESLint gets angry about array versus dot notation here, but some permission
   // names use hyphens. Disabling rule for line to keep things consistent.
   // eslint-disable-next-line dot-notation
   delete gPermissionObject["midi"];
   delete gPermissionObject["midi-sysex"];
--- a/browser/modules/test/unit/test_SitePermissions.js
+++ b/browser/modules/test/unit/test_SitePermissions.js
@@ -7,17 +7,17 @@ ChromeUtils.import("resource:///modules/
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 const RESIST_FINGERPRINTING_ENABLED = Services.prefs.getBoolPref("privacy.resistFingerprinting");
 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.
     expectedPermissions.push("canvas");
   }
   if (MIDI_ENABLED) {
     // Should remove this checking and add it as default after it is fully pref'd-on.
     expectedPermissions.push("midi");
@@ -114,17 +114,18 @@ add_task(async function testExactHostMat
     exactHostMatched.push("canvas");
   }
   if (MIDI_ENABLED) {
     // WebMIDI is only pref'd on in nightly.
     // Should remove this checking and add it as default after it is fully pref-on.
     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) {
     SitePermissions.set(uri, permission, SitePermissions.ALLOW);
 
     if (exactHostMatched.includes(permission)) {
       // Check that the sub-origin does not inherit the permission from its parent.
       Assert.equal(SitePermissions.get(subUri, permission).state, SitePermissions.getDefault(permission),
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -27,17 +27,19 @@
 
 /* INDIVIDUAL NOTIFICATIONS */
 
 .focus-tab-by-prompt-icon {
   list-style-image: url(chrome://browser/skin/notification-icons/focus-tab-by-prompt.svg);
 }
 
 .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);
 }
 
 .persistent-storage-icon.blocked-permission-icon {
   list-style-image: url(chrome://browser/skin/notification-icons/persistent-storage-blocked.svg);
 }
 
 .popup-notification-icon[popupid="web-notifications"],
@@ -68,16 +70,34 @@
 .popup-notification-icon[popupid="autoplay-media"] {
   list-style-image: url(chrome://browser/skin/notification-icons/autoplay-media-detailed.svg);
 }
 
 .autoplay-media-icon.blocked-permission-icon {
   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);
 }
 
 .login-icon {
   list-style-image: url(chrome://browser/skin/notification-icons/login.svg);
 }
new file mode 100644
--- /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<nsString>()));
+}
+
+StorageAccessPermissionRequest::~StorageAccessPermissionRequest()
+{
+  Cancel();
+}
+
+NS_IMETHODIMP
+StorageAccessPermissionRequest::Cancel()
+{
+  if (!mCallbackCalled) {
+    mCallbackCalled = true;
+    mCancelCallback();
+  }
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+StorageAccessPermissionRequest::Allow(JS::HandleValue aChoices)
+{
+  nsTArray<PermissionChoice> 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>
+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<StorageAccessPermissionRequest> request =
+    new StorageAccessPermissionRequest(aWindow,
+                                       win->GetPrincipal(),
+                                       std::move(aAllowCallback),
+                                       std::move(aAllowAnySiteCallback),
+                                       std::move(aCancelCallback));
+  return request.forget();
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /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 <functional>
+
+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<void()> AllowCallback;
+  typedef std::function<void()> AllowAnySiteCallback;
+  typedef std::function<void()> CancelCallback;
+
+  static already_AddRefed<StorageAccessPermissionRequest> 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<PermissionRequest> mPermissionRequests;
+  bool mCallbackCalled;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // StorageAccessPermissionRequest_h_
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -368,16 +368,17 @@ UNIFIED_SOURCES += [
     'ProcessMessageManager.cpp',
     'ResponsiveImageSelector.cpp',
     'SameProcessMessageQueue.cpp',
     'ScreenLuminance.cpp',
     'ScreenOrientation.cpp',
     'Selection.cpp',
     'SelectionChangeEventDispatcher.cpp',
     'ShadowRoot.cpp',
+    'StorageAccessPermissionRequest.cpp',
     'StructuredCloneBlob.cpp',
     'StructuredCloneHolder.cpp',
     'StructuredCloneTester.cpp',
     'StyleSheetList.cpp',
     'SubtleCrypto.cpp',
     'TabGroup.cpp',
     'Text.cpp',
     'TextInputProcessor.cpp',
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -279,16 +279,17 @@
 #include "mozilla/DocumentStyleRootIterator.h"
 #include "mozilla/PendingFullscreenEvent.h"
 #include "mozilla/RestyleManager.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "nsHTMLTags.h"
 #include "NodeUbiReporting.h"
 #include "nsICookieService.h"
 #include "mozilla/net/RequestContextService.h"
+#include "StorageAccessPermissionRequest.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 typedef nsTArray<Link*> LinkArray;
 
 static LazyLogModule gDocumentLeakPRLog("DocumentLeak");
 static LazyLogModule gCspPRLog("CSP");
@@ -13901,17 +13902,17 @@ nsIDocument::RequestStorageAccess(mozill
   // Propagate user input event handling to the resolve handler
   RefPtr<Promise> promise = Promise::Create(global, aRv,
                                             Promise::ePropagateUserInteraction);
   if (aRv.Failed()) {
     return nullptr;
   }
 
   // Step 1. If the document already has been granted access, resolve.
-  nsPIDOMWindowInner* inner = GetInnerWindow();
+  nsCOMPtr<nsPIDOMWindowInner> inner = GetInnerWindow();
   RefPtr<nsGlobalWindowOuter> outer;
   if (inner) {
     outer = nsGlobalWindowOuter::Cast(inner->GetOuterWindow());
     if (outer->HasStorageAccess()) {
       promise->MaybeResolveWithUndefined();
       return promise.forget();
     }
   }
@@ -13971,63 +13972,97 @@ nsIDocument::RequestStorageAccess(mozill
 
   if (nsContentUtils::IsInPrivateBrowsing(this)) {
     // If the document is in PB mode, it doesn't have access to its persistent
     // cookie jar, so reject the promise here.
     promise->MaybeRejectWithUndefined();
     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
       // to not be on the Content Blocking allow list.
       DebugOnly<bool> isOnAllowList = false;
       // If we have a parent document, it has to be non-private since we verified
       // earlier that our own document is non-private and a private document can
       // never have a non-private document as its child.
       MOZ_ASSERT_IF(parent, !nsContentUtils::IsInPrivateBrowsing(parent));
       MOZ_ASSERT_IF(NS_SUCCEEDED(AntiTrackingCommon::IsOnContentBlockingAllowList(
                                    parent->GetDocumentURI(),
                                    false,
                                    AntiTrackingCommon::eStorageChecks,
                                    isOnAllowList)),
                     !isOnAllowList);
 
-      isTrackingWindow = true;
-      // TODO: prompt for permission
-    }
-  }
-
-  // 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();
-    }
-  }
+      auto performFinalChecks = [inner] () -> RefPtr<AntiTrackingCommon::StorageAccessFinalCheckPromise> {
+          RefPtr<AntiTrackingCommon::StorageAccessFinalCheckPromise::Private> p =
+            new AntiTrackingCommon::StorageAccessFinalCheckPromise::Private(__func__);
+          RefPtr<StorageAccessPermissionRequest> 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();
+    }
+  }
+
+  outer->SetHasStorageAccess(true);
+  promise->MaybeResolveWithUndefined();
   return promise.forget();
 }
 
 void
 nsIDocument::RecordNavigationTiming(ReadyState aReadyState)
 {
   if (!XRE_IsContentProcess()) {
     return;
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -6005,24 +6005,33 @@ ContentParent::RecvBHRThreadHang(const H
       new nsHangDetails(HangDetails(aDetails));
     obs->NotifyObservers(hangDetails, "bhr-thread-hang", nullptr);
   }
   return IPC_OK();
 }
 
 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();
 }
 
 mozilla::ipc::IPCResult
 ContentParent::RecvStoreUserInteractionAsPermission(const Principal& aPrincipal)
 {
   AntiTrackingCommon::StoreUserInteractionFor(aPrincipal);
   return IPC_OK();
--- a/dom/ipc/ContentParent.h
+++ b/dom/ipc/ContentParent.h
@@ -1243,18 +1243,20 @@ public:
   virtual mozilla::ipc::IPCResult RecvRecordDiscardedData(
     const DiscardedData& aDiscardedData) override;
 
   virtual mozilla::ipc::IPCResult RecvBHRThreadHang(
     const HangDetails& aHangDetails) override;
 
   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
   RecvStoreUserInteractionAsPermission(const Principal& aPrincipal) override;
 
   // Notify the ContentChild to enable the input event prioritization when
   // initializing.
   void MaybeEnableRemoteInputEventQueue();
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -1152,18 +1152,20 @@ parent:
 
     async AddPerformanceMetrics(nsID aID, PerformanceInfo[] aMetrics);
 
     /*
      * A 3rd party tracking origin (aTrackingOrigin) has received the permission
      * 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);
 
     /**
      * Sync the BrowsingContext with id 'aContextId' and name 'aName'
      * to the parent, and attach it to the BrowsingContext with id
      * 'aParentContextId'. If 'aParentContextId' is '0' the
--- a/toolkit/components/antitracking/AntiTrackingCommon.cpp
+++ b/toolkit/components/antitracking/AntiTrackingCommon.cpp
@@ -412,29 +412,30 @@ CompareBaseDomains(nsIURI* aTrackingURI,
                                    nsCaseInsensitiveCStringComparator());
 }
 
 } // anonymous
 
 /* static */ RefPtr<AntiTrackingCommon::StorageAccessGrantPromise>
 AntiTrackingCommon::AddFirstPartyStorageAccessGrantedFor(nsIPrincipal* aPrincipal,
                                                          nsPIDOMWindowInner* aParentWindow,
-                                                         StorageAccessGrantedReason aReason)
+                                                         StorageAccessGrantedReason aReason,
+                                                         const AntiTrackingCommon::PerformFinalChecks& aPerformFinalChecks)
 {
   MOZ_ASSERT(aParentWindow);
 
   nsCOMPtr<nsIURI> 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__);
   }
 
   LOG(("Adding a first-party storage exception for %s...",
        NS_ConvertUTF16toUTF8(origin).get()));
 
@@ -449,17 +450,17 @@ AntiTrackingCommon::AddFirstPartyStorage
     return StorageAccessGrantPromise::CreateAndResolve(true, __func__);
   }
 
   nsCOMPtr<nsIPrincipal> topLevelStoragePrincipal;
   nsCOMPtr<nsIURI> trackingURI;
   nsAutoCString trackingOrigin;
   nsCOMPtr<nsIPrincipal> trackingPrincipal;
 
-  nsGlobalWindowInner* parentWindow = nsGlobalWindowInner::Cast(aParentWindow);
+  RefPtr<nsGlobalWindowInner> parentWindow = nsGlobalWindowInner::Cast(aParentWindow);
   nsGlobalWindowOuter* outerParentWindow =
     nsGlobalWindowOuter::Cast(parentWindow->GetOuterWindow());
   if (NS_WARN_IF(!outerParentWindow)) {
     LOG(("No outer window found for our parent window, bailing out early"));
     return StorageAccessGrantPromise::CreateAndReject(false, __func__);
   }
 
   LOG(("The current resource is %s-party",
@@ -505,17 +506,23 @@ AntiTrackingCommon::AddFirstPartyStorage
 
   // We hardcode this block reason since the first-party storage access
   // permission is granted for the purpose of blocking trackers.
   // Note that if aReason is eOpenerAfterUserInteraction and the
   // trackingPrincipal is not in a blacklist, we don't check the
   // 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")) &&
       !HasUserInteraction(trackingPrincipal)) {
     LOG_SPEC(("Tracking principal (%s) hasn't been interacted with before, "
               "refusing to add a first-party storage permission to access it",
               _spec), trackingURI);
     NotifyBlockingDecision(aParentWindow, BlockingDecision::eBlock, blockReason);
@@ -523,125 +530,165 @@ AntiTrackingCommon::AddFirstPartyStorage
   }
 
   nsCOMPtr<nsPIDOMWindowOuter> pwin = GetTopWindow(parentWindow);
   if (!pwin) {
     LOG(("Couldn't get the top window"));
     return StorageAccessGrantPromise::CreateAndReject(false, __func__);
   }
 
-  NS_ConvertUTF16toUTF8 grantedOrigin(origin);
+  auto storePermission = [pwin, parentWindow, origin, trackingOrigin,
+                          trackingPrincipal, trackingURI, topInnerWindow,
+                          topLevelStoragePrincipal, aReason]
+                         (bool aAnySite) -> RefPtr<StorageAccessGrantPromise> {
+    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();
+
+    pwin->NotifyContentBlockingState(blockReason, channel, false, trackingURI);
+
+    ReportUnblockingConsole(parentWindow, NS_ConvertUTF8toUTF16(trackingOrigin),
+                            origin, aReason);
 
-  nsIChannel* channel =
-    pwin->GetCurrentInnerWindow()->GetExtantDoc()->GetChannel();
-
-  pwin->NotifyContentBlockingState(blockReason, channel, false, trackingURI);
+    if (XRE_IsParentProcess()) {
+      LOG(("Saving the permission: trackingOrigin=%s, grantedOrigin=%s",
+           trackingOrigin.get(), grantedOrigin.get()));
 
-  ReportUnblockingConsole(parentWindow, NS_ConvertUTF8toUTF16(trackingOrigin),
-                          origin, aReason);
+      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__);
+               });
+    }
 
-  if (XRE_IsParentProcess()) {
-    LOG(("Saving the permission: trackingOrigin=%s, grantedOrigin=%s",
+    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<StorageAccessGrantPromise::Private> p = new StorageAccessGrantPromise::Private(__func__);
-    SaveFirstPartyStorageAccessGrantedForOriginOnParentProcess(topLevelStoragePrincipal,
-                                                               trackingOrigin,
-                                                               grantedOrigin,
-                                                               [p] (bool success) {
-                                                                 p->Resolve(success, __func__);
-                                                               });
-    return p;
-  }
-
-  ContentChild* cc = ContentChild::GetSingleton();
-  MOZ_ASSERT(cc);
+    // 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__);
+             });
+  };
 
-  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<StorageAccessGrantPromise::Private> 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;
+  if (aPerformFinalChecks) {
+    return aPerformFinalChecks()
+      ->Then(GetCurrentThreadSerialEventTarget(), __func__,
+             [storePermission] (StorageAccessGrantPromise::ResolveOrRejectValue&& aValue) {
+               if (aValue.IsResolve()) {
+                 return storePermission(aValue.ResolveValue());
+               }
+               return StorageAccessGrantPromise::CreateAndReject(false, __func__);
+             });
+  }
+  return storePermission(false);
 }
 
-/* static */ void
+/* static */ RefPtr<mozilla::AntiTrackingCommon::FirstPartyStorageAccessGrantPromise>
 AntiTrackingCommon::SaveFirstPartyStorageAccessGrantedForOriginOnParentProcess(nsIPrincipal* aParentPrincipal,
+                                                                               nsIPrincipal* aTrackingPrincipal,
                                                                                const nsCString& aTrackingOrigin,
                                                                                const nsCString& aGrantedOrigin,
-                                                                               FirstPartyStorageAccessGrantedForOriginResolver&& aResolver)
+                                                                               bool aAnySite)
 {
   MOZ_ASSERT(XRE_IsParentProcess());
 
   nsCOMPtr<nsIURI> parentPrincipalURI;
   Unused << aParentPrincipal->GetURI(getter_AddRefs(parentPrincipalURI));
   LOG_SPEC(("Saving a first-party storage permission on %s for trackingOrigin=%s grantedOrigin=%s",
             _spec, aTrackingOrigin.get(), aGrantedOrigin.get()), parentPrincipalURI);
 
   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<nsIPermissionManager> 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!
   uint32_t expirationType = nsIPermissionManager::EXPIRE_TIME;
   uint32_t expirationTime =
     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));
 
-  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(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;
+    }
 
-  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
 bool
 AntiTrackingCommon::IsStorageAccessPermission(nsIPermission* aPermission,
                                               nsIPrincipal* aPrincipal)
 {
   MOZ_ASSERT(aPermission);
--- a/toolkit/components/antitracking/AntiTrackingCommon.h
+++ b/toolkit/components/antitracking/AntiTrackingCommon.h
@@ -88,39 +88,44 @@ public:
   //   loaded by tracker.com when loaded by example.net.
   // - aParentWindow is a first party context and a 3rd party resource (probably
   //   becuase of a script) opens a popup and the user interacts with it. We
   //   want to grant the permission for the 3rd party context to have access to
   //   the first party stoage when loaded in aParentWindow.
   //   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<bool, bool, false> StorageAccessGrantPromise;
+  typedef MozPromise<bool, bool, true> StorageAccessFinalCheckPromise;
+  typedef std::function<RefPtr<StorageAccessFinalCheckPromise>()> PerformFinalChecks;
+  typedef MozPromise<bool, bool, true> StorageAccessGrantPromise;
   static MOZ_MUST_USE RefPtr<StorageAccessGrantPromise>
   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.
   static bool
   IsStorageAccessPermission(nsIPermission* aPermission, nsIPrincipal* aPrincipal);
 
   static void
   StoreUserInteractionFor(nsIPrincipal* aPrincipal);
 
   static bool
   HasUserInteraction(nsIPrincipal* aPrincipal);
 
   // For IPC only.
-  static void
+  typedef MozPromise<nsresult, bool, true> FirstPartyStorageAccessGrantPromise;
+  static RefPtr<FirstPartyStorageAccessGrantPromise>
   SaveFirstPartyStorageAccessGrantedForOriginOnParentProcess(nsIPrincipal* aPrincipal,
+                                                             nsIPrincipal* aTrackingPrinciapl,
                                                              const nsCString& aParentOrigin,
                                                              const nsCString& aGrantedOrigin,
-                                                             FirstPartyStorageAccessGrantedForOriginResolver&& aResolver);
+                                                             bool aAnySite);
 
   enum ContentBlockingAllowListPurpose {
     eStorageChecks,
     eTrackingProtection,
     eTrackingAnnotations,
   };
 
   // Check whether a top window URI is on the content blocking allow list.
--- a/toolkit/components/antitracking/test/browser/browser.ini
+++ b/toolkit/components/antitracking/test/browser/browser.ini
@@ -1,9 +1,14 @@
 [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
   image.sjs
   imageCacheWorker.js
   page.html
   3rdParty.html
   3rdPartySVG.html
--- a/toolkit/content/widgets/notification.xml
+++ b/toolkit/content/widgets/notification.xml
@@ -30,17 +30,18 @@
               <xul:label class="popup-notification-origin header"
                          xbl:inherits="value=origin,tooltiptext=origin"
                          crop="center"/>
               <!-- These need to be on the same line to avoid creating
                    whitespace between them (whitespace is added in the
                    localization file, if necessary). -->
               <xul:description class="popup-notification-description" xbl:inherits="popupid"><html:span
                 xbl:inherits="xbl:text=label,popupid"/><html:b xbl:inherits="xbl:text=name,popupid"/><html:span
-              xbl:inherits="xbl:text=endlabel,popupid"/></xul:description>
+              xbl:inherits="xbl:text=endlabel,popupid"/><html:b xbl:inherits="xbl:text=secondname,popupid"/><html:span
+              xbl:inherits="xbl:text=secondendlabel,popupid"/></xul:description>
             </xul:vbox>
             <xul:toolbarbutton anonid="closebutton"
                                class="messageCloseButton close-icon popup-notification-closebutton tabbable"
                                xbl:inherits="oncommand=closebuttoncommand,hidden=closebuttonhidden"
                                tooltiptext="&closeNotification.tooltip;"/>
           </xul:hbox>
           <children includes="popupnotificationcontent"/>
           <xul:label class="text-link popup-notification-learnmore-link"
--- a/toolkit/modules/PopupNotifications.jsm
+++ b/toolkit/modules/PopupNotifications.jsm
@@ -324,20 +324,22 @@ PopupNotifications.prototype = {
    *        The <xul:browser> element associated with the notification. Must not
    *        be null.
    * @param id
    *        A unique ID that identifies the type of notification (e.g.
    *        "geolocation"). Only one notification with a given ID can be visible
    *        at a time. If a notification already exists with the given ID, it
    *        will be replaced.
    * @param message
-   *        A string containing the text to be displayed as the notification header.
-   *        The string may optionally contain "<>" 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
    *        anchored to the iconBox.
    * @param mainAction
    *        A JavaScript object literal describing the notification button's
    *        action. If present, it must have the following properties:
    *          - label (string): the button's label.
@@ -455,16 +457,21 @@ PopupNotifications.prototype = {
    *                     The nsIURI of the page the notification came
    *                     from. If present, this will be displayed above the message.
    *                     If the nsIURI represents a file, the path will be displayed,
    *                     otherwise the hostPort will be displayed.
    *        name:
    *                     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,
                                          mainAction, secondaryActions, options) {
     function isInvalidAction(a) {
       return !a || !(typeof(a.callback) == "function") || !a.label || !a.accessKey;
     }
 
@@ -776,16 +783,23 @@ PopupNotifications.prototype = {
    * end:   The last part of the description message.
    */
   _formatDescriptionMessage(n) {
     let text = {};
     let array = n.message.split("<>");
     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;
   },
 
   _refreshPanel: function PopupNotifications_refreshPanel(notificationsToShow) {
     this._clearPanel();
 
     notificationsToShow.forEach(function(n) {
       let doc = this.window.document;
@@ -802,16 +816,21 @@ PopupNotifications.prototype = {
       else
         popupnotification = doc.createXULElement("popupnotification");
 
       // Create the notification description element.
       let desc = this._formatDescriptionMessage(n);
       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);
       popupnotification.setAttribute("oncommand", "PopupNotifications._onCommand(event);");
       if (Services.prefs.getBoolPref("privacy.permissionPrompts.showCloseButton")) {
         popupnotification.setAttribute("closebuttoncommand", "PopupNotifications._onButtonEvent(event, 'secondarybuttoncommand', 'esc-press');");
       } else {
         popupnotification.setAttribute("closebuttoncommand", `PopupNotifications._dismiss(event, ${TELEMETRY_STAT_DISMISSAL_CLOSE_BUTTON});`);