mail/actors/PromptParent.jsm
author Richard Marti <richard.marti@gmail.com>
Fri, 01 Jul 2022 17:46:48 +0000
changeset 36182 811db43a95de1aaf2bd15afb4bcf06d609603f10
parent 36024 5833586a5bd02a939aca190d47bd86469bc8876e
permissions -rw-r--r--
Bug 1777722 - Port bug 1777563: Make about:support point to about:processes instead of about:performance. r=#thunderbird-reviewers Differential Revision: https://phabricator.services.mozilla.com/D150868

/* vim: set ts=2 sw=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

var EXPORTED_SYMBOLS = ["PromptParent"];

const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");

const lazy = {};

ChromeUtils.defineModuleGetter(
  lazy,
  "PromptUtils",
  "resource://gre/modules/SharedPromptUtils.jsm"
);

/**
 * @typedef {Object} Prompt
 * @property {Function} resolver
 *           The resolve function to be called with the data from the Prompt
 *           after the user closes it.
 * @property {Object} tabModalPrompt
 *           The TabModalPrompt being shown to the user.
 */

/**
 * gBrowserPrompts weakly maps BrowsingContexts to a Map of their currently
 * active Prompts.
 *
 * @type {WeakMap<BrowsingContext, Prompt>}
 */
let gBrowserPrompts = new WeakMap();

class PromptParent extends JSWindowActorParent {
  didDestroy() {
    // In the event that the subframe or tab crashed, make sure that
    // we close any active Prompts.
    this.forceClosePrompts();
  }

  /**
   * Registers a new Prompt to be tracked for a particular BrowsingContext.
   * We need to track a Prompt so that we can, for example, force-close the
   * TabModalPrompt if the originating subframe or tab unloads or crashes.
   *
   * @param {Object} tabModalPrompt
   *        The TabModalPrompt that will be shown to the user.
   * @param {string} id
   *        A unique ID to differentiate multiple Prompts coming from the same
   *        BrowsingContext.
   * @return {Promise}
   * @resolves {Object}
   *           Resolves with the arguments returned from the TabModalPrompt when it
   *           is dismissed.
   */
  registerPrompt(tabModalPrompt, id) {
    let prompts = gBrowserPrompts.get(this.browsingContext);
    if (!prompts) {
      prompts = new Map();
      gBrowserPrompts.set(this.browsingContext, prompts);
    }

    let promise = new Promise(resolve => {
      prompts.set(id, {
        tabModalPrompt,
        resolver: resolve,
      });
    });

    return promise;
  }

  /**
   * Removes a Prompt for a BrowsingContext with a particular ID from the registry.
   * This needs to be done to avoid leaking <xul:browser>'s.
   *
   * @param {string} id
   *        A unique ID to differentiate multiple Prompts coming from the same
   *        BrowsingContext.
   */
  unregisterPrompt(id) {
    let prompts = gBrowserPrompts.get(this.browsingContext);
    if (prompts) {
      prompts.delete(id);
    }
  }

  /**
   * Programmatically closes all Prompts for the current BrowsingContext.
   */
  forceClosePrompts() {
    let prompts = gBrowserPrompts.get(this.browsingContext) || [];

    for (let [, prompt] of prompts) {
      prompt.tabModalPrompt && prompt.tabModalPrompt.abortPrompt();
    }
  }

  receiveMessage(message) {
    let args = message.data;

    switch (message.name) {
      case "Prompt:Open": {
        return this.openWindowPrompt(args);
      }
    }

    return undefined;
  }

  /**
   * Opens a window prompt for a BrowsingContext, and puts the associated
   * browser in the modal state until the prompt is closed.
   *
   * @param {Object} args
   *        The arguments passed up from the BrowsingContext to be passed
   *        directly to the modal window.
   * @return {Promise}
   *         Resolves when the window prompt is dismissed.
   * @resolves {Object}
   *           The arguments returned from the window prompt.
   */
  async openWindowPrompt(args) {
    const COMMON_DIALOG = "chrome://global/content/commonDialog.xhtml";
    const SELECT_DIALOG = "chrome://global/content/selectDialog.xhtml";
    let uri = args.promptType == "select" ? SELECT_DIALOG : COMMON_DIALOG;

    let browsingContext = this.browsingContext.top;

    let browser = browsingContext.embedderElement;
    let win;

    // If we are a chrome actor we can use the associated chrome win.
    if (!browsingContext.isContent && browsingContext.window) {
      win = browsingContext.window;
    } else {
      win = browser?.ownerGlobal;
    }

    // There's a requirement for prompts to be blocked if a window is
    // passed and that window is hidden (eg, auth prompts are suppressed if the
    // passed window is the hidden window).
    // See bug 875157 comment 30 for more..
    if (win?.winUtils && !win.winUtils.isParentWindowMainWidgetVisible) {
      throw new Error("Cannot call openModalWindow on a hidden window");
    }

    try {
      if (browser) {
        // The compose editor does not support enter/leaveModalState.
        browser.enterModalState?.();
        lazy.PromptUtils.fireDialogEvent(
          win,
          "DOMWillOpenModalDialog",
          browser
        );
      }

      let bag = lazy.PromptUtils.objectToPropBag(args);

      Services.ww.openWindow(
        win,
        uri,
        "_blank",
        "centerscreen,chrome,modal,titlebar",
        bag
      );

      lazy.PromptUtils.propBagToObject(bag, args);
    } finally {
      if (browser) {
        browser.leaveModalState?.();
        lazy.PromptUtils.fireDialogEvent(win, "DOMModalDialogClosed", browser);
      }
    }
    return args;
  }
}