From 58eba8e1ac7602e2f64a8412a6662e637d00a8c2 Mon Sep 17 00:00:00 2001
From: Paul Zuehlcke <pbz@mozilla.com>
Date: Tue, 13 Apr 2021 16:11:21 +0000
Subject: [PATCH] Bug 1699668 - Refactor siteProtections blockers and add a
 warning for trackers allowed by shims. r=johannh,flod

Differential Revision: https://phabricator.services.mozilla.com/D109717
---
 .../base/content/browser-siteProtections.js   | 1139 +++++++----------
 .../content/protectionsPanel.inc.xhtml        |   32 +-
 .../en-US/browser/protectionsPanel.ftl        |    4 +
 .../themes/shared/controlcenter/panel.inc.css |   38 +-
 4 files changed, 552 insertions(+), 661 deletions(-)

diff --git a/browser/base/content/browser-siteProtections.js b/browser/base/content/browser-siteProtections.js
index 2c211a7169c25..e966a3a255cdf 100644
--- a/browser/base/content/browser-siteProtections.js
+++ b/browser/base/content/browser-siteProtections.js
@@ -17,272 +17,211 @@ XPCOMUtils.defineLazyServiceGetter(
   "nsITrackingDBService"
 );
 
-var Fingerprinting = {
-  PREF_ENABLED: "privacy.trackingprotection.fingerprinting.enabled",
-  reportBreakageLabel: "fingerprinting",
-
-  strings: {
-    get subViewBlocked() {
-      delete this.subViewBlocked;
-      return (this.subViewBlocked = gNavigatorBundle.getString(
-        "contentBlocking.fingerprintersView.blocked.label"
-      ));
-    },
-
-    get subViewTitleBlocking() {
-      delete this.subViewTitleBlocking;
-      return (this.subViewTitleBlocking = gNavigatorBundle.getString(
-        "protections.blocking.fingerprinters.title"
-      ));
-    },
+/**
+ * Represents a protection category shown in the protections UI. For the most
+ * common categories we can directly instantiate this category. Some protections
+ * categories inherit from this class and overwrite some of its members.
+ */
+class ProtectionCategory {
+  /**
+   * Creates a protection category.
+   * @param {string} id - Identifier of the category. Used to query the category
+   * UI elements in the DOM.
+   * @param {Object} options - Category options.
+   * @param {string} options.prefEnabled - ID of pref which controls the
+   * category enabled state.
+   * @param {string} [options.reportBreakageLabel] - Telemetry label to use when
+   * users report TP breakage. Defaults to protection ID.
+   * @param {string} [options.l10nId] - Identifier l10n strings are keyed under
+   * for this category. Defaults to protection ID.
+   * @param {Object} flags - Flags for this category to look for in the content
+   * blocking event and content blocking log.
+   * @param {Number} [flags.load] - Load flag for this protection category. If
+   * omitted, we will never match a isAllowing check for this category.
+   * @param {Number} [flags.block] - Block flag for this protection category. If
+   * omitted, we will never match a isBlocking check for this category.
+   * @param {Number} [flags.shim] - Shim flag for this protection category. This
+   * flag is set if we replaced tracking content with a non-tracking shim
+   * script.
+   * @param {Number} [flags.allow] - Allow flag for this protection category.
+   * This flag is set if we explicitly allow normally blocked tracking content.
+   * The webcompat extension can do this if it needs to unblock content on user
+   * opt-in.
+   */
+  constructor(
+    id,
+    { prefEnabled, reportBreakageLabel, l10nId },
+    {
+      load,
+      block,
+      shim = Ci.nsIWebProgressListener.STATE_REPLACED_TRACKING_CONTENT,
+      allow = Ci.nsIWebProgressListener.STATE_ALLOWED_TRACKING_CONTENT,
+    }
+  ) {
+    this._id = id;
+    this.prefEnabled = prefEnabled;
+    this._reportBreakageLabel = reportBreakageLabel || id;
+
+    this._flags = { load, block, shim, allow };
 
-    get subViewTitleNotBlocking() {
-      delete this.subViewTitleNotBlocking;
-      return (this.subViewTitleNotBlocking = gNavigatorBundle.getString(
-        "protections.notBlocking.fingerprinters.title"
-      ));
-    },
-  },
+    if (
+      Services.prefs.getPrefType(this.prefEnabled) == Services.prefs.PREF_BOOL
+    ) {
+      XPCOMUtils.defineLazyPreferenceGetter(
+        this,
+        "_enabled",
+        this.prefEnabled,
+        false,
+        this.updateCategoryItem.bind(this)
+      );
+    }
 
-  init() {
-    XPCOMUtils.defineLazyPreferenceGetter(
-      this,
-      "enabled",
-      this.PREF_ENABLED,
-      false,
-      this.updateCategoryItem.bind(this)
+    l10nId = l10nId || id;
+    this.strings = {};
+    XPCOMUtils.defineLazyGetter(this.strings, "subViewBlocked", () =>
+      gNavigatorBundle.getString(`contentBlocking.${l10nId}View.blocked.label`)
     );
-  },
-
-  get categoryItem() {
-    let item = document.getElementById(
-      "protections-popup-category-fingerprinters"
+    XPCOMUtils.defineLazyGetter(this.strings, "subViewTitleBlocking", () =>
+      gNavigatorBundle.getString(`protections.blocking.${l10nId}.title`)
     );
-    if (item) {
-      delete this.categoryItem;
-      this.categoryItem = item;
-    }
-    return item;
-  },
-
-  updateCategoryItem() {
-    // Can't get `this.categoryItem` without the popup. Using the popup instead
-    // of `this.categoryItem` to guard access, because the category item getter
-    // can trigger bug 1543537. If there's no popup, we'll be called again the
-    // first time the popup shows.
-    if (gProtectionsHandler._protectionsPopup) {
-      this.categoryItem.classList.toggle("blocked", this.enabled);
-    }
-  },
-
-  get subView() {
-    delete this.subView;
-    return (this.subView = document.getElementById(
-      "protections-popup-fingerprintersView"
-    ));
-  },
-
-  get subViewList() {
-    delete this.subViewList;
-    return (this.subViewList = document.getElementById(
-      "protections-popup-fingerprintersView-list"
-    ));
-  },
-
-  isBlocking(state) {
-    return (
-      (state &
-        Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT) !=
-      0
+    XPCOMUtils.defineLazyGetter(this.strings, "subViewTitleNotBlocking", () =>
+      gNavigatorBundle.getString(`protections.notBlocking.${l10nId}.title`)
     );
-  },
 
-  isAllowing(state) {
-    return (
-      (state & Ci.nsIWebProgressListener.STATE_LOADED_FINGERPRINTING_CONTENT) !=
-      0
+    XPCOMUtils.defineLazyGetter(this, "subView", () =>
+      document.getElementById(`protections-popup-${this._id}View`)
     );
-  },
-
-  isDetected(state) {
-    return this.isBlocking(state) || this.isAllowing(state);
-  },
 
-  isShimming(state) {
-    return (
-      state & Ci.nsIWebProgressListener.STATE_REPLACED_TRACKING_CONTENT &&
-      this.isAllowing(state)
+    XPCOMUtils.defineLazyGetter(this, "subViewHeading", () =>
+      document.getElementById(`protections-popup-${this._id}View-heading`)
     );
-  },
-
-  updateSubView() {
-    let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog();
-    contentBlockingLog = JSON.parse(contentBlockingLog);
 
-    let fragment = document.createDocumentFragment();
-    for (let [origin, actions] of Object.entries(contentBlockingLog)) {
-      let listItem = this._createListItem(origin, actions);
-      if (listItem) {
-        fragment.appendChild(listItem);
-      }
-    }
-
-    this.subViewList.textContent = "";
-    this.subViewList.append(fragment);
-    this.subView.setAttribute(
-      "title",
-      this.enabled && !gProtectionsHandler.hasException
-        ? this.strings.subViewTitleBlocking
-        : this.strings.subViewTitleNotBlocking
+    XPCOMUtils.defineLazyGetter(this, "subViewList", () =>
+      document.getElementById(`protections-popup-${this._id}View-list`)
     );
-  },
 
-  _createListItem(origin, actions) {
-    let isAllowed = actions.some(
-      ([state]) => this.isAllowing(state) && !this.isShimming(state)
+    XPCOMUtils.defineLazyGetter(this, "subViewShimAllowHint", () =>
+      document.getElementById(
+        `protections-popup-${this._id}View-shim-allow-hint`
+      )
     );
-    let isDetected =
-      isAllowed || actions.some(([state]) => this.isBlocking(state));
-
-    if (!isDetected) {
-      return null;
-    }
-
-    let listItem = document.createXULElement("hbox");
-    listItem.className = "protections-popup-list-item";
-    listItem.classList.toggle("allowed", isAllowed);
-    // Repeat the host in the tooltip in case it's too long
-    // and overflows in our panel.
-    listItem.tooltipText = origin;
-
-    let label = document.createXULElement("label");
-    label.value = origin;
-    label.className = "protections-popup-list-host-label";
-    label.setAttribute("crop", "end");
-    listItem.append(label);
-
-    return listItem;
-  },
-};
-
-var Cryptomining = {
-  PREF_ENABLED: "privacy.trackingprotection.cryptomining.enabled",
-  reportBreakageLabel: "cryptomining",
+  }
 
-  strings: {
-    get subViewBlocked() {
-      delete this.subViewBlocked;
-      return (this.subViewBlocked = gNavigatorBundle.getString(
-        "contentBlocking.cryptominersView.blocked.label"
-      ));
-    },
-
-    get subViewTitleBlocking() {
-      delete this.subViewTitleBlocking;
-      return (this.subViewTitleBlocking = gNavigatorBundle.getString(
-        "protections.blocking.cryptominers.title"
-      ));
-    },
+  // Child classes may override these to do init / teardown. We expect them to
+  // be called when the protections panel is initialized or destroyed.
+  init() {}
+  uninit() {}
 
-    get subViewTitleNotBlocking() {
-      delete this.subViewTitleNotBlocking;
-      return (this.subViewTitleNotBlocking = gNavigatorBundle.getString(
-        "protections.notBlocking.cryptominers.title"
-      ));
-    },
-  },
+  // Some child classes may overide this getter.
+  get enabled() {
+    return this._enabled;
+  }
 
-  init() {
-    XPCOMUtils.defineLazyPreferenceGetter(
-      this,
-      "enabled",
-      this.PREF_ENABLED,
-      false,
-      this.updateCategoryItem.bind(this)
-    );
-  },
+  get reportBreakageLabel() {
+    return this._reportBreakageLabel;
+  }
 
+  /**
+   * Get the category item associated with this protection from the main
+   * protections panel.
+   * @returns {xul:toolbarbutton|undefined} - Item or undefined if the panel is
+   * not yet initialized.
+   */
   get categoryItem() {
-    let item = document.getElementById(
-      "protections-popup-category-cryptominers"
+    // We don't use defineLazyGetter for the category item, since it may be null
+    // on first access.
+    return (
+      this._categoryItem ||
+      (this._categoryItem = document.getElementById(
+        `protections-popup-category-${this._id}`
+      ))
     );
-    if (item) {
-      delete this.categoryItem;
-      this.categoryItem = item;
-    }
-    return item;
-  },
+  }
 
+  /**
+   * Defaults to enabled state. May be overridden by child classes.
+   * @returns {boolean} - Whether the protection is set to block trackers.
+   */
+  get blockingEnabled() {
+    return this.enabled;
+  }
+
+  /**
+   * Update the category item state in the main view of the protections panel.
+   * Determines whether the category is set to block trackers.
+   * @returns {boolean} - true if the state has been updated, false if the
+   * protections popup has not been initialized yet.
+   */
   updateCategoryItem() {
     // Can't get `this.categoryItem` without the popup. Using the popup instead
     // of `this.categoryItem` to guard access, because the category item getter
     // can trigger bug 1543537. If there's no popup, we'll be called again the
     // first time the popup shows.
-    if (gProtectionsHandler._protectionsPopup) {
-      this.categoryItem.classList.toggle("blocked", this.enabled);
+    if (!gProtectionsHandler._protectionsPopup) {
+      return false;
     }
-  },
-
-  get subView() {
-    delete this.subView;
-    return (this.subView = document.getElementById(
-      "protections-popup-cryptominersView"
-    ));
-  },
-
-  get subViewList() {
-    delete this.subViewList;
-    return (this.subViewList = document.getElementById(
-      "protections-popup-cryptominersView-list"
-    ));
-  },
-
-  isBlocking(state) {
-    return (
-      (state & Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT) !=
-      0
-    );
-  },
-
-  isAllowing(state) {
-    return (
-      (state & Ci.nsIWebProgressListener.STATE_LOADED_CRYPTOMINING_CONTENT) != 0
-    );
-  },
+    this.categoryItem.classList.toggle("blocked", this.enabled);
+    return true;
+  }
 
-  isDetected(state) {
-    return this.isBlocking(state) || this.isAllowing(state);
-  },
+  /**
+   * Update the category sub view that is shown when users click on the category
+   * button.
+   */
+  async updateSubView() {
+    let { items, anyShimAllowed } = await this._generateSubViewListItems();
+    this.subViewShimAllowHint.hidden = !anyShimAllowed;
 
-  isShimming(state) {
-    return (
-      state & Ci.nsIWebProgressListener.STATE_REPLACED_TRACKING_CONTENT &&
-      this.isAllowing(state)
+    this.subViewList.textContent = "";
+    this.subViewList.append(items);
+    this.subView.setAttribute(
+      "title",
+      this.blockingEnabled && !gProtectionsHandler.hasException
+        ? this.strings.subViewTitleBlocking
+        : this.strings.subViewTitleNotBlocking
     );
-  },
+  }
 
-  updateSubView() {
+  /**
+   * Create a list of items, each representing a tracker.
+   * @returns {Object} result - An object containing the results.
+   * @returns {HTMLDivElement[]} result.items - Generated tracker items. May be
+   * empty.
+   * @returns {boolean} result.anyShimAllowed - Flag indicating if any of the
+   * items have been unblocked by a shim script.
+   */
+  async _generateSubViewListItems() {
     let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog();
     contentBlockingLog = JSON.parse(contentBlockingLog);
+    let anyShimAllowed = false;
 
     let fragment = document.createDocumentFragment();
     for (let [origin, actions] of Object.entries(contentBlockingLog)) {
-      let listItem = this._createListItem(origin, actions);
-      if (listItem) {
-        fragment.appendChild(listItem);
+      let { item, shimAllowed } = await this._createListItem(origin, actions);
+      if (!item) {
+        continue;
       }
+      anyShimAllowed = anyShimAllowed || shimAllowed;
+      fragment.appendChild(item);
     }
 
-    this.subViewList.textContent = "";
-    this.subViewList.append(fragment);
-    this.subView.setAttribute(
-      "title",
-      this.enabled && !gProtectionsHandler.hasException
-        ? this.strings.subViewTitleBlocking
-        : this.strings.subViewTitleNotBlocking
-    );
-  },
+    return {
+      items: fragment,
+      anyShimAllowed,
+    };
+  }
 
+  /**
+   * Create a DOM item representing a tracker.
+   * @param {string} origin - Origin of the tracker.
+   * @param {Array} actions - Array of actions from the content blocking log
+   * associated with the tracking origin.
+   * @returns {Object} result - An object containing the results.
+   * @returns {HTMLDListElement} [options.item] - Generated item or null if we
+   * don't have an item for this origin based on the actions log.
+   * @returns {boolean} options.shimAllowed - Flag indicating whether the
+   * tracking origin was allowed by a shim script.
+   */
   _createListItem(origin, actions) {
     let isAllowed = actions.some(
       ([state]) => this.isAllowing(state) && !this.isShimming(state)
@@ -291,125 +230,184 @@ var Cryptomining = {
       isAllowed || actions.some(([state]) => this.isBlocking(state));
 
     if (!isDetected) {
-      return null;
+      return {};
     }
 
-    let listItem = document.createXULElement("hbox");
+    // Create an item to hold the origin label and shim allow indicator. Using
+    // an html element here, so we can use CSS flex rather than -moz-box, which
+    // handles the label overflow in combination with the icon correctly.
+    let listItem = document.createElementNS(
+      "http://www.w3.org/1999/xhtml",
+      "div"
+    );
     listItem.className = "protections-popup-list-item";
     listItem.classList.toggle("allowed", isAllowed);
-    // Repeat the host in the tooltip in case it's too long
-    // and overflows in our panel.
-    listItem.tooltipText = origin;
 
     let label = document.createXULElement("label");
+    // Repeat the host in the tooltip in case it's too long
+    // and overflows in our panel.
+    label.tooltipText = origin;
     label.value = origin;
     label.className = "protections-popup-list-host-label";
     label.setAttribute("crop", "end");
     listItem.append(label);
 
-    return listItem;
-  },
-};
+    // Determine whether we should show a shim-allow indicator for this item.
+    let shimAllowed = actions.some(([flag]) => flag == this._flags.allow);
+    if (shimAllowed) {
+      listItem.append(this._getShimAllowIndicator());
+    }
 
-var TrackingProtection = {
-  reportBreakageLabel: "trackingprotection",
-  PREF_ENABLED_GLOBALLY: "privacy.trackingprotection.enabled",
-  PREF_ENABLED_IN_PRIVATE_WINDOWS: "privacy.trackingprotection.pbmode.enabled",
-  PREF_TRACKING_TABLE: "urlclassifier.trackingTable",
-  PREF_TRACKING_ANNOTATION_TABLE: "urlclassifier.trackingAnnotationTable",
-  PREF_ANNOTATIONS_LEVEL_2_ENABLED:
-    "privacy.annotate_channels.strict_list.enabled",
-  enabledGlobally: false,
-  enabledInPrivateWindows: false,
+    return { item: listItem, shimAllowed };
+  }
 
-  get categoryItem() {
-    let item = document.getElementById(
-      "protections-popup-category-tracking-protection"
+  /**
+   * Create an indicator icon for marking origins that have been allowed by a
+   * shim script.
+   * @returns {HTMLImageElement} - Created element.
+   */
+  _getShimAllowIndicator() {
+    let allowIndicator = document.createXULElement("image");
+    document.l10n.setAttributes(
+      allowIndicator,
+      "protections-panel-shim-allowed-indicator"
     );
-    if (item) {
-      delete this.categoryItem;
-      this.categoryItem = item;
-    }
-    return item;
-  },
+    allowIndicator.tooltipText = this.strings.subViewShimAllowedTooltip;
+    allowIndicator.classList.add(
+      "protections-popup-list-host-shim-allow-indicator"
+    );
+    return allowIndicator;
+  }
 
-  get subView() {
-    delete this.subView;
-    return (this.subView = document.getElementById(
-      "protections-popup-trackersView"
-    ));
-  },
+  /**
+   * @param {Number} state - Content blocking event flags.
+   * @returns {boolean} - Whether the protection has blocked a tracker.
+   */
+  isBlocking(state) {
+    return (state & this._flags.block) != 0;
+  }
 
-  get subViewList() {
-    delete this.subViewList;
-    return (this.subViewList = document.getElementById(
-      "protections-popup-trackersView-list"
-    ));
-  },
+  /**
+   * @param {Number} state - Content blocking event flags.
+   * @returns {boolean} - Whether the protection has allowed a tracker.
+   */
+  isAllowing(state) {
+    return (state & this._flags.load) != 0;
+  }
 
-  strings: {
-    get subViewBlocked() {
-      delete this.subViewBlocked;
-      return (this.subViewBlocked = gNavigatorBundle.getString(
-        "contentBlocking.trackersView.blocked.label"
-      ));
-    },
+  /**
+   * @param {Number} state - Content blocking event flags.
+   * @returns {boolean} - Whether the protection has detected (blocked or
+   * allowed) a tracker.
+   */
+  isDetected(state) {
+    return this.isBlocking(state) || this.isAllowing(state);
+  }
 
-    get subViewTitleBlocking() {
-      delete this.subViewTitleBlocking;
-      return (this.subViewTitleBlocking = gNavigatorBundle.getString(
-        "protections.blocking.trackingContent.title"
-      ));
-    },
+  /**
+   * @param {Number} state - Content blocking event flags.
+   * @returns {boolean} - Whether the protections has allowed a tracker that
+   * would have normally been blocked.
+   */
+  isShimming(state) {
+    return (state & this._flags.shim) != 0 && this.isAllowing(state);
+  }
+}
+
+let Fingerprinting = new ProtectionCategory(
+  "fingerprinters",
+  {
+    prefEnabled: "privacy.trackingprotection.fingerprinting.enabled",
+    reportBreakageLabel: "fingerprinting",
+  },
+  {
+    load: Ci.nsIWebProgressListener.STATE_LOADED_FINGERPRINTING_CONTENT,
+    block: Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT,
+  }
+);
 
-    get subViewTitleNotBlocking() {
-      delete this.subViewTitleNotBlocking;
-      return (this.subViewTitleNotBlocking = gNavigatorBundle.getString(
-        "protections.notBlocking.trackingContent.title"
-      ));
-    },
+let Cryptomining = new ProtectionCategory(
+  "cryptominers",
+  {
+    prefEnabled: "privacy.trackingprotection.cryptomining.enabled",
+    reportBreakageLabel: "cryptomining",
   },
+  {
+    load: Ci.nsIWebProgressListener.STATE_LOADED_CRYPTOMINING_CONTENT,
+    block: Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT,
+  }
+);
 
-  init() {
-    this.updateEnabled();
+let TrackingProtection = new (class TrackingProtection extends ProtectionCategory {
+  constructor() {
+    super(
+      "trackers",
+      {
+        l10nId: "trackingContent",
+        prefEnabled: "privacy.trackingprotection.enabled",
+        reportBreakageLabel: "trackingprotection",
+      },
+      {
+        load: null,
+        block: Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT,
+      }
+    );
+
+    // Blocked label has custom key, overwrite the getter.
+    XPCOMUtils.defineLazyGetter(this.strings, "subViewBlocked", () =>
+      gNavigatorBundle.getString("contentBlocking.trackersView.blocked.label")
+    );
 
-    Services.prefs.addObserver(this.PREF_ENABLED_GLOBALLY, this);
-    Services.prefs.addObserver(this.PREF_ENABLED_IN_PRIVATE_WINDOWS, this);
+    this.prefEnabledInPrivateWindows =
+      "privacy.trackingprotection.pbmode.enabled";
+    this.prefTrackingTable = "urlclassifier.trackingTable";
+    this.prefTrackingAnnotationTable = "urlclassifier.trackingAnnotationTable";
+    this.prefAnnotationsLevel2Enabled =
+      "privacy.annotate_channels.strict_list.enabled";
+    this.enabledGlobally = false;
+    this.enabledInPrivateWindows = false;
 
     XPCOMUtils.defineLazyPreferenceGetter(
       this,
       "trackingTable",
-      this.PREF_TRACKING_TABLE,
+      this.prefTrackingTable,
       ""
     );
     XPCOMUtils.defineLazyPreferenceGetter(
       this,
       "trackingAnnotationTable",
-      this.PREF_TRACKING_ANNOTATION_TABLE,
+      this.prefTrackingAnnotationTable,
       ""
     );
     XPCOMUtils.defineLazyPreferenceGetter(
       this,
       "annotationsLevel2Enabled",
-      this.PREF_ANNOTATIONS_LEVEL_2_ENABLED,
+      this.prefAnnotationsLevel2Enabled,
       false
     );
-  },
+  }
+
+  init() {
+    this.updateEnabled();
+
+    Services.prefs.addObserver(this.prefEnabled, this);
+    Services.prefs.addObserver(this.prefEnabledInPrivateWindows, this);
+  }
 
   uninit() {
-    Services.prefs.removeObserver(this.PREF_ENABLED_GLOBALLY, this);
-    Services.prefs.removeObserver(this.PREF_ENABLED_IN_PRIVATE_WINDOWS, this);
-  },
+    Services.prefs.removeObserver(this.prefEnabled, this);
+    Services.prefs.removeObserver(this.prefEnabledInPrivateWindows, this);
+  }
 
   observe() {
     this.updateEnabled();
     this.updateCategoryItem();
-  },
+  }
 
   get trackingProtectionLevel2Enabled() {
     const CONTENT_TABLE = "content-track-digest256";
     return this.trackingTable.includes(CONTENT_TABLE);
-  },
+  }
 
   get enabled() {
     return (
@@ -417,28 +415,14 @@ var TrackingProtection = {
       (this.enabledInPrivateWindows &&
         PrivateBrowsingUtils.isWindowPrivate(window))
     );
-  },
+  }
 
   updateEnabled() {
-    this.enabledGlobally = Services.prefs.getBoolPref(
-      this.PREF_ENABLED_GLOBALLY
-    );
+    this.enabledGlobally = Services.prefs.getBoolPref(this.prefEnabled);
     this.enabledInPrivateWindows = Services.prefs.getBoolPref(
-      this.PREF_ENABLED_IN_PRIVATE_WINDOWS
+      this.prefEnabledInPrivateWindows
     );
-  },
-
-  updateCategoryItem() {
-    if (this.categoryItem) {
-      this.categoryItem.classList.toggle("blocked", this.enabled);
-    }
-  },
-
-  isBlocking(state) {
-    return (
-      (state & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT) != 0
-    );
-  },
+  }
 
   isAllowingLevel1(state) {
     return (
@@ -446,7 +430,7 @@ var TrackingProtection = {
         Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT) !=
       0
     );
-  },
+  }
 
   isAllowingLevel2(state) {
     return (
@@ -454,37 +438,17 @@ var TrackingProtection = {
         Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_2_TRACKING_CONTENT) !=
       0
     );
-  },
+  }
 
   isAllowing(state) {
     return this.isAllowingLevel1(state) || this.isAllowingLevel2(state);
-  },
-
-  isDetected(state) {
-    return this.isBlocking(state) || this.isAllowing(state);
-  },
-
-  isShimming(state) {
-    return (
-      state & Ci.nsIWebProgressListener.STATE_REPLACED_TRACKING_CONTENT &&
-      this.isAllowing(state)
-    );
-  },
+  }
 
   async updateSubView() {
     let previousURI = gBrowser.currentURI.spec;
     let previousWindow = gBrowser.selectedBrowser.innerWindowID;
 
-    let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog();
-    contentBlockingLog = JSON.parse(contentBlockingLog);
-
-    let fragment = document.createDocumentFragment();
-    for (let [origin, actions] of Object.entries(contentBlockingLog)) {
-      let listItem = await this._createListItem(origin, actions);
-      if (listItem) {
-        fragment.appendChild(listItem);
-      }
-    }
+    let { items, anyShimAllowed } = await this._generateSubViewListItems();
 
     // If we don't have trackers we would usually not show the menu item
     // allowing the user to show the sub-panel. However, in the edge case
@@ -492,11 +456,11 @@ var TrackingProtection = {
     // not detect trackers on the page using the basic list, we currently
     // still show the panel. To reduce the confusion, tell the user that we have
     // not detected any tracker.
-    if (!fragment.childNodes.length) {
+    if (!items.childNodes.length) {
       let emptyBox = document.createXULElement("vbox");
       let emptyImage = document.createXULElement("image");
       emptyImage.classList.add("protections-popup-trackersView-empty-image");
-      emptyImage.classList.add("tracking-protection-icon");
+      emptyImage.classList.add("trackers-icon");
 
       let emptyLabel = document.createXULElement("label");
       emptyLabel.classList.add("protections-popup-empty-label");
@@ -506,7 +470,7 @@ var TrackingProtection = {
 
       emptyBox.appendChild(emptyImage);
       emptyBox.appendChild(emptyLabel);
-      fragment.appendChild(emptyBox);
+      items.appendChild(emptyBox);
 
       this.subViewList.classList.add("empty");
     } else {
@@ -518,8 +482,10 @@ var TrackingProtection = {
       previousURI == gBrowser.currentURI.spec &&
       previousWindow == gBrowser.selectedBrowser.innerWindowID
     ) {
+      this.subViewShimAllowHint.hidden = !anyShimAllowed;
+
       this.subViewList.textContent = "";
-      this.subViewList.append(fragment);
+      this.subViewList.append(items);
       this.subView.setAttribute(
         "title",
         this.enabled && !gProtectionsHandler.hasException
@@ -527,7 +493,7 @@ var TrackingProtection = {
           : this.strings.subViewTitleNotBlocking
       );
     }
-  },
+  }
 
   async _createListItem(origin, actions) {
     // Figure out if this list entry was actually detected by TP or something else.
@@ -538,7 +504,7 @@ var TrackingProtection = {
       isAllowed || actions.some(([state]) => this.isBlocking(state));
 
     if (!isDetected) {
-      return null;
+      return {};
     }
 
     // Because we might use different lists for annotation vs. blocking, we
@@ -554,89 +520,81 @@ var TrackingProtection = {
           0
       )
     ) {
-      return null;
+      return {};
     }
 
-    let listItem = document.createXULElement("hbox");
+    let listItem = document.createElementNS(
+      "http://www.w3.org/1999/xhtml",
+      "div"
+    );
     listItem.className = "protections-popup-list-item";
     listItem.classList.toggle("allowed", isAllowed);
-    // Repeat the host in the tooltip in case it's too long
-    // and overflows in our panel.
-    listItem.tooltipText = origin;
 
     let label = document.createXULElement("label");
+    // Repeat the host in the tooltip in case it's too long
+    // and overflows in our panel.
+    label.tooltipText = origin;
     label.value = origin;
     label.className = "protections-popup-list-host-label";
     label.setAttribute("crop", "end");
-    listItem.append(label);
-
-    return listItem;
-  },
-};
-
-var ThirdPartyCookies = {
-  PREF_ENABLED: "network.cookie.cookieBehavior",
-  PREF_ENABLED_VALUES: [
-    // These values match the ones exposed under the Content Blocking section
-    // of the Preferences UI.
-    Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, // Block all third-party cookies
-    Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, // Block third-party cookies from trackers
-    Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, // Block trackers and patition third-party trackers
-    Ci.nsICookieService.BEHAVIOR_REJECT, // Block all cookies
-  ],
+    listItem.append(label);
 
-  get categoryItem() {
-    let item = document.getElementById("protections-popup-category-cookies");
-    if (item) {
-      delete this.categoryItem;
-      this.categoryItem = item;
+    let shimAllowed = actions.some(([flag]) => flag == this._flags.allow);
+    if (shimAllowed) {
+      listItem.append(this._getShimAllowIndicator());
     }
-    return item;
-  },
 
-  get subView() {
-    delete this.subView;
-    return (this.subView = document.getElementById(
-      "protections-popup-cookiesView"
-    ));
-  },
+    return { item: listItem, shimAllowed };
+  }
+})();
 
-  get subViewHeading() {
-    delete this.subViewHeading;
-    return (this.subViewHeading = document.getElementById(
-      "protections-popup-cookiesView-heading"
-    ));
-  },
+let ThirdPartyCookies = new (class ThirdPartyCookies extends ProtectionCategory {
+  constructor() {
+    super(
+      "cookies",
+      {
+        // This would normally expect a boolean pref. However, this category
+        // overwrites the enabled getter for custom handling of cookie behavior
+        // states.
+        prefEnabled: "network.cookie.cookieBehavior",
+      },
+      {
+        // ThirdPartyCookies implements custom flag processing.
+        allow: null,
+        shim: null,
+        load: null,
+        block: null,
+      }
+    );
 
-  get subViewList() {
-    delete this.subViewList;
-    return (this.subViewList = document.getElementById(
-      "protections-popup-cookiesView-list"
-    ));
-  },
+    XPCOMUtils.defineLazyGetter(this, "categoryLabel", () =>
+      document.getElementById("protections-popup-cookies-category-label")
+    );
 
-  strings: {
-    get subViewAllowed() {
-      delete this.subViewAllowed;
-      return (this.subViewAllowed = gNavigatorBundle.getString(
-        "contentBlocking.cookiesView.allowed.label"
-      ));
-    },
+    // Not blocking title has custom key, overwrite the getter.
+    XPCOMUtils.defineLazyGetter(this.strings, "subViewTitleNotBlocking", () =>
+      gNavigatorBundle.getString(
+        "protections.notBlocking.crossSiteTrackingCookies.title"
+      )
+    );
 
-    get subViewBlocked() {
-      delete this.subViewAllowed;
-      return (this.subViewAllowed = gNavigatorBundle.getString(
-        "contentBlocking.cookiesView.blocked.label"
-      ));
-    },
+    this.prefEnabledValues = [
+      // These values match the ones exposed under the Content Blocking section
+      // of the Preferences UI.
+      Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, // Block all third-party cookies
+      Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, // Block third-party cookies from trackers
+      Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, // Block trackers and patition third-party trackers
+      Ci.nsICookieService.BEHAVIOR_REJECT, // Block all cookies
+    ];
 
-    get subViewTitleNotBlocking() {
-      delete this.subViewTitleNotBlocking;
-      return (this.subViewTitleNotBlocking = gNavigatorBundle.getString(
-        "protections.notBlocking.crossSiteTrackingCookies.title"
-      ));
-    },
-  },
+    XPCOMUtils.defineLazyPreferenceGetter(
+      this,
+      "behaviorPref",
+      this.prefEnabled,
+      Ci.nsICookieService.BEHAVIOR_ACCEPT,
+      this.updateCategoryItem.bind(this)
+    );
+  }
 
   get reportBreakageLabel() {
     switch (this.behaviorPref) {
@@ -658,35 +616,50 @@ var ThirdPartyCookies = {
       case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN:
         return "cookierestrictionsforeignpartitioned";
     }
-  },
+  }
 
-  init() {
-    XPCOMUtils.defineLazyPreferenceGetter(
-      this,
-      "behaviorPref",
-      this.PREF_ENABLED,
-      Ci.nsICookieService.BEHAVIOR_ACCEPT,
-      this.updateCategoryItem.bind(this)
+  isBlocking(state) {
+    return (
+      (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER) != 0 ||
+      (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER) !=
+        0 ||
+      (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL) != 0 ||
+      (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION) !=
+        0 ||
+      (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN) != 0
     );
-  },
+  }
 
-  get categoryLabel() {
-    delete this.categoryLabel;
-    return (this.categoryLabel = document.getElementById(
-      "protections-popup-cookies-category-label"
-    ));
-  },
+  isDetected(state) {
+    if (this.isBlocking(state)) {
+      return true;
+    }
+
+    if (
+      [
+        Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+        Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+        Ci.nsICookieService.BEHAVIOR_ACCEPT,
+      ].includes(this.behaviorPref)
+    ) {
+      return (
+        (state & Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_TRACKER) != 0 ||
+        (SocialTracking.enabled &&
+          (state &
+            Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER) !=
+            0)
+      );
+    }
+
+    // We don't have specific flags for the other cookie behaviors so just
+    // fall back to STATE_COOKIES_LOADED.
+    return (state & Ci.nsIWebProgressListener.STATE_COOKIES_LOADED) != 0;
+  }
 
   updateCategoryItem() {
-    // Can't get `this.categoryItem` without the popup. Using the popup instead
-    // of `this.categoryItem` to guard access, because the category item getter
-    // can trigger bug 1543537. If there's no popup, we'll be called again the
-    // first time the popup shows.
-    if (!gProtectionsHandler._protectionsPopup) {
+    if (!super.updateCategoryItem()) {
       return;
     }
-    this.categoryItem.classList.toggle("blocked", this.enabled);
-
     let label;
 
     if (!this.enabled) {
@@ -716,49 +689,11 @@ var ThirdPartyCookies = {
     this.categoryLabel.textContent = label
       ? gNavigatorBundle.getString(label)
       : "";
-  },
+  }
 
   get enabled() {
-    return this.PREF_ENABLED_VALUES.includes(this.behaviorPref);
-  },
-
-  isBlocking(state) {
-    return (
-      (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER) != 0 ||
-      (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER) !=
-        0 ||
-      (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL) != 0 ||
-      (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION) !=
-        0 ||
-      (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN) != 0
-    );
-  },
-
-  isDetected(state) {
-    if (this.isBlocking(state)) {
-      return true;
-    }
-
-    if (
-      [
-        Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
-        Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
-        Ci.nsICookieService.BEHAVIOR_ACCEPT,
-      ].includes(this.behaviorPref)
-    ) {
-      return (
-        (state & Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_TRACKER) != 0 ||
-        (SocialTracking.enabled &&
-          (state &
-            Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER) !=
-            0)
-      );
-    }
-
-    // We don't have specific flags for the other cookie behaviors so just
-    // fall back to STATE_COOKIES_LOADED.
-    return (state & Ci.nsIWebProgressListener.STATE_COOKIES_LOADED) != 0;
-  },
+    return this.prefEnabledValues.includes(this.behaviorPref);
+  }
 
   updateSubView() {
     let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog();
@@ -840,7 +775,7 @@ var ThirdPartyCookies = {
     }
 
     this.subView.setAttribute("title", gNavigatorBundle.getString(title));
-  },
+  }
 
   _getExceptionState(origin) {
     for (let perm of Services.perms.getAllForPrincipal(
@@ -857,7 +792,7 @@ var ThirdPartyCookies = {
     // Cookie exceptions get "inherited" from parent- to sub-domain, so we need to
     // make sure to include parent domains in the permission check for "cookie".
     return Services.perms.testPermissionFromPrincipal(principal, "cookie");
-  },
+  }
 
   _clearException(origin) {
     for (let perm of Services.perms.getAllForPrincipal(
@@ -881,7 +816,7 @@ var ThirdPartyCookies = {
         Services.perms.removePermission(perm);
       }
     }
-  },
+  }
 
   // Transforms and filters cookie entries in the content blocking log
   // so that we can categorize and display them in the UI.
@@ -965,10 +900,13 @@ var ThirdPartyCookies = {
     }
 
     return newLog;
-  },
+  }
 
   _createListItem({ origin, isAllowed, exceptionState }) {
-    let listItem = document.createXULElement("hbox");
+    let listItem = document.createElementNS(
+      "http://www.w3.org/1999/xhtml",
+      "div"
+    );
     listItem.className = "protections-popup-list-item";
     // Repeat the origin in the tooltip in case it's too long
     // and overflows in our panel.
@@ -1018,50 +956,39 @@ var ThirdPartyCookies = {
     }
 
     return listItem;
-  },
-};
-
-var SocialTracking = {
-  PREF_STP_TP_ENABLED: "privacy.trackingprotection.socialtracking.enabled",
-  PREF_STP_COOKIE_ENABLED: "privacy.socialtracking.block_cookies.enabled",
-  PREF_COOKIE_BEHAVIOR: "network.cookie.cookieBehavior",
-  reportBreakageLabel: "socialtracking",
-
-  strings: {
-    get subViewBlocked() {
-      delete this.subViewBlocked;
-      return (this.subViewBlocked = gNavigatorBundle.getString(
-        "contentBlocking.fingerprintersView.blocked.label"
-      ));
-    },
+  }
+})();
 
-    get subViewTitleBlocking() {
-      delete this.subViewTitleBlocking;
-      return (this.subViewTitleBlocking = gNavigatorBundle.getString(
-        "protections.blocking.socialMediaTrackers.title"
-      ));
-    },
+let SocialTracking = new (class SocialTrackingProtection extends ProtectionCategory {
+  constructor() {
+    super(
+      "socialblock",
+      {
+        l10nId: "socialMediaTrackers",
+        prefEnabled: "privacy.socialtracking.block_cookies.enabled",
+        reportBreakageLabel: "socialtracking",
+      },
+      {
+        load: Ci.nsIWebProgressListener.STATE_LOADED_SOCIALTRACKING_CONTENT,
+        block: Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT,
+      }
+    );
 
-    get subViewTitleNotBlocking() {
-      delete this.subViewTitleNotBlocking;
-      return (this.subViewTitleNotBlocking = gNavigatorBundle.getString(
-        "protections.notBlocking.socialMediaTrackers.title"
-      ));
-    },
-  },
+    this.prefStpTpEnabled = "privacy.trackingprotection.socialtracking.enabled";
+    this.prefSTPCookieEnabled = this.prefEnabled;
+    this.prefCookieBehavior = "network.cookie.cookieBehavior";
 
-  init() {
     XPCOMUtils.defineLazyPreferenceGetter(
       this,
       "socialTrackingProtectionEnabled",
-      this.PREF_STP_TP_ENABLED,
+      this.prefStpTpEnabled,
       false,
       this.updateCategoryItem.bind(this)
     );
     XPCOMUtils.defineLazyPreferenceGetter(
       this,
       "rejectTrackingCookies",
-      this.PREF_COOKIE_BEHAVIOR,
+      this.prefCookieBehavior,
       null,
       this.updateCategoryItem.bind(this),
       val =>
@@ -1070,149 +997,53 @@ var SocialTracking = {
           Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
         ].includes(val)
     );
-    XPCOMUtils.defineLazyPreferenceGetter(
-      this,
-      "enabled",
-      this.PREF_STP_COOKIE_ENABLED,
-      false,
-      this.updateCategoryItem.bind(this)
-    );
-  },
+  }
 
   get blockingEnabled() {
     return (
       (this.socialTrackingProtectionEnabled || this.rejectTrackingCookies) &&
       this.enabled
     );
-  },
+  }
 
-  updateCategoryItem() {
-    // Can't get `this.categoryItem` without the popup. Using the popup instead
-    // of `this.categoryItem` to guard access, because the category item getter
-    // can trigger bug 1543537. If there's no popup, we'll be called again the
-    // first time the popup shows.
-    if (!gProtectionsHandler._protectionsPopup) {
-      return;
-    }
-    if (this.enabled) {
-      this.categoryItem.removeAttribute("uidisabled");
-    } else {
-      this.categoryItem.setAttribute("uidisabled", true);
-    }
-    this.categoryItem.classList.toggle("blocked", this.blockingEnabled);
-  },
+  isBlockingCookies(state) {
+    return (
+      (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER) !=
+      0
+    );
+  }
 
   isBlocking(state) {
-    let socialtrackingContentBlocked =
-      (state &
-        Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT) !=
-      0;
-    let socialtrackingCookieBlocked =
-      (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER) !=
-      0;
-    return socialtrackingCookieBlocked || socialtrackingContentBlocked;
-  },
+    return super.isBlocking(state) || this.isBlockingCookies(state);
+  }
 
   isAllowing(state) {
     if (this.socialTrackingProtectionEnabled) {
-      return (
-        (state &
-          Ci.nsIWebProgressListener.STATE_LOADED_SOCIALTRACKING_CONTENT) !=
-        0
-      );
+      return super.isAllowing(state);
     }
 
     return (
       (state & Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER) !=
       0
     );
-  },
-
-  isDetected(state) {
-    return this.isBlocking(state) || this.isAllowing(state);
-  },
-
-  isShimming(state) {
-    return (
-      state & Ci.nsIWebProgressListener.STATE_REPLACED_TRACKING_CONTENT &&
-      this.isAllowing(state)
-    );
-  },
-
-  get categoryItem() {
-    let item = document.getElementById(
-      "protections-popup-category-socialblock"
-    );
-    if (item) {
-      delete this.categoryItem;
-      this.categoryItem = item;
-    }
-    return item;
-  },
-
-  get subView() {
-    delete this.subView;
-    return (this.subView = document.getElementById(
-      "protections-popup-socialblockView"
-    ));
-  },
-
-  get subViewList() {
-    delete this.subViewList;
-    return (this.subViewList = document.getElementById(
-      "protections-popup-socialblockView-list"
-    ));
-  },
+  }
 
-  updateSubView() {
-    let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog();
-    contentBlockingLog = JSON.parse(contentBlockingLog);
-
-    let fragment = document.createDocumentFragment();
-    for (let [origin, actions] of Object.entries(contentBlockingLog)) {
-      let listItem = this._createListItem(origin, actions);
-      if (listItem) {
-        fragment.appendChild(listItem);
-      }
+  updateCategoryItem() {
+    // Can't get `this.categoryItem` without the popup. Using the popup instead
+    // of `this.categoryItem` to guard access, because the category item getter
+    // can trigger bug 1543537. If there's no popup, we'll be called again the
+    // first time the popup shows.
+    if (!gProtectionsHandler._protectionsPopup) {
+      return;
     }
-
-    this.subViewList.textContent = "";
-    this.subViewList.append(fragment);
-    this.subView.setAttribute(
-      "title",
-      this.blockingEnabled && !gProtectionsHandler.hasException
-        ? this.strings.subViewTitleBlocking
-        : this.strings.subViewTitleNotBlocking
-    );
-  },
-
-  _createListItem(origin, actions) {
-    let isAllowed = actions.some(
-      ([state]) => this.isAllowing(state) && !this.isShimming(state)
-    );
-    let isDetected =
-      isAllowed || actions.some(([state]) => this.isBlocking(state));
-
-    if (!isDetected) {
-      return null;
+    if (this.enabled) {
+      this.categoryItem.removeAttribute("uidisabled");
+    } else {
+      this.categoryItem.setAttribute("uidisabled", true);
     }
-
-    let listItem = document.createXULElement("hbox");
-    listItem.className = "protections-popup-list-item";
-    listItem.classList.toggle("allowed", isAllowed);
-    // Repeat the host in the tooltip in case it's too long
-    // and overflows in our panel.
-    listItem.tooltipText = origin;
-
-    let label = document.createXULElement("label");
-    label.value = origin;
-    label.className = "protections-popup-list-host-label";
-    label.setAttribute("crop", "end");
-    listItem.append(label);
-
-    return listItem;
-  },
-};
+    this.categoryItem.classList.toggle("blocked", this.blockingEnabled);
+  }
+})();
 
 /**
  * Utility object to handle manipulations of the protections indicators in the UI
@@ -1230,7 +1061,7 @@ var gProtectionsHandler = {
 
       this.maybeSetMilestoneCounterText();
 
-      for (let blocker of this.blockers) {
+      for (let blocker of Object.values(this.blockers)) {
         blocker.updateCategoryItem();
       }
 
@@ -1449,13 +1280,13 @@ var gProtectionsHandler = {
   // It may also contain an init() and uninit() function, which will be called
   // on gProtectionsHandler.init() and gProtectionsHandler.uninit().
   // The buttons in the protections panel will appear in the same order as this array.
-  blockers: [
+  blockers: {
     SocialTracking,
     ThirdPartyCookies,
     TrackingProtection,
     Fingerprinting,
     Cryptomining,
-  ],
+  },
 
   init() {
     this.animatedIcon.addEventListener("animationend", () =>
@@ -1503,7 +1334,7 @@ var gProtectionsHandler = {
       () => this.maybeSetMilestoneCounterText()
     );
 
-    for (let blocker of this.blockers) {
+    for (let blocker of Object.values(this.blockers)) {
       if (blocker.init) {
         blocker.init();
       }
@@ -1514,7 +1345,7 @@ var gProtectionsHandler = {
   },
 
   uninit() {
-    for (let blocker of this.blockers) {
+    for (let blocker of Object.values(this.blockers)) {
       if (blocker.uninit) {
         blocker.uninit();
       }
@@ -1789,7 +1620,7 @@ var gProtectionsHandler = {
    */
   updatePanelForBlockingEvent(event, isShown) {
     // Update the categories:
-    for (let blocker of this.blockers) {
+    for (let blocker of Object.values(this.blockers)) {
       if (blocker.categoryItem.hasAttribute("uidisabled")) {
         continue;
       }
@@ -1884,7 +1715,7 @@ var gProtectionsHandler = {
     );
 
     // Update blocker state and find if they detected or blocked anything.
-    for (let blocker of this.blockers) {
+    for (let blocker of Object.values(this.blockers)) {
       if (blocker.categoryItem?.hasAttribute("uidisabled")) {
         continue;
       }
@@ -2074,7 +1905,7 @@ var gProtectionsHandler = {
     this._protectionsPopupNotBlockingHeader.hidden = true;
     this._protectionsPopupNotFoundHeader.hidden = true;
 
-    for (let { categoryItem } of this.blockers) {
+    for (let { categoryItem } of Object.values(this.blockers)) {
       if (
         categoryItem.classList.contains("notFound") ||
         categoryItem.hasAttribute("uidisabled")
@@ -2425,15 +2256,13 @@ var gProtectionsHandler = {
     body += `userAgent: ${navigator.userAgent}\n`;
 
     body += "\n**Preferences**\n";
-    body += `${
-      TrackingProtection.PREF_ENABLED_GLOBALLY
-    }: ${Services.prefs.getBoolPref(
-      TrackingProtection.PREF_ENABLED_GLOBALLY
+    body += `${TrackingProtection.prefEnabled}: ${Services.prefs.getBoolPref(
+      TrackingProtection.prefEnabled
     )}\n`;
     body += `${
-      TrackingProtection.PREF_ENABLED_IN_PRIVATE_WINDOWS
+      TrackingProtection.prefEnabledInPrivateWindows
     }: ${Services.prefs.getBoolPref(
-      TrackingProtection.PREF_ENABLED_IN_PRIVATE_WINDOWS
+      TrackingProtection.prefEnabledInPrivateWindows
     )}\n`;
     body += `urlclassifier.trackingTable: ${Services.prefs.getStringPref(
       "urlclassifier.trackingTable"
@@ -2444,8 +2273,8 @@ var gProtectionsHandler = {
     body += `network.http.referer.defaultPolicy.pbmode: ${Services.prefs.getIntPref(
       "network.http.referer.defaultPolicy.pbmode"
     )}\n`;
-    body += `${ThirdPartyCookies.PREF_ENABLED}: ${Services.prefs.getIntPref(
-      ThirdPartyCookies.PREF_ENABLED
+    body += `${ThirdPartyCookies.prefEnabled}: ${Services.prefs.getIntPref(
+      ThirdPartyCookies.prefEnabled
     )}\n`;
     body += `network.cookie.lifetimePolicy: ${Services.prefs.getIntPref(
       "network.cookie.lifetimePolicy"
@@ -2456,11 +2285,11 @@ var gProtectionsHandler = {
     body += `privacy.restrict3rdpartystorage.expiration: ${Services.prefs.getIntPref(
       "privacy.restrict3rdpartystorage.expiration"
     )}\n`;
-    body += `${Fingerprinting.PREF_ENABLED}: ${Services.prefs.getBoolPref(
-      Fingerprinting.PREF_ENABLED
+    body += `${Fingerprinting.prefEnabled}: ${Services.prefs.getBoolPref(
+      Fingerprinting.prefEnabled
     )}\n`;
-    body += `${Cryptomining.PREF_ENABLED}: ${Services.prefs.getBoolPref(
-      Cryptomining.PREF_ENABLED
+    body += `${Cryptomining.prefEnabled}: ${Services.prefs.getBoolPref(
+      Cryptomining.prefEnabled
     )}\n`;
     body += `\nhasException: ${this.hasException}\n`;
 
@@ -2469,7 +2298,7 @@ var gProtectionsHandler = {
     formData.set("body", body);
 
     let activatedBlockers = [];
-    for (let blocker of this.blockers) {
+    for (let blocker of Object.values(this.blockers)) {
       if (blocker.activated) {
         activatedBlockers.push(blocker.reportBreakageLabel);
       }
diff --git a/browser/components/controlcenter/content/protectionsPanel.inc.xhtml b/browser/components/controlcenter/content/protectionsPanel.inc.xhtml
index e4c99be7459dc..6a5036ca2d17d 100644
--- a/browser/components/controlcenter/content/protectionsPanel.inc.xhtml
+++ b/browser/components/controlcenter/content/protectionsPanel.inc.xhtml
@@ -70,11 +70,11 @@
         <vbox id="protections-popup-content" flex="1">
           <vbox id="protections-popup-category-list">
             <!-- wrap=true is needed for descriptionheightworkaround, see bug 1564077 -->
-            <toolbarbutton id="protections-popup-category-tracking-protection"
+            <toolbarbutton id="protections-popup-category-trackers"
                            oncommand="gProtectionsHandler.showTrackersSubview(event); gProtectionsHandler.recordClick('trackers');"
                            class="protections-popup-category" align="center"
                            wrap="true">
-              <image class="protections-popup-category-icon tracking-protection-icon"/>
+              <image class="protections-popup-category-icon trackers-icon"/>
               <label flex="1" class="protections-popup-category-label" data-l10n-id="protections-panel-content-blocking-tracking-protection"></label>
             </toolbarbutton>
             <!-- wrap=true is needed for descriptionheightworkaround, see bug 1564077 -->
@@ -214,6 +214,13 @@
                descriptionheightworkaround="true">
         <vbox id="protections-popup-trackersView-description" class="protections-popup-description">
           <description data-l10n-id="protections-panel-tracking-content"></description>
+          <hbox id="protections-popup-trackersView-shim-allow-hint"
+                class="protections-popup-shim-allow-hint">
+            <image class="protections-popup-shim-allow-hint-icon"></image>
+            <description flex="1"
+                         data-l10n-id="protections-panel-description-shim-allowed">
+            </description>
+          </hbox>
         </vbox>
         <vbox id="protections-popup-trackersView-list" class="protections-popup-list">
         </vbox>
@@ -230,6 +237,13 @@
                descriptionheightworkaround="true">
         <vbox id="protections-popup-socialblockView-heading" class="protections-popup-description">
           <description data-l10n-id="protections-panel-social-media-trackers"></description>
+          <hbox id="protections-popup-socialblockView-shim-allow-hint"
+                class="protections-popup-shim-allow-hint">
+            <image class="protections-popup-shim-allow-hint-icon"></image>
+            <description flex="1"
+                         data-l10n-id="protections-panel-description-shim-allowed">
+            </description>
+          </hbox>
         </vbox>
         <vbox id="protections-popup-socialblockView-list" class="protections-popup-list">
         </vbox>
@@ -262,6 +276,13 @@
                descriptionheightworkaround="true">
         <vbox id="protections-popup-fingerprinters-heading" class="protections-popup-description">
           <description data-l10n-id="protections-panel-fingerprinters"></description>
+          <hbox id="protections-popup-fingerprintersView-shim-allow-hint"
+                class="protections-popup-shim-allow-hint">
+            <image class="protections-popup-shim-allow-hint-icon"></image>
+            <description flex="1"
+                        data-l10n-id="protections-panel-description-shim-allowed">
+            </description>
+          </hbox>
         </vbox>
         <vbox id="protections-popup-fingerprintersView-list" class="protections-popup-list">
         </vbox>
@@ -278,6 +299,13 @@
                descriptionheightworkaround="true">
         <vbox id="protections-popup-cryptominers-heading" class="protections-popup-description">
           <description data-l10n-id="protections-panel-cryptominers"></description>
+          <hbox id="protections-popup-cryptominersView-shim-allow-hint"
+                class="protections-popup-shim-allow-hint">
+            <image class="protections-popup-shim-allow-hint-icon"></image>
+            <description flex="1"
+                        data-l10n-id="protections-panel-description-shim-allowed">
+            </description>
+          </hbox>
         </vbox>
         <vbox id="protections-popup-cryptominersView-list" class="protections-popup-list">
         </vbox>
diff --git a/browser/locales/en-US/browser/protectionsPanel.ftl b/browser/locales/en-US/browser/protectionsPanel.ftl
index 407a02cae0b88..1a64666e5e670 100644
--- a/browser/locales/en-US/browser/protectionsPanel.ftl
+++ b/browser/locales/en-US/browser/protectionsPanel.ftl
@@ -89,6 +89,10 @@ protections-panel-fingerprinters = Fingerprinters collect settings from your bro
 protections-panel-tracking-content = Websites may load external ads, videos, and other content with tracking code. Blocking tracking content can help sites load faster, but some buttons, forms, and login fields might not work.
 protections-panel-social-media-trackers = Social networks place trackers on other websites to follow what you do, see, and watch online. This allows social media companies to learn more about you beyond what you share on your social media profiles.
 
+protections-panel-description-shim-allowed = Some trackers marked below have been partially unblocked on this page because you interacted with them.
+protections-panel-shim-allowed-indicator =
+  .tooltiptext = Tracker partially unblocked
+
 protections-panel-content-blocking-manage-settings =
   .label = Manage Protection Settings
   .accesskey = M
diff --git a/browser/themes/shared/controlcenter/panel.inc.css b/browser/themes/shared/controlcenter/panel.inc.css
index cf3ae3115c85b..e6e31be736233 100644
--- a/browser/themes/shared/controlcenter/panel.inc.css
+++ b/browser/themes/shared/controlcenter/panel.inc.css
@@ -581,13 +581,11 @@ description#identity-popup-content-verifier,
 }
 
 .protections-popup-list-item {
+  display: flex;
   margin: 5px 0;
-  overflow: hidden;
-  -moz-box-align: center;
 }
 
 .protections-popup-list-host-label {
-  -moz-box-flex: 1;
   direction: ltr;
   text-align: match-parent;
 }
@@ -721,7 +719,7 @@ description#identity-popup-content-verifier,
   display: none;
 }
 
-.tracking-protection-icon {
+.trackers-icon {
   list-style-image: url(chrome://browser/skin/controlcenter/tracker-image.svg);
 }
 
@@ -1220,3 +1218,35 @@ description#identity-popup-content-verifier,
   color: var(--panel-description-color);
 }
 }/** END Proton **/
+
+/** Shim-allow warning and indicator icons **/
+
+.protections-popup-shim-allow-hint {
+  padding: 0 2em;
+  margin-block-end: 10px;
+}
+
+.protections-popup-shim-allow-hint-icon,
+.protections-popup-list-host-shim-allow-indicator{
+  width: 16px;
+  -moz-context-properties: fill;
+  background-image: url("chrome://global/skin/icons/info-filled.svg");
+  background-repeat: no-repeat;
+  background-position: center;
+
+  fill: #0090ED;
+}
+
+:root[lwt-popup-brighttext] .protections-popup-shim-allow-hint-icon,
+:root[lwt-popup-brighttext] .protections-popup-list-host-shim-allow-indicator {
+  fill: #80EBFF;
+}
+
+.protections-popup-shim-allow-hint-icon {
+  margin-inline-end: 0.5em;
+}
+
+.protections-popup-list-host-shim-allow-indicator {
+  margin-inline-start: 0.5em;
+  min-width: 16px;
+}
-- 
GitLab