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