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 256859 02ff02df924ad184ec843ac0d9677cb38a95cba8
parent 256858 eb46ecd87491d1ce1cd648f8be0cd5d3063fff16
child 256860 96f128a9b4656aee393a3d022bb9d40a2a8c0384
push id29191
push userkwierso@gmail.com
push dateSat, 08 Aug 2015 00:10:29 +0000
treeherdermozilla-central@a5bde89f6829 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs433238
milestone42.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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;
       }
     }