PromptParent.sys.mjs (12316B)
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/. */ 5 6 const lazy = {}; 7 8 ChromeUtils.defineESModuleGetters(lazy, { 9 PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs", 10 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", 11 }); 12 13 ChromeUtils.defineLazyGetter(lazy, "gTabBrowserLocalization", function () { 14 return new Localization(["browser/tabbrowser.ftl"], true); 15 }); 16 17 /** 18 * @typedef {object} Dialog 19 */ 20 21 /** 22 * gBrowserDialogs weakly maps BrowsingContexts to a Map of their currently 23 * active Dialogs. 24 * 25 * @type {WeakMap<BrowsingContext, Dialog>} 26 */ 27 let gBrowserDialogs = new WeakMap(); 28 29 export class PromptParent extends JSWindowActorParent { 30 didDestroy() { 31 // In the event that the subframe or tab crashed, make sure that 32 // we close any active Prompts. 33 this.forceClosePrompts(); 34 } 35 36 /** 37 * Registers a new dialog to be tracked for a particular BrowsingContext. 38 * We need to track a dialog so that we can, for example, force-close the 39 * dialog if the originating subframe or tab unloads or crashes. 40 * 41 * @param {Dialog} dialog 42 * The dialog that will be shown to the user. 43 * @param {string} id 44 * A unique ID to differentiate multiple dialogs coming from the same 45 * BrowsingContext. 46 */ 47 registerDialog(dialog, id) { 48 let dialogs = gBrowserDialogs.get(this.browsingContext); 49 if (!dialogs) { 50 dialogs = new Map(); 51 gBrowserDialogs.set(this.browsingContext, dialogs); 52 } 53 54 dialogs.set(id, dialog); 55 } 56 57 /** 58 * Removes a Prompt for a BrowsingContext with a particular ID from the registry. 59 * This needs to be done to avoid leaking <xul:browser>'s. 60 * 61 * @param {string} id 62 * A unique ID to differentiate multiple Prompts coming from the same 63 * BrowsingContext. 64 */ 65 unregisterPrompt(id) { 66 let dialogs = gBrowserDialogs.get(this.browsingContext); 67 dialogs?.delete(id); 68 } 69 70 /** 71 * Programmatically closes all Prompts for the current BrowsingContext. 72 */ 73 forceClosePrompts() { 74 let dialogs = gBrowserDialogs.get(this.browsingContext) || []; 75 76 for (let [, dialog] of dialogs) { 77 dialog?.abort(); 78 } 79 } 80 81 isAboutAddonsOptionsPage(browsingContext) { 82 const { embedderWindowGlobal, name } = browsingContext; 83 if (!embedderWindowGlobal) { 84 // Return earlier if there is no embedder global, this is definitely 85 // not an about:addons extensions options page. 86 return false; 87 } 88 89 return ( 90 embedderWindowGlobal.documentPrincipal.isSystemPrincipal && 91 embedderWindowGlobal.documentURI.spec === "about:addons" && 92 name === "addon-inline-options" 93 ); 94 } 95 96 // Note that this will return false for the sidebar <browser> element 97 // itself. 98 isEmbeddedInSidebar(browser) { 99 if ( 100 browser?.ownerGlobal?.browsingContext.embedderElement?.id != "sidebar" 101 ) { 102 return false; 103 } 104 // Extensions in the sidebar have more layers of nesting, and this causes 105 // window leaks in tests. We would like to fix this at some point (bug 1513656) 106 if (browser.getAttribute("messagemanagergroup") == "webext-browsers") { 107 return false; 108 } 109 return true; 110 } 111 112 receiveMessage(message) { 113 switch (message.name) { 114 case "Prompt:Open": 115 if (!this.windowContext.isActiveInTab) { 116 return undefined; 117 } 118 119 return this.openPromptWithTabDialogBox(message.data); 120 } 121 122 return undefined; 123 } 124 125 /** 126 * Opens either a window prompt or TabDialogBox at the content or tab level 127 * for a BrowsingContext, and puts the associated browser in the modal state 128 * until the prompt is closed. 129 * 130 * @param {object} args 131 * The arguments passed up from the BrowsingContext to be passed 132 * directly to the modal prompt. 133 * @return {Promise<object>} 134 * Resolves with the arguments returned from the modal prompt. 135 */ 136 async openPromptWithTabDialogBox(args) { 137 const COMMON_DIALOG = "chrome://global/content/commonDialog.xhtml"; 138 const SELECT_DIALOG = "chrome://global/content/selectDialog.xhtml"; 139 let uri = args.promptType == "select" ? SELECT_DIALOG : COMMON_DIALOG; 140 141 let browsingContext = this.browsingContext.top; 142 143 let browser = browsingContext.embedderElement; 144 145 let isEmbeddedInSidebar = this.isEmbeddedInSidebar(browser); 146 if (isEmbeddedInSidebar || this.isAboutAddonsOptionsPage(browsingContext)) { 147 browser = browser.ownerGlobal.browsingContext.embedderElement; 148 } 149 150 let promptRequiresBrowser = 151 args.modalType === Services.prompt.MODAL_TYPE_TAB || 152 args.modalType === Services.prompt.MODAL_TYPE_CONTENT; 153 if (promptRequiresBrowser && !browser) { 154 let modal_type = 155 args.modalType === Services.prompt.MODAL_TYPE_TAB ? "tab" : "content"; 156 throw new Error(`Cannot ${modal_type}-prompt without a browser!`); 157 } 158 159 const closingEventDetails = 160 args.modalType === Services.prompt.MODAL_TYPE_CONTENT 161 ? { 162 owningBrowsingContext: this.browsingContext, 163 promptType: args.inPermitUnload ? "beforeunload" : args.promptType, 164 } 165 : null; 166 167 let win; 168 169 // If we are a chrome actor we can use the associated chrome win. 170 if (!browsingContext.isContent && browsingContext.window) { 171 win = browsingContext.window; 172 } else { 173 win = browser?.ownerGlobal; 174 } 175 176 // There's a requirement for prompts to be blocked if a window is 177 // passed and that window is hidden (eg, auth prompts are suppressed if the 178 // passed window is the hidden window). 179 // See bug 875157 comment 30 for more.. 180 if (win?.winUtils && !win.winUtils.isParentWindowMainWidgetVisible) { 181 throw new Error("Cannot open a prompt in a hidden window"); 182 } 183 184 try { 185 if (browsingContext.embedderElement) { 186 browsingContext.embedderElement.enterModalState(); 187 lazy.PromptUtils.fireDialogEvent( 188 win, 189 "DOMWillOpenModalDialog", 190 browsingContext.embedderElement, 191 this.getOpenEventDetail(args) 192 ); 193 } 194 195 args.promptAborted = false; 196 args.openedWithTabDialog = true; 197 args.owningBrowsingContext = this.browsingContext; 198 199 // Convert args object to a prop bag for the dialog to consume. 200 let bag; 201 202 if (promptRequiresBrowser && win?.gBrowser?.getTabDialogBox) { 203 // Tab or content level prompt 204 let dialogBox = win.gBrowser.getTabDialogBox(browser); 205 206 if (dialogBox._allowTabFocusByPromptPrincipal) { 207 this.addTabSwitchCheckboxToArgs(dialogBox, args); 208 } 209 210 let currentLocationsTabLabel; 211 212 let targetTab = win.gBrowser.getTabForBrowser(browser); 213 if ( 214 !Services.prefs.getBoolPref( 215 "privacy.authPromptSpoofingProtection", 216 false 217 ) 218 ) { 219 args.isTopLevelCrossDomainAuth = false; 220 } 221 // Auth prompt spoofing protection, see bug 791594. 222 if (args.isTopLevelCrossDomainAuth && targetTab) { 223 // Set up the url bar with the url of the cross domain resource. 224 // onLocationChange will change the url back to the current browsers 225 // if we do not hold the state here. 226 // onLocationChange will favour currentAuthPromptURI over the current browsers uri 227 browser.currentAuthPromptURI = args.channel.URI; 228 if (browser == win.gBrowser.selectedBrowser) { 229 win.gURLBar.setURI(); 230 } 231 // Set up the tab title for the cross domain resource. 232 // We need to remember the original tab title in case 233 // the load does not happen after the prompt, then we need to reset the tab title manually. 234 currentLocationsTabLabel = targetTab.label; 235 win.gBrowser.setTabLabelForAuthPrompts( 236 targetTab, 237 lazy.BrowserUtils.formatURIForDisplay(args.channel.URI) 238 ); 239 } 240 bag = lazy.PromptUtils.objectToPropBag(args); 241 let promptID = args._remoteId; 242 try { 243 let { dialog, closedPromise } = dialogBox.open( 244 uri, 245 { 246 features: "resizable=no", 247 modalType: args.modalType, 248 allowFocusCheckbox: args.allowFocusCheckbox, 249 hideContent: args.isTopLevelCrossDomainAuth, 250 // If we are in the sidebar, use the inner browser to detect when navigation is done 251 webProgress: isEmbeddedInSidebar 252 ? browsingContext?.webProgress 253 : undefined, 254 }, 255 bag 256 ); 257 dialog.promptID = promptID; 258 this.registerDialog(dialog, promptID); 259 await closedPromise; 260 } finally { 261 if (args.isTopLevelCrossDomainAuth) { 262 browser.currentAuthPromptURI = null; 263 // If the user is stopping the page load before answering the prompt, no navigation will happen after the prompt 264 // so we need to reset the uri and tab title here to the current browsers for that specific case 265 if (browser == win.gBrowser.selectedBrowser) { 266 win.gURLBar.setURI(); 267 } 268 win.gBrowser.setTabLabelForAuthPrompts( 269 targetTab, 270 currentLocationsTabLabel 271 ); 272 } 273 this.unregisterPrompt(promptID); 274 } 275 } else { 276 // Ensure we set the correct modal type at this point. 277 // If we use window prompts as a fallback it may not be set. 278 args.modalType = Services.prompt.MODAL_TYPE_WINDOW; 279 // Window prompt 280 bag = lazy.PromptUtils.objectToPropBag(args); 281 Services.ww.openWindow( 282 win, 283 uri, 284 "_blank", 285 "centerscreen,chrome,modal,titlebar", 286 bag 287 ); 288 } 289 290 lazy.PromptUtils.propBagToObject(bag, args); 291 } finally { 292 if (browsingContext.embedderElement) { 293 browsingContext.embedderElement.maybeLeaveModalState(); 294 lazy.PromptUtils.fireDialogEvent( 295 win, 296 "DOMModalDialogClosed", 297 browsingContext.embedderElement, 298 closingEventDetails 299 ? { 300 ...closingEventDetails, 301 areLeaving: args.ok, 302 // If a prompt was not accepted, do not return the prompt value. 303 value: args.ok ? args.value : null, 304 } 305 : null 306 ); 307 } 308 } 309 return args; 310 } 311 312 getOpenEventDetail(args) { 313 let details = 314 args.modalType === Services.prompt.MODAL_TYPE_CONTENT 315 ? { 316 inPermitUnload: args.inPermitUnload, 317 promptPrincipal: args.promptPrincipal, 318 tabPrompt: true, 319 } 320 : null; 321 322 return details; 323 } 324 325 /** 326 * Set properties on `args` needed by the dialog to allow tab switching for the 327 * page that opened the prompt. 328 * 329 * @param {TabDialogBox} dialogBox 330 * The dialog to show the tab-switch checkbox for. 331 * @param {object} args 332 * The `args` object to set tab switching permission info on. 333 */ 334 addTabSwitchCheckboxToArgs(dialogBox, args) { 335 let allowTabFocusByPromptPrincipal = 336 dialogBox._allowTabFocusByPromptPrincipal; 337 338 if ( 339 allowTabFocusByPromptPrincipal && 340 args.modalType === Services.prompt.MODAL_TYPE_CONTENT 341 ) { 342 let domain = allowTabFocusByPromptPrincipal.addonPolicy?.name; 343 try { 344 domain ||= allowTabFocusByPromptPrincipal.URI.displayHostPort; 345 } catch (ex) { 346 /* Ignore exceptions from fetching the display host/port. */ 347 } 348 // If it's still empty, use `prePath` so we have *something* to show: 349 domain ||= allowTabFocusByPromptPrincipal.URI.prePath; 350 let [allowFocusMsg] = lazy.gTabBrowserLocalization.formatMessagesSync([ 351 { 352 id: "tabbrowser-allow-dialogs-to-get-focus", 353 args: { domain }, 354 }, 355 ]); 356 let labelAttr = allowFocusMsg.attributes.find(a => a.name == "label"); 357 if (labelAttr) { 358 args.allowFocusCheckbox = true; 359 args.checkLabel = labelAttr.value; 360 } 361 } 362 } 363 }