Bug 1208145 - Back out changeset e4e3a8b66db4 (bug 1121291). r=dolske draft
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Mon, 11 Jan 2016 18:56:30 -0800
changeset 320658 2d016e6875e79ab719fb135ad90e38c7a2f06381
parent 320657 b7d00293390bd54000b6dedc3e0d31020f2fd2b1
child 320659 6c41e84f0a5d06ffeeb37f0d7b25d9dd76f26ffe
push id9263
push usermozilla@noorenberghe.ca
push dateTue, 12 Jan 2016 03:16:56 +0000
reviewersdolske
bugs1208145, 1121291
milestone46.0a1
Bug 1208145 - Back out changeset e4e3a8b66db4 (bug 1121291). r=dolske
toolkit/components/passwordmgr/content/passwordManager.js
toolkit/components/passwordmgr/content/passwordManager.xul
toolkit/components/passwordmgr/test/browser/browser_passwordmgr_contextmenu.js
toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js
toolkit/components/passwordmgr/test/browser/browser_passwordmgr_sort.js
toolkit/components/passwordmgr/test/browser/browser_passwordmgrdlg.js
toolkit/components/telemetry/Histograms.json
toolkit/locales/en-US/chrome/passwordmgr/passwordmgr.properties
--- a/toolkit/components/passwordmgr/content/passwordManager.js
+++ b/toolkit/components/passwordmgr/content/passwordManager.js
@@ -1,23 +1,26 @@
 /* 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 =================== ***/
 
 var kSignonBundle;
+var showingPasswords = false;
 var dateFormatter = new Intl.DateTimeFormat(undefined,
                       { day: "numeric", month: "short", year: "numeric" });
 var dateAndTimeFormatter = new Intl.DateTimeFormat(undefined,
                              { day: "numeric", month: "short", year: "numeric",
                                hour: "numeric", minute: "numeric" });
 
 function SignonsStartup() {
   kSignonBundle = document.getElementById("signonBundle");
+  document.getElementById("togglePasswords").label = kSignonBundle.getString("showPasswords");
+  document.getElementById("togglePasswords").accessKey = kSignonBundle.getString("showPasswordsAccessKey");
   document.getElementById("signonsIntro").textContent = kSignonBundle.getString("loginsDescriptionAll");
 
   let treecols = document.getElementsByTagName("treecols")[0];
   treecols.addEventListener("click", HandleTreeColumnClick.bind(null, SignonColumnSort));
 
   LoadSignons();
 
   // filter the table if requested by caller
@@ -71,24 +74,19 @@ var signonsTreeView = {
         return dateFormatter.format(time);
       case "timesUsedCol":
         return signon.timesUsed;
       default:
         return "";
     }
   },
   isEditable : function(row, col) {
-    if (col.id == "userCol") {
+    if (col.id == "userCol" || col.id == "passwordCol") {
       return true;
     }
-
-    if (col.id == "passwordCol") {
-      return masterPasswordLogin();
-    }
-
     return false;
   },
   isSeparator : function(index) { return false; },
   isSorted : function() { return false; },
   isContainer : function(index) { return false; },
   cycleHeader : function(column) {},
   getRowProperties : function(row) { return ""; },
   getColumnProperties : function(column) { return ""; },
@@ -125,36 +123,38 @@ var signonsTreeView = {
 };
 
 
 function LoadSignons() {
   // loads signons into table
   try {
     signons = passwordmanager.getAllLogins();
   } catch (e) {
-    window.close();
-    return;
+    signons = [];
   }
   signons.forEach(login => login.QueryInterface(Components.interfaces.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
   var element = document.getElementById("removeAllSignons");
+  var toggle = document.getElementById("togglePasswords");
   if (signons.length == 0) {
     element.setAttribute("disabled","true");
+    toggle.setAttribute("disabled","true");
   } else {
     element.removeAttribute("disabled");
+    toggle.removeAttribute("disabled");
   }
 
   return true;
 }
 
 function SignonSelected() {
   var selections = GetTreeSelections(signonsTree);
   if (selections.length) {
@@ -188,16 +188,44 @@ function DeleteAllSignons() {
   var syncNeeded = (signonsTreeView._filterSet.length != 0);
   DeleteAllFromTree(signonsTree, signonsTreeView,
                         signonsTreeView._filterSet.length ? signonsTreeView._filterSet : signons,
                         deletedSignons, "removeSignon", "removeAllSignons");
   FinalizeSignonDeletions(syncNeeded);
   Services.telemetry.getHistogramById("PWMGR_MANAGE_DELETED_ALL").add(1);
 }
 
+function TogglePasswordVisible() {
+  if (showingPasswords || masterPasswordLogin(AskUserShowPasswords)) {
+    showingPasswords = !showingPasswords;
+    document.getElementById("togglePasswords").label = kSignonBundle.getString(showingPasswords ? "hidePasswords" : "showPasswords");
+    document.getElementById("togglePasswords").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)
+  Components.classes["@mozilla.org/observer-service;1"]
+            .getService(Components.interfaces.nsIObserverService)
+            .notifyObservers(null, "passwordmgr-password-toggle-complete", null);
+  Services.telemetry.getHistogramById("PWMGR_MANAGE_VISIBILITY_TOGGLED").add(showingPasswords);
+}
+
+function AskUserShowPasswords() {
+  var prompter = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(Components.interfaces.nsIPromptService);
+  var 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 (var s=0; s<deletedSignons.length; s++) {
     passwordmanager.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) {
@@ -301,17 +329,17 @@ function SignonMatchesFilter(aSignon, aF
   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 (Services.logins.isLoggedIn && aSignon.password &&
+  if (showingPasswords && aSignon.password &&
       aSignon.password.toLowerCase().indexOf(aFilterValue) != -1)
     return true;
 
   return false;
 }
 
 function FilterPasswords(aFilterValue, view) {
   aFilterValue = aFilterValue.toLowerCase();
@@ -357,18 +385,19 @@ function _filterPasswords()
   // if the view is not empty then select the first item
   if (signonsTreeView.rowCount > 0)
     signonsTreeView.selection.select(0);
 
   document.getElementById("signonsIntro").textContent = kSignonBundle.getString("loginsDescriptionFiltered");
 }
 
 function CopyPassword() {
-  // Don't copy passwords if a master password hasn't been entered.
-  if (!masterPasswordLogin())
+  // 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
   var clipboard = Components.classes["@mozilla.org/widget/clipboardhelper;1"].
                   getService(Components.interfaces.nsIClipboardHelper);
   var row = document.getElementById("signonsTree").currentIndex;
   var password = signonsTreeView.getCellText(row, {id : "passwordCol" });
   clipboard.copyString(password);
   Services.telemetry.getHistogramById("PWMGR_MANAGE_COPIED_PASSWORD").add(1);
@@ -411,17 +440,23 @@ function UpdateContextMenu() {
   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");
-  menuItems.get("context-editpassword").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
   var tokendb = Components.classes["@mozilla.org/security/pk11tokendb;1"]
                     .createInstance(Components.interfaces.nsIPK11TokenDB);
   var token = tokendb.getInternalKeyToken();
 
--- a/toolkit/components/passwordmgr/content/passwordManager.xul
+++ b/toolkit/components/passwordmgr/content/passwordManager.xul
@@ -77,18 +77,19 @@
                  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"
-                 data-field-name="password" persist="width hidden"
-                 type="password"/>
+                 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"/>
         <splitter class="tree-splitter"/>
@@ -111,16 +112,18 @@
               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;"/>
--- a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_contextmenu.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_contextmenu.js
@@ -45,27 +45,28 @@ function test() {
             assertMenuitemEnabled("copypassword", false);
             assertMenuitemEnabled("editpassword", false);
 
             info("Select the first row (with an empty username)");
             selection.select(0);
             assertMenuitemEnabled("copyusername", false, "empty username");
             assertMenuitemEnabled("editusername", true);
             assertMenuitemEnabled("copypassword", true);
-            assertMenuitemEnabled("editpassword", true);
+            assertMenuitemEnabled("editpassword", false, "password column hidden");
 
             info("Clear the selection");
             selection.clearSelection();
             assertMenuitemEnabled("copyusername", false);
             assertMenuitemEnabled("editusername", false);
             assertMenuitemEnabled("copypassword", false);
             assertMenuitemEnabled("editpassword", false);
 
             info("Select the third row and making the password column visible");
             selection.select(2);
+            doc.getElementById("passwordCol").hidden = false;
             assertMenuitemEnabled("copyusername", true);
             assertMenuitemEnabled("editusername", true);
             assertMenuitemEnabled("copypassword", true);
             assertMenuitemEnabled("editpassword", true, "password column visible");
             menuitem.doCommand();
         }
 
         function assertMenuitemEnabled(idSuffix, expected, reason = "") {
@@ -74,16 +75,17 @@ function test() {
             is(actual, expected, idSuffix + " should be " + (expected ? "enabled" : "disabled") +
                (reason ? ": " + reason : ""));
         }
 
         function cleanUp() {
             Services.ww.registerNotification(function (aSubject, aTopic, aData) {
                 Services.ww.unregisterNotification(arguments.callee);
                 Services.logins.removeAllLogins();
+                doc.getElementById("passwordCol").hidden = true;
                 finish();
             });
             pwmgrdlg.close();
         }
 
         function testPassword() {
             info("Testing Copy Password");
             waitForClipboard("coded", function copyPassword() {
--- a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js
@@ -27,16 +27,21 @@ function synthesizeDblClickOnCell(aTree,
   let rect = tbo.getCoordsForCellItem(row, aTree.columns[column], "text");
   let x = rect.x + rect.width / 2;
   let y = rect.y + rect.height / 2;
   // Simulate the double click.
   EventUtils.synthesizeMouse(aTree.body, x, y, { clickCount: 2 },
                              aTree.ownerDocument.defaultView);
 }
 
+function* togglePasswords() {
+  pwmgrdlg.document.querySelector("#togglePasswords").doCommand();
+  yield new Promise(resolve => waitForFocus(resolve, pwmgrdlg));
+}
+
 function* editUsernamePromises(site, oldUsername, newUsername) {
   is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login found");
   let login = Services.logins.findLogins({}, site, "", "")[0];
   is(login.username, oldUsername, "Correct username saved");
   is(getUsername(0), oldUsername, "Correct username shown");
   synthesizeDblClickOnCell(signonsTree, 1, 0);
   yield ContentTaskUtils.waitForCondition(() => signonsTree.getAttribute("editing"),
                                           "Waiting for editing");
@@ -104,17 +109,19 @@ add_task(function* test_setup() {
     }, pwmgrdlg);
   });
 });
 
 add_task(function* test_edit_multiple_logins() {
   function* testLoginChange(site, oldUsername, oldPassword, newUsername, newPassword) {
     addLogin(site, oldUsername, oldPassword);
     yield* editUsernamePromises(site, oldUsername, newUsername);
+    yield* togglePasswords();
     yield* editPasswordPromises(site, oldPassword, newPassword);
+    yield* togglePasswords();
   }
 
   yield* testLoginChange("http://c.tn/", "userC", "passC", "usernameC", "passwordC");
   yield* testLoginChange("http://b.tn/", "userB", "passB", "usernameB", "passwordB");
   yield* testLoginChange("http://a.tn/", "userA", "passA", "usernameA", "passwordA");
 
   pwmgrdlg.close();
 });
--- a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_sort.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_sort.js
@@ -62,16 +62,45 @@ function test() {
         let doc = pwmgrdlg.document;
         let win = doc.defaultView;
         let sTree = doc.getElementById("signonsTree");
         let filter = doc.getElementById("filter");
         let siteCol = doc.getElementById("siteCol");
         let userCol = doc.getElementById("userCol");
         let passwordCol = doc.getElementById("passwordCol");
 
+        let toggleCalls = 0;
+        function toggleShowPasswords(func) {
+            let toggleButton = doc.getElementById("togglePasswords");
+            let showMode = (toggleCalls++ % 2) == 0;
+
+            // only watch for a confirmation dialog every other time being called
+            if (showMode) {
+                Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+                    if (aTopic == "domwindowclosed")
+                        Services.ww.unregisterNotification(arguments.callee);
+                    else if (aTopic == "domwindowopened") {
+                        let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+                        SimpleTest.waitForFocus(function() {
+                            EventUtils.sendKey("RETURN", win);
+                        }, win);
+                    }
+                });
+            }
+
+            Services.obs.addObserver(function (aSubject, aTopic, aData) {
+                if (aTopic == "passwordmgr-password-toggle-complete") {
+                    Services.obs.removeObserver(arguments.callee, aTopic);
+                    func();
+                }
+            }, "passwordmgr-password-toggle-complete", false);
+
+            EventUtils.synthesizeMouse(toggleButton, 1, 1, {}, win);
+        }
+
         function clickCol(col) {
             EventUtils.synthesizeMouse(col, 20, 1, {}, win);
             setTimeout(runNextTest, 0);
         }
 
         function setFilter(string) {
             filter.value = string;
             filter.doCommand();
@@ -168,11 +197,12 @@ function test() {
 
                     pwmgr.removeAllLogins();
                     finish();
                 });
                 pwmgrdlg.close();
             }
         }
 
-        runNextTest();
+        // Toggle Show Passwords to display Password column, then start tests
+        toggleShowPasswords(runNextTest);
     }
 }
--- a/toolkit/components/passwordmgr/test/browser/browser_passwordmgrdlg.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgrdlg.js
@@ -52,44 +52,98 @@ function test() {
         let view = tree.view;
 
         is(filter.value, "", "Filter box should initially be empty");
         is(view.rowCount, 10, "There should be 10 passwords initially");
 
         // Prepare a set of tests
         //   filter: the text entered in the filter search box
         //   count: the number of logins which should match the respective filter
+        //   count2: the number of logins which should match the respective filter
+        //           if the passwords are being shown as well
+        //   Note: if a test doesn't have count2 set, count is used instead.
         let tests = [
-            {filter: "pass", count: 4},
+            {filter: "pass", count: 0, count2: 4},
             {filter: "", count: 10}, // test clearing the filter
             {filter: "moz", count: 7},
             {filter: "mozi", count: 7},
             {filter: "mozil", count: 7},
             {filter: "mozill", count: 7},
             {filter: "mozilla", count: 7},
-            {filter: "mozilla.com", count: 2},
+            {filter: "mozilla.com", count: 1, count2: 2},
             {filter: "user", count: 4},
             {filter: "user ", count: 1},
             {filter: " user", count: 2},
             {filter: "http", count: 10},
             {filter: "https", count: 1},
-            {filter: "secret", count: 2},
+            {filter: "secret", count: 0, count2: 2},
             {filter: "secret!", count: 0},
         ];
 
-        function runTests(endFunction) {
+        let toggleCalls = 0;
+        function toggleShowPasswords(func) {
+            let toggleButton = doc.getElementById("togglePasswords");
+            let showMode = (toggleCalls++ % 2) == 0;
+
+            // only watch for a confirmation dialog every other time being called
+            if (showMode) {
+                Services.ww.registerNotification(function (aSubject, aTopic, aData) {
+                    if (aTopic == "domwindowclosed")
+                        Services.ww.unregisterNotification(arguments.callee);
+                    else if (aTopic == "domwindowopened") {
+                        let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+                        SimpleTest.waitForFocus(function() {
+                            EventUtils.sendKey("RETURN", win);
+                        }, win);
+                    }
+                });
+            }
+
+            Services.obs.addObserver(function (aSubject, aTopic, aData) {
+                if (aTopic == "passwordmgr-password-toggle-complete") {
+                    Services.obs.removeObserver(arguments.callee, aTopic);
+                    func();
+                }
+            }, "passwordmgr-password-toggle-complete", false);
+
+            EventUtils.synthesizeMouse(toggleButton, 1, 1, {}, win);
+        }
+
+        function runTests(mode, endFunction) {
             let testCounter = 0;
 
             function setFilter(string) {
                 filter.value = string;
                 filter.doCommand();
             }
 
             function runOneTest(test) {
-                is(view.rowCount, test.count, test.count + " logins should match '" + test.filter + "'");
+                function tester() {
+                    is(view.rowCount, expected, expected + " logins should match '" + test.filter + "'");
+                }
+
+                let expected;
+                switch (mode) {
+                case 1: // without showing passwords
+                    expected = test.count;
+                    break;
+                case 2: // showing passwords
+                    expected = ("count2" in test) ? test.count2 : test.count;
+                    break;
+                case 3: // toggle
+                    expected = test.count;
+                    tester();
+                    toggleShowPasswords(function () {
+                        expected = ("count2" in test) ? test.count2 : test.count;
+                        tester();
+                        toggleShowPasswords(proceed);
+                    });
+                    return;
+                }
+                tester();
                 proceed();
             }
 
             function proceed() {
                 // run the next test if necessary or proceed with the tests
                 if (testCounter != tests.length)
                     runNextTest();
                 else
@@ -101,17 +155,29 @@ function test() {
                 setFilter(test.filter);
                 setTimeout(runOneTest, 0, test);
             }
 
             runNextTest();
         }
 
         function step1() {
-            runTests(lastStep);
+            runTests(1, step2);
+        }
+
+        function step2() {
+            toggleShowPasswords(function() {
+                runTests(2, step3);
+            });
+        }
+
+        function step3() {
+            toggleShowPasswords(function() {
+                runTests(3, lastStep);
+            });
         }
 
         function lastStep() {
             // cleanup
             Services.ww.registerNotification(function (aSubject, aTopic, aData) {
                 // unregister ourself
                 Services.ww.unregisterNotification(arguments.callee);
 
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -8738,16 +8738,21 @@
     "description": "Accumulates how the password management interface was opened. (0=Preferences, 1=Page Info)"
   },
   "PWMGR_MANAGE_SORTED": {
     "expires_in_version": "never",
     "keyed": true,
     "kind": "count",
     "description": "Reports the column that logins are sorted by"
   },
+  "PWMGR_MANAGE_VISIBILITY_TOGGLED": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "Whether the visibility of passwords was toggled (0=Hide, 1=Show)"
+  },
   "PWMGR_NUM_PASSWORDS_PER_HOSTNAME": {
     "expires_in_version": "never",
     "kind": "linear",
     "high": 21,
     "n_buckets" : 20,
     "description": "The number of passwords per hostname"
   },
   "PWMGR_NUM_SAVED_PASSWORDS": {
--- a/toolkit/locales/en-US/chrome/passwordmgr/passwordmgr.properties
+++ b/toolkit/locales/en-US/chrome/passwordmgr/passwordmgr.properties
@@ -42,16 +42,21 @@ passwordChangeTitle = Confirm Password C
 # String is the username for the login.
 updatePasswordMsg = Would you like to update the saved password for "%S"?
 updatePasswordMsgNoUser = Would you like to update the saved password?
 notifyBarUpdateButtonText = Update Password
 notifyBarUpdateButtonAccessKey = U
 notifyBarDontChangeButtonText = Don't Change
 notifyBarDontChangeButtonAccessKey = D
 userSelectText = Please confirm which user you are changing the password for
+hidePasswords=Hide Passwords
+hidePasswordsAccessKey=P
+showPasswords=Show Passwords
+showPasswordsAccessKey=P
+noMasterPasswordPrompt=Are you sure you wish to show your passwords?
 removeAllPasswordsPrompt=Are you sure you wish to remove all passwords?
 removeAllPasswordsTitle=Remove all passwords
 removeLoginPrompt=Are you sure you wish to remove this login?
 removeLoginTitle=Remove login
 loginsDescriptionAll=Logins for the following sites are stored on your computer:
 loginsDescriptionFiltered=The following logins match your search:
 # LOCALIZATION NOTE (loginHostAge):
 # This is used to show the context menu login items with their age.