1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5"use strict"; 6 7var EXPORTED_SYMBOLS = ["DialogHandler"]; 8 9var { XPCOMUtils } = ChromeUtils.import( 10 "resource://gre/modules/XPCOMUtils.jsm" 11); 12 13XPCOMUtils.defineLazyModuleGetters(this, { 14 EventEmitter: "resource://gre/modules/EventEmitter.jsm", 15 Services: "resource://gre/modules/Services.jsm", 16}); 17 18const DIALOG_TYPES = { 19 ALERT: "alert", 20 BEFOREUNLOAD: "beforeunload", 21 CONFIRM: "confirm", 22 PROMPT: "prompt", 23}; 24 25/** 26 * Helper dedicated to detect and interact with browser dialogs such as `alert`, 27 * `confirm` etc. The current implementation only supports tabmodal dialogs, 28 * not full window dialogs. 29 * 30 * Emits "dialog-loaded" when a javascript dialog is opened for the current 31 * browser. 32 * 33 * @param {BrowserElement} browser 34 */ 35class DialogHandler { 36 constructor(browser) { 37 EventEmitter.decorate(this); 38 this._dialog = null; 39 this._browser = browser; 40 41 this._onCommonDialogLoaded = this._onCommonDialogLoaded.bind(this); 42 this._onTabDialogLoaded = this._onTabDialogLoaded.bind(this); 43 44 Services.obs.addObserver( 45 this._onCommonDialogLoaded, 46 "common-dialog-loaded" 47 ); 48 Services.obs.addObserver(this._onTabDialogLoaded, "tabmodal-dialog-loaded"); 49 } 50 51 destructor() { 52 this._dialog = null; 53 this._pageTarget = null; 54 55 Services.obs.removeObserver( 56 this._onCommonDialogLoaded, 57 "common-dialog-loaded" 58 ); 59 Services.obs.removeObserver( 60 this._onTabDialogLoaded, 61 "tabmodal-dialog-loaded" 62 ); 63 } 64 65 async handleJavaScriptDialog({ accept, promptText }) { 66 if (!this._dialog) { 67 throw new Error("No dialog available for handleJavaScriptDialog"); 68 } 69 70 const type = this._getDialogType(); 71 if (promptText && type === "prompt") { 72 this._dialog.ui.loginTextbox.value = promptText; 73 } 74 75 const onDialogClosed = new Promise(r => { 76 this._browser.addEventListener("DOMModalDialogClosed", r, { 77 once: true, 78 }); 79 }); 80 81 // 0 corresponds to the OK callback, 1 to the CANCEL callback. 82 if (accept) { 83 this._dialog.ui.button0.click(); 84 } else { 85 this._dialog.ui.button1.click(); 86 } 87 88 await onDialogClosed; 89 90 // Resetting dialog to null here might be racy and lead to errors if the 91 // content page is triggering several prompts in a row. 92 // See Bug 1569578. 93 this._dialog = null; 94 } 95 96 _getDialogType() { 97 const { inPermitUnload, promptType } = this._dialog.args; 98 99 if (inPermitUnload) { 100 return DIALOG_TYPES.BEFOREUNLOAD; 101 } 102 103 switch (promptType) { 104 case "alert": 105 return DIALOG_TYPES.ALERT; 106 case "confirm": 107 return DIALOG_TYPES.CONFIRM; 108 case "prompt": 109 return DIALOG_TYPES.PROMPT; 110 default: 111 throw new Error("Unsupported dialog type: " + promptType); 112 } 113 } 114 115 _onCommonDialogLoaded(dialogWindow) { 116 const dialogs = this._browser.tabDialogBox.getContentDialogManager() 117 .dialogs; 118 const dialog = dialogs.find(d => d.frameContentWindow === dialogWindow); 119 120 if (!dialog) { 121 // The dialog is not for the current tab. 122 return; 123 } 124 125 this._dialog = dialogWindow.Dialog; 126 const message = this._dialog.args.text; 127 const type = this._getDialogType(); 128 129 this.emit("dialog-loaded", { message, type }); 130 } 131 132 _onTabDialogLoaded(promptContainer) { 133 const prompts = this._browser.tabModalPromptBox.listPrompts(); 134 const prompt = prompts.find(p => p.ui.promptContainer === promptContainer); 135 136 if (!prompt) { 137 // The dialog is not for the current tab. 138 return; 139 } 140 141 this._dialog = prompt; 142 const message = this._dialog.args.text; 143 const type = this._getDialogType(); 144 145 this.emit("dialog-loaded", { message, type }); 146 } 147} 148