Bug 1188719 - Show login fill context menu on username field. r=MattN a=sylvestre
authorBernardo P. Rittmeyer <bernardo@rittme.com>
Wed, 14 Oct 2015 17:23:30 -0400
changeset 289560 b0aa8d67c934
parent 289559 6398da8b9482
child 289561 645e5fe8f354
push id5188
push usermozilla@noorenberghe.ca
push date2015-10-15 17:59 +0000
treeherdermozilla-beta@645e5fe8f354 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN, sylvestre
bugs1188719
milestone42.0
Bug 1188719 - Show login fill context menu on username field. r=MattN a=sylvestre
browser/base/content/browser-context.inc
browser/base/content/content.js
browser/base/content/nsContextMenu.js
browser/base/content/tabbrowser.xml
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/LoginManagerContextMenu.jsm
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -414,18 +414,24 @@
                 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;"
-            accesskey="&fillPasswordMenu.accesskey;"
+            label="&fillLoginMenu.label;"
+            label-login="&fillLoginMenu.label;"
+            label-password="&fillPasswordMenu.label;"
+            label-username="&fillUsernameMenu.label;"
+            accesskey="&fillLoginMenu.accesskey;"
+            accesskey-login="&fillLoginMenu.accesskey;"
+            accesskey-password="&fillPasswordMenu.accesskey;"
+            accesskey-username="&fillUsernameMenu.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"
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -102,16 +102,17 @@ let handleContentContextMenu = function 
   let docLocation = doc.location.href;
   let charSet = doc.characterSet;
   let baseURI = doc.baseURI;
   let referrer = doc.referrer;
   let referrerPolicy = doc.referrerPolicy;
   let frameOuterWindowID = doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
                                           .getInterface(Ci.nsIDOMWindowUtils)
                                           .outerWindowID;
+  let loginFillInfo = LoginManagerContent.getFieldContext(event.target);
 
   // get referrer attribute from clicked link and parse it
   // if per element referrer is enabled, the element referrer overrules
   // the document wide referrer
   if (Services.prefs.getBoolPref("network.http.enablePerElementReferrer")) {
     let referrerAttrValue = Services.netUtils.parseAttributePolicyString(event.target.
                             getAttribute("referrer"));
     if (referrerAttrValue !== Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT) {
@@ -163,17 +164,18 @@ let handleContentContextMenu = function 
     event.target.ownerDocument.defaultView.updateCommands("contentcontextmenu");
 
     let customMenuItems = PageMenuChild.build(event.target);
     let principal = doc.nodePrincipal;
     sendRpcMessage("contextmenu",
                    { editFlags, spellInfo, customMenuItems, addonInfo,
                      principal, docLocation, charSet, baseURI, referrer,
                      referrerPolicy, contentType, contentDisposition,
-                     frameOuterWindowID, selectionInfo, disableSetDesktopBg },
+                     frameOuterWindowID, selectionInfo, disableSetDesktopBg,
+                     loginFillInfo, },
                    { event, popupNode: event.target });
   }
   else {
     // Break out to the parent window and pass the add-on info along
     let browser = docShell.chromeEventHandler;
     let mainWin = browser.ownerDocument.defaultView;
     mainWin.gContextMenuContentData = {
       isRemote: false,
@@ -185,16 +187,17 @@ let handleContentContextMenu = function 
       docLocation: docLocation,
       charSet: charSet,
       referrer: referrer,
       referrerPolicy: referrerPolicy,
       contentType: contentType,
       contentDisposition: contentDisposition,
       selectionInfo: selectionInfo,
       disableSetDesktopBackground: disableSetDesktopBg,
+      loginFillInfo,
     };
   }
 }
 
 Cc["@mozilla.org/eventlistenerservice;1"]
   .getService(Ci.nsIEventListenerService)
   .addSystemEventListener(global, "contextmenu", handleContentContextMenu, false);
 
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -501,23 +501,45 @@ 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);
+    let loginFillInfo = gContextMenuContentData && gContextMenuContentData.loginFillInfo;
+
+    // If we could not find a password field we
+    // don't want to show the form fill option.
+    let showFill = loginFillInfo && loginFillInfo.passwordField.found;
+
+    // Disable the fill option if the user has set a master password
+    // or if the password field or target field are disabled.
+    let disableFill = !loginFillInfo ||
+                      !Services.logins ||
+                      !Services.logins.isLoggedIn ||
+                      loginFillInfo.passwordField.disabled ||
+                      (!this.onPassword && loginFillInfo.usernameField.disabled);
 
-    if (!showFillPassword || disableFillPassword) {
+    this.showItem("fill-login-separator", showFill);
+    this.showItem("fill-login", showFill);
+    this.setItemAttr("fill-login", "disabled", disableFill);
+
+    // Set the correct label for the fill menu
+    let fillMenu = document.getElementById("fill-login");
+    if (this.onPassword) {
+      fillMenu.setAttribute("label", fillMenu.getAttribute("label-password"));
+      fillMenu.setAttribute("accesskey", fillMenu.getAttribute("accesskey-password"));
+    } else {
+      fillMenu.setAttribute("label", fillMenu.getAttribute("label-login"));
+      fillMenu.setAttribute("accesskey", fillMenu.getAttribute("accesskey-login"));
+    }
+
+    if (!showFill || disableFill) {
       return;
     }
     let documentURI = gContextMenuContentData.documentURIObject;
     let fragment = LoginManagerContextMenu.addLoginsToMenu(this.target, this.browser, documentURI);
 
     this.showItem("fill-login-no-logins", !fragment);
 
     if (!fragment) {
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -4009,16 +4009,17 @@
                                           charSet: aMessage.data.charSet,
                                           referrer: aMessage.data.referrer,
                                           referrerPolicy: aMessage.data.referrerPolicy,
                                           contentType: aMessage.data.contentType,
                                           contentDisposition: aMessage.data.contentDisposition,
                                           frameOuterWindowID: aMessage.data.frameOuterWindowID,
                                           selectionInfo: aMessage.data.selectionInfo,
                                           disableSetDesktopBackground: aMessage.data.disableSetDesktopBg,
+                                          loginFillInfo: aMessage.data.loginFillInfo,
                                         };
               let popup = browser.ownerDocument.getElementById("contentAreaContextMenu");
               let event = gContextMenuContentData.event;
               popup.openPopupAtScreen(event.screenX, event.screenY, true);
               break;
             }
             case "DOMServiceWorkerFocusClient":
             case "DOMWebNotificationClicked": {
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -471,17 +471,19 @@ var LoginManagerContent = {
     let clobberUsername = true;
     let options = {
       inputElement,
     };
 
     // If we have a target input, fills it's form.
     if (inputElement) {
       form = FormLikeFactory.createFromField(inputElement);
-      clobberUsername = false;
+      if (inputElement.type == "password") {
+        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);
 
@@ -808,18 +810,21 @@ var LoginManagerContent = {
   },
 
   /**
    * Attempt to find the username and password fields in a form, and fill them
    * in using the provided logins and recipes.
    *
    * @param {HTMLFormElement} form
    * @param {bool} autofillForm denotes if we should fill the form in automatically
-   * @param {bool} clobberUsername controls if an existing username can be
-   *                               overwritten
+   * @param {bool} clobberUsername controls if an existing username can be overwritten.
+   *                               If this is false and an inputElement of type password
+   *                               is also passed, the username field will be ignored.
+   *                               If this is false and no inputElement is passed, if the username
+   *                               field value is not found in foundLogins, it will not fill the password.
    * @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
@@ -863,21 +868,26 @@ var LoginManagerContent = {
       // 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") {
+        if (inputElement.type == "password") {
+          passwordField = inputElement;
+          if (!clobberUsername) {
+            usernameField = null;
+          }
+        } else if (LoginHelper.isUsernameFieldType(inputElement)) {
+          usernameField = inputElement;
+        } else {
           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;
       }
@@ -1027,16 +1037,63 @@ var LoginManagerContent = {
       let win = doc.defaultView;
       let messageManager = messageManagerFromWindow(win);
       messageManager.sendAsyncMessage("LoginStats:LoginFillSuccessful");
     } finally {
       Services.obs.notifyObservers(form.rootElement, "passwordmgr-processed-form", null);
     }
   },
 
+  /**
+   * Verify if a field is a valid login form field and
+   * returns some information about it's FormLike.
+   *
+   * @param {Element} aField
+   *                  A form field we want to verify.
+   *
+   * @returns {Object} an object with information about the
+   *                   FormLike username and password field
+   *                   or null if the passed field is invalid.
+   */
+  getFieldContext(aField) {
+    // If the element is not a proper form field, return null.
+    if (!(aField instanceof Ci.nsIDOMHTMLInputElement) ||
+        (aField.type != "password" && !LoginHelper.isUsernameFieldType(aField)) ||
+        !aField.ownerDocument) {
+      return null;
+    }
+    let form = FormLikeFactory.createFromField(aField);
+
+    let doc = aField.ownerDocument;
+    let messageManager = messageManagerFromWindow(doc.defaultView);
+    let recipes = messageManager.sendSyncMessage("RemoteLogins:findRecipes", {
+      formOrigin: LoginUtils._getPasswordOrigin(doc.documentURI),
+    })[0];
+
+    let [usernameField, newPasswordField, oldPasswordField] =
+          this._getFormFields(form, false, recipes);
+
+    // If we are not verifying a password field, we want
+    // to use aField as the username field.
+    if (aField.type != "password") {
+      usernameField = aField;
+    }
+
+    return {
+      usernameField: {
+        found: !!usernameField,
+        disabled: usernameField && (usernameField.disabled || usernameField.readOnly),
+      },
+      passwordField: {
+        found: !!newPasswordField,
+        disabled: newPasswordField && (newPasswordField.disabled || newPasswordField.readOnly),
+      },
+    };
+  },
+
 };
 
 var LoginUtils = {
   /*
    * _getPasswordOrigin
    *
    * Get the parts of the URL we want for identification.
    */
--- a/toolkit/components/passwordmgr/LoginManagerContextMenu.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContextMenu.jsm
@@ -55,17 +55,17 @@ let LoginManagerContextMenu = {
           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);
+          this._fillTargetField(login, inputElement, browser, documentURI);
         }.bind(this, login));
 
         fragment.appendChild(item);
     }
 
     return fragment;
   },
 
@@ -146,17 +146,17 @@ let LoginManagerContextMenu = {
    *        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) {
+  _fillTargetField(login, inputElement, browser, documentURI) {
     LoginManagerParent.fillForm({
       browser: browser,
       loginFormOrigin: documentURI.prePath,
       login: login,
       inputElement: inputElement,
     }).catch(Cu.reportError);
   },