diff options
Diffstat (limited to 'browser/actors/PromptParent.sys.mjs')
-rw-r--r-- | browser/actors/PromptParent.sys.mjs | 465 |
1 files changed, 465 insertions, 0 deletions
diff --git a/browser/actors/PromptParent.sys.mjs b/browser/actors/PromptParent.sys.mjs new file mode 100644 index 0000000000..76eca1a66f --- /dev/null +++ b/browser/actors/PromptParent.sys.mjs @@ -0,0 +1,465 @@ +/* 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 +); + +XPCOMUtils.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.isCurrentGlobal) { + 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; + + // 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, + } + : 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; + } + } + } +} |