summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/content
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/content')
-rw-r--r--toolkit/components/passwordmgr/content/passwordManager.js728
-rw-r--r--toolkit/components/passwordmgr/content/passwordManager.xul134
-rw-r--r--toolkit/components/passwordmgr/content/recipes.json31
3 files changed, 893 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/content/passwordManager.js b/toolkit/components/passwordmgr/content/passwordManager.js
new file mode 100644
index 000000000..333dc1d24
--- /dev/null
+++ b/toolkit/components/passwordmgr/content/passwordManager.js
@@ -0,0 +1,728 @@
+/* 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 =================== ***/
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
+ "resource://gre/modules/DeferredTask.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+
+let kSignonBundle;
+
+// Default value for signon table sorting
+let lastSignonSortColumn = "hostname";
+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: function(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();
+ }
+ break;
+ }
+ Services.obs.notifyObservers(null, "passwordmgr-dialog-updated", null);
+ }
+ }
+};
+
+// Formatter for localization.
+let dateFormatter = new Intl.DateTimeFormat(undefined,
+ { day: "numeric", month: "short", year: "numeric" });
+let dateAndTimeFormatter = new Intl.DateTimeFormat(undefined,
+ { day: "numeric", month: "short", year: "numeric",
+ hour: "numeric", minute: "numeric" });
+
+function Startup() {
+ // be prepared to reload the display if anything changes
+ Services.obs.addObserver(signonReloadDisplay, "passwordmgr-storage-changed", false);
+
+ signonsTree = document.getElementById("signonsTree");
+ kSignonBundle = document.getElementById("signonBundle");
+ filterField = document.getElementById("filter");
+ togglePasswordsButton = document.getElementById("togglePasswords");
+ signonsIntro = document.getElementById("signonsIntro");
+ removeButton = document.getElementById("removeSignon");
+ removeAllButton = document.getElementById("removeAllSignons");
+
+ togglePasswordsButton.label = kSignonBundle.getString("showPasswords");
+ togglePasswordsButton.accessKey = kSignonBundle.getString("showPasswordsAccessKey");
+ signonsIntro.textContent = kSignonBundle.getString("loginsDescriptionAll");
+ 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);
+ Services.telemetry.getKeyedHistogramById("PWMGR_MANAGE_SORTED").add(sortField);
+ });
+
+ LoadSignons();
+
+ // filter the table if requested by caller
+ if (window.arguments &&
+ window.arguments[0] &&
+ window.arguments[0].filterString) {
+ setFilter(window.arguments[0].filterString);
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_OPENED").add(1);
+ } else {
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_OPENED").add(0);
+ }
+
+ FocusFilterBox();
+}
+
+function Shutdown() {
+ Services.obs.removeObserver(signonReloadDisplay, "passwordmgr-storage-changed");
+}
+
+function setFilter(aFilterString) {
+ filterField.value = aFilterString;
+ FilterPasswords();
+}
+
+let signonsTreeView = {
+ // Keep track of which favicons we've fetched or started fetching.
+ // Maps a login origin to a favicon URL.
+ _faviconMap: new Map(),
+ _filterSet: [],
+ // Coalesce invalidations to avoid repeated flickering.
+ _invalidateTask: new DeferredTask(() => {
+ signonsTree.treeBoxObject.invalidateColumn(signonsTree.columns.siteCol);
+ }, 10),
+ _lastSelectedRanges: [],
+ selection: null,
+
+ rowCount: 0,
+ setTree(tree) {},
+ getImageSrc(row, column) {
+ if (column.element.getAttribute("id") !== "siteCol") {
+ return "";
+ }
+
+ const signon = this._filterSet.length ? this._filterSet[row] : signons[row];
+
+ // We already have the favicon URL or we started to fetch (value is null).
+ if (this._faviconMap.has(signon.hostname)) {
+ return this._faviconMap.get(signon.hostname);
+ }
+
+ // Record the fact that we already starting fetching a favicon for this
+ // origin in order to avoid multiple requests for the same origin.
+ this._faviconMap.set(signon.hostname, null);
+
+ PlacesUtils.promiseFaviconLinkUrl(signon.hostname)
+ .then(faviconURI => {
+ this._faviconMap.set(signon.hostname, faviconURI.spec);
+ this._invalidateTask.arm();
+ }).catch(Cu.reportError);
+
+ return "";
+ },
+ getProgressMode(row, column) {},
+ getCellValue(row, column) {},
+ getCellText(row, column) {
+ let time;
+ let signon = this._filterSet.length ? this._filterSet[row] : signons[row];
+ switch (column.id) {
+ case "siteCol":
+ return signon.httpRealm ?
+ (signon.hostname + " (" + signon.httpRealm + ")") :
+ signon.hostname;
+ 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") == "siteCol")
+ return "ltr";
+
+ return "";
+ },
+ setCellText(row, col, value) {
+ // If there is a filter, _filterSet needs to be used, otherwise signons is used.
+ let table = signonsTreeView._filterSet.length ? signonsTreeView._filterSet : signons;
+ 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.treeBoxObject.invalidateRow(row);
+ }
+
+ if (col.id == "userCol") {
+ _editLogin("username");
+
+ } else if (col.id == "passwordCol") {
+ if (!value) {
+ return;
+ }
+ _editLogin("password");
+ }
+ },
+};
+
+function SortTree(column, ascending) {
+ let table = signonsTreeView._filterSet.length ? signonsTreeView._filterSet : signons;
+ // 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 "hostname":
+ 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.treeBoxObject.invalidate();
+ if (selectedRow >= 0) {
+ signonsTree.treeBoxObject.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 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 filterSet = signonsTreeView._filterSet;
+ let syncNeeded = (filterSet.length != 0);
+ let tree = signonsTree;
+ let view = signonsTreeView;
+ let table = filterSet.length ? filterSet : signons;
+
+ // 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.treeBoxObject.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);
+ tree.treeBoxObject.ensureRowIsVisible(nextSelection);
+ } else {
+ // disable buttons
+ removeButton.setAttribute("disabled", "true");
+ removeAllButton.setAttribute("disabled", "true");
+ }
+ tree.view.selection.selectEventsSuppressed = false;
+ FinalizeSignonDeletions(syncNeeded);
+}
+
+function DeleteAllSignons() {
+ let prompter = Cc["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Ci.nsIPromptService);
+
+ // Confirm the user wants to remove all passwords
+ let dummy = { value: false };
+ if (prompter.confirmEx(window,
+ kSignonBundle.getString("removeAllPasswordsTitle"),
+ kSignonBundle.getString("removeAllPasswordsPrompt"),
+ prompter.STD_YES_NO_BUTTONS + prompter.BUTTON_POS_1_DEFAULT,
+ null, null, null, null, dummy) == 1) // 1 == "No" button
+ return;
+
+ let filterSet = signonsTreeView._filterSet;
+ let syncNeeded = (filterSet.length != 0);
+ let view = signonsTreeView;
+ let table = filterSet.length ? filterSet : signons;
+
+ // 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;
+
+ let box = signonsTree.treeBoxObject;
+ box.rowCountChanged(0, -deletedSignons.length);
+ box.invalidate();
+
+ // disable buttons
+ removeButton.setAttribute("disabled", "true");
+ removeAllButton.setAttribute("disabled", "true");
+ FinalizeSignonDeletions(syncNeeded);
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_DELETED_ALL").add(1);
+}
+
+function TogglePasswordVisible() {
+ if (showingPasswords || masterPasswordLogin(AskUserShowPasswords)) {
+ showingPasswords = !showingPasswords;
+ togglePasswordsButton.label = kSignonBundle.getString(showingPasswords ? "hidePasswords" : "showPasswords");
+ togglePasswordsButton.accessKey = kSignonBundle.getString(showingPasswords ? "hidePasswordsAccessKey" : "showPasswordsAccessKey");
+ 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", null);
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_VISIBILITY_TOGGLED").add(showingPasswords);
+}
+
+function AskUserShowPasswords() {
+ let prompter = Cc["@mozilla.org/embedcomp/prompt-service;1"].getService(Ci.nsIPromptService);
+ let dummy = { value: false };
+
+ // Confirm the user wants to display passwords
+ return prompter.confirmEx(window,
+ null,
+ kSignonBundle.getString("noMasterPasswordPrompt"), prompter.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]);
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_DELETED").add(1);
+ }
+ // 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();
+ }
+}
+
+function getColumnByName(column) {
+ switch (column) {
+ case "hostname":
+ return document.getElementById("siteCol");
+ 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.treeBoxObject.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 = [];
+
+ signonsIntro.textContent = kSignonBundle.getString("loginsDescriptionAll");
+}
+
+function FocusFilterBox() {
+ if (filterField.getAttribute("focused") != "true") {
+ filterField.focus();
+ }
+}
+
+function SignonMatchesFilter(aSignon, aFilterValue) {
+ if (aSignon.hostname.toLowerCase().indexOf(aFilterValue) != -1)
+ return true;
+ if (aSignon.username &&
+ aSignon.username.toLowerCase().indexOf(aFilterValue) != -1)
+ return true;
+ if (aSignon.httpRealm &&
+ aSignon.httpRealm.toLowerCase().indexOf(aFilterValue) != -1)
+ return true;
+ if (showingPasswords && aSignon.password &&
+ aSignon.password.toLowerCase().indexOf(aFilterValue) != -1)
+ 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.treeBoxObject.rowCountChanged(0, -oldRowCount);
+ // Set up the filtered display
+ signonsTreeView.rowCount = signonsTreeView._filterSet.length;
+ signonsTree.treeBoxObject.rowCountChanged(0, signonsTreeView.rowCount);
+
+ // if the view is not empty then select the first item
+ if (signonsTreeView.rowCount > 0)
+ signonsTreeView.selection.select(0);
+
+ signonsIntro.textContent = kSignonBundle.getString("loginsDescriptionFiltered");
+}
+
+function CopyPassword() {
+ // Don't copy passwords if we aren't already showing the passwords & a master
+ // password hasn't been entered.
+ if (!showingPasswords && !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);
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_COPIED_PASSWORD").add(1);
+}
+
+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);
+ Services.telemetry.getHistogramById("PWMGR_MANAGE_COPIED_USERNAME").add(1);
+}
+
+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-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");
+ }
+}
+
+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 master password, still give the user a chance to opt-out of displaying passwords
+ if (token.checkPassword(""))
+ return noPasswordCallback ? noPasswordCallback() : true;
+
+ // So there's a master password. But since checkPassword didn't succeed, we're logged out (per nsIPK11Token.idl).
+ try {
+ // Relogin and ask for the master 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();
+}
+
+function OpenMigrator() {
+ const { MigrationUtils } = Cu.import("resource:///modules/MigrationUtils.jsm", {});
+ // We pass in the type of source we're using for use in telemetry:
+ MigrationUtils.showMigrationWizard(window, [MigrationUtils.MIGRATION_ENTRYPOINT_PASSWORDS]);
+}
diff --git a/toolkit/components/passwordmgr/content/passwordManager.xul b/toolkit/components/passwordmgr/content/passwordManager.xul
new file mode 100644
index 000000000..d248283b6
--- /dev/null
+++ b/toolkit/components/passwordmgr/content/passwordManager.xul
@@ -0,0 +1,134 @@
+<?xml version="1.0"?> <!-- -*- Mode: SGML; indent-tabs-mode: nil -*- -->
+# 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/.
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://global/skin/passwordmgr.css" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://passwordmgr/locale/passwordManager.dtd" >
+
+<window id="SignonViewerDialog"
+ windowtype="Toolkit:PasswordManager"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="Startup();"
+ onunload="Shutdown();"
+ title="&savedLogins.title;"
+ style="width: 45em;"
+ persist="width height screenX screenY">
+
+ <script type="application/javascript" src="chrome://passwordmgr/content/passwordManager.js"/>
+
+ <stringbundle id="signonBundle"
+ src="chrome://passwordmgr/locale/passwordmgr.properties"/>
+
+ <keyset>
+ <key keycode="VK_ESCAPE" oncommand="escapeKeyHandler();"/>
+ <key key="&windowClose.key;" modifiers="accel" oncommand="escapeKeyHandler();"/>
+ <key key="&focusSearch1.key;" modifiers="accel" oncommand="FocusFilterBox();"/>
+ <key key="&focusSearch2.key;" modifiers="accel" oncommand="FocusFilterBox();"/>
+ </keyset>
+
+ <popupset id="signonsTreeContextSet">
+ <menupopup id="signonsTreeContextMenu"
+ onpopupshowing="UpdateContextMenu()">
+ <menuitem id="context-copyusername"
+ label="&copyUsernameCmd.label;"
+ accesskey="&copyUsernameCmd.accesskey;"
+ oncommand="CopyUsername()"/>
+ <menuitem id="context-editusername"
+ label="&editUsernameCmd.label;"
+ accesskey="&editUsernameCmd.accesskey;"
+ oncommand="EditCellInSelectedRow('username')"/>
+ <menuseparator/>
+ <menuitem id="context-copypassword"
+ label="&copyPasswordCmd.label;"
+ accesskey="&copyPasswordCmd.accesskey;"
+ oncommand="CopyPassword()"/>
+ <menuitem id="context-editpassword"
+ label="&editPasswordCmd.label;"
+ accesskey="&editPasswordCmd.accesskey;"
+ oncommand="EditCellInSelectedRow('password')"/>
+ </menupopup>
+ </popupset>
+
+ <!-- saved signons -->
+ <vbox id="savedsignons" class="contentPane" flex="1">
+ <!-- filter -->
+ <hbox align="center">
+ <label accesskey="&filter.accesskey;" control="filter">&filter.label;</label>
+ <textbox id="filter" flex="1" type="search"
+ aria-controls="signonsTree"
+ oncommand="FilterPasswords();"/>
+ </hbox>
+
+ <label control="signonsTree" id="signonsIntro"/>
+ <separator class="thin"/>
+ <tree id="signonsTree" flex="1"
+ width="750"
+ style="height: 20em;"
+ onkeypress="HandleSignonKeyPress(event)"
+ onselect="SignonSelected();"
+ editable="true"
+ context="signonsTreeContextMenu">
+ <treecols>
+ <treecol id="siteCol" label="&treehead.site.label;" flex="40"
+ data-field-name="hostname" persist="width"
+ ignoreincolumnpicker="true"
+ sortDirection="ascending"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="userCol" label="&treehead.username.label;" flex="25"
+ ignoreincolumnpicker="true"
+ data-field-name="username" persist="width"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="passwordCol" label="&treehead.password.label;" flex="15"
+ ignoreincolumnpicker="true"
+ data-field-name="password" persist="width"
+ hidden="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="timeCreatedCol" label="&treehead.timeCreated.label;" flex="10"
+ data-field-name="timeCreated" persist="width hidden"
+ hidden="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="timeLastUsedCol" label="&treehead.timeLastUsed.label;" flex="20"
+ data-field-name="timeLastUsed" persist="width hidden"
+ hidden="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="timePasswordChangedCol" label="&treehead.timePasswordChanged.label;" flex="10"
+ data-field-name="timePasswordChanged" persist="width hidden"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="timesUsedCol" label="&treehead.timesUsed.label;" flex="1"
+ data-field-name="timesUsed" persist="width hidden"
+ hidden="true"/>
+ <splitter class="tree-splitter"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+ <separator class="thin"/>
+ <hbox id="SignonViewerButtons">
+ <button id="removeSignon" disabled="true" icon="remove"
+ label="&remove.label;" accesskey="&remove.accesskey;"
+ oncommand="DeleteSignon();"/>
+ <button id="removeAllSignons" icon="clear"
+ label="&removeall.label;" accesskey="&removeall.accesskey;"
+ oncommand="DeleteAllSignons();"/>
+ <spacer flex="1"/>
+#if defined(MOZ_BUILD_APP_IS_BROWSER) && defined(XP_WIN)
+ <button accesskey="&import.accesskey;"
+ label="&import.label;"
+ oncommand="OpenMigrator();"/>
+#endif
+ <button id="togglePasswords"
+ oncommand="TogglePasswordVisible();"/>
+ </hbox>
+ </vbox>
+ <hbox align="end">
+ <hbox class="actionButtons" flex="1">
+ <spacer flex="1"/>
+#ifndef XP_MACOSX
+ <button oncommand="close();" icon="close"
+ label="&closebutton.label;" accesskey="&closebutton.accesskey;"/>
+#endif
+ </hbox>
+ </hbox>
+</window>
diff --git a/toolkit/components/passwordmgr/content/recipes.json b/toolkit/components/passwordmgr/content/recipes.json
new file mode 100644
index 000000000..fc747219b
--- /dev/null
+++ b/toolkit/components/passwordmgr/content/recipes.json
@@ -0,0 +1,31 @@
+{
+ "siteRecipes": [
+ {
+ "description": "okta uses a hidden password field to disable filling",
+ "hosts": ["mozilla.okta.com"],
+ "passwordSelector": "#pass-signin"
+ },
+ {
+ "description": "anthem uses a hidden password and username field to disable filling",
+ "hosts": ["www.anthem.com"],
+ "passwordSelector": "#LoginContent_txtLoginPass"
+ },
+ {
+ "description": "An ephemeral password-shim field is incorrectly selected as the username field.",
+ "hosts": ["www.discover.com"],
+ "usernameSelector": "#login-account"
+ },
+ {
+ "description": "Tibia uses type=password for its username field and puts the email address before the password field during registration",
+ "hosts": ["secure.tibia.com"],
+ "usernameSelector": "#accountname, input[name='loginname']",
+ "passwordSelector": "#password1, input[name='loginpassword']",
+ "pathRegex": "^\/account\/"
+ },
+ {
+ "description": "Username field will be incorrectly captured in the change password form (bug 1243722)",
+ "hosts": ["www.facebook.com"],
+ "notUsernameSelector": "#password_strength"
+ }
+ ]
+}