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/. */
8 ChromeUtils.defineESModuleGetters(lazy, {
9 PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
10 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
12 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
14 XPCOMUtils.defineLazyPreferenceGetter(
16 "tabChromePromptSubDialog",
17 "prompts.tabChromePromptSubDialog",
21 XPCOMUtils.defineLazyPreferenceGetter(
23 "contentPromptSubDialog",
24 "prompts.contentPromptSubDialog",
28 ChromeUtils.defineLazyGetter(lazy, "gTabBrowserLocalization", function () {
29 return new Localization(["browser/tabbrowser.ftl"], true);
33 * @typedef {Object} Prompt
34 * @property {Function} resolver
35 * The resolve function to be called with the data from the Prompt
36 * after the user closes it.
37 * @property {Object} tabModalPrompt
38 * The TabModalPrompt being shown to the user.
42 * gBrowserPrompts weakly maps BrowsingContexts to a Map of their currently
45 * @type {WeakMap<BrowsingContext, Prompt>}
47 let gBrowserPrompts = new WeakMap();
49 export class PromptParent extends JSWindowActorParent {
51 // In the event that the subframe or tab crashed, make sure that
52 // we close any active Prompts.
53 this.forceClosePrompts();
57 * Registers a new Prompt to be tracked for a particular BrowsingContext.
58 * We need to track a Prompt so that we can, for example, force-close the
59 * TabModalPrompt if the originating subframe or tab unloads or crashes.
61 * @param {Object} tabModalPrompt
62 * The TabModalPrompt that will be shown to the user.
64 * A unique ID to differentiate multiple Prompts coming from the same
68 * Resolves with the arguments returned from the TabModalPrompt when it
71 registerPrompt(tabModalPrompt, id) {
72 let prompts = gBrowserPrompts.get(this.browsingContext);
75 gBrowserPrompts.set(this.browsingContext, prompts);
78 let promise = new Promise(resolve => {
89 * Removes a Prompt for a BrowsingContext with a particular ID from the registry.
90 * This needs to be done to avoid leaking <xul:browser>'s.
93 * A unique ID to differentiate multiple Prompts coming from the same
96 unregisterPrompt(id) {
97 let prompts = gBrowserPrompts.get(this.browsingContext);
104 * Programmatically closes all Prompts for the current BrowsingContext.
106 forceClosePrompts() {
107 let prompts = gBrowserPrompts.get(this.browsingContext) || [];
109 for (let [, prompt] of prompts) {
110 prompt.tabModalPrompt && prompt.tabModalPrompt.abortPrompt();
114 isAboutAddonsOptionsPage(browsingContext) {
115 const { embedderWindowGlobal, name } = browsingContext;
116 if (!embedderWindowGlobal) {
117 // Return earlier if there is no embedder global, this is definitely
118 // not an about:addons extensions options page.
123 embedderWindowGlobal.documentPrincipal.isSystemPrincipal &&
124 embedderWindowGlobal.documentURI.spec === "about:addons" &&
125 name === "addon-inline-options"
129 receiveMessage(message) {
130 let args = message.data;
131 let id = args._remoteId;
133 switch (message.name) {
136 (args.modalType === Ci.nsIPrompt.MODAL_TYPE_CONTENT &&
137 !lazy.contentPromptSubDialog) ||
138 (args.modalType === Ci.nsIPrompt.MODAL_TYPE_TAB &&
139 !lazy.tabChromePromptSubDialog) ||
140 this.isAboutAddonsOptionsPage(this.browsingContext)
142 return this.openContentPrompt(args, id);
144 return this.openPromptWithTabDialogBox(args);
151 * Opens a TabModalPrompt for a BrowsingContext, and puts the associated browser
152 * in the modal state until the TabModalPrompt is closed.
154 * @param {Object} args
155 * The arguments passed up from the BrowsingContext to be passed directly
156 * to the TabModalPrompt.
158 * A unique ID to differentiate multiple Prompts coming from the same
161 * Resolves when the TabModalPrompt is dismissed.
163 * The arguments returned from the TabModalPrompt.
165 openContentPrompt(args, id) {
166 let browser = this.browsingContext.top.embedderElement;
168 throw new Error("Cannot tab-prompt without a browser!");
170 let window = browser.ownerGlobal;
171 let tabPrompt = window.gBrowser.getTabModalPromptBox(browser);
173 let needRemove = false;
175 // If the page which called the prompt is different from the the top context
176 // where we show the prompt, ask the prompt implementation to display the origin.
177 // For example, this can happen if a cross origin subframe shows a prompt.
178 args.showCallerOrigin =
179 args.promptPrincipal &&
180 !browser.contentPrincipal.equals(args.promptPrincipal);
182 let onPromptClose = () => {
183 let promptData = gBrowserPrompts.get(this.browsingContext);
184 if (!promptData || !promptData.has(id)) {
186 "Failed to close a prompt since it wasn't registered for some reason."
190 let { resolver, tabModalPrompt } = promptData.get(id);
191 // It's possible that we removed the prompt during the
192 // appendPrompt call below. In that case, newPrompt will be
193 // undefined. We set the needRemove flag to remember to remove
194 // it right after we've finished adding it.
195 if (tabModalPrompt) {
196 tabPrompt.removePrompt(tabModalPrompt);
201 this.unregisterPrompt(id);
203 lazy.PromptUtils.fireDialogEvent(
205 "DOMModalDialogClosed",
207 this.getClosingEventDetail(args)
210 browser.maybeLeaveModalState();
214 browser.enterModalState();
215 lazy.PromptUtils.fireDialogEvent(
217 "DOMWillOpenModalDialog",
219 this.getOpenEventDetail(args)
222 args.promptActive = true;
224 newPrompt = tabPrompt.appendPrompt(args, onPromptClose);
225 let promise = this.registerPrompt(newPrompt, id);
228 tabPrompt.removePrompt(newPrompt);
241 * Opens either a window prompt or TabDialogBox at the content or tab level
242 * for a BrowsingContext, and puts the associated browser in the modal state
243 * until the prompt is closed.
245 * @param {Object} args
246 * The arguments passed up from the BrowsingContext to be passed
247 * directly to the modal prompt.
249 * Resolves when the modal prompt is dismissed.
251 * The arguments returned from the modal prompt.
253 async openPromptWithTabDialogBox(args) {
254 const COMMON_DIALOG = "chrome://global/content/commonDialog.xhtml";
255 const SELECT_DIALOG = "chrome://global/content/selectDialog.xhtml";
256 let uri = args.promptType == "select" ? SELECT_DIALOG : COMMON_DIALOG;
258 let browsingContext = this.browsingContext.top;
260 let browser = browsingContext.embedderElement;
261 let promptRequiresBrowser =
262 args.modalType === Services.prompt.MODAL_TYPE_TAB ||
263 args.modalType === Services.prompt.MODAL_TYPE_CONTENT;
264 if (promptRequiresBrowser && !browser) {
266 args.modalType === Services.prompt.MODAL_TYPE_TAB ? "tab" : "content";
267 throw new Error(`Cannot ${modal_type}-prompt without a browser!`);
272 // If we are a chrome actor we can use the associated chrome win.
273 if (!browsingContext.isContent && browsingContext.window) {
274 win = browsingContext.window;
276 win = browser?.ownerGlobal;
279 // There's a requirement for prompts to be blocked if a window is
280 // passed and that window is hidden (eg, auth prompts are suppressed if the
281 // passed window is the hidden window).
282 // See bug 875157 comment 30 for more..
283 if (win?.winUtils && !win.winUtils.isParentWindowMainWidgetVisible) {
284 throw new Error("Cannot open a prompt in a hidden window");
289 browser.enterModalState();
290 lazy.PromptUtils.fireDialogEvent(
292 "DOMWillOpenModalDialog",
294 this.getOpenEventDetail(args)
298 args.promptAborted = false;
299 args.openedWithTabDialog = true;
301 // Convert args object to a prop bag for the dialog to consume.
304 if (promptRequiresBrowser && win?.gBrowser?.getTabDialogBox) {
305 // Tab or content level prompt
306 let dialogBox = win.gBrowser.getTabDialogBox(browser);
308 if (dialogBox._allowTabFocusByPromptPrincipal) {
309 this.addTabSwitchCheckboxToArgs(dialogBox, args);
312 let currentLocationsTabLabel;
314 let targetTab = win.gBrowser.getTabForBrowser(browser);
316 !Services.prefs.getBoolPref(
317 "privacy.authPromptSpoofingProtection",
321 args.isTopLevelCrossDomainAuth = false;
323 // Auth prompt spoofing protection, see bug 791594.
324 if (args.isTopLevelCrossDomainAuth && targetTab) {
325 // Set up the url bar with the url of the cross domain resource.
326 // onLocationChange will change the url back to the current browsers
327 // if we do not hold the state here.
328 // onLocationChange will favour currentAuthPromptURI over the current browsers uri
329 browser.currentAuthPromptURI = args.channel.URI;
330 if (browser == win.gBrowser.selectedBrowser) {
331 win.gURLBar.setURI();
333 // Set up the tab title for the cross domain resource.
334 // We need to remember the original tab title in case
335 // the load does not happen after the prompt, then we need to reset the tab title manually.
336 currentLocationsTabLabel = targetTab.label;
337 win.gBrowser.setTabLabelForAuthPrompts(
339 lazy.BrowserUtils.formatURIForDisplay(args.channel.URI)
342 bag = lazy.PromptUtils.objectToPropBag(args);
344 await dialogBox.open(
347 features: "resizable=no",
348 modalType: args.modalType,
349 allowFocusCheckbox: args.allowFocusCheckbox,
350 hideContent: args.isTopLevelCrossDomainAuth,
355 if (args.isTopLevelCrossDomainAuth) {
356 browser.currentAuthPromptURI = null;
357 // If the user is stopping the page load before answering the prompt, no navigation will happen after the prompt
358 // so we need to reset the uri and tab title here to the current browsers for that specific case
359 if (browser == win.gBrowser.selectedBrowser) {
360 win.gURLBar.setURI();
362 win.gBrowser.setTabLabelForAuthPrompts(
364 currentLocationsTabLabel
369 // Ensure we set the correct modal type at this point.
370 // If we use window prompts as a fallback it may not be set.
371 args.modalType = Services.prompt.MODAL_TYPE_WINDOW;
373 bag = lazy.PromptUtils.objectToPropBag(args);
374 Services.ww.openWindow(
378 "centerscreen,chrome,modal,titlebar",
383 lazy.PromptUtils.propBagToObject(bag, args);
386 browser.maybeLeaveModalState();
387 lazy.PromptUtils.fireDialogEvent(
389 "DOMModalDialogClosed",
391 this.getClosingEventDetail(args)
398 getClosingEventDetail(args) {
400 args.modalType === Services.prompt.MODAL_TYPE_CONTENT
402 wasPermitUnload: args.inPermitUnload,
410 getOpenEventDetail(args) {
412 args.modalType === Services.prompt.MODAL_TYPE_CONTENT
414 inPermitUnload: args.inPermitUnload,
415 promptPrincipal: args.promptPrincipal,
424 * Set properties on `args` needed by the dialog to allow tab switching for the
425 * page that opened the prompt.
427 * @param {TabDialogBox} dialogBox
428 * The dialog to show the tab-switch checkbox for.
429 * @param {Object} args
430 * The `args` object to set tab switching permission info on.
432 addTabSwitchCheckboxToArgs(dialogBox, args) {
433 let allowTabFocusByPromptPrincipal =
434 dialogBox._allowTabFocusByPromptPrincipal;
437 allowTabFocusByPromptPrincipal &&
438 args.modalType === Services.prompt.MODAL_TYPE_CONTENT
440 let domain = allowTabFocusByPromptPrincipal.addonPolicy?.name;
442 domain ||= allowTabFocusByPromptPrincipal.URI.displayHostPort;
444 /* Ignore exceptions from fetching the display host/port. */
446 // If it's still empty, use `prePath` so we have *something* to show:
447 domain ||= allowTabFocusByPromptPrincipal.URI.prePath;
448 let [allowFocusMsg] = lazy.gTabBrowserLocalization.formatMessagesSync([
450 id: "tabbrowser-allow-dialogs-to-get-focus",
454 let labelAttr = allowFocusMsg.attributes.find(a => a.name == "label");
456 args.allowFocusCheckbox = true;
457 args.checkLabel = labelAttr.value;