summaryrefslogtreecommitdiffstats
path: root/browser/actors/PromptParent.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/actors/PromptParent.sys.mjs')
-rw-r--r--browser/actors/PromptParent.sys.mjs468
1 files changed, 468 insertions, 0 deletions
diff --git a/browser/actors/PromptParent.sys.mjs b/browser/actors/PromptParent.sys.mjs
new file mode 100644
index 0000000000..1407e06a75
--- /dev/null
+++ b/browser/actors/PromptParent.sys.mjs
@@ -0,0 +1,468 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
+});
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "tabChromePromptSubDialog",
+ "prompts.tabChromePromptSubDialog",
+ false
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "contentPromptSubDialog",
+ "prompts.contentPromptSubDialog",
+ false
+);
+
+ChromeUtils.defineLazyGetter(lazy, "gTabBrowserLocalization", function () {
+ return new Localization(["browser/tabbrowser.ftl"], true);
+});
+
+/**
+ * @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();
+
+export 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();
+ }
+ }
+
+ isAboutAddonsOptionsPage(browsingContext) {
+ const { embedderWindowGlobal, name } = browsingContext;
+ if (!embedderWindowGlobal) {
+ // Return earlier if there is no embedder global, this is definitely
+ // not an about:addons extensions options page.
+ return false;
+ }
+
+ return (
+ embedderWindowGlobal.documentPrincipal.isSystemPrincipal &&
+ embedderWindowGlobal.documentURI.spec === "about:addons" &&
+ name === "addon-inline-options"
+ );
+ }
+
+ receiveMessage(message) {
+ let args = message.data;
+ let id = args._remoteId;
+
+ switch (message.name) {
+ case "Prompt:Open":
+ if (!this.windowContext.isActiveInTab) {
+ return undefined;
+ }
+
+ if (
+ (args.modalType === Ci.nsIPrompt.MODAL_TYPE_CONTENT &&
+ !lazy.contentPromptSubDialog) ||
+ (args.modalType === Ci.nsIPrompt.MODAL_TYPE_TAB &&
+ !lazy.tabChromePromptSubDialog) ||
+ this.isAboutAddonsOptionsPage(this.browsingContext)
+ ) {
+ return this.openContentPrompt(args, id);
+ }
+ return this.openPromptWithTabDialogBox(args);
+ }
+
+ return undefined;
+ }
+
+ /**
+ * Opens a TabModalPrompt for a BrowsingContext, and puts the associated browser
+ * in the modal state until the TabModalPrompt is closed.
+ *
+ * @param {Object} args
+ * The arguments passed up from the BrowsingContext to be passed directly
+ * to the TabModalPrompt.
+ * @param {string} id
+ * A unique ID to differentiate multiple Prompts coming from the same
+ * BrowsingContext.
+ * @return {Promise}
+ * Resolves when the TabModalPrompt is dismissed.
+ * @resolves {Object}
+ * The arguments returned from the TabModalPrompt.
+ */
+ openContentPrompt(args, id) {
+ let browser = this.browsingContext.top.embedderElement;
+ if (!browser) {
+ throw new Error("Cannot tab-prompt without a browser!");
+ }
+ let window = browser.ownerGlobal;
+ let tabPrompt = window.gBrowser.getTabModalPromptBox(browser);
+ let newPrompt;
+ let needRemove = false;
+
+ // If the page which called the prompt is different from the the top context
+ // where we show the prompt, ask the prompt implementation to display the origin.
+ // For example, this can happen if a cross origin subframe shows a prompt.
+ args.showCallerOrigin =
+ args.promptPrincipal &&
+ !browser.contentPrincipal.equals(args.promptPrincipal);
+
+ let onPromptClose = () => {
+ let promptData = gBrowserPrompts.get(this.browsingContext);
+ if (!promptData || !promptData.has(id)) {
+ throw new Error(
+ "Failed to close a prompt since it wasn't registered for some reason."
+ );
+ }
+
+ let { resolver, tabModalPrompt } = promptData.get(id);
+ // It's possible that we removed the prompt during the
+ // appendPrompt call below. In that case, newPrompt will be
+ // undefined. We set the needRemove flag to remember to remove
+ // it right after we've finished adding it.
+ if (tabModalPrompt) {
+ tabPrompt.removePrompt(tabModalPrompt);
+ } else {
+ needRemove = true;
+ }
+
+ this.unregisterPrompt(id);
+
+ lazy.PromptUtils.fireDialogEvent(
+ window,
+ "DOMModalDialogClosed",
+ browser,
+ this.getClosingEventDetail(args)
+ );
+ resolver(args);
+ browser.maybeLeaveModalState();
+ };
+
+ try {
+ browser.enterModalState();
+ lazy.PromptUtils.fireDialogEvent(
+ window,
+ "DOMWillOpenModalDialog",
+ browser,
+ this.getOpenEventDetail(args)
+ );
+
+ args.promptActive = true;
+
+ newPrompt = tabPrompt.appendPrompt(args, onPromptClose);
+ let promise = this.registerPrompt(newPrompt, id);
+
+ if (needRemove) {
+ tabPrompt.removePrompt(newPrompt);
+ }
+
+ return promise;
+ } catch (ex) {
+ console.error(ex);
+ onPromptClose(true);
+ }
+
+ return null;
+ }
+
+ /**
+ * Opens either a window prompt or TabDialogBox at the content or tab level
+ * 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 prompt.
+ * @return {Promise}
+ * Resolves when the modal prompt is dismissed.
+ * @resolves {Object}
+ * The arguments returned from the modal prompt.
+ */
+ async openPromptWithTabDialogBox(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 promptRequiresBrowser =
+ args.modalType === Services.prompt.MODAL_TYPE_TAB ||
+ args.modalType === Services.prompt.MODAL_TYPE_CONTENT;
+ if (promptRequiresBrowser && !browser) {
+ let modal_type =
+ args.modalType === Services.prompt.MODAL_TYPE_TAB ? "tab" : "content";
+ throw new Error(`Cannot ${modal_type}-prompt without a browser!`);
+ }
+
+ 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 open a prompt in a hidden window");
+ }
+
+ try {
+ if (browser) {
+ browser.enterModalState();
+ lazy.PromptUtils.fireDialogEvent(
+ win,
+ "DOMWillOpenModalDialog",
+ browser,
+ this.getOpenEventDetail(args)
+ );
+ }
+
+ args.promptAborted = false;
+ args.openedWithTabDialog = true;
+ args.owningBrowsingContext = this.browsingContext;
+
+ // Convert args object to a prop bag for the dialog to consume.
+ let bag;
+
+ if (promptRequiresBrowser && win?.gBrowser?.getTabDialogBox) {
+ // Tab or content level prompt
+ let dialogBox = win.gBrowser.getTabDialogBox(browser);
+
+ if (dialogBox._allowTabFocusByPromptPrincipal) {
+ this.addTabSwitchCheckboxToArgs(dialogBox, args);
+ }
+
+ let currentLocationsTabLabel;
+
+ let targetTab = win.gBrowser.getTabForBrowser(browser);
+ if (
+ !Services.prefs.getBoolPref(
+ "privacy.authPromptSpoofingProtection",
+ false
+ )
+ ) {
+ args.isTopLevelCrossDomainAuth = false;
+ }
+ // Auth prompt spoofing protection, see bug 791594.
+ if (args.isTopLevelCrossDomainAuth && targetTab) {
+ // Set up the url bar with the url of the cross domain resource.
+ // onLocationChange will change the url back to the current browsers
+ // if we do not hold the state here.
+ // onLocationChange will favour currentAuthPromptURI over the current browsers uri
+ browser.currentAuthPromptURI = args.channel.URI;
+ if (browser == win.gBrowser.selectedBrowser) {
+ win.gURLBar.setURI();
+ }
+ // Set up the tab title for the cross domain resource.
+ // We need to remember the original tab title in case
+ // the load does not happen after the prompt, then we need to reset the tab title manually.
+ currentLocationsTabLabel = targetTab.label;
+ win.gBrowser.setTabLabelForAuthPrompts(
+ targetTab,
+ lazy.BrowserUtils.formatURIForDisplay(args.channel.URI)
+ );
+ }
+ bag = lazy.PromptUtils.objectToPropBag(args);
+ try {
+ await dialogBox.open(
+ uri,
+ {
+ features: "resizable=no",
+ modalType: args.modalType,
+ allowFocusCheckbox: args.allowFocusCheckbox,
+ hideContent: args.isTopLevelCrossDomainAuth,
+ },
+ bag
+ ).closedPromise;
+ } finally {
+ if (args.isTopLevelCrossDomainAuth) {
+ browser.currentAuthPromptURI = null;
+ // If the user is stopping the page load before answering the prompt, no navigation will happen after the prompt
+ // so we need to reset the uri and tab title here to the current browsers for that specific case
+ if (browser == win.gBrowser.selectedBrowser) {
+ win.gURLBar.setURI();
+ }
+ win.gBrowser.setTabLabelForAuthPrompts(
+ targetTab,
+ currentLocationsTabLabel
+ );
+ }
+ }
+ } else {
+ // Ensure we set the correct modal type at this point.
+ // If we use window prompts as a fallback it may not be set.
+ args.modalType = Services.prompt.MODAL_TYPE_WINDOW;
+ // Window prompt
+ 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.maybeLeaveModalState();
+ lazy.PromptUtils.fireDialogEvent(
+ win,
+ "DOMModalDialogClosed",
+ browser,
+ this.getClosingEventDetail(args)
+ );
+ }
+ }
+ return args;
+ }
+
+ getClosingEventDetail(args) {
+ let details =
+ args.modalType === Services.prompt.MODAL_TYPE_CONTENT
+ ? {
+ wasPermitUnload: args.inPermitUnload,
+ areLeaving: args.ok,
+ // If a prompt was not accepted, do not return the prompt value.
+ value: args.ok ? args.value : null,
+ }
+ : null;
+
+ return details;
+ }
+
+ getOpenEventDetail(args) {
+ let details =
+ args.modalType === Services.prompt.MODAL_TYPE_CONTENT
+ ? {
+ inPermitUnload: args.inPermitUnload,
+ promptPrincipal: args.promptPrincipal,
+ tabPrompt: true,
+ }
+ : null;
+
+ return details;
+ }
+
+ /**
+ * Set properties on `args` needed by the dialog to allow tab switching for the
+ * page that opened the prompt.
+ *
+ * @param {TabDialogBox} dialogBox
+ * The dialog to show the tab-switch checkbox for.
+ * @param {Object} args
+ * The `args` object to set tab switching permission info on.
+ */
+ addTabSwitchCheckboxToArgs(dialogBox, args) {
+ let allowTabFocusByPromptPrincipal =
+ dialogBox._allowTabFocusByPromptPrincipal;
+
+ if (
+ allowTabFocusByPromptPrincipal &&
+ args.modalType === Services.prompt.MODAL_TYPE_CONTENT
+ ) {
+ let domain = allowTabFocusByPromptPrincipal.addonPolicy?.name;
+ try {
+ domain ||= allowTabFocusByPromptPrincipal.URI.displayHostPort;
+ } catch (ex) {
+ /* Ignore exceptions from fetching the display host/port. */
+ }
+ // If it's still empty, use `prePath` so we have *something* to show:
+ domain ||= allowTabFocusByPromptPrincipal.URI.prePath;
+ let [allowFocusMsg] = lazy.gTabBrowserLocalization.formatMessagesSync([
+ {
+ id: "tabbrowser-allow-dialogs-to-get-focus",
+ args: { domain },
+ },
+ ]);
+ let labelAttr = allowFocusMsg.attributes.find(a => a.name == "label");
+ if (labelAttr) {
+ args.allowFocusCheckbox = true;
+ args.checkLabel = labelAttr.value;
+ }
+ }
+ }
+}