tor-browser

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

PromptParent.sys.mjs (12316B)


      1 /* vim: set ts=2 sw=2 et tw=80: */
      2 /* This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 const lazy = {};
      7 
      8 ChromeUtils.defineESModuleGetters(lazy, {
      9  PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
     10  BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
     11 });
     12 
     13 ChromeUtils.defineLazyGetter(lazy, "gTabBrowserLocalization", function () {
     14  return new Localization(["browser/tabbrowser.ftl"], true);
     15 });
     16 
     17 /**
     18 * @typedef {object} Dialog
     19 */
     20 
     21 /**
     22 * gBrowserDialogs weakly maps BrowsingContexts to a Map of their currently
     23 * active Dialogs.
     24 *
     25 * @type {WeakMap<BrowsingContext, Dialog>}
     26 */
     27 let gBrowserDialogs = new WeakMap();
     28 
     29 export class PromptParent extends JSWindowActorParent {
     30  didDestroy() {
     31    // In the event that the subframe or tab crashed, make sure that
     32    // we close any active Prompts.
     33    this.forceClosePrompts();
     34  }
     35 
     36  /**
     37   * Registers a new dialog to be tracked for a particular BrowsingContext.
     38   * We need to track a dialog so that we can, for example, force-close the
     39   * dialog if the originating subframe or tab unloads or crashes.
     40   *
     41   * @param {Dialog} dialog
     42   *        The dialog that will be shown to the user.
     43   * @param {string} id
     44   *        A unique ID to differentiate multiple dialogs coming from the same
     45   *        BrowsingContext.
     46   */
     47  registerDialog(dialog, id) {
     48    let dialogs = gBrowserDialogs.get(this.browsingContext);
     49    if (!dialogs) {
     50      dialogs = new Map();
     51      gBrowserDialogs.set(this.browsingContext, dialogs);
     52    }
     53 
     54    dialogs.set(id, dialog);
     55  }
     56 
     57  /**
     58   * Removes a Prompt for a BrowsingContext with a particular ID from the registry.
     59   * This needs to be done to avoid leaking <xul:browser>'s.
     60   *
     61   * @param {string} id
     62   *        A unique ID to differentiate multiple Prompts coming from the same
     63   *        BrowsingContext.
     64   */
     65  unregisterPrompt(id) {
     66    let dialogs = gBrowserDialogs.get(this.browsingContext);
     67    dialogs?.delete(id);
     68  }
     69 
     70  /**
     71   * Programmatically closes all Prompts for the current BrowsingContext.
     72   */
     73  forceClosePrompts() {
     74    let dialogs = gBrowserDialogs.get(this.browsingContext) || [];
     75 
     76    for (let [, dialog] of dialogs) {
     77      dialog?.abort();
     78    }
     79  }
     80 
     81  isAboutAddonsOptionsPage(browsingContext) {
     82    const { embedderWindowGlobal, name } = browsingContext;
     83    if (!embedderWindowGlobal) {
     84      // Return earlier if there is no embedder global, this is definitely
     85      // not an about:addons extensions options page.
     86      return false;
     87    }
     88 
     89    return (
     90      embedderWindowGlobal.documentPrincipal.isSystemPrincipal &&
     91      embedderWindowGlobal.documentURI.spec === "about:addons" &&
     92      name === "addon-inline-options"
     93    );
     94  }
     95 
     96  // Note that this will return false for the sidebar <browser> element
     97  // itself.
     98  isEmbeddedInSidebar(browser) {
     99    if (
    100      browser?.ownerGlobal?.browsingContext.embedderElement?.id != "sidebar"
    101    ) {
    102      return false;
    103    }
    104    // Extensions in the sidebar have more layers of nesting, and this causes
    105    // window leaks in tests. We would like to fix this at some point (bug 1513656)
    106    if (browser.getAttribute("messagemanagergroup") == "webext-browsers") {
    107      return false;
    108    }
    109    return true;
    110  }
    111 
    112  receiveMessage(message) {
    113    switch (message.name) {
    114      case "Prompt:Open":
    115        if (!this.windowContext.isActiveInTab) {
    116          return undefined;
    117        }
    118 
    119        return this.openPromptWithTabDialogBox(message.data);
    120    }
    121 
    122    return undefined;
    123  }
    124 
    125  /**
    126   * Opens either a window prompt or TabDialogBox at the content or tab level
    127   * for a BrowsingContext, and puts the associated browser in the modal state
    128   * until the prompt is closed.
    129   *
    130   * @param {object} args
    131   *        The arguments passed up from the BrowsingContext to be passed
    132   *        directly to the modal prompt.
    133   * @return {Promise<object>}
    134   *         Resolves with the arguments returned from the modal prompt.
    135   */
    136  async openPromptWithTabDialogBox(args) {
    137    const COMMON_DIALOG = "chrome://global/content/commonDialog.xhtml";
    138    const SELECT_DIALOG = "chrome://global/content/selectDialog.xhtml";
    139    let uri = args.promptType == "select" ? SELECT_DIALOG : COMMON_DIALOG;
    140 
    141    let browsingContext = this.browsingContext.top;
    142 
    143    let browser = browsingContext.embedderElement;
    144 
    145    let isEmbeddedInSidebar = this.isEmbeddedInSidebar(browser);
    146    if (isEmbeddedInSidebar || this.isAboutAddonsOptionsPage(browsingContext)) {
    147      browser = browser.ownerGlobal.browsingContext.embedderElement;
    148    }
    149 
    150    let promptRequiresBrowser =
    151      args.modalType === Services.prompt.MODAL_TYPE_TAB ||
    152      args.modalType === Services.prompt.MODAL_TYPE_CONTENT;
    153    if (promptRequiresBrowser && !browser) {
    154      let modal_type =
    155        args.modalType === Services.prompt.MODAL_TYPE_TAB ? "tab" : "content";
    156      throw new Error(`Cannot ${modal_type}-prompt without a browser!`);
    157    }
    158 
    159    const closingEventDetails =
    160      args.modalType === Services.prompt.MODAL_TYPE_CONTENT
    161        ? {
    162            owningBrowsingContext: this.browsingContext,
    163            promptType: args.inPermitUnload ? "beforeunload" : args.promptType,
    164          }
    165        : null;
    166 
    167    let win;
    168 
    169    // If we are a chrome actor we can use the associated chrome win.
    170    if (!browsingContext.isContent && browsingContext.window) {
    171      win = browsingContext.window;
    172    } else {
    173      win = browser?.ownerGlobal;
    174    }
    175 
    176    // There's a requirement for prompts to be blocked if a window is
    177    // passed and that window is hidden (eg, auth prompts are suppressed if the
    178    // passed window is the hidden window).
    179    // See bug 875157 comment 30 for more..
    180    if (win?.winUtils && !win.winUtils.isParentWindowMainWidgetVisible) {
    181      throw new Error("Cannot open a prompt in a hidden window");
    182    }
    183 
    184    try {
    185      if (browsingContext.embedderElement) {
    186        browsingContext.embedderElement.enterModalState();
    187        lazy.PromptUtils.fireDialogEvent(
    188          win,
    189          "DOMWillOpenModalDialog",
    190          browsingContext.embedderElement,
    191          this.getOpenEventDetail(args)
    192        );
    193      }
    194 
    195      args.promptAborted = false;
    196      args.openedWithTabDialog = true;
    197      args.owningBrowsingContext = this.browsingContext;
    198 
    199      // Convert args object to a prop bag for the dialog to consume.
    200      let bag;
    201 
    202      if (promptRequiresBrowser && win?.gBrowser?.getTabDialogBox) {
    203        // Tab or content level prompt
    204        let dialogBox = win.gBrowser.getTabDialogBox(browser);
    205 
    206        if (dialogBox._allowTabFocusByPromptPrincipal) {
    207          this.addTabSwitchCheckboxToArgs(dialogBox, args);
    208        }
    209 
    210        let currentLocationsTabLabel;
    211 
    212        let targetTab = win.gBrowser.getTabForBrowser(browser);
    213        if (
    214          !Services.prefs.getBoolPref(
    215            "privacy.authPromptSpoofingProtection",
    216            false
    217          )
    218        ) {
    219          args.isTopLevelCrossDomainAuth = false;
    220        }
    221        // Auth prompt spoofing protection, see bug 791594.
    222        if (args.isTopLevelCrossDomainAuth && targetTab) {
    223          // Set up the url bar with the url of the cross domain resource.
    224          // onLocationChange will change the url back to the current browsers
    225          // if we do not hold the state here.
    226          // onLocationChange will favour currentAuthPromptURI over the current browsers uri
    227          browser.currentAuthPromptURI = args.channel.URI;
    228          if (browser == win.gBrowser.selectedBrowser) {
    229            win.gURLBar.setURI();
    230          }
    231          // Set up the tab title for the cross domain resource.
    232          // We need to remember the original tab title in case
    233          // the load does not happen after the prompt, then we need to reset the tab title manually.
    234          currentLocationsTabLabel = targetTab.label;
    235          win.gBrowser.setTabLabelForAuthPrompts(
    236            targetTab,
    237            lazy.BrowserUtils.formatURIForDisplay(args.channel.URI)
    238          );
    239        }
    240        bag = lazy.PromptUtils.objectToPropBag(args);
    241        let promptID = args._remoteId;
    242        try {
    243          let { dialog, closedPromise } = dialogBox.open(
    244            uri,
    245            {
    246              features: "resizable=no",
    247              modalType: args.modalType,
    248              allowFocusCheckbox: args.allowFocusCheckbox,
    249              hideContent: args.isTopLevelCrossDomainAuth,
    250              // If we are in the sidebar, use the inner browser to detect when navigation is done
    251              webProgress: isEmbeddedInSidebar
    252                ? browsingContext?.webProgress
    253                : undefined,
    254            },
    255            bag
    256          );
    257          dialog.promptID = promptID;
    258          this.registerDialog(dialog, promptID);
    259          await closedPromise;
    260        } finally {
    261          if (args.isTopLevelCrossDomainAuth) {
    262            browser.currentAuthPromptURI = null;
    263            // If the user is stopping the page load before answering the prompt, no navigation will happen after the prompt
    264            // so we need to reset the uri and tab title here to the current browsers for that specific case
    265            if (browser == win.gBrowser.selectedBrowser) {
    266              win.gURLBar.setURI();
    267            }
    268            win.gBrowser.setTabLabelForAuthPrompts(
    269              targetTab,
    270              currentLocationsTabLabel
    271            );
    272          }
    273          this.unregisterPrompt(promptID);
    274        }
    275      } else {
    276        // Ensure we set the correct modal type at this point.
    277        // If we use window prompts as a fallback it may not be set.
    278        args.modalType = Services.prompt.MODAL_TYPE_WINDOW;
    279        // Window prompt
    280        bag = lazy.PromptUtils.objectToPropBag(args);
    281        Services.ww.openWindow(
    282          win,
    283          uri,
    284          "_blank",
    285          "centerscreen,chrome,modal,titlebar",
    286          bag
    287        );
    288      }
    289 
    290      lazy.PromptUtils.propBagToObject(bag, args);
    291    } finally {
    292      if (browsingContext.embedderElement) {
    293        browsingContext.embedderElement.maybeLeaveModalState();
    294        lazy.PromptUtils.fireDialogEvent(
    295          win,
    296          "DOMModalDialogClosed",
    297          browsingContext.embedderElement,
    298          closingEventDetails
    299            ? {
    300                ...closingEventDetails,
    301                areLeaving: args.ok,
    302                // If a prompt was not accepted, do not return the prompt value.
    303                value: args.ok ? args.value : null,
    304              }
    305            : null
    306        );
    307      }
    308    }
    309    return args;
    310  }
    311 
    312  getOpenEventDetail(args) {
    313    let details =
    314      args.modalType === Services.prompt.MODAL_TYPE_CONTENT
    315        ? {
    316            inPermitUnload: args.inPermitUnload,
    317            promptPrincipal: args.promptPrincipal,
    318            tabPrompt: true,
    319          }
    320        : null;
    321 
    322    return details;
    323  }
    324 
    325  /**
    326   * Set properties on `args` needed by the dialog to allow tab switching for the
    327   * page that opened the prompt.
    328   *
    329   * @param {TabDialogBox}  dialogBox
    330   *        The dialog to show the tab-switch checkbox for.
    331   * @param {object}  args
    332   *        The `args` object to set tab switching permission info on.
    333   */
    334  addTabSwitchCheckboxToArgs(dialogBox, args) {
    335    let allowTabFocusByPromptPrincipal =
    336      dialogBox._allowTabFocusByPromptPrincipal;
    337 
    338    if (
    339      allowTabFocusByPromptPrincipal &&
    340      args.modalType === Services.prompt.MODAL_TYPE_CONTENT
    341    ) {
    342      let domain = allowTabFocusByPromptPrincipal.addonPolicy?.name;
    343      try {
    344        domain ||= allowTabFocusByPromptPrincipal.URI.displayHostPort;
    345      } catch (ex) {
    346        /* Ignore exceptions from fetching the display host/port. */
    347      }
    348      // If it's still empty, use `prePath` so we have *something* to show:
    349      domain ||= allowTabFocusByPromptPrincipal.URI.prePath;
    350      let [allowFocusMsg] = lazy.gTabBrowserLocalization.formatMessagesSync([
    351        {
    352          id: "tabbrowser-allow-dialogs-to-get-focus",
    353          args: { domain },
    354        },
    355      ]);
    356      let labelAttr = allowFocusMsg.attributes.find(a => a.name == "label");
    357      if (labelAttr) {
    358        args.allowFocusCheckbox = true;
    359        args.checkLabel = labelAttr.value;
    360      }
    361    }
    362  }
    363 }