GeckoViewPrompter.sys.mjs (5460B)
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 import { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs"; 6 7 const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPrompter"); 8 9 export class GeckoViewPrompter { 10 constructor(aParent) { 11 this.id = Services.uuid.generateUUID().toString().slice(1, -1); // Discard surrounding braces 12 13 if (aParent) { 14 if (Window.isInstance(aParent)) { 15 this._domWin = aParent; 16 } else if (aParent.window) { 17 this._domWin = aParent.window; 18 } else { 19 this._domWin = 20 aParent.embedderElement && aParent.embedderElement.ownerGlobal; 21 } 22 } 23 24 if (!this._domWin) { 25 this._domWin = Services.wm.getMostRecentWindow("navigator:geckoview"); 26 } 27 28 this._innerWindowId = 29 this._domWin?.browsingContext.currentWindowContext.innerWindowId; 30 } 31 32 get domWin() { 33 return this._domWin; 34 } 35 36 get prompterActor() { 37 const actor = this.domWin?.windowGlobalChild.getActor("GeckoViewPrompter"); 38 return actor; 39 } 40 41 _changeModalState(aEntering) { 42 if (!this._domWin) { 43 // Allow not having a DOM window. 44 return true; 45 } 46 // Accessing the document object can throw if this window no longer exists. See bug 789888. 47 try { 48 const winUtils = this._domWin.windowUtils; 49 if (!aEntering) { 50 winUtils.leaveModalState(); 51 } 52 53 const event = this._domWin.document.createEvent("Events"); 54 event.initEvent( 55 aEntering ? "DOMWillOpenModalDialog" : "DOMModalDialogClosed", 56 true, 57 true 58 ); 59 winUtils.dispatchEventToChromeOnly(this._domWin, event); 60 61 if (aEntering) { 62 winUtils.enterModalState(); 63 } 64 return true; 65 } catch (ex) { 66 console.error("Failed to change modal state:", ex); 67 } 68 return false; 69 } 70 71 _dismissUi() { 72 this.prompterActor?.dismissPrompt(this); 73 } 74 75 accept(aInputText = this.inputText) { 76 if (this.callback) { 77 let acceptMsg = {}; 78 switch (this.message.type) { 79 case "alert": 80 acceptMsg = null; 81 break; 82 case "button": 83 acceptMsg.button = 0; 84 break; 85 case "text": 86 acceptMsg.text = aInputText; 87 break; 88 default: 89 acceptMsg = null; 90 break; 91 } 92 this.callback(acceptMsg); 93 // Notify the UI that this prompt should be hidden. 94 this._dismissUi(); 95 } 96 } 97 98 dismiss() { 99 this.callback(null); 100 // Notify the UI that this prompt should be hidden. 101 this._dismissUi(); 102 } 103 104 getPromptType() { 105 switch (this.message.type) { 106 case "alert": 107 return this.message.checkValue ? "alertCheck" : "alert"; 108 case "button": 109 return this.message.checkValue ? "confirmCheck" : "confirm"; 110 case "text": 111 return this.message.checkValue ? "promptCheck" : "prompt"; 112 default: 113 return this.message.type; 114 } 115 } 116 117 getPromptText() { 118 return this.message.msg; 119 } 120 121 getInputText() { 122 return this.inputText; 123 } 124 125 setInputText(aInput) { 126 this.inputText = aInput; 127 } 128 129 /** 130 * Shows a native prompt, and then spins the event loop for this thread while we wait 131 * for a response 132 */ 133 showPrompt(aMsg) { 134 let result = undefined; 135 if (!this._domWin || !this._changeModalState(/* aEntering */ true)) { 136 return result; 137 } 138 try { 139 this.asyncShowPrompt(aMsg, res => (result = res)); 140 141 // Spin this thread while we wait for a result 142 Services.tm.spinEventLoopUntil( 143 "GeckoViewPrompter.sys.mjs:showPrompt", 144 () => this._domWin.closed || result !== undefined 145 ); 146 } finally { 147 this._changeModalState(/* aEntering */ false); 148 } 149 return result; 150 } 151 152 checkInnerWindow() { 153 // Checks that the innerWindow where this prompt was created still matches 154 // the current innerWindow. 155 // This checks will fail if the page navigates away, making this prompt 156 // obsolete. 157 return ( 158 this._innerWindowId === 159 this._domWin.browsingContext.currentWindowContext.innerWindowId 160 ); 161 } 162 163 asyncShowPromptPromise(aMsg) { 164 return new Promise(resolve => { 165 this.asyncShowPrompt(aMsg, resolve); 166 }); 167 } 168 169 async asyncShowPrompt(aMsg, aCallback) { 170 this.message = aMsg; 171 this.inputText = aMsg.value; 172 this.callback = aCallback; 173 174 aMsg.id = this.id; 175 176 let response = null; 177 try { 178 if (this.checkInnerWindow()) { 179 response = await this.prompterActor.prompt(this, aMsg); 180 } 181 } catch (error) { 182 // Nothing we can do really, we will treat this as a dismiss. 183 warn`Error while prompting: ${error}`; 184 } 185 186 if (!this.checkInnerWindow()) { 187 // Page has navigated away, let's dismiss the prompt 188 aCallback(null); 189 } else { 190 aCallback(response); 191 } 192 // This callback object is tied to the Java garbage collector because 193 // it is invoked from Java. Manually release the target callback 194 // here; otherwise we may hold onto resources for too long, because 195 // we would be relying on both the Java and the JS garbage collectors 196 // to run. 197 aMsg = undefined; 198 aCallback = undefined; 199 } 200 201 update(aMsg) { 202 this.message = aMsg; 203 aMsg.id = this.id; 204 this.prompterActor?.updatePrompt(aMsg); 205 } 206 }