requestBridgeDialog.js (6148B)
1 "use strict"; 2 3 const { BridgeDB } = ChromeUtils.importESModule( 4 "resource://gre/modules/BridgeDB.sys.mjs" 5 ); 6 7 const { TorConnect, TorConnectStage, TorConnectTopics } = 8 ChromeUtils.importESModule("resource://gre/modules/TorConnect.sys.mjs"); 9 10 const log = console.createInstance({ 11 maxLogLevel: "Warn", 12 prefix: "requestBridgeDialog", 13 }); 14 15 const gRequestBridgeDialog = { 16 selectors: { 17 dialogHeader: "#torPreferences-requestBridge-header", 18 captchaImage: "image#torPreferences-requestBridge-captchaImage", 19 captchaEntryTextbox: "input#torPreferences-requestBridge-captchaTextbox", 20 refreshCaptchaButton: 21 "button#torPreferences-requestBridge-refreshCaptchaButton", 22 incorrectCaptchaHbox: 23 "hbox#torPreferences-requestBridge-incorrectCaptchaHbox", 24 }, 25 26 init() { 27 this._result = window.arguments[0]; 28 29 const selectors = this.selectors; 30 31 this._dialog = document.getElementById( 32 "torPreferences-requestBridge-dialog" 33 ); 34 35 // Add styling for tor-button to the dialog shadow root. 36 const styleLink = document.createElement("link"); 37 styleLink.rel = "stylesheet"; 38 styleLink.href = 39 "chrome://browser/content/torpreferences/torPreferences.css"; 40 this._dialog.shadowRoot.append(styleLink); 41 42 // user may have opened a Request Bridge dialog in another tab, so update the 43 // CAPTCHA image or close out the dialog if we have a bridge list 44 this._dialog.addEventListener("focusin", () => { 45 const uri = BridgeDB.currentCaptchaImage; 46 const bridges = BridgeDB.currentBridges; 47 48 // new captcha image 49 if (uri) { 50 this._setcaptchaImage(uri); 51 } else if (bridges) { 52 this._dialog.cancelDialog(); 53 } 54 }); 55 56 this._submitButton = this._dialog.getButton("accept"); 57 this._submitButton.disabled = true; 58 this._dialog.addEventListener("dialogaccept", e => { 59 e.preventDefault(); 60 this.onSubmitCaptcha(); 61 }); 62 63 this._dialogHeader = this._dialog.querySelector(selectors.dialogHeader); 64 65 this._captchaImage = this._dialog.querySelector(selectors.captchaImage); 66 67 // request captcha from bridge db 68 BridgeDB.requestNewCaptchaImage().then(uri => { 69 this._setcaptchaImage(uri); 70 }); 71 72 this._captchaEntryTextbox = this._dialog.querySelector( 73 selectors.captchaEntryTextbox 74 ); 75 this._captchaEntryTextbox.disabled = true; 76 // disable submit if entry textbox is empty 77 this._captchaEntryTextbox.oninput = () => { 78 this._submitButton.disabled = this._captchaEntryTextbox.value == ""; 79 }; 80 81 this._captchaRefreshButton = this._dialog.querySelector( 82 selectors.refreshCaptchaButton 83 ); 84 this._captchaRefreshButton.disabled = true; 85 this._captchaRefreshButton.addEventListener("command", () => { 86 this.onRefreshCaptcha(); 87 }); 88 89 this._incorrectCaptchaHbox = this._dialog.querySelector( 90 selectors.incorrectCaptchaHbox 91 ); 92 93 Services.obs.addObserver(this, TorConnectTopics.StageChange); 94 this.onAcceptStateChange(); 95 }, 96 97 uninit() { 98 BridgeDB.close(); 99 // Unregister our observer topics. 100 Services.obs.removeObserver(this, TorConnectTopics.StageChange); 101 }, 102 103 onAcceptStateChange() { 104 const connect = TorConnect.stageName !== TorConnectStage.Bootstrapped; 105 this._result.connect = connect; 106 this._submitButton.setAttribute( 107 "data-l10n-id", 108 connect ? "bridge-dialog-button-connect2" : "bridge-dialog-button-submit2" 109 ); 110 this._submitButton.classList.toggle("tor-button", connect); 111 }, 112 113 observe(subject, topic) { 114 switch (topic) { 115 case TorConnectTopics.StageChange: 116 this.onAcceptStateChange(); 117 break; 118 } 119 }, 120 121 _setcaptchaImage(uri) { 122 if (!uri) { 123 return; 124 } 125 if (uri != this._captchaImage.src) { 126 this._captchaImage.src = uri; 127 this._dialogHeader.setAttribute( 128 "data-l10n-id", 129 "request-bridge-dialog-top-solve" 130 ); 131 this._setUIDisabled(false); 132 this._captchaEntryTextbox.focus(); 133 this._captchaEntryTextbox.select(); 134 } 135 }, 136 137 _setUIDisabled(disabled) { 138 this._submitButton.disabled = this._captchaGuessIsEmpty() || disabled; 139 this._captchaEntryTextbox.disabled = disabled; 140 this._captchaRefreshButton.disabled = disabled; 141 }, 142 143 _captchaGuessIsEmpty() { 144 return this._captchaEntryTextbox.value == ""; 145 }, 146 147 /* 148 Event Handlers 149 */ 150 onSubmitCaptcha() { 151 let captchaText = this._captchaEntryTextbox.value.trim(); 152 // noop if the field is empty 153 if (captchaText == "") { 154 return; 155 } 156 157 // freeze ui while we make request 158 this._setUIDisabled(true); 159 this._incorrectCaptchaHbox.style.visibility = "hidden"; 160 161 BridgeDB.submitCaptchaGuess(captchaText) 162 .then(aBridges => { 163 if (aBridges && aBridges.length) { 164 this._result.accepted = true; 165 this._result.bridges = aBridges; 166 this._submitButton.disabled = false; 167 // This was successful, but use cancelDialog() to close, since 168 // we intercept the `dialogaccept` event. 169 this._dialog.cancelDialog(); 170 } else { 171 this._setUIDisabled(false); 172 this._incorrectCaptchaHbox.style.visibility = "visible"; 173 } 174 }) 175 .catch(aError => { 176 // TODO: handle other errors properly here when we do the bridge settings re-design 177 this._setUIDisabled(false); 178 this._incorrectCaptchaHbox.style.visibility = "visible"; 179 log.error(aError); 180 }); 181 }, 182 183 onRefreshCaptcha() { 184 this._setUIDisabled(true); 185 this._captchaImage.src = ""; 186 this._dialogHeader.setAttribute( 187 "data-l10n-id", 188 "request-bridge-dialog-top-wait" 189 ); 190 this._captchaEntryTextbox.value = ""; 191 this._incorrectCaptchaHbox.style.visibility = "hidden"; 192 193 BridgeDB.requestNewCaptchaImage().then(uri => { 194 this._setcaptchaImage(uri); 195 }); 196 }, 197 }; 198 199 window.addEventListener( 200 "DOMContentLoaded", 201 () => { 202 gRequestBridgeDialog.init(); 203 window.addEventListener( 204 "unload", 205 () => { 206 gRequestBridgeDialog.uninit(); 207 }, 208 { once: true } 209 ); 210 }, 211 { once: true } 212 );