/* 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 */ XPCOMUtils.defineLazyModuleGetters(this, { ContentBlockingAllowList: "resource://gre/modules/ContentBlockingAllowList.jsm", ToolbarPanelHub: "resource://activity-stream/lib/ToolbarPanelHub.jsm", }); XPCOMUtils.defineLazyServiceGetter( this, "TrackingDBService", "@mozilla.org/tracking-db-service;1", "nsITrackingDBService" ); var Fingerprinting = { PREF_ENABLED: "privacy.trackingprotection.fingerprinting.enabled", reportBreakageLabel: "fingerprinting", strings: { get subViewBlocked() { delete this.subViewBlocked; return (this.subViewBlocked = gNavigatorBundle.getString( "contentBlocking.fingerprintersView.blocked.label" )); }, get subViewTitleBlocking() { delete this.subViewTitleBlocking; return (this.subViewTitleBlocking = gNavigatorBundle.getString( "protections.blocking.fingerprinters.title" )); }, get subViewTitleNotBlocking() { delete this.subViewTitleNotBlocking; return (this.subViewTitleNotBlocking = gNavigatorBundle.getString( "protections.notBlocking.fingerprinters.title" )); }, }, init() { XPCOMUtils.defineLazyPreferenceGetter( this, "enabled", this.PREF_ENABLED, false, this.updateCategoryItem.bind(this) ); }, get categoryItem() { let item = document.getElementById( "protections-popup-category-fingerprinters" ); if (item) { delete this.categoryItem; this.categoryItem = item; } return item; }, updateCategoryItem() { // Can't get `this.categoryItem` without the popup. Using the popup instead // of `this.categoryItem` to guard access, because the category item getter // can trigger bug 1543537. If there's no popup, we'll be called again the // first time the popup shows. if (gProtectionsHandler._protectionsPopup) { this.categoryItem.classList.toggle("blocked", this.enabled); } }, get subView() { delete this.subView; return (this.subView = document.getElementById( "protections-popup-fingerprintersView" )); }, get subViewList() { delete this.subViewList; return (this.subViewList = document.getElementById( "protections-popup-fingerprintersView-list" )); }, isBlocking(state) { return ( (state & Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT) != 0 ); }, isAllowing(state) { return ( (state & Ci.nsIWebProgressListener.STATE_LOADED_FINGERPRINTING_CONTENT) != 0 ); }, isDetected(state) { return this.isBlocking(state) || this.isAllowing(state); }, isShimming(state) { return ( state & Ci.nsIWebProgressListener.STATE_UNBLOCKED_TRACKING_CONTENT && this.isAllowing(state) ); }, updateSubView() { let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog(); contentBlockingLog = JSON.parse(contentBlockingLog); let fragment = document.createDocumentFragment(); for (let [origin, actions] of Object.entries(contentBlockingLog)) { let listItem = this._createListItem(origin, actions); if (listItem) { fragment.appendChild(listItem); } } this.subViewList.textContent = ""; this.subViewList.append(fragment); this.subView.setAttribute( "title", this.enabled && !gProtectionsHandler.hasException ? this.strings.subViewTitleBlocking : this.strings.subViewTitleNotBlocking ); }, _createListItem(origin, actions) { let isAllowed = actions.some( ([state]) => this.isAllowing(state) && !this.isShimming(state) ); let isDetected = isAllowed || actions.some(([state]) => this.isBlocking(state)); if (!isDetected) { return null; } let listItem = document.createXULElement("hbox"); listItem.className = "protections-popup-list-item"; listItem.classList.toggle("allowed", isAllowed); // Repeat the host in the tooltip in case it's too long // and overflows in our panel. listItem.tooltipText = origin; let label = document.createXULElement("label"); label.value = origin; label.className = "protections-popup-list-host-label"; label.setAttribute("crop", "end"); listItem.append(label); return listItem; }, }; var Cryptomining = { PREF_ENABLED: "privacy.trackingprotection.cryptomining.enabled", reportBreakageLabel: "cryptomining", strings: { get subViewBlocked() { delete this.subViewBlocked; return (this.subViewBlocked = gNavigatorBundle.getString( "contentBlocking.cryptominersView.blocked.label" )); }, get subViewTitleBlocking() { delete this.subViewTitleBlocking; return (this.subViewTitleBlocking = gNavigatorBundle.getString( "protections.blocking.cryptominers.title" )); }, get subViewTitleNotBlocking() { delete this.subViewTitleNotBlocking; return (this.subViewTitleNotBlocking = gNavigatorBundle.getString( "protections.notBlocking.cryptominers.title" )); }, }, init() { XPCOMUtils.defineLazyPreferenceGetter( this, "enabled", this.PREF_ENABLED, false, this.updateCategoryItem.bind(this) ); }, get categoryItem() { let item = document.getElementById( "protections-popup-category-cryptominers" ); if (item) { delete this.categoryItem; this.categoryItem = item; } return item; }, updateCategoryItem() { // Can't get `this.categoryItem` without the popup. Using the popup instead // of `this.categoryItem` to guard access, because the category item getter // can trigger bug 1543537. If there's no popup, we'll be called again the // first time the popup shows. if (gProtectionsHandler._protectionsPopup) { this.categoryItem.classList.toggle("blocked", this.enabled); } }, get subView() { delete this.subView; return (this.subView = document.getElementById( "protections-popup-cryptominersView" )); }, get subViewList() { delete this.subViewList; return (this.subViewList = document.getElementById( "protections-popup-cryptominersView-list" )); }, isBlocking(state) { return ( (state & Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT) != 0 ); }, isAllowing(state) { return ( (state & Ci.nsIWebProgressListener.STATE_LOADED_CRYPTOMINING_CONTENT) != 0 ); }, isDetected(state) { return this.isBlocking(state) || this.isAllowing(state); }, isShimming(state) { return ( state & Ci.nsIWebProgressListener.STATE_UNBLOCKED_TRACKING_CONTENT && this.isAllowing(state) ); }, updateSubView() { let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog(); contentBlockingLog = JSON.parse(contentBlockingLog); let fragment = document.createDocumentFragment(); for (let [origin, actions] of Object.entries(contentBlockingLog)) { let listItem = this._createListItem(origin, actions); if (listItem) { fragment.appendChild(listItem); } } this.subViewList.textContent = ""; this.subViewList.append(fragment); this.subView.setAttribute( "title", this.enabled && !gProtectionsHandler.hasException ? this.strings.subViewTitleBlocking : this.strings.subViewTitleNotBlocking ); }, _createListItem(origin, actions) { let isAllowed = actions.some( ([state]) => this.isAllowing(state) && !this.isShimming(state) ); let isDetected = isAllowed || actions.some(([state]) => this.isBlocking(state)); if (!isDetected) { return null; } let listItem = document.createXULElement("hbox"); listItem.className = "protections-popup-list-item"; listItem.classList.toggle("allowed", isAllowed); // Repeat the host in the tooltip in case it's too long // and overflows in our panel. listItem.tooltipText = origin; let label = document.createXULElement("label"); label.value = origin; label.className = "protections-popup-list-host-label"; label.setAttribute("crop", "end"); listItem.append(label); return listItem; }, }; var TrackingProtection = { reportBreakageLabel: "trackingprotection", PREF_ENABLED_GLOBALLY: "privacy.trackingprotection.enabled", PREF_ENABLED_IN_PRIVATE_WINDOWS: "privacy.trackingprotection.pbmode.enabled", PREF_TRACKING_TABLE: "urlclassifier.trackingTable", PREF_TRACKING_ANNOTATION_TABLE: "urlclassifier.trackingAnnotationTable", PREF_ANNOTATIONS_LEVEL_2_ENABLED: "privacy.annotate_channels.strict_list.enabled", enabledGlobally: false, enabledInPrivateWindows: false, get categoryItem() { let item = document.getElementById( "protections-popup-category-tracking-protection" ); if (item) { delete this.categoryItem; this.categoryItem = item; } return item; }, get subView() { delete this.subView; return (this.subView = document.getElementById( "protections-popup-trackersView" )); }, get subViewList() { delete this.subViewList; return (this.subViewList = document.getElementById( "protections-popup-trackersView-list" )); }, strings: { get subViewBlocked() { delete this.subViewBlocked; return (this.subViewBlocked = gNavigatorBundle.getString( "contentBlocking.trackersView.blocked.label" )); }, get subViewTitleBlocking() { delete this.subViewTitleBlocking; return (this.subViewTitleBlocking = gNavigatorBundle.getString( "protections.blocking.trackingContent.title" )); }, get subViewTitleNotBlocking() { delete this.subViewTitleNotBlocking; return (this.subViewTitleNotBlocking = gNavigatorBundle.getString( "protections.notBlocking.trackingContent.title" )); }, }, init() { this.updateEnabled(); Services.prefs.addObserver(this.PREF_ENABLED_GLOBALLY, this); Services.prefs.addObserver(this.PREF_ENABLED_IN_PRIVATE_WINDOWS, this); XPCOMUtils.defineLazyPreferenceGetter( this, "trackingTable", this.PREF_TRACKING_TABLE, false ); XPCOMUtils.defineLazyPreferenceGetter( this, "trackingAnnotationTable", this.PREF_TRACKING_ANNOTATION_TABLE, false ); XPCOMUtils.defineLazyPreferenceGetter( this, "annotationsLevel2Enabled", this.PREF_ANNOTATIONS_LEVEL_2_ENABLED, false ); }, uninit() { Services.prefs.removeObserver(this.PREF_ENABLED_GLOBALLY, this); Services.prefs.removeObserver(this.PREF_ENABLED_IN_PRIVATE_WINDOWS, 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.enabledInPrivateWindows && PrivateBrowsingUtils.isWindowPrivate(window)) ); }, updateEnabled() { this.enabledGlobally = Services.prefs.getBoolPref( this.PREF_ENABLED_GLOBALLY ); this.enabledInPrivateWindows = Services.prefs.getBoolPref( this.PREF_ENABLED_IN_PRIVATE_WINDOWS ); }, updateCategoryItem() { if (this.categoryItem) { this.categoryItem.classList.toggle("blocked", this.enabled); } }, isBlocking(state) { return ( (state & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT) != 0 ); }, isAllowingLevel1(state) { return ( (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); }, isDetected(state) { return this.isBlocking(state) || this.isAllowing(state); }, isShimming(state) { return ( state & Ci.nsIWebProgressListener.STATE_UNBLOCKED_TRACKING_CONTENT && this.isAllowing(state) ); }, async updateSubView() { let previousURI = gBrowser.currentURI.spec; let previousWindow = gBrowser.selectedBrowser.innerWindowID; let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog(); contentBlockingLog = JSON.parse(contentBlockingLog); let fragment = document.createDocumentFragment(); for (let [origin, actions] of Object.entries(contentBlockingLog)) { let listItem = await this._createListItem(origin, actions); if (listItem) { fragment.appendChild(listItem); } } // 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 (!fragment.childNodes.length) { let emptyBox = document.createXULElement("vbox"); let emptyImage = document.createXULElement("image"); emptyImage.classList.add("protections-popup-trackersView-empty-image"); emptyImage.classList.add("tracking-protection-icon"); let emptyLabel = document.createXULElement("label"); emptyLabel.classList.add("protections-popup-empty-label"); emptyLabel.textContent = gNavigatorBundle.getString( "contentBlocking.trackersView.empty.label" ); emptyBox.appendChild(emptyImage); emptyBox.appendChild(emptyLabel); fragment.appendChild(emptyBox); 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.subViewList.textContent = ""; this.subViewList.append(fragment); 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 null; } // 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 null; } let listItem = document.createXULElement("hbox"); listItem.className = "protections-popup-list-item"; listItem.classList.toggle("allowed", isAllowed); // Repeat the host in the tooltip in case it's too long // and overflows in our panel. listItem.tooltipText = origin; let label = document.createXULElement("label"); label.value = origin; label.className = "protections-popup-list-host-label"; label.setAttribute("crop", "end"); listItem.append(label); return listItem; }, }; var ThirdPartyCookies = { PREF_ENABLED: "network.cookie.cookieBehavior", PREF_ENABLED_VALUES: [ // These values match the ones exposed under the Content Blocking section // of the Preferences UI. Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, // Block all third-party cookies Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, // Block third-party cookies from trackers Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, // Block trackers and patition third-party trackers Ci.nsICookieService.BEHAVIOR_REJECT, // Block all cookies ], get categoryItem() { let item = document.getElementById("protections-popup-category-cookies"); if (item) { delete this.categoryItem; this.categoryItem = item; } return item; }, get subView() { delete this.subView; return (this.subView = document.getElementById( "protections-popup-cookiesView" )); }, get subViewHeading() { delete this.subViewHeading; return (this.subViewHeading = document.getElementById( "protections-popup-cookiesView-heading" )); }, get subViewList() { delete this.subViewList; return (this.subViewList = document.getElementById( "protections-popup-cookiesView-list" )); }, strings: { get subViewAllowed() { delete this.subViewAllowed; return (this.subViewAllowed = gNavigatorBundle.getString( "contentBlocking.cookiesView.allowed.label" )); }, get subViewBlocked() { delete this.subViewAllowed; return (this.subViewAllowed = gNavigatorBundle.getString( "contentBlocking.cookiesView.blocked.label" )); }, get subViewTitleNotBlocking() { delete this.subViewTitleNotBlocking; return (this.subViewTitleNotBlocking = gNavigatorBundle.getString( "protections.notBlocking.crossSiteTrackingCookies.title" )); }, }, 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: Cu.reportError( `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"; } }, init() { XPCOMUtils.defineLazyPreferenceGetter( this, "behaviorPref", this.PREF_ENABLED, Ci.nsICookieService.BEHAVIOR_ACCEPT, this.updateCategoryItem.bind(this) ); }, get categoryLabel() { delete this.categoryLabel; return (this.categoryLabel = document.getElementById( "protections-popup-cookies-category-label" )); }, updateCategoryItem() { // Can't get `this.categoryItem` without the popup. Using the popup instead // of `this.categoryItem` to guard access, because the category item getter // can trigger bug 1543537. If there's no popup, we'll be called again the // first time the popup shows. if (!gProtectionsHandler._protectionsPopup) { return; } this.categoryItem.classList.toggle("blocked", this.enabled); 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: Cu.reportError( `Error: Unknown cookieBehavior pref observed: ${this.behaviorPref}` ); break; } } this.categoryLabel.textContent = label ? gNavigatorBundle.getString(label) : ""; }, get enabled() { return this.PREF_ENABLED_VALUES.includes(this.behaviorPref); }, isBlocking(state) { return ( (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER) != 0 || (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER) != 0 || (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL) != 0 || (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION) != 0 || (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN) != 0 ); }, isDetected(state) { if (this.isBlocking(state)) { return true; } if ( [ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, Ci.nsICookieService.BEHAVIOR_ACCEPT, ].includes(this.behaviorPref) ) { return ( (state & Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_TRACKER) != 0 || (SocialTracking.enabled && (state & Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER) != 0) ); } // We don't have specific flags for the other cookie behaviors so just // fall back to STATE_COOKIES_LOADED. return (state & Ci.nsIWebProgressListener.STATE_COOKIES_LOADED) != 0; }, 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; break; case Ci.nsICookieService.BEHAVIOR_REJECT: title = titleStringPrefix + "all.title"; this.subViewHeading.hidden = true; break; case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN: title = "protections.blocking.cookies.unvisited.title"; this.subViewHeading.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: Cu.reportError( `Error: Unknown cookieBehavior pref when updating subview: ${this.behaviorPref}` ); break; } this.subView.setAttribute("title", gNavigatorBundle.getString(title)); }, _getExceptionState(origin) { for (let perm of Services.perms.getAllForPrincipal( gBrowser.contentPrincipal )) { if (perm.type == "3rdPartyStorage^" + origin) { return perm.capability; } } 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.createXULElement("hbox"); 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) ) { let stateLabel; if (isAllowed) { stateLabel = document.createXULElement("label"); stateLabel.value = this.strings.subViewAllowed; stateLabel.className = "protections-popup-list-state-label"; listItem.append(stateLabel); listItem.classList.toggle("allowed", true); } else { stateLabel = document.createXULElement("label"); stateLabel.value = this.strings.subViewBlocked; stateLabel.className = "protections-popup-list-state-label"; listItem.append(stateLabel); } let removeException = document.createXULElement("button"); removeException.className = "identity-popup-permission-remove-button"; removeException.tooltipText = gNavigatorBundle.getFormattedString( "contentBlocking.cookiesView.removeButton.tooltip", [origin] ); removeException.addEventListener( "click", () => { this._clearException(origin); stateLabel.remove(); removeException.remove(); listItem.classList.toggle("allowed", !isAllowed); }, { once: true } ); listItem.append(removeException); } return listItem; }, }; var SocialTracking = { PREF_STP_TP_ENABLED: "privacy.trackingprotection.socialtracking.enabled", PREF_STP_COOKIE_ENABLED: "privacy.socialtracking.block_cookies.enabled", PREF_COOKIE_BEHAVIOR: "network.cookie.cookieBehavior", reportBreakageLabel: "socialtracking", strings: { get subViewBlocked() { delete this.subViewBlocked; return (this.subViewBlocked = gNavigatorBundle.getString( "contentBlocking.fingerprintersView.blocked.label" )); }, get subViewTitleBlocking() { delete this.subViewTitleBlocking; return (this.subViewTitleBlocking = gNavigatorBundle.getString( "protections.blocking.socialMediaTrackers.title" )); }, get subViewTitleNotBlocking() { delete this.subViewTitleNotBlocking; return (this.subViewTitleNotBlocking = gNavigatorBundle.getString( "protections.notBlocking.socialMediaTrackers.title" )); }, }, init() { XPCOMUtils.defineLazyPreferenceGetter( this, "socialTrackingProtectionEnabled", this.PREF_STP_TP_ENABLED, false, this.updateCategoryItem.bind(this) ); XPCOMUtils.defineLazyPreferenceGetter( this, "rejectTrackingCookies", this.PREF_COOKIE_BEHAVIOR, false, this.updateCategoryItem.bind(this), val => [ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, ].includes(val) ); XPCOMUtils.defineLazyPreferenceGetter( this, "enabled", this.PREF_STP_COOKIE_ENABLED, false, this.updateCategoryItem.bind(this) ); }, get blockingEnabled() { return ( (this.socialTrackingProtectionEnabled || this.rejectTrackingCookies) && this.enabled ); }, updateCategoryItem() { // Can't get `this.categoryItem` without the popup. Using the popup instead // of `this.categoryItem` to guard access, because the category item getter // can trigger bug 1543537. If there's no popup, we'll be called again the // first time the popup shows. if (!gProtectionsHandler._protectionsPopup) { return; } if (this.enabled) { this.categoryItem.removeAttribute("uidisabled"); } else { this.categoryItem.setAttribute("uidisabled", true); } this.categoryItem.classList.toggle("blocked", this.blockingEnabled); }, isBlocking(state) { let socialtrackingContentBlocked = (state & Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT) != 0; let socialtrackingCookieBlocked = (state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER) != 0; return socialtrackingCookieBlocked || socialtrackingContentBlocked; }, isAllowing(state) { if (this.socialTrackingProtectionEnabled) { return ( (state & Ci.nsIWebProgressListener.STATE_LOADED_SOCIALTRACKING_CONTENT) != 0 ); } return ( (state & Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER) != 0 ); }, isDetected(state) { return this.isBlocking(state) || this.isAllowing(state); }, isShimming(state) { return ( state & Ci.nsIWebProgressListener.STATE_UNBLOCKED_TRACKING_CONTENT && this.isAllowing(state) ); }, get categoryItem() { let item = document.getElementById( "protections-popup-category-socialblock" ); if (item) { delete this.categoryItem; this.categoryItem = item; } return item; }, get subView() { delete this.subView; return (this.subView = document.getElementById( "protections-popup-socialblockView" )); }, get subViewList() { delete this.subViewList; return (this.subViewList = document.getElementById( "protections-popup-socialblockView-list" )); }, updateSubView() { let contentBlockingLog = gBrowser.selectedBrowser.getContentBlockingLog(); contentBlockingLog = JSON.parse(contentBlockingLog); let fragment = document.createDocumentFragment(); for (let [origin, actions] of Object.entries(contentBlockingLog)) { let listItem = this._createListItem(origin, actions); if (listItem) { fragment.appendChild(listItem); } } this.subViewList.textContent = ""; this.subViewList.append(fragment); this.subView.setAttribute( "title", this.blockingEnabled && !gProtectionsHandler.hasException ? this.strings.subViewTitleBlocking : this.strings.subViewTitleNotBlocking ); }, _createListItem(origin, actions) { let isAllowed = actions.some( ([state]) => this.isAllowing(state) && !this.isShimming(state) ); let isDetected = isAllowed || actions.some(([state]) => this.isBlocking(state)); if (!isDetected) { return null; } let listItem = document.createXULElement("hbox"); listItem.className = "protections-popup-list-item"; listItem.classList.toggle("allowed", isAllowed); // Repeat the host in the tooltip in case it's too long // and overflows in our panel. listItem.tooltipText = origin; let label = document.createXULElement("label"); label.value = origin; label.className = "protections-popup-list-host-label"; label.setAttribute("crop", "end"); listItem.append(label); return listItem; }, }; /** * 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 this.blockers) { blocker.updateCategoryItem(); } let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL"); document.getElementById( "protections-popup-sendReportView-learn-more" ).href = baseURL + "blocking-breakage"; } }, _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 animatedIcon() { delete this.animatedIcon; return (this.animatedIcon = document.getElementById( "tracking-protection-icon-animatable-image" )); }, get _protectionsIconBox() { delete this._protectionsIconBox; return (this._protectionsIconBox = document.getElementById( "tracking-protection-icon-animatable-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 _protectionsPopupTPSwitchSection() { delete this._protectionsPopupTPSwitchSection; return (this._protectionsPopupTPSwitchSection = document.getElementById( "protections-popup-tp-switch-section" )); }, 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" )); }, 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() { this.animatedIcon.addEventListener("animationend", () => this.iconBox.removeAttribute("animate") ); 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 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 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" ); }, 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.addEventListener("focus", this, true); // Add the "open" attribute to the tracking protection icon container // for styling. this._trackingProtectionIconContainer.setAttribute("open", "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); this._trackingProtectionIconContainer.removeAttribute("open"); } }, 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, isShown) { // Update the categories: for (let blocker of this.blockers) { if (blocker.categoryItem.hasAttribute("uidisabled")) { continue; } blocker.categoryItem.classList.toggle( "notFound", !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(); if (isShown) { // Until we encounter a site that triggers them, category elements might // be invisible when descriptionHeightWorkaround gets called, i.e. they // are omitted from the workaround and the content overflows the panel. // Solution: call it manually here. PanelMultiView.forNode( this._protectionsPopupMainView ).descriptionHeightWorkaround(); } } }, 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("animate"); 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 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: // Reset the animation in case the user is switching tabs or if no blockers were detected // (this is most likely happening because the user navigated on to a different site). This // allows us to play it from the start without choppiness next time. if (isSimulated || !this.anyBlocking) { this.iconBox.removeAttribute("animate"); // Only play the animation when the shield is not already shown on the page (the visibility // of the shield based on this onSecurityChange be determined afterwards). } else if (this.anyBlocking && !this.iconBox.hasAttribute("active")) { this.iconBox.setAttribute("animate", "true"); } // 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, true); } // 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(); // Display a short TP switch section depending on the enable state. We need // to use a separate attribute here since the 'hasException' attribute will // be toggled as well as the TP switch, we cannot rely on that to decide the // height of TP switch section, or it will change when toggling the switch, // which is not desirable for us. So, we need to use a different attribute // here. this._protectionsPopupTPSwitchSection.toggleAttribute( "short", !currentlyEnabled ); // 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"); } 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 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; }, 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); 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: "bottomcenter topleft", triggerEvent: event, } ).catch(Cu.reportError); }, showSiteNotWorkingView() { 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.PREF_ENABLED_GLOBALLY }: ${Services.prefs.getBoolPref( TrackingProtection.PREF_ENABLED_GLOBALLY )}\n`; body += `${ TrackingProtection.PREF_ENABLED_IN_PRIVATE_WINDOWS }: ${Services.prefs.getBoolPref( TrackingProtection.PREF_ENABLED_IN_PRIVATE_WINDOWS )}\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.PREF_ENABLED}: ${Services.prefs.getIntPref( ThirdPartyCookies.PREF_ENABLED )}\n`; body += `network.cookie.lifetimePolicy: ${Services.prefs.getIntPref( "network.cookie.lifetimePolicy" )}\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.PREF_ENABLED}: ${Services.prefs.getBoolPref( Fingerprinting.PREF_ENABLED )}\n`; body += `${Cryptomining.PREF_ENABLED}: ${Services.prefs.getBoolPref( Cryptomining.PREF_ENABLED )}\n`; body += `\nhasException: ${this.hasException}\n`; body += "\n**Comments**\n" + commentsTextarea.value; formData.set("body", body); let activatedBlockers = []; for (let blocker of 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) { Cu.reportError( `Content Blocking report to ${reportEndpoint} failed with status ${response.status}` ); this._protectionsPopupSiteNotWorkingReportError.hidden = false; } else { this._protectionsPopup.hidePopup(); ConfirmationHint.show(this.iconBox, "breakageReport"); } }) .catch(Cu.reportError); }, 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 ); }, };