aboutLogins.mjs (9875B)
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 { 6 recordTelemetryEvent, 7 setKeyboardAccessForNonDialogElements, 8 } from "./aboutLoginsUtils.mjs"; 9 10 // The init code isn't wrapped in a DOMContentLoaded/load event listener so the 11 // page works properly when restored from session restore. 12 const gElements = { 13 fxAccountsButton: document.querySelector("fxaccounts-button"), 14 loginList: document.querySelector("login-list"), 15 loginIntro: document.querySelector("login-intro"), 16 loginItem: document.querySelector("login-item"), 17 loginFilter: document 18 .querySelector("login-list") 19 .shadowRoot.querySelector("login-filter"), 20 menuButton: document.querySelector("menu-button"), 21 get exportButton() { 22 return this.menuButton.shadowRoot.querySelector(".menuitem-export"); 23 }, 24 // removeAllLogins button is nested inside of menuButton 25 get removeAllButton() { 26 return this.menuButton.shadowRoot.querySelector( 27 ".menuitem-remove-all-logins" 28 ); 29 }, 30 }; 31 32 let numberOfLogins = 0; 33 34 function updateNoLogins() { 35 document.documentElement.classList.toggle("no-logins", numberOfLogins == 0); 36 gElements.loginList.classList.toggle("no-logins", numberOfLogins == 0); 37 gElements.loginItem.classList.toggle("no-logins", numberOfLogins == 0); 38 gElements.exportButton.disabled = numberOfLogins == 0; 39 gElements.removeAllButton.disabled = numberOfLogins == 0; 40 } 41 42 function handleAllLogins(logins) { 43 gElements.loginList.setLogins(logins); 44 numberOfLogins = logins.length; 45 updateNoLogins(); 46 } 47 48 let fxaLoggedIn = null; 49 let passwordSyncEnabled = null; 50 51 function handleSyncState(syncState) { 52 gElements.fxAccountsButton.updateState(syncState); 53 gElements.loginIntro.updateState(syncState); 54 fxaLoggedIn = syncState.loggedIn; 55 passwordSyncEnabled = syncState.passwordSyncEnabled; 56 } 57 58 window.addEventListener("AboutLoginsChromeToContent", event => { 59 switch (event.detail.messageType) { 60 case "AllLogins": { 61 document.documentElement.classList.remove( 62 "primary-password-auth-required" 63 ); 64 setKeyboardAccessForNonDialogElements(true); 65 handleAllLogins(event.detail.value); 66 break; 67 } 68 case "ImportPasswordsDialog": { 69 let dialog = document.querySelector("import-summary-dialog"); 70 let options = { 71 logins: event.detail.value, 72 }; 73 dialog.show(options); 74 break; 75 } 76 case "ImportPasswordsErrorDialog": { 77 let dialog = document.querySelector("import-error-dialog"); 78 dialog.show(event.detail.value); 79 break; 80 } 81 case "LoginAdded": { 82 gElements.loginList.loginAdded(event.detail.value); 83 gElements.loginItem.loginAdded(event.detail.value); 84 numberOfLogins++; 85 updateNoLogins(); 86 break; 87 } 88 case "LoginModified": { 89 gElements.loginList.loginModified(event.detail.value); 90 gElements.loginItem.loginModified(event.detail.value); 91 break; 92 } 93 case "LoginRemoved": { 94 // The loginRemoved function of loginItem needs to be called before 95 // the one in loginList since it will remove the editing. So that the 96 // discard dialog won't show up if we delete a login after edit it. 97 gElements.loginItem.loginRemoved(event.detail.value); 98 gElements.loginList.loginRemoved(event.detail.value); 99 numberOfLogins--; 100 updateNoLogins(); 101 break; 102 } 103 case "PrimaryPasswordAuthRequired": { 104 document.documentElement.classList.add("primary-password-auth-required"); 105 setKeyboardAccessForNonDialogElements(false); 106 break; 107 } 108 case "RemaskPassword": { 109 window.dispatchEvent(new CustomEvent("AboutLoginsRemaskPassword")); 110 break; 111 } 112 case "RemoveAllLogins": { 113 handleAllLogins(event.detail.value); 114 document.documentElement.classList.remove("login-selected"); 115 break; 116 } 117 case "SetBreaches": { 118 gElements.loginList.setBreaches(event.detail.value); 119 gElements.loginItem.setBreaches(event.detail.value); 120 break; 121 } 122 case "SetVulnerableLogins": { 123 gElements.loginList.setVulnerableLogins(event.detail.value); 124 gElements.loginItem.setVulnerableLogins(event.detail.value); 125 break; 126 } 127 case "Setup": { 128 gElements.loginList.selectLoginByDomainOrGuid( 129 event.detail.value.preselectedLogin 130 ); 131 handleAllLogins(event.detail.value.logins); 132 handleSyncState(event.detail.value.syncState); 133 gElements.loginList.setSortDirection(event.detail.value.selectedSort); 134 document.documentElement.classList.add("initialized"); 135 gElements.loginList.classList.add("initialized"); 136 gElements.loginList.canCreateLogins = event.detail.value.canCreateLogins; 137 break; 138 } 139 case "ShowLoginItemError": { 140 gElements.loginItem.showLoginItemError(event.detail.value); 141 break; 142 } 143 case "SyncState": { 144 handleSyncState(event.detail.value); 145 break; 146 } 147 case "UpdateBreaches": { 148 gElements.loginList.updateBreaches(event.detail.value); 149 gElements.loginItem.updateBreaches(event.detail.value); 150 break; 151 } 152 case "UpdateVulnerableLogins": { 153 gElements.loginList.updateVulnerableLogins(event.detail.value); 154 gElements.loginItem.updateVulnerableLogins(event.detail.value); 155 break; 156 } 157 } 158 }); 159 160 window.addEventListener("AboutLoginsRemoveAllLoginsDialog", () => { 161 let loginItem = document.querySelector("login-item"); 162 let options = {}; 163 if (fxaLoggedIn && passwordSyncEnabled) { 164 options.title = "about-logins-confirm-remove-all-sync-dialog-title2"; 165 options.message = "about-logins-confirm-remove-all-sync-dialog-message3"; 166 } else { 167 options.title = "about-logins-confirm-remove-all-dialog-title2"; 168 options.message = "about-logins-confirm-remove-all-dialog-message2"; 169 } 170 options.confirmCheckboxLabel = 171 "about-logins-confirm-remove-all-dialog-checkbox-label2"; 172 options.confirmButtonLabel = 173 "about-logins-confirm-remove-all-dialog-confirm-button-label"; 174 options.count = numberOfLogins; 175 176 let dialog = document.querySelector("remove-logins-dialog"); 177 let dialogPromise = dialog.show(options); 178 try { 179 dialogPromise.then( 180 () => { 181 if (loginItem.dataset.isNewLogin) { 182 // Bug 1681042 - Resetting the form prevents a double confirmation dialog since there 183 // may be pending changes in the new login. 184 loginItem.resetForm(); 185 window.dispatchEvent(new CustomEvent("AboutLoginsClearSelection")); 186 } else if (loginItem.dataset.editing) { 187 loginItem._toggleEditing(); 188 } 189 window.document.documentElement.classList.remove("login-selected"); 190 let removeAllEvt = new CustomEvent("AboutLoginsRemoveAllLogins", { 191 bubbles: true, 192 }); 193 window.dispatchEvent(removeAllEvt); 194 }, 195 () => {} 196 ); 197 } catch (e) { 198 if (e != undefined) { 199 throw e; 200 } 201 } 202 }); 203 204 window.addEventListener("AboutLoginsExportPasswordsDialog", async () => { 205 recordTelemetryEvent({ 206 name: "mgmtMenuItemUsedExport", 207 }); 208 let dialog = document.querySelector("confirmation-dialog"); 209 let options = { 210 title: "about-logins-confirm-export-dialog-title2", 211 message: "about-logins-confirm-export-dialog-message2", 212 confirmButtonLabel: "about-logins-confirm-export-dialog-confirm-button2", 213 }; 214 try { 215 await dialog.show(options); 216 document.dispatchEvent( 217 new CustomEvent("AboutLoginsExportPasswords", { bubbles: true }) 218 ); 219 } catch (ex) { 220 // The user cancelled the dialog. 221 } 222 }); 223 224 async function interceptFocusKey() { 225 // Intercept Ctrl+F on the page to focus login filter box 226 const [findKey] = await document.l10n.formatMessages([ 227 { id: "about-logins-login-filter2" }, 228 ]); 229 const focusKey = findKey.attributes 230 .find(a => a.name == "key") 231 .value.toLowerCase(); 232 document.addEventListener("keydown", event => { 233 if (event.key == focusKey && event.getModifierState("Accel")) { 234 event.preventDefault(); 235 document 236 .querySelector("login-list") 237 .shadowRoot.querySelector("login-filter") 238 .shadowRoot.querySelector("input") 239 .focus(); 240 } 241 }); 242 } 243 244 await interceptFocusKey(); 245 246 // Begin code that executes on page load. 247 248 let searchParamsChanged = false; 249 let { protocol, pathname, searchParams } = new URL(document.location); 250 251 recordTelemetryEvent({ 252 name: "openManagement" + (searchParams.get("entryPoint") || "Direct"), 253 }); 254 255 if (searchParams.has("entryPoint")) { 256 // Remove this parameter from the URL (after recording above) to make it 257 // cleaner for bookmarking and switch-to-tab and so that bookmarked values 258 // don't skew telemetry. 259 searchParams.delete("entryPoint"); 260 searchParamsChanged = true; 261 } 262 263 if (searchParams.has("filter")) { 264 let filter = searchParams.get("filter"); 265 if (!filter) { 266 // Remove empty `filter` params to give a cleaner URL for bookmarking and 267 // switch-to-tab 268 searchParams.delete("filter"); 269 searchParamsChanged = true; 270 } 271 } 272 273 if (searchParamsChanged) { 274 const paramsPart = searchParams.toString() ? `?${searchParams}` : ""; 275 const newURL = protocol + pathname + paramsPart + document.location.hash; 276 // This redirect doesn't stop this script from running so ensure you guard 277 // later code if it shouldn't run before and after the redirect. 278 window.location.replace(newURL); 279 } else if (searchParams.has("filter")) { 280 // This must be after the `location.replace` so it doesn't cause telemetry to 281 // record a filter event before the navigation to clean the URL. 282 gElements.loginFilter.value = searchParams.get("filter"); 283 } 284 285 if (!searchParamsChanged) { 286 gElements.loginFilter.focus(); 287 document.dispatchEvent(new CustomEvent("AboutLoginsInit", { bubbles: true })); 288 }