Bug 1548381 - Password Generation Autocomplete Result. r=sfoster
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Tue, 21 May 2019 00:24:24 +0000
changeset 474683 1433d315b1b74f64a560349c1a06a863b4083566
parent 474682 c5b9ba215f498e7e9b00e455df27c426dc1da4a0
child 474684 e92a0032aa2af1bdf34a30dc72860ad8a3b2108a
push id85933
push usermozilla@noorenberghe.ca
push dateTue, 21 May 2019 05:19:43 +0000
treeherderautoland@b74e5737da64 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssfoster
bugs1548381
milestone69.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 1548381 - Password Generation Autocomplete Result. r=sfoster Differential Revision: https://phabricator.services.mozilla.com/D31209
toolkit/components/passwordmgr/LoginAutoCompleteResult.jsm
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/test/unit/test_login_autocomplete_result.js
toolkit/locales/en-US/chrome/passwordmgr/passwordmgr.properties
--- a/toolkit/components/passwordmgr/LoginAutoCompleteResult.jsm
+++ b/toolkit/components/passwordmgr/LoginAutoCompleteResult.jsm
@@ -134,30 +134,42 @@ class LoginAutocompleteItem extends Auto
       this._messageManager.sendAsyncMessage("PasswordManager:removeLogin",
                                             { login: vanilla });
     } else {
       Services.logins.removeLogin(this._login);
     }
   }
 }
 
+class GeneratedPasswordAutocompleteItem extends AutocompleteItem {
+  constructor(generatedPassword) {
+    super("generatedPassword");
+    this.value = generatedPassword;
+
+    XPCOMUtils.defineLazyGetter(this, "label", () => {
+      return getLocalizedString("useGeneratedPassword");
+    });
+  }
+}
+
 class LoginsFooterAutocompleteItem extends AutocompleteItem {
   constructor(hostname) {
     super("loginsFooter");
     this.comment = hostname;
 
     XPCOMUtils.defineLazyGetter(this, "label", () => {
       return getLocalizedString("viewSavedLogins.label");
     });
   }
 }
 
 
 // nsIAutoCompleteResult implementation
 function LoginAutoCompleteResult(aSearchString, matchingLogins, {
+  generatedPassword,
   isSecure,
   messageManager,
   isPasswordField,
   hostname,
 }) {
   let hidingFooterOnPWFieldAutoOpened = false;
   function isFooterEnabled() {
     // We need to check LoginHelper.enabled here since the insecure warning should
@@ -168,17 +180,18 @@ function LoginAutoCompleteResult(aSearch
 
     // Don't show the footer on non-empty password fields as it's not providing
     // value and only adding noise since a password was already filled.
     if (isPasswordField && aSearchString) {
       log.debug("Hiding footer: non-empty password field");
       return false;
     }
 
-    if (!matchingLogins.length && isPasswordField && formFillController.passwordPopupAutomaticallyOpened) {
+    if (!matchingLogins.length && !generatedPassword && isPasswordField
+        && formFillController.passwordPopupAutomaticallyOpened) {
       hidingFooterOnPWFieldAutoOpened = true;
       log.debug("Hiding footer: no logins and the popup was opened upon focus of the pw. field");
       return false;
     }
 
     return true;
   }
 
@@ -200,16 +213,19 @@ function LoginAutoCompleteResult(aSearch
   for (let login of logins) {
     let item = new LoginAutocompleteItem(login, isPasswordField, dateAndTimeFormatter,
                                          duplicateUsernames, messageManager);
     this._rows.push(item);
   }
 
   // The footer comes last if it's enabled
   if (isFooterEnabled()) {
+    if (generatedPassword) {
+      this._rows.push(new GeneratedPasswordAutocompleteItem(generatedPassword));
+    }
     this._rows.push(new LoginsFooterAutocompleteItem(hostname));
   }
 
   // Determine the result code and default index.
   if (this.matchCount > 0) {
     this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
     this.defaultIndex = 0;
   } else if (hidingFooterOnPWFieldAutoOpened) {
@@ -337,25 +353,30 @@ LoginAutoComplete.prototype = {
     //   able to affect the identity icon in the address bar by adding a password field.
     if (isSecure) {
       let form = LoginFormFactory.createFromField(aElement);
       isSecure = InsecurePasswordUtils.isFormSecure(form);
     }
     let isPasswordField = aElement.type == "password";
     let hostname = aElement.ownerDocument.documentURIObject.host;
 
-    let completeSearch = (autoCompleteLookupPromise, { logins, messageManager }) => {
+    let completeSearch = (autoCompleteLookupPromise, {
+      generatedPassword,
+      logins,
+      messageManager,
+    }) => {
       // If the search was canceled before we got our
       // results, don't bother reporting them.
       if (this._autoCompleteLookupPromise !== autoCompleteLookupPromise) {
         return;
       }
 
       this._autoCompleteLookupPromise = null;
       let results = new LoginAutoCompleteResult(aSearchString, logins, {
+        generatedPassword,
         messageManager,
         isSecure,
         isPasswordField,
         hostname,
       });
       aCallback.onSearchCompletion(results);
     };
 
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -288,17 +288,21 @@ var LoginManagerContent = {
         });
         break;
       }
 
       case "PasswordManager:loginsAutoCompleted": {
         let loginsFound = LoginHelper.vanillaObjectsToLogins(msg.data.logins);
         let messageManager = msg.target;
         let request = this._takeRequest(msg);
-        request.promise.resolve({ logins: loginsFound, messageManager });
+        request.promise.resolve({
+          generatedPassword: msg.data.generatedPassword,
+          logins: loginsFound,
+          messageManager,
+        });
         break;
       }
 
       case "FormAutoComplete:PopupOpened": {
         let {chromeEventHandler} = msg.target.docShell;
         chromeEventHandler.addEventListener("keydown", this._onKeyDown,
                                             true);
         break;
--- a/toolkit/components/passwordmgr/test/unit/test_login_autocomplete_result.js
+++ b/toolkit/components/passwordmgr/test/unit/test_login_autocomplete_result.js
@@ -1,14 +1,13 @@
 const {LoginAutoCompleteResult} = ChromeUtils.import("resource://gre/modules/LoginAutoCompleteResult.jsm");
 var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
                                          Ci.nsILoginInfo, "init");
 
 const PREF_INSECURE_FIELD_WARNING_ENABLED = "security.insecure_field_warning.contextual.enabled";
-const PREF_INSECURE_AUTOFILLFORMS_ENABLED = "signon.autofillForms.http";
 
 let matchingLogins = [];
 matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
                                     "", "emptypass1", "uname", "pword"));
 
 matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
                                     "tempuser1", "temppass1", "uname", "pword"));
 
@@ -25,17 +24,16 @@ let meta = matchingLogins[0].QueryInterf
 let dateAndTimeFormatter = new Services.intl.DateTimeFormat(undefined,
                                                             { dateStyle: "medium" });
 let time = dateAndTimeFormatter.format(new Date(meta.timePasswordChanged));
 const LABEL_NO_USERNAME = "No username (" + time + ")";
 
 let expectedResults = [
   {
     insecureFieldWarningEnabled: true,
-    insecureAutoFillFormsEnabled: true,
     isSecure: true,
     isPasswordField: false,
     matchingLogins,
     items: [{
       value: "",
       label: LABEL_NO_USERNAME,
       style: "loginWithOrigin",
     }, {
@@ -57,33 +55,31 @@ let expectedResults = [
     }, {
       value: "",
       label: "View Saved Logins",
       style: "loginsFooter",
     }],
   },
   {
     insecureFieldWarningEnabled: true,
-    insecureAutoFillFormsEnabled: true,
     isSecure: false,
     isPasswordField: false,
     matchingLogins: [],
     items: [{
       value: "",
       label: "This connection is not secure. Logins entered here could be compromised. Learn More",
       style: "insecureWarning",
     }, {
       value: "",
       label: "View Saved Logins",
       style: "loginsFooter",
     }],
   },
   {
     insecureFieldWarningEnabled: true,
-    insecureAutoFillFormsEnabled: true,
     isSecure: false,
     isPasswordField: false,
     matchingLogins,
     items: [{
       value: "",
       label: "This connection is not secure. Logins entered here could be compromised. Learn More",
       style: "insecureWarning",
     }, {
@@ -109,17 +105,16 @@ let expectedResults = [
     }, {
       value: "",
       label: "View Saved Logins",
       style: "loginsFooter",
     }],
   },
   {
     insecureFieldWarningEnabled: true,
-    insecureAutoFillFormsEnabled: true,
     isSecure: true,
     isPasswordField: true,
     matchingLogins,
     items: [{
       value: "emptypass1",
       label: LABEL_NO_USERNAME,
       style: "loginWithOrigin",
     }, {
@@ -141,17 +136,16 @@ let expectedResults = [
     }, {
       value: "",
       label: "View Saved Logins",
       style: "loginsFooter",
     }],
   },
   {
     insecureFieldWarningEnabled: true,
-    insecureAutoFillFormsEnabled: true,
     isSecure: false,
     isPasswordField: true,
     matchingLogins,
     items: [{
       value: "",
       label: "This connection is not secure. Logins entered here could be compromised. Learn More",
       style: "insecureWarning",
     }, {
@@ -177,17 +171,16 @@ let expectedResults = [
     }, {
       value: "",
       label: "View Saved Logins",
       style: "loginsFooter",
     }],
   },
   {
     insecureFieldWarningEnabled: false,
-    insecureAutoFillFormsEnabled: true,
     isSecure: true,
     isPasswordField: false,
     matchingLogins,
     items: [{
       value: "",
       label: LABEL_NO_USERNAME,
       style: "loginWithOrigin",
     }, {
@@ -209,17 +202,16 @@ let expectedResults = [
     }, {
       value: "",
       label: "View Saved Logins",
       style: "loginsFooter",
     }],
   },
   {
     insecureFieldWarningEnabled: false,
-    insecureAutoFillFormsEnabled: true,
     isSecure: false,
     isPasswordField: false,
     matchingLogins,
     items: [{
       value: "",
       label: LABEL_NO_USERNAME,
       style: "loginWithOrigin",
     }, {
@@ -241,17 +233,16 @@ let expectedResults = [
     }, {
       value: "",
       label: "View Saved Logins",
       style: "loginsFooter",
     }],
   },
   {
     insecureFieldWarningEnabled: false,
-    insecureAutoFillFormsEnabled: true,
     isSecure: true,
     isPasswordField: true,
     matchingLogins,
     items: [{
       value: "emptypass1",
       label: LABEL_NO_USERNAME,
       style: "loginWithOrigin",
     }, {
@@ -273,17 +264,16 @@ let expectedResults = [
     }, {
       value: "",
       label: "View Saved Logins",
       style: "loginsFooter",
     }],
   },
   {
     insecureFieldWarningEnabled: false,
-    insecureAutoFillFormsEnabled: true,
     isSecure: false,
     isPasswordField: true,
     matchingLogins,
     items: [{
       value: "emptypass1",
       label: LABEL_NO_USERNAME,
       style: "loginWithOrigin",
     }, {
@@ -305,17 +295,16 @@ let expectedResults = [
     }, {
       value: "",
       label: "View Saved Logins",
       style: "loginsFooter",
     }],
   },
   {
     insecureFieldWarningEnabled: true,
-    insecureAutoFillFormsEnabled: false,
     isSecure: true,
     isPasswordField: false,
     matchingLogins,
     items: [{
       value: "",
       label: LABEL_NO_USERNAME,
       style: "loginWithOrigin",
     }, {
@@ -337,17 +326,16 @@ let expectedResults = [
     }, {
       value: "",
       label: "View Saved Logins",
       style: "loginsFooter",
     }],
   },
   {
     insecureFieldWarningEnabled: true,
-    insecureAutoFillFormsEnabled: false,
     isSecure: false,
     isPasswordField: false,
     matchingLogins,
     items: [{
       value: "",
       label: "This connection is not secure. Logins entered here could be compromised. Learn More",
       style: "insecureWarning",
     }, {
@@ -373,17 +361,16 @@ let expectedResults = [
     }, {
       value: "",
       label: "View Saved Logins",
       style: "loginsFooter",
     }],
   },
   {
     insecureFieldWarningEnabled: true,
-    insecureAutoFillFormsEnabled: false,
     isSecure: true,
     isPasswordField: true,
     matchingLogins,
     items: [{
       value: "emptypass1",
       label: LABEL_NO_USERNAME,
       style: "loginWithOrigin",
     }, {
@@ -405,17 +392,16 @@ let expectedResults = [
     }, {
       value: "",
       label: "View Saved Logins",
       style: "loginsFooter",
     }],
   },
   {
     insecureFieldWarningEnabled: true,
-    insecureAutoFillFormsEnabled: false,
     isSecure: false,
     isPasswordField: true,
     matchingLogins,
     items: [{
       value: "",
       label: "This connection is not secure. Logins entered here could be compromised. Learn More",
       style: "insecureWarning",
     }, {
@@ -441,17 +427,16 @@ let expectedResults = [
     }, {
       value: "",
       label: "View Saved Logins",
       style: "loginsFooter",
     }],
   },
   {
     insecureFieldWarningEnabled: false,
-    insecureAutoFillFormsEnabled: false,
     isSecure: true,
     isPasswordField: false,
     matchingLogins,
     items: [{
       value: "",
       label: LABEL_NO_USERNAME,
       style: "loginWithOrigin",
     }, {
@@ -473,29 +458,39 @@ let expectedResults = [
     }, {
       value: "",
       label: "View Saved Logins",
       style: "loginsFooter",
     }],
   },
   {
     insecureFieldWarningEnabled: false,
-    insecureAutoFillFormsEnabled: false,
     isSecure: false,
     isPasswordField: false,
     matchingLogins: [],
     items: [{
       value: "",
       label: "View Saved Logins",
       style: "loginsFooter",
     }],
   },
   {
     insecureFieldWarningEnabled: false,
-    insecureAutoFillFormsEnabled: false,
+    isSecure: false,
+    isPasswordField: false,
+    matchingLogins: [],
+    searchString: "foo",
+    items: [{
+      value: "",
+      label: "View Saved Logins",
+      style: "loginsFooter",
+    }],
+  },
+  {
+    insecureFieldWarningEnabled: false,
     isSecure: false,
     isPasswordField: false,
     matchingLogins,
     items: [{
       value: "",
       label: LABEL_NO_USERNAME,
       style: "loginWithOrigin",
     }, {
@@ -517,17 +512,16 @@ let expectedResults = [
     }, {
       value: "",
       label: "View Saved Logins",
       style: "loginsFooter",
     }],
   },
   {
     insecureFieldWarningEnabled: false,
-    insecureAutoFillFormsEnabled: false,
     isSecure: true,
     isPasswordField: true,
     matchingLogins,
     items: [{
       value: "emptypass1",
       label: LABEL_NO_USERNAME,
       style: "loginWithOrigin",
     }, {
@@ -549,17 +543,16 @@ let expectedResults = [
     }, {
       value: "",
       label: "View Saved Logins",
       style: "loginsFooter",
     }],
   },
   {
     insecureFieldWarningEnabled: false,
-    insecureAutoFillFormsEnabled: false,
     isSecure: false,
     isPasswordField: true,
     matchingLogins,
     items: [{
       value: "emptypass1",
       label: LABEL_NO_USERNAME,
       style: "loginWithOrigin",
     }, {
@@ -579,30 +572,73 @@ let expectedResults = [
       label: "zzzuser4",
       style: "loginWithOrigin",
     }, {
       value: "",
       label: "View Saved Logins",
       style: "loginsFooter",
     }],
   },
+  {
+    insecureFieldWarningEnabled: true,
+    isSecure: true,
+    isPasswordField: true,
+    matchingLogins: [],
+    items: [{
+      value: "",
+      label: "View Saved Logins",
+      style: "loginsFooter",
+    }],
+  },
+  {
+    insecureFieldWarningEnabled: true,
+    isSecure: true,
+    isPasswordField: true,
+    matchingLogins: [],
+    searchString: "foo",
+    items: [],
+  },
+  {
+    generatedPassword: "9ljgfd4shyktb45",
+    insecureFieldWarningEnabled: true,
+    isSecure: true,
+    isPasswordField: true,
+    matchingLogins: [],
+    items: [{
+      value: "9ljgfd4shyktb45",
+      label: "Use Generated Password",
+      style: "generatedPassword",
+    }, {
+      value: "",
+      label: "View Saved Logins",
+      style: "loginsFooter",
+    }],
+  },
+  {
+    generatedPassword: "9ljgfd4shyktb45",
+    insecureFieldWarningEnabled: true,
+    isSecure: true,
+    isPasswordField: true,
+    matchingLogins: [],
+    searchString: "9ljgfd4shyktb45",
+    items: [],
+  },
 ];
 
 add_task(async function test_all_patterns() {
   LoginHelper.createLogger("LoginAutoCompleteResult");
   Services.prefs.setBoolPref("signon.showAutoCompleteFooter", true);
   Services.prefs.setBoolPref("signon.showAutoCompleteOrigins", true);
 
   expectedResults.forEach(pattern => {
     info(JSON.stringify(pattern, null, 2));
     Services.prefs.setBoolPref(PREF_INSECURE_FIELD_WARNING_ENABLED,
                                pattern.insecureFieldWarningEnabled);
-    Services.prefs.setBoolPref(PREF_INSECURE_AUTOFILLFORMS_ENABLED,
-                               pattern.insecureAutoFillFormsEnabled);
-    let actual = new LoginAutoCompleteResult("", pattern.matchingLogins, {
+    let actual = new LoginAutoCompleteResult(pattern.searchString || "", pattern.matchingLogins, {
+      generatedPassword: pattern.generatedPassword,
       isSecure: pattern.isSecure,
       isPasswordField: pattern.isPasswordField,
     });
     equal(actual.matchCount, pattern.items.length, "Check matching row count");
     pattern.items.forEach((item, index) => {
       equal(actual.getValueAt(index), item.value, `Value ${index}`);
       equal(actual.getLabelAt(index), item.label, `Label ${index}`);
       equal(actual.getStyleAt(index), item.style, `Style ${index}`);
--- a/toolkit/locales/en-US/chrome/passwordmgr/passwordmgr.properties
+++ b/toolkit/locales/en-US/chrome/passwordmgr/passwordmgr.properties
@@ -40,23 +40,28 @@ passwordChangeTitle = Confirm Password C
 # LOCALIZATION NOTE (updatePasswordMsg):
 # 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?
 userSelectText2 = Select which login to update:
 removeLoginPrompt=Are you sure you wish to remove this login?
 removeLoginTitle=Remove login
 loginsDescriptionAll2=Logins for the following sites are stored on your computer
+
+# LOCALIZATION NOTE (useGeneratedPassword):
+# Shown in the autocomplete popup to allow filling a generated password into a password field.
+useGeneratedPassword=Use Generated Password
 # 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
+
 duplicateLoginTitle=Login already exists
 duplicateLogin=A duplicate login already exists.
 
 # LOCALIZATION NOTE (insecureFieldWarningDescription2, insecureFieldWarningDescription3):
 # %1$S will contain insecureFieldWarningLearnMore and look like a link to indicate that clicking will open a tab with support information.
 insecureFieldWarningDescription2 = This connection is not secure. Logins entered here could be compromised. %1$S
 insecureFieldWarningDescription3 = Logins entered here could be compromised. %1$S
 insecureFieldWarningLearnMore = Learn More