/* 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(); }