diff options
Diffstat (limited to 'comm/mail/components/preferences/passwordManager.js')
-rw-r--r-- | comm/mail/components/preferences/passwordManager.js | 819 |
1 files changed, 819 insertions, 0 deletions
diff --git a/comm/mail/components/preferences/passwordManager.js b/comm/mail/components/preferences/passwordManager.js new file mode 100644 index 0000000000..67f08767d3 --- /dev/null +++ b/comm/mail/components/preferences/passwordManager.js @@ -0,0 +1,819 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** * =================== SAVED SIGNONS CODE =================== */ +/* eslint-disable-next-line no-var */ +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +/* eslint-disable-next-line no-var */ +/* eslint-disable-next-line no-var */ +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", + OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", +}); + +// Default value for signon table sorting +let lastSignonSortColumn = "origin"; +let lastSignonSortAscending = true; + +let showingPasswords = false; + +// password-manager lists +let signons = []; +let deletedSignons = []; + +// Elements that would be used frequently +let filterField; +let togglePasswordsButton; +let signonsIntro; +let removeButton; +let removeAllButton; +let signonsTree; + +let signonReloadDisplay = { + observe(subject, topic, data) { + if (topic == "passwordmgr-storage-changed") { + switch (data) { + case "addLogin": + case "modifyLogin": + case "removeLogin": + case "removeAllLogins": + if (!signonsTree) { + return; + } + signons.length = 0; + LoadSignons(); + // apply the filter if needed + if (filterField && filterField.value != "") { + FilterPasswords(); + } + signonsTree.ensureRowIsVisible( + signonsTree.view.selection.currentIndex + ); + break; + } + Services.obs.notifyObservers(null, "passwordmgr-dialog-updated"); + } + }, +}; + +// Formatter for localization. +let dateFormatter = new Services.intl.DateTimeFormat(undefined, { + dateStyle: "medium", +}); +let dateAndTimeFormatter = new Services.intl.DateTimeFormat(undefined, { + dateStyle: "medium", + timeStyle: "short", +}); + +function Startup() { + // be prepared to reload the display if anything changes + Services.obs.addObserver(signonReloadDisplay, "passwordmgr-storage-changed"); + + signonsTree = document.getElementById("signonsTree"); + filterField = document.getElementById("filter"); + togglePasswordsButton = document.getElementById("togglePasswords"); + signonsIntro = document.getElementById("signonsIntro"); + removeButton = document.getElementById("removeSignon"); + removeAllButton = document.getElementById("removeAllSignons"); + + document.l10n.setAttributes(togglePasswordsButton, "show-passwords"); + document.l10n.setAttributes(signonsIntro, "logins-description-all"); + document.l10n.setAttributes(removeAllButton, "remove-all"); + + document + .getElementsByTagName("treecols")[0] + .addEventListener("click", event => { + let { target, button } = event; + let sortField = target.getAttribute("data-field-name"); + + if (target.nodeName != "treecol" || button != 0 || !sortField) { + return; + } + + SignonColumnSort(sortField); + }); + + LoadSignons(); + + // filter the table if requested by caller + if ( + window.arguments && + window.arguments[0] && + window.arguments[0].filterString + ) { + setFilter(window.arguments[0].filterString); + } + + FocusFilterBox(); + document.l10n + .translateElements(document.querySelectorAll("[data-l10n-id]")) + .then(() => window.sizeToContent()); +} + +function Shutdown() { + Services.obs.removeObserver( + signonReloadDisplay, + "passwordmgr-storage-changed" + ); +} + +function setFilter(aFilterString) { + filterField.value = aFilterString; + FilterPasswords(); +} + +let signonsTreeView = { + QueryInterface: ChromeUtils.generateQI(["nsITreeView"]), + _filterSet: [], + _lastSelectedRanges: [], + selection: null, + + rowCount: 0, + setTree(tree) {}, + getImageSrc(row, column) { + if (column.element.getAttribute("id") !== "providerCol") { + return ""; + } + + const signon = GetVisibleLogins()[row]; + + return PlacesUtils.urlWithSizeRef(window, "page-icon:" + signon.origin, 16); + }, + getCellValue(row, column) {}, + getCellText(row, column) { + let time; + let signon = GetVisibleLogins()[row]; + switch (column.id) { + case "providerCol": + return signon.httpRealm + ? signon.origin + " (" + signon.httpRealm + ")" + : signon.origin; + case "userCol": + return signon.username || ""; + case "passwordCol": + return signon.password || ""; + case "timeCreatedCol": + time = new Date(signon.timeCreated); + return dateFormatter.format(time); + case "timeLastUsedCol": + time = new Date(signon.timeLastUsed); + return dateAndTimeFormatter.format(time); + case "timePasswordChangedCol": + time = new Date(signon.timePasswordChanged); + return dateFormatter.format(time); + case "timesUsedCol": + return signon.timesUsed; + default: + return ""; + } + }, + isEditable(row, col) { + if (col.id == "userCol" || col.id == "passwordCol") { + return true; + } + return false; + }, + isSeparator(index) { + return false; + }, + isSorted() { + return false; + }, + isContainer(index) { + return false; + }, + cycleHeader(column) {}, + getRowProperties(row) { + return ""; + }, + getColumnProperties(column) { + return ""; + }, + getCellProperties(row, column) { + if (column.element.getAttribute("id") == "providerCol") { + return "ltr"; + } + + return ""; + }, + setCellText(row, col, value) { + let table = GetVisibleLogins(); + function _editLogin(field) { + if (value == table[row][field]) { + return; + } + let existingLogin = table[row].clone(); + table[row][field] = value; + table[row].timePasswordChanged = Date.now(); + Services.logins.modifyLogin(existingLogin, table[row]); + signonsTree.invalidateRow(row); + } + + if (col.id == "userCol") { + _editLogin("username"); + } else if (col.id == "passwordCol") { + if (!value) { + return; + } + _editLogin("password"); + } + }, +}; + +function SortTree(column, ascending) { + let table = GetVisibleLogins(); + // remember which item was selected so we can restore it after the sort + let selections = GetTreeSelections(); + let selectedNumber = selections.length ? table[selections[0]].number : -1; + function compareFunc(a, b) { + let valA, valB; + switch (column) { + case "origin": + let realmA = a.httpRealm; + let realmB = b.httpRealm; + realmA = realmA == null ? "" : realmA.toLowerCase(); + realmB = realmB == null ? "" : realmB.toLowerCase(); + + valA = a[column].toLowerCase() + realmA; + valB = b[column].toLowerCase() + realmB; + break; + case "username": + case "password": + valA = a[column].toLowerCase(); + valB = b[column].toLowerCase(); + break; + + default: + valA = a[column]; + valB = b[column]; + } + + if (valA < valB) { + return -1; + } + if (valA > valB) { + return 1; + } + return 0; + } + + // do the sort + table.sort(compareFunc); + if (!ascending) { + table.reverse(); + } + + // restore the selection + let selectedRow = -1; + if (selectedNumber >= 0 && false) { + for (let s = 0; s < table.length; s++) { + if (table[s].number == selectedNumber) { + // update selection + // note: we need to deselect before reselecting in order to trigger ...Selected() + signonsTree.view.selection.select(-1); + signonsTree.view.selection.select(s); + selectedRow = s; + break; + } + } + } + + // display the results + signonsTree.invalidate(); + if (selectedRow >= 0) { + signonsTree.ensureRowIsVisible(selectedRow); + } +} + +function LoadSignons() { + // loads signons into table + try { + signons = Services.logins.getAllLogins(); + } catch (e) { + signons = []; + } + signons.forEach(login => login.QueryInterface(Ci.nsILoginMetaInfo)); + signonsTreeView.rowCount = signons.length; + + // sort and display the table + signonsTree.view = signonsTreeView; + // The sort column didn't change. SortTree (called by + // SignonColumnSort) assumes we want to toggle the sort + // direction but here we don't so we have to trick it + lastSignonSortAscending = !lastSignonSortAscending; + SignonColumnSort(lastSignonSortColumn); + + // disable "remove all signons" button if there are no signons + if (signons.length == 0) { + removeAllButton.setAttribute("disabled", "true"); + togglePasswordsButton.setAttribute("disabled", "true"); + } else { + removeAllButton.removeAttribute("disabled"); + togglePasswordsButton.removeAttribute("disabled"); + } + + return true; +} + +function GetVisibleLogins() { + return signonsTreeView._filterSet.length + ? signonsTreeView._filterSet + : signons; +} + +function GetTreeSelections() { + let selections = []; + let select = signonsTree.view.selection; + if (select) { + let count = select.getRangeCount(); + let min = {}; + let max = {}; + for (let i = 0; i < count; i++) { + select.getRangeAt(i, min, max); + for (let k = min.value; k <= max.value; k++) { + if (k != -1) { + selections[selections.length] = k; + } + } + } + } + return selections; +} + +function SignonSelected() { + let selections = GetTreeSelections(); + if (selections.length) { + removeButton.removeAttribute("disabled"); + } else { + removeButton.setAttribute("disabled", true); + } +} + +function DeleteSignon() { + let syncNeeded = signonsTreeView._filterSet.length != 0; + let tree = signonsTree; + let view = signonsTreeView; + let table = GetVisibleLogins(); + + // Turn off tree selection notifications during the deletion + tree.view.selection.selectEventsSuppressed = true; + + // remove selected items from list (by setting them to null) and place in deleted list + let selections = GetTreeSelections(); + for (let s = selections.length - 1; s >= 0; s--) { + let i = selections[s]; + deletedSignons.push(table[i]); + table[i] = null; + } + + // collapse list by removing all the null entries + for (let j = 0; j < table.length; j++) { + if (table[j] == null) { + let k = j; + while (k < table.length && table[k] == null) { + k++; + } + table.splice(j, k - j); + view.rowCount -= k - j; + tree.rowCountChanged(j, j - k); + } + } + + // update selection and/or buttons + if (table.length) { + // update selection + let nextSelection = + selections[0] < table.length ? selections[0] : table.length - 1; + tree.view.selection.select(nextSelection); + } else { + // disable buttons + removeButton.setAttribute("disabled", "true"); + removeAllButton.setAttribute("disabled", "true"); + } + tree.view.selection.selectEventsSuppressed = false; + FinalizeSignonDeletions(syncNeeded); +} + +async function DeleteAllSignons() { + // Confirm the user wants to remove all passwords + let dummy = { value: false }; + let [title, message] = await document.l10n.formatValues([ + { id: "remove-all-passwords-title" }, + { id: "remove-all-passwords-prompt" }, + ]); + if ( + Services.prompt.confirmEx( + window, + title, + message, + Services.prompt.STD_YES_NO_BUTTONS + Services.prompt.BUTTON_POS_1_DEFAULT, + null, + null, + null, + null, + dummy + ) == 1 + ) { + // 1 == "No" button + return; + } + + let syncNeeded = signonsTreeView._filterSet.length != 0; + let view = signonsTreeView; + let table = GetVisibleLogins(); + + // remove all items from table and place in deleted table + for (let i = 0; i < table.length; i++) { + deletedSignons.push(table[i]); + } + table.length = 0; + + // clear out selections + view.selection.select(-1); + + // update the tree view and notify the tree + view.rowCount = 0; + + signonsTree.rowCountChanged(0, -deletedSignons.length); + signonsTree.invalidate(); + + // disable buttons + removeButton.setAttribute("disabled", "true"); + removeAllButton.setAttribute("disabled", "true"); + FinalizeSignonDeletions(syncNeeded); +} + +async function TogglePasswordVisible() { + if (showingPasswords || (await masterPasswordLogin(AskUserShowPasswords))) { + showingPasswords = !showingPasswords; + document.l10n.setAttributes( + togglePasswordsButton, + showingPasswords ? "hide-passwords" : "show-passwords" + ); + document.getElementById("passwordCol").hidden = !showingPasswords; + FilterPasswords(); + } + + // Notify observers that the password visibility toggling is + // completed. (Mostly useful for tests) + Services.obs.notifyObservers(null, "passwordmgr-password-toggle-complete"); +} + +async function AskUserShowPasswords() { + let dummy = { value: false }; + + // Confirm the user wants to display passwords + return ( + Services.prompt.confirmEx( + window, + null, + await document.l10n.formatValue("no-master-password-prompt"), + Services.prompt.STD_YES_NO_BUTTONS, + null, + null, + null, + null, + dummy + ) == 0 + ); // 0=="Yes" button +} + +function FinalizeSignonDeletions(syncNeeded) { + for (let s = 0; s < deletedSignons.length; s++) { + Services.logins.removeLogin(deletedSignons[s]); + } + // If the deletion has been performed in a filtered view, reflect the deletion in the unfiltered table. + // See bug 405389. + if (syncNeeded) { + try { + signons = Services.logins.getAllLogins(); + } catch (e) { + signons = []; + } + } + deletedSignons.length = 0; +} + +function HandleSignonKeyPress(e) { + // If editing is currently performed, don't do anything. + if (signonsTree.getAttribute("editing")) { + return; + } + if ( + e.keyCode == KeyboardEvent.DOM_VK_DELETE || + (AppConstants.platform == "macosx" && + e.keyCode == KeyboardEvent.DOM_VK_BACK_SPACE) + ) { + DeleteSignon(); + e.preventDefault(); + } +} + +function getColumnByName(column) { + switch (column) { + case "origin": + return document.getElementById("providerCol"); + case "username": + return document.getElementById("userCol"); + case "password": + return document.getElementById("passwordCol"); + case "timeCreated": + return document.getElementById("timeCreatedCol"); + case "timeLastUsed": + return document.getElementById("timeLastUsedCol"); + case "timePasswordChanged": + return document.getElementById("timePasswordChangedCol"); + case "timesUsed": + return document.getElementById("timesUsedCol"); + } + return undefined; +} + +function SignonColumnSort(column) { + let sortedCol = getColumnByName(column); + let lastSortedCol = getColumnByName(lastSignonSortColumn); + + // clear out the sortDirection attribute on the old column + lastSortedCol.removeAttribute("sortDirection"); + + // determine if sort is to be ascending or descending + lastSignonSortAscending = + column == lastSignonSortColumn ? !lastSignonSortAscending : true; + + // sort + lastSignonSortColumn = column; + SortTree(lastSignonSortColumn, lastSignonSortAscending); + + // set the sortDirection attribute to get the styling going + // first we need to get the right element + sortedCol.setAttribute( + "sortDirection", + lastSignonSortAscending ? "ascending" : "descending" + ); +} + +function SignonClearFilter() { + let singleSelection = signonsTreeView.selection.count == 1; + + // Clear the Tree Display + signonsTreeView.rowCount = 0; + signonsTree.rowCountChanged(0, -signonsTreeView._filterSet.length); + signonsTreeView._filterSet = []; + + // Just reload the list to make sure deletions are respected + LoadSignons(); + + // Restore selection + if (singleSelection) { + signonsTreeView.selection.clearSelection(); + for (let i = 0; i < signonsTreeView._lastSelectedRanges.length; ++i) { + let range = signonsTreeView._lastSelectedRanges[i]; + signonsTreeView.selection.rangedSelect(range.min, range.max, true); + } + } else { + signonsTreeView.selection.select(0); + } + signonsTreeView._lastSelectedRanges = []; + + document.l10n.setAttributes(signonsIntro, "logins-description-all"); + document.l10n.setAttributes(removeAllButton, "remove-all"); +} + +function FocusFilterBox() { + if (filterField.getAttribute("focused") != "true") { + filterField.focus(); + } +} + +function SignonMatchesFilter(aSignon, aFilterValue) { + if (aSignon.origin.toLowerCase().includes(aFilterValue)) { + return true; + } + if ( + aSignon.username && + aSignon.username.toLowerCase().includes(aFilterValue) + ) { + return true; + } + if ( + aSignon.httpRealm && + aSignon.httpRealm.toLowerCase().includes(aFilterValue) + ) { + return true; + } + if ( + showingPasswords && + aSignon.password && + aSignon.password.toLowerCase().includes(aFilterValue) + ) { + return true; + } + + return false; +} + +function _filterPasswords(aFilterValue, view) { + aFilterValue = aFilterValue.toLowerCase(); + return signons.filter(s => SignonMatchesFilter(s, aFilterValue)); +} + +function SignonSaveState() { + // Save selection + let seln = signonsTreeView.selection; + signonsTreeView._lastSelectedRanges = []; + let rangeCount = seln.getRangeCount(); + for (let i = 0; i < rangeCount; ++i) { + let min = {}; + let max = {}; + seln.getRangeAt(i, min, max); + signonsTreeView._lastSelectedRanges.push({ + min: min.value, + max: max.value, + }); + } +} + +function FilterPasswords() { + if (filterField.value == "") { + SignonClearFilter(); + return; + } + + let newFilterSet = _filterPasswords(filterField.value, signonsTreeView); + if (!signonsTreeView._filterSet.length) { + // Save Display Info for the Non-Filtered mode when we first + // enter Filtered mode. + SignonSaveState(); + } + signonsTreeView._filterSet = newFilterSet; + + // Clear the display + let oldRowCount = signonsTreeView.rowCount; + signonsTreeView.rowCount = 0; + signonsTree.rowCountChanged(0, -oldRowCount); + // Set up the filtered display + signonsTreeView.rowCount = signonsTreeView._filterSet.length; + signonsTree.rowCountChanged(0, signonsTreeView.rowCount); + + // if the view is not empty then select the first item + if (signonsTreeView.rowCount > 0) { + signonsTreeView.selection.select(0); + } + + document.l10n.setAttributes(signonsIntro, "logins-description-filtered"); + document.l10n.setAttributes(removeAllButton, "remove-all-shown"); +} + +function CopyProviderUrl() { + // Copy selected provider url to clipboard + let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( + Ci.nsIClipboardHelper + ); + let row = signonsTree.currentIndex; + let url = signonsTreeView.getCellText(row, { id: "providerCol" }); + clipboard.copyString(url); +} + +async function CopyPassword() { + // Don't copy passwords if we aren't already showing the passwords & a master + // password hasn't been entered. + if (!showingPasswords && !(await masterPasswordLogin())) { + return; + } + // Copy selected signon's password to clipboard + let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( + Ci.nsIClipboardHelper + ); + let row = signonsTree.currentIndex; + let password = signonsTreeView.getCellText(row, { id: "passwordCol" }); + clipboard.copyString(password); +} + +function CopyUsername() { + // Copy selected signon's username to clipboard + let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( + Ci.nsIClipboardHelper + ); + let row = signonsTree.currentIndex; + let username = signonsTreeView.getCellText(row, { id: "userCol" }); + clipboard.copyString(username); +} + +function EditCellInSelectedRow(columnName) { + let row = signonsTree.currentIndex; + let columnElement = getColumnByName(columnName); + signonsTree.startEditing( + row, + signonsTree.columns.getColumnFor(columnElement) + ); +} + +function UpdateContextMenu() { + let singleSelection = signonsTreeView.selection.count == 1; + let menuItems = new Map(); + let menupopup = document.getElementById("signonsTreeContextMenu"); + for (let menuItem of menupopup.querySelectorAll("menuitem")) { + menuItems.set(menuItem.id, menuItem); + } + + if (!singleSelection) { + for (let menuItem of menuItems.values()) { + menuItem.setAttribute("disabled", "true"); + } + return; + } + + let selectedRow = signonsTree.currentIndex; + + // Disable "Copy Username" if the username is empty. + if (signonsTreeView.getCellText(selectedRow, { id: "userCol" }) != "") { + menuItems.get("context-copyusername").removeAttribute("disabled"); + } else { + menuItems.get("context-copyusername").setAttribute("disabled", "true"); + } + + menuItems.get("context-copyproviderurl").removeAttribute("disabled"); + menuItems.get("context-editusername").removeAttribute("disabled"); + menuItems.get("context-copypassword").removeAttribute("disabled"); + + // Disable "Edit Password" if the password column isn't showing. + if (!document.getElementById("passwordCol").hidden) { + menuItems.get("context-editpassword").removeAttribute("disabled"); + } else { + menuItems.get("context-editpassword").setAttribute("disabled", "true"); + } +} + +async function masterPasswordLogin(noPasswordCallback) { + // This doesn't harm if passwords are not encrypted + let tokendb = Cc["@mozilla.org/security/pk11tokendb;1"].createInstance( + Ci.nsIPK11TokenDB + ); + let token = tokendb.getInternalKeyToken(); + + // If there is no primary password, still give the user a chance to opt-out of displaying passwords + if (token.checkPassword("")) { + // The OS re-authentication on Linux isn't working (Bug 1527745), + // still add the confirm dialog for Linux. + if ( + Services.prefs.getBoolPref("signon.management.page.os-auth.enabled") && + AppConstants.platform !== "linux" + ) { + // Require OS authentication before the user can show the passwords or copy them. + let messageId = "password-os-auth-dialog-message"; + if (AppConstants.platform == "macosx") { + // MacOS requires a special format of this dialog string. + // See preferences.ftl for more information. + messageId += "-macosx"; + } + let [messageText, captionText] = await document.l10n.formatMessages([ + { + id: messageId, + }, + { + id: "password-os-auth-dialog-caption", + }, + ]); + let win = Services.wm.getMostRecentWindow(""); + let loggedIn = await OSKeyStore.ensureLoggedIn( + messageText.value, + captionText.value, + win, + false + ); + if (!loggedIn.authenticated) { + return false; + } + return true; + } + return noPasswordCallback ? noPasswordCallback() : true; + } + + // So there's a primary password. But since checkPassword didn't succeed, we're logged out (per nsIPK11Token.idl). + try { + // Relogin and ask for the primary password. + token.login(true); // 'true' means always prompt for token password. User will be prompted until + // clicking 'Cancel' or entering the correct password. + } catch (e) { + // An exception will be thrown if the user cancels the login prompt dialog. + // User is also logged out of Software Security Device. + } + + return token.isLoggedIn(); +} + +function escapeKeyHandler() { + // If editing is currently performed, don't do anything. + if (signonsTree.getAttribute("editing")) { + return; + } + window.close(); +} |