/* 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",
ReportBrokenSite:
"resource:///modules/ReportBrokenSite.sys.mjs",
SpecialMessageActions:
"resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
});
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 {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 },
{
load,
block,
shim = Ci.nsIWebProgressListener.STATE_REPLACED_TRACKING_CONTENT,
allow = Ci.nsIWebProgressListener.STATE_ALLOWED_TRACKING_CONTENT,
}
) {
this._id = id;
this.prefEnabled = prefEnabled;
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)
);
}
MozXULElement.insertFTLIfNeeded(
"browser/siteProtections.ftl");
ChromeUtils.defineLazyGetter(
this,
"subView", () =>
document.getElementById(`protections-popup-${
this._id}View`)
);
ChromeUtils.defineLazyGetter(
this,
"subViewHeading", () =>
document.getElementById(`protections-popup-${
this._id}View-heading`)
);
ChromeUtils.defineLazyGetter(
this,
"subViewList", () =>
document.getElementById(`protections-popup-${
this._id}View-list`)
);
ChromeUtils.defineLazyGetter(
this,
"subViewShimAllowHint", () =>
document.getElementById(
`protections-popup-${
this._id}View-shim-allow-hint`
)
);
ChromeUtils.defineLazyGetter(
this,
"isWindowPrivate", () =>
PrivateBrowsingUtils.isWindowPrivate(window)
);
}
// 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 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);
const isBlocking =
this.blockingEnabled && !gProtectionsHandler.hasException;
let l10nId;
switch (
this._id) {
case "cryptominers":
l10nId = isBlocking
?
"protections-blocking-cryptominers"
:
"protections-not-blocking-cryptominers";
break;
case "fingerprinters":
l10nId = isBlocking
?
"protections-blocking-fingerprinters"
:
"protections-not-blocking-fingerprinters";
break;
case "socialblock":
l10nId = isBlocking
?
"protections-blocking-social-media-trackers"
:
"protections-not-blocking-social-media-trackers";
break;
}
if (l10nId) {
document.l10n.setAttributes(
this.subView, l10nId);
}
}
/**
* 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.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 (
class FingerprintingProtection
extends ProtectionCategory {
constructor() {
super(
"fingerprinters",
{
prefEnabled:
"privacy.trackingprotection.fingerprinting.enabled",
},
{
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,
}
);
this.prefFPPEnabled =
"privacy.fingerprintingProtection";
this.prefFPPEnabledInPrivateWindows =
"privacy.fingerprintingProtection.pbmode";
this.enabledFPB =
false;
this.enabledFPPGlobally =
false;
this.enabledFPPInPrivateWindows =
false;
}
init() {
this.updateEnabled();
Services.prefs.addObserver(
this.prefEnabled,
this);
Services.prefs.addObserver(
this.prefFPPEnabled,
this);
Services.prefs.addObserver(
this.prefFPPEnabledInPrivateWindows,
this);
}
uninit() {
Services.prefs.removeObserver(
this.prefEnabled,
this);
Services.prefs.removeObserver(
this.prefFPPEnabled,
this);
Services.prefs.removeObserver(
this.prefFPPEnabledInPrivateWindows,
this);
}
updateEnabled() {
this.enabledFPB = Services.prefs.getBoolPref(
this.prefEnabled);
this.enabledFPPGlobally = Services.prefs.getBoolPref(
this.prefFPPEnabled);
this.enabledFPPInPrivateWindows = Services.prefs.getBoolPref(
this.prefFPPEnabledInPrivateWindows
);
}
observe() {
this.updateEnabled();
this.updateCategoryItem();
}
get enabled() {
return (
this.enabledFPB ||
this.enabledFPPGlobally ||
(
this.isWindowPrivate &&
this.enabledFPPInPrivateWindows)
);
}
isBlocking(state) {
let blockFlag =
this._flags.block;
// We only consider the suspicious fingerprinting flag if the
// fingerprinting protection is enabled in the context.
if (
this.enabledFPPGlobally ||
(
this.isWindowPrivate &&
this.enabledFPPInPrivateWindows)
) {
blockFlag |=
Ci.nsIWebProgressListener.STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING;
}
return (state & blockFlag) != 0;
}
// TODO (Bug 1864914): Consider showing suspicious fingerprinting as allowed
// when the fingerprinting protection is disabled.
})();
let Cryptomining =
new ProtectionCategory(
"cryptominers",
{
prefEnabled:
"privacy.trackingprotection.cryptomining.enabled",
},
{
load: Ci.nsIWebProgressListener.STATE_LOADED_CRYPTOMINING_CONTENT,
block: Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT,
}
);
let TrackingProtection =
new (
class TrackingProtection
extends ProtectionCategory {
constructor() {
super(
"trackers",
{
prefEnabled:
"privacy.trackingprotection.enabled",
},
{
load:
null,
block:
Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT |
Ci.nsIWebProgressListener.STATE_BLOCKED_EMAILTRACKING_CONTENT,
}
);
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 ||
(
this.isWindowPrivate &&
(
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");
document.l10n.setAttributes(
emptyLabel,
"content-blocking-trackers-view-empty"
);
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);
const l10nId =
this.enabled && !gProtectionsHandler.hasException
?
"protections-blocking-tracking-content"
:
"protections-not-blocking-tracking-content";
document.l10n.setAttributes(
this.subView, l10nId);
}
}
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,
}
);
ChromeUtils.defineLazyGetter(
this,
"categoryLabel", () =>
document.getElementById(
"protections-popup-cookies-category-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)
);
}
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 ||
(state & Ci.nsIWebProgressListener.STATE_COOKIES_PARTITIONED_TRACKER) !=
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 l10nId;
if (!
this.enabled) {
l10nId =
"content-blocking-cookies-blocking-trackers-label";
}
else {
switch (
this.behaviorPref) {
case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
l10nId =
"content-blocking-cookies-blocking-third-party-label";
break;
case Ci.nsICookieService.BEHAVIOR_REJECT:
l10nId =
"content-blocking-cookies-blocking-all-label";
break;
case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN:
l10nId =
"content-blocking-cookies-blocking-unvisited-label";
break;
case Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER:
case Ci.nsICookieService
.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN:
l10nId =
"content-blocking-cookies-blocking-trackers-label";
break;
default:
console.error(
`Error: Unknown cookieBehavior pref observed: ${
this.behaviorPref}`
);
this.categoryLabel.removeAttribute(
"data-l10n-id");
this.categoryLabel.textContent =
"";
return;
}
}
document.l10n.setAttributes(
this.categoryLabel, l10nId);
}
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";
let l10nId;
switch (category) {
case "trackers":
l10nId =
"content-blocking-cookies-view-trackers-label";
break;
case "firstParty":
l10nId =
"content-blocking-cookies-view-first-party-label";
break;
case "thirdParty":
l10nId =
"content-blocking-cookies-view-third-party-label";
break;
}
if (l10nId) {
document.l10n.setAttributes(label, l10nId);
}
box.appendChild(label);
for (let info of itemsToShow) {
box.appendChild(
this._createListItem(info));
}
this.subViewList.appendChild(box);
}
this.subViewHeading.hidden =
false;
if (!
this.enabled) {
document.l10n.setAttributes(
this.subView,
"protections-not-blocking-cross-site-tracking-cookies"
);
return;
}
let l10nId;
let siteException = gProtectionsHandler.hasException;
switch (
this.behaviorPref) {
case Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN:
l10nId = siteException
?
"protections-not-blocking-cookies-third-party"
:
"protections-blocking-cookies-third-party";
this.subViewHeading.hidden =
true;
if (
this.subViewHeading.nextSibling.nodeName ==
"toolbarseparator") {
this.subViewHeading.nextSibling.hidden =
true;
}
break;
case Ci.nsICookieService.BEHAVIOR_REJECT:
l10nId = siteException
?
"protections-not-blocking-cookies-all"
:
"protections-blocking-cookies-all";
this.subViewHeading.hidden =
true;
if (
this.subViewHeading.nextSibling.nodeName ==
"toolbarseparator") {
this.subViewHeading.nextSibling.hidden =
true;
}
break;
case Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN:
l10nId =
"protections-blocking-cookies-unvisited";
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:
l10nId = siteException
?
"protections-not-blocking-cross-site-tracking-cookies"
:
"protections-blocking-cookies-trackers";
break;
default:
console.error(
`Error: Unknown cookieBehavior pref when updating subview: ${
this.behaviorPref}`
);
return;
}
document.l10n.setAttributes(
this.subView, l10nId);
}
_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";
let l10nId;
if (isAllowed) {
l10nId =
"content-blocking-cookies-view-allowed-label";
listItem.classList.toggle(
"allowed",
true);
}
else {
l10nId =
"content-blocking-cookies-view-blocked-label";
}
document.l10n.setAttributes(stateLabel, l10nId);
let removeException = document.createXULElement(
"button");
removeException.className =
"permission-popup-permission-remove-button";
document.l10n.setAttributes(
removeException,
"content-blocking-cookies-view-remove-button",
{ domain: 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",
{
prefEnabled:
"privacy.socialtracking.block_cookies.enabled",
},
{
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
);
ChromeUtils.defineLazyGetter(
this,
"_cookieBannerSection", () =>
document.getElementById(
"protections-popup-cookie-banner-section")
);
ChromeUtils.defineLazyGetter(
this,
"_cookieBannerSectionSeparator", () =>
document.getElementById(
"protections-popup-cookie-banner-section-separator"
)
);
ChromeUtils.defineLazyGetter(
this,
"_cookieBannerSwitch", () =>
document.getElementById(
"protections-popup-cookie-banner-switch")
);
ChromeUtils.defineLazyGetter(
this,
"_cookieBannerSubview", () =>
document.getElementById(
"protections-popup-cookieBannerView")
);
ChromeUtils.defineLazyGetter(
this,
"_cookieBannerEnableSite", () =>
document.getElementById(
"cookieBannerView-enable-site")
);
ChromeUtils.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();
Glean.securityUiProtectionspopup.clickCookiebToggleOff.record();
}
else {
this.#enableCookieBannerHandling();
Glean.securityUiProtectionspopup.clickCookiebToggleOn.record();
}
gProtectionsHandler._hidePopup();
gBrowser.reloadTab(gBrowser.selectedTab);
}
})();
/**
* Utility object to handle manipulations of the protections indicators in the UI
*/
var gProtectionsHandler = {
PREF_CB_CATEGORY:
"browser.contentblocking.category",
/**
* Contains an array of smartblock compatible sites and information on the corresponding shim
* sites is a list of compatible sites
* shimId is the id of the shim blocking content from the origin
* displayName is the name shown for the toggle used for blocking/unblocking the origin
*/
smartblockEmbedInfo: [
{
sites: [
"https://itisatracker.org"],
shimId:
"EmbedTestShim",
displayName:
"Test",
},
{
sites: [
"https://www.instagram.com", "https://platform.instagram.com"],
shimId:
"InstagramEmbed",
displayName:
"Instagram",
},
{
sites: [
"https://www.tiktok.com"],
shimId:
"TiktokEmbed",
displayName:
"Tiktok",
},
],
/**
* Keeps track of if a smartblock toggle has been clicked since the panel was opened. Resets
* everytime the panel is closed. Used for telemetry purposes.
*/
_hasClickedSmartBlockEmbedToggle:
false,
/**
* Keeps track of what was responsible for opening the protections panel popup. Used for
* telemetry purposes.
*/
_protectionsPopupOpeningReason:
null,
_protectionsPopup:
null,
_initializePopup() {
if (!
this._protectionsPopup) {
let wrapper = document.getElementById(
"template-protections-popup");
this._protectionsPopup = wrapper.content.firstElementChild;
this._protectionsPopup.addEventListener(
"popupshown",
this);
this._protectionsPopup.addEventListener(
"popuphidden",
this);
wrapper.replaceWith(wrapper.content);
this.maybeSetMilestoneCounterText();
for (let blocker of Object.values(
this.blockers)) {
blocker.updateCategoryItem();
}
this._protectionsPopup.addEventListener(
"command",
this);
this._protectionsPopup.addEventListener(
"popupshown",
this);
this._protectionsPopup.addEventListener(
"popuphidden",
this);
function openTooltip(event) {
document.getElementById(event.target.tooltip).openPopup(event.target);
}
function closeTooltip(event) {
document.getElementById(event.target.tooltip).hidePopup();
}
let notBlockingWhy = document.getElementById(
"protections-popup-not-blocking-section-why"
);
notBlockingWhy.addEventListener(
"mouseover", openTooltip);
notBlockingWhy.addEventListener(
"focus", openTooltip);
notBlockingWhy.addEventListener(
"mouseout", closeTooltip);
notBlockingWhy.addEventListener(
"blur", closeTooltip);
document
.getElementById(
"protections-popup-trackers-blocked-counter-description"
)
.addEventListener(
"click", () =>
gProtectionsHandler.openProtections(
true)
);
document
.getElementById(
"protections-popup-cookie-banner-switch")
.addEventListener(
"click", () =>
gProtectionsHandler.onCookieBannerClick()
);
}
},
_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 _protectionsPopupTPSwitch() {
delete this._protectionsPopupTPSwitch;
return (
this._protectionsPopupTPSwitch = document.getElementById(
"protections-popup-tp-switch"
));
},
get _protectionsPopupCategoryList() {
delete this._protectionsPopupCategoryList;
return (
this._protectionsPopupCategoryList = document.getElementById(
"protections-popup-category-list"
));
},
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 _protectionsPopupSmartblockContainer() {
delete this._protectionsPopupSmartblockContainer;
return (
this._protectionsPopupSmartblockContainer = document.getElementById(
"protections-popup-smartblock-highlight-container"
));
},
get _protectionsPopupSmartblockDescription() {
delete this._protectionsPopupSmartblockDescription;
return (
this._protectionsPopupSmartblockDescription =
document.getElementById(
"protections-popup-smartblock-description"));
},
get _protectionsPopupSmartblockToggleContainer() {
delete this._protectionsPopupSmartblockToggleContainer;
return (
this._protectionsPopupSmartblockToggleContainer =
document.getElementById(
"protections-popup-smartblock-toggle-container"));
},
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 _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"
));
},
// 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,
"_protectionsPopupButtonDelay",
"security.notification_enable_delay",
500
);
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()
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"protectionsPanelMessageSeen",
"browser.protections_panel.infoMessage.seen",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"smartblockEmbedsEnabledPref",
"extensions.webcompat.smartblockEmbeds.enabled",
false
);
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");
// Add an observer to listen to requests to open the protections panel
Services.obs.addObserver(
this,
"smartblock:open-protections-panel");
// bind the reset toggle sec delay function to this so we can use it
// as an event listener without this becoming the event target inside
// the function
this._resetToggleSecDelay =
this._resetToggleSecDelay.bind(
this);
},
uninit() {
for (let blocker of Object.values(
this.blockers)) {
if (blocker.uninit) {
blocker.uninit();
}
}
Services.obs.removeObserver(
this,
"browser:purge-session-history");
Services.obs.removeObserver(
this,
"smartblock:open-protections-panel");
},
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() {
await TrackingProtection.updateSubView();
this._protectionsPopupMultiView.showSubView(
"protections-popup-trackersView"
);
},
async showSocialblockerSubview() {
await SocialTracking.updateSubView();
this._protectionsPopupMultiView.showSubView(
"protections-popup-socialblockView"
);
},
async showCookiesSubview() {
await ThirdPartyCookies.updateSubView();
this._protectionsPopupMultiView.showSubView(
"protections-popup-cookiesView"
);
},
async showFingerprintersSubview() {
await Fingerprinting.updateSubView();
this._protectionsPopupMultiView.showSubView(
"protections-popup-fingerprintersView"
);
},
async showCryptominersSubview() {
await Cryptomining.updateSubView();
this._protectionsPopupMultiView.showSubView(
"protections-popup-cryptominersView"
);
},
async onCookieBannerClick() {
if (!cookieBannerHandling.isSiteSupported) {
return;
}
await cookieBannerHandling.updateSubView();
this._protectionsPopupMultiView.showSubView(
"protections-popup-cookieBannerView"
);
},
shieldHistogramAdd(value) {
if (PrivateBrowsingUtils.isWindowPrivate(window)) {
return;
}
Glean.contentblocking.trackingProtectionShield.accumulateSingleSample(
value
);
},
cryptominersHistogramAdd(value) {
Glean.contentblocking.cryptominersBlockedCount[value].add(1);
},
fingerprintersHistogramAdd(value) {
Glean.contentblocking.fingerprintersBlockedCount[value].add(1);
},
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, openingReason:
"shieldButtonClicked" });
},
onPopupShown(event) {
if (event.target ==
this._protectionsPopup) {
PopupNotifications.suppressWhileOpen(
this._protectionsPopup);
window.addEventListener(
"focus",
this,
true);
this._protectionsPopupTPSwitch.addEventListener(
"toggle",
this);
// Insert the info message if needed. This will be shown once and then
// remain collapsed.
this._insertProtectionsPanelInfoMessage(event);
// Record telemetry for open, don't record if the panel open is only a toast
if (!event.target.hasAttribute(
"toast")) {
Glean.securityUiProtectionspopup.openProtectionsPopup.record({
openingReason:
this._protectionsPopupOpeningReason,
smartblockEmbedTogglesShown:
!
this._protectionsPopupSmartblockContainer.hidden,
});
}
// Add the "open" attribute to the tracking protection icon container
// for styling.
// Only set the attribute once the panel is opened to avoid icon being
// incorrectly highlighted if opening is cancelled. See Bug 1926460.
this._trackingProtectionIconContainer.setAttribute(
"open",
"true");
// Disable the toggles for a short time after opening via SmartBlock placeholder button
// to prevent clickjacking.
if (
this._protectionsPopupOpeningReason ==
"embedPlaceholderButton") {
this._disablePopupToggles();
this._protectionsPopupToggleDelayTimer = setTimeout(() => {
this._enablePopupToggles();
delete this._protectionsPopupToggleDelayTimer;
},
this._protectionsPopupButtonDelay);
}
ReportBrokenSite.updateParentMenu(event);
}
},
onPopupHidden(event) {
if (event.target ==
this._protectionsPopup) {
window.removeEventListener(
"focus",
this,
true);
this._protectionsPopupTPSwitch.removeEventListener(
"toggle",
this);
// Record close telemetry, don't record for toasts
if (!event.target.hasAttribute(
"toast")) {
Glean.securityUiProtectionspopup.closeProtectionsPopup.record({
openingReason:
this._protectionsPopupOpeningReason,
smartblockToggleClicked:
this._hasClickedSmartBlockEmbedToggle,
});
}
if (
this._protectionsPopupToggleDelayTimer) {
clearTimeout(
this._protectionsPopupToggleDelayTimer);
this._enablePopupToggles();
delete this._protectionsPopupToggleDelayTimer;
}
this._hasClickedSmartBlockEmbedToggle =
false;
this._protectionsPopupOpeningReason =
null;
}
},
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(trackerCount);
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;
--> --------------------
--> maximum size reached
--> --------------------