tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

browser-siteProtections.js (105350B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 ChromeUtils.defineESModuleGetters(this, {
      6  ContentBlockingAllowList:
      7    "resource://gre/modules/ContentBlockingAllowList.sys.mjs",
      8  ReportBrokenSite:
      9    "moz-src:///browser/components/reportbrokensite/ReportBrokenSite.sys.mjs",
     10  SpecialMessageActions:
     11    "resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
     12 });
     13 
     14 XPCOMUtils.defineLazyServiceGetter(
     15  this,
     16  "TrackingDBService",
     17  "@mozilla.org/tracking-db-service;1",
     18  Ci.nsITrackingDBService
     19 );
     20 
     21 /**
     22 * Represents a protection category shown in the protections UI. For the most
     23 * common categories we can directly instantiate this category. Some protections
     24 * categories inherit from this class and overwrite some of its members.
     25 */
     26 class ProtectionCategory {
     27  /**
     28   * Creates a protection category.
     29   *
     30   * @param {string} id - Identifier of the category. Used to query the category
     31   * UI elements in the DOM.
     32   * @param {object} options - Category options.
     33   * @param {string} options.prefEnabled - ID of pref which controls the
     34   * category enabled state.
     35   * @param {object} flags - Flags for this category to look for in the content
     36   * blocking event and content blocking log.
     37   * @param {number} [flags.load] - Load flag for this protection category. If
     38   * omitted, we will never match a isAllowing check for this category.
     39   * @param {number} [flags.block] - Block flag for this protection category. If
     40   * omitted, we will never match a isBlocking check for this category.
     41   * @param {number} [flags.shim] - Shim flag for this protection category. This
     42   * flag is set if we replaced tracking content with a non-tracking shim
     43   * script.
     44   * @param {number} [flags.allow] - Allow flag for this protection category.
     45   * This flag is set if we explicitly allow normally blocked tracking content.
     46   * The webcompat extension can do this if it needs to unblock content on user
     47   * opt-in.
     48   */
     49  constructor(
     50    id,
     51    { prefEnabled },
     52    {
     53      load,
     54      block,
     55      shim = Ci.nsIWebProgressListener.STATE_REPLACED_TRACKING_CONTENT,
     56      allow = Ci.nsIWebProgressListener.STATE_ALLOWED_TRACKING_CONTENT,
     57    }
     58  ) {
     59    this._id = id;
     60    this.prefEnabled = prefEnabled;
     61 
     62    this._flags = { load, block, shim, allow };
     63 
     64    if (
     65      Services.prefs.getPrefType(this.prefEnabled) == Services.prefs.PREF_BOOL
     66    ) {
     67      XPCOMUtils.defineLazyPreferenceGetter(
     68        this,
     69        "_enabled",
     70        this.prefEnabled,
     71        false,
     72        this.updateCategoryItem.bind(this)
     73      );
     74    }
     75 
     76    MozXULElement.insertFTLIfNeeded("browser/siteProtections.ftl");
     77 
     78    ChromeUtils.defineLazyGetter(this, "subView", () =>
     79      document.getElementById(`protections-popup-${this._id}View`)
     80    );
     81 
     82    ChromeUtils.defineLazyGetter(this, "subViewHeading", () =>
     83      document.getElementById(`protections-popup-${this._id}View-heading`)
     84    );
     85 
     86    ChromeUtils.defineLazyGetter(this, "subViewList", () =>
     87      document.getElementById(`protections-popup-${this._id}View-list`)
     88    );
     89 
     90    ChromeUtils.defineLazyGetter(this, "subViewShimAllowHint", () =>
     91      document.getElementById(
     92        `protections-popup-${this._id}View-shim-allow-hint`
     93      )
     94    );
     95 
     96    ChromeUtils.defineLazyGetter(this, "isWindowPrivate", () =>
     97      PrivateBrowsingUtils.isWindowPrivate(window)
     98    );
     99  }
    100 
    101  // Child classes may override these to do init / teardown. We expect them to
    102  // be called when the protections panel is initialized or destroyed.
    103  init() {}
    104  uninit() {}
    105 
    106  // Some child classes may overide this getter.
    107  get enabled() {
    108    return this._enabled;
    109  }
    110 
    111  /**
    112   * Get the category item associated with this protection from the main
    113   * protections panel.
    114   *
    115   * @returns {xul:toolbarbutton|undefined} - Item or undefined if the panel is
    116   * not yet initialized.
    117   */
    118  get categoryItem() {
    119    // We don't use defineLazyGetter for the category item, since it may be null
    120    // on first access.
    121    return (
    122      this._categoryItem ||
    123      (this._categoryItem = document.getElementById(
    124        `protections-popup-category-${this._id}`
    125      ))
    126    );
    127  }
    128 
    129  /**
    130   * Defaults to enabled state. May be overridden by child classes.
    131   *
    132   * @returns {boolean} - Whether the protection is set to block trackers.
    133   */
    134  get blockingEnabled() {
    135    return this.enabled;
    136  }
    137 
    138  /**
    139   * Update the category item state in the main view of the protections panel.
    140   * Determines whether the category is set to block trackers.
    141   *
    142   * @returns {boolean} - true if the state has been updated, false if the
    143   * protections popup has not been initialized yet.
    144   */
    145  updateCategoryItem() {
    146    // Can't get `this.categoryItem` without the popup. Using the popup instead
    147    // of `this.categoryItem` to guard access, because the category item getter
    148    // can trigger bug 1543537. If there's no popup, we'll be called again the
    149    // first time the popup shows.
    150    if (!gProtectionsHandler._protectionsPopup) {
    151      return false;
    152    }
    153    this.categoryItem.classList.toggle("blocked", this.enabled);
    154    this.categoryItem.classList.toggle("subviewbutton-nav", this.enabled);
    155    return true;
    156  }
    157 
    158  /**
    159   * Update the category sub view that is shown when users click on the category
    160   * button.
    161   */
    162  async updateSubView() {
    163    let { items, anyShimAllowed } = await this._generateSubViewListItems();
    164    this.subViewShimAllowHint.hidden = !anyShimAllowed;
    165 
    166    this.subViewList.textContent = "";
    167    this.subViewList.append(items);
    168    const isBlocking =
    169      this.blockingEnabled && !gProtectionsHandler.hasException;
    170    let l10nId;
    171    switch (this._id) {
    172      case "cryptominers":
    173        l10nId = isBlocking
    174          ? "protections-blocking-cryptominers"
    175          : "protections-not-blocking-cryptominers";
    176        break;
    177      case "fingerprinters":
    178        l10nId = isBlocking
    179          ? "protections-blocking-fingerprinters"
    180          : "protections-not-blocking-fingerprinters";
    181        break;
    182      case "socialblock":
    183        l10nId = isBlocking
    184          ? "protections-blocking-social-media-trackers"
    185          : "protections-not-blocking-social-media-trackers";
    186        break;
    187    }
    188    if (l10nId) {
    189      document.l10n.setAttributes(this.subView, l10nId);
    190    }
    191  }
    192 
    193  /**
    194   * Create a list of items, each representing a tracker.
    195   *
    196   * @returns {object} result - An object containing the results.
    197   * @returns {HTMLDivElement[]} result.items - Generated tracker items. May be
    198   * empty.
    199   * @returns {boolean} result.anyShimAllowed - Flag indicating if any of the
    200   * items have been unblocked by a shim script.
    201   */
    202  async _generateSubViewListItems() {
    203    let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog();
    204    contentBlockingLog = JSON.parse(contentBlockingLog);
    205    let anyShimAllowed = false;
    206 
    207    let fragment = document.createDocumentFragment();
    208    for (let [origin, actions] of Object.entries(contentBlockingLog)) {
    209      let { item, shimAllowed } = await this._createListItem(origin, actions);
    210      if (!item) {
    211        continue;
    212      }
    213      anyShimAllowed = anyShimAllowed || shimAllowed;
    214      fragment.appendChild(item);
    215    }
    216 
    217    return {
    218      items: fragment,
    219      anyShimAllowed,
    220    };
    221  }
    222 
    223  /**
    224   * Return the number items blocked by this blocker.
    225   *
    226   * @returns {Integer} count - The number of items blocked.
    227   */
    228  async getBlockerCount() {
    229    let { items } = await this._generateSubViewListItems();
    230    return items?.childElementCount ?? 0;
    231  }
    232 
    233  /**
    234   * Create a DOM item representing a tracker.
    235   *
    236   * @param {string} origin - Origin of the tracker.
    237   * @param {Array} actions - Array of actions from the content blocking log
    238   * associated with the tracking origin.
    239   * @returns {object} result - An object containing the results.
    240   * @returns {HTMLDListElement} [options.item] - Generated item or null if we
    241   * don't have an item for this origin based on the actions log.
    242   * @returns {boolean} options.shimAllowed - Flag indicating whether the
    243   * tracking origin was allowed by a shim script.
    244   */
    245  _createListItem(origin, actions) {
    246    let isAllowed = actions.some(
    247      ([state]) => this.isAllowing(state) && !this.isShimming(state)
    248    );
    249    let isDetected =
    250      isAllowed || actions.some(([state]) => this.isBlocking(state));
    251 
    252    if (!isDetected) {
    253      return {};
    254    }
    255 
    256    // Create an item to hold the origin label and shim allow indicator. Using
    257    // an html element here, so we can use CSS flex, which handles the label
    258    // overflow in combination with the icon correctly.
    259    let listItem = document.createElementNS(
    260      "http://www.w3.org/1999/xhtml",
    261      "div"
    262    );
    263    listItem.className = "protections-popup-list-item";
    264    listItem.classList.toggle("allowed", isAllowed);
    265 
    266    let label = document.createXULElement("label");
    267    // Repeat the host in the tooltip in case it's too long
    268    // and overflows in our panel.
    269    label.tooltipText = origin;
    270    label.value = origin;
    271    label.className = "protections-popup-list-host-label";
    272    label.setAttribute("crop", "end");
    273    listItem.append(label);
    274 
    275    // Determine whether we should show a shim-allow indicator for this item.
    276    let shimAllowed = actions.some(([flag]) => flag == this._flags.allow);
    277    if (shimAllowed) {
    278      listItem.append(this._getShimAllowIndicator());
    279    }
    280 
    281    return { item: listItem, shimAllowed };
    282  }
    283 
    284  /**
    285   * Create an indicator icon for marking origins that have been allowed by a
    286   * shim script.
    287   *
    288   * @returns {HTMLImageElement} - Created element.
    289   */
    290  _getShimAllowIndicator() {
    291    let allowIndicator = document.createXULElement("image");
    292    document.l10n.setAttributes(
    293      allowIndicator,
    294      "protections-panel-shim-allowed-indicator"
    295    );
    296    allowIndicator.classList.add(
    297      "protections-popup-list-host-shim-allow-indicator"
    298    );
    299    return allowIndicator;
    300  }
    301 
    302  /**
    303   * @param {number} state - Content blocking event flags.
    304   * @returns {boolean} - Whether the protection has blocked a tracker.
    305   */
    306  isBlocking(state) {
    307    return (state & this._flags.block) != 0;
    308  }
    309 
    310  /**
    311   * @param {number} state - Content blocking event flags.
    312   * @returns {boolean} - Whether the protection has allowed a tracker.
    313   */
    314  isAllowing(state) {
    315    return (state & this._flags.load) != 0;
    316  }
    317 
    318  /**
    319   * @param {number} state - Content blocking event flags.
    320   * @returns {boolean} - Whether the protection has detected (blocked or
    321   * allowed) a tracker.
    322   */
    323  isDetected(state) {
    324    return this.isBlocking(state) || this.isAllowing(state);
    325  }
    326 
    327  /**
    328   * @param {number} state - Content blocking event flags.
    329   * @returns {boolean} - Whether the protections has allowed a tracker that
    330   * would have normally been blocked.
    331   */
    332  isShimming(state) {
    333    return (state & this._flags.shim) != 0 && this.isAllowing(state);
    334  }
    335 }
    336 
    337 let Fingerprinting =
    338  new (class FingerprintingProtection extends ProtectionCategory {
    339    iconSrc = "chrome://browser/skin/fingerprint.svg";
    340    l10nKeys = {
    341      content: "fingerprinters",
    342      general: "fingerprinter",
    343      title: {
    344        blocking: "protections-blocking-fingerprinters",
    345        "not-blocking": "protections-not-blocking-fingerprinters",
    346      },
    347    };
    348    #isInitialized = false;
    349 
    350    constructor() {
    351      super(
    352        "fingerprinters",
    353        {
    354          prefEnabled: "privacy.trackingprotection.fingerprinting.enabled",
    355        },
    356        {
    357          load: Ci.nsIWebProgressListener.STATE_LOADED_FINGERPRINTING_CONTENT,
    358          block: Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT,
    359          shim: Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT,
    360          allow: Ci.nsIWebProgressListener.STATE_ALLOWED_FINGERPRINTING_CONTENT,
    361        }
    362      );
    363 
    364      this.prefFPPEnabled = "privacy.fingerprintingProtection";
    365      this.prefFPPEnabledInPrivateWindows =
    366        "privacy.fingerprintingProtection.pbmode";
    367 
    368      this.enabledFPB = false;
    369      this.enabledFPPGlobally = false;
    370      this.enabledFPPInPrivateWindows = false;
    371    }
    372 
    373    init() {
    374      this.updateEnabled();
    375 
    376      if (!this.#isInitialized) {
    377        Services.prefs.addObserver(this.prefEnabled, this);
    378        Services.prefs.addObserver(this.prefFPPEnabled, this);
    379        Services.prefs.addObserver(this.prefFPPEnabledInPrivateWindows, this);
    380        this.#isInitialized = true;
    381      }
    382    }
    383 
    384    uninit() {
    385      if (this.#isInitialized) {
    386        Services.prefs.removeObserver(this.prefEnabled, this);
    387        Services.prefs.removeObserver(this.prefFPPEnabled, this);
    388        Services.prefs.removeObserver(
    389          this.prefFPPEnabledInPrivateWindows,
    390          this
    391        );
    392        this.#isInitialized = false;
    393      }
    394    }
    395 
    396    updateEnabled() {
    397      this.enabledFPB = Services.prefs.getBoolPref(this.prefEnabled);
    398      this.enabledFPPGlobally = Services.prefs.getBoolPref(this.prefFPPEnabled);
    399      this.enabledFPPInPrivateWindows = Services.prefs.getBoolPref(
    400        this.prefFPPEnabledInPrivateWindows
    401      );
    402    }
    403 
    404    observe() {
    405      this.updateEnabled();
    406      this.updateCategoryItem();
    407    }
    408 
    409    get enabled() {
    410      return (
    411        this.enabledFPB ||
    412        this.enabledFPPGlobally ||
    413        (this.isWindowPrivate && this.enabledFPPInPrivateWindows)
    414      );
    415    }
    416 
    417    isBlocking(state) {
    418      let blockFlag = this._flags.block;
    419 
    420      // We only consider the suspicious fingerprinting flag if the
    421      // fingerprinting protection is enabled in the context.
    422      if (
    423        this.enabledFPPGlobally ||
    424        (this.isWindowPrivate && this.enabledFPPInPrivateWindows)
    425      ) {
    426        blockFlag |=
    427          Ci.nsIWebProgressListener.STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING;
    428      }
    429 
    430      return (state & blockFlag) != 0;
    431    }
    432    // TODO (Bug 1864914): Consider showing suspicious fingerprinting as allowed
    433    // when the fingerprinting protection is disabled.
    434  })();
    435 
    436 let Cryptomining = new ProtectionCategory(
    437  "cryptominers",
    438  {
    439    prefEnabled: "privacy.trackingprotection.cryptomining.enabled",
    440  },
    441  {
    442    load: Ci.nsIWebProgressListener.STATE_LOADED_CRYPTOMINING_CONTENT,
    443    block: Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT,
    444  }
    445 );
    446 
    447 Cryptomining.l10nId = "trustpanel-cryptomining";
    448 Cryptomining.iconSrc = "chrome://browser/skin/controlcenter/cryptominers.svg";
    449 Cryptomining.l10nKeys = {
    450  content: "cryptominers",
    451  general: "cryptominer",
    452  title: {
    453    blocking: "protections-blocking-cryptominers",
    454    "not-blocking": "protections-not-blocking-cryptominers",
    455  },
    456 };
    457 
    458 let TrackingProtection =
    459  new (class TrackingProtection extends ProtectionCategory {
    460    iconSrc = "chrome://browser/skin/canvas.svg";
    461    l10nKeys = {
    462      content: "tracking-content",
    463      general: "tracking-content",
    464      title: {
    465        blocking: "protections-blocking-tracking-content",
    466        "not-blocking": "protections-not-blocking-tracking-content",
    467      },
    468    };
    469    #isInitialized = false;
    470 
    471    constructor() {
    472      super(
    473        "trackers",
    474        {
    475          prefEnabled: "privacy.trackingprotection.enabled",
    476        },
    477        {
    478          load: null,
    479          block:
    480            Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT |
    481            Ci.nsIWebProgressListener.STATE_BLOCKED_EMAILTRACKING_CONTENT,
    482        }
    483      );
    484 
    485      this.prefEnabledInPrivateWindows =
    486        "privacy.trackingprotection.pbmode.enabled";
    487      this.prefTrackingTable = "urlclassifier.trackingTable";
    488      this.prefTrackingAnnotationTable =
    489        "urlclassifier.trackingAnnotationTable";
    490      this.prefAnnotationsLevel2Enabled =
    491        "privacy.annotate_channels.strict_list.enabled";
    492      this.prefEmailTrackingProtectionEnabled =
    493        "privacy.trackingprotection.emailtracking.enabled";
    494      this.prefEmailTrackingProtectionEnabledInPrivateWindows =
    495        "privacy.trackingprotection.emailtracking.pbmode.enabled";
    496 
    497      this.enabledGlobally = false;
    498      this.emailTrackingProtectionEnabledGlobally = false;
    499 
    500      this.enabledInPrivateWindows = false;
    501      this.emailTrackingProtectionEnabledInPrivateWindows = false;
    502 
    503      XPCOMUtils.defineLazyPreferenceGetter(
    504        this,
    505        "trackingTable",
    506        this.prefTrackingTable,
    507        ""
    508      );
    509      XPCOMUtils.defineLazyPreferenceGetter(
    510        this,
    511        "trackingAnnotationTable",
    512        this.prefTrackingAnnotationTable,
    513        ""
    514      );
    515      XPCOMUtils.defineLazyPreferenceGetter(
    516        this,
    517        "annotationsLevel2Enabled",
    518        this.prefAnnotationsLevel2Enabled,
    519        false
    520      );
    521    }
    522 
    523    init() {
    524      this.updateEnabled();
    525 
    526      if (!this.#isInitialized) {
    527        Services.prefs.addObserver(this.prefEnabled, this);
    528        Services.prefs.addObserver(this.prefEnabledInPrivateWindows, this);
    529        Services.prefs.addObserver(
    530          this.prefEmailTrackingProtectionEnabled,
    531          this
    532        );
    533        Services.prefs.addObserver(
    534          this.prefEmailTrackingProtectionEnabledInPrivateWindows,
    535          this
    536        );
    537        this.#isInitialized = true;
    538      }
    539    }
    540 
    541    uninit() {
    542      if (this.#isInitialized) {
    543        Services.prefs.removeObserver(this.prefEnabled, this);
    544        Services.prefs.removeObserver(this.prefEnabledInPrivateWindows, this);
    545        Services.prefs.removeObserver(
    546          this.prefEmailTrackingProtectionEnabled,
    547          this
    548        );
    549        Services.prefs.removeObserver(
    550          this.prefEmailTrackingProtectionEnabledInPrivateWindows,
    551          this
    552        );
    553        this.#isInitialized = false;
    554      }
    555    }
    556 
    557    observe() {
    558      this.updateEnabled();
    559      this.updateCategoryItem();
    560    }
    561 
    562    get trackingProtectionLevel2Enabled() {
    563      const CONTENT_TABLE = "content-track-digest256";
    564      return this.trackingTable.includes(CONTENT_TABLE);
    565    }
    566 
    567    get enabled() {
    568      return (
    569        this.enabledGlobally ||
    570        this.emailTrackingProtectionEnabledGlobally ||
    571        (this.isWindowPrivate &&
    572          (this.enabledInPrivateWindows ||
    573            this.emailTrackingProtectionEnabledInPrivateWindows))
    574      );
    575    }
    576 
    577    updateEnabled() {
    578      this.enabledGlobally = Services.prefs.getBoolPref(this.prefEnabled);
    579      this.enabledInPrivateWindows = Services.prefs.getBoolPref(
    580        this.prefEnabledInPrivateWindows
    581      );
    582      this.emailTrackingProtectionEnabledGlobally = Services.prefs.getBoolPref(
    583        this.prefEmailTrackingProtectionEnabled
    584      );
    585      this.emailTrackingProtectionEnabledInPrivateWindows =
    586        Services.prefs.getBoolPref(
    587          this.prefEmailTrackingProtectionEnabledInPrivateWindows
    588        );
    589    }
    590 
    591    isAllowingLevel1(state) {
    592      return (
    593        (state &
    594          Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT) !=
    595        0
    596      );
    597    }
    598 
    599    isAllowingLevel2(state) {
    600      return (
    601        (state &
    602          Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_2_TRACKING_CONTENT) !=
    603        0
    604      );
    605    }
    606 
    607    isAllowing(state) {
    608      return this.isAllowingLevel1(state) || this.isAllowingLevel2(state);
    609    }
    610 
    611    async updateSubView() {
    612      let previousURI = gBrowser.currentURI.spec;
    613      let previousWindow = gBrowser.selectedBrowser.innerWindowID;
    614 
    615      let { items, anyShimAllowed } = await this._generateSubViewListItems();
    616 
    617      // If we don't have trackers we would usually not show the menu item
    618      // allowing the user to show the sub-panel. However, in the edge case
    619      // that we annotated trackers on the page using the strict list but did
    620      // not detect trackers on the page using the basic list, we currently
    621      // still show the panel. To reduce the confusion, tell the user that we have
    622      // not detected any tracker.
    623      if (!items.childNodes.length) {
    624        let emptyImage = document.createXULElement("image");
    625        emptyImage.classList.add("protections-popup-trackersView-empty-image");
    626        emptyImage.classList.add("trackers-icon");
    627 
    628        let emptyLabel = document.createXULElement("label");
    629        emptyLabel.classList.add("protections-popup-empty-label");
    630        document.l10n.setAttributes(
    631          emptyLabel,
    632          "content-blocking-trackers-view-empty"
    633        );
    634 
    635        items.appendChild(emptyImage);
    636        items.appendChild(emptyLabel);
    637 
    638        this.subViewList.classList.add("empty");
    639      } else {
    640        this.subViewList.classList.remove("empty");
    641      }
    642 
    643      // This might have taken a while. Only update the list if we're still on the same page.
    644      if (
    645        previousURI == gBrowser.currentURI.spec &&
    646        previousWindow == gBrowser.selectedBrowser.innerWindowID
    647      ) {
    648        this.subViewShimAllowHint.hidden = !anyShimAllowed;
    649 
    650        this.subViewList.textContent = "";
    651        this.subViewList.append(items);
    652        const l10nId =
    653          this.enabled && !gProtectionsHandler.hasException
    654            ? "protections-blocking-tracking-content"
    655            : "protections-not-blocking-tracking-content";
    656        document.l10n.setAttributes(this.subView, l10nId);
    657      }
    658    }
    659 
    660    async _createListItem(origin, actions) {
    661      // Figure out if this list entry was actually detected by TP or something else.
    662      let isAllowed = actions.some(
    663        ([state]) => this.isAllowing(state) && !this.isShimming(state)
    664      );
    665      let isDetected =
    666        isAllowed || actions.some(([state]) => this.isBlocking(state));
    667 
    668      if (!isDetected) {
    669        return {};
    670      }
    671 
    672      // Because we might use different lists for annotation vs. blocking, we
    673      // need to make sure that this is a tracker that we would actually have blocked
    674      // before showing it to the user.
    675      if (
    676        this.annotationsLevel2Enabled &&
    677        !this.trackingProtectionLevel2Enabled &&
    678        actions.some(
    679          ([state]) =>
    680            (state &
    681              Ci.nsIWebProgressListener
    682                .STATE_LOADED_LEVEL_2_TRACKING_CONTENT) !=
    683            0
    684        )
    685      ) {
    686        return {};
    687      }
    688 
    689      let listItem = document.createElementNS(
    690        "http://www.w3.org/1999/xhtml",
    691        "div"
    692      );
    693      listItem.className = "protections-popup-list-item";
    694      listItem.classList.toggle("allowed", isAllowed);
    695 
    696      let label = document.createXULElement("label");
    697      // Repeat the host in the tooltip in case it's too long
    698      // and overflows in our panel.
    699      label.tooltipText = origin;
    700      label.value = origin;
    701      label.className = "protections-popup-list-host-label";
    702      label.setAttribute("crop", "end");
    703      listItem.append(label);
    704 
    705      let shimAllowed = actions.some(([flag]) => flag == this._flags.allow);
    706      if (shimAllowed) {
    707        listItem.append(this._getShimAllowIndicator());
    708      }
    709 
    710      return { item: listItem, shimAllowed };
    711    }
    712  })();
    713 
    714 let ThirdPartyCookies =
    715  new (class ThirdPartyCookies extends ProtectionCategory {
    716    iconSrc = "chrome://browser/skin/controlcenter/3rdpartycookies.svg";
    717    l10nKeys = {
    718      content: "cross-site-tracking-cookies",
    719      general: "tracking-cookies",
    720      title: {
    721        blocking: "protections-blocking-cookies-third-party",
    722        "not-blocking": "protections-not-blocking-cookies-third-party",
    723      },
    724    };
    725 
    726    constructor() {
    727      super(
    728        "cookies",
    729        {
    730          // This would normally expect a boolean pref. However, this category
    731          // overwrites the enabled getter for custom handling of cookie behavior
    732          // states.
    733          prefEnabled: "network.cookie.cookieBehavior",
    734        },
    735        {
    736          // ThirdPartyCookies implements custom flag processing.
    737          allow: null,
    738          shim: null,
    739          load: null,
    740          block: null,
    741        }
    742      );
    743 
    744      ChromeUtils.defineLazyGetter(this, "categoryLabel", () =>
    745        document.getElementById("protections-popup-cookies-category-label")
    746      );
    747 
    748      this.prefEnabledValues = [
    749        // These values match the ones exposed under the Content Blocking section
    750        // of the Preferences UI.
    751        Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, // Block all third-party cookies
    752        Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, // Block third-party cookies from trackers
    753        Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, // Block trackers and patition third-party trackers
    754        Ci.nsICookieService.BEHAVIOR_REJECT, // Block all cookies
    755      ];
    756 
    757      XPCOMUtils.defineLazyPreferenceGetter(
    758        this,
    759        "behaviorPref",
    760        this.prefEnabled,
    761        Ci.nsICookieService.BEHAVIOR_ACCEPT,
    762        this.updateCategoryItem.bind(this)
    763      );
    764    }
    765 
    766    isBlocking(state) {
    767      return (
    768        (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER) !=
    769          0 ||
    770        (state &
    771          Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER) !=
    772          0 ||
    773        (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL) != 0 ||
    774        (state &
    775          Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION) !=
    776          0 ||
    777        (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN) !=
    778          0 ||
    779        (state & Ci.nsIWebProgressListener.STATE_COOKIES_PARTITIONED_TRACKER) !=
    780          0
    781      );
    782    }
    783 
    784    isDetected(state) {
    785      if (this.isBlocking(state)) {
    786        return true;
    787      }
    788 
    789      if (
    790        [
    791          Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
    792          Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
    793          Ci.nsICookieService.BEHAVIOR_ACCEPT,
    794        ].includes(this.behaviorPref)
    795      ) {
    796        return (
    797          (state & Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_TRACKER) !=
    798            0 ||
    799          (SocialTracking.enabled &&
    800            (state &
    801              Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER) !=
    802              0)
    803        );
    804      }
    805 
    806      // We don't have specific flags for the other cookie behaviors so just
    807      // fall back to STATE_COOKIES_LOADED.
    808      return (state & Ci.nsIWebProgressListener.STATE_COOKIES_LOADED) != 0;
    809    }
    810 
    811    updateCategoryItem() {
    812      if (!super.updateCategoryItem()) {
    813        return;
    814      }
    815 
    816      let l10nId;
    817      if (!this.enabled) {
    818        l10nId = "content-blocking-cookies-blocking-trackers-label";
    819      } else {
    820        switch (this.behaviorPref) {
    821          case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
    822            l10nId = "content-blocking-cookies-blocking-third-party-label";
    823            break;
    824          case Ci.nsICookieService.BEHAVIOR_REJECT:
    825            l10nId = "content-blocking-cookies-blocking-all-label";
    826            break;
    827          case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN:
    828            l10nId = "content-blocking-cookies-blocking-unvisited-label";
    829            break;
    830          case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER:
    831          case Ci.nsICookieService
    832            .BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN:
    833            l10nId = "content-blocking-cookies-blocking-trackers-label";
    834            break;
    835          default:
    836            console.error(
    837              `Error: Unknown cookieBehavior pref observed: ${this.behaviorPref}`
    838            );
    839            this.categoryLabel.removeAttribute("data-l10n-id");
    840            this.categoryLabel.textContent = "";
    841            return;
    842        }
    843      }
    844      document.l10n.setAttributes(this.categoryLabel, l10nId);
    845    }
    846 
    847    get enabled() {
    848      return this.prefEnabledValues.includes(this.behaviorPref);
    849    }
    850 
    851    _generateSubViewListItems() {
    852      let fragment = document.createDocumentFragment();
    853      let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog();
    854      contentBlockingLog = JSON.parse(contentBlockingLog);
    855      let categories = this._processContentBlockingLog(contentBlockingLog);
    856 
    857      let categoryNames = ["trackers"];
    858      switch (this.behaviorPref) {
    859        case Ci.nsICookieService.BEHAVIOR_REJECT:
    860          categoryNames.push("firstParty");
    861        // eslint-disable-next-line no-fallthrough
    862        case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
    863          categoryNames.push("thirdParty");
    864      }
    865 
    866      for (let category of categoryNames) {
    867        let itemsToShow = categories[category];
    868 
    869        if (!itemsToShow.length) {
    870          continue;
    871        }
    872        for (let info of itemsToShow) {
    873          fragment.appendChild(this._createListItem(info));
    874        }
    875      }
    876      return { items: fragment };
    877    }
    878 
    879    updateSubView() {
    880      let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog();
    881      contentBlockingLog = JSON.parse(contentBlockingLog);
    882 
    883      let categories = this._processContentBlockingLog(contentBlockingLog);
    884 
    885      this.subViewList.textContent = "";
    886 
    887      let categoryNames = ["trackers"];
    888      switch (this.behaviorPref) {
    889        case Ci.nsICookieService.BEHAVIOR_REJECT:
    890          categoryNames.push("firstParty");
    891        // eslint-disable-next-line no-fallthrough
    892        case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
    893          categoryNames.push("thirdParty");
    894      }
    895 
    896      for (let category of categoryNames) {
    897        let itemsToShow = categories[category];
    898 
    899        if (!itemsToShow.length) {
    900          continue;
    901        }
    902 
    903        let box = document.createXULElement("vbox");
    904        box.className = "protections-popup-cookiesView-list-section";
    905        let label = document.createXULElement("label");
    906        label.className = "protections-popup-cookiesView-list-header";
    907        let l10nId;
    908        switch (category) {
    909          case "trackers":
    910            l10nId = "content-blocking-cookies-view-trackers-label";
    911            break;
    912          case "firstParty":
    913            l10nId = "content-blocking-cookies-view-first-party-label";
    914            break;
    915          case "thirdParty":
    916            l10nId = "content-blocking-cookies-view-third-party-label";
    917            break;
    918        }
    919        if (l10nId) {
    920          document.l10n.setAttributes(label, l10nId);
    921        }
    922        box.appendChild(label);
    923 
    924        for (let info of itemsToShow) {
    925          box.appendChild(this._createListItem(info));
    926        }
    927 
    928        this.subViewList.appendChild(box);
    929      }
    930 
    931      this.subViewHeading.hidden = false;
    932      if (!this.enabled) {
    933        document.l10n.setAttributes(
    934          this.subView,
    935          "protections-not-blocking-cross-site-tracking-cookies"
    936        );
    937        return;
    938      }
    939 
    940      let l10nId;
    941      let siteException = gProtectionsHandler.hasException;
    942      switch (this.behaviorPref) {
    943        case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
    944          l10nId = siteException
    945            ? "protections-not-blocking-cookies-third-party"
    946            : "protections-blocking-cookies-third-party";
    947          this.subViewHeading.hidden = true;
    948          if (this.subViewHeading.nextSibling.nodeName == "toolbarseparator") {
    949            this.subViewHeading.nextSibling.hidden = true;
    950          }
    951          break;
    952        case Ci.nsICookieService.BEHAVIOR_REJECT:
    953          l10nId = siteException
    954            ? "protections-not-blocking-cookies-all"
    955            : "protections-blocking-cookies-all";
    956          this.subViewHeading.hidden = true;
    957          if (this.subViewHeading.nextSibling.nodeName == "toolbarseparator") {
    958            this.subViewHeading.nextSibling.hidden = true;
    959          }
    960          break;
    961        case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN:
    962          l10nId = "protections-blocking-cookies-unvisited";
    963          this.subViewHeading.hidden = true;
    964          if (this.subViewHeading.nextSibling.nodeName == "toolbarseparator") {
    965            this.subViewHeading.nextSibling.hidden = true;
    966          }
    967          break;
    968        case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER:
    969        case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN:
    970          l10nId = siteException
    971            ? "protections-not-blocking-cross-site-tracking-cookies"
    972            : "protections-blocking-cookies-trackers";
    973          break;
    974        default:
    975          console.error(
    976            `Error: Unknown cookieBehavior pref when updating subview: ${this.behaviorPref}`
    977          );
    978          return;
    979      }
    980 
    981      document.l10n.setAttributes(this.subView, l10nId);
    982    }
    983 
    984    _getExceptionState(origin) {
    985      let thirdPartyStorage = Services.perms.testPermissionFromPrincipal(
    986        gBrowser.contentPrincipal,
    987        "3rdPartyStorage^" + origin
    988      );
    989 
    990      if (thirdPartyStorage != Services.perms.UNKNOWN_ACTION) {
    991        return thirdPartyStorage;
    992      }
    993 
    994      let principal =
    995        Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
    996      // Cookie exceptions get "inherited" from parent- to sub-domain, so we need to
    997      // make sure to include parent domains in the permission check for "cookie".
    998      return Services.perms.testPermissionFromPrincipal(principal, "cookie");
    999    }
   1000 
   1001    _clearException(origin) {
   1002      for (let perm of Services.perms.getAllForPrincipal(
   1003        gBrowser.contentPrincipal
   1004      )) {
   1005        if (perm.type == "3rdPartyStorage^" + origin) {
   1006          Services.perms.removePermission(perm);
   1007        }
   1008      }
   1009 
   1010      // OAs don't matter here, so we can just use the hostname.
   1011      let host = Services.io.newURI(origin).host;
   1012 
   1013      // Cookie exceptions get "inherited" from parent- to sub-domain, so we need to
   1014      // clear any cookie permissions from parent domains as well.
   1015      for (let perm of Services.perms.all) {
   1016        if (
   1017          perm.type == "cookie" &&
   1018          Services.eTLD.hasRootDomain(host, perm.principal.host)
   1019        ) {
   1020          Services.perms.removePermission(perm);
   1021        }
   1022      }
   1023    }
   1024 
   1025    // Transforms and filters cookie entries in the content blocking log
   1026    // so that we can categorize and display them in the UI.
   1027    _processContentBlockingLog(log) {
   1028      let newLog = {
   1029        firstParty: [],
   1030        trackers: [],
   1031        thirdParty: [],
   1032      };
   1033 
   1034      let firstPartyDomain = null;
   1035      try {
   1036        firstPartyDomain = Services.eTLD.getBaseDomain(gBrowser.currentURI);
   1037      } catch (e) {
   1038        // There are nasty edge cases here where someone is trying to set a cookie
   1039        // on a public suffix or an IP address. Just categorize those as third party...
   1040        if (
   1041          e.result != Cr.NS_ERROR_HOST_IS_IP_ADDRESS &&
   1042          e.result != Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
   1043        ) {
   1044          throw e;
   1045        }
   1046      }
   1047 
   1048      for (let [origin, actions] of Object.entries(log)) {
   1049        if (!origin.startsWith("http")) {
   1050          continue;
   1051        }
   1052 
   1053        let info = {
   1054          origin,
   1055          isAllowed: true,
   1056          exceptionState: this._getExceptionState(origin),
   1057        };
   1058        let hasCookie = false;
   1059        let isTracker = false;
   1060 
   1061        // Extract information from the states entries in the content blocking log.
   1062        // Each state will contain a single state flag from nsIWebProgressListener.
   1063        // Note that we are using the same helper functions that are applied to the
   1064        // bit map passed to onSecurityChange (which contains multiple states), thus
   1065        // not checking exact equality, just presence of bits.
   1066        for (let [state, blocked] of actions) {
   1067          if (this.isDetected(state)) {
   1068            hasCookie = true;
   1069          }
   1070          if (TrackingProtection.isAllowing(state)) {
   1071            isTracker = true;
   1072          }
   1073          // blocked tells us whether the resource was actually blocked
   1074          // (which it may not be in case of an exception).
   1075          if (this.isBlocking(state)) {
   1076            info.isAllowed = !blocked;
   1077          }
   1078        }
   1079 
   1080        if (!hasCookie) {
   1081          continue;
   1082        }
   1083 
   1084        let isFirstParty = false;
   1085        try {
   1086          let uri = Services.io.newURI(origin);
   1087          isFirstParty = Services.eTLD.getBaseDomain(uri) == firstPartyDomain;
   1088        } catch (e) {
   1089          if (
   1090            e.result != Cr.NS_ERROR_HOST_IS_IP_ADDRESS &&
   1091            e.result != Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
   1092          ) {
   1093            throw e;
   1094          }
   1095        }
   1096 
   1097        if (isFirstParty) {
   1098          newLog.firstParty.push(info);
   1099        } else if (isTracker) {
   1100          newLog.trackers.push(info);
   1101        } else {
   1102          newLog.thirdParty.push(info);
   1103        }
   1104      }
   1105 
   1106      return newLog;
   1107    }
   1108 
   1109    _createListItem({ origin, isAllowed, exceptionState }) {
   1110      let listItem = document.createElementNS(
   1111        "http://www.w3.org/1999/xhtml",
   1112        "div"
   1113      );
   1114      listItem.className = "protections-popup-list-item";
   1115      // Repeat the origin in the tooltip in case it's too long
   1116      // and overflows in our panel.
   1117      listItem.tooltipText = origin;
   1118 
   1119      let label = document.createXULElement("label");
   1120      label.value = origin;
   1121      label.className = "protections-popup-list-host-label";
   1122      label.setAttribute("crop", "end");
   1123      listItem.append(label);
   1124 
   1125      if (
   1126        (isAllowed && exceptionState == Services.perms.ALLOW_ACTION) ||
   1127        (!isAllowed && exceptionState == Services.perms.DENY_ACTION)
   1128      ) {
   1129        listItem.classList.add("protections-popup-list-item-with-state");
   1130 
   1131        let stateLabel = document.createXULElement("label");
   1132        stateLabel.className = "protections-popup-list-state-label";
   1133        let l10nId;
   1134        if (isAllowed) {
   1135          l10nId = "content-blocking-cookies-view-allowed-label";
   1136          listItem.classList.toggle("allowed", true);
   1137        } else {
   1138          l10nId = "content-blocking-cookies-view-blocked-label";
   1139        }
   1140        document.l10n.setAttributes(stateLabel, l10nId);
   1141 
   1142        let removeException = document.createXULElement("button");
   1143        removeException.className = "permission-popup-permission-remove-button";
   1144        document.l10n.setAttributes(
   1145          removeException,
   1146          "content-blocking-cookies-view-remove-button",
   1147          { domain: origin }
   1148        );
   1149        removeException.appendChild(stateLabel);
   1150 
   1151        removeException.addEventListener(
   1152          "click",
   1153          () => {
   1154            this._clearException(origin);
   1155            removeException.remove();
   1156            listItem.classList.toggle("allowed", !isAllowed);
   1157          },
   1158          { once: true }
   1159        );
   1160        listItem.append(removeException);
   1161      }
   1162 
   1163      return listItem;
   1164    }
   1165  })();
   1166 
   1167 let SocialTracking =
   1168  new (class SocialTrackingProtection extends ProtectionCategory {
   1169    iconSrc = "chrome://browser/skin/thumb-down.svg";
   1170    l10nKeys = {
   1171      content: "social-media-trackers",
   1172      general: "social-tracking",
   1173      title: {
   1174        blocking: "protections-blocking-social-media-trackers",
   1175        "not-blocking": "protections-not-blocking-social-media-trackers",
   1176      },
   1177    };
   1178 
   1179    constructor() {
   1180      super(
   1181        "socialblock",
   1182        {
   1183          prefEnabled: "privacy.socialtracking.block_cookies.enabled",
   1184        },
   1185        {
   1186          load: Ci.nsIWebProgressListener.STATE_LOADED_SOCIALTRACKING_CONTENT,
   1187          block: Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT,
   1188        }
   1189      );
   1190 
   1191      this.prefStpTpEnabled =
   1192        "privacy.trackingprotection.socialtracking.enabled";
   1193      this.prefSTPCookieEnabled = this.prefEnabled;
   1194      this.prefCookieBehavior = "network.cookie.cookieBehavior";
   1195 
   1196      XPCOMUtils.defineLazyPreferenceGetter(
   1197        this,
   1198        "socialTrackingProtectionEnabled",
   1199        this.prefStpTpEnabled,
   1200        false,
   1201        this.updateCategoryItem.bind(this)
   1202      );
   1203      XPCOMUtils.defineLazyPreferenceGetter(
   1204        this,
   1205        "rejectTrackingCookies",
   1206        this.prefCookieBehavior,
   1207        null,
   1208        this.updateCategoryItem.bind(this),
   1209        val =>
   1210          [
   1211            Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
   1212            Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
   1213          ].includes(val)
   1214      );
   1215    }
   1216 
   1217    get blockingEnabled() {
   1218      return (
   1219        (this.socialTrackingProtectionEnabled || this.rejectTrackingCookies) &&
   1220        this.enabled
   1221      );
   1222    }
   1223 
   1224    isBlockingCookies(state) {
   1225      return (
   1226        (state &
   1227          Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER) !=
   1228        0
   1229      );
   1230    }
   1231 
   1232    isBlocking(state) {
   1233      return super.isBlocking(state) || this.isBlockingCookies(state);
   1234    }
   1235 
   1236    isAllowing(state) {
   1237      if (this.socialTrackingProtectionEnabled) {
   1238        return super.isAllowing(state);
   1239      }
   1240 
   1241      return (
   1242        (state &
   1243          Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER) !=
   1244        0
   1245      );
   1246    }
   1247 
   1248    updateCategoryItem() {
   1249      // Can't get `this.categoryItem` without the popup. Using the popup instead
   1250      // of `this.categoryItem` to guard access, because the category item getter
   1251      // can trigger bug 1543537. If there's no popup, we'll be called again the
   1252      // first time the popup shows.
   1253      if (!gProtectionsHandler._protectionsPopup) {
   1254        return;
   1255      }
   1256      if (this.enabled) {
   1257        this.categoryItem.removeAttribute("uidisabled");
   1258      } else {
   1259        this.categoryItem.setAttribute("uidisabled", true);
   1260      }
   1261      this.categoryItem.classList.toggle("blocked", this.blockingEnabled);
   1262    }
   1263  })();
   1264 
   1265 /**
   1266 * Singleton to manage the cookie banner feature section in the protections
   1267 * panel and the cookie banner handling subview.
   1268 */
   1269 let cookieBannerHandling = new (class {
   1270  // Check if this is a private window. We don't expect PBM state to change
   1271  // during the lifetime of this window.
   1272  #isPrivateBrowsing = PrivateBrowsingUtils.isWindowPrivate(window);
   1273 
   1274  constructor() {
   1275    XPCOMUtils.defineLazyPreferenceGetter(
   1276      this,
   1277      "_serviceModePref",
   1278      "cookiebanners.service.mode",
   1279      Ci.nsICookieBannerService.MODE_DISABLED
   1280    );
   1281    XPCOMUtils.defineLazyPreferenceGetter(
   1282      this,
   1283      "_serviceModePrefPrivateBrowsing",
   1284      "cookiebanners.service.mode.privateBrowsing",
   1285      Ci.nsICookieBannerService.MODE_DISABLED
   1286    );
   1287    XPCOMUtils.defineLazyPreferenceGetter(
   1288      this,
   1289      "_serviceDetectOnly",
   1290      "cookiebanners.service.detectOnly",
   1291      false
   1292    );
   1293    XPCOMUtils.defineLazyPreferenceGetter(
   1294      this,
   1295      "_uiEnabled",
   1296      "cookiebanners.ui.desktop.enabled",
   1297      false
   1298    );
   1299    ChromeUtils.defineLazyGetter(this, "_cookieBannerSection", () =>
   1300      document.getElementById("protections-popup-cookie-banner-section")
   1301    );
   1302    ChromeUtils.defineLazyGetter(this, "_cookieBannerSectionSeparator", () =>
   1303      document.getElementById(
   1304        "protections-popup-cookie-banner-section-separator"
   1305      )
   1306    );
   1307    ChromeUtils.defineLazyGetter(this, "_cookieBannerSwitch", () =>
   1308      document.getElementById("protections-popup-cookie-banner-switch")
   1309    );
   1310    ChromeUtils.defineLazyGetter(this, "_cookieBannerSubview", () =>
   1311      document.getElementById("protections-popup-cookieBannerView")
   1312    );
   1313    ChromeUtils.defineLazyGetter(this, "_cookieBannerEnableSite", () =>
   1314      document.getElementById("cookieBannerView-enable-site")
   1315    );
   1316    ChromeUtils.defineLazyGetter(this, "_cookieBannerDisableSite", () =>
   1317      document.getElementById("cookieBannerView-disable-site")
   1318    );
   1319  }
   1320 
   1321  /**
   1322   * Tests if the current site has a user-created exception from the default
   1323   * cookie banner handling mode. Currently that means the feature is disabled
   1324   * for the current site.
   1325   *
   1326   * Note: bug 1790688 will move this mode handling logic into the
   1327   * nsCookieBannerService.
   1328   *
   1329   * @returns {boolean} - true if the user has manually created an exception.
   1330   */
   1331  get #hasException() {
   1332    // If the CBH feature is preffed off, we can't have an exception.
   1333    if (!Services.cookieBanners.isEnabled) {
   1334      return false;
   1335    }
   1336 
   1337    // URLs containing IP addresses are not supported by the CBH service, and
   1338    // will throw. In this case, users can't create an exception, so initialize
   1339    // `pref` to the default value returned by `getDomainPref`.
   1340    let pref = Ci.nsICookieBannerService.MODE_UNSET;
   1341    try {
   1342      pref = Services.cookieBanners.getDomainPref(
   1343        gBrowser.currentURI,
   1344        this.#isPrivateBrowsing
   1345      );
   1346    } catch (ex) {
   1347      console.error(
   1348        "Cookie Banner Handling error checking for per-site exceptions: ",
   1349        ex
   1350      );
   1351    }
   1352    return pref == Ci.nsICookieBannerService.MODE_DISABLED;
   1353  }
   1354 
   1355  /**
   1356   * Tests if the cookie banner handling code supports the current site.
   1357   *
   1358   * See nsICookieBannerService.hasRuleForBrowsingContextTree for details.
   1359   *
   1360   * @returns {boolean} - true if the base domain is in the list of rules.
   1361   */
   1362  get isSiteSupported() {
   1363    return (
   1364      Services.cookieBanners.isEnabled &&
   1365      Services.cookieBanners.hasRuleForBrowsingContextTree(
   1366        gBrowser.selectedBrowser.browsingContext
   1367      )
   1368    );
   1369  }
   1370 
   1371  /**
   1372   * @returns {string} - Base domain (eTLD + 1) used for clearing site data.
   1373   */
   1374  get #currentBaseDomain() {
   1375    return gBrowser.contentPrincipal.baseDomain;
   1376  }
   1377 
   1378  /**
   1379   * Helper method used by both updateSection and updateSubView to map internal
   1380   * state to UI attribute state. We have to separately set the subview's state
   1381   * because the subview is not a descendant of the menu item in the DOM, and
   1382   * we rely on CSS to toggle UI visibility based on attribute state.
   1383   *
   1384   * @returns A string value to be set as a UI attribute value.
   1385   */
   1386  get #uiState() {
   1387    if (this.#hasException) {
   1388      return "site-disabled";
   1389    } else if (this.isSiteSupported) {
   1390      return "detected";
   1391    }
   1392    return "undetected";
   1393  }
   1394 
   1395  updateSection() {
   1396    let showSection = this.#shouldShowSection();
   1397    let state = this.#uiState;
   1398 
   1399    for (let el of [
   1400      this._cookieBannerSection,
   1401      this._cookieBannerSectionSeparator,
   1402    ]) {
   1403      el.hidden = !showSection;
   1404    }
   1405 
   1406    this._cookieBannerSection.dataset.state = state;
   1407 
   1408    // On unsupported sites, disable button styling and click behavior.
   1409    // Note: to be replaced with a "please support site" subview in bug 1801971.
   1410    if (state == "undetected") {
   1411      this._cookieBannerSection.setAttribute("disabled", true);
   1412      this._cookieBannerSwitch.classList.remove("subviewbutton-nav");
   1413      this._cookieBannerSwitch.setAttribute("disabled", true);
   1414    } else {
   1415      this._cookieBannerSection.removeAttribute("disabled");
   1416      this._cookieBannerSwitch.classList.add("subviewbutton-nav");
   1417      this._cookieBannerSwitch.removeAttribute("disabled");
   1418    }
   1419  }
   1420 
   1421  #shouldShowSection() {
   1422    // Don't show UI if globally disabled by pref, or if the cookie service
   1423    // is in detect-only mode.
   1424    if (!this._uiEnabled || this._serviceDetectOnly) {
   1425      return false;
   1426    }
   1427 
   1428    // Show the section if the feature is not in disabled mode, being sure to
   1429    // check the different prefs for regular and private windows.
   1430    if (this.#isPrivateBrowsing) {
   1431      return (
   1432        this._serviceModePrefPrivateBrowsing !=
   1433        Ci.nsICookieBannerService.MODE_DISABLED
   1434      );
   1435    }
   1436    return this._serviceModePref != Ci.nsICookieBannerService.MODE_DISABLED;
   1437  }
   1438 
   1439  /*
   1440   * Updates the cookie banner handling subview just before it's shown.
   1441   */
   1442  updateSubView() {
   1443    this._cookieBannerSubview.dataset.state = this.#uiState;
   1444 
   1445    let baseDomain = JSON.stringify({ host: this.#currentBaseDomain });
   1446    this._cookieBannerEnableSite.setAttribute("data-l10n-args", baseDomain);
   1447    this._cookieBannerDisableSite.setAttribute("data-l10n-args", baseDomain);
   1448  }
   1449 
   1450  async #disableCookieBannerHandling() {
   1451    // We can't clear data during a private browsing session until bug 1818783
   1452    // is fixed. In the meantime, don't allow the cookie banner controls in a
   1453    // private window to clear data for regular browsing mode.
   1454    if (!this.#isPrivateBrowsing) {
   1455      await SiteDataManager.remove(this.#currentBaseDomain);
   1456    }
   1457    Services.cookieBanners.setDomainPref(
   1458      gBrowser.currentURI,
   1459      Ci.nsICookieBannerService.MODE_DISABLED,
   1460      this.#isPrivateBrowsing
   1461    );
   1462  }
   1463 
   1464  #enableCookieBannerHandling() {
   1465    Services.cookieBanners.removeDomainPref(
   1466      gBrowser.currentURI,
   1467      this.#isPrivateBrowsing
   1468    );
   1469  }
   1470 
   1471  async onCookieBannerToggleCommand() {
   1472    let hasException =
   1473      this._cookieBannerSection.toggleAttribute("hasException");
   1474    if (hasException) {
   1475      await this.#disableCookieBannerHandling();
   1476      Glean.securityUiProtectionspopup.clickCookiebToggleOff.record();
   1477    } else {
   1478      this.#enableCookieBannerHandling();
   1479      Glean.securityUiProtectionspopup.clickCookiebToggleOn.record();
   1480    }
   1481    gProtectionsHandler._hidePopup();
   1482    gBrowser.reloadTab(gBrowser.selectedTab);
   1483  }
   1484 })();
   1485 
   1486 /**
   1487 * Utility object to handle manipulations of the protections indicators in the UI
   1488 */
   1489 var gProtectionsHandler = {
   1490  PREF_CB_CATEGORY: "browser.contentblocking.category",
   1491 
   1492  /**
   1493   * Contains an array of smartblock compatible sites and information on the corresponding shim
   1494   * sites is a list of compatible sites
   1495   * shimId is the id of the shim blocking content from the origin
   1496   * displayName is the name shown for the toggle used for blocking/unblocking the origin
   1497   */
   1498  smartblockEmbedInfo: [
   1499    {
   1500      matchPatterns: ["https://itisatracker.org/*"],
   1501      shimId: "EmbedTestShim",
   1502      displayName: "Test",
   1503    },
   1504    {
   1505      matchPatterns: [
   1506        "https://www.instagram.com/*",
   1507        "https://platform.instagram.com/*",
   1508      ],
   1509      shimId: "InstagramEmbed",
   1510      displayName: "Instagram",
   1511    },
   1512    {
   1513      matchPatterns: ["https://www.tiktok.com/*"],
   1514      shimId: "TiktokEmbed",
   1515      displayName: "Tiktok",
   1516    },
   1517    {
   1518      matchPatterns: ["https://platform.twitter.com/*"],
   1519      shimId: "TwitterEmbed",
   1520      displayName: "X",
   1521    },
   1522    {
   1523      matchPatterns: ["https://*.disqus.com/*"],
   1524      shimId: "DisqusEmbed",
   1525      displayName: "Disqus",
   1526    },
   1527  ],
   1528 
   1529  /**
   1530   * Keeps track of if a smartblock toggle has been clicked since the panel was opened. Resets
   1531   * everytime the panel is closed. Used for telemetry purposes.
   1532   */
   1533  _hasClickedSmartBlockEmbedToggle: false,
   1534 
   1535  /**
   1536   * Keeps track of what was responsible for opening the protections panel popup. Used for
   1537   * telemetry purposes.
   1538   */
   1539  _protectionsPopupOpeningReason: null,
   1540 
   1541  _protectionsPopup: null,
   1542  _initializePopup() {
   1543    if (!this._protectionsPopup) {
   1544      let wrapper = document.getElementById("template-protections-popup");
   1545      this._protectionsPopup = wrapper.content.firstElementChild;
   1546      this._protectionsPopup.addEventListener("popupshown", this);
   1547      this._protectionsPopup.addEventListener("popuphidden", this);
   1548      wrapper.replaceWith(wrapper.content);
   1549 
   1550      this.maybeSetMilestoneCounterText();
   1551 
   1552      for (let blocker of Object.values(this.blockers)) {
   1553        blocker.updateCategoryItem();
   1554      }
   1555 
   1556      this._protectionsPopup.addEventListener("command", this);
   1557      this._protectionsPopup.addEventListener("popupshown", this);
   1558      this._protectionsPopup.addEventListener("popuphidden", this);
   1559 
   1560      function openTooltip(event) {
   1561        document.getElementById(event.target.tooltip).openPopup(event.target);
   1562      }
   1563      function closeTooltip(event) {
   1564        document.getElementById(event.target.tooltip).hidePopup();
   1565      }
   1566      let notBlockingWhy = document.getElementById(
   1567        "protections-popup-not-blocking-section-why"
   1568      );
   1569      notBlockingWhy.addEventListener("mouseover", openTooltip);
   1570      notBlockingWhy.addEventListener("focus", openTooltip);
   1571      notBlockingWhy.addEventListener("mouseout", closeTooltip);
   1572      notBlockingWhy.addEventListener("blur", closeTooltip);
   1573 
   1574      document
   1575        .getElementById(
   1576          "protections-popup-trackers-blocked-counter-description"
   1577        )
   1578        .addEventListener("click", () =>
   1579          gProtectionsHandler.openProtections(true)
   1580        );
   1581      document
   1582        .getElementById("protections-popup-cookie-banner-switch")
   1583        .addEventListener("click", () =>
   1584          gProtectionsHandler.onCookieBannerClick()
   1585        );
   1586    }
   1587  },
   1588 
   1589  _hidePopup() {
   1590    if (this._protectionsPopup) {
   1591      PanelMultiView.hidePopup(this._protectionsPopup);
   1592    }
   1593  },
   1594 
   1595  // smart getters
   1596  get iconBox() {
   1597    delete this.iconBox;
   1598    return (this.iconBox = document.getElementById(
   1599      "tracking-protection-icon-box"
   1600    ));
   1601  },
   1602  get _protectionsPopupMultiView() {
   1603    delete this._protectionsPopupMultiView;
   1604    return (this._protectionsPopupMultiView = document.getElementById(
   1605      "protections-popup-multiView"
   1606    ));
   1607  },
   1608  get _protectionsPopupMainView() {
   1609    delete this._protectionsPopupMainView;
   1610    return (this._protectionsPopupMainView = document.getElementById(
   1611      "protections-popup-mainView"
   1612    ));
   1613  },
   1614  get _protectionsPopupMainViewHeaderLabel() {
   1615    delete this._protectionsPopupMainViewHeaderLabel;
   1616    return (this._protectionsPopupMainViewHeaderLabel = document.getElementById(
   1617      "protections-popup-mainView-panel-header-span"
   1618    ));
   1619  },
   1620  get _protectionsPopupTPSwitch() {
   1621    delete this._protectionsPopupTPSwitch;
   1622    return (this._protectionsPopupTPSwitch = document.getElementById(
   1623      "protections-popup-tp-switch"
   1624    ));
   1625  },
   1626  get _protectionsPopupCategoryList() {
   1627    delete this._protectionsPopupCategoryList;
   1628    return (this._protectionsPopupCategoryList = document.getElementById(
   1629      "protections-popup-category-list"
   1630    ));
   1631  },
   1632  get _protectionsPopupBlockingHeader() {
   1633    delete this._protectionsPopupBlockingHeader;
   1634    return (this._protectionsPopupBlockingHeader = document.getElementById(
   1635      "protections-popup-blocking-section-header"
   1636    ));
   1637  },
   1638  get _protectionsPopupNotBlockingHeader() {
   1639    delete this._protectionsPopupNotBlockingHeader;
   1640    return (this._protectionsPopupNotBlockingHeader = document.getElementById(
   1641      "protections-popup-not-blocking-section-header"
   1642    ));
   1643  },
   1644  get _protectionsPopupNotFoundHeader() {
   1645    delete this._protectionsPopupNotFoundHeader;
   1646    return (this._protectionsPopupNotFoundHeader = document.getElementById(
   1647      "protections-popup-not-found-section-header"
   1648    ));
   1649  },
   1650  get _protectionsPopupSmartblockContainer() {
   1651    delete this._protectionsPopupSmartblockContainer;
   1652    return (this._protectionsPopupSmartblockContainer = document.getElementById(
   1653      "protections-popup-smartblock-highlight-container"
   1654    ));
   1655  },
   1656  get _protectionsPopupSmartblockDescription() {
   1657    delete this._protectionsPopupSmartblockDescription;
   1658    return (this._protectionsPopupSmartblockDescription =
   1659      document.getElementById("protections-popup-smartblock-description"));
   1660  },
   1661  get _protectionsPopupSmartblockToggleContainer() {
   1662    delete this._protectionsPopupSmartblockToggleContainer;
   1663    return (this._protectionsPopupSmartblockToggleContainer =
   1664      document.getElementById("protections-popup-smartblock-toggle-container"));
   1665  },
   1666  get _protectionsPopupSettingsButton() {
   1667    delete this._protectionsPopupSettingsButton;
   1668    return (this._protectionsPopupSettingsButton = document.getElementById(
   1669      "protections-popup-settings-button"
   1670    ));
   1671  },
   1672  get _protectionsPopupFooter() {
   1673    delete this._protectionsPopupFooter;
   1674    return (this._protectionsPopupFooter = document.getElementById(
   1675      "protections-popup-footer"
   1676    ));
   1677  },
   1678  get _protectionsPopupTrackersCounterBox() {
   1679    delete this._protectionsPopupTrackersCounterBox;
   1680    return (this._protectionsPopupTrackersCounterBox = document.getElementById(
   1681      "protections-popup-trackers-blocked-counter-box"
   1682    ));
   1683  },
   1684  get _protectionsPopupTrackersCounterDescription() {
   1685    delete this._protectionsPopupTrackersCounterDescription;
   1686    return (this._protectionsPopupTrackersCounterDescription =
   1687      document.getElementById(
   1688        "protections-popup-trackers-blocked-counter-description"
   1689      ));
   1690  },
   1691  get _protectionsPopupFooterProtectionTypeLabel() {
   1692    delete this._protectionsPopupFooterProtectionTypeLabel;
   1693    return (this._protectionsPopupFooterProtectionTypeLabel =
   1694      document.getElementById(
   1695        "protections-popup-footer-protection-type-label"
   1696      ));
   1697  },
   1698  get _trackingProtectionIconTooltipLabel() {
   1699    delete this._trackingProtectionIconTooltipLabel;
   1700    return (this._trackingProtectionIconTooltipLabel = document.getElementById(
   1701      "tracking-protection-icon-tooltip-label"
   1702    ));
   1703  },
   1704  get _trackingProtectionIconContainer() {
   1705    delete this._trackingProtectionIconContainer;
   1706    return (this._trackingProtectionIconContainer = document.getElementById(
   1707      "tracking-protection-icon-container"
   1708    ));
   1709  },
   1710 
   1711  get noTrackersDetectedDescription() {
   1712    delete this.noTrackersDetectedDescription;
   1713    return (this.noTrackersDetectedDescription = document.getElementById(
   1714      "protections-popup-no-trackers-found-description"
   1715    ));
   1716  },
   1717 
   1718  get _protectionsPopupMilestonesText() {
   1719    delete this._protectionsPopupMilestonesText;
   1720    return (this._protectionsPopupMilestonesText = document.getElementById(
   1721      "protections-popup-milestones-text"
   1722    ));
   1723  },
   1724 
   1725  get _notBlockingWhyLink() {
   1726    delete this._notBlockingWhyLink;
   1727    return (this._notBlockingWhyLink = document.getElementById(
   1728      "protections-popup-not-blocking-section-why"
   1729    ));
   1730  },
   1731 
   1732  // A list of blockers that will be displayed in the categories list
   1733  // when blockable content is detected. A blocker must be an object
   1734  // with at least the following two properties:
   1735  //  - enabled: Whether the blocker is currently turned on.
   1736  //  - isDetected(state): Given a content blocking state, whether the blocker has
   1737  //                       either allowed or blocked elements.
   1738  //  - categoryItem: The DOM item that represents the entry in the category list.
   1739  //
   1740  // It may also contain an init() and uninit() function, which will be called
   1741  // on gProtectionsHandler.init() and gProtectionsHandler.uninit().
   1742  // The buttons in the protections panel will appear in the same order as this array.
   1743  blockers: {
   1744    SocialTracking,
   1745    ThirdPartyCookies,
   1746    TrackingProtection,
   1747    Fingerprinting,
   1748    Cryptomining,
   1749  },
   1750 
   1751  init() {
   1752    XPCOMUtils.defineLazyPreferenceGetter(
   1753      this,
   1754      "_protectionsPopupToastTimeout",
   1755      "browser.protections_panel.toast.timeout",
   1756      3000
   1757    );
   1758 
   1759    XPCOMUtils.defineLazyPreferenceGetter(
   1760      this,
   1761      "_protectionsPopupButtonDelay",
   1762      "security.notification_enable_delay",
   1763      500
   1764    );
   1765 
   1766    XPCOMUtils.defineLazyPreferenceGetter(
   1767      this,
   1768      "milestoneListPref",
   1769      "browser.contentblocking.cfr-milestone.milestones",
   1770      "[]",
   1771      () => this.maybeSetMilestoneCounterText(),
   1772      val => JSON.parse(val)
   1773    );
   1774 
   1775    XPCOMUtils.defineLazyPreferenceGetter(
   1776      this,
   1777      "milestonePref",
   1778      "browser.contentblocking.cfr-milestone.milestone-achieved",
   1779      0,
   1780      () => this.maybeSetMilestoneCounterText()
   1781    );
   1782 
   1783    XPCOMUtils.defineLazyPreferenceGetter(
   1784      this,
   1785      "milestoneTimestampPref",
   1786      "browser.contentblocking.cfr-milestone.milestone-shown-time",
   1787      "0",
   1788      null,
   1789      val => parseInt(val)
   1790    );
   1791 
   1792    XPCOMUtils.defineLazyPreferenceGetter(
   1793      this,
   1794      "milestonesEnabledPref",
   1795      "browser.contentblocking.cfr-milestone.enabled",
   1796      false,
   1797      () => this.maybeSetMilestoneCounterText()
   1798    );
   1799 
   1800    XPCOMUtils.defineLazyPreferenceGetter(
   1801      this,
   1802      "protectionsPanelMessageSeen",
   1803      "browser.protections_panel.infoMessage.seen",
   1804      false
   1805    );
   1806 
   1807    XPCOMUtils.defineLazyPreferenceGetter(
   1808      this,
   1809      "smartblockEmbedsEnabledPref",
   1810      "extensions.webcompat.smartblockEmbeds.enabled",
   1811      false
   1812    );
   1813 
   1814    XPCOMUtils.defineLazyPreferenceGetter(
   1815      this,
   1816      "trustPanelEnabledPref",
   1817      "browser.urlbar.trustPanel.featureGate",
   1818      false
   1819    );
   1820 
   1821    for (let blocker of Object.values(this.blockers)) {
   1822      if (blocker.init) {
   1823        blocker.init();
   1824      }
   1825    }
   1826 
   1827    // Add an observer to observe that the history has been cleared.
   1828    Services.obs.addObserver(this, "browser:purge-session-history");
   1829    // Add an observer to listen to requests to open the protections panel
   1830    Services.obs.addObserver(this, "smartblock:open-protections-panel");
   1831 
   1832    // bind the reset toggle sec delay function to this so we can use it
   1833    // as an event listener without this becoming the event target inside
   1834    // the function
   1835    this._resetToggleSecDelay = this._resetToggleSecDelay.bind(this);
   1836  },
   1837 
   1838  uninit() {
   1839    for (let blocker of Object.values(this.blockers)) {
   1840      if (blocker.uninit) {
   1841        blocker.uninit();
   1842      }
   1843    }
   1844 
   1845    Services.obs.removeObserver(this, "browser:purge-session-history");
   1846    Services.obs.removeObserver(this, "smartblock:open-protections-panel");
   1847  },
   1848 
   1849  getTrackingProtectionLabel() {
   1850    const value = Services.prefs.getStringPref(this.PREF_CB_CATEGORY);
   1851 
   1852    switch (value) {
   1853      case "strict":
   1854        return "protections-popup-footer-protection-label-strict";
   1855      case "custom":
   1856        return "protections-popup-footer-protection-label-custom";
   1857      case "standard":
   1858      /* fall through */
   1859      default:
   1860        return "protections-popup-footer-protection-label-standard";
   1861    }
   1862  },
   1863 
   1864  openPreferences(origin) {
   1865    openPreferences("privacy-trackingprotection", { origin });
   1866  },
   1867 
   1868  openProtections(relatedToCurrent = false) {
   1869    switchToTabHavingURI("about:protections", true, {
   1870      replaceQueryString: true,
   1871      relatedToCurrent,
   1872      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
   1873    });
   1874 
   1875    // Don't show the milestones section anymore.
   1876    Services.prefs.clearUserPref(
   1877      "browser.contentblocking.cfr-milestone.milestone-shown-time"
   1878    );
   1879  },
   1880 
   1881  async showTrackersSubview() {
   1882    await TrackingProtection.updateSubView();
   1883    this._protectionsPopupMultiView.showSubView(
   1884      "protections-popup-trackersView"
   1885    );
   1886  },
   1887 
   1888  async showSocialblockerSubview() {
   1889    await SocialTracking.updateSubView();
   1890    this._protectionsPopupMultiView.showSubView(
   1891      "protections-popup-socialblockView"
   1892    );
   1893  },
   1894 
   1895  async showCookiesSubview() {
   1896    await ThirdPartyCookies.updateSubView();
   1897    this._protectionsPopupMultiView.showSubView(
   1898      "protections-popup-cookiesView"
   1899    );
   1900  },
   1901 
   1902  async showFingerprintersSubview() {
   1903    await Fingerprinting.updateSubView();
   1904    this._protectionsPopupMultiView.showSubView(
   1905      "protections-popup-fingerprintersView"
   1906    );
   1907  },
   1908 
   1909  async showCryptominersSubview() {
   1910    await Cryptomining.updateSubView();
   1911    this._protectionsPopupMultiView.showSubView(
   1912      "protections-popup-cryptominersView"
   1913    );
   1914  },
   1915 
   1916  async onCookieBannerClick() {
   1917    if (!cookieBannerHandling.isSiteSupported) {
   1918      return;
   1919    }
   1920    await cookieBannerHandling.updateSubView();
   1921    this._protectionsPopupMultiView.showSubView(
   1922      "protections-popup-cookieBannerView"
   1923    );
   1924  },
   1925 
   1926  shieldHistogramAdd(value) {
   1927    if (PrivateBrowsingUtils.isWindowPrivate(window)) {
   1928      return;
   1929    }
   1930    Glean.contentblocking.trackingProtectionShield.accumulateSingleSample(
   1931      value
   1932    );
   1933  },
   1934 
   1935  cryptominersHistogramAdd(value) {
   1936    Glean.contentblocking.cryptominersBlockedCount[value].add(1);
   1937  },
   1938 
   1939  fingerprintersHistogramAdd(value) {
   1940    Glean.contentblocking.fingerprintersBlockedCount[value].add(1);
   1941  },
   1942 
   1943  handleProtectionsButtonEvent(event) {
   1944    event.stopPropagation();
   1945    if (
   1946      (event.type == "click" && event.button != 0) ||
   1947      (event.type == "keypress" &&
   1948        event.charCode != KeyEvent.DOM_VK_SPACE &&
   1949        event.keyCode != KeyEvent.DOM_VK_RETURN)
   1950    ) {
   1951      return; // Left click, space or enter only
   1952    }
   1953 
   1954    this.showProtectionsPopup({ event, openingReason: "shieldButtonClicked" });
   1955  },
   1956 
   1957  onPopupShown(event) {
   1958    if (event.target == this._protectionsPopup) {
   1959      PopupNotifications.suppressWhileOpen(this._protectionsPopup);
   1960 
   1961      window.addEventListener("focus", this, true);
   1962      this._protectionsPopupTPSwitch.addEventListener("toggle", this);
   1963 
   1964      // Insert the info message if needed. This will be shown once and then
   1965      // remain collapsed.
   1966      this._insertProtectionsPanelInfoMessage(event);
   1967 
   1968      // Record telemetry for open, don't record if the panel open is only a toast
   1969      if (!event.target.hasAttribute("toast")) {
   1970        Glean.securityUiProtectionspopup.openProtectionsPopup.record({
   1971          openingReason: this._protectionsPopupOpeningReason,
   1972          smartblockEmbedTogglesShown:
   1973            !this._protectionsPopupSmartblockContainer.hidden,
   1974        });
   1975      }
   1976 
   1977      // Add the "open" attribute to the tracking protection icon container
   1978      // for styling.
   1979      // Only set the attribute once the panel is opened to avoid icon being
   1980      // incorrectly highlighted if opening is cancelled. See Bug 1926460.
   1981      this._trackingProtectionIconContainer.setAttribute("open", "true");
   1982 
   1983      // Disable the toggles for a short time after opening via SmartBlock placeholder button
   1984      // to prevent clickjacking.
   1985      if (this._protectionsPopupOpeningReason == "embedPlaceholderButton") {
   1986        this._disablePopupToggles();
   1987        this._protectionsPopupToggleDelayTimer = setTimeout(() => {
   1988          this._enablePopupToggles();
   1989          delete this._protectionsPopupToggleDelayTimer;
   1990        }, this._protectionsPopupButtonDelay);
   1991      }
   1992 
   1993      ReportBrokenSite.updateParentMenu(event);
   1994    }
   1995  },
   1996 
   1997  onPopupHidden(event) {
   1998    if (event.target == this._protectionsPopup) {
   1999      window.removeEventListener("focus", this, true);
   2000      this._protectionsPopupTPSwitch.removeEventListener("toggle", this);
   2001 
   2002      // Record close telemetry, don't record for toasts
   2003      if (!event.target.hasAttribute("toast")) {
   2004        Glean.securityUiProtectionspopup.closeProtectionsPopup.record({
   2005          openingReason: this._protectionsPopupOpeningReason,
   2006          smartblockToggleClicked: this._hasClickedSmartBlockEmbedToggle,
   2007        });
   2008      }
   2009 
   2010      if (this._protectionsPopupToggleDelayTimer) {
   2011        clearTimeout(this._protectionsPopupToggleDelayTimer);
   2012        this._enablePopupToggles();
   2013        delete this._protectionsPopupToggleDelayTimer;
   2014      }
   2015 
   2016      this._hasClickedSmartBlockEmbedToggle = false;
   2017      this._protectionsPopupOpeningReason = null;
   2018    }
   2019  },
   2020 
   2021  async onTrackingProtectionIconHoveredOrFocused() {
   2022    // We would try to pre-fetch the data whenever the shield icon is hovered or
   2023    // focused. We check focus event here due to the keyboard navigation.
   2024    if (this._updatingFooter) {
   2025      return;
   2026    }
   2027    this._updatingFooter = true;
   2028 
   2029    // Take the popup out of its template.
   2030    this._initializePopup();
   2031 
   2032    // Get the tracker count and set it to the counter in the footer.
   2033    const trackerCount = await TrackingDBService.sumAllEvents();
   2034    this.setTrackersBlockedCounter(trackerCount);
   2035 
   2036    // Set tracking protection label
   2037    const l10nId = this.getTrackingProtectionLabel();
   2038    const elem = this._protectionsPopupFooterProtectionTypeLabel;
   2039    document.l10n.setAttributes(elem, l10nId);
   2040 
   2041    // Try to get the earliest recorded date in case that there was no record
   2042    // during the initiation but new records come after that.
   2043    await this.maybeUpdateEarliestRecordedDateTooltip(trackerCount);
   2044 
   2045    this._updatingFooter = false;
   2046  },
   2047 
   2048  // This triggers from top level location changes.
   2049  onLocationChange() {
   2050    if (this._showToastAfterRefresh) {
   2051      this._showToastAfterRefresh = false;
   2052 
   2053      // We only display the toast if we're still on the same page.
   2054      if (
   2055        this._previousURI == gBrowser.currentURI.spec &&
   2056        this._previousOuterWindowID == gBrowser.selectedBrowser.outerWindowID
   2057      ) {
   2058        this.showProtectionsPopup({
   2059          toast: true,
   2060        });
   2061      }
   2062    }
   2063 
   2064    // Reset blocking and exception status so that we can send telemetry
   2065    this.hadShieldState = false;
   2066 
   2067    // Don't deal with about:, file: etc.
   2068    if (!ContentBlockingAllowList.canHandle(gBrowser.selectedBrowser)) {
   2069      // We hide the icon and thus avoid showing the doorhanger, since
   2070      // the information contained there would mostly be broken and/or
   2071      // irrelevant anyway.
   2072      this._trackingProtectionIconContainer.hidden = true;
   2073      return;
   2074    }
   2075    this._trackingProtectionIconContainer.hidden = false;
   2076 
   2077    // Check whether the user has added an exception for this site.
   2078    this.hasException = ContentBlockingAllowList.includes(
   2079      gBrowser.selectedBrowser
   2080    );
   2081 
   2082    if (this._protectionsPopup) {
   2083      this._protectionsPopup.toggleAttribute("hasException", this.hasException);
   2084    }
   2085    this.iconBox.toggleAttribute("hasException", this.hasException);
   2086 
   2087    // Add to telemetry per page load as a baseline measurement.
   2088    this.fingerprintersHistogramAdd("pageLoad");
   2089    this.cryptominersHistogramAdd("pageLoad");
   2090    this.shieldHistogramAdd(0);
   2091  },
   2092 
   2093  notifyContentBlockingEvent(event) {
   2094    // We don't notify observers until the document stops loading, therefore
   2095    // a merged event can be sent, which gives an opportunity to decide the
   2096    // priority by the handler.
   2097    // Content blocking events coming after stopping will not be merged, and are
   2098    // sent directly.
   2099    if (!this._isStoppedState || !this.anyDetected) {
   2100      return;
   2101    }
   2102 
   2103    let uri = gBrowser.currentURI;
   2104    let uriHost = uri.asciiHost ? uri.host : uri.spec;
   2105    Services.obs.notifyObservers(
   2106      {
   2107        wrappedJSObject: {
   2108          browser: gBrowser.selectedBrowser,
   2109          host: uriHost,
   2110          event,
   2111        },
   2112      },
   2113      "SiteProtection:ContentBlockingEvent"
   2114    );
   2115  },
   2116 
   2117  onStateChange(aWebProgress, stateFlags) {
   2118    if (!aWebProgress.isTopLevel) {
   2119      return;
   2120    }
   2121 
   2122    this._isStoppedState = !!(
   2123      stateFlags & Ci.nsIWebProgressListener.STATE_STOP
   2124    );
   2125    this.notifyContentBlockingEvent(
   2126      gBrowser.selectedBrowser.getContentBlockingEvents()
   2127    );
   2128  },
   2129 
   2130  /**
   2131   * Update the in-panel UI given a blocking event. Called when the popup
   2132   * is being shown, or when the popup is open while a new event comes in.
   2133   */
   2134  updatePanelForBlockingEvent(event) {
   2135    // Update the categories:
   2136    for (let blocker of Object.values(this.blockers)) {
   2137      if (blocker.categoryItem.hasAttribute("uidisabled")) {
   2138        continue;
   2139      }
   2140      blocker.categoryItem.classList.toggle(
   2141        "notFound",
   2142        !blocker.isDetected(event)
   2143      );
   2144      blocker.categoryItem.classList.toggle(
   2145        "subviewbutton-nav",
   2146        blocker.isDetected(event)
   2147      );
   2148    }
   2149 
   2150    // And the popup attributes:
   2151    this._protectionsPopup.toggleAttribute("detected", this.anyDetected);
   2152    this._protectionsPopup.toggleAttribute("blocking", this.anyBlocking);
   2153    this._protectionsPopup.toggleAttribute("hasException", this.hasException);
   2154 
   2155    this.noTrackersDetectedDescription.hidden = this.anyDetected;
   2156 
   2157    if (this.anyDetected) {
   2158      // Reorder categories if any are in use.
   2159      this.reorderCategoryItems();
   2160    }
   2161  },
   2162 
   2163  reportBlockingEventTelemetry(event, isSimulated, previousState) {
   2164    if (!isSimulated) {
   2165      if (this.hasException && !this.hadShieldState) {
   2166        this.hadShieldState = true;
   2167        this.shieldHistogramAdd(1);
   2168      } else if (
   2169        !this.hasException &&
   2170        this.anyBlocking &&
   2171        !this.hadShieldState
   2172      ) {
   2173        this.hadShieldState = true;
   2174        this.shieldHistogramAdd(2);
   2175      }
   2176    }
   2177 
   2178    // We report up to one instance of fingerprinting and cryptomining
   2179    // blocking and/or allowing per page load.
   2180    let fingerprintingBlocking =
   2181      Fingerprinting.isBlocking(event) &&
   2182      !Fingerprinting.isBlocking(previousState);
   2183    let fingerprintingAllowing =
   2184      Fingerprinting.isAllowing(event) &&
   2185      !Fingerprinting.isAllowing(previousState);
   2186    let cryptominingBlocking =
   2187      Cryptomining.isBlocking(event) && !Cryptomining.isBlocking(previousState);
   2188    let cryptominingAllowing =
   2189      Cryptomining.isAllowing(event) && !Cryptomining.isAllowing(previousState);
   2190 
   2191    if (fingerprintingBlocking) {
   2192      this.fingerprintersHistogramAdd("blocked");
   2193    } else if (fingerprintingAllowing) {
   2194      this.fingerprintersHistogramAdd("allowed");
   2195    }
   2196 
   2197    if (cryptominingBlocking) {
   2198      this.cryptominersHistogramAdd("blocked");
   2199    } else if (cryptominingAllowing) {
   2200      this.cryptominersHistogramAdd("allowed");
   2201    }
   2202  },
   2203 
   2204  onContentBlockingEvent(event, webProgress, isSimulated, previousState) {
   2205    // Don't deal with about:, file: etc.
   2206    if (!ContentBlockingAllowList.canHandle(gBrowser.selectedBrowser)) {
   2207      this.iconBox.removeAttribute("active");
   2208      this.iconBox.removeAttribute("hasException");
   2209      return;
   2210    }
   2211 
   2212    // First update all our internal state based on the allowlist and the
   2213    // different blockers:
   2214    this.anyDetected = false;
   2215    this.anyBlocking = false;
   2216    this._lastEvent = event;
   2217 
   2218    // Check whether the user has added an exception for this site.
   2219    this.hasException = ContentBlockingAllowList.includes(
   2220      gBrowser.selectedBrowser
   2221    );
   2222 
   2223    // Update blocker state and find if they detected or blocked anything.
   2224    for (let blocker of Object.values(this.blockers)) {
   2225      if (blocker.categoryItem?.hasAttribute("uidisabled")) {
   2226        continue;
   2227      }
   2228      // Store data on whether the blocker is activated for reporting it
   2229      // using the "report breakage" dialog. Under normal circumstances this
   2230      // dialog should only be able to open in the currently selected tab
   2231      // and onSecurityChange runs on tab switch, so we can avoid associating
   2232      // the data with the document directly.
   2233      blocker.activated = blocker.isBlocking(event);
   2234      this.anyDetected = this.anyDetected || blocker.isDetected(event);
   2235      this.anyBlocking = this.anyBlocking || blocker.activated;
   2236    }
   2237 
   2238    this._categoryItemOrderInvalidated = true;
   2239 
   2240    // Now, update the icon UI:
   2241 
   2242    // We consider the shield state "active" when some kind of blocking activity
   2243    // occurs on the page.  Note that merely allowing the loading of content that
   2244    // we could have blocked does not trigger the appearance of the shield.
   2245    // This state will be overriden later if there's an exception set for this site.
   2246    this.iconBox.toggleAttribute("active", this.anyBlocking);
   2247    this.iconBox.toggleAttribute("hasException", this.hasException);
   2248 
   2249    // Update the icon's tooltip:
   2250    if (this.hasException) {
   2251      this.showDisabledTooltipForTPIcon();
   2252    } else if (this.anyBlocking) {
   2253      this.showActiveTooltipForTPIcon();
   2254    } else {
   2255      this.showNoTrackerTooltipForTPIcon();
   2256    }
   2257 
   2258    // Update the panel if it's open.
   2259    let isPanelOpen = ["showing", "open"].includes(
   2260      this._protectionsPopup?.state
   2261    );
   2262    if (isPanelOpen) {
   2263      this.updatePanelForBlockingEvent(event);
   2264    }
   2265 
   2266    // Notify other consumers, like CFR.
   2267    // Don't send a content blocking event to CFR for
   2268    // tab switches since this will already be done via
   2269    // onStateChange.
   2270    if (!isSimulated) {
   2271      this.notifyContentBlockingEvent(event);
   2272    }
   2273 
   2274    // Finally, report telemetry.
   2275    this.reportBlockingEventTelemetry(event, isSimulated, previousState);
   2276  },
   2277 
   2278  onCommand(event) {
   2279    switch (event.target.id) {
   2280      case "protections-popup-category-trackers":
   2281        gProtectionsHandler.showTrackersSubview(event);
   2282        Glean.securityUiProtectionspopup.clickTrackers.record();
   2283        break;
   2284      case "protections-popup-category-socialblock":
   2285        gProtectionsHandler.showSocialblockerSubview(event);
   2286        Glean.securityUiProtectionspopup.clickSocial.record();
   2287        break;
   2288      case "protections-popup-category-cookies":
   2289        gProtectionsHandler.showCookiesSubview(event);
   2290        Glean.securityUiProtectionspopup.clickCookies.record();
   2291        break;
   2292      case "protections-popup-category-cryptominers":
   2293        gProtectionsHandler.showCryptominersSubview(event);
   2294        Glean.securityUiProtectionspopup.clickCryptominers.record();
   2295        return;
   2296      case "protections-popup-category-fingerprinters":
   2297        gProtectionsHandler.showFingerprintersSubview(event);
   2298        Glean.securityUiProtectionspopup.clickFingerprinters.record();
   2299        break;
   2300      case "protections-popup-settings-button":
   2301        gProtectionsHandler.openPreferences();
   2302        Glean.securityUiProtectionspopup.clickSettings.record();
   2303        break;
   2304      case "protections-popup-show-report-button":
   2305        gProtectionsHandler.openProtections(true);
   2306        Glean.securityUiProtectionspopup.clickFullReport.record();
   2307        break;
   2308      case "protections-popup-milestones-content":
   2309        gProtectionsHandler.openProtections(true);
   2310        Glean.securityUiProtectionspopup.clickMilestoneMessage.record();
   2311        break;
   2312      case "protections-popup-trackersView-settings-button":
   2313        gProtectionsHandler.openPreferences();
   2314        Glean.securityUiProtectionspopup.clickSubviewSettings.record({
   2315          value: "trackers",
   2316        });
   2317        break;
   2318      case "protections-popup-socialblockView-settings-button":
   2319        gProtectionsHandler.openPreferences();
   2320        Glean.securityUiProtectionspopup.clickSubviewSettings.record({
   2321          value: "social",
   2322        });
   2323        break;
   2324      case "protections-popup-cookiesView-settings-button":
   2325        gProtectionsHandler.openPreferences();
   2326        Glean.securityUiProtectionspopup.clickSubviewSettings.record({
   2327          value: "cookies",
   2328        });
   2329        break;
   2330      case "protections-popup-fingerprintersView-settings-button":
   2331        gProtectionsHandler.openPreferences();
   2332        Glean.securityUiProtectionspopup.clickSubviewSettings.record({
   2333          value: "fingerprinters",
   2334        });
   2335        break;
   2336      case "protections-popup-cryptominersView-settings-button":
   2337        gProtectionsHandler.openPreferences();
   2338        Glean.securityUiProtectionspopup.clickSubviewSettings.record({
   2339          value: "cryptominers",
   2340        });
   2341        break;
   2342      case "protections-popup-cookieBannerView-cancel":
   2343        gProtectionsHandler._protectionsPopupMultiView.goBack();
   2344        break;
   2345      case "protections-popup-cookieBannerView-enable-button":
   2346      case "protections-popup-cookieBannerView-disable-button":
   2347        gProtectionsHandler.onCookieBannerToggleCommand();
   2348        break;
   2349      case "protections-popup-toast-panel-tp-on-desc":
   2350      case "protections-popup-toast-panel-tp-off-desc":
   2351        // Hide the toast first.
   2352        PanelMultiView.hidePopup(this._protectionsPopup);
   2353 
   2354        // Open the full protections panel.
   2355        this.showProtectionsPopup({
   2356          event,
   2357          openingReason: "toastButtonClicked",
   2358        });
   2359        break;
   2360    }
   2361  },
   2362 
   2363  // We handle focus here when the panel is shown.
   2364  handleEvent(event) {
   2365    switch (event.type) {
   2366      case "command":
   2367        this.onCommand(event);
   2368        break;
   2369      case "focus": {
   2370        let elem = document.activeElement;
   2371        let position = elem.compareDocumentPosition(this._protectionsPopup);
   2372 
   2373        if (
   2374          !(
   2375            position &
   2376            (Node.DOCUMENT_POSITION_CONTAINS |
   2377              Node.DOCUMENT_POSITION_CONTAINED_BY)
   2378          ) &&
   2379          !this._protectionsPopup.hasAttribute("noautohide")
   2380        ) {
   2381          // Hide the panel when focusing an element that is
   2382          // neither an ancestor nor descendant unless the panel has
   2383          // @noautohide (e.g. for a tour).
   2384          PanelMultiView.hidePopup(this._protectionsPopup);
   2385        }
   2386        break;
   2387      }
   2388      case "popupshown":
   2389        this.onPopupShown(event);
   2390        break;
   2391      case "popuphidden":
   2392        this.onPopupHidden(event);
   2393        break;
   2394      case "toggle": {
   2395        this.onTPSwitchCommand(event);
   2396        break;
   2397      }
   2398    }
   2399  },
   2400 
   2401  observe(subject, topic) {
   2402    switch (topic) {
   2403      case "browser:purge-session-history":
   2404        // We need to update the earliest recorded date if history has been
   2405        // cleared.
   2406        this._earliestRecordedDate = 0;
   2407        this.maybeUpdateEarliestRecordedDateTooltip();
   2408        break;
   2409      case "smartblock:open-protections-panel":
   2410        if (!this.smartblockEmbedsEnabledPref) {
   2411          // don't react if smartblock disabled by pref
   2412          break;
   2413        }
   2414 
   2415        if (gBrowser.selectedBrowser.browserId !== subject.browserId) {
   2416          break;
   2417        }
   2418 
   2419        // Ensure panel is fully hidden before trying to open
   2420        this._hidePopup();
   2421 
   2422        this.showProtectionsPopup({
   2423          openingReason: "embedPlaceholderButton",
   2424        });
   2425        break;
   2426    }
   2427  },
   2428 
   2429  /**
   2430   * Update the popup contents. Only called when the popup has been taken
   2431   * out of the template and is shown or about to be shown.
   2432   */
   2433  refreshProtectionsPopup() {
   2434    let host = gIdentityHandler.getHostForDisplay();
   2435    document.l10n.setAttributes(
   2436      this._protectionsPopupMainViewHeaderLabel,
   2437      "protections-header",
   2438      { host }
   2439    );
   2440 
   2441    let currentlyEnabled = !this.hasException;
   2442 
   2443    this.updateProtectionsToggle(currentlyEnabled);
   2444 
   2445    this._notBlockingWhyLink.setAttribute(
   2446      "tooltip",
   2447      currentlyEnabled
   2448        ? "protections-popup-not-blocking-why-etp-on-tooltip"
   2449        : "protections-popup-not-blocking-why-etp-off-tooltip"
   2450    );
   2451 
   2452    // Update the tooltip of the blocked tracker counter.
   2453    this.maybeUpdateEarliestRecordedDateTooltip();
   2454 
   2455    let today = Date.now();
   2456    let threeDaysMillis = 72 * 60 * 60 * 1000;
   2457    let expired = today - this.milestoneTimestampPref > threeDaysMillis;
   2458 
   2459    if (this._milestoneTextSet && !expired) {
   2460      this._protectionsPopup.setAttribute("milestone", this.milestonePref);
   2461    } else {
   2462      this._protectionsPopup.removeAttribute("milestone");
   2463    }
   2464 
   2465    cookieBannerHandling.updateSection();
   2466 
   2467    this._protectionsPopup.toggleAttribute("detected", this.anyDetected);
   2468    this._protectionsPopup.toggleAttribute("blocking", this.anyBlocking);
   2469    this._protectionsPopup.toggleAttribute("hasException", this.hasException);
   2470  },
   2471 
   2472  /**
   2473   * Updates the "pressed" state and labels for the toggle
   2474   *
   2475   * @param {boolean} isPressed - Whether or not the toggle should be pressed.
   2476   *  True if ETP is enabled for a given site.
   2477   */
   2478  updateProtectionsToggle(isPressed) {
   2479    let host = gIdentityHandler.getHostForDisplay();
   2480    let toggle = this._protectionsPopupTPSwitch;
   2481    toggle.toggleAttribute("pressed", isPressed);
   2482    toggle.toggleAttribute("disabled", !!this._TPSwitchCommanding);
   2483    document.l10n.setAttributes(
   2484      toggle,
   2485      isPressed
   2486        ? "protections-panel-etp-toggle-on"
   2487        : "protections-panel-etp-toggle-off",
   2488      { host }
   2489    );
   2490  },
   2491 
   2492  /*
   2493   * This function sorts the category items into the Blocked/Allowed/None Detected
   2494   * sections. It's called immediately in onContentBlockingEvent if the popup
   2495   * is presently open. Otherwise, the next time the popup is shown.
   2496   */
   2497  reorderCategoryItems() {
   2498    if (!this._categoryItemOrderInvalidated) {
   2499      return;
   2500    }
   2501 
   2502    delete this._categoryItemOrderInvalidated;
   2503 
   2504    // Hide all the headers to start with.
   2505    this._protectionsPopupBlockingHeader.hidden = true;
   2506    this._protectionsPopupNotBlockingHeader.hidden = true;
   2507    this._protectionsPopupNotFoundHeader.hidden = true;
   2508    this._protectionsPopupSmartblockContainer.hidden = true;
   2509 
   2510    for (let { categoryItem } of Object.values(this.blockers)) {
   2511      if (
   2512        categoryItem.classList.contains("notFound") ||
   2513        categoryItem.hasAttribute("uidisabled")
   2514      ) {
   2515        // Add the item to the bottom of the list. This will be under
   2516        // the "None Detected" section.
   2517        this._protectionsPopupCategoryList.insertAdjacentElement(
   2518          "beforeend",
   2519          categoryItem
   2520        );
   2521        categoryItem.setAttribute("disabled", true);
   2522        // We have an undetected category, show the header.
   2523        this._protectionsPopupNotFoundHeader.hidden = false;
   2524        continue;
   2525      }
   2526 
   2527      // Clear the disabled attribute in case we are moving the item out of
   2528      // "None Detected"
   2529      categoryItem.removeAttribute("disabled");
   2530 
   2531      if (categoryItem.classList.contains("blocked") && !this.hasException) {
   2532        // Add the item just above the Smartblock embeds section - this will be the
   2533        // bottom of the "Blocked" section.
   2534        categoryItem.parentNode.insertBefore(
   2535          categoryItem,
   2536          this._protectionsPopupSmartblockContainer
   2537        );
   2538        // We have a blocking category, show the header.
   2539        this._protectionsPopupBlockingHeader.hidden = false;
   2540        continue;
   2541      }
   2542 
   2543      // Add the item just above the "None Detected" section - this will be the
   2544      // bottom of the "Allowed" section.
   2545      categoryItem.parentNode.insertBefore(
   2546        categoryItem,
   2547        this._protectionsPopupNotFoundHeader
   2548      );
   2549      // We have an allowing category, show the header.
   2550      this._protectionsPopupNotBlockingHeader.hidden = false;
   2551    }
   2552 
   2553    // add toggles if required to the Smartblock embed section
   2554    let smartblockEmbedDetected = this._addSmartblockEmbedToggles();
   2555 
   2556    if (smartblockEmbedDetected) {
   2557      // We have a compatible smartblock toggle, show the smartblock
   2558      // embed section
   2559      this._protectionsPopupSmartblockContainer.hidden = false;
   2560    }
   2561  },
   2562 
   2563  /**
   2564   * Adds the toggles into the smartblock toggle container. Clears existing toggles first, then
   2565   * searches through the contentBlockingLog for smartblock-compatible content.
   2566   *
   2567   * @returns {boolean} true if a smartblock compatible resource is blocked or shimmed, false otherwise
   2568   */
   2569  _addSmartblockEmbedToggles() {
   2570    if (!this.smartblockEmbedsEnabledPref) {
   2571      // Do not insert toggles if feature is disabled.
   2572      return false;
   2573    }
   2574 
   2575    let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog();
   2576    contentBlockingLog = JSON.parse(contentBlockingLog);
   2577    let smartBlockEmbedToggleAdded = false;
   2578 
   2579    // remove all old toggles
   2580    while (this._protectionsPopupSmartblockToggleContainer.lastChild) {
   2581      this._protectionsPopupSmartblockToggleContainer.lastChild.remove();
   2582    }
   2583 
   2584    // check that there is an allowed or replaced flag present
   2585    let contentBlockingEvents =
   2586      gBrowser.selectedBrowser.getContentBlockingEvents();
   2587 
   2588    // In the future, we should add a flag specifically for smartblock embeds so that
   2589    // these checks do not trigger when a non-embed-related shim is shimming
   2590    // a smartblock compatible site, see Bug 1926461
   2591    let somethingAllowedOrReplaced =
   2592      contentBlockingEvents &
   2593        Ci.nsIWebProgressListener.STATE_ALLOWED_TRACKING_CONTENT ||
   2594      contentBlockingEvents &
   2595        Ci.nsIWebProgressListener.STATE_REPLACED_TRACKING_CONTENT;
   2596 
   2597    if (!somethingAllowedOrReplaced) {
   2598      // return early if there is no content that is allowed or replaced
   2599      return smartBlockEmbedToggleAdded;
   2600    }
   2601 
   2602    // search through content log for compatible blocked origins
   2603    for (let [origin, actions] of Object.entries(contentBlockingLog)) {
   2604      let shimAllowed = actions.some(
   2605        ([flag]) =>
   2606          (flag & Ci.nsIWebProgressListener.STATE_ALLOWED_TRACKING_CONTENT) != 0
   2607      );
   2608 
   2609      let shimDetected = actions.some(
   2610        ([flag]) =>
   2611          (flag & Ci.nsIWebProgressListener.STATE_REPLACED_TRACKING_CONTENT) !=
   2612          0
   2613      );
   2614 
   2615      if (!shimAllowed && !shimDetected) {
   2616        // origin is not being shimmed or allowed
   2617        continue;
   2618      }
   2619 
   2620      let shimInfo = this.smartblockEmbedInfo.find(element => {
   2621        let matchPatternSet = new MatchPatternSet(element.matchPatterns);
   2622        return matchPatternSet.matches(origin);
   2623      });
   2624      if (!shimInfo) {
   2625        // origin not relevant to smartblock
   2626        continue;
   2627      }
   2628 
   2629      const { shimId, displayName } = shimInfo;
   2630      smartBlockEmbedToggleAdded = true;
   2631 
   2632      // check that a toggle doesn't already exist
   2633      let existingToggle = document.getElementById(
   2634        `smartblock-${shimId.toLowerCase()}-toggle`
   2635      );
   2636      if (existingToggle) {
   2637        // make sure toggle state is allowed if ANY of the sites are allowed
   2638        if (shimAllowed) {
   2639          existingToggle.setAttribute("pressed", true);
   2640        }
   2641        // skip adding a new toggle
   2642        continue;
   2643      }
   2644 
   2645      // create the toggle element
   2646      let toggle = document.createElement("moz-toggle");
   2647      toggle.setAttribute("id", `smartblock-${shimId.toLowerCase()}-toggle`);
   2648      toggle.setAttribute("data-l10n-attrs", "label");
   2649      document.l10n.setAttributes(
   2650        toggle,
   2651        "protections-panel-smartblock-blocking-toggle",
   2652        {
   2653          trackername: displayName,
   2654        }
   2655      );
   2656 
   2657      // set toggle to correct position
   2658      toggle.toggleAttribute("pressed", !!shimAllowed);
   2659 
   2660      // add functionality to toggle
   2661      toggle.addEventListener("toggle", event => {
   2662        let newToggleState = event.target.pressed;
   2663 
   2664        if (newToggleState) {
   2665          this._sendUnblockMessageToSmartblock(shimId);
   2666        } else {
   2667          this._sendReblockMessageToSmartblock(shimId);
   2668        }
   2669 
   2670        Glean.securityUiProtectionspopup.clickSmartblockembedsToggle.record({
   2671          isBlock: !newToggleState,
   2672          openingReason: this._protectionsPopupOpeningReason,
   2673        });
   2674 
   2675        this._hasClickedSmartBlockEmbedToggle = true;
   2676      });
   2677 
   2678      this._protectionsPopupSmartblockToggleContainer.insertAdjacentElement(
   2679        "beforeend",
   2680        toggle
   2681      );
   2682    }
   2683 
   2684    return smartBlockEmbedToggleAdded;
   2685  },
   2686 
   2687  disableForCurrentPage(shouldReload = true) {
   2688    ContentBlockingAllowList.add(gBrowser.selectedBrowser);
   2689    if (shouldReload) {
   2690      this._hidePopup();
   2691      BrowserCommands.reload();
   2692    }
   2693  },
   2694 
   2695  enableForCurrentPage(shouldReload = true) {
   2696    ContentBlockingAllowList.remove(gBrowser.selectedBrowser);
   2697    if (shouldReload) {
   2698      this._hidePopup();
   2699      BrowserCommands.reload();
   2700    }
   2701  },
   2702 
   2703  async onTPSwitchCommand() {
   2704    // When the switch is clicked, we wait 500ms and then disable/enable
   2705    // protections, causing the page to refresh, and close the popup.
   2706    // We need to ensure we don't handle more clicks during the 500ms delay,
   2707    // so we keep track of state and return early if needed.
   2708    if (this._TPSwitchCommanding) {
   2709      return;
   2710    }
   2711 
   2712    this._TPSwitchCommanding = true;
   2713 
   2714    // Toggling the 'hasException' on the protections panel in order to do some
   2715    // styling after toggling the TP switch.
   2716    let newExceptionState =
   2717      this._protectionsPopup.toggleAttribute("hasException");
   2718 
   2719    this.updateProtectionsToggle(!newExceptionState);
   2720 
   2721    // Change the tooltip of the tracking protection icon.
   2722    if (newExceptionState) {
   2723      this.showDisabledTooltipForTPIcon();
   2724    } else {
   2725      this.showNoTrackerTooltipForTPIcon();
   2726    }
   2727 
   2728    // Change the state of the tracking protection icon.
   2729    this.iconBox.toggleAttribute("hasException", newExceptionState);
   2730 
   2731    // Indicating that we need to show a toast after refreshing the page.
   2732    // And caching the current URI and window ID in order to only show the mini
   2733    // panel if it's still on the same page.
   2734    this._showToastAfterRefresh = true;
   2735    this._previousURI = gBrowser.currentURI.spec;
   2736    this._previousOuterWindowID = gBrowser.selectedBrowser.outerWindowID;
   2737 
   2738    if (newExceptionState) {
   2739      this.disableForCurrentPage(false);
   2740      Glean.securityUiProtectionspopup.clickEtpToggleOff.record();
   2741    } else {
   2742      this.enableForCurrentPage(false);
   2743      Glean.securityUiProtectionspopup.clickEtpToggleOn.record();
   2744    }
   2745 
   2746    // We need to flush the TP state change immediately without waiting the
   2747    // 500ms delay if the Tab get switched out.
   2748    let targetTab = gBrowser.selectedTab;
   2749    let onTabSelectHandler;
   2750    let tabSelectPromise = new Promise(resolve => {
   2751      onTabSelectHandler = () => resolve();
   2752      gBrowser.tabContainer.addEventListener("TabSelect", onTabSelectHandler);
   2753    });
   2754    let timeoutPromise = new Promise(resolve => setTimeout(resolve, 500));
   2755 
   2756    await Promise.race([tabSelectPromise, timeoutPromise]);
   2757    gBrowser.tabContainer.removeEventListener("TabSelect", onTabSelectHandler);
   2758    PanelMultiView.hidePopup(this._protectionsPopup);
   2759    gBrowser.reloadTab(targetTab);
   2760 
   2761    delete this._TPSwitchCommanding;
   2762  },
   2763 
   2764  onCookieBannerToggleCommand() {
   2765    cookieBannerHandling.onCookieBannerToggleCommand();
   2766  },
   2767 
   2768  setTrackersBlockedCounter(trackerCount) {
   2769    if (this._earliestRecordedDate) {
   2770      document.l10n.setAttributes(
   2771        this._protectionsPopupTrackersCounterDescription,
   2772        "protections-footer-blocked-tracker-counter",
   2773        { trackerCount, date: this._earliestRecordedDate }
   2774      );
   2775    } else {
   2776      document.l10n.setAttributes(
   2777        this._protectionsPopupTrackersCounterDescription,
   2778        "protections-footer-blocked-tracker-counter-no-tooltip",
   2779        { trackerCount }
   2780      );
   2781      this._protectionsPopupTrackersCounterDescription.removeAttribute(
   2782        "tooltiptext"
   2783      );
   2784    }
   2785 
   2786    // Show the counter if the number of tracker is not zero.
   2787    this._protectionsPopupTrackersCounterBox.toggleAttribute(
   2788      "showing",
   2789      trackerCount != 0
   2790    );
   2791  },
   2792 
   2793  // Whenever one of the milestone prefs are changed, we attempt to update
   2794  // the milestone section string. This requires us to fetch the earliest
   2795  // recorded date from the Tracking DB, hence this process is async.
   2796  // When completed, we set _milestoneSetText to signal that the section
   2797  // is populated and ready to be shown - which happens next time we call
   2798  // refreshProtectionsPopup.
   2799  _milestoneTextSet: false,
   2800  async maybeSetMilestoneCounterText() {
   2801    if (!this._protectionsPopup) {
   2802      return;
   2803    }
   2804    let trackerCount = this.milestonePref;
   2805    if (
   2806      !this.milestonesEnabledPref ||
   2807      !trackerCount ||
   2808      !this.milestoneListPref.includes(trackerCount)
   2809    ) {
   2810      this._milestoneTextSet = false;
   2811      return;
   2812    }
   2813 
   2814    let date = await TrackingDBService.getEarliestRecordedDate();
   2815    document.l10n.setAttributes(
   2816      this._protectionsPopupMilestonesText,
   2817      "protections-milestone",
   2818      { date: date ?? 0, trackerCount }
   2819    );
   2820    this._milestoneTextSet = true;
   2821  },
   2822 
   2823  showDisabledTooltipForTPIcon() {
   2824    document.l10n.setAttributes(
   2825      this._trackingProtectionIconTooltipLabel,
   2826      "tracking-protection-icon-disabled"
   2827    );
   2828    document.l10n.setAttributes(
   2829      this._trackingProtectionIconContainer,
   2830      "tracking-protection-icon-disabled-container"
   2831    );
   2832  },
   2833 
   2834  showActiveTooltipForTPIcon() {
   2835    document.l10n.setAttributes(
   2836      this._trackingProtectionIconTooltipLabel,
   2837      "tracking-protection-icon-active"
   2838    );
   2839    document.l10n.setAttributes(
   2840      this._trackingProtectionIconContainer,
   2841      "tracking-protection-icon-active-container"
   2842    );
   2843  },
   2844 
   2845  showNoTrackerTooltipForTPIcon() {
   2846    document.l10n.setAttributes(
   2847      this._trackingProtectionIconTooltipLabel,
   2848      "tracking-protection-icon-no-trackers-detected"
   2849    );
   2850    document.l10n.setAttributes(
   2851      this._trackingProtectionIconContainer,
   2852      "tracking-protection-icon-no-trackers-detected-container"
   2853    );
   2854  },
   2855 
   2856  /**
   2857   * Showing the protections popup.
   2858   *
   2859   * @param {object} options
   2860   *                 The object could have two properties.
   2861   *                 event:
   2862   *                   The event triggers the protections popup to be opened.
   2863   *                 toast:
   2864   *                   A boolean to indicate if we need to open the protections
   2865   *                   popup as a toast. A toast only has a header section and
   2866   *                   will be hidden after a certain amount of time.
   2867   *                 openingReason:
   2868   *                   A string indicating why the panel was opened. Used for
   2869   *                   telemetry purposes.
   2870   */
   2871  showProtectionsPopup(options = {}) {
   2872    if (this.trustPanelEnabledPref) {
   2873      return;
   2874    }
   2875    const { event, toast, openingReason } = options;
   2876 
   2877    this._initializePopup();
   2878 
   2879    // Set opening reason variable for telemetry
   2880    this._protectionsPopupOpeningReason = openingReason;
   2881 
   2882    // Ensure we've updated category state based on the last blocking event:
   2883    if (this.hasOwnProperty("_lastEvent")) {
   2884      this.updatePanelForBlockingEvent(this._lastEvent);
   2885      delete this._lastEvent;
   2886    }
   2887 
   2888    // We need to clear the toast timer if it exists before showing the
   2889    // protections popup.
   2890    if (this._toastPanelTimer) {
   2891      clearTimeout(this._toastPanelTimer);
   2892      delete this._toastPanelTimer;
   2893    }
   2894 
   2895    this._protectionsPopup.toggleAttribute("toast", !!toast);
   2896    if (!toast) {
   2897      // Refresh strings if we want to open it as a standard protections popup.
   2898      this.refreshProtectionsPopup();
   2899    }
   2900 
   2901    if (toast) {
   2902      this._protectionsPopup.addEventListener(
   2903        "popupshown",
   2904        () => {
   2905          this._toastPanelTimer = setTimeout(() => {
   2906            PanelMultiView.hidePopup(this._protectionsPopup, true);
   2907            delete this._toastPanelTimer;
   2908          }, this._protectionsPopupToastTimeout);
   2909        },
   2910        { once: true }
   2911      );
   2912    }
   2913 
   2914    // Check the panel state of other panels. Hide them if needed.
   2915    let openPanels = Array.from(document.querySelectorAll("panel[openpanel]"));
   2916    for (let panel of openPanels) {
   2917      PanelMultiView.hidePopup(panel);
   2918    }
   2919 
   2920    // Now open the popup, anchored off the primary chrome element
   2921    PanelMultiView.openPopup(
   2922      this._protectionsPopup,
   2923      this._trackingProtectionIconContainer,
   2924      {
   2925        position: "bottomleft topleft",
   2926        triggerEvent: event,
   2927      }
   2928    ).catch(console.error);
   2929  },
   2930 
   2931  async maybeUpdateEarliestRecordedDateTooltip(trackerCount) {
   2932    // If we've already updated or the popup isn't in the DOM yet, don't bother
   2933    // doing this:
   2934    if (this._earliestRecordedDate || !this._protectionsPopup) {
   2935      return;
   2936    }
   2937 
   2938    let date = await TrackingDBService.getEarliestRecordedDate();
   2939 
   2940    // If there is no record for any blocked tracker, we don't have to do anything
   2941    // since the tracker counter won't be shown.
   2942    if (date) {
   2943      if (typeof trackerCount !== "number") {
   2944        trackerCount = await TrackingDBService.sumAllEvents();
   2945      }
   2946      document.l10n.setAttributes(
   2947        this._protectionsPopupTrackersCounterDescription,
   2948        "protections-footer-blocked-tracker-counter",
   2949        { trackerCount, date }
   2950      );
   2951      this._earliestRecordedDate = date;
   2952    }
   2953  },
   2954 
   2955  /**
   2956   * Sends a message to webcompat extension to unblock content and remove placeholders
   2957   *
   2958   * @param {string} shimId - the id of the shim blocking the content
   2959   */
   2960  _sendUnblockMessageToSmartblock(shimId) {
   2961    Services.obs.notifyObservers(
   2962      gBrowser.selectedTab,
   2963      "smartblock:unblock-embed",
   2964      shimId
   2965    );
   2966  },
   2967 
   2968  /**
   2969   * Sends a message to webcompat extension to reblock content
   2970   *
   2971   * @param {string} shimId - the id of the shim blocking the content
   2972   */
   2973  _sendReblockMessageToSmartblock(shimId) {
   2974    Services.obs.notifyObservers(
   2975      gBrowser.selectedTab,
   2976      "smartblock:reblock-embed",
   2977      shimId
   2978    );
   2979  },
   2980 
   2981  /**
   2982   * Dispatch the action defined in the message and user telemetry event.
   2983   */
   2984  _dispatchUserAction(message) {
   2985    let url;
   2986    try {
   2987      // Set platform specific path variables for SUMO articles
   2988      url = Services.urlFormatter.formatURL(message.content.cta_url);
   2989    } catch (e) {
   2990      console.error(e);
   2991      url = message.content.cta_url;
   2992    }
   2993    SpecialMessageActions.handleAction(
   2994      {
   2995        type: message.content.cta_type,
   2996        data: {
   2997          args: url,
   2998          where: message.content.cta_where || "tabshifted",
   2999        },
   3000      },
   3001      window.browser
   3002    );
   3003 
   3004    // Only send telemetry for non private browsing windows
   3005    if (!PrivateBrowsingUtils.isWindowPrivate(window)) {
   3006      Glean.securityUiProtectionspopup.clickProtectionspopupCfr.record({
   3007        value: "learn_more_link",
   3008        message: message.id,
   3009      });
   3010    }
   3011  },
   3012 
   3013  /**
   3014   * Attach event listener to dispatch message defined action.
   3015   */
   3016  _attachCommandListener(element, message) {
   3017    // Add event listener for `mouseup` not to overlap with the
   3018    // `mousedown` & `click` events dispatched from PanelMultiView.sys.mjs
   3019    // https://searchfox.org/mozilla-central/rev/7531325c8660cfa61bf71725f83501028178cbb9/browser/components/customizableui/PanelMultiView.jsm#1830-1837
   3020    element.addEventListener("mouseup", () => {
   3021      this._dispatchUserAction(message);
   3022    });
   3023    element.addEventListener("keyup", e => {
   3024      if (e.key === "Enter" || e.key === " ") {
   3025        this._dispatchUserAction(message);
   3026      }
   3027    });
   3028  },
   3029 
   3030  /**
   3031   * Inserts a message into the Protections Panel. The message is visible once
   3032   * and afterwards set in a collapsed state. It can be shown again using the
   3033   * info button in the panel header.
   3034   */
   3035  _insertProtectionsPanelInfoMessage(event) {
   3036    // const PROTECTIONS_PANEL_INFOMSG_PREF =
   3037    //   "browser.protections_panel.infoMessage.seen";
   3038    const message = {
   3039      id: "PROTECTIONS_PANEL_1",
   3040      content: {
   3041        title: { string_id: "cfr-protections-panel-header" },
   3042        body: { string_id: "cfr-protections-panel-body" },
   3043        link_text: { string_id: "cfr-protections-panel-link-text" },
   3044        cta_url: `${Services.urlFormatter.formatURLPref(
   3045          "app.support.baseURL"
   3046        )}etp-promotions?as=u&utm_source=inproduct`,
   3047        cta_type: "OPEN_URL",
   3048      },
   3049    };
   3050 
   3051    const doc = event.target.ownerDocument;
   3052    const container = doc.getElementById("info-message-container");
   3053    const infoButton = doc.getElementById("protections-popup-info-button");
   3054    const panelContainer = doc.getElementById("protections-popup");
   3055    const toggleMessage = () => {
   3056      const learnMoreLink = doc.querySelector(
   3057        "#info-message-container .text-link"
   3058      );
   3059      if (learnMoreLink) {
   3060        container.toggleAttribute("disabled");
   3061        infoButton.toggleAttribute("checked");
   3062        panelContainer.toggleAttribute("infoMessageShowing");
   3063        learnMoreLink.disabled = !learnMoreLink.disabled;
   3064      }
   3065      // If the message panel is opened, send impression telemetry
   3066      // if we are in a non private browsing window.
   3067      if (
   3068        panelContainer.hasAttribute("infoMessageShowing") &&
   3069        !PrivateBrowsingUtils.isWindowPrivate(window)
   3070      ) {
   3071        Glean.securityUiProtectionspopup.openProtectionspopupCfr.record({
   3072          value: "impression",
   3073          message: message.id,
   3074        });
   3075      }
   3076    };
   3077    if (!container.childElementCount) {
   3078      const messageEl = this._createHeroElement(doc, message);
   3079      container.appendChild(messageEl);
   3080      infoButton.addEventListener("click", toggleMessage);
   3081    }
   3082    // Message is collapsed by default. If it was never shown before we want
   3083    // to expand it
   3084    if (
   3085      !this.protectionsPanelMessageSeen &&
   3086      container.hasAttribute("disabled")
   3087    ) {
   3088      toggleMessage(message);
   3089    }
   3090    // Save state that we displayed the message
   3091    if (!this.protectionsPanelMessageSeen) {
   3092      Services.prefs.setBoolPref(
   3093        "browser.protections_panel.infoMessage.seen",
   3094        true
   3095      );
   3096    }
   3097    // Collapse the message after the panel is hidden so we don't get the
   3098    // animation when opening the panel
   3099    panelContainer.addEventListener(
   3100      "popuphidden",
   3101      () => {
   3102        if (
   3103          this.protectionsPanelMessageSeen &&
   3104          !container.hasAttribute("disabled")
   3105        ) {
   3106          toggleMessage(message);
   3107        }
   3108      },
   3109      {
   3110        once: true,
   3111      }
   3112    );
   3113  },
   3114 
   3115  _createElement(doc, elem, options = {}) {
   3116    const node = doc.createElementNS("http://www.w3.org/1999/xhtml", elem);
   3117    if (options.classList) {
   3118      node.classList.add(options.classList);
   3119    }
   3120    if (options.content) {
   3121      doc.l10n.setAttributes(node, options.content.string_id);
   3122    }
   3123    return node;
   3124  },
   3125 
   3126  _createHeroElement(doc, message) {
   3127    const messageEl = this._createElement(doc, "div");
   3128    messageEl.setAttribute("id", "protections-popup-message");
   3129    messageEl.classList.add("protections-hero-message");
   3130    const wrapperEl = this._createElement(doc, "div");
   3131    wrapperEl.classList.add("protections-popup-message-body");
   3132    messageEl.appendChild(wrapperEl);
   3133 
   3134    wrapperEl.appendChild(
   3135      this._createElement(doc, "h2", {
   3136        classList: "protections-popup-message-title",
   3137        content: message.content.title,
   3138      })
   3139    );
   3140 
   3141    wrapperEl.appendChild(
   3142      this._createElement(doc, "p", { content: message.content.body })
   3143    );
   3144 
   3145    if (message.content.link_text) {
   3146      let linkEl = this._createElement(doc, "a", {
   3147        classList: "text-link",
   3148        content: message.content.link_text,
   3149      });
   3150 
   3151      linkEl.disabled = true;
   3152      wrapperEl.appendChild(linkEl);
   3153      this._attachCommandListener(linkEl, message);
   3154    } else {
   3155      this._attachCommandListener(wrapperEl, message);
   3156    }
   3157 
   3158    return messageEl;
   3159  },
   3160 
   3161  _resetToggleSecDelay() {
   3162    // Note: `this` is bound to gProtectionsHandler in init.
   3163    clearTimeout(this._protectionsPopupToggleDelayTimer);
   3164    this._protectionsPopupToggleDelayTimer = setTimeout(() => {
   3165      this._enablePopupToggles();
   3166      delete this._protectionsPopupToggleDelayTimer;
   3167    }, this._protectionsPopupButtonDelay);
   3168  },
   3169 
   3170  _disablePopupToggles() {
   3171    // Disables all toggles in the protections panel
   3172    this._protectionsPopup.querySelectorAll("moz-toggle").forEach(toggle => {
   3173      toggle.setAttribute("disabled", true);
   3174      toggle.addEventListener("pointerdown", this._resetToggleSecDelay);
   3175    });
   3176  },
   3177 
   3178  _enablePopupToggles() {
   3179    // Enables all toggles in the protections panel
   3180    this._protectionsPopup.querySelectorAll("moz-toggle").forEach(toggle => {
   3181      toggle.removeAttribute("disabled");
   3182      toggle.removeEventListener("pointerdown", this._resetToggleSecDelay);
   3183    });
   3184  },
   3185 };