Bug 1845017 - Disable the TestPHCExhaustion test r=glandium
[gecko.git] / browser / actors / PromptParent.sys.mjs
blob985ba5951f6444b252dfdd7423c712e321f97365
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/. */
6 const lazy = {};
8 ChromeUtils.defineESModuleGetters(lazy, {
9   PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
10   BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
11 });
12 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
14 XPCOMUtils.defineLazyPreferenceGetter(
15   lazy,
16   "tabChromePromptSubDialog",
17   "prompts.tabChromePromptSubDialog",
18   false
21 XPCOMUtils.defineLazyPreferenceGetter(
22   lazy,
23   "contentPromptSubDialog",
24   "prompts.contentPromptSubDialog",
25   false
28 ChromeUtils.defineLazyGetter(lazy, "gTabBrowserLocalization", function () {
29   return new Localization(["browser/tabbrowser.ftl"], true);
30 });
32 /**
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.
39  */
41 /**
42  * gBrowserPrompts weakly maps BrowsingContexts to a Map of their currently
43  * active Prompts.
44  *
45  * @type {WeakMap<BrowsingContext, Prompt>}
46  */
47 let gBrowserPrompts = new WeakMap();
49 export class PromptParent extends JSWindowActorParent {
50   didDestroy() {
51     // In the event that the subframe or tab crashed, make sure that
52     // we close any active Prompts.
53     this.forceClosePrompts();
54   }
56   /**
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.
60    *
61    * @param {Object} tabModalPrompt
62    *        The TabModalPrompt that will be shown to the user.
63    * @param {string} id
64    *        A unique ID to differentiate multiple Prompts coming from the same
65    *        BrowsingContext.
66    * @return {Promise}
67    * @resolves {Object}
68    *           Resolves with the arguments returned from the TabModalPrompt when it
69    *           is dismissed.
70    */
71   registerPrompt(tabModalPrompt, id) {
72     let prompts = gBrowserPrompts.get(this.browsingContext);
73     if (!prompts) {
74       prompts = new Map();
75       gBrowserPrompts.set(this.browsingContext, prompts);
76     }
78     let promise = new Promise(resolve => {
79       prompts.set(id, {
80         tabModalPrompt,
81         resolver: resolve,
82       });
83     });
85     return promise;
86   }
88   /**
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.
91    *
92    * @param {string} id
93    *        A unique ID to differentiate multiple Prompts coming from the same
94    *        BrowsingContext.
95    */
96   unregisterPrompt(id) {
97     let prompts = gBrowserPrompts.get(this.browsingContext);
98     if (prompts) {
99       prompts.delete(id);
100     }
101   }
103   /**
104    * Programmatically closes all Prompts for the current BrowsingContext.
105    */
106   forceClosePrompts() {
107     let prompts = gBrowserPrompts.get(this.browsingContext) || [];
109     for (let [, prompt] of prompts) {
110       prompt.tabModalPrompt && prompt.tabModalPrompt.abortPrompt();
111     }
112   }
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.
119       return false;
120     }
122     return (
123       embedderWindowGlobal.documentPrincipal.isSystemPrincipal &&
124       embedderWindowGlobal.documentURI.spec === "about:addons" &&
125       name === "addon-inline-options"
126     );
127   }
129   receiveMessage(message) {
130     let args = message.data;
131     let id = args._remoteId;
133     switch (message.name) {
134       case "Prompt:Open":
135         if (
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)
141         ) {
142           return this.openContentPrompt(args, id);
143         }
144         return this.openPromptWithTabDialogBox(args);
145     }
147     return undefined;
148   }
150   /**
151    * Opens a TabModalPrompt for a BrowsingContext, and puts the associated browser
152    * in the modal state until the TabModalPrompt is closed.
153    *
154    * @param {Object} args
155    *        The arguments passed up from the BrowsingContext to be passed directly
156    *        to the TabModalPrompt.
157    * @param {string} id
158    *        A unique ID to differentiate multiple Prompts coming from the same
159    *        BrowsingContext.
160    * @return {Promise}
161    *         Resolves when the TabModalPrompt is dismissed.
162    * @resolves {Object}
163    *           The arguments returned from the TabModalPrompt.
164    */
165   openContentPrompt(args, id) {
166     let browser = this.browsingContext.top.embedderElement;
167     if (!browser) {
168       throw new Error("Cannot tab-prompt without a browser!");
169     }
170     let window = browser.ownerGlobal;
171     let tabPrompt = window.gBrowser.getTabModalPromptBox(browser);
172     let newPrompt;
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)) {
185         throw new Error(
186           "Failed to close a prompt since it wasn't registered for some reason."
187         );
188       }
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);
197       } else {
198         needRemove = true;
199       }
201       this.unregisterPrompt(id);
203       lazy.PromptUtils.fireDialogEvent(
204         window,
205         "DOMModalDialogClosed",
206         browser,
207         this.getClosingEventDetail(args)
208       );
209       resolver(args);
210       browser.maybeLeaveModalState();
211     };
213     try {
214       browser.enterModalState();
215       lazy.PromptUtils.fireDialogEvent(
216         window,
217         "DOMWillOpenModalDialog",
218         browser,
219         this.getOpenEventDetail(args)
220       );
222       args.promptActive = true;
224       newPrompt = tabPrompt.appendPrompt(args, onPromptClose);
225       let promise = this.registerPrompt(newPrompt, id);
227       if (needRemove) {
228         tabPrompt.removePrompt(newPrompt);
229       }
231       return promise;
232     } catch (ex) {
233       console.error(ex);
234       onPromptClose(true);
235     }
237     return null;
238   }
240   /**
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.
244    *
245    * @param {Object} args
246    *        The arguments passed up from the BrowsingContext to be passed
247    *        directly to the modal prompt.
248    * @return {Promise}
249    *         Resolves when the modal prompt is dismissed.
250    * @resolves {Object}
251    *           The arguments returned from the modal prompt.
252    */
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) {
265       let modal_type =
266         args.modalType === Services.prompt.MODAL_TYPE_TAB ? "tab" : "content";
267       throw new Error(`Cannot ${modal_type}-prompt without a browser!`);
268     }
270     let win;
272     // If we are a chrome actor we can use the associated chrome win.
273     if (!browsingContext.isContent && browsingContext.window) {
274       win = browsingContext.window;
275     } else {
276       win = browser?.ownerGlobal;
277     }
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");
285     }
287     try {
288       if (browser) {
289         browser.enterModalState();
290         lazy.PromptUtils.fireDialogEvent(
291           win,
292           "DOMWillOpenModalDialog",
293           browser,
294           this.getOpenEventDetail(args)
295         );
296       }
298       args.promptAborted = false;
299       args.openedWithTabDialog = true;
301       // Convert args object to a prop bag for the dialog to consume.
302       let bag;
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);
310         }
312         let currentLocationsTabLabel;
314         let targetTab = win.gBrowser.getTabForBrowser(browser);
315         if (
316           !Services.prefs.getBoolPref(
317             "privacy.authPromptSpoofingProtection",
318             false
319           )
320         ) {
321           args.isTopLevelCrossDomainAuth = false;
322         }
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();
332           }
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(
338             targetTab,
339             lazy.BrowserUtils.formatURIForDisplay(args.channel.URI)
340           );
341         }
342         bag = lazy.PromptUtils.objectToPropBag(args);
343         try {
344           await dialogBox.open(
345             uri,
346             {
347               features: "resizable=no",
348               modalType: args.modalType,
349               allowFocusCheckbox: args.allowFocusCheckbox,
350               hideContent: args.isTopLevelCrossDomainAuth,
351             },
352             bag
353           ).closedPromise;
354         } finally {
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();
361             }
362             win.gBrowser.setTabLabelForAuthPrompts(
363               targetTab,
364               currentLocationsTabLabel
365             );
366           }
367         }
368       } else {
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;
372         // Window prompt
373         bag = lazy.PromptUtils.objectToPropBag(args);
374         Services.ww.openWindow(
375           win,
376           uri,
377           "_blank",
378           "centerscreen,chrome,modal,titlebar",
379           bag
380         );
381       }
383       lazy.PromptUtils.propBagToObject(bag, args);
384     } finally {
385       if (browser) {
386         browser.maybeLeaveModalState();
387         lazy.PromptUtils.fireDialogEvent(
388           win,
389           "DOMModalDialogClosed",
390           browser,
391           this.getClosingEventDetail(args)
392         );
393       }
394     }
395     return args;
396   }
398   getClosingEventDetail(args) {
399     let details =
400       args.modalType === Services.prompt.MODAL_TYPE_CONTENT
401         ? {
402             wasPermitUnload: args.inPermitUnload,
403             areLeaving: args.ok,
404           }
405         : null;
407     return details;
408   }
410   getOpenEventDetail(args) {
411     let details =
412       args.modalType === Services.prompt.MODAL_TYPE_CONTENT
413         ? {
414             inPermitUnload: args.inPermitUnload,
415             promptPrincipal: args.promptPrincipal,
416             tabPrompt: true,
417           }
418         : null;
420     return details;
421   }
423   /**
424    * Set properties on `args` needed by the dialog to allow tab switching for the
425    * page that opened the prompt.
426    *
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.
431    */
432   addTabSwitchCheckboxToArgs(dialogBox, args) {
433     let allowTabFocusByPromptPrincipal =
434       dialogBox._allowTabFocusByPromptPrincipal;
436     if (
437       allowTabFocusByPromptPrincipal &&
438       args.modalType === Services.prompt.MODAL_TYPE_CONTENT
439     ) {
440       let domain = allowTabFocusByPromptPrincipal.addonPolicy?.name;
441       try {
442         domain ||= allowTabFocusByPromptPrincipal.URI.displayHostPort;
443       } catch (ex) {
444         /* Ignore exceptions from fetching the display host/port. */
445       }
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([
449         {
450           id: "tabbrowser-allow-dialogs-to-get-focus",
451           args: { domain },
452         },
453       ]);
454       let labelAttr = allowFocusMsg.attributes.find(a => a.name == "label");
455       if (labelAttr) {
456         args.allowFocusCheckbox = true;
457         args.checkLabel = labelAttr.value;
458       }
459     }
460   }