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