Bug 433238 - Password manager contextual menu password field manual fill. r=MattN
authorBernardo P. Rittmeyer <bernardo@rittme.com>
Thu, 06 Aug 2015 15:28:07 -0700
changeset 256802 02ff02df924ad184ec843ac0d9677cb38a95cba8
parent 256801 eb46ecd87491d1ce1cd648f8be0cd5d3063fff16
child 256803 96f128a9b4656aee393a3d022bb9d40a2a8c0384
push id14512
push usermozilla@noorenberghe.ca
push dateFri, 07 Aug 2015 20:16:50 +0000
treeherderfx-team@a4836b5699de [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs433238
milestone42.0a1
Bug 433238 - Password manager contextual menu password field manual fill. r=MattN
browser/base/content/browser-context.inc
browser/base/content/nsContextMenu.js
browser/themes/shared/contextmenu.inc.css
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/LoginManagerContextMenu.jsm
toolkit/components/passwordmgr/LoginManagerParent.jsm
toolkit/components/passwordmgr/moz.build
toolkit/locales/en-US/chrome/global/textcontext.dtd
toolkit/locales/en-US/chrome/passwordmgr/passwordmgr.properties
toolkit/modules/InlineSpellChecker.jsm
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -412,16 +412,33 @@
       <menuitem hidden="true" id="context-bidi-text-direction-toggle"
                 label="&bidiSwitchTextDirectionItem.label;"
                 accesskey="&bidiSwitchTextDirectionItem.accesskey;"
                 command="cmd_switchTextDirection"/>
       <menuitem hidden="true" id="context-bidi-page-direction-toggle"
                 label="&bidiSwitchPageDirectionItem.label;"
                 accesskey="&bidiSwitchPageDirectionItem.accesskey;"
                 oncommand="gContextMenu.switchPageDirection();"/>
+      <menuseparator id="fill-login-separator" hidden="true"/>
+      <menu id="fill-login"
+            label="&fillPasswordMenu.label;"
+            class="menu-iconic"
+            accesskey="&fillPasswordMenu.accesskey;"
+            hidden="true">
+        <menupopup id="fill-login-popup">
+          <menuitem id="fill-login-no-logins"
+                    label="&noLoginSuggestions.label;"
+                    disabled="true"
+                    hidden="true"/>
+          <menuseparator id="saved-logins-separator"/>
+          <menuitem id="fill-login-saved-passwords"
+                    label="&viewSavedLogins.label;"
+                    oncommand="gContextMenu.openPasswordManager();"/>
+        </menupopup>
+      </menu>
       <menuseparator id="inspect-separator" hidden="true"/>
       <menuitem id="context-inspect"
                 hidden="true"
                 label="&inspectContextMenu.label;"
                 accesskey="&inspectContextMenu.accesskey;"
                 oncommand="gContextMenu.inspectNode();"/>
       <menuseparator id="context-media-eme-separator" hidden="true"/>
       <menuitem id="context-media-eme-learnmore"
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -1,22 +1,25 @@
 /* vim: set ts=2 sw=2 sts=2 et tw=80: */
 # 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/.
 
 Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
 Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm");
+Components.utils.import("resource://gre/modules/LoginManagerContextMenu.jsm");
 Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Pocket",
   "resource:///modules/Pocket.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+  "resource://gre/modules/LoginHelper.jsm");
 
 var gContextMenuContentData = null;
 
 function nsContextMenu(aXulMenu, aIsShift) {
   this.shouldDisplay = true;
   this.initMenu(aXulMenu, aIsShift);
 }
 
@@ -62,16 +65,17 @@ nsContextMenu.prototype = {
     this._checkTelemetryForMenu(aXulMenu);
   },
 
   hiding: function CM_hiding() {
     gContextMenuContentData = null;
     InlineSpellCheckerUI.clearSuggestionsFromMenu();
     InlineSpellCheckerUI.clearDictionaryListFromMenu();
     InlineSpellCheckerUI.uninit();
+    LoginManagerContextMenu.clearLoginsFromMenu(document);
 
     // This handler self-deletes, only run it if it is still there:
     if (this._onPopupHiding) {
       this._onPopupHiding();
     }
   },
 
   initItems: function CM_initItems() {
@@ -81,16 +85,17 @@ nsContextMenu.prototype = {
     this.initViewItems();
     this.initMiscItems();
     this.initSpellingItems();
     this.initSaveItems();
     this.initClipboardItems();
     this.initMediaPlayerItems();
     this.initLeaveDOMFullScreenItems();
     this.initClickToPlayItems();
+    this.initPasswordManagerItems();
   },
 
   initPageMenuSeparator: function CM_initPageMenuSeparator() {
     this.showItem("page-menu-separator", this.hasPageMenu);
   },
 
   initOpenItems: function CM_initOpenItems() {
     var isMailtoInternal = false;
@@ -495,16 +500,43 @@ nsContextMenu.prototype = {
   },
 
   initClickToPlayItems: function() {
     this.showItem("context-ctp-play", this.onCTPPlugin);
     this.showItem("context-ctp-hide", this.onCTPPlugin);
     this.showItem("context-sep-ctp", this.onCTPPlugin);
   },
 
+  initPasswordManagerItems: function() {
+    let showFillPassword = this.onPassword;
+    let disableFillPassword = !Services.logins.isLoggedIn || this.target.disabled || this.target.readOnly;
+    this.showItem("fill-login-separator", showFillPassword);
+    this.showItem("fill-login", showFillPassword);
+    this.setItemAttr("fill-login", "disabled", disableFillPassword);
+
+    if (!showFillPassword || disableFillPassword) {
+      return;
+    }
+    let documentURI = gContextMenuContentData.documentURIObject;
+    let fragment = LoginManagerContextMenu.addLoginsToMenu(this.target, this.browser, documentURI);
+
+    this.showItem("fill-login-no-logins", !fragment);
+
+    if (!fragment) {
+      return;
+    }
+    let popup = document.getElementById("fill-login-popup");
+    let insertBeforeElement = document.getElementById("fill-login-no-logins");
+    popup.insertBefore(fragment, insertBeforeElement);
+  },
+
+  openPasswordManager: function() {
+    LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
+  },
+
   inspectNode: function CM_inspectNode() {
     let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
     let gBrowser = this.browser.ownerDocument.defaultView.gBrowser;
     let tt = devtools.TargetFactory.forTab(gBrowser.selectedTab);
     return gDevTools.showToolbox(tt, "inspector").then(function(toolbox) {
       let inspector = toolbox.getCurrentPanel();
       if (this.isRemote) {
         this.browser.messageManager.sendAsyncMessage("debug:inspect", {}, {node: this.target});
@@ -566,16 +598,17 @@ nsContextMenu.prototype = {
     this.inSrcdocFrame     = false;
     this.inSyntheticDoc    = false;
     this.hasBGImage        = false;
     this.bgImageURL        = "";
     this.onEditableArea    = false;
     this.isDesignMode      = false;
     this.onCTPPlugin       = false;
     this.canSpellCheck     = false;
+    this.onPassword        = false;
 
     if (this.isRemote) {
       this.selectionInfo = gContextMenuContentData.selectionInfo;
     } else {
       this.selectionInfo = BrowserUtils.getSelectionDetails(window);
     }
 
     this.textSelected      = this.selectionInfo.text;
@@ -663,16 +696,17 @@ nsContextMenu.prototype = {
         if (this.target.isEncrypted) {
           this.onDRMMedia = true;
         }
       }
       else if (editFlags & (SpellCheckHelper.INPUT | SpellCheckHelper.TEXTAREA)) {
         this.onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0;
         this.onNumeric = (editFlags & SpellCheckHelper.NUMERIC) !== 0;
         this.onEditableArea = (editFlags & SpellCheckHelper.EDITABLE) !== 0;
+        this.onPassword = (editFlags & SpellCheckHelper.PASSWORD) !== 0;
         if (this.onEditableArea) {
           if (this.isRemote) {
             InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
           }
           else {
             InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
             InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
           }
--- a/browser/themes/shared/contextmenu.inc.css
+++ b/browser/themes/shared/contextmenu.inc.css
@@ -90,8 +90,18 @@
   width: 16px;
   height: 16px;
   margin: 7px;
 }
 
 #context-media-eme-learnmore {
   list-style-image: url("chrome://browser/skin/drm-icon.svg#chains");
 }
+
+#fill-login {
+  list-style-image: url("chrome://mozapps/skin/passwordmgr/key-16.png");
+}
+
+@media (min-resolution: 1.1dppx) {
+  #fill-login {
+    list-style-image: url("chrome://mozapps/skin/passwordmgr/key-16@2x.png");
+  }
+}
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -183,16 +183,17 @@ var LoginManagerContent = {
     }
 
     if (msg.name == "RemoteLogins:fillForm") {
       this.fillForm({
         topDocument: window.document,
         loginFormOrigin: msg.data.loginFormOrigin,
         loginsFound: jsLoginsToXPCOM(msg.data.logins),
         recipes: msg.data.recipes,
+        inputElement: msg.objects.inputElement,
       });
       return;
     }
 
     let request = this._takeRequest(msg);
     switch (msg.name) {
       case "RemoteLogins:loginsFound": {
         let loginsFound = jsLoginsToXPCOM(msg.data.logins);
@@ -440,35 +441,49 @@ var LoginManagerContent = {
    *            This must match the origin of the form used for the fill.
    *          loginsFound:
    *            Array containing the login to fill. While other messages may
    *            have more logins, for this use case this is expected to have
    *            exactly one element. The origin of the login may be different
    *            from the origin of the form used for the fill.
    *          recipes:
    *            Fill recipes transmitted together with the original message.
+   *          inputElement:
+   *            Optional input password element from the form we want to fill.
    *        }
    */
-  fillForm({ topDocument, loginFormOrigin, loginsFound, recipes }) {
+  fillForm({ topDocument, loginFormOrigin, loginsFound, recipes, inputElement }) {
     let topState = this.stateForDocument(topDocument);
     if (!topState.loginFormForFill) {
       log("fillForm: There is no login form anymore. The form may have been",
           "removed or the document may have changed.");
       return;
     }
-    if (LoginUtils._getPasswordOrigin(topDocument.documentURI) !=
-        loginFormOrigin) {
-      log("fillForm: The requested origin doesn't match the one form the",
-          "document. This may mean we navigated to a document from a different",
-          "site before we had a chance to indicate this change in the user",
-          "interface.");
-      return;
+    if (LoginUtils._getPasswordOrigin(topDocument.documentURI) != loginFormOrigin) {
+      if (!inputElement ||
+          LoginUtils._getPasswordOrigin(inputElement.ownerDocument.documentURI) != loginFormOrigin) {
+        log("fillForm: The requested origin doesn't match the one form the",
+            "document. This may mean we navigated to a document from a different",
+            "site before we had a chance to indicate this change in the user",
+            "interface.");
+        return;
+      }
     }
-    this._fillForm(topState.loginFormForFill, true, true, true, true,
-                   loginsFound, recipes);
+    let form = topState.loginFormForFill;
+    let clobberUsername = true;
+    let options = {
+      inputElement,
+    };
+
+    // If we have a target input, fills it's form.
+    if (inputElement) {
+      form = FormLikeFactory.createFromPasswordField(inputElement);
+      clobberUsername = false;
+    }
+    this._fillForm(form, true, clobberUsername, true, true, loginsFound, recipes, options);
   },
 
   loginsFound: function({ form, loginsFound, recipes }) {
     let doc = form.ownerDocument;
     let autofillForm = gAutofillForms && !PrivateBrowsingUtils.isContentWindowPrivate(doc.defaultView);
 
     this._fillForm(form, autofillForm, false, false, false, loginsFound, recipes);
   },
@@ -809,19 +824,21 @@ var LoginManagerContent = {
    * @param {bool} clobberUsername controls if an existing username can be
    *                               overwritten
    * @param {bool} clobberPassword controls if an existing password value can be
    *                               overwritten
    * @param {bool} userTriggered is an indication of whether this filling was triggered by
    *                             the user
    * @param {nsILoginInfo[]} foundLogins is an array of nsILoginInfo that could be used for the form
    * @param {Set} recipes that could be used to affect how the form is filled
+   * @param {Object} [options = {}] is a list of options for this method.
+            - [inputElement] is an optional target input element we want to fill
    */
   _fillForm : function (form, autofillForm, clobberUsername, clobberPassword,
-                        userTriggered, foundLogins, recipes) {
+                        userTriggered, foundLogins, recipes, {inputElement} = {}) {
     let ignoreAutocomplete = true;
     const AUTOFILL_RESULT = {
       FILLED: 0,
       NO_PASSWORD_FIELD: 1,
       PASSWORD_DISABLED_READONLY: 2,
       NO_LOGINS_FIT: 3,
       NO_SAVED_LOGINS: 4,
       EXISTING_PASSWORD: 5,
@@ -850,16 +867,27 @@ var LoginManagerContent = {
 
       // Heuristically determine what the user/pass fields are
       // We do this before checking to see if logins are stored,
       // so that the user isn't prompted for a master password
       // without need.
       var [usernameField, passwordField, ignored] =
             this._getFormFields(form, false, recipes);
 
+      // If we have a password inputElement parameter and it's not
+      // the same as the one heuristically found, use the parameter
+      // one instead.
+      if (inputElement) {
+        if (inputElement.type != "password") {
+          throw new Error("Unexpected input element type.");
+        }
+        passwordField = inputElement;
+        usernameField = null;
+      }
+
       // Need a valid password field to do anything.
       if (passwordField == null) {
         log("not filling form, no password field found");
         recordAutofillResult(AUTOFILL_RESULT.NO_PASSWORD_FIELD);
         return;
       }
 
       // If the password field is disabled or read-only, there's nothing to do.
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/LoginManagerContextMenu.jsm
@@ -0,0 +1,183 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["LoginManagerContextMenu"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerParent",
+                                  "resource://gre/modules/LoginManagerParent.jsm");
+
+/*
+ * Password manager object for the browser contextual menu.
+ */
+let LoginManagerContextMenu = {
+  dateAndTimeFormatter: new Intl.DateTimeFormat(undefined,
+                        { day: "numeric", month: "short", year: "numeric" }),
+  /**
+   * Look for login items and add them to the contextual menu.
+   *
+   * @param {HTMLInputElement} inputElement
+   *        The target input element of the context menu click.
+   * @param {xul:browser} browser
+   *        The browser for the document the context menu was open on.
+   * @param {nsIURI} documentURI
+   *        The URI of the document that the context menu was activated from.
+   *        This isn't the same as the browser's top-level document URI
+   *        when subframes are involved.
+   * @returns {DocumentFragment} a document fragment with all the login items.
+   */
+  addLoginsToMenu(inputElement, browser, documentURI) {
+
+    let foundLogins = this._findLogins(documentURI);
+
+    if (!foundLogins.length) {
+      return null;
+    }
+
+    let fragment = browser.ownerDocument.createDocumentFragment();
+    let duplicateUsernames = this._findDuplicates(foundLogins);
+    for (let login of foundLogins) {
+        let item = fragment.ownerDocument.createElement("menuitem");
+
+        let username = login.username;
+        // If login is empty or duplicated we want to append a modification date to it.
+        if (!username || duplicateUsernames.has(username)){
+          if (!username) {
+            username = this._getLocalizedString("noUsername");
+          }
+          let meta = login.QueryInterface(Ci.nsILoginMetaInfo);
+          let time = this.dateAndTimeFormatter.format(new Date(meta.timePasswordChanged));
+          username = this._getLocalizedString("loginHostAge", [username, time]);
+        }
+        item.setAttribute("label", username);
+        item.setAttribute("class", "context-login-item");
+
+        // login is bound so we can keep the reference to each object.
+        item.addEventListener("command", function(login, event) {
+          this._fillPassword(login, inputElement, browser, documentURI);
+        }.bind(this, login));
+
+        fragment.appendChild(item);
+    }
+
+    return fragment;
+  },
+
+  /**
+   * Undoes the work of addLoginsToMenu for the same menu.
+   *
+   * @param {Document}
+   *        The context menu owner document.
+   */
+  clearLoginsFromMenu(document) {
+    let loginItems = document.getElementsByClassName("context-login-item");
+    while (loginItems.item(0)) {
+      loginItems.item(0).remove();
+    }
+  },
+
+  /**
+   * Find logins for the current URI.
+   *
+   * @param {nsIURI} documentURI
+   *        URI object with the hostname of the logins we want to find.
+   *        This isn't the same as the browser's top-level document URI
+   *        when subframes are involved.
+   *
+   * @returns {nsILoginInfo[]} a login list
+   */
+  _findLogins(documentURI) {
+    let logins = Services.logins.findLogins({}, documentURI.prePath, "", "");
+
+    // Sort logins in alphabetical order and by date.
+    logins.sort((loginA, loginB) => {
+      // Sort alphabetically
+      let result = loginA.username.localeCompare(loginB.username);
+      if (result) {
+        // Forces empty logins to be at the end
+        if (!loginA.username) {
+          return 1;
+        }
+        if (!loginB.username) {
+          return -1;
+        }
+        return result;
+      }
+
+      // Same username logins are sorted by last change date
+      let metaA = loginA.QueryInterface(Ci.nsILoginMetaInfo);
+      let metaB = loginB.QueryInterface(Ci.nsILoginMetaInfo);
+      return metaB.timePasswordChanged - metaA.timePasswordChanged;
+    });
+
+    return logins;
+  },
+
+  /**
+   * Find duplicate usernames in a login list.
+   *
+   * @param {nsILoginInfo[]} loginList
+   *        A list of logins we want to look for duplicate usernames.
+   *
+   * @returns {Set} a set with the duplicate usernames.
+   */
+  _findDuplicates(loginList) {
+    let seen = new Set();
+    let duplicates = new Set();
+    for (let login of loginList) {
+      if (seen.has(login.username)) {
+        duplicates.add(login.username);
+      }
+      seen.add(login.username);
+    }
+    return duplicates;
+  },
+
+  /**
+   * @param {nsILoginInfo} login
+   *        The login we want to fill the form with.
+   * @param {Element} inputElement
+   *        The target input element we want to fill.
+   * @param {xul:browser} browser
+   *        The target tab browser.
+   * @param {nsIURI} documentURI
+   *        URI of the document owning the form we want to fill.
+   *        This isn't the same as the browser's top-level
+   *        document URI when subframes are involved.
+   */
+  _fillPassword(login, inputElement, browser, documentURI) {
+    LoginManagerParent.fillForm({
+      browser: browser,
+      loginFormOrigin: documentURI.prePath,
+      login: login,
+      inputElement: inputElement,
+    }).catch(Cu.reportError);
+  },
+
+  /**
+   * @param {string} key
+   *        The localized string key
+   * @param {string[]} formatArgs
+   *        An array of formatting argument string
+   *
+   * @returns {string} the localized string for the specified key,
+   *          formatted with arguments if required.
+   */
+  _getLocalizedString(key, formatArgs) {
+    if (formatArgs) {
+      return this._stringBundle.formatStringFromName(key, formatArgs, formatArgs.length);
+    }
+    return this._stringBundle.GetStringFromName(key);
+  },
+};
+
+XPCOMUtils.defineLazyGetter(LoginManagerContextMenu, "_stringBundle", function() {
+  return Services.strings.
+         createBundle("chrome://passwordmgr/locale/passwordmgr.properties");
+});
--- a/toolkit/components/passwordmgr/LoginManagerParent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm
@@ -230,37 +230,39 @@ var LoginManagerParent = {
       }
     }
   },
 
   /**
    * Trigger a login form fill and send relevant data (e.g. logins and recipes)
    * to the child process (LoginManagerContent).
    */
-  fillForm: Task.async(function* ({ browser, loginFormOrigin, login }) {
+  fillForm: Task.async(function* ({ browser, loginFormOrigin, login, inputElement }) {
     let recipes = [];
     if (loginFormOrigin) {
       let formHost;
       try {
         formHost = (new URL(loginFormOrigin)).host;
         let recipeManager = yield this.recipeParentPromise;
         recipes = recipeManager.getRecipesForHost(formHost);
       } catch (ex) {
         // Some schemes e.g. chrome aren't supported by URL
       }
     }
 
     // Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
     // doesn't support structured cloning.
     let jsLogins = JSON.parse(JSON.stringify([login]));
+
+    let objects = inputElement ? {inputElement} : null;
     browser.messageManager.sendAsyncMessage("RemoteLogins:fillForm", {
       loginFormOrigin,
       logins: jsLogins,
       recipes,
-    });
+    }, objects);
   }),
 
   /**
    * Send relevant data (e.g. logins and recipes) to the child process (LoginManagerContent).
    */
   sendLoginDataToChild: Task.async(function*(showMasterPassword, formOrigin, actionOrigin,
                                              requestId, target) {
     let recipes = [];
--- a/toolkit/components/passwordmgr/moz.build
+++ b/toolkit/components/passwordmgr/moz.build
@@ -51,22 +51,27 @@ if CONFIG['OS_TARGET'] == 'Android':
     EXTRA_COMPONENTS += [
         'storage-mozStorage.js',
     ]
 else:
     EXTRA_COMPONENTS += [
         'storage-json.js',
     ]
     EXTRA_JS_MODULES += [
-        'LoginDoorhangers.jsm',
         'LoginImport.jsm',
         'LoginStore.jsm',
     ]
 
 if CONFIG['OS_TARGET'] == 'WINNT':
     EXTRA_JS_MODULES += [
         'OSCrypto_win.js',
     ]
 
+if CONFIG['MOZ_BUILD_APP'] == 'browser':
+    EXTRA_JS_MODULES += [
+        'LoginDoorhangers.jsm',
+        'LoginManagerContextMenu.jsm',
+    ]
+
 JAR_MANIFESTS += ['jar.mn']
 
 with Files('**'):
     BUG_COMPONENT = ('Toolkit', 'Password Manager')
--- a/toolkit/locales/en-US/chrome/global/textcontext.dtd
+++ b/toolkit/locales/en-US/chrome/global/textcontext.dtd
@@ -21,8 +21,17 @@
 <!ENTITY spellUndoAddToDictionary.accesskey "n">
 <!ENTITY spellCheckToggle.label "Check Spelling">
 <!ENTITY spellCheckToggle.accesskey "g">
 <!ENTITY spellNoSuggestions.label "(No Spelling Suggestions)">
 <!ENTITY spellDictionaries.label "Languages">
 <!ENTITY spellDictionaries.accesskey "l">
 
 <!ENTITY searchTextBox.clear.label "Clear">
+
+<!ENTITY fillLoginMenu.label          "Fill Login">
+<!ENTITY fillLoginMenu.accesskey      "F">
+<!ENTITY fillPasswordMenu.label       "Fill Password">
+<!ENTITY fillPasswordMenu.accesskey   "F">
+<!ENTITY fillUsernameMenu.label       "Fill Username">
+<!ENTITY fillUsernameMenu.accesskey   "F">
+<!ENTITY noLoginSuggestions.label     "(No Login Suggestions)">
+<!ENTITY viewSavedLogins.label        "View Saved Logins">
--- a/toolkit/locales/en-US/chrome/passwordmgr/passwordmgr.properties
+++ b/toolkit/locales/en-US/chrome/passwordmgr/passwordmgr.properties
@@ -51,8 +51,15 @@ 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
 loginsSpielAll=Passwords for the following sites are stored on your computer:
 loginsSpielFiltered=The following passwords match your search:
+# LOCALIZATION NOTE (loginHostAge):
+# This is used to show the context menu login items with their age.
+# 1st string is the username for the login, 2nd is the login's age.
+loginHostAge=%1$S (%2$S)
+# LOCALIZATION NOTE (noUsername):
+# String is used on the context menu when a login doesn't have a username.
+noUsername=No username
--- a/toolkit/modules/InlineSpellChecker.jsm
+++ b/toolkit/modules/InlineSpellChecker.jsm
@@ -424,16 +424,19 @@ var SpellCheckHelper = {
 
   // Set when over an element that otherwise would not be considered
   // "editable" but is because content editable is enabled for the document.
   CONTENTEDITABLE: 0x20,
 
   // Set when over an <input type="number"> or other non-text field.
   NUMERIC: 0x40,
 
+  // Set when over an <input type="password"> field.
+  PASSWORD: 0x80,
+
   isTargetAKeywordField(aNode, window) {
     if (!(aNode instanceof window.HTMLInputElement))
       return false;
 
     var form = aNode.form;
     if (!form || aNode.type == "password")
       return false;
 
@@ -474,16 +477,19 @@ var SpellCheckHelper = {
 
         // Allow spellchecking UI on all text and search inputs.
         if (!element.readOnly &&
             (element.type == "text" || element.type == "search")) {
           flags |= this.EDITABLE;
         }
         if (this.isTargetAKeywordField(element, window))
           flags |= this.KEYWORD;
+        if (element.type == "password") {
+          flags |= this.PASSWORD;
+        }
       }
     } else if (element instanceof window.HTMLTextAreaElement) {
       flags |= this.TEXTINPUT | this.TEXTAREA;
       if (!element.readOnly) {
         flags |= this.EDITABLE;
       }
     }