savedKeysDialog.js (8270B)
1 // Copyright (c) 2020, The Tor Project, Inc. 2 3 "use strict"; 4 5 ChromeUtils.defineESModuleGetters(this, { 6 TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs", 7 }); 8 9 var gOnionServicesSavedKeysDialog = { 10 _tree: undefined, 11 _busyCount: 0, 12 get _isBusy() { 13 // true when loading data, deleting a key, etc. 14 return this._busyCount > 0; 15 }, 16 17 /** 18 * Whether the "remove selected" button is disabled. 19 * 20 * @type {boolean} 21 */ 22 _removeSelectedDisabled: true, 23 24 /** 25 * Whether the "remove all" button is disabled. 26 * 27 * @type {boolean} 28 */ 29 _removeAllDisabled: true, 30 31 async _deleteSelectedKeys() { 32 this._showError(null); 33 this._withBusy(async () => { 34 const indexesToDelete = []; 35 const count = this._tree.view.selection.getRangeCount(); 36 for (let i = 0; i < count; ++i) { 37 const minObj = {}; 38 const maxObj = {}; 39 this._tree.view.selection.getRangeAt(i, minObj, maxObj); 40 for (let idx = minObj.value; idx <= maxObj.value; ++idx) { 41 indexesToDelete.push(idx); 42 } 43 } 44 45 if (indexesToDelete.length) { 46 const provider = await TorProviderBuilder.build(); 47 try { 48 // Remove in reverse index order to avoid issues caused by index 49 // changes. 50 for (let i = indexesToDelete.length - 1; i >= 0; --i) { 51 await this._deleteOneKey(provider, indexesToDelete[i]); 52 } 53 // If successful and the user focus is still on the buttons move focus 54 // to the table with the updated state. We do this before calling 55 // _updateButtonState and potentially making the buttons disabled. 56 if ( 57 this._removeButton.contains(document.activeElement) || 58 this._removeAllButton.contains(document.activeElement) 59 ) { 60 this._tree.focus(); 61 } 62 } catch (e) { 63 console.error("Removing a saved key failed", e); 64 this._showError( 65 "onion-site-saved-keys-dialog-remove-keys-error-message" 66 ); 67 } 68 } 69 }); 70 }, 71 72 async _deleteAllKeys() { 73 this._tree.view.selection.selectAll(); 74 await this._deleteSelectedKeys(); 75 }, 76 77 /** 78 * Show the given button as being disabled or enabled. 79 * 80 * @param {Button} button - The button to change. 81 * @param {boolean} disable - Whether to show the button as disabled or 82 * enabled. 83 */ 84 _disableButton(button, disable) { 85 // If we are disabled we show the button as disabled, and we also remove it 86 // from the tab focus cycle using `tabIndex = -1`. 87 // This is similar to using the `disabled` attribute, except that 88 // `tabIndex = -1` still allows the button to be focusable. I.e. not part of 89 // the focus cycle but can *keep* existing focus when the button becomes 90 // disabled to avoid loosing focus to the top of the dialog. 91 // TODO: Replace with moz-button when it handles this for us. See 92 // tor-browser#43275. 93 button.classList.toggle("spoof-button-disabled", disable); 94 button.tabIndex = disable ? -1 : 0; 95 if (disable) { 96 this._removeButton.setAttribute("aria-disabled", "true"); 97 } else { 98 this._removeButton.removeAttribute("aria-disabled"); 99 } 100 }, 101 102 _updateButtonsState() { 103 const haveSelection = this._tree.view.selection.getRangeCount() > 0; 104 this._removeSelectedDisabled = this._isBusy || !haveSelection; 105 this._removeAllDisabled = this._isBusy || this.rowCount === 0; 106 this._disableButton(this._removeButton, this._removeSelectedDisabled); 107 this._disableButton(this._removeAllButton, this._removeAllDisabled); 108 }, 109 110 // Private functions. 111 _onLoad() { 112 document.mozSubdialogReady = this._init(); 113 }, 114 115 _init() { 116 this._populateXUL(); 117 window.addEventListener("keypress", this._onWindowKeyPress.bind(this)); 118 this._loadSavedKeys(); 119 }, 120 121 _populateXUL() { 122 this._errorMessageContainer = document.getElementById( 123 "onionservices-savedkeys-errorContainer" 124 ); 125 this._errorMessageEl = document.getElementById( 126 "onionservices-savedkeys-errorMessage" 127 ); 128 this._removeButton = document.getElementById( 129 "onionservices-savedkeys-remove" 130 ); 131 this._removeButton.addEventListener("click", () => { 132 if (this._removeSelectedDisabled) { 133 return; 134 } 135 this._deleteSelectedKeys(); 136 }); 137 this._removeAllButton = document.getElementById( 138 "onionservices-savedkeys-removeall" 139 ); 140 this._removeAllButton.addEventListener("click", () => { 141 if (this._removeAllDisabled) { 142 return; 143 } 144 this._deleteAllKeys(); 145 }); 146 147 this._tree = document.getElementById("onionservices-savedkeys-tree"); 148 this._tree.addEventListener("select", () => { 149 this._updateButtonsState(); 150 }); 151 }, 152 153 async _loadSavedKeys() { 154 this._showError(null); 155 this._withBusy(async () => { 156 try { 157 this._tree.view = this; 158 159 const provider = await TorProviderBuilder.build(); 160 const keyInfoList = await provider.onionAuthViewKeys(); 161 if (keyInfoList) { 162 // Filter out temporary keys. 163 this._keyInfoList = keyInfoList.filter(aKeyInfo => 164 aKeyInfo.flags?.includes("Permanent") 165 ); 166 // Sort by the .onion address. 167 this._keyInfoList.sort((aObj1, aObj2) => { 168 const hsAddr1 = aObj1.address.toLowerCase(); 169 const hsAddr2 = aObj2.address.toLowerCase(); 170 if (hsAddr1 < hsAddr2) { 171 return -1; 172 } 173 return hsAddr1 > hsAddr2 ? 1 : 0; 174 }); 175 } 176 177 // Render the tree content. 178 this._tree.rowCountChanged(0, this.rowCount); 179 } catch (e) { 180 console.error("Failed to load keys", e); 181 this._showError( 182 "onion-site-saved-keys-dialog-fetch-keys-error-message" 183 ); 184 } 185 }); 186 }, 187 188 // This method may throw; callers should catch errors. 189 async _deleteOneKey(provider, aIndex) { 190 const keyInfoObj = this._keyInfoList[aIndex]; 191 await provider.onionAuthRemove(keyInfoObj.address); 192 this._tree.view.selection.clearRange(aIndex, aIndex); 193 this._keyInfoList.splice(aIndex, 1); 194 this._tree.rowCountChanged(aIndex + 1, -1); 195 }, 196 197 async _withBusy(func) { 198 this._busyCount++; 199 if (this._busyCount === 1) { 200 this._updateButtonsState(); 201 } 202 try { 203 await func(); 204 } finally { 205 this._busyCount--; 206 if (this._busyCount === 0) { 207 this._updateButtonsState(); 208 } 209 } 210 }, 211 212 _onWindowKeyPress(event) { 213 if (this._isBusy) { 214 return; 215 } 216 if (event.keyCode === KeyEvent.DOM_VK_ESCAPE) { 217 window.close(); 218 } else if (event.keyCode === KeyEvent.DOM_VK_DELETE) { 219 this._deleteSelectedKeys(); 220 } 221 }, 222 223 /** 224 * Show an error, or clear it. 225 * 226 * @param {?string} messageId - The l10n ID of the message to show, or null to 227 * clear it. 228 */ 229 _showError(messageId) { 230 this._errorMessageContainer.classList.toggle("show-error", !!messageId); 231 if (messageId) { 232 document.l10n.setAttributes(this._errorMessageEl, messageId); 233 } else { 234 // Clean up. 235 this._errorMessageEl.removeAttribute("data-l10n-id"); 236 this._errorMessageEl.textContent = ""; 237 } 238 }, 239 240 // XUL tree widget view implementation. 241 get rowCount() { 242 return this._keyInfoList?.length ?? 0; 243 }, 244 245 getCellText(aRow, aCol) { 246 if (this._keyInfoList && aRow < this._keyInfoList.length) { 247 const keyInfo = this._keyInfoList[aRow]; 248 if (aCol.id.endsWith("-siteCol")) { 249 return keyInfo.address; 250 } else if (aCol.id.endsWith("-keyCol")) { 251 // keyType is always "x25519", so do not show it. 252 return keyInfo.keyBlob; 253 } 254 } 255 return ""; 256 }, 257 258 isSeparator(_index) { 259 return false; 260 }, 261 262 isSorted() { 263 return false; 264 }, 265 266 isContainer(_index) { 267 return false; 268 }, 269 270 setTree(_tree) {}, 271 272 getImageSrc(_row, _column) {}, 273 274 getCellValue(_row, _column) {}, 275 276 cycleHeader(_column) {}, 277 278 getRowProperties(_row) { 279 return ""; 280 }, 281 282 getColumnProperties(_column) { 283 return ""; 284 }, 285 286 getCellProperties(_row, _column) { 287 return ""; 288 }, 289 }; 290 291 window.addEventListener("load", () => gOnionServicesSavedKeysDialog._onLoad());