PopupAndRedirectBlockerObserver.sys.mjs (13043B)
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 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 6 7 export var PopupAndRedirectBlockerObserver = { 8 /** 9 * Check if we are currently in the process of appending a notification. 10 * We can't rely on `getNotificationWithValue()`: It returns `null` 11 * while `appendNotification()` is resolving, so we keep track of the 12 * promise instead. 13 */ 14 mNotificationPromise: null, 15 16 handleEvent(aEvent) { 17 switch (aEvent.type) { 18 case "DOMUpdateBlockedPopups": 19 this.onDOMUpdateBlockedPopupsAndRedirect(aEvent); 20 break; 21 case "DOMUpdateBlockedRedirect": 22 this.onDOMUpdateBlockedPopupsAndRedirect(aEvent); 23 break; 24 case "command": 25 this.onCommand(aEvent); 26 break; 27 case "popupshowing": 28 this.onPopupShowing(aEvent); 29 break; 30 case "popuphiding": 31 this.onPopupHiding(aEvent); 32 break; 33 } 34 }, 35 36 /** 37 * Handles a "DOMUpdateBlockedPopups" or "DOMUpdateBlockedRedirect" event 38 * received from the JSWindowActorParent. 39 * 40 * @param {*} aEvent 41 */ 42 onDOMUpdateBlockedPopupsAndRedirect(aEvent) { 43 const window = aEvent.originalTarget.ownerGlobal; 44 const { gBrowser, gPermissionPanel } = window; 45 if (aEvent.originalTarget != gBrowser.selectedBrowser) { 46 return; 47 } 48 49 gPermissionPanel.refreshPermissionIcons(); 50 51 const popupCount = 52 gBrowser.selectedBrowser.popupAndRedirectBlocker.getBlockedPopupCount(); 53 const isRedirectBlocked = 54 gBrowser.selectedBrowser.popupAndRedirectBlocker.isRedirectBlocked(); 55 if (!popupCount && !isRedirectBlocked) { 56 this.hideNotification(gBrowser); 57 return; 58 } 59 60 if (Services.prefs.getBoolPref("privacy.popups.showBrowserMessage")) { 61 this.ensureInitializedForWindow(window); 62 this.showBrowserMessage(gBrowser, popupCount, isRedirectBlocked); 63 } 64 }, 65 66 hideNotification(aBrowser) { 67 const notificationBox = aBrowser.getNotificationBox(); 68 const notification = 69 notificationBox.getNotificationWithValue("popup-blocked"); 70 if (notification) { 71 notificationBox.removeNotification(notification); 72 } 73 }, 74 75 ensureInitializedForWindow(aWindow) { 76 const popup = aWindow.document.getElementById("blockedPopupOptions"); 77 // Make sure we don't add the same event handlers multiple times. 78 if (popup.getAttribute("initialized")) { 79 return; 80 } 81 82 popup.setAttribute("initialized", true); 83 popup.addEventListener("command", this); 84 popup.addEventListener("popupshowing", this); 85 popup.addEventListener("popuphiding", this); 86 }, 87 88 async showBrowserMessage(aBrowser, aPopupCount, aIsRedirectBlocked) { 89 const selectedBrowser = aBrowser.selectedBrowser; 90 const popupAndRedirectBlocker = selectedBrowser.popupAndRedirectBlocker; 91 92 // Check if the notification was previously shown and then dismissed 93 // by the user. 94 if (popupAndRedirectBlocker.hasBeenDismissed()) { 95 return; 96 } 97 98 const l10nId = (() => { 99 if (aPopupCount >= this.maxReportedPopups) { 100 return aIsRedirectBlocked 101 ? "popup-warning-exceeded-with-redirect-message" 102 : "popup-warning-exceeded-message"; 103 } 104 105 return aIsRedirectBlocked 106 ? "redirect-warning-with-popup-message" 107 : "popup-warning-message"; 108 })(); 109 const label = { 110 "l10n-id": l10nId, 111 "l10n-args": { 112 popupCount: aPopupCount, 113 }, 114 }; 115 const notificationBox = aBrowser.getNotificationBox(); 116 const notification = this.mNotificationPromise 117 ? await this.mNotificationPromise 118 : notificationBox.getNotificationWithValue("popup-blocked"); 119 if (notification) { 120 notification.label = label; 121 return; 122 } 123 124 const image = "chrome://browser/skin/notification-icons/popup.svg"; 125 const priority = notificationBox.PRIORITY_INFO_MEDIUM; 126 const eventCallback = popupAndRedirectBlocker.eventCallback.bind( 127 popupAndRedirectBlocker 128 ); 129 130 this.mNotificationPromise = notificationBox.appendNotification( 131 "popup-blocked", 132 { label, image, priority, eventCallback }, 133 [ 134 { 135 "l10n-id": "popup-warning-button", 136 popup: "blockedPopupOptions", 137 callback: null, 138 }, 139 ] 140 ); 141 await this.mNotificationPromise; 142 this.mNotificationPromise = null; 143 }, 144 145 /** 146 * Event handler that is triggered when a user clicks on the "options" 147 * button in the notification which opens a popup. 148 * 149 * @param {*} aEvent 150 */ 151 async onPopupShowing(aEvent) { 152 const window = aEvent.originalTarget.ownerGlobal; 153 const { gBrowser, document } = window; 154 155 // We get `uriHost` from the principal whenever possible and fall 156 // back to the `spec` for special pages without a host, e.g. "about:". 157 const browser = gBrowser.selectedBrowser; 158 const uriOrPrincipal = browser.isContentPrincipal 159 ? browser.contentPrincipal 160 : browser.currentURI; 161 const uriHost = uriOrPrincipal.asciiHost 162 ? uriOrPrincipal.displayHost 163 : uriOrPrincipal.spec; 164 165 // "Allow pop-ups for site..." 166 const blockedPopupAllowSite = document.getElementById( 167 "blockedPopupAllowSite" 168 ); 169 blockedPopupAllowSite.removeAttribute("hidden"); 170 document.l10n.setAttributes( 171 blockedPopupAllowSite, 172 "popups-infobar-allow2", 173 { uriHost } 174 ); 175 176 // "Dont show this message when..." 177 const blockedPopupDontShowMessage = document.getElementById( 178 "blockedPopupDontShowMessage" 179 ); 180 blockedPopupDontShowMessage.setAttribute("checked", false); 181 182 gBrowser.selectedBrowser.popupAndRedirectBlocker 183 .getBlockedRedirect() 184 .then(blockedRedirect => { 185 this.onPopupShowingBlockedRedirect(blockedRedirect, window); 186 }); 187 gBrowser.selectedBrowser.popupAndRedirectBlocker 188 .getBlockedPopups() 189 .then(blockedPopups => { 190 this.onPopupShowingBlockedPopups(blockedPopups, window); 191 }); 192 }, 193 194 onPopupShowingBlockedRedirect(aBlockedRedirect, aWindow) { 195 const { gBrowser, document } = aWindow; 196 const browser = gBrowser.selectedBrowser; 197 198 const blockedRedirectSeparator = document.getElementById( 199 "blockedRedirectSeparator" 200 ); 201 blockedRedirectSeparator.hidden = !aBlockedRedirect; 202 203 if (!aBlockedRedirect) { 204 return; 205 } 206 207 // We may end up in a race condition and to avoid showing duplicate 208 // items, make sure the list is actually empty. 209 const nextElement = blockedRedirectSeparator.nextElementSibling; 210 if (nextElement?.hasAttribute("redirectInnerWindowId")) { 211 return; 212 } 213 214 const menuitem = document.createXULElement("menuitem"); 215 document.l10n.setAttributes(menuitem, "popup-trigger-redirect-menuitem", { 216 redirectURI: aBlockedRedirect.redirectURISpec, 217 }); 218 menuitem.setAttribute("redirectURISpec", aBlockedRedirect.redirectURISpec); 219 // Store the source inner window id, so we can check if the document 220 // that triggered the redirect is still the same. 221 menuitem.setAttribute( 222 "redirectInnerWindowId", 223 aBlockedRedirect.innerWindowId 224 ); 225 // Store the browser for the current tab. The active tab may change, 226 // so we keep a reference to it. 227 menuitem.browser = browser; 228 // Same reason as source inner window id, we compare it with the one 229 // of the browsing context at the time of unblocking. 230 menuitem.browsingContext = aBlockedRedirect.browsingContext; 231 232 blockedRedirectSeparator.after(menuitem); 233 }, 234 235 onPopupShowingBlockedPopups(aBlockedPopups, aWindow) { 236 const { gBrowser, document } = aWindow; 237 const browser = gBrowser.selectedBrowser; 238 239 const blockedPopupsSeparator = document.getElementById( 240 "blockedPopupsSeparator" 241 ); 242 blockedPopupsSeparator.hidden = !aBlockedPopups.length; 243 244 if (!aBlockedPopups.length) { 245 return; 246 } 247 248 // We may end up in a race condition and to avoid showing duplicate 249 // items, make sure the list is actually empty. 250 const nextElement = blockedPopupsSeparator.nextElementSibling; 251 if (nextElement?.hasAttribute("popupInnerWindowId")) { 252 return; 253 } 254 255 for (let i = 0; i < aBlockedPopups.length; ++i) { 256 const blockedPopup = aBlockedPopups[i]; 257 258 const menuitem = document.createXULElement("menuitem"); 259 document.l10n.setAttributes(menuitem, "popup-show-popup-menuitem", { 260 popupURI: blockedPopup.popupWindowURISpec, 261 }); 262 menuitem.setAttribute("popupReportIndex", i); 263 // Store the source inner window id, so we can check if the document 264 // that triggered the redirect is still the same. 265 menuitem.setAttribute("popupInnerWindowId", blockedPopup.innerWindowId); 266 // Store the browser for the current tab. The active tab may change, 267 // so we keep a reference to it. 268 menuitem.browser = browser; 269 // Same reason as source inner window id, we compare it with the one 270 // of the browsing context at the time of unblocking. 271 menuitem.browsingContext = blockedPopup.browsingContext; 272 273 blockedPopupsSeparator.after(menuitem); 274 } 275 }, 276 277 /** 278 * Event handler that is triggered when the "options" popup of the 279 * notification closes. 280 * 281 * @param {*} aEvent 282 */ 283 onPopupHiding(aEvent) { 284 const window = aEvent.originalTarget.ownerGlobal; 285 const { document } = window; 286 287 // Remove the blocked redirect, if any. 288 const blockedRedirectSeparator = document.getElementById( 289 "blockedRedirectSeparator" 290 ); 291 let item = blockedRedirectSeparator.nextElementSibling; 292 if (item?.hasAttribute("redirectInnerWindowId")) { 293 item.remove(); 294 } 295 296 // Remove the blocked popups, if any. 297 const blockedPopupsSeparator = document.getElementById( 298 "blockedPopupsSeparator" 299 ); 300 let next = null; 301 for ( 302 item = blockedPopupsSeparator.nextElementSibling; 303 item?.hasAttribute("popupInnerWindowId"); 304 item = next 305 ) { 306 next = item.nextElementSibling; 307 item.remove(); 308 } 309 }, 310 311 /** 312 * Event handler that is triggered when a user clicks on one of the 313 * fields in the "options" popup of the notification. 314 * 315 * @param {*} aEvent 316 */ 317 onCommand(aEvent) { 318 if (aEvent.target.hasAttribute("popupReportIndex")) { 319 this.showBlockedPopup(aEvent); 320 return; 321 } 322 323 if (aEvent.target.hasAttribute("redirectURISpec")) { 324 this.navigateToBlockedRedirect(aEvent); 325 return; 326 } 327 328 switch (aEvent.target.id) { 329 case "blockedPopupAllowSite": 330 this.toggleAllowPopupsForSite(aEvent); 331 break; 332 case "blockedPopupEdit": 333 this.editPopupSettings(aEvent); 334 break; 335 case "blockedPopupDontShowMessage": 336 this.dontShowMessage(aEvent); 337 break; 338 } 339 }, 340 341 showBlockedPopup(aEvent) { 342 const { browser, browsingContext } = aEvent.target; 343 const innerWindowId = aEvent.target.getAttribute("popupInnerWindowId"); 344 const popupReportIndex = aEvent.target.getAttribute("popupReportIndex"); 345 346 browser.popupAndRedirectBlocker.unblockPopup( 347 browsingContext, 348 innerWindowId, 349 popupReportIndex 350 ); 351 }, 352 353 navigateToBlockedRedirect(aEvent) { 354 const { browser, browsingContext } = aEvent.target; 355 const innerWindowId = aEvent.target.getAttribute("redirectInnerWindowId"); 356 const redirectURISpec = aEvent.target.getAttribute("redirectURISpec"); 357 358 browser.popupAndRedirectBlocker.unblockRedirect( 359 browsingContext, 360 innerWindowId, 361 redirectURISpec 362 ); 363 }, 364 365 async toggleAllowPopupsForSite(aEvent) { 366 const window = aEvent.originalTarget.ownerGlobal; 367 const { gBrowser } = window; 368 369 // The toggle should only be visible (and therefore clickable) if 370 // popups are currently blocked. 371 Services.perms.addFromPrincipal( 372 gBrowser.contentPrincipal, 373 "popup", 374 Services.perms.ALLOW_ACTION 375 ); 376 gBrowser.getNotificationBox().removeCurrentNotification(); 377 378 // The order is important here. We want to unblock all popups of the 379 // current document first and then potentially redirect somewhere 380 // else. 381 await gBrowser.selectedBrowser.popupAndRedirectBlocker.unblockAllPopups(); 382 await gBrowser.selectedBrowser.popupAndRedirectBlocker.unblockFirstRedirect(); 383 }, 384 385 editPopupSettings(aEvent) { 386 const window = aEvent.originalTarget.ownerGlobal; 387 const { openPreferences } = window; 388 389 openPreferences("privacy-permissions-block-popups"); 390 }, 391 392 dontShowMessage(aEvent) { 393 const window = aEvent.originalTarget.ownerGlobal; 394 const { gBrowser } = window; 395 396 Services.prefs.setBoolPref("privacy.popups.showBrowserMessage", false); 397 gBrowser.getNotificationBox().removeCurrentNotification(); 398 }, 399 }; 400 401 XPCOMUtils.defineLazyPreferenceGetter( 402 PopupAndRedirectBlockerObserver, 403 "maxReportedPopups", 404 "privacy.popups.maxReported" 405 );