summaryrefslogtreecommitdiffstats
path: root/browser/base/content/browser-siteProtections.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/base/content/browser-siteProtections.js2644
1 files changed, 2644 insertions, 0 deletions
diff --git a/browser/base/content/browser-siteProtections.js b/browser/base/content/browser-siteProtections.js
new file mode 100644
index 0000000000..7b3b0a12a7
--- /dev/null
+++ b/browser/base/content/browser-siteProtections.js
@@ -0,0 +1,2644 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env mozilla/browser-window */
+
+ChromeUtils.defineESModuleGetters(this, {
+ ContentBlockingAllowList:
+ "resource://gre/modules/ContentBlockingAllowList.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ ToolbarPanelHub: "resource://activity-stream/lib/ToolbarPanelHub.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "TrackingDBService",
+ "@mozilla.org/tracking-db-service;1",
+ "nsITrackingDBService"
+);
+
+/**
+ * Represents a protection category shown in the protections UI. For the most
+ * common categories we can directly instantiate this category. Some protections
+ * categories inherit from this class and overwrite some of its members.
+ */
+class ProtectionCategory {
+ /**
+ * Creates a protection category.
+ * @param {string} id - Identifier of the category. Used to query the category
+ * UI elements in the DOM.
+ * @param {Object} options - Category options.
+ * @param {string} options.prefEnabled - ID of pref which controls the
+ * category enabled state.
+ * @param {string} [options.reportBreakageLabel] - Telemetry label to use when
+ * users report TP breakage. Defaults to protection ID.
+ * @param {string} [options.l10nId] - Identifier l10n strings are keyed under
+ * for this category. Defaults to protection ID.
+ * @param {Object} flags - Flags for this category to look for in the content
+ * blocking event and content blocking log.
+ * @param {Number} [flags.load] - Load flag for this protection category. If
+ * omitted, we will never match a isAllowing check for this category.
+ * @param {Number} [flags.block] - Block flag for this protection category. If
+ * omitted, we will never match a isBlocking check for this category.
+ * @param {Number} [flags.shim] - Shim flag for this protection category. This
+ * flag is set if we replaced tracking content with a non-tracking shim
+ * script.
+ * @param {Number} [flags.allow] - Allow flag for this protection category.
+ * This flag is set if we explicitly allow normally blocked tracking content.
+ * The webcompat extension can do this if it needs to unblock content on user
+ * opt-in.
+ */
+ constructor(
+ id,
+ { prefEnabled, reportBreakageLabel, l10nId },
+ {
+ load,
+ block,
+ shim = Ci.nsIWebProgressListener.STATE_REPLACED_TRACKING_CONTENT,
+ allow = Ci.nsIWebProgressListener.STATE_ALLOWED_TRACKING_CONTENT,
+ }
+ ) {
+ this._id = id;
+ this.prefEnabled = prefEnabled;
+ this._reportBreakageLabel = reportBreakageLabel || id;
+
+ this._flags = { load, block, shim, allow };
+
+ if (
+ Services.prefs.getPrefType(this.prefEnabled) == Services.prefs.PREF_BOOL
+ ) {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_enabled",
+ this.prefEnabled,
+ false,
+ this.updateCategoryItem.bind(this)
+ );
+ }
+
+ l10nId = l10nId || id;
+ this.strings = {};
+ XPCOMUtils.defineLazyGetter(this.strings, "subViewBlocked", () =>
+ gNavigatorBundle.getString(`contentBlocking.${l10nId}View.blocked.label`)
+ );
+ XPCOMUtils.defineLazyGetter(this.strings, "subViewTitleBlocking", () =>
+ gNavigatorBundle.getString(`protections.blocking.${l10nId}.title`)
+ );
+ XPCOMUtils.defineLazyGetter(this.strings, "subViewTitleNotBlocking", () =>
+ gNavigatorBundle.getString(`protections.notBlocking.${l10nId}.title`)
+ );
+
+ XPCOMUtils.defineLazyGetter(this, "subView", () =>
+ document.getElementById(`protections-popup-${this._id}View`)
+ );
+
+ XPCOMUtils.defineLazyGetter(this, "subViewHeading", () =>
+ document.getElementById(`protections-popup-${this._id}View-heading`)
+ );
+
+ XPCOMUtils.defineLazyGetter(this, "subViewList", () =>
+ document.getElementById(`protections-popup-${this._id}View-list`)
+ );
+
+ XPCOMUtils.defineLazyGetter(this, "subViewShimAllowHint", () =>
+ document.getElementById(
+ `protections-popup-${this._id}View-shim-allow-hint`
+ )
+ );
+ }
+
+ // Child classes may override these to do init / teardown. We expect them to
+ // be called when the protections panel is initialized or destroyed.
+ init() {}
+ uninit() {}
+
+ // Some child classes may overide this getter.
+ get enabled() {
+ return this._enabled;
+ }
+
+ get reportBreakageLabel() {
+ return this._reportBreakageLabel;
+ }
+
+ /**
+ * Get the category item associated with this protection from the main
+ * protections panel.
+ * @returns {xul:toolbarbutton|undefined} - Item or undefined if the panel is
+ * not yet initialized.
+ */
+ get categoryItem() {
+ // We don't use defineLazyGetter for the category item, since it may be null
+ // on first access.
+ return (
+ this._categoryItem ||
+ (this._categoryItem = document.getElementById(
+ `protections-popup-category-${this._id}`
+ ))
+ );
+ }
+
+ /**
+ * Defaults to enabled state. May be overridden by child classes.
+ * @returns {boolean} - Whether the protection is set to block trackers.
+ */
+ get blockingEnabled() {
+ return this.enabled;
+ }
+
+ /**
+ * Update the category item state in the main view of the protections panel.
+ * Determines whether the category is set to block trackers.
+ * @returns {boolean} - true if the state has been updated, false if the
+ * protections popup has not been initialized yet.
+ */
+ updateCategoryItem() {
+ // Can't get `this.categoryItem` without the popup. Using the popup instead
+ // of `this.categoryItem` to guard access, because the category item getter
+ // can trigger bug 1543537. If there's no popup, we'll be called again the
+ // first time the popup shows.
+ if (!gProtectionsHandler._protectionsPopup) {
+ return false;
+ }
+ this.categoryItem.classList.toggle("blocked", this.enabled);
+ this.categoryItem.classList.toggle("subviewbutton-nav", this.enabled);
+ return true;
+ }
+
+ /**
+ * Update the category sub view that is shown when users click on the category
+ * button.
+ */
+ async updateSubView() {
+ let { items, anyShimAllowed } = await this._generateSubViewListItems();
+ this.subViewShimAllowHint.hidden = !anyShimAllowed;
+
+ this.subViewList.textContent = "";
+ this.subViewList.append(items);
+ this.subView.setAttribute(
+ "title",
+ this.blockingEnabled && !gProtectionsHandler.hasException
+ ? this.strings.subViewTitleBlocking
+ : this.strings.subViewTitleNotBlocking
+ );
+ }
+
+ /**
+ * Create a list of items, each representing a tracker.
+ * @returns {Object} result - An object containing the results.
+ * @returns {HTMLDivElement[]} result.items - Generated tracker items. May be
+ * empty.
+ * @returns {boolean} result.anyShimAllowed - Flag indicating if any of the
+ * items have been unblocked by a shim script.
+ */
+ async _generateSubViewListItems() {
+ let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog();
+ contentBlockingLog = JSON.parse(contentBlockingLog);
+ let anyShimAllowed = false;
+
+ let fragment = document.createDocumentFragment();
+ for (let [origin, actions] of Object.entries(contentBlockingLog)) {
+ let { item, shimAllowed } = await this._createListItem(origin, actions);
+ if (!item) {
+ continue;
+ }
+ anyShimAllowed = anyShimAllowed || shimAllowed;
+ fragment.appendChild(item);
+ }
+
+ return {
+ items: fragment,
+ anyShimAllowed,
+ };
+ }
+
+ /**
+ * Create a DOM item representing a tracker.
+ * @param {string} origin - Origin of the tracker.
+ * @param {Array} actions - Array of actions from the content blocking log
+ * associated with the tracking origin.
+ * @returns {Object} result - An object containing the results.
+ * @returns {HTMLDListElement} [options.item] - Generated item or null if we
+ * don't have an item for this origin based on the actions log.
+ * @returns {boolean} options.shimAllowed - Flag indicating whether the
+ * tracking origin was allowed by a shim script.
+ */
+ _createListItem(origin, actions) {
+ let isAllowed = actions.some(
+ ([state]) => this.isAllowing(state) && !this.isShimming(state)
+ );
+ let isDetected =
+ isAllowed || actions.some(([state]) => this.isBlocking(state));
+
+ if (!isDetected) {
+ return {};
+ }
+
+ // Create an item to hold the origin label and shim allow indicator. Using
+ // an html element here, so we can use CSS flex, which handles the label
+ // overflow in combination with the icon correctly.
+ let listItem = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "div"
+ );
+ listItem.className = "protections-popup-list-item";
+ listItem.classList.toggle("allowed", isAllowed);
+
+ let label = document.createXULElement("label");
+ // Repeat the host in the tooltip in case it's too long
+ // and overflows in our panel.
+ label.tooltipText = origin;
+ label.value = origin;
+ label.className = "protections-popup-list-host-label";
+ label.setAttribute("crop", "end");
+ listItem.append(label);
+
+ // Determine whether we should show a shim-allow indicator for this item.
+ let shimAllowed = actions.some(([flag]) => flag == this._flags.allow);
+ if (shimAllowed) {
+ listItem.append(this._getShimAllowIndicator());
+ }
+
+ return { item: listItem, shimAllowed };
+ }
+
+ /**
+ * Create an indicator icon for marking origins that have been allowed by a
+ * shim script.
+ * @returns {HTMLImageElement} - Created element.
+ */
+ _getShimAllowIndicator() {
+ let allowIndicator = document.createXULElement("image");
+ document.l10n.setAttributes(
+ allowIndicator,
+ "protections-panel-shim-allowed-indicator"
+ );
+ allowIndicator.tooltipText = this.strings.subViewShimAllowedTooltip;
+ allowIndicator.classList.add(
+ "protections-popup-list-host-shim-allow-indicator"
+ );
+ return allowIndicator;
+ }
+
+ /**
+ * @param {Number} state - Content blocking event flags.
+ * @returns {boolean} - Whether the protection has blocked a tracker.
+ */
+ isBlocking(state) {
+ return (state & this._flags.block) != 0;
+ }
+
+ /**
+ * @param {Number} state - Content blocking event flags.
+ * @returns {boolean} - Whether the protection has allowed a tracker.
+ */
+ isAllowing(state) {
+ return (state & this._flags.load) != 0;
+ }
+
+ /**
+ * @param {Number} state - Content blocking event flags.
+ * @returns {boolean} - Whether the protection has detected (blocked or
+ * allowed) a tracker.
+ */
+ isDetected(state) {
+ return this.isBlocking(state) || this.isAllowing(state);
+ }
+
+ /**
+ * @param {Number} state - Content blocking event flags.
+ * @returns {boolean} - Whether the protections has allowed a tracker that
+ * would have normally been blocked.
+ */
+ isShimming(state) {
+ return (state & this._flags.shim) != 0 && this.isAllowing(state);
+ }
+}
+
+let Fingerprinting = new ProtectionCategory(
+ "fingerprinters",
+ {
+ prefEnabled: "privacy.trackingprotection.fingerprinting.enabled",
+ reportBreakageLabel: "fingerprinting",
+ },
+ {
+ load: Ci.nsIWebProgressListener.STATE_LOADED_FINGERPRINTING_CONTENT,
+ block: Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT,
+ shim: Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT,
+ allow: Ci.nsIWebProgressListener.STATE_ALLOWED_FINGERPRINTING_CONTENT,
+ }
+);
+
+let Cryptomining = new ProtectionCategory(
+ "cryptominers",
+ {
+ prefEnabled: "privacy.trackingprotection.cryptomining.enabled",
+ reportBreakageLabel: "cryptomining",
+ },
+ {
+ load: Ci.nsIWebProgressListener.STATE_LOADED_CRYPTOMINING_CONTENT,
+ block: Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT,
+ }
+);
+
+let TrackingProtection =
+ new (class TrackingProtection extends ProtectionCategory {
+ constructor() {
+ super(
+ "trackers",
+ {
+ l10nId: "trackingContent",
+ prefEnabled: "privacy.trackingprotection.enabled",
+ reportBreakageLabel: "trackingprotection",
+ },
+ {
+ load: null,
+ block:
+ Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT |
+ Ci.nsIWebProgressListener.STATE_BLOCKED_EMAILTRACKING_CONTENT,
+ }
+ );
+
+ // Blocked label has custom key, overwrite the getter.
+ XPCOMUtils.defineLazyGetter(this.strings, "subViewBlocked", () =>
+ gNavigatorBundle.getString("contentBlocking.trackersView.blocked.label")
+ );
+
+ this.prefEnabledInPrivateWindows =
+ "privacy.trackingprotection.pbmode.enabled";
+ this.prefTrackingTable = "urlclassifier.trackingTable";
+ this.prefTrackingAnnotationTable =
+ "urlclassifier.trackingAnnotationTable";
+ this.prefAnnotationsLevel2Enabled =
+ "privacy.annotate_channels.strict_list.enabled";
+ this.prefEmailTrackingProtectionEnabled =
+ "privacy.trackingprotection.emailtracking.enabled";
+ this.prefEmailTrackingProtectionEnabledInPrivateWindows =
+ "privacy.trackingprotection.emailtracking.pbmode.enabled";
+
+ this.enabledGlobally = false;
+ this.emailTrackingProtectionEnabledGlobally = false;
+
+ this.enabledInPrivateWindows = false;
+ this.emailTrackingProtectionEnabledInPrivateWindows = false;
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "trackingTable",
+ this.prefTrackingTable,
+ ""
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "trackingAnnotationTable",
+ this.prefTrackingAnnotationTable,
+ ""
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "annotationsLevel2Enabled",
+ this.prefAnnotationsLevel2Enabled,
+ false
+ );
+ }
+
+ init() {
+ this.updateEnabled();
+
+ Services.prefs.addObserver(this.prefEnabled, this);
+ Services.prefs.addObserver(this.prefEnabledInPrivateWindows, this);
+ Services.prefs.addObserver(this.prefEmailTrackingProtectionEnabled, this);
+ Services.prefs.addObserver(
+ this.prefEmailTrackingProtectionEnabledInPrivateWindows,
+ this
+ );
+ }
+
+ uninit() {
+ Services.prefs.removeObserver(this.prefEnabled, this);
+ Services.prefs.removeObserver(this.prefEnabledInPrivateWindows, this);
+ Services.prefs.removeObserver(
+ this.prefEmailTrackingProtectionEnabled,
+ this
+ );
+ Services.prefs.removeObserver(
+ this.prefEmailTrackingProtectionEnabledInPrivateWindows,
+ this
+ );
+ }
+
+ observe() {
+ this.updateEnabled();
+ this.updateCategoryItem();
+ }
+
+ get trackingProtectionLevel2Enabled() {
+ const CONTENT_TABLE = "content-track-digest256";
+ return this.trackingTable.includes(CONTENT_TABLE);
+ }
+
+ get enabled() {
+ return (
+ this.enabledGlobally ||
+ this.emailTrackingProtectionEnabledGlobally ||
+ (PrivateBrowsingUtils.isWindowPrivate(window) &&
+ (this.enabledInPrivateWindows ||
+ this.emailTrackingProtectionEnabledInPrivateWindows))
+ );
+ }
+
+ updateEnabled() {
+ this.enabledGlobally = Services.prefs.getBoolPref(this.prefEnabled);
+ this.enabledInPrivateWindows = Services.prefs.getBoolPref(
+ this.prefEnabledInPrivateWindows
+ );
+ this.emailTrackingProtectionEnabledGlobally = Services.prefs.getBoolPref(
+ this.prefEmailTrackingProtectionEnabled
+ );
+ this.emailTrackingProtectionEnabledInPrivateWindows =
+ Services.prefs.getBoolPref(
+ this.prefEmailTrackingProtectionEnabledInPrivateWindows
+ );
+ }
+
+ isAllowingLevel1(state) {
+ return (
+ (state &
+ Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT) !=
+ 0
+ );
+ }
+
+ isAllowingLevel2(state) {
+ return (
+ (state &
+ Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_2_TRACKING_CONTENT) !=
+ 0
+ );
+ }
+
+ isAllowing(state) {
+ return this.isAllowingLevel1(state) || this.isAllowingLevel2(state);
+ }
+
+ async updateSubView() {
+ let previousURI = gBrowser.currentURI.spec;
+ let previousWindow = gBrowser.selectedBrowser.innerWindowID;
+
+ let { items, anyShimAllowed } = await this._generateSubViewListItems();
+
+ // If we don't have trackers we would usually not show the menu item
+ // allowing the user to show the sub-panel. However, in the edge case
+ // that we annotated trackers on the page using the strict list but did
+ // not detect trackers on the page using the basic list, we currently
+ // still show the panel. To reduce the confusion, tell the user that we have
+ // not detected any tracker.
+ if (!items.childNodes.length) {
+ let emptyImage = document.createXULElement("image");
+ emptyImage.classList.add("protections-popup-trackersView-empty-image");
+ emptyImage.classList.add("trackers-icon");
+
+ let emptyLabel = document.createXULElement("label");
+ emptyLabel.classList.add("protections-popup-empty-label");
+ emptyLabel.textContent = gNavigatorBundle.getString(
+ "contentBlocking.trackersView.empty.label"
+ );
+
+ items.appendChild(emptyImage);
+ items.appendChild(emptyLabel);
+
+ this.subViewList.classList.add("empty");
+ } else {
+ this.subViewList.classList.remove("empty");
+ }
+
+ // This might have taken a while. Only update the list if we're still on the same page.
+ if (
+ previousURI == gBrowser.currentURI.spec &&
+ previousWindow == gBrowser.selectedBrowser.innerWindowID
+ ) {
+ this.subViewShimAllowHint.hidden = !anyShimAllowed;
+
+ this.subViewList.textContent = "";
+ this.subViewList.append(items);
+ this.subView.setAttribute(
+ "title",
+ this.enabled && !gProtectionsHandler.hasException
+ ? this.strings.subViewTitleBlocking
+ : this.strings.subViewTitleNotBlocking
+ );
+ }
+ }
+
+ async _createListItem(origin, actions) {
+ // Figure out if this list entry was actually detected by TP or something else.
+ let isAllowed = actions.some(
+ ([state]) => this.isAllowing(state) && !this.isShimming(state)
+ );
+ let isDetected =
+ isAllowed || actions.some(([state]) => this.isBlocking(state));
+
+ if (!isDetected) {
+ return {};
+ }
+
+ // Because we might use different lists for annotation vs. blocking, we
+ // need to make sure that this is a tracker that we would actually have blocked
+ // before showing it to the user.
+ if (
+ this.annotationsLevel2Enabled &&
+ !this.trackingProtectionLevel2Enabled &&
+ actions.some(
+ ([state]) =>
+ (state &
+ Ci.nsIWebProgressListener
+ .STATE_LOADED_LEVEL_2_TRACKING_CONTENT) !=
+ 0
+ )
+ ) {
+ return {};
+ }
+
+ let listItem = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "div"
+ );
+ listItem.className = "protections-popup-list-item";
+ listItem.classList.toggle("allowed", isAllowed);
+
+ let label = document.createXULElement("label");
+ // Repeat the host in the tooltip in case it's too long
+ // and overflows in our panel.
+ label.tooltipText = origin;
+ label.value = origin;
+ label.className = "protections-popup-list-host-label";
+ label.setAttribute("crop", "end");
+ listItem.append(label);
+
+ let shimAllowed = actions.some(([flag]) => flag == this._flags.allow);
+ if (shimAllowed) {
+ listItem.append(this._getShimAllowIndicator());
+ }
+
+ return { item: listItem, shimAllowed };
+ }
+ })();
+
+let ThirdPartyCookies =
+ new (class ThirdPartyCookies extends ProtectionCategory {
+ constructor() {
+ super(
+ "cookies",
+ {
+ // This would normally expect a boolean pref. However, this category
+ // overwrites the enabled getter for custom handling of cookie behavior
+ // states.
+ prefEnabled: "network.cookie.cookieBehavior",
+ },
+ {
+ // ThirdPartyCookies implements custom flag processing.
+ allow: null,
+ shim: null,
+ load: null,
+ block: null,
+ }
+ );
+
+ XPCOMUtils.defineLazyGetter(this, "categoryLabel", () =>
+ document.getElementById("protections-popup-cookies-category-label")
+ );
+
+ // Not blocking title has custom key, overwrite the getter.
+ XPCOMUtils.defineLazyGetter(this.strings, "subViewTitleNotBlocking", () =>
+ gNavigatorBundle.getString(
+ "protections.notBlocking.crossSiteTrackingCookies.title"
+ )
+ );
+
+ // Cookie permission state label.
+ XPCOMUtils.defineLazyGetter(this.strings, "subViewAllowed", () =>
+ gNavigatorBundle.getString("contentBlocking.cookiesView.allowed.label")
+ );
+
+ this.prefEnabledValues = [
+ // These values match the ones exposed under the Content Blocking section
+ // of the Preferences UI.
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, // Block all third-party cookies
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, // Block third-party cookies from trackers
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, // Block trackers and patition third-party trackers
+ Ci.nsICookieService.BEHAVIOR_REJECT, // Block all cookies
+ ];
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "behaviorPref",
+ this.prefEnabled,
+ Ci.nsICookieService.BEHAVIOR_ACCEPT,
+ this.updateCategoryItem.bind(this)
+ );
+ }
+
+ get reportBreakageLabel() {
+ switch (this.behaviorPref) {
+ case Ci.nsICookieService.BEHAVIOR_ACCEPT:
+ return "nocookiesblocked";
+ case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
+ return "allthirdpartycookiesblocked";
+ case Ci.nsICookieService.BEHAVIOR_REJECT:
+ return "allcookiesblocked";
+ case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN:
+ return "cookiesfromunvisitedsitesblocked";
+ default:
+ console.error(
+ `Error: Unknown cookieBehavior pref observed: ${this.behaviorPref}`
+ );
+ // fall through
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER:
+ return "cookierestrictions";
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN:
+ return "cookierestrictionsforeignpartitioned";
+ }
+ }
+
+ isBlocking(state) {
+ return (
+ (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER) !=
+ 0 ||
+ (state &
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER) !=
+ 0 ||
+ (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL) != 0 ||
+ (state &
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION) !=
+ 0 ||
+ (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN) != 0
+ );
+ }
+
+ isDetected(state) {
+ if (this.isBlocking(state)) {
+ return true;
+ }
+
+ if (
+ [
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ Ci.nsICookieService.BEHAVIOR_ACCEPT,
+ ].includes(this.behaviorPref)
+ ) {
+ return (
+ (state & Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_TRACKER) !=
+ 0 ||
+ (SocialTracking.enabled &&
+ (state &
+ Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER) !=
+ 0)
+ );
+ }
+
+ // We don't have specific flags for the other cookie behaviors so just
+ // fall back to STATE_COOKIES_LOADED.
+ return (state & Ci.nsIWebProgressListener.STATE_COOKIES_LOADED) != 0;
+ }
+
+ updateCategoryItem() {
+ if (!super.updateCategoryItem()) {
+ return;
+ }
+ let label;
+
+ if (!this.enabled) {
+ label = "contentBlocking.cookies.blockingTrackers3.label";
+ } else {
+ switch (this.behaviorPref) {
+ case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
+ label = "contentBlocking.cookies.blocking3rdParty2.label";
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT:
+ label = "contentBlocking.cookies.blockingAll2.label";
+ break;
+ case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN:
+ label = "contentBlocking.cookies.blockingUnvisited2.label";
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER:
+ case Ci.nsICookieService
+ .BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN:
+ label = "contentBlocking.cookies.blockingTrackers3.label";
+ break;
+ default:
+ console.error(
+ `Error: Unknown cookieBehavior pref observed: ${this.behaviorPref}`
+ );
+ break;
+ }
+ }
+ this.categoryLabel.textContent = label
+ ? gNavigatorBundle.getString(label)
+ : "";
+ }
+
+ get enabled() {
+ return this.prefEnabledValues.includes(this.behaviorPref);
+ }
+
+ updateSubView() {
+ let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog();
+ contentBlockingLog = JSON.parse(contentBlockingLog);
+
+ let categories = this._processContentBlockingLog(contentBlockingLog);
+
+ this.subViewList.textContent = "";
+
+ let categoryNames = ["trackers"];
+ switch (this.behaviorPref) {
+ case Ci.nsICookieService.BEHAVIOR_REJECT:
+ categoryNames.push("firstParty");
+ // eslint-disable-next-line no-fallthrough
+ case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
+ categoryNames.push("thirdParty");
+ }
+
+ for (let category of categoryNames) {
+ let itemsToShow = categories[category];
+
+ if (!itemsToShow.length) {
+ continue;
+ }
+
+ let box = document.createXULElement("vbox");
+ box.className = "protections-popup-cookiesView-list-section";
+ let label = document.createXULElement("label");
+ label.className = "protections-popup-cookiesView-list-header";
+ label.textContent = gNavigatorBundle.getString(
+ `contentBlocking.cookiesView.${
+ category == "trackers" ? "trackers2" : category
+ }.label`
+ );
+ box.appendChild(label);
+
+ for (let info of itemsToShow) {
+ box.appendChild(this._createListItem(info));
+ }
+
+ this.subViewList.appendChild(box);
+ }
+
+ this.subViewHeading.hidden = false;
+ if (!this.enabled) {
+ this.subView.setAttribute(
+ "title",
+ this.strings.subViewTitleNotBlocking
+ );
+ return;
+ }
+
+ let title;
+ let siteException = gProtectionsHandler.hasException;
+ let titleStringPrefix = `protections.${
+ siteException ? "notBlocking" : "blocking"
+ }.cookies.`;
+ switch (this.behaviorPref) {
+ case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
+ title = titleStringPrefix + "3rdParty.title";
+ this.subViewHeading.hidden = true;
+ if (this.subViewHeading.nextSibling.nodeName == "toolbarseparator") {
+ this.subViewHeading.nextSibling.hidden = true;
+ }
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT:
+ title = titleStringPrefix + "all.title";
+ this.subViewHeading.hidden = true;
+ if (this.subViewHeading.nextSibling.nodeName == "toolbarseparator") {
+ this.subViewHeading.nextSibling.hidden = true;
+ }
+ break;
+ case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN:
+ title = "protections.blocking.cookies.unvisited.title";
+ this.subViewHeading.hidden = true;
+ if (this.subViewHeading.nextSibling.nodeName == "toolbarseparator") {
+ this.subViewHeading.nextSibling.hidden = true;
+ }
+ break;
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER:
+ case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN:
+ title = siteException
+ ? "protections.notBlocking.crossSiteTrackingCookies.title"
+ : "protections.blocking.cookies.trackers.title";
+ break;
+ default:
+ console.error(
+ `Error: Unknown cookieBehavior pref when updating subview: ${this.behaviorPref}`
+ );
+ break;
+ }
+
+ this.subView.setAttribute("title", gNavigatorBundle.getString(title));
+ }
+
+ _getExceptionState(origin) {
+ let thirdPartyStorage = Services.perms.testPermissionFromPrincipal(
+ gBrowser.contentPrincipal,
+ "3rdPartyStorage^" + origin
+ );
+
+ if (thirdPartyStorage != Services.perms.UNKNOWN_ACTION) {
+ return thirdPartyStorage;
+ }
+
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
+ // Cookie exceptions get "inherited" from parent- to sub-domain, so we need to
+ // make sure to include parent domains in the permission check for "cookie".
+ return Services.perms.testPermissionFromPrincipal(principal, "cookie");
+ }
+
+ _clearException(origin) {
+ for (let perm of Services.perms.getAllForPrincipal(
+ gBrowser.contentPrincipal
+ )) {
+ if (perm.type == "3rdPartyStorage^" + origin) {
+ Services.perms.removePermission(perm);
+ }
+ }
+
+ // OAs don't matter here, so we can just use the hostname.
+ let host = Services.io.newURI(origin).host;
+
+ // Cookie exceptions get "inherited" from parent- to sub-domain, so we need to
+ // clear any cookie permissions from parent domains as well.
+ for (let perm of Services.perms.all) {
+ if (
+ perm.type == "cookie" &&
+ Services.eTLD.hasRootDomain(host, perm.principal.host)
+ ) {
+ Services.perms.removePermission(perm);
+ }
+ }
+ }
+
+ // Transforms and filters cookie entries in the content blocking log
+ // so that we can categorize and display them in the UI.
+ _processContentBlockingLog(log) {
+ let newLog = {
+ firstParty: [],
+ trackers: [],
+ thirdParty: [],
+ };
+
+ let firstPartyDomain = null;
+ try {
+ firstPartyDomain = Services.eTLD.getBaseDomain(gBrowser.currentURI);
+ } catch (e) {
+ // There are nasty edge cases here where someone is trying to set a cookie
+ // on a public suffix or an IP address. Just categorize those as third party...
+ if (
+ e.result != Cr.NS_ERROR_HOST_IS_IP_ADDRESS &&
+ e.result != Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
+ ) {
+ throw e;
+ }
+ }
+
+ for (let [origin, actions] of Object.entries(log)) {
+ if (!origin.startsWith("http")) {
+ continue;
+ }
+
+ let info = {
+ origin,
+ isAllowed: true,
+ exceptionState: this._getExceptionState(origin),
+ };
+ let hasCookie = false;
+ let isTracker = false;
+
+ // Extract information from the states entries in the content blocking log.
+ // Each state will contain a single state flag from nsIWebProgressListener.
+ // Note that we are using the same helper functions that are applied to the
+ // bit map passed to onSecurityChange (which contains multiple states), thus
+ // not checking exact equality, just presence of bits.
+ for (let [state, blocked] of actions) {
+ if (this.isDetected(state)) {
+ hasCookie = true;
+ }
+ if (TrackingProtection.isAllowing(state)) {
+ isTracker = true;
+ }
+ // blocked tells us whether the resource was actually blocked
+ // (which it may not be in case of an exception).
+ if (this.isBlocking(state)) {
+ info.isAllowed = !blocked;
+ }
+ }
+
+ if (!hasCookie) {
+ continue;
+ }
+
+ let isFirstParty = false;
+ try {
+ let uri = Services.io.newURI(origin);
+ isFirstParty = Services.eTLD.getBaseDomain(uri) == firstPartyDomain;
+ } catch (e) {
+ if (
+ e.result != Cr.NS_ERROR_HOST_IS_IP_ADDRESS &&
+ e.result != Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
+ ) {
+ throw e;
+ }
+ }
+
+ if (isFirstParty) {
+ newLog.firstParty.push(info);
+ } else if (isTracker) {
+ newLog.trackers.push(info);
+ } else {
+ newLog.thirdParty.push(info);
+ }
+ }
+
+ return newLog;
+ }
+
+ _createListItem({ origin, isAllowed, exceptionState }) {
+ let listItem = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "div"
+ );
+ listItem.className = "protections-popup-list-item";
+ // Repeat the origin in the tooltip in case it's too long
+ // and overflows in our panel.
+ listItem.tooltipText = origin;
+
+ let label = document.createXULElement("label");
+ label.value = origin;
+ label.className = "protections-popup-list-host-label";
+ label.setAttribute("crop", "end");
+ listItem.append(label);
+
+ if (
+ (isAllowed && exceptionState == Services.perms.ALLOW_ACTION) ||
+ (!isAllowed && exceptionState == Services.perms.DENY_ACTION)
+ ) {
+ listItem.classList.add("protections-popup-list-item-with-state");
+
+ let stateLabel = document.createXULElement("label");
+ stateLabel.className = "protections-popup-list-state-label";
+ if (isAllowed) {
+ stateLabel.value = this.strings.subViewAllowed;
+ listItem.classList.toggle("allowed", true);
+ } else {
+ stateLabel.value = this.strings.subViewBlocked;
+ }
+
+ let removeException = document.createXULElement("button");
+ removeException.className = "permission-popup-permission-remove-button";
+ removeException.tooltipText = gNavigatorBundle.getFormattedString(
+ "contentBlocking.cookiesView.removeButton.tooltip",
+ [origin]
+ );
+ removeException.appendChild(stateLabel);
+
+ removeException.addEventListener(
+ "click",
+ () => {
+ this._clearException(origin);
+ removeException.remove();
+ listItem.classList.toggle("allowed", !isAllowed);
+ },
+ { once: true }
+ );
+ listItem.append(removeException);
+ }
+
+ return listItem;
+ }
+ })();
+
+let SocialTracking =
+ new (class SocialTrackingProtection extends ProtectionCategory {
+ constructor() {
+ super(
+ "socialblock",
+ {
+ l10nId: "socialMediaTrackers",
+ prefEnabled: "privacy.socialtracking.block_cookies.enabled",
+ reportBreakageLabel: "socialtracking",
+ },
+ {
+ load: Ci.nsIWebProgressListener.STATE_LOADED_SOCIALTRACKING_CONTENT,
+ block: Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT,
+ }
+ );
+
+ this.prefStpTpEnabled =
+ "privacy.trackingprotection.socialtracking.enabled";
+ this.prefSTPCookieEnabled = this.prefEnabled;
+ this.prefCookieBehavior = "network.cookie.cookieBehavior";
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "socialTrackingProtectionEnabled",
+ this.prefStpTpEnabled,
+ false,
+ this.updateCategoryItem.bind(this)
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "rejectTrackingCookies",
+ this.prefCookieBehavior,
+ null,
+ this.updateCategoryItem.bind(this),
+ val =>
+ [
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ].includes(val)
+ );
+ }
+
+ get blockingEnabled() {
+ return (
+ (this.socialTrackingProtectionEnabled || this.rejectTrackingCookies) &&
+ this.enabled
+ );
+ }
+
+ isBlockingCookies(state) {
+ return (
+ (state &
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER) !=
+ 0
+ );
+ }
+
+ isBlocking(state) {
+ return super.isBlocking(state) || this.isBlockingCookies(state);
+ }
+
+ isAllowing(state) {
+ if (this.socialTrackingProtectionEnabled) {
+ return super.isAllowing(state);
+ }
+
+ return (
+ (state &
+ Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER) !=
+ 0
+ );
+ }
+
+ updateCategoryItem() {
+ // Can't get `this.categoryItem` without the popup. Using the popup instead
+ // of `this.categoryItem` to guard access, because the category item getter
+ // can trigger bug 1543537. If there's no popup, we'll be called again the
+ // first time the popup shows.
+ if (!gProtectionsHandler._protectionsPopup) {
+ return;
+ }
+ if (this.enabled) {
+ this.categoryItem.removeAttribute("uidisabled");
+ } else {
+ this.categoryItem.setAttribute("uidisabled", true);
+ }
+ this.categoryItem.classList.toggle("blocked", this.blockingEnabled);
+ }
+ })();
+
+/**
+ * Singleton to manage the cookie banner feature section in the protections
+ * panel and the cookie banner handling subview.
+ */
+let cookieBannerHandling = new (class {
+ // Check if this is a private window. We don't expect PBM state to change
+ // during the lifetime of this window.
+ #isPrivateBrowsing = PrivateBrowsingUtils.isWindowPrivate(window);
+
+ constructor() {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_serviceModePref",
+ "cookiebanners.service.mode",
+ Ci.nsICookieBannerService.MODE_DISABLED
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_serviceModePrefPrivateBrowsing",
+ "cookiebanners.service.mode.privateBrowsing",
+ Ci.nsICookieBannerService.MODE_DISABLED
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_serviceDetectOnly",
+ "cookiebanners.service.detectOnly",
+ false
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_uiEnabled",
+ "cookiebanners.ui.desktop.enabled",
+ false
+ );
+ XPCOMUtils.defineLazyGetter(this, "_cookieBannerSection", () =>
+ document.getElementById("protections-popup-cookie-banner-section")
+ );
+ XPCOMUtils.defineLazyGetter(this, "_cookieBannerSectionSeparator", () =>
+ document.getElementById(
+ "protections-popup-cookie-banner-section-separator"
+ )
+ );
+ XPCOMUtils.defineLazyGetter(this, "_cookieBannerSwitch", () =>
+ document.getElementById("protections-popup-cookie-banner-switch")
+ );
+ XPCOMUtils.defineLazyGetter(this, "_cookieBannerSubview", () =>
+ document.getElementById("protections-popup-cookieBannerView")
+ );
+ XPCOMUtils.defineLazyGetter(this, "_cookieBannerEnableSite", () =>
+ document.getElementById("cookieBannerView-enable-site")
+ );
+ XPCOMUtils.defineLazyGetter(this, "_cookieBannerDisableSite", () =>
+ document.getElementById("cookieBannerView-disable-site")
+ );
+ }
+
+ /**
+ * Tests if the current site has a user-created exception from the default
+ * cookie banner handling mode. Currently that means the feature is disabled
+ * for the current site.
+ *
+ * Note: bug 1790688 will move this mode handling logic into the
+ * nsCookieBannerService.
+ *
+ * @returns {boolean} - true if the user has manually created an exception.
+ */
+ get #hasException() {
+ // If the CBH feature is preffed off, we can't have an exception.
+ if (!Services.cookieBanners.isEnabled) {
+ return false;
+ }
+
+ // URLs containing IP addresses are not supported by the CBH service, and
+ // will throw. In this case, users can't create an exception, so initialize
+ // `pref` to the default value returned by `getDomainPref`.
+ let pref = Ci.nsICookieBannerService.MODE_UNSET;
+ try {
+ pref = Services.cookieBanners.getDomainPref(
+ gBrowser.currentURI,
+ this.#isPrivateBrowsing
+ );
+ } catch (ex) {
+ console.error(
+ "Cookie Banner Handling error checking for per-site exceptions: ",
+ ex
+ );
+ }
+ return pref == Ci.nsICookieBannerService.MODE_DISABLED;
+ }
+
+ /**
+ * Tests if the cookie banner handling code supports the current site.
+ *
+ * See nsICookieBannerService.hasRuleForBrowsingContextTree for details.
+ *
+ * @returns {boolean} - true if the base domain is in the list of rules.
+ */
+ get isSiteSupported() {
+ return (
+ Services.cookieBanners.isEnabled &&
+ Services.cookieBanners.hasRuleForBrowsingContextTree(
+ gBrowser.selectedBrowser.browsingContext
+ )
+ );
+ }
+
+ /*
+ * @returns {string} - Base domain (eTLD + 1) used for clearing site data.
+ */
+ get #currentBaseDomain() {
+ return gBrowser.contentPrincipal.baseDomain;
+ }
+
+ /**
+ * Helper method used by both updateSection and updateSubView to map internal
+ * state to UI attribute state. We have to separately set the subview's state
+ * because the subview is not a descendant of the menu item in the DOM, and
+ * we rely on CSS to toggle UI visibility based on attribute state.
+ *
+ * @returns A string value to be set as a UI attribute value.
+ */
+ get #uiState() {
+ if (this.#hasException) {
+ return "site-disabled";
+ } else if (this.isSiteSupported) {
+ return "detected";
+ }
+ return "undetected";
+ }
+
+ updateSection() {
+ let showSection = this.#shouldShowSection();
+ let state = this.#uiState;
+
+ for (let el of [
+ this._cookieBannerSection,
+ this._cookieBannerSectionSeparator,
+ ]) {
+ el.hidden = !showSection;
+ }
+
+ this._cookieBannerSection.dataset.state = state;
+
+ // On unsupported sites, disable button styling and click behavior.
+ // Note: to be replaced with a "please support site" subview in bug 1801971.
+ if (state == "undetected") {
+ this._cookieBannerSection.setAttribute("disabled", true);
+ this._cookieBannerSwitch.classList.remove("subviewbutton-nav");
+ this._cookieBannerSwitch.setAttribute("disabled", true);
+ } else {
+ this._cookieBannerSection.removeAttribute("disabled");
+ this._cookieBannerSwitch.classList.add("subviewbutton-nav");
+ this._cookieBannerSwitch.removeAttribute("disabled");
+ }
+ }
+
+ #shouldShowSection() {
+ // Don't show UI if globally disabled by pref, or if the cookie service
+ // is in detect-only mode.
+ if (!this._uiEnabled || this._serviceDetectOnly) {
+ return false;
+ }
+
+ // Show the section if the feature is not in disabled mode, being sure to
+ // check the different prefs for regular and private windows.
+ if (this.#isPrivateBrowsing) {
+ return (
+ this._serviceModePrefPrivateBrowsing !=
+ Ci.nsICookieBannerService.MODE_DISABLED
+ );
+ }
+ return this._serviceModePref != Ci.nsICookieBannerService.MODE_DISABLED;
+ }
+
+ /*
+ * Updates the cookie banner handling subview just before it's shown.
+ */
+ updateSubView() {
+ this._cookieBannerSubview.dataset.state = this.#uiState;
+
+ let baseDomain = JSON.stringify({ host: this.#currentBaseDomain });
+ this._cookieBannerEnableSite.setAttribute("data-l10n-args", baseDomain);
+ this._cookieBannerDisableSite.setAttribute("data-l10n-args", baseDomain);
+ }
+
+ async #disableCookieBannerHandling() {
+ // We can't clear data during a private browsing session until bug 1818783
+ // is fixed. In the meantime, don't allow the cookie banner controls in a
+ // private window to clear data for regular browsing mode.
+ if (!this.#isPrivateBrowsing) {
+ await SiteDataManager.remove(this.#currentBaseDomain);
+ }
+ Services.cookieBanners.setDomainPref(
+ gBrowser.currentURI,
+ Ci.nsICookieBannerService.MODE_DISABLED,
+ this.#isPrivateBrowsing
+ );
+ }
+
+ #enableCookieBannerHandling() {
+ Services.cookieBanners.removeDomainPref(
+ gBrowser.currentURI,
+ this.#isPrivateBrowsing
+ );
+ }
+
+ async onCookieBannerToggleCommand() {
+ let hasException =
+ this._cookieBannerSection.toggleAttribute("hasException");
+ if (hasException) {
+ await this.#disableCookieBannerHandling();
+ gProtectionsHandler.recordClick("cookieb_toggle_off");
+ } else {
+ this.#enableCookieBannerHandling();
+ gProtectionsHandler.recordClick("cookieb_toggle_on");
+ }
+ gProtectionsHandler._hidePopup();
+ gBrowser.reloadTab(gBrowser.selectedTab);
+ }
+})();
+
+/**
+ * Utility object to handle manipulations of the protections indicators in the UI
+ */
+var gProtectionsHandler = {
+ PREF_REPORT_BREAKAGE_URL: "browser.contentblocking.reportBreakage.url",
+ PREF_CB_CATEGORY: "browser.contentblocking.category",
+
+ _protectionsPopup: null,
+ _initializePopup() {
+ if (!this._protectionsPopup) {
+ let wrapper = document.getElementById("template-protections-popup");
+ this._protectionsPopup = wrapper.content.firstElementChild;
+ wrapper.replaceWith(wrapper.content);
+
+ this.maybeSetMilestoneCounterText();
+
+ for (let blocker of Object.values(this.blockers)) {
+ blocker.updateCategoryItem();
+ }
+
+ let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ document.getElementById(
+ "protections-popup-sendReportView-learn-more"
+ ).href = baseURL + "blocking-breakage";
+
+ let shimAllowLearnMoreURL =
+ baseURL + "smartblock-enhanced-tracking-protection";
+
+ document
+ .querySelectorAll(".protections-popup-shim-allow-learn-more")
+ .forEach(label => {
+ label.href = shimAllowLearnMoreURL;
+ });
+ }
+ },
+
+ _hidePopup() {
+ if (this._protectionsPopup) {
+ PanelMultiView.hidePopup(this._protectionsPopup);
+ }
+ },
+
+ // smart getters
+ get iconBox() {
+ delete this.iconBox;
+ return (this.iconBox = document.getElementById(
+ "tracking-protection-icon-box"
+ ));
+ },
+ get _protectionsPopupMultiView() {
+ delete this._protectionsPopupMultiView;
+ return (this._protectionsPopupMultiView = document.getElementById(
+ "protections-popup-multiView"
+ ));
+ },
+ get _protectionsPopupMainView() {
+ delete this._protectionsPopupMainView;
+ return (this._protectionsPopupMainView = document.getElementById(
+ "protections-popup-mainView"
+ ));
+ },
+ get _protectionsPopupMainViewHeaderLabel() {
+ delete this._protectionsPopupMainViewHeaderLabel;
+ return (this._protectionsPopupMainViewHeaderLabel = document.getElementById(
+ "protections-popup-mainView-panel-header-span"
+ ));
+ },
+ get _protectionsPopupTPSwitchBreakageLink() {
+ delete this._protectionsPopupTPSwitchBreakageLink;
+ return (this._protectionsPopupTPSwitchBreakageLink =
+ document.getElementById("protections-popup-tp-switch-breakage-link"));
+ },
+ get _protectionsPopupTPSwitchBreakageFixedLink() {
+ delete this._protectionsPopupTPSwitchBreakageFixedLink;
+ return (this._protectionsPopupTPSwitchBreakageFixedLink =
+ document.getElementById(
+ "protections-popup-tp-switch-breakage-fixed-link"
+ ));
+ },
+ get _protectionsPopupTPSwitch() {
+ delete this._protectionsPopupTPSwitch;
+ return (this._protectionsPopupTPSwitch = document.getElementById(
+ "protections-popup-tp-switch"
+ ));
+ },
+ get _protectionsPopupBlockingHeader() {
+ delete this._protectionsPopupBlockingHeader;
+ return (this._protectionsPopupBlockingHeader = document.getElementById(
+ "protections-popup-blocking-section-header"
+ ));
+ },
+ get _protectionsPopupNotBlockingHeader() {
+ delete this._protectionsPopupNotBlockingHeader;
+ return (this._protectionsPopupNotBlockingHeader = document.getElementById(
+ "protections-popup-not-blocking-section-header"
+ ));
+ },
+ get _protectionsPopupNotFoundHeader() {
+ delete this._protectionsPopupNotFoundHeader;
+ return (this._protectionsPopupNotFoundHeader = document.getElementById(
+ "protections-popup-not-found-section-header"
+ ));
+ },
+ get _protectionsPopupSettingsButton() {
+ delete this._protectionsPopupSettingsButton;
+ return (this._protectionsPopupSettingsButton = document.getElementById(
+ "protections-popup-settings-button"
+ ));
+ },
+ get _protectionsPopupFooter() {
+ delete this._protectionsPopupFooter;
+ return (this._protectionsPopupFooter = document.getElementById(
+ "protections-popup-footer"
+ ));
+ },
+ get _protectionsPopupTrackersCounterBox() {
+ delete this._protectionsPopupTrackersCounterBox;
+ return (this._protectionsPopupTrackersCounterBox = document.getElementById(
+ "protections-popup-trackers-blocked-counter-box"
+ ));
+ },
+ get _protectionsPopupTrackersCounterDescription() {
+ delete this._protectionsPopupTrackersCounterDescription;
+ return (this._protectionsPopupTrackersCounterDescription =
+ document.getElementById(
+ "protections-popup-trackers-blocked-counter-description"
+ ));
+ },
+ get _protectionsPopupFooterProtectionTypeLabel() {
+ delete this._protectionsPopupFooterProtectionTypeLabel;
+ return (this._protectionsPopupFooterProtectionTypeLabel =
+ document.getElementById(
+ "protections-popup-footer-protection-type-label"
+ ));
+ },
+ get _protectionsPopupSiteNotWorkingTPSwitch() {
+ delete this._protectionsPopupSiteNotWorkingTPSwitch;
+ return (this._protectionsPopupSiteNotWorkingTPSwitch =
+ document.getElementById("protections-popup-siteNotWorking-tp-switch"));
+ },
+ get _protectionsPopupSiteNotWorkingReportError() {
+ delete this._protectionsPopupSiteNotWorkingReportError;
+ return (this._protectionsPopupSiteNotWorkingReportError =
+ document.getElementById("protections-popup-sendReportView-report-error"));
+ },
+ get _protectionsPopupSendReportURL() {
+ delete this._protectionsPopupSendReportURL;
+ return (this._protectionsPopupSendReportURL = document.getElementById(
+ "protections-popup-sendReportView-collection-url"
+ ));
+ },
+ get _protectionsPopupSendReportButton() {
+ delete this._protectionsPopupSendReportButton;
+ return (this._protectionsPopupSendReportButton = document.getElementById(
+ "protections-popup-sendReportView-submit"
+ ));
+ },
+ get _trackingProtectionIconTooltipLabel() {
+ delete this._trackingProtectionIconTooltipLabel;
+ return (this._trackingProtectionIconTooltipLabel = document.getElementById(
+ "tracking-protection-icon-tooltip-label"
+ ));
+ },
+ get _trackingProtectionIconContainer() {
+ delete this._trackingProtectionIconContainer;
+ return (this._trackingProtectionIconContainer = document.getElementById(
+ "tracking-protection-icon-container"
+ ));
+ },
+
+ get noTrackersDetectedDescription() {
+ delete this.noTrackersDetectedDescription;
+ return (this.noTrackersDetectedDescription = document.getElementById(
+ "protections-popup-no-trackers-found-description"
+ ));
+ },
+
+ get _protectionsPopupMilestonesText() {
+ delete this._protectionsPopupMilestonesText;
+ return (this._protectionsPopupMilestonesText = document.getElementById(
+ "protections-popup-milestones-text"
+ ));
+ },
+
+ get _notBlockingWhyLink() {
+ delete this._notBlockingWhyLink;
+ return (this._notBlockingWhyLink = document.getElementById(
+ "protections-popup-not-blocking-section-why"
+ ));
+ },
+
+ get _siteNotWorkingIssueListFonts() {
+ delete this._siteNotWorkingIssueListFonts;
+ return (this._siteNotWorkingIssueListFonts = document.getElementById(
+ "protections-panel-site-not-working-view-issue-list-fonts"
+ ));
+ },
+
+ strings: {
+ get activeTooltipText() {
+ delete this.activeTooltipText;
+ return (this.activeTooltipText = gNavigatorBundle.getString(
+ "trackingProtection.icon.activeTooltip2"
+ ));
+ },
+
+ get disabledTooltipText() {
+ delete this.disabledTooltipText;
+ return (this.disabledTooltipText = gNavigatorBundle.getString(
+ "trackingProtection.icon.disabledTooltip2"
+ ));
+ },
+
+ get noTrackerTooltipText() {
+ delete this.noTrackerTooltipText;
+ return (this.noTrackerTooltipText = gNavigatorBundle.getFormattedString(
+ "trackingProtection.icon.noTrackersDetectedTooltip",
+ [gBrandBundle.GetStringFromName("brandShortName")]
+ ));
+ },
+ },
+
+ // A list of blockers that will be displayed in the categories list
+ // when blockable content is detected. A blocker must be an object
+ // with at least the following two properties:
+ // - enabled: Whether the blocker is currently turned on.
+ // - isDetected(state): Given a content blocking state, whether the blocker has
+ // either allowed or blocked elements.
+ // - categoryItem: The DOM item that represents the entry in the category list.
+ //
+ // It may also contain an init() and uninit() function, which will be called
+ // on gProtectionsHandler.init() and gProtectionsHandler.uninit().
+ // The buttons in the protections panel will appear in the same order as this array.
+ blockers: {
+ SocialTracking,
+ ThirdPartyCookies,
+ TrackingProtection,
+ Fingerprinting,
+ Cryptomining,
+ },
+
+ init() {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_fontVisibilityTrackingProtection",
+ "layout.css.font-visibility.trackingprotection",
+ 3000
+ );
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_protectionsPopupToastTimeout",
+ "browser.protections_panel.toast.timeout",
+ 3000
+ );
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "milestoneListPref",
+ "browser.contentblocking.cfr-milestone.milestones",
+ "[]",
+ () => this.maybeSetMilestoneCounterText(),
+ val => JSON.parse(val)
+ );
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "milestonePref",
+ "browser.contentblocking.cfr-milestone.milestone-achieved",
+ 0,
+ () => this.maybeSetMilestoneCounterText()
+ );
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "milestoneTimestampPref",
+ "browser.contentblocking.cfr-milestone.milestone-shown-time",
+ "0",
+ null,
+ val => parseInt(val)
+ );
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "milestonesEnabledPref",
+ "browser.contentblocking.cfr-milestone.enabled",
+ false,
+ () => this.maybeSetMilestoneCounterText()
+ );
+
+ for (let blocker of Object.values(this.blockers)) {
+ if (blocker.init) {
+ blocker.init();
+ }
+ }
+
+ // Add an observer to observe that the history has been cleared.
+ Services.obs.addObserver(this, "browser:purge-session-history");
+ },
+
+ uninit() {
+ for (let blocker of Object.values(this.blockers)) {
+ if (blocker.uninit) {
+ blocker.uninit();
+ }
+ }
+
+ Services.obs.removeObserver(this, "browser:purge-session-history");
+ },
+
+ getTrackingProtectionLabel() {
+ const value = Services.prefs.getStringPref(this.PREF_CB_CATEGORY);
+
+ switch (value) {
+ case "strict":
+ return "protections-popup-footer-protection-label-strict";
+ case "custom":
+ return "protections-popup-footer-protection-label-custom";
+ case "standard":
+ /* fall through */
+ default:
+ return "protections-popup-footer-protection-label-standard";
+ }
+ },
+
+ openPreferences(origin) {
+ openPreferences("privacy-trackingprotection", { origin });
+ },
+
+ openProtections(relatedToCurrent = false) {
+ switchToTabHavingURI("about:protections", true, {
+ replaceQueryString: true,
+ relatedToCurrent,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+
+ // Don't show the milestones section anymore.
+ Services.prefs.clearUserPref(
+ "browser.contentblocking.cfr-milestone.milestone-shown-time"
+ );
+ },
+
+ async showTrackersSubview(event) {
+ await TrackingProtection.updateSubView();
+ this._protectionsPopupMultiView.showSubView(
+ "protections-popup-trackersView"
+ );
+ },
+
+ async showSocialblockerSubview(event) {
+ await SocialTracking.updateSubView();
+ this._protectionsPopupMultiView.showSubView(
+ "protections-popup-socialblockView"
+ );
+ },
+
+ async showCookiesSubview(event) {
+ await ThirdPartyCookies.updateSubView();
+ this._protectionsPopupMultiView.showSubView(
+ "protections-popup-cookiesView"
+ );
+ },
+
+ async showFingerprintersSubview(event) {
+ await Fingerprinting.updateSubView();
+ this._protectionsPopupMultiView.showSubView(
+ "protections-popup-fingerprintersView"
+ );
+ },
+
+ async showCryptominersSubview(event) {
+ await Cryptomining.updateSubView();
+ this._protectionsPopupMultiView.showSubView(
+ "protections-popup-cryptominersView"
+ );
+ },
+
+ async onCookieBannerClick(event) {
+ if (!cookieBannerHandling.isSiteSupported) {
+ return;
+ }
+ await cookieBannerHandling.updateSubView();
+ this._protectionsPopupMultiView.showSubView(
+ "protections-popup-cookieBannerView"
+ );
+ },
+
+ recordClick(object, value = null, source = "protectionspopup") {
+ Services.telemetry.recordEvent(
+ `security.ui.${source}`,
+ "click",
+ object,
+ value
+ );
+ },
+
+ shieldHistogramAdd(value) {
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ return;
+ }
+ Services.telemetry
+ .getHistogramById("TRACKING_PROTECTION_SHIELD")
+ .add(value);
+ },
+
+ cryptominersHistogramAdd(value) {
+ Services.telemetry
+ .getHistogramById("CRYPTOMINERS_BLOCKED_COUNT")
+ .add(value);
+ },
+
+ fingerprintersHistogramAdd(value) {
+ Services.telemetry
+ .getHistogramById("FINGERPRINTERS_BLOCKED_COUNT")
+ .add(value);
+ },
+
+ handleProtectionsButtonEvent(event) {
+ event.stopPropagation();
+ if (
+ (event.type == "click" && event.button != 0) ||
+ (event.type == "keypress" &&
+ event.charCode != KeyEvent.DOM_VK_SPACE &&
+ event.keyCode != KeyEvent.DOM_VK_RETURN)
+ ) {
+ return; // Left click, space or enter only
+ }
+
+ this.showProtectionsPopup({ event });
+ },
+
+ onPopupShown(event) {
+ if (event.target == this._protectionsPopup) {
+ window.ensureCustomElements("moz-button-group");
+
+ PopupNotifications.suppressWhileOpen(this._protectionsPopup);
+
+ window.addEventListener("focus", this, true);
+
+ // Insert the info message if needed. This will be shown once and then
+ // remain collapsed.
+ ToolbarPanelHub.insertProtectionPanelMessage(event);
+
+ if (!event.target.hasAttribute("toast")) {
+ Services.telemetry.recordEvent(
+ "security.ui.protectionspopup",
+ "open",
+ "protections_popup"
+ );
+ }
+ }
+ },
+
+ onPopupHidden(event) {
+ if (event.target == this._protectionsPopup) {
+ window.removeEventListener("focus", this, true);
+ }
+ },
+
+ onHeaderClicked(event) {
+ // Display the whole protections panel if the toast has been clicked.
+ if (this._protectionsPopup.hasAttribute("toast")) {
+ // Hide the toast first.
+ PanelMultiView.hidePopup(this._protectionsPopup);
+
+ // Open the full protections panel.
+ this.showProtectionsPopup({ event });
+ }
+ },
+
+ async onTrackingProtectionIconHoveredOrFocused() {
+ // We would try to pre-fetch the data whenever the shield icon is hovered or
+ // focused. We check focus event here due to the keyboard navigation.
+ if (this._updatingFooter) {
+ return;
+ }
+ this._updatingFooter = true;
+
+ // Take the popup out of its template.
+ this._initializePopup();
+
+ // Get the tracker count and set it to the counter in the footer.
+ const trackerCount = await TrackingDBService.sumAllEvents();
+ this.setTrackersBlockedCounter(trackerCount);
+
+ // Set tracking protection label
+ const l10nId = this.getTrackingProtectionLabel();
+ const elem = this._protectionsPopupFooterProtectionTypeLabel;
+ document.l10n.setAttributes(elem, l10nId);
+
+ // Try to get the earliest recorded date in case that there was no record
+ // during the initiation but new records come after that.
+ await this.maybeUpdateEarliestRecordedDateTooltip();
+
+ this._updatingFooter = false;
+ },
+
+ // This triggers from top level location changes.
+ onLocationChange() {
+ if (this._showToastAfterRefresh) {
+ this._showToastAfterRefresh = false;
+
+ // We only display the toast if we're still on the same page.
+ if (
+ this._previousURI == gBrowser.currentURI.spec &&
+ this._previousOuterWindowID == gBrowser.selectedBrowser.outerWindowID
+ ) {
+ this.showProtectionsPopup({
+ toast: true,
+ });
+ }
+ }
+
+ // Reset blocking and exception status so that we can send telemetry
+ this.hadShieldState = false;
+
+ // Don't deal with about:, file: etc.
+ if (!ContentBlockingAllowList.canHandle(gBrowser.selectedBrowser)) {
+ // We hide the icon and thus avoid showing the doorhanger, since
+ // the information contained there would mostly be broken and/or
+ // irrelevant anyway.
+ this._trackingProtectionIconContainer.hidden = true;
+ return;
+ }
+ this._trackingProtectionIconContainer.hidden = false;
+
+ // Check whether the user has added an exception for this site.
+ this.hasException = ContentBlockingAllowList.includes(
+ gBrowser.selectedBrowser
+ );
+
+ if (this._protectionsPopup) {
+ this._protectionsPopup.toggleAttribute("hasException", this.hasException);
+ }
+ this.iconBox.toggleAttribute("hasException", this.hasException);
+
+ // Add to telemetry per page load as a baseline measurement.
+ this.fingerprintersHistogramAdd("pageLoad");
+ this.cryptominersHistogramAdd("pageLoad");
+ this.shieldHistogramAdd(0);
+ },
+
+ notifyContentBlockingEvent(event) {
+ // We don't notify observers until the document stops loading, therefore
+ // a merged event can be sent, which gives an opportunity to decide the
+ // priority by the handler.
+ // Content blocking events coming after stopping will not be merged, and are
+ // sent directly.
+ if (!this._isStoppedState || !this.anyDetected) {
+ return;
+ }
+
+ let uri = gBrowser.currentURI;
+ let uriHost = uri.asciiHost ? uri.host : uri.spec;
+ Services.obs.notifyObservers(
+ {
+ wrappedJSObject: {
+ browser: gBrowser.selectedBrowser,
+ host: uriHost,
+ event,
+ },
+ },
+ "SiteProtection:ContentBlockingEvent"
+ );
+ },
+
+ onStateChange(aWebProgress, stateFlags) {
+ if (!aWebProgress.isTopLevel) {
+ return;
+ }
+
+ this._isStoppedState = !!(
+ stateFlags & Ci.nsIWebProgressListener.STATE_STOP
+ );
+ this.notifyContentBlockingEvent(
+ gBrowser.selectedBrowser.getContentBlockingEvents()
+ );
+ },
+
+ /**
+ * Update the in-panel UI given a blocking event. Called when the popup
+ * is being shown, or when the popup is open while a new event comes in.
+ */
+ updatePanelForBlockingEvent(event) {
+ // Update the categories:
+ for (let blocker of Object.values(this.blockers)) {
+ if (blocker.categoryItem.hasAttribute("uidisabled")) {
+ continue;
+ }
+ blocker.categoryItem.classList.toggle(
+ "notFound",
+ !blocker.isDetected(event)
+ );
+ blocker.categoryItem.classList.toggle(
+ "subviewbutton-nav",
+ blocker.isDetected(event)
+ );
+ }
+
+ // And the popup attributes:
+ this._protectionsPopup.toggleAttribute("detected", this.anyDetected);
+ this._protectionsPopup.toggleAttribute("blocking", this.anyBlocking);
+ this._protectionsPopup.toggleAttribute("hasException", this.hasException);
+
+ this.noTrackersDetectedDescription.hidden = this.anyDetected;
+
+ if (this.anyDetected) {
+ // Reorder categories if any are in use.
+ this.reorderCategoryItems();
+ }
+ },
+
+ reportBlockingEventTelemetry(event, isSimulated, previousState) {
+ if (!isSimulated) {
+ if (this.hasException && !this.hadShieldState) {
+ this.hadShieldState = true;
+ this.shieldHistogramAdd(1);
+ } else if (
+ !this.hasException &&
+ this.anyBlocking &&
+ !this.hadShieldState
+ ) {
+ this.hadShieldState = true;
+ this.shieldHistogramAdd(2);
+ }
+ }
+
+ // We report up to one instance of fingerprinting and cryptomining
+ // blocking and/or allowing per page load.
+ let fingerprintingBlocking =
+ Fingerprinting.isBlocking(event) &&
+ !Fingerprinting.isBlocking(previousState);
+ let fingerprintingAllowing =
+ Fingerprinting.isAllowing(event) &&
+ !Fingerprinting.isAllowing(previousState);
+ let cryptominingBlocking =
+ Cryptomining.isBlocking(event) && !Cryptomining.isBlocking(previousState);
+ let cryptominingAllowing =
+ Cryptomining.isAllowing(event) && !Cryptomining.isAllowing(previousState);
+
+ if (fingerprintingBlocking) {
+ this.fingerprintersHistogramAdd("blocked");
+ } else if (fingerprintingAllowing) {
+ this.fingerprintersHistogramAdd("allowed");
+ }
+
+ if (cryptominingBlocking) {
+ this.cryptominersHistogramAdd("blocked");
+ } else if (cryptominingAllowing) {
+ this.cryptominersHistogramAdd("allowed");
+ }
+ },
+
+ onContentBlockingEvent(event, webProgress, isSimulated, previousState) {
+ // Don't deal with about:, file: etc.
+ if (!ContentBlockingAllowList.canHandle(gBrowser.selectedBrowser)) {
+ this.iconBox.removeAttribute("active");
+ this.iconBox.removeAttribute("hasException");
+ return;
+ }
+
+ // First update all our internal state based on the allowlist and the
+ // different blockers:
+ this.anyDetected = false;
+ this.anyBlocking = false;
+ this._lastEvent = event;
+
+ // Check whether the user has added an exception for this site.
+ this.hasException = ContentBlockingAllowList.includes(
+ gBrowser.selectedBrowser
+ );
+
+ // Update blocker state and find if they detected or blocked anything.
+ for (let blocker of Object.values(this.blockers)) {
+ if (blocker.categoryItem?.hasAttribute("uidisabled")) {
+ continue;
+ }
+ // Store data on whether the blocker is activated for reporting it
+ // using the "report breakage" dialog. Under normal circumstances this
+ // dialog should only be able to open in the currently selected tab
+ // and onSecurityChange runs on tab switch, so we can avoid associating
+ // the data with the document directly.
+ blocker.activated = blocker.isBlocking(event);
+ this.anyDetected = this.anyDetected || blocker.isDetected(event);
+ this.anyBlocking = this.anyBlocking || blocker.activated;
+ }
+
+ this._categoryItemOrderInvalidated = true;
+
+ // Now, update the icon UI:
+
+ // We consider the shield state "active" when some kind of blocking activity
+ // occurs on the page. Note that merely allowing the loading of content that
+ // we could have blocked does not trigger the appearance of the shield.
+ // This state will be overriden later if there's an exception set for this site.
+ this.iconBox.toggleAttribute("active", this.anyBlocking);
+ this.iconBox.toggleAttribute("hasException", this.hasException);
+
+ // Update the icon's tooltip:
+ if (this.hasException) {
+ this.showDisabledTooltipForTPIcon();
+ } else if (this.anyBlocking) {
+ this.showActiveTooltipForTPIcon();
+ } else {
+ this.showNoTrackerTooltipForTPIcon();
+ }
+
+ // Update the panel if it's open.
+ let isPanelOpen = ["showing", "open"].includes(
+ this._protectionsPopup?.state
+ );
+ if (isPanelOpen) {
+ this.updatePanelForBlockingEvent(event);
+ }
+
+ // Notify other consumers, like CFR.
+ // Don't send a content blocking event to CFR for
+ // tab switches since this will already be done via
+ // onStateChange.
+ if (!isSimulated) {
+ this.notifyContentBlockingEvent(event);
+ }
+
+ // Finally, report telemetry.
+ this.reportBlockingEventTelemetry(event, isSimulated, previousState);
+ },
+
+ // We handle focus here when the panel is shown.
+ handleEvent(event) {
+ let elem = document.activeElement;
+ let position = elem.compareDocumentPosition(this._protectionsPopup);
+
+ if (
+ !(
+ position &
+ (Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_CONTAINED_BY)
+ ) &&
+ !this._protectionsPopup.hasAttribute("noautohide")
+ ) {
+ // Hide the panel when focusing an element that is
+ // neither an ancestor nor descendant unless the panel has
+ // @noautohide (e.g. for a tour).
+ PanelMultiView.hidePopup(this._protectionsPopup);
+ }
+ },
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "browser:purge-session-history":
+ // We need to update the earliest recorded date if history has been
+ // cleared.
+ this._hasEarliestRecord = false;
+ this.maybeUpdateEarliestRecordedDateTooltip();
+ break;
+ }
+ },
+
+ /**
+ * Update the popup contents. Only called when the popup has been taken
+ * out of the template and is shown or about to be shown.
+ */
+ refreshProtectionsPopup() {
+ let host = gIdentityHandler.getHostForDisplay();
+
+ // Push the appropriate strings out to the UI.
+ this._protectionsPopupMainViewHeaderLabel.textContent =
+ gNavigatorBundle.getFormattedString("protections.header", [host]);
+
+ let currentlyEnabled = !this.hasException;
+
+ for (let tpSwitch of [
+ this._protectionsPopupTPSwitch,
+ this._protectionsPopupSiteNotWorkingTPSwitch,
+ ]) {
+ tpSwitch.toggleAttribute("enabled", currentlyEnabled);
+ }
+
+ this._notBlockingWhyLink.setAttribute(
+ "tooltip",
+ currentlyEnabled
+ ? "protections-popup-not-blocking-why-etp-on-tooltip"
+ : "protections-popup-not-blocking-why-etp-off-tooltip"
+ );
+
+ // Toggle the breakage link according to the current enable state.
+ this.toggleBreakageLink();
+
+ // Give the button an accessible label for screen readers.
+ if (currentlyEnabled) {
+ this._protectionsPopupTPSwitch.setAttribute(
+ "aria-label",
+ gNavigatorBundle.getFormattedString("protections.disableAriaLabel", [
+ host,
+ ])
+ );
+ } else {
+ this._protectionsPopupTPSwitch.setAttribute(
+ "aria-label",
+ gNavigatorBundle.getFormattedString("protections.enableAriaLabel", [
+ host,
+ ])
+ );
+ }
+
+ // Update the tooltip of the blocked tracker counter.
+ this.maybeUpdateEarliestRecordedDateTooltip();
+
+ let today = Date.now();
+ let threeDaysMillis = 72 * 60 * 60 * 1000;
+ let expired = today - this.milestoneTimestampPref > threeDaysMillis;
+
+ if (this._milestoneTextSet && !expired) {
+ this._protectionsPopup.setAttribute("milestone", this.milestonePref);
+ } else {
+ this._protectionsPopup.removeAttribute("milestone");
+ }
+
+ cookieBannerHandling.updateSection();
+
+ this._protectionsPopup.toggleAttribute("detected", this.anyDetected);
+ this._protectionsPopup.toggleAttribute("blocking", this.anyBlocking);
+ this._protectionsPopup.toggleAttribute("hasException", this.hasException);
+ },
+
+ /*
+ * This function sorts the category items into the Blocked/Allowed/None Detected
+ * sections. It's called immediately in onContentBlockingEvent if the popup
+ * is presently open. Otherwise, the next time the popup is shown.
+ */
+ reorderCategoryItems() {
+ if (!this._categoryItemOrderInvalidated) {
+ return;
+ }
+
+ delete this._categoryItemOrderInvalidated;
+
+ // Hide all the headers to start with.
+ this._protectionsPopupBlockingHeader.hidden = true;
+ this._protectionsPopupNotBlockingHeader.hidden = true;
+ this._protectionsPopupNotFoundHeader.hidden = true;
+
+ for (let { categoryItem } of Object.values(this.blockers)) {
+ if (
+ categoryItem.classList.contains("notFound") ||
+ categoryItem.hasAttribute("uidisabled")
+ ) {
+ // Add the item to the bottom of the list. This will be under
+ // the "None Detected" section.
+ categoryItem.parentNode.insertAdjacentElement(
+ "beforeend",
+ categoryItem
+ );
+ categoryItem.setAttribute("disabled", true);
+ // We have an undetected category, show the header.
+ this._protectionsPopupNotFoundHeader.hidden = false;
+ continue;
+ }
+
+ // Clear the disabled attribute in case we are moving the item out of
+ // "None Detected"
+ categoryItem.removeAttribute("disabled");
+
+ if (categoryItem.classList.contains("blocked") && !this.hasException) {
+ // Add the item just above the "Allowed" section - this will be the
+ // bottom of the "Blocked" section.
+ categoryItem.parentNode.insertBefore(
+ categoryItem,
+ this._protectionsPopupNotBlockingHeader
+ );
+ // We have a blocking category, show the header.
+ this._protectionsPopupBlockingHeader.hidden = false;
+ continue;
+ }
+
+ // Add the item just above the "None Detected" section - this will be the
+ // bottom of the "Allowed" section.
+ categoryItem.parentNode.insertBefore(
+ categoryItem,
+ this._protectionsPopupNotFoundHeader
+ );
+ // We have an allowing category, show the header.
+ this._protectionsPopupNotBlockingHeader.hidden = false;
+ }
+ },
+
+ disableForCurrentPage(shouldReload = true) {
+ ContentBlockingAllowList.add(gBrowser.selectedBrowser);
+ if (shouldReload) {
+ this._hidePopup();
+ BrowserReload();
+ }
+ },
+
+ enableForCurrentPage(shouldReload = true) {
+ ContentBlockingAllowList.remove(gBrowser.selectedBrowser);
+ if (shouldReload) {
+ this._hidePopup();
+ BrowserReload();
+ }
+ },
+
+ async onTPSwitchCommand(event) {
+ // When the switch is clicked, we wait 500ms and then disable/enable
+ // protections, causing the page to refresh, and close the popup.
+ // We need to ensure we don't handle more clicks during the 500ms delay,
+ // so we keep track of state and return early if needed.
+ if (this._TPSwitchCommanding) {
+ return;
+ }
+
+ this._TPSwitchCommanding = true;
+
+ // Toggling the 'hasException' on the protections panel in order to do some
+ // styling after toggling the TP switch.
+ let newExceptionState =
+ this._protectionsPopup.toggleAttribute("hasException");
+ for (let tpSwitch of [
+ this._protectionsPopupTPSwitch,
+ this._protectionsPopupSiteNotWorkingTPSwitch,
+ ]) {
+ tpSwitch.toggleAttribute("enabled", !newExceptionState);
+ }
+
+ // Toggle the breakage link if needed.
+ this.toggleBreakageLink();
+
+ // Change the tooltip of the tracking protection icon.
+ if (newExceptionState) {
+ this.showDisabledTooltipForTPIcon();
+ } else {
+ this.showNoTrackerTooltipForTPIcon();
+ }
+
+ // Change the state of the tracking protection icon.
+ this.iconBox.toggleAttribute("hasException", newExceptionState);
+
+ // Indicating that we need to show a toast after refreshing the page.
+ // And caching the current URI and window ID in order to only show the mini
+ // panel if it's still on the same page.
+ this._showToastAfterRefresh = true;
+ this._previousURI = gBrowser.currentURI.spec;
+ this._previousOuterWindowID = gBrowser.selectedBrowser.outerWindowID;
+
+ if (newExceptionState) {
+ this.disableForCurrentPage(false);
+ this.recordClick("etp_toggle_off");
+ } else {
+ this.enableForCurrentPage(false);
+ this.recordClick("etp_toggle_on");
+ }
+
+ // We need to flush the TP state change immediately without waiting the
+ // 500ms delay if the Tab get switched out.
+ let targetTab = gBrowser.selectedTab;
+ let onTabSelectHandler;
+ let tabSelectPromise = new Promise(resolve => {
+ onTabSelectHandler = () => resolve();
+ gBrowser.tabContainer.addEventListener("TabSelect", onTabSelectHandler);
+ });
+ let timeoutPromise = new Promise(resolve => setTimeout(resolve, 500));
+
+ await Promise.race([tabSelectPromise, timeoutPromise]);
+ gBrowser.tabContainer.removeEventListener("TabSelect", onTabSelectHandler);
+ PanelMultiView.hidePopup(this._protectionsPopup);
+ gBrowser.reloadTab(targetTab);
+
+ delete this._TPSwitchCommanding;
+ },
+
+ onCookieBannerToggleCommand() {
+ cookieBannerHandling.onCookieBannerToggleCommand();
+ },
+
+ setTrackersBlockedCounter(trackerCount) {
+ let forms = gNavigatorBundle.getString(
+ "protections.footer.blockedTrackerCounter.description"
+ );
+ this._protectionsPopupTrackersCounterDescription.textContent =
+ PluralForm.get(trackerCount, forms).replace(
+ "#1",
+ trackerCount.toLocaleString(Services.locale.appLocalesAsBCP47)
+ );
+
+ // Show the counter if the number of tracker is not zero.
+ this._protectionsPopupTrackersCounterBox.toggleAttribute(
+ "showing",
+ trackerCount != 0
+ );
+ },
+
+ // Whenever one of the milestone prefs are changed, we attempt to update
+ // the milestone section string. This requires us to fetch the earliest
+ // recorded date from the Tracking DB, hence this process is async.
+ // When completed, we set _milestoneSetText to signal that the section
+ // is populated and ready to be shown - which happens next time we call
+ // refreshProtectionsPopup.
+ _milestoneTextSet: false,
+ async maybeSetMilestoneCounterText() {
+ if (!this._protectionsPopup) {
+ return;
+ }
+ let trackerCount = this.milestonePref;
+ if (
+ !this.milestonesEnabledPref ||
+ !trackerCount ||
+ !this.milestoneListPref.includes(trackerCount)
+ ) {
+ this._milestoneTextSet = false;
+ return;
+ }
+
+ let date = await TrackingDBService.getEarliestRecordedDate();
+ let dateLocaleStr = new Date(date).toLocaleDateString("default", {
+ month: "long",
+ year: "numeric",
+ });
+
+ let desc = PluralForm.get(
+ trackerCount,
+ gNavigatorBundle.getString("protections.milestone.description")
+ );
+
+ this._protectionsPopupMilestonesText.textContent = desc
+ .replace("#1", gBrandBundle.GetStringFromName("brandShortName"))
+ .replace(
+ "#2",
+ trackerCount.toLocaleString(Services.locale.appLocalesAsBCP47)
+ )
+ .replace("#3", dateLocaleStr);
+
+ this._milestoneTextSet = true;
+ },
+
+ showDisabledTooltipForTPIcon() {
+ this._trackingProtectionIconTooltipLabel.textContent =
+ this.strings.disabledTooltipText;
+ this._trackingProtectionIconContainer.setAttribute(
+ "aria-label",
+ this.strings.disabledTooltipText
+ );
+ },
+
+ showActiveTooltipForTPIcon() {
+ this._trackingProtectionIconTooltipLabel.textContent =
+ this.strings.activeTooltipText;
+ this._trackingProtectionIconContainer.setAttribute(
+ "aria-label",
+ this.strings.activeTooltipText
+ );
+ },
+
+ showNoTrackerTooltipForTPIcon() {
+ this._trackingProtectionIconTooltipLabel.textContent =
+ this.strings.noTrackerTooltipText;
+ this._trackingProtectionIconContainer.setAttribute(
+ "aria-label",
+ this.strings.noTrackerTooltipText
+ );
+ },
+
+ /**
+ * Showing the protections popup.
+ *
+ * @param {Object} options
+ * The object could have two properties.
+ * event:
+ * The event triggers the protections popup to be opened.
+ * toast:
+ * A boolean to indicate if we need to open the protections
+ * popup as a toast. A toast only has a header section and
+ * will be hidden after a certain amount of time.
+ */
+ showProtectionsPopup(options = {}) {
+ const { event, toast } = options;
+
+ this._initializePopup();
+
+ // Ensure we've updated category state based on the last blocking event:
+ if (this.hasOwnProperty("_lastEvent")) {
+ this.updatePanelForBlockingEvent(this._lastEvent);
+ delete this._lastEvent;
+ }
+
+ // We need to clear the toast timer if it exists before showing the
+ // protections popup.
+ if (this._toastPanelTimer) {
+ clearTimeout(this._toastPanelTimer);
+ delete this._toastPanelTimer;
+ }
+
+ this._protectionsPopup.toggleAttribute("toast", !!toast);
+ if (!toast) {
+ // Refresh strings if we want to open it as a standard protections popup.
+ this.refreshProtectionsPopup();
+ }
+
+ if (toast) {
+ this._protectionsPopup.addEventListener(
+ "popupshown",
+ () => {
+ this._toastPanelTimer = setTimeout(() => {
+ PanelMultiView.hidePopup(this._protectionsPopup, true);
+ delete this._toastPanelTimer;
+ }, this._protectionsPopupToastTimeout);
+ },
+ { once: true }
+ );
+ }
+
+ // Add the "open" attribute to the tracking protection icon container
+ // for styling.
+ this._trackingProtectionIconContainer.setAttribute("open", "true");
+
+ // Check the panel state of other panels. Hide them if needed.
+ let openPanels = Array.from(document.querySelectorAll("panel[openpanel]"));
+ for (let panel of openPanels) {
+ PanelMultiView.hidePopup(panel);
+ }
+
+ // Now open the popup, anchored off the primary chrome element
+ PanelMultiView.openPopup(
+ this._protectionsPopup,
+ this._trackingProtectionIconContainer,
+ {
+ position: "bottomleft topleft",
+ triggerEvent: event,
+ }
+ ).catch(console.error);
+ },
+
+ showSiteNotWorkingView() {
+ // Only show the Fonts item if we are restricting font visibility
+ if (this._fontVisibilityTrackingProtection >= 3) {
+ this._siteNotWorkingIssueListFonts.setAttribute("hidden", "true");
+ } else {
+ this._siteNotWorkingIssueListFonts.removeAttribute("hidden");
+ }
+
+ this._protectionsPopupMultiView.showSubView(
+ "protections-popup-siteNotWorkingView"
+ );
+ },
+
+ showSendReportView() {
+ // Save this URI to make sure that the user really only submits the location
+ // they see in the report breakage dialog.
+ this.reportURI = gBrowser.currentURI;
+ let urlWithoutQuery = this.reportURI.asciiSpec.replace(
+ "?" + this.reportURI.query,
+ ""
+ );
+ let commentsTextarea = document.getElementById(
+ "protections-popup-sendReportView-collection-comments"
+ );
+ commentsTextarea.value = "";
+ this._protectionsPopupSendReportURL.value = urlWithoutQuery;
+ this._protectionsPopupSiteNotWorkingReportError.hidden = true;
+ this._protectionsPopupMultiView.showSubView(
+ "protections-popup-sendReportView"
+ );
+ },
+
+ toggleBreakageLink() {
+ // The breakage link will only be shown if tracking protection is enabled
+ // for the site and the TP toggle state is on. And we won't show the
+ // link as toggling TP switch to On from Off. In order to do so, we need to
+ // know the previous TP state. We check the ContentBlockingAllowList instead
+ // of 'hasException' attribute of the protection popup for the previous
+ // since the 'hasException' will also be toggled as well as toggling the TP
+ // switch. We won't be able to know the previous TP state through the
+ // 'hasException' attribute. So we fallback to check the
+ // ContentBlockingAllowList here.
+ this._protectionsPopupTPSwitchBreakageLink.hidden =
+ ContentBlockingAllowList.includes(gBrowser.selectedBrowser) ||
+ !this.anyBlocking ||
+ !this._protectionsPopupTPSwitch.hasAttribute("enabled");
+ // The "Site Fixed?" link behaves similarly but for the opposite state.
+ this._protectionsPopupTPSwitchBreakageFixedLink.hidden =
+ !ContentBlockingAllowList.includes(gBrowser.selectedBrowser) ||
+ this._protectionsPopupTPSwitch.hasAttribute("enabled");
+ },
+
+ submitBreakageReport(uri) {
+ let reportEndpoint = Services.prefs.getStringPref(
+ this.PREF_REPORT_BREAKAGE_URL
+ );
+ if (!reportEndpoint) {
+ return;
+ }
+
+ let commentsTextarea = document.getElementById(
+ "protections-popup-sendReportView-collection-comments"
+ );
+
+ let formData = new FormData();
+ formData.set("title", uri.host);
+
+ // Leave the ? at the end of the URL to signify that this URL had its query stripped.
+ let urlWithoutQuery = uri.asciiSpec.replace(uri.query, "");
+ let body = `Full URL: ${urlWithoutQuery}\n`;
+ body += `userAgent: ${navigator.userAgent}\n`;
+
+ body += "\n**Preferences**\n";
+ body += `${TrackingProtection.prefEnabled}: ${Services.prefs.getBoolPref(
+ TrackingProtection.prefEnabled
+ )}\n`;
+ body += `${
+ TrackingProtection.prefEnabledInPrivateWindows
+ }: ${Services.prefs.getBoolPref(
+ TrackingProtection.prefEnabledInPrivateWindows
+ )}\n`;
+ body += `urlclassifier.trackingTable: ${Services.prefs.getStringPref(
+ "urlclassifier.trackingTable"
+ )}\n`;
+ body += `network.http.referer.defaultPolicy: ${Services.prefs.getIntPref(
+ "network.http.referer.defaultPolicy"
+ )}\n`;
+ body += `network.http.referer.defaultPolicy.pbmode: ${Services.prefs.getIntPref(
+ "network.http.referer.defaultPolicy.pbmode"
+ )}\n`;
+ body += `${ThirdPartyCookies.prefEnabled}: ${Services.prefs.getIntPref(
+ ThirdPartyCookies.prefEnabled
+ )}\n`;
+ body += `privacy.annotate_channels.strict_list.enabled: ${Services.prefs.getBoolPref(
+ "privacy.annotate_channels.strict_list.enabled"
+ )}\n`;
+ body += `privacy.restrict3rdpartystorage.expiration: ${Services.prefs.getIntPref(
+ "privacy.restrict3rdpartystorage.expiration"
+ )}\n`;
+ body += `${Fingerprinting.prefEnabled}: ${Services.prefs.getBoolPref(
+ Fingerprinting.prefEnabled
+ )}\n`;
+ body += `${Cryptomining.prefEnabled}: ${Services.prefs.getBoolPref(
+ Cryptomining.prefEnabled
+ )}\n`;
+ body += `\nhasException: ${this.hasException}\n`;
+
+ body += "\n**Comments**\n" + commentsTextarea.value;
+
+ formData.set("body", body);
+
+ let activatedBlockers = [];
+ for (let blocker of Object.values(this.blockers)) {
+ if (blocker.activated) {
+ activatedBlockers.push(blocker.reportBreakageLabel);
+ }
+ }
+
+ formData.set("labels", activatedBlockers.join(","));
+
+ this._protectionsPopupSendReportButton.disabled = true;
+
+ fetch(reportEndpoint, {
+ method: "POST",
+ credentials: "omit",
+ body: formData,
+ })
+ .then(response => {
+ this._protectionsPopupSendReportButton.disabled = false;
+ if (!response.ok) {
+ console.error(
+ `Content Blocking report to ${reportEndpoint} failed with status ${response.status}`
+ );
+ this._protectionsPopupSiteNotWorkingReportError.hidden = false;
+ } else {
+ this._protectionsPopup.hidePopup();
+ ConfirmationHint.show(
+ this.iconBox,
+ "confirmation-hint-breakage-report-sent"
+ );
+ }
+ })
+ .catch(console.error);
+ },
+
+ onSendReportClicked() {
+ this.submitBreakageReport(this.reportURI);
+ },
+
+ async maybeUpdateEarliestRecordedDateTooltip() {
+ // If we've already updated or the popup isn't in the DOM yet, don't bother
+ // doing this:
+ if (this._hasEarliestRecord || !this._protectionsPopup) {
+ return;
+ }
+
+ let date = await TrackingDBService.getEarliestRecordedDate();
+
+ // If there is no record for any blocked tracker, we don't have to do anything
+ // since the tracker counter won't be shown.
+ if (!date) {
+ return;
+ }
+ this._hasEarliestRecord = true;
+
+ const dateLocaleStr = new Date(date).toLocaleDateString("default", {
+ month: "long",
+ day: "numeric",
+ year: "numeric",
+ });
+
+ const tooltipStr = gNavigatorBundle.getFormattedString(
+ "protections.footer.blockedTrackerCounter.tooltip",
+ [dateLocaleStr]
+ );
+
+ this._protectionsPopupTrackersCounterDescription.setAttribute(
+ "tooltiptext",
+ tooltipStr
+ );
+ },
+};