tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 );